Skip to main content

a3s_runtime/
consumer.rs

1use crate::contract::{
2    NetworkMode, RuntimeCapabilities, RuntimeFeature, RuntimeHealthState, RuntimeObservation,
3    RuntimeUnitClass, RuntimeUnitSpec, RuntimeUnitState,
4};
5use crate::{RuntimeAttestationBinding, RuntimeError, RuntimeResult};
6use std::collections::BTreeSet;
7
8/// Provider-neutral requirements imposed by one Runtime consumer profile.
9///
10/// This is an admission and readiness abstraction, not a wire-contract type.
11/// Callers retain ownership of their domain semantics and compose them from the
12/// two generic Runtime unit classes and advertised capabilities.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct RuntimeConsumerRequirements {
15    unit_class: RuntimeUnitClass,
16    required_features: BTreeSet<RuntimeFeature>,
17    semantics_profile_required: bool,
18    health_required: bool,
19    service_lifecycle_required: bool,
20    service_endpoints_required: bool,
21    identity_attestation_required: bool,
22}
23
24impl RuntimeConsumerRequirements {
25    pub fn new(unit_class: RuntimeUnitClass) -> Self {
26        Self {
27            unit_class,
28            required_features: BTreeSet::new(),
29            semantics_profile_required: false,
30            health_required: false,
31            service_lifecycle_required: false,
32            service_endpoints_required: false,
33            identity_attestation_required: false,
34        }
35    }
36
37    pub fn require_feature(mut self, feature: RuntimeFeature) -> Self {
38        self.required_features.insert(feature);
39        self
40    }
41
42    /// Requires the immutable consumer-owned semantics profile to be present
43    /// in the specification and repeated by provider evidence.
44    pub fn require_semantics_profile(mut self) -> Self {
45        self.semantics_profile_required = true;
46        self
47    }
48
49    /// Requires a Service readiness policy and a ready running observation.
50    pub fn require_health(mut self) -> Self {
51        self.health_required = true;
52        self
53    }
54
55    /// Requires distinct Service liveness evidence plus a bounded graceful
56    /// shutdown policy.
57    pub fn require_service_lifecycle(mut self) -> Self {
58        self.service_lifecycle_required = true;
59        self.required_features
60            .insert(RuntimeFeature::ServiceLifecycle);
61        self
62    }
63
64    /// Requires a Service network declaration and exact observed endpoints.
65    pub fn require_service_endpoints(mut self) -> Self {
66        self.service_endpoints_required = true;
67        self
68    }
69
70    /// Requires one opaque identity attachment in the desired specification
71    /// and exact generation-bound provider attestation in the observation.
72    pub fn require_identity_attestation(mut self) -> Self {
73        self.identity_attestation_required = true;
74        self.required_features
75            .insert(RuntimeFeature::IdentityAttachment);
76        self.required_features.insert(RuntimeFeature::Attestation);
77        self
78    }
79
80    /// Fails closed unless the immutable unit specification can be fulfilled
81    /// by the selected provider and satisfies this consumer profile.
82    pub fn admit_spec(
83        &self,
84        spec: &RuntimeUnitSpec,
85        capabilities: &RuntimeCapabilities,
86    ) -> RuntimeResult<()> {
87        self.validate().map_err(RuntimeError::InvalidRequest)?;
88        spec.validate().map_err(RuntimeError::InvalidRequest)?;
89        capabilities.validate().map_err(RuntimeError::Protocol)?;
90        self.validate_spec_shape(spec)
91            .map_err(RuntimeError::InvalidRequest)?;
92
93        let mut missing = capabilities
94            .missing_for(spec)
95            .map_err(RuntimeError::Protocol)?;
96        missing.extend(
97            self.required_features
98                .iter()
99                .filter(|feature| !capabilities.supports_feature(**feature))
100                .map(|feature| format!("feature:{feature:?}")),
101        );
102        missing.sort();
103        missing.dedup();
104        if missing.is_empty() {
105            Ok(())
106        } else {
107            Err(RuntimeError::UnsupportedCapabilities(missing))
108        }
109    }
110
111    /// Accepts an observation only when it is bound to the admitted unit and
112    /// contains every readiness proof required by this consumer profile.
113    pub fn accept_observation(
114        &self,
115        spec: &RuntimeUnitSpec,
116        observation: &RuntimeObservation,
117    ) -> RuntimeResult<()> {
118        self.validate().map_err(RuntimeError::InvalidRequest)?;
119        spec.validate().map_err(RuntimeError::InvalidRequest)?;
120        self.validate_spec_shape(spec)
121            .map_err(RuntimeError::InvalidRequest)?;
122        observation
123            .validate_against(spec)
124            .map_err(RuntimeError::Protocol)?;
125
126        if self.semantics_profile_required {
127            let expected = spec
128                .semantics_profile_digest
129                .as_ref()
130                .expect("validated consumer semantics profile");
131            let actual = observation
132                .evidence
133                .as_ref()
134                .and_then(|evidence| evidence.semantics_profile_digest.as_ref());
135            if actual != Some(expected) {
136                return Err(RuntimeError::Protocol(
137                    "Runtime observation omits exact semantics profile evidence".into(),
138                ));
139            }
140        }
141
142        if self.health_required
143            && (observation.state != RuntimeUnitState::Running
144                || observation.health.as_ref().map(|health| health.state)
145                    != Some(RuntimeHealthState::Healthy))
146        {
147            return Err(RuntimeError::Protocol(
148                "Runtime consumer requires a healthy running Service observation".into(),
149            ));
150        }
151
152        if self.service_lifecycle_required
153            && (observation.state != RuntimeUnitState::Running
154                || observation.liveness.as_ref().map(|health| health.state)
155                    != Some(RuntimeHealthState::Healthy))
156        {
157            return Err(RuntimeError::Protocol(
158                "Runtime consumer requires a live running Service observation".into(),
159            ));
160        }
161
162        if self.service_endpoints_required
163            && observation
164                .service_endpoints()
165                .map_err(RuntimeError::Protocol)?
166                .is_empty()
167        {
168            return Err(RuntimeError::Protocol(
169                "Runtime consumer requires exact Service endpoint evidence".into(),
170            ));
171        }
172        if self.identity_attestation_required {
173            RuntimeAttestationBinding::from_observation(spec, observation)
174                .map_err(RuntimeError::Protocol)?;
175        }
176        Ok(())
177    }
178
179    fn validate(&self) -> Result<(), String> {
180        if (self.health_required
181            || self.service_lifecycle_required
182            || self.service_endpoints_required)
183            && self.unit_class != RuntimeUnitClass::Service
184        {
185            return Err(
186                "health, lifecycle, and endpoint requirements apply only to Runtime Service".into(),
187            );
188        }
189        Ok(())
190    }
191
192    fn validate_spec_shape(&self, spec: &RuntimeUnitSpec) -> Result<(), String> {
193        if spec.class != self.unit_class {
194            return Err(format!(
195                "Runtime consumer requires {:?}, but the specification is {:?}",
196                self.unit_class, spec.class
197            ));
198        }
199        if self.semantics_profile_required && spec.semantics_profile_digest.is_none() {
200            return Err("Runtime consumer requires a semantics profile digest".into());
201        }
202        if self.identity_attestation_required && spec.identity_attachment_digest.is_none() {
203            return Err("Runtime consumer requires an identity attachment digest".into());
204        }
205        if self.health_required && spec.health.is_none() {
206            return Err("Runtime consumer requires a Service readiness policy".into());
207        }
208        if self.service_lifecycle_required && spec.service_lifecycle.is_none() {
209            return Err("Runtime consumer requires a Service lifecycle policy".into());
210        }
211        if self.service_endpoints_required
212            && (spec.network.mode != NetworkMode::Service || spec.network.ports.is_empty())
213        {
214            return Err("Runtime consumer requires declared Service endpoints".into());
215        }
216        Ok(())
217    }
218}