Skip to main content

uptrakit_surfaces/
protocol.rs

1use serde::{Deserialize, Serialize};
2use serde_json::{Map, Value};
3use std::collections::HashSet;
4use thiserror::Error;
5use uptrakit_shared_macros::wire_safe_enum;
6use uuid::Uuid;
7
8use crate::{
9    Capability, CapabilitySet, DataSourceDescriptor, DataSourceKind, FrameworkGeneration,
10    FrameworkGenerationRange, InteractionDescriptor, InteractionId, InteractionKind,
11    InteractionTransport, ProviderKind, Scope, SlotValidationError, SurfaceDescriptor, SurfaceId,
12    SurfaceNode, SurfaceSlotDef, Targeting, validate_slot_id, validate_surface_identifier,
13};
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub struct SurfaceRegistration {
17    pub provider: ProviderIdentity,
18    pub framework_generation: FrameworkGeneration,
19    pub capabilities: CapabilitySet,
20    pub effective_tenant_binding: EffectiveTenantBinding,
21    #[serde(default, skip_serializing_if = "Vec::is_empty")]
22    pub surfaces: Vec<RegisteredSurface>,
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    pub encryption_metadata: Option<ProviderEncryptionMetadata>,
25}
26
27impl SurfaceRegistration {
28    /// Validates a registration payload against policy and contract rules.
29    ///
30    /// # Errors
31    /// Returns [`SurfaceRegistrationError`] when any validation check fails,
32    /// including unsupported framework generation, missing required
33    /// capabilities, invalid or duplicate identifiers, slot violations,
34    /// provider-kind mismatches, invalid interaction/data-source declarations,
35    /// or broken cross-reference links in the surface graph.
36    pub fn validate_against(
37        &self,
38        policy: &SurfaceRegistrationPolicy,
39    ) -> Result<(), SurfaceRegistrationError> {
40        validate_supported_generation(self, policy)?;
41        validate_required_registration_capabilities(self, policy)?;
42        validate_registered_surfaces(self)?;
43        Ok(())
44    }
45}
46
47fn validate_supported_generation(
48    registration: &SurfaceRegistration,
49    policy: &SurfaceRegistrationPolicy,
50) -> Result<(), SurfaceRegistrationError> {
51    if policy
52        .supported_generation
53        .includes(registration.framework_generation)
54    {
55        return Ok(());
56    }
57
58    Err(registration_error(
59        SurfaceRegistrationErrorCode::UnsupportedGeneration,
60        format!(
61            "framework generation {}.{} is outside supported range {}.{}..={}.{}",
62            registration.framework_generation.major,
63            registration.framework_generation.minor,
64            policy.supported_generation.min.major,
65            policy.supported_generation.min.minor,
66            policy.supported_generation.max.major,
67            policy.supported_generation.max.minor,
68        ),
69    ))
70}
71
72fn validate_required_registration_capabilities(
73    registration: &SurfaceRegistration,
74    policy: &SurfaceRegistrationPolicy,
75) -> Result<(), SurfaceRegistrationError> {
76    if registration
77        .capabilities
78        .contains_all(&policy.required_capabilities)
79    {
80        return Ok(());
81    }
82
83    Err(missing_capability(
84        "registration is missing one or more required capabilities",
85    ))
86}
87
88fn validate_registered_surfaces(
89    registration: &SurfaceRegistration,
90) -> Result<(), SurfaceRegistrationError> {
91    RegisteredSurfacesValidator::new(registration).validate()
92}
93
94struct RegisteredSurfacesValidator<'a> {
95    provider_kind: ProviderKind,
96    capabilities: &'a CapabilitySet,
97    surfaces: &'a [RegisteredSurface],
98    surface_ids: HashSet<&'a str>,
99    single_entry_slots: HashSet<&'static str>,
100}
101
102impl<'a> RegisteredSurfacesValidator<'a> {
103    fn new(registration: &'a SurfaceRegistration) -> Self {
104        Self {
105            provider_kind: registration.provider.provider_kind,
106            capabilities: &registration.capabilities,
107            surfaces: &registration.surfaces,
108            surface_ids: HashSet::new(),
109            single_entry_slots: HashSet::new(),
110        }
111    }
112
113    fn validate(mut self) -> Result<(), SurfaceRegistrationError> {
114        for surface in self.surfaces {
115            self.validate_surface(surface)?;
116        }
117
118        Ok(())
119    }
120
121    fn validate_surface(
122        &mut self,
123        surface: &'a RegisteredSurface,
124    ) -> Result<(), SurfaceRegistrationError> {
125        validate_surface_descriptor_rules(
126            surface,
127            self.provider_kind,
128            self.capabilities,
129            &mut self.surface_ids,
130            &mut self.single_entry_slots,
131        )?;
132
133        let interaction_ids = validate_surface_interaction_rules(surface)?;
134        validate_workflow_step_references(
135            &surface.descriptor.surface_id,
136            &surface.interactions,
137            &interaction_ids,
138        )?;
139
140        let data_source_ids = validate_surface_data_source_rules(surface)?;
141        validate_root_node_references(
142            &surface.descriptor.surface_id,
143            &surface.descriptor.root_node,
144            &interaction_ids,
145            &data_source_ids,
146        )
147    }
148}
149
150fn validate_surface_descriptor_rules<'a>(
151    surface: &'a RegisteredSurface,
152    registration_provider_kind: ProviderKind,
153    capabilities: &CapabilitySet,
154    surface_ids: &mut HashSet<&'a str>,
155    single_entry_slots: &mut HashSet<&'static str>,
156) -> Result<(), SurfaceRegistrationError> {
157    validate_unique_surface_id(surface, surface_ids)?;
158    validate_surface_provider_kind(surface, registration_provider_kind)?;
159    let slot_def = validate_surface_slot(surface)?;
160    validate_single_entry_slot_occupancy(slot_def, single_entry_slots)?;
161    validate_surface_priority_range(surface, slot_def)?;
162    validate_surface_required_capabilities(surface, capabilities)?;
163    validate_surface_usage_capabilities(
164        &surface.descriptor.surface_id,
165        capabilities,
166        &surface.descriptor.root_node,
167        &surface.descriptor.targeting,
168        &surface.interactions,
169        &surface.data_sources,
170    )
171}
172
173fn validate_unique_surface_id<'a>(
174    surface: &'a RegisteredSurface,
175    surface_ids: &mut HashSet<&'a str>,
176) -> Result<(), SurfaceRegistrationError> {
177    if surface_ids.insert(surface.descriptor.surface_id.as_str()) {
178        return Ok(());
179    }
180
181    Err(invalid_contract(format!(
182        "duplicate surface_id `{}` within registration batch",
183        surface.descriptor.surface_id
184    )))
185}
186
187fn validate_surface_provider_kind(
188    surface: &RegisteredSurface,
189    registration_provider_kind: ProviderKind,
190) -> Result<(), SurfaceRegistrationError> {
191    if surface.descriptor.provider_kind == registration_provider_kind {
192        return Ok(());
193    }
194
195    Err(invalid_contract(format!(
196        "surface `{}` provider_kind does not match registration provider_kind",
197        surface.descriptor.surface_id
198    )))
199}
200
201fn validate_surface_slot(
202    surface: &RegisteredSurface,
203) -> Result<&'static SurfaceSlotDef, SurfaceRegistrationError> {
204    validate_slot_id(&surface.descriptor.slot).map_err(map_slot_validation_error)
205}
206
207fn validate_single_entry_slot_occupancy(
208    slot_def: &'static SurfaceSlotDef,
209    single_entry_slots: &mut HashSet<&'static str>,
210) -> Result<(), SurfaceRegistrationError> {
211    if slot_def.multi_entry || single_entry_slots.insert(slot_def.id) {
212        return Ok(());
213    }
214
215    Err(invalid_contract(format!(
216        "slot `{}` is single-entry and cannot accept multiple surfaces in one registration batch",
217        slot_def.id
218    )))
219}
220
221fn validate_surface_priority_range(
222    surface: &RegisteredSurface,
223    slot_def: &'static SurfaceSlotDef,
224) -> Result<(), SurfaceRegistrationError> {
225    if surface.descriptor.provider_kind == ProviderKind::BuiltIn {
226        return Ok(());
227    }
228
229    if surface.descriptor.priority >= slot_def.provider_priority_min
230        && surface.descriptor.priority <= slot_def.provider_priority_max
231    {
232        return Ok(());
233    }
234
235    Err(invalid_contract(format!(
236        "surface `{}` priority {} is outside slot `{}` provider range {}..={}",
237        surface.descriptor.surface_id,
238        surface.descriptor.priority,
239        slot_def.id,
240        slot_def.provider_priority_min,
241        slot_def.provider_priority_max
242    )))
243}
244
245fn validate_surface_required_capabilities(
246    surface: &RegisteredSurface,
247    capabilities: &CapabilitySet,
248) -> Result<(), SurfaceRegistrationError> {
249    if capabilities.contains_all(&surface.descriptor.required_capabilities) {
250        return Ok(());
251    }
252
253    Err(missing_capability(format!(
254        "surface `{}` requires capabilities not advertised by registration",
255        surface.descriptor.surface_id
256    )))
257}
258
259fn validate_surface_interaction_rules(
260    surface: &RegisteredSurface,
261) -> Result<HashSet<&str>, SurfaceRegistrationError> {
262    let mut interaction_ids: HashSet<&str> = HashSet::new();
263    for interaction in &surface.interactions {
264        validate_unique_interaction_id(surface, interaction, &mut interaction_ids)?;
265        validate_interaction_provider_rules(surface, interaction)?;
266    }
267
268    Ok(interaction_ids)
269}
270
271fn validate_unique_interaction_id<'a>(
272    surface: &'a RegisteredSurface,
273    interaction: &'a InteractionDescriptor,
274    interaction_ids: &mut HashSet<&'a str>,
275) -> Result<(), SurfaceRegistrationError> {
276    if interaction_ids.insert(interaction.interaction_id.as_str()) {
277        return Ok(());
278    }
279
280    Err(invalid_contract(format!(
281        "duplicate interaction_id `{}` within surface `{}`",
282        interaction.interaction_id, surface.descriptor.surface_id
283    )))
284}
285
286fn validate_interaction_provider_rules(
287    surface: &RegisteredSurface,
288    interaction: &InteractionDescriptor,
289) -> Result<(), SurfaceRegistrationError> {
290    interaction
291        .validate_for_provider(surface.descriptor.provider_kind)
292        .map_err(|err| invalid_contract(err.to_string()))
293}
294
295fn validate_surface_data_source_rules(
296    surface: &RegisteredSurface,
297) -> Result<HashSet<&str>, SurfaceRegistrationError> {
298    let mut data_source_ids: HashSet<&str> = HashSet::new();
299    for data_source in &surface.data_sources {
300        validate_unique_data_source_id(surface, data_source, &mut data_source_ids)?;
301        validate_data_source_provider_rules(surface, data_source)?;
302    }
303
304    Ok(data_source_ids)
305}
306
307fn validate_unique_data_source_id<'a>(
308    surface: &'a RegisteredSurface,
309    data_source: &'a DataSourceDescriptor,
310    data_source_ids: &mut HashSet<&'a str>,
311) -> Result<(), SurfaceRegistrationError> {
312    if data_source_ids.insert(data_source.data_source_id.as_str()) {
313        return Ok(());
314    }
315
316    Err(invalid_contract(format!(
317        "duplicate data_source_id `{}` within surface `{}`",
318        data_source.data_source_id, surface.descriptor.surface_id
319    )))
320}
321
322fn validate_data_source_provider_rules(
323    surface: &RegisteredSurface,
324    data_source: &DataSourceDescriptor,
325) -> Result<(), SurfaceRegistrationError> {
326    data_source
327        .validate_for_provider(surface.descriptor.provider_kind)
328        .map_err(|err| invalid_contract(err.to_string()))
329}
330
331fn map_slot_validation_error(err: SlotValidationError) -> SurfaceRegistrationError {
332    let code = match err {
333        SlotValidationError::UnknownSlot(_) => SurfaceRegistrationErrorCode::InvalidSlot,
334        SlotValidationError::InvalidIdentifier(_) => SurfaceRegistrationErrorCode::InvalidContract,
335    };
336
337    registration_error(code, err.to_string())
338}
339
340fn registration_error(
341    code: SurfaceRegistrationErrorCode,
342    message: impl Into<String>,
343) -> SurfaceRegistrationError {
344    SurfaceRegistrationError::new(code, message.into())
345}
346
347fn invalid_contract(message: impl Into<String>) -> SurfaceRegistrationError {
348    registration_error(SurfaceRegistrationErrorCode::InvalidContract, message)
349}
350
351fn missing_capability(message: impl Into<String>) -> SurfaceRegistrationError {
352    registration_error(SurfaceRegistrationErrorCode::MissingCapability, message)
353}
354
355fn validate_surface_usage_capabilities(
356    surface_id: &SurfaceId,
357    capabilities: &CapabilitySet,
358    root_node: &SurfaceNode,
359    targeting: &Targeting,
360    interactions: &[InteractionDescriptor],
361    data_sources: &[DataSourceDescriptor],
362) -> Result<(), SurfaceRegistrationError> {
363    validate_node_capabilities(surface_id, capabilities, root_node)?;
364
365    let targeting_capability = match targeting {
366        Targeting::Universal => Capability::UniversalTargeting,
367        Targeting::Targeted => Capability::TargetedTargeting,
368    };
369    require_capability(
370        capabilities,
371        targeting_capability,
372        surface_id,
373        "targeting mode",
374    )?;
375
376    for interaction in interactions {
377        let kind_capability = match interaction.kind {
378            InteractionKind::MutationAction => Capability::MutationAction,
379            InteractionKind::FormSubmit => Capability::FormSubmit,
380            InteractionKind::Workflow => Capability::Workflow,
381            InteractionKind::Navigate => Capability::Navigate,
382            InteractionKind::DataLoad => Capability::DataLoad,
383            InteractionKind::ConfirmableAction => Capability::ConfirmableAction,
384        };
385        require_capability(
386            capabilities,
387            kind_capability,
388            surface_id,
389            "interaction kind",
390        )?;
391
392        if matches!(
393            &interaction.transport,
394            InteractionTransport::ProviderProxied
395        ) {
396            require_capability(
397                capabilities,
398                Capability::ProviderInitiatedActions,
399                surface_id,
400                "interaction transport",
401            )?;
402        }
403
404        if !interaction.sensitive_fields.is_empty() {
405            require_capability(
406                capabilities,
407                Capability::SensitiveFields,
408                surface_id,
409                "sensitive fields",
410            )?;
411        }
412    }
413
414    for data_source in data_sources {
415        let kind_capability = match &data_source.kind {
416            DataSourceKind::Static { .. } => Capability::StaticDataSource,
417            DataSourceKind::ControllerQuery { .. } => Capability::ControllerQueryDataSource,
418            DataSourceKind::ProviderQuery { .. } => Capability::ProviderQueryDataSource,
419        };
420        require_capability(
421            capabilities,
422            kind_capability,
423            surface_id,
424            "data source kind",
425        )?;
426    }
427
428    Ok(())
429}
430
431fn validate_node_capabilities(
432    surface_id: &SurfaceId,
433    capabilities: &CapabilitySet,
434    node: &SurfaceNode,
435) -> Result<(), SurfaceRegistrationError> {
436    let node_capability = match node {
437        SurfaceNode::Section { children, .. } => {
438            for child in children {
439                validate_node_capabilities(surface_id, capabilities, child)?;
440            }
441            Capability::SectionNode
442        }
443        SurfaceNode::TextBlock { .. } => Capability::TextBlockNode,
444        SurfaceNode::KeyValue { .. } => Capability::KeyValueNode,
445        SurfaceNode::Table { .. } => Capability::TableNode,
446        SurfaceNode::Form { .. } => Capability::FormNode,
447        SurfaceNode::ActionBar { .. } => Capability::ActionBarNode,
448        SurfaceNode::Tabs { tabs } => {
449            for tab in tabs {
450                validate_node_capabilities(surface_id, capabilities, &tab.root)?;
451            }
452            Capability::TabsNode
453        }
454        SurfaceNode::Callout { .. } => Capability::CalloutNode,
455        SurfaceNode::EmptyState { .. } => Capability::EmptyStateNode,
456        SurfaceNode::ModalTrigger { modal_nodes, .. } => {
457            for child in modal_nodes {
458                validate_node_capabilities(surface_id, capabilities, child)?;
459            }
460            Capability::ModalTriggerNode
461        }
462        SurfaceNode::WorkflowTrigger { step_nodes, .. } => {
463            for child in step_nodes {
464                validate_node_capabilities(surface_id, capabilities, child)?;
465            }
466            Capability::WorkflowTriggerNode
467        }
468    };
469
470    require_capability(capabilities, node_capability, surface_id, "root_node kind")
471}
472
473fn require_capability(
474    capabilities: &CapabilitySet,
475    required: Capability,
476    surface_id: &SurfaceId,
477    usage: &str,
478) -> Result<(), SurfaceRegistrationError> {
479    if capabilities.0.contains(&required) {
480        return Ok(());
481    }
482
483    Err(missing_capability(format!(
484        "surface `{}` uses {} that requires capability `{}`",
485        surface_id,
486        usage,
487        serde_json::to_string(&required)
488            .unwrap_or_else(|_| "\"unknown\"".to_owned())
489            .trim_matches('"')
490    )))
491}
492
493fn validate_root_node_references(
494    surface_id: &SurfaceId,
495    node: &SurfaceNode,
496    interaction_ids: &HashSet<&str>,
497    data_source_ids: &HashSet<&str>,
498) -> Result<(), SurfaceRegistrationError> {
499    RootNodeReferenceValidator::new(surface_id, interaction_ids, data_source_ids).validate(node)
500}
501
502struct RootNodeReferenceValidator<'a> {
503    surface_id: &'a SurfaceId,
504    interaction_ids: &'a HashSet<&'a str>,
505    data_source_ids: &'a HashSet<&'a str>,
506}
507
508impl<'a> RootNodeReferenceValidator<'a> {
509    fn new(
510        surface_id: &'a SurfaceId,
511        interaction_ids: &'a HashSet<&'a str>,
512        data_source_ids: &'a HashSet<&'a str>,
513    ) -> Self {
514        Self {
515            surface_id,
516            interaction_ids,
517            data_source_ids,
518        }
519    }
520
521    fn validate(&self, node: &SurfaceNode) -> Result<(), SurfaceRegistrationError> {
522        match node {
523            SurfaceNode::Section { children, .. } => self.validate_children(children),
524            SurfaceNode::TextBlock { .. } => Ok(()),
525            SurfaceNode::KeyValue { data_source_id } => {
526                self.require_data_source_reference(data_source_id.as_str())
527            }
528            SurfaceNode::Table {
529                data_source_id,
530                row_actions,
531                ..
532            } => {
533                self.require_data_source_reference(data_source_id.as_str())?;
534                self.validate_table_row_actions(row_actions)
535            }
536            SurfaceNode::Form { interaction_id } => {
537                self.require_root_interaction_reference(interaction_id.as_str())
538            }
539            SurfaceNode::ActionBar { action_ids } => self.validate_action_bar(action_ids),
540            SurfaceNode::Tabs { tabs } => self.validate_tabs(tabs),
541            SurfaceNode::Callout { .. } | SurfaceNode::EmptyState { .. } => Ok(()),
542            SurfaceNode::ModalTrigger {
543                interaction_id,
544                modal_nodes,
545            } => {
546                self.require_root_interaction_reference(interaction_id.as_str())?;
547                self.validate_children(modal_nodes)
548            }
549            SurfaceNode::WorkflowTrigger {
550                interaction_id,
551                step_nodes,
552            } => {
553                self.require_root_interaction_reference(interaction_id.as_str())?;
554                self.validate_children(step_nodes)
555            }
556        }
557    }
558
559    fn validate_children(&self, nodes: &[SurfaceNode]) -> Result<(), SurfaceRegistrationError> {
560        for child in nodes {
561            self.validate(child)?;
562        }
563        Ok(())
564    }
565
566    fn validate_table_row_actions(
567        &self,
568        row_actions: &[crate::SurfaceTableRowAction],
569    ) -> Result<(), SurfaceRegistrationError> {
570        for row_action in row_actions {
571            self.require_interaction_reference(row_action.interaction_id.as_str(), || {
572                format!(
573                    "surface `{}` table references unknown row-action interaction_id `{}`",
574                    self.surface_id, row_action.interaction_id
575                )
576            })?;
577        }
578        Ok(())
579    }
580
581    fn validate_action_bar(
582        &self,
583        action_ids: &[InteractionId],
584    ) -> Result<(), SurfaceRegistrationError> {
585        for action_id in action_ids {
586            self.require_root_interaction_reference(action_id.as_str())?;
587        }
588        Ok(())
589    }
590
591    fn validate_tabs(&self, tabs: &[crate::SurfaceTab]) -> Result<(), SurfaceRegistrationError> {
592        let mut tab_ids: HashSet<&str> = HashSet::new();
593
594        for tab in tabs {
595            validate_surface_identifier(tab.id.as_str()).map_err(|err| {
596                invalid_contract(format!(
597                    "surface `{}` root_node contains invalid tab id `{}`: {}",
598                    self.surface_id, tab.id, err
599                ))
600            })?;
601
602            if !tab_ids.insert(tab.id.as_str()) {
603                return Err(invalid_contract(format!(
604                    "surface `{}` root_node contains duplicate tab id `{}` within one tabs node",
605                    self.surface_id, tab.id
606                )));
607            }
608
609            self.validate(&tab.root)?;
610        }
611
612        Ok(())
613    }
614
615    fn require_root_interaction_reference(
616        &self,
617        interaction_id: &str,
618    ) -> Result<(), SurfaceRegistrationError> {
619        self.require_interaction_reference(interaction_id, || {
620            format!(
621                "surface `{}` root_node references unknown interaction_id `{}`",
622                self.surface_id, interaction_id
623            )
624        })
625    }
626
627    fn require_data_source_reference(
628        &self,
629        data_source_id: &str,
630    ) -> Result<(), SurfaceRegistrationError> {
631        ensure_known_reference(self.data_source_ids, data_source_id, || {
632            format!(
633                "surface `{}` root_node references unknown data_source_id `{}`",
634                self.surface_id, data_source_id
635            )
636        })
637    }
638
639    fn require_interaction_reference(
640        &self,
641        interaction_id: &str,
642        error_message: impl FnOnce() -> String,
643    ) -> Result<(), SurfaceRegistrationError> {
644        ensure_known_reference(self.interaction_ids, interaction_id, error_message)
645    }
646}
647
648fn validate_workflow_step_references(
649    surface_id: &SurfaceId,
650    interactions: &[InteractionDescriptor],
651    interaction_ids: &HashSet<&str>,
652) -> Result<(), SurfaceRegistrationError> {
653    WorkflowStepReferenceValidator::new(surface_id, interaction_ids).validate(interactions)
654}
655
656struct WorkflowStepReferenceValidator<'a> {
657    surface_id: &'a SurfaceId,
658    interaction_ids: &'a HashSet<&'a str>,
659}
660
661impl<'a> WorkflowStepReferenceValidator<'a> {
662    fn new(surface_id: &'a SurfaceId, interaction_ids: &'a HashSet<&'a str>) -> Self {
663        Self {
664            surface_id,
665            interaction_ids,
666        }
667    }
668
669    fn validate(
670        &self,
671        interactions: &[InteractionDescriptor],
672    ) -> Result<(), SurfaceRegistrationError> {
673        for interaction in interactions {
674            if interaction.kind != InteractionKind::Workflow {
675                continue;
676            }
677            self.validate_workflow_interaction_steps(interaction)?;
678        }
679
680        Ok(())
681    }
682
683    fn validate_workflow_interaction_steps(
684        &self,
685        interaction: &InteractionDescriptor,
686    ) -> Result<(), SurfaceRegistrationError> {
687        for step in &interaction.workflow_steps {
688            if let Some(submit_interaction_id) = &step.submit_interaction_id {
689                ensure_known_reference(
690                    self.interaction_ids,
691                    submit_interaction_id.as_str(),
692                    || {
693                        format!(
694                            "surface `{}` workflow interaction `{}` references unknown submit_interaction_id `{}` in step `{}`",
695                            self.surface_id,
696                            interaction.interaction_id,
697                            submit_interaction_id,
698                            step.step_id
699                        )
700                    },
701                )?;
702            }
703
704            if let Some(form_ui) = &step.form_ui
705                && let Some(pre_load_interaction_id) = &form_ui.pre_load_interaction_id
706            {
707                ensure_known_reference(
708                    self.interaction_ids,
709                    pre_load_interaction_id.as_str(),
710                    || {
711                        format!(
712                            "surface `{}` workflow interaction `{}` references unknown pre_load_interaction_id `{}` in step `{}`",
713                            self.surface_id,
714                            interaction.interaction_id,
715                            pre_load_interaction_id,
716                            step.step_id
717                        )
718                    },
719                )?;
720            }
721        }
722
723        Ok(())
724    }
725}
726
727fn ensure_known_reference(
728    known_ids: &HashSet<&str>,
729    reference_id: &str,
730    error_message: impl FnOnce() -> String,
731) -> Result<(), SurfaceRegistrationError> {
732    if known_ids.contains(reference_id) {
733        return Ok(());
734    }
735
736    Err(invalid_contract(error_message()))
737}
738
739#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
740pub struct ProviderIdentity {
741    pub provider_id: String,
742    pub provider_kind: ProviderKind,
743    pub provider_namespace: String,
744}
745
746#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
747pub struct EffectiveTenantBinding {
748    pub scope: Scope,
749    #[serde(default, skip_serializing_if = "Option::is_none")]
750    pub tenant_id: Option<String>,
751}
752
753#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
754pub struct ProviderEncryptionMetadata {
755    pub key_id: String,
756    pub algorithm: ProviderEncryptionAlgorithm,
757    pub public_key: String,
758}
759
760wire_safe_enum! {
761    /// Encryption algorithm used for ECIES sealed-box parameter encryption.
762    #[derive(Debug, Clone, PartialEq, Eq)]
763    pub enum ProviderEncryptionAlgorithm {
764        EciesP256 => "ecies_p256",
765    }
766    parse_error = ParseProviderEncryptionAlgorithmError("invalid provider encryption algorithm");
767}
768
769impl ProviderEncryptionAlgorithm {
770    /// All known (non-`Other`) variants. Used in tests for exhaustive iteration
771    /// (`strum::EnumIter` is incompatible with the `Other(String)` tuple variant).
772    pub const KNOWN_VARIANTS: &'static [ProviderEncryptionAlgorithm] =
773        &[ProviderEncryptionAlgorithm::EciesP256];
774}
775
776#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
777pub struct RegisteredSurface {
778    pub descriptor: SurfaceDescriptor,
779    #[serde(default, skip_serializing_if = "Vec::is_empty")]
780    pub interactions: Vec<InteractionDescriptor>,
781    #[serde(default, skip_serializing_if = "Vec::is_empty")]
782    pub data_sources: Vec<DataSourceDescriptor>,
783}
784
785#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
786pub struct SurfaceRegistrationPolicy {
787    pub supported_generation: FrameworkGenerationRange,
788    pub required_capabilities: CapabilitySet,
789}
790
791#[derive(Debug, Clone, PartialEq, Eq, Error)]
792#[error("{code:?}: {message}")]
793pub struct SurfaceRegistrationError {
794    pub code: SurfaceRegistrationErrorCode,
795    pub message: String,
796}
797
798impl SurfaceRegistrationError {
799    pub fn new(code: SurfaceRegistrationErrorCode, message: String) -> Self {
800        Self { code, message }
801    }
802}
803
804#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
805#[serde(rename_all = "snake_case")]
806pub enum SurfaceRegistrationErrorCode {
807    UnsupportedGeneration,
808    MissingCapability,
809    InvalidSlot,
810    InvalidContract,
811}
812
813#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
814pub struct SurfaceActionRequest {
815    pub request_id: Uuid,
816    pub tenant_id: String,
817    pub surface_id: SurfaceId,
818    pub interaction_id: InteractionId,
819    pub idempotency_key: String,
820    #[serde(default, skip_serializing_if = "Option::is_none")]
821    pub target_provider_id: Option<String>,
822    pub caller_origin: CallerOrigin,
823    #[serde(default, skip_serializing_if = "Map::is_empty")]
824    pub params: Map<String, Value>,
825    #[serde(default, skip_serializing_if = "Option::is_none")]
826    pub encrypted_sensitive_params: Option<EncryptedSensitiveParams>,
827}
828
829#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
830#[serde(rename_all = "snake_case", tag = "kind")]
831pub enum CallerOrigin {
832    UserSession { user_id: String, session_id: String },
833    BuiltInSystem { principal: String },
834    Provider { provider_id: String },
835}
836
837#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
838pub struct EncryptedSensitiveParams {
839    pub key_id: String,
840    pub algorithm: ProviderEncryptionAlgorithm,
841    pub ciphertext_b64: String,
842}
843
844#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
845pub struct SurfaceActionCancel {
846    pub request_id: Uuid,
847    pub target_provider_id: String,
848    pub reason: SurfaceActionCancelReason,
849}
850
851#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
852#[serde(rename_all = "snake_case")]
853pub enum SurfaceActionCancelReason {
854    Timeout,
855    RequestCancelled,
856    ProviderDisconnected,
857}
858
859#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
860pub struct SurfaceActionResponse {
861    pub request_id: Uuid,
862    pub success: bool,
863    #[serde(default, skip_serializing_if = "Option::is_none")]
864    pub result: Option<Value>,
865    #[serde(default, skip_serializing_if = "Option::is_none")]
866    pub error: Option<SurfaceActionError>,
867}
868
869#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
870pub struct SurfaceActionError {
871    pub code: SurfaceActionErrorCode,
872    pub message: String,
873    #[serde(default, skip_serializing_if = "Option::is_none")]
874    pub details: Option<Value>,
875}
876
877#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
878#[serde(rename_all = "snake_case")]
879pub enum SurfaceActionErrorCode {
880    PermissionDenied,
881    InvalidRequest,
882    SchemaValidationFailed,
883    UnsupportedCapability,
884    ProviderUnavailable,
885    Timeout,
886    DuplicateRequest,
887    InternalError,
888}