1use super::{verify_runtime_base, RuntimeBaseConformanceCase, RuntimeBaseConformanceReport};
2use crate::contract::{
3 HealthCheckKind, MountKind, NetworkMode, ResourceControl, RuntimeCapabilities, RuntimeFeature,
4};
5use crate::{RuntimeClient, RuntimeError, RuntimeResult};
6use async_trait::async_trait;
7use std::collections::{BTreeMap, BTreeSet};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
11pub enum RuntimeConformanceProfile {
12 Base,
13 Recovery,
14 Networking,
15 Mounts,
16 Health,
17 Resources,
18 Logs,
19 Exec,
20 Security,
21 Outputs,
22 Evidence,
23}
24
25impl RuntimeConformanceProfile {
26 pub const fn as_str(self) -> &'static str {
27 match self {
28 Self::Base => "base",
29 Self::Recovery => "recovery",
30 Self::Networking => "networking",
31 Self::Mounts => "mounts",
32 Self::Health => "health",
33 Self::Resources => "resources",
34 Self::Logs => "logs",
35 Self::Exec => "exec",
36 Self::Security => "security",
37 Self::Outputs => "outputs",
38 Self::Evidence => "evidence",
39 }
40 }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct RuntimeConformanceProfileRequirements {
46 pub profile: RuntimeConformanceProfile,
47 pub case_ids: BTreeSet<String>,
48 pub capability_claims: BTreeSet<String>,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct RuntimeConformanceProfileEvidence {
54 pub profile: RuntimeConformanceProfile,
55 pub case_ids: BTreeSet<String>,
56 pub capability_claims: BTreeSet<String>,
57}
58
59#[derive(Debug, Clone, Default, PartialEq, Eq)]
63pub struct RuntimeConformanceInventory {
64 pub entries: BTreeMap<String, String>,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct RuntimeConformanceSuiteReport {
70 pub base: RuntimeBaseConformanceReport,
71 pub profiles: Vec<RuntimeConformanceProfileEvidence>,
72 pub inventory_before: RuntimeConformanceInventory,
73 pub inventory_after: RuntimeConformanceInventory,
74}
75
76#[async_trait]
81pub trait RuntimeConformanceFixture: Send + Sync {
82 fn base_case(&self) -> &RuntimeBaseConformanceCase;
83
84 fn available_profiles(&self) -> BTreeSet<RuntimeConformanceProfile>;
85
86 async fn inventory(&self) -> RuntimeResult<RuntimeConformanceInventory>;
87
88 async fn run_profile(
89 &self,
90 client: &dyn RuntimeClient,
91 capabilities: &RuntimeCapabilities,
92 profile: RuntimeConformanceProfile,
93 ) -> RuntimeResult<RuntimeConformanceProfileEvidence>;
94
95 async fn cleanup(&self) -> RuntimeResult<()>;
96}
97
98pub fn required_runtime_profiles(
101 capabilities: &RuntimeCapabilities,
102) -> RuntimeResult<BTreeSet<RuntimeConformanceProfile>> {
103 capabilities.validate().map_err(RuntimeError::Protocol)?;
104 let mut profiles = BTreeSet::from([
105 RuntimeConformanceProfile::Base,
106 RuntimeConformanceProfile::Recovery,
107 ]);
108 if !capabilities.network_modes.is_empty() {
109 profiles.insert(RuntimeConformanceProfile::Networking);
110 }
111 if !capabilities.mount_kinds.is_empty() {
112 profiles.insert(RuntimeConformanceProfile::Mounts);
113 }
114 if !capabilities.health_check_kinds.is_empty() {
115 profiles.insert(RuntimeConformanceProfile::Health);
116 }
117 if !capabilities.resource_controls.is_empty() {
118 profiles.insert(RuntimeConformanceProfile::Resources);
119 }
120 if capabilities.supports_feature(RuntimeFeature::Logs) {
121 profiles.insert(RuntimeConformanceProfile::Logs);
122 }
123 if capabilities.supports_feature(RuntimeFeature::Exec) {
124 profiles.insert(RuntimeConformanceProfile::Exec);
125 }
126 if !capabilities.isolation_levels.is_empty()
127 || capabilities.supports_feature(RuntimeFeature::SecretReferences)
128 {
129 profiles.insert(RuntimeConformanceProfile::Security);
130 }
131 if capabilities.supports_feature(RuntimeFeature::OutputArtifacts) {
132 profiles.insert(RuntimeConformanceProfile::Outputs);
133 }
134 if capabilities.supports_feature(RuntimeFeature::Usage)
135 || capabilities.supports_feature(RuntimeFeature::Attestation)
136 {
137 profiles.insert(RuntimeConformanceProfile::Evidence);
138 }
139 Ok(profiles)
140}
141
142pub fn runtime_profile_requirements(
145 capabilities: &RuntimeCapabilities,
146 profile: RuntimeConformanceProfile,
147) -> RuntimeResult<RuntimeConformanceProfileRequirements> {
148 capabilities.validate().map_err(RuntimeError::Protocol)?;
149 let mut case_ids = base_case_ids(profile)
150 .iter()
151 .copied()
152 .map(str::to_owned)
153 .collect::<BTreeSet<_>>();
154 extend_capability_case_ids(capabilities, profile, &mut case_ids);
155 if profile == RuntimeConformanceProfile::Security
156 && capabilities.supports_feature(RuntimeFeature::SecretReferences)
157 {
158 case_ids.insert("SECURITY-SECRET-NONDISCLOSURE".into());
159 }
160 if profile == RuntimeConformanceProfile::Evidence
161 && capabilities.supports_feature(RuntimeFeature::Attestation)
162 {
163 case_ids.insert("EVIDENCE-ATTESTATION-VALIDITY".into());
164 }
165 if profile == RuntimeConformanceProfile::Evidence
166 && capabilities.supports_feature(RuntimeFeature::Usage)
167 {
168 case_ids.insert("EVIDENCE-USAGE-VALIDITY".into());
169 }
170 let capability_claims = capability_claims(capabilities, profile);
171 Ok(RuntimeConformanceProfileRequirements {
172 profile,
173 case_ids,
174 capability_claims,
175 })
176}
177
178pub async fn verify_runtime_profiles(
182 client: &dyn RuntimeClient,
183 fixture: &dyn RuntimeConformanceFixture,
184) -> RuntimeResult<RuntimeConformanceSuiteReport> {
185 let capabilities = client.capabilities().await?;
186 capabilities.validate().map_err(RuntimeError::Protocol)?;
187 let missing_lifecycle = [
188 RuntimeFeature::DurableIdentity,
189 RuntimeFeature::Stop,
190 RuntimeFeature::Remove,
191 ]
192 .into_iter()
193 .filter(|feature| !capabilities.supports_feature(*feature))
194 .map(|feature| format!("feature:{feature:?}"))
195 .collect::<Vec<_>>();
196 if !missing_lifecycle.is_empty() {
197 return Err(RuntimeError::UnsupportedCapabilities(missing_lifecycle));
198 }
199 fixture
200 .base_case()
201 .validate()
202 .map_err(RuntimeError::InvalidRequest)?;
203 for spec in fixture.base_case().specifications() {
204 let missing = capabilities
205 .missing_for(spec)
206 .map_err(RuntimeError::InvalidRequest)?;
207 if !missing.is_empty() {
208 return Err(RuntimeError::UnsupportedCapabilities(missing));
209 }
210 }
211
212 let required = required_runtime_profiles(&capabilities)?;
213 let mut selected = fixture.available_profiles();
214 selected.insert(RuntimeConformanceProfile::Base);
215 let missing_profiles = required.difference(&selected).copied().collect::<Vec<_>>();
216 if !missing_profiles.is_empty() {
217 return Err(RuntimeError::Protocol(format!(
218 "conformance fixture omits required profiles: {}",
219 missing_profiles
220 .iter()
221 .map(|profile| profile.as_str())
222 .collect::<Vec<_>>()
223 .join(", ")
224 )));
225 }
226 selected.extend(required);
227
228 let inventory_before = fixture.inventory().await?;
229 let execution: RuntimeResult<(
230 RuntimeBaseConformanceReport,
231 Vec<RuntimeConformanceProfileEvidence>,
232 )> = async {
233 let base = verify_runtime_base(client, fixture.base_case()).await?;
234 let mut evidence = vec![base_evidence(&capabilities)?];
235 for profile in selected
236 .iter()
237 .copied()
238 .filter(|profile| *profile != RuntimeConformanceProfile::Base)
239 {
240 let actual = fixture.run_profile(client, &capabilities, profile).await?;
241 validate_evidence(&capabilities, profile, &actual)?;
242 evidence.push(actual);
243 }
244 Ok((base, evidence))
245 }
246 .await;
247
248 let cleanup = fixture.cleanup().await;
249 let inventory_after = fixture.inventory().await;
250 cleanup?;
251 let inventory_after = inventory_after?;
252 if inventory_after != inventory_before {
253 return Err(RuntimeError::Protocol(format!(
254 "conformance cleanup changed provider inventory: before={:?}, after={:?}",
255 inventory_before.entries, inventory_after.entries
256 )));
257 }
258 let (base, profiles) = execution?;
259 Ok(RuntimeConformanceSuiteReport {
260 base,
261 profiles,
262 inventory_before,
263 inventory_after,
264 })
265}
266
267fn validate_evidence(
268 capabilities: &RuntimeCapabilities,
269 profile: RuntimeConformanceProfile,
270 evidence: &RuntimeConformanceProfileEvidence,
271) -> RuntimeResult<()> {
272 if evidence.profile != profile {
273 return Err(RuntimeError::Protocol(format!(
274 "conformance {} fixture returned {} evidence",
275 profile.as_str(),
276 evidence.profile.as_str()
277 )));
278 }
279 let required = runtime_profile_requirements(capabilities, profile)?;
280 let missing_cases = required
281 .case_ids
282 .difference(&evidence.case_ids)
283 .cloned()
284 .collect::<Vec<_>>();
285 let missing_claims = required
286 .capability_claims
287 .difference(&evidence.capability_claims)
288 .cloned()
289 .collect::<Vec<_>>();
290 if !missing_cases.is_empty() || !missing_claims.is_empty() {
291 return Err(RuntimeError::Protocol(format!(
292 "conformance {} evidence is incomplete: missing cases {:?}, missing claims {:?}",
293 profile.as_str(),
294 missing_cases,
295 missing_claims
296 )));
297 }
298 Ok(())
299}
300
301fn base_evidence(
302 capabilities: &RuntimeCapabilities,
303) -> RuntimeResult<RuntimeConformanceProfileEvidence> {
304 let required = runtime_profile_requirements(capabilities, RuntimeConformanceProfile::Base)?;
305 Ok(RuntimeConformanceProfileEvidence {
306 profile: required.profile,
307 case_ids: required.case_ids,
308 capability_claims: required.capability_claims,
309 })
310}
311
312fn base_case_ids(profile: RuntimeConformanceProfile) -> &'static [&'static str] {
313 match profile {
314 RuntimeConformanceProfile::Base => &[
315 "BASE-TASK-SUCCESS",
316 "BASE-TASK-FAILURE",
317 "BASE-TASK-TIMEOUT",
318 "BASE-SERVICE-LIFECYCLE",
319 "BASE-EXACT-REPLAY",
320 "BASE-GENERATION-CONFLICT",
321 "BASE-TOMBSTONE",
322 ],
323 RuntimeConformanceProfile::Recovery => &[
324 "RECOVERY-CREATE-BEFORE-ACK",
325 "RECOVERY-CLIENT-RESTART",
326 "RECOVERY-PROVIDER-RESTART",
327 "RECOVERY-EXTERNAL-DELETION",
328 "RECOVERY-SAME-GENERATION-REPLACEMENT",
329 "RECOVERY-DUPLICATE-DETECTION",
330 ],
331 RuntimeConformanceProfile::Networking => &[],
332 RuntimeConformanceProfile::Mounts => &["MOUNT-READ-ONLY", "MOUNT-CLEANUP"],
333 RuntimeConformanceProfile::Health => &[
334 "HEALTH-THRESHOLD-TRANSITION",
335 "HEALTH-PROBE-TIMEOUT",
336 "HEALTH-START-PERIOD",
337 "HEALTH-UNHEALTHY-EXIT",
338 ],
339 RuntimeConformanceProfile::Resources => &[],
340 RuntimeConformanceProfile::Logs => &[
341 "LOG-STREAM-FILTER",
342 "LOG-TOTAL-ORDER",
343 "LOG-CURSOR-RESUME",
344 "LOG-SAME-TIMESTAMP",
345 "LOG-LIMIT",
346 "LOG-ROTATION-GAP",
347 "LOG-RETENTION",
348 "LOG-LARGE-RECORD",
349 ],
350 RuntimeConformanceProfile::Exec => &[
351 "EXEC-STATE-POLICY",
352 "EXEC-TIMEOUT-REPLAY",
353 "EXEC-EXIT-CODE",
354 "EXEC-OUTPUT-BOUNDS",
355 "EXEC-OUTPUT-TRUNCATION",
356 "EXEC-IDENTITY-GENERATION-BINDING",
357 ],
358 RuntimeConformanceProfile::Security => &[
359 "SECURITY-DIGEST-PINNING",
360 "SECURITY-METADATA-TAMPER",
361 "SECURITY-NAMESPACE-SEPARATION",
362 "SECURITY-LEAST-PRIVILEGE",
363 "SECURITY-HOSTILE-INPUT",
364 ],
365 RuntimeConformanceProfile::Outputs => &["OUTPUT-EXACT-BOUNDED", "OUTPUT-DIGEST-BINDING"],
366 RuntimeConformanceProfile::Evidence => &[
367 "EVIDENCE-SPEC-BINDING",
368 "EVIDENCE-SEMANTICS-PROFILE-BINDING",
369 ],
370 }
371}
372
373fn extend_capability_case_ids(
374 capabilities: &RuntimeCapabilities,
375 profile: RuntimeConformanceProfile,
376 case_ids: &mut BTreeSet<String>,
377) {
378 match profile {
379 RuntimeConformanceProfile::Networking => {
380 for mode in &capabilities.network_modes {
381 match mode {
382 NetworkMode::None => {
383 case_ids.insert("NETWORK-MODE-NONE".into());
384 case_ids.insert("NETWORK-OUTBOUND-DENIED".into());
385 }
386 NetworkMode::Outbound => {
387 case_ids.insert("NETWORK-MODE-OUTBOUND".into());
388 case_ids.insert("NETWORK-OUTBOUND-ALLOWED".into());
389 }
390 NetworkMode::Service => {
391 case_ids.insert("NETWORK-MODE-SERVICE".into());
392 case_ids.insert("NETWORK-PROTOCOL-TCP".into());
393 case_ids.insert("NETWORK-PROTOCOL-UDP".into());
394 case_ids.insert("NETWORK-LOOPBACK-PUBLICATION".into());
395 case_ids.insert("NETWORK-PORT-COLLISION".into());
396 }
397 }
398 }
399 }
400 RuntimeConformanceProfile::Mounts => {
401 for kind in &capabilities.mount_kinds {
402 case_ids.insert(
403 match kind {
404 MountKind::Artifact => "MOUNT-ARTIFACT-BEHAVIOR",
405 MountKind::Volume => "MOUNT-VOLUME-PERSISTENCE",
406 MountKind::Tmpfs => "MOUNT-TMPFS-ISOLATION",
407 }
408 .into(),
409 );
410 }
411 }
412 RuntimeConformanceProfile::Health => {
413 for kind in &capabilities.health_check_kinds {
414 case_ids.insert(
415 match kind {
416 HealthCheckKind::Http => "HEALTH-PROBE-HTTP",
417 HealthCheckKind::Tcp => "HEALTH-PROBE-TCP",
418 HealthCheckKind::Command => "HEALTH-PROBE-COMMAND",
419 }
420 .into(),
421 );
422 }
423 }
424 RuntimeConformanceProfile::Resources => {
425 for control in &capabilities.resource_controls {
426 let (configuration, behavior) = match control {
427 ResourceControl::Cpu => ("RESOURCE-CPU-CONFIG", "RESOURCE-CPU-BEHAVIOR"),
428 ResourceControl::Memory => {
429 ("RESOURCE-MEMORY-CONFIG", "RESOURCE-MEMORY-BEHAVIOR")
430 }
431 ResourceControl::Pids => ("RESOURCE-PIDS-CONFIG", "RESOURCE-PIDS-BEHAVIOR"),
432 ResourceControl::EphemeralStorage => (
433 "RESOURCE-EPHEMERAL-STORAGE-CONFIG",
434 "RESOURCE-EPHEMERAL-STORAGE-BEHAVIOR",
435 ),
436 ResourceControl::ExecutionTimeout => (
437 "RESOURCE-EXECUTION-TIMEOUT-CONFIG",
438 "RESOURCE-EXECUTION-TIMEOUT-BEHAVIOR",
439 ),
440 };
441 case_ids.insert(configuration.into());
442 case_ids.insert(behavior.into());
443 }
444 }
445 RuntimeConformanceProfile::Base
446 | RuntimeConformanceProfile::Recovery
447 | RuntimeConformanceProfile::Logs
448 | RuntimeConformanceProfile::Exec
449 | RuntimeConformanceProfile::Security
450 | RuntimeConformanceProfile::Outputs
451 | RuntimeConformanceProfile::Evidence => {}
452 }
453}
454
455fn capability_claims(
456 capabilities: &RuntimeCapabilities,
457 profile: RuntimeConformanceProfile,
458) -> BTreeSet<String> {
459 match profile {
460 RuntimeConformanceProfile::Base => capabilities
461 .unit_classes
462 .iter()
463 .map(|class| format!("unit_class:{class:?}"))
464 .collect(),
465 RuntimeConformanceProfile::Recovery => BTreeSet::from(["feature:DurableIdentity".into()]),
466 RuntimeConformanceProfile::Networking => capabilities
467 .network_modes
468 .iter()
469 .map(|mode| format!("network_mode:{mode:?}"))
470 .collect(),
471 RuntimeConformanceProfile::Mounts => capabilities
472 .mount_kinds
473 .iter()
474 .map(|kind| format!("mount_kind:{kind:?}"))
475 .collect(),
476 RuntimeConformanceProfile::Health => capabilities
477 .health_check_kinds
478 .iter()
479 .map(|kind| format!("health_check:{kind:?}"))
480 .collect(),
481 RuntimeConformanceProfile::Resources => capabilities
482 .resource_controls
483 .iter()
484 .map(|control| format!("resource_control:{control:?}"))
485 .collect(),
486 RuntimeConformanceProfile::Logs => BTreeSet::from(["feature:Logs".into()]),
487 RuntimeConformanceProfile::Exec => BTreeSet::from(["feature:Exec".into()]),
488 RuntimeConformanceProfile::Security => {
489 let mut claims = capabilities
490 .isolation_levels
491 .iter()
492 .map(|level| format!("isolation:{level:?}"))
493 .collect::<BTreeSet<_>>();
494 claims.insert("feature:DurableIdentity".into());
495 if capabilities.supports_feature(RuntimeFeature::SecretReferences) {
496 claims.insert("feature:SecretReferences".into());
497 }
498 claims
499 }
500 RuntimeConformanceProfile::Outputs => BTreeSet::from(["feature:OutputArtifacts".into()]),
501 RuntimeConformanceProfile::Evidence => [RuntimeFeature::Usage, RuntimeFeature::Attestation]
502 .into_iter()
503 .filter(|feature| capabilities.supports_feature(*feature))
504 .map(|feature| format!("feature:{feature:?}"))
505 .collect(),
506 }
507}