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#[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 pub fn require_semantics_profile(mut self) -> Self {
45 self.semantics_profile_required = true;
46 self
47 }
48
49 pub fn require_health(mut self) -> Self {
51 self.health_required = true;
52 self
53 }
54
55 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 pub fn require_service_endpoints(mut self) -> Self {
66 self.service_endpoints_required = true;
67 self
68 }
69
70 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 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 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}