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 || capabilities.supports_feature(RuntimeFeature::IdentityAttachment)
137 {
138 profiles.insert(RuntimeConformanceProfile::Evidence);
139 }
140 Ok(profiles)
141}
142
143pub fn runtime_profile_requirements(
146 capabilities: &RuntimeCapabilities,
147 profile: RuntimeConformanceProfile,
148) -> RuntimeResult<RuntimeConformanceProfileRequirements> {
149 capabilities.validate().map_err(RuntimeError::Protocol)?;
150 let mut case_ids = base_case_ids(profile)
151 .iter()
152 .copied()
153 .map(str::to_owned)
154 .collect::<BTreeSet<_>>();
155 extend_capability_case_ids(capabilities, profile, &mut case_ids);
156 if profile == RuntimeConformanceProfile::Security
157 && capabilities.supports_feature(RuntimeFeature::SecretReferences)
158 {
159 case_ids.insert("SECURITY-SECRET-NONDISCLOSURE".into());
160 }
161 if profile == RuntimeConformanceProfile::Evidence
162 && capabilities.supports_feature(RuntimeFeature::Attestation)
163 {
164 case_ids.insert("EVIDENCE-ATTESTATION-VALIDITY".into());
165 }
166 if profile == RuntimeConformanceProfile::Evidence
167 && capabilities.supports_feature(RuntimeFeature::Usage)
168 {
169 case_ids.insert("EVIDENCE-USAGE-VALIDITY".into());
170 }
171 if profile == RuntimeConformanceProfile::Evidence
172 && capabilities.supports_feature(RuntimeFeature::IdentityAttachment)
173 {
174 case_ids.insert("EVIDENCE-IDENTITY-ATTACHMENT-BINDING".into());
175 }
176 let capability_claims = capability_claims(capabilities, profile);
177 Ok(RuntimeConformanceProfileRequirements {
178 profile,
179 case_ids,
180 capability_claims,
181 })
182}
183
184pub async fn verify_runtime_profiles(
188 client: &dyn RuntimeClient,
189 fixture: &dyn RuntimeConformanceFixture,
190) -> RuntimeResult<RuntimeConformanceSuiteReport> {
191 let capabilities = client.capabilities().await?;
192 capabilities.validate().map_err(RuntimeError::Protocol)?;
193 let missing_lifecycle = [
194 RuntimeFeature::DurableIdentity,
195 RuntimeFeature::Stop,
196 RuntimeFeature::Remove,
197 ]
198 .into_iter()
199 .filter(|feature| !capabilities.supports_feature(*feature))
200 .map(|feature| format!("feature:{feature:?}"))
201 .collect::<Vec<_>>();
202 if !missing_lifecycle.is_empty() {
203 return Err(RuntimeError::UnsupportedCapabilities(missing_lifecycle));
204 }
205 fixture
206 .base_case()
207 .validate()
208 .map_err(RuntimeError::InvalidRequest)?;
209 for spec in fixture.base_case().specifications() {
210 let missing = capabilities
211 .missing_for(spec)
212 .map_err(RuntimeError::InvalidRequest)?;
213 if !missing.is_empty() {
214 return Err(RuntimeError::UnsupportedCapabilities(missing));
215 }
216 }
217
218 let required = required_runtime_profiles(&capabilities)?;
219 let mut selected = fixture.available_profiles();
220 selected.insert(RuntimeConformanceProfile::Base);
221 let missing_profiles = required.difference(&selected).copied().collect::<Vec<_>>();
222 if !missing_profiles.is_empty() {
223 return Err(RuntimeError::Protocol(format!(
224 "conformance fixture omits required profiles: {}",
225 missing_profiles
226 .iter()
227 .map(|profile| profile.as_str())
228 .collect::<Vec<_>>()
229 .join(", ")
230 )));
231 }
232 selected.extend(required);
233
234 let inventory_before = fixture.inventory().await?;
235 let execution: RuntimeResult<(
236 RuntimeBaseConformanceReport,
237 Vec<RuntimeConformanceProfileEvidence>,
238 )> = async {
239 let base = verify_runtime_base(client, fixture.base_case()).await?;
240 let mut evidence = vec![base_evidence(&capabilities)?];
241 for profile in selected
242 .iter()
243 .copied()
244 .filter(|profile| *profile != RuntimeConformanceProfile::Base)
245 {
246 let actual = fixture.run_profile(client, &capabilities, profile).await?;
247 validate_evidence(&capabilities, profile, &actual)?;
248 evidence.push(actual);
249 }
250 Ok((base, evidence))
251 }
252 .await;
253
254 let cleanup = fixture.cleanup().await;
255 let inventory_after = fixture.inventory().await;
256 cleanup?;
257 let inventory_after = inventory_after?;
258 if inventory_after != inventory_before {
259 return Err(RuntimeError::Protocol(format!(
260 "conformance cleanup changed provider inventory: before={:?}, after={:?}",
261 inventory_before.entries, inventory_after.entries
262 )));
263 }
264 let (base, profiles) = execution?;
265 Ok(RuntimeConformanceSuiteReport {
266 base,
267 profiles,
268 inventory_before,
269 inventory_after,
270 })
271}
272
273fn validate_evidence(
274 capabilities: &RuntimeCapabilities,
275 profile: RuntimeConformanceProfile,
276 evidence: &RuntimeConformanceProfileEvidence,
277) -> RuntimeResult<()> {
278 if evidence.profile != profile {
279 return Err(RuntimeError::Protocol(format!(
280 "conformance {} fixture returned {} evidence",
281 profile.as_str(),
282 evidence.profile.as_str()
283 )));
284 }
285 let required = runtime_profile_requirements(capabilities, profile)?;
286 let missing_cases = required
287 .case_ids
288 .difference(&evidence.case_ids)
289 .cloned()
290 .collect::<Vec<_>>();
291 let missing_claims = required
292 .capability_claims
293 .difference(&evidence.capability_claims)
294 .cloned()
295 .collect::<Vec<_>>();
296 if !missing_cases.is_empty() || !missing_claims.is_empty() {
297 return Err(RuntimeError::Protocol(format!(
298 "conformance {} evidence is incomplete: missing cases {:?}, missing claims {:?}",
299 profile.as_str(),
300 missing_cases,
301 missing_claims
302 )));
303 }
304 Ok(())
305}
306
307fn base_evidence(
308 capabilities: &RuntimeCapabilities,
309) -> RuntimeResult<RuntimeConformanceProfileEvidence> {
310 let required = runtime_profile_requirements(capabilities, RuntimeConformanceProfile::Base)?;
311 Ok(RuntimeConformanceProfileEvidence {
312 profile: required.profile,
313 case_ids: required.case_ids,
314 capability_claims: required.capability_claims,
315 })
316}
317
318fn base_case_ids(profile: RuntimeConformanceProfile) -> &'static [&'static str] {
319 match profile {
320 RuntimeConformanceProfile::Base => &[
321 "BASE-TASK-SUCCESS",
322 "BASE-TASK-FAILURE",
323 "BASE-TASK-TIMEOUT",
324 "BASE-SERVICE-LIFECYCLE",
325 "BASE-EXACT-REPLAY",
326 "BASE-GENERATION-CONFLICT",
327 "BASE-TOMBSTONE",
328 ],
329 RuntimeConformanceProfile::Recovery => &[
330 "RECOVERY-CREATE-BEFORE-ACK",
331 "RECOVERY-CLIENT-RESTART",
332 "RECOVERY-PROVIDER-RESTART",
333 "RECOVERY-EXTERNAL-DELETION",
334 "RECOVERY-SAME-GENERATION-REPLACEMENT",
335 "RECOVERY-DUPLICATE-DETECTION",
336 ],
337 RuntimeConformanceProfile::Networking => &[],
338 RuntimeConformanceProfile::Mounts => &["MOUNT-READ-ONLY", "MOUNT-CLEANUP"],
339 RuntimeConformanceProfile::Health => &[
340 "HEALTH-THRESHOLD-TRANSITION",
341 "HEALTH-PROBE-TIMEOUT",
342 "HEALTH-START-PERIOD",
343 "HEALTH-UNHEALTHY-EXIT",
344 ],
345 RuntimeConformanceProfile::Resources => &[],
346 RuntimeConformanceProfile::Logs => &[
347 "LOG-STREAM-FILTER",
348 "LOG-TOTAL-ORDER",
349 "LOG-CURSOR-RESUME",
350 "LOG-SAME-TIMESTAMP",
351 "LOG-LIMIT",
352 "LOG-ROTATION-GAP",
353 "LOG-RETENTION",
354 "LOG-LARGE-RECORD",
355 ],
356 RuntimeConformanceProfile::Exec => &[
357 "EXEC-STATE-POLICY",
358 "EXEC-TIMEOUT-REPLAY",
359 "EXEC-EXIT-CODE",
360 "EXEC-OUTPUT-BOUNDS",
361 "EXEC-OUTPUT-TRUNCATION",
362 "EXEC-IDENTITY-GENERATION-BINDING",
363 ],
364 RuntimeConformanceProfile::Security => &[
365 "SECURITY-DIGEST-PINNING",
366 "SECURITY-METADATA-TAMPER",
367 "SECURITY-NAMESPACE-SEPARATION",
368 "SECURITY-LEAST-PRIVILEGE",
369 "SECURITY-HOSTILE-INPUT",
370 ],
371 RuntimeConformanceProfile::Outputs => &["OUTPUT-EXACT-BOUNDED", "OUTPUT-DIGEST-BINDING"],
372 RuntimeConformanceProfile::Evidence => &[
373 "EVIDENCE-SPEC-BINDING",
374 "EVIDENCE-SEMANTICS-PROFILE-BINDING",
375 ],
376 }
377}
378
379fn extend_capability_case_ids(
380 capabilities: &RuntimeCapabilities,
381 profile: RuntimeConformanceProfile,
382 case_ids: &mut BTreeSet<String>,
383) {
384 match profile {
385 RuntimeConformanceProfile::Networking => {
386 for mode in &capabilities.network_modes {
387 match mode {
388 NetworkMode::None => {
389 case_ids.insert("NETWORK-MODE-NONE".into());
390 case_ids.insert("NETWORK-OUTBOUND-DENIED".into());
391 }
392 NetworkMode::Outbound => {
393 case_ids.insert("NETWORK-MODE-OUTBOUND".into());
394 case_ids.insert("NETWORK-OUTBOUND-ALLOWED".into());
395 }
396 NetworkMode::Service => {
397 case_ids.insert("NETWORK-MODE-SERVICE".into());
398 case_ids.insert("NETWORK-LOOPBACK-PUBLICATION".into());
399 case_ids.insert("NETWORK-PORT-COLLISION".into());
400 if capabilities.supports_feature(RuntimeFeature::ServiceTcp) {
401 case_ids.insert("NETWORK-PROTOCOL-TCP".into());
402 }
403 if capabilities.supports_feature(RuntimeFeature::ServiceUdp) {
404 case_ids.insert("NETWORK-PROTOCOL-UDP".into());
405 }
406 }
407 }
408 }
409 }
410 RuntimeConformanceProfile::Mounts => {
411 for kind in &capabilities.mount_kinds {
412 case_ids.insert(
413 match kind {
414 MountKind::Artifact => "MOUNT-ARTIFACT-BEHAVIOR",
415 MountKind::Volume => "MOUNT-VOLUME-PERSISTENCE",
416 MountKind::Tmpfs => "MOUNT-TMPFS-ISOLATION",
417 }
418 .into(),
419 );
420 }
421 }
422 RuntimeConformanceProfile::Health => {
423 for kind in &capabilities.health_check_kinds {
424 case_ids.insert(
425 match kind {
426 HealthCheckKind::Http => "HEALTH-PROBE-HTTP",
427 HealthCheckKind::Tcp => "HEALTH-PROBE-TCP",
428 HealthCheckKind::Command => "HEALTH-PROBE-COMMAND",
429 }
430 .into(),
431 );
432 }
433 if capabilities.supports_feature(RuntimeFeature::ServiceLifecycle) {
434 case_ids.extend(
435 [
436 "HEALTH-READINESS-LIVENESS-SEPARATION",
437 "HEALTH-LIVENESS-TRANSITION",
438 "HEALTH-GRACEFUL-STOP",
439 "HEALTH-GRACE-DEADLINE-FORCE",
440 ]
441 .into_iter()
442 .map(str::to_owned),
443 );
444 }
445 }
446 RuntimeConformanceProfile::Resources => {
447 for control in &capabilities.resource_controls {
448 let (configuration, behavior) = match control {
449 ResourceControl::Cpu => ("RESOURCE-CPU-CONFIG", "RESOURCE-CPU-BEHAVIOR"),
450 ResourceControl::Memory => {
451 ("RESOURCE-MEMORY-CONFIG", "RESOURCE-MEMORY-BEHAVIOR")
452 }
453 ResourceControl::Pids => ("RESOURCE-PIDS-CONFIG", "RESOURCE-PIDS-BEHAVIOR"),
454 ResourceControl::EphemeralStorage => (
455 "RESOURCE-EPHEMERAL-STORAGE-CONFIG",
456 "RESOURCE-EPHEMERAL-STORAGE-BEHAVIOR",
457 ),
458 ResourceControl::ExecutionTimeout => (
459 "RESOURCE-EXECUTION-TIMEOUT-CONFIG",
460 "RESOURCE-EXECUTION-TIMEOUT-BEHAVIOR",
461 ),
462 };
463 case_ids.insert(configuration.into());
464 case_ids.insert(behavior.into());
465 }
466 }
467 RuntimeConformanceProfile::Base
468 | RuntimeConformanceProfile::Recovery
469 | RuntimeConformanceProfile::Logs
470 | RuntimeConformanceProfile::Exec
471 | RuntimeConformanceProfile::Security
472 | RuntimeConformanceProfile::Outputs
473 | RuntimeConformanceProfile::Evidence => {}
474 }
475}
476
477fn capability_claims(
478 capabilities: &RuntimeCapabilities,
479 profile: RuntimeConformanceProfile,
480) -> BTreeSet<String> {
481 match profile {
482 RuntimeConformanceProfile::Base => capabilities
483 .unit_classes
484 .iter()
485 .map(|class| format!("unit_class:{class:?}"))
486 .collect(),
487 RuntimeConformanceProfile::Recovery => BTreeSet::from(["feature:DurableIdentity".into()]),
488 RuntimeConformanceProfile::Networking => capabilities
489 .network_modes
490 .iter()
491 .map(|mode| format!("network_mode:{mode:?}"))
492 .collect(),
493 RuntimeConformanceProfile::Mounts => capabilities
494 .mount_kinds
495 .iter()
496 .map(|kind| format!("mount_kind:{kind:?}"))
497 .collect(),
498 RuntimeConformanceProfile::Health => {
499 let mut claims = capabilities
500 .health_check_kinds
501 .iter()
502 .map(|kind| format!("health_check:{kind:?}"))
503 .collect::<BTreeSet<_>>();
504 if capabilities.supports_feature(RuntimeFeature::ServiceLifecycle) {
505 claims.insert("feature:ServiceLifecycle".into());
506 }
507 claims
508 }
509 RuntimeConformanceProfile::Resources => capabilities
510 .resource_controls
511 .iter()
512 .map(|control| format!("resource_control:{control:?}"))
513 .collect(),
514 RuntimeConformanceProfile::Logs => BTreeSet::from(["feature:Logs".into()]),
515 RuntimeConformanceProfile::Exec => BTreeSet::from(["feature:Exec".into()]),
516 RuntimeConformanceProfile::Security => {
517 let mut claims = capabilities
518 .isolation_levels
519 .iter()
520 .map(|level| format!("isolation:{level:?}"))
521 .collect::<BTreeSet<_>>();
522 claims.insert("feature:DurableIdentity".into());
523 if capabilities.supports_feature(RuntimeFeature::SecretReferences) {
524 claims.insert("feature:SecretReferences".into());
525 }
526 claims
527 }
528 RuntimeConformanceProfile::Outputs => BTreeSet::from(["feature:OutputArtifacts".into()]),
529 RuntimeConformanceProfile::Evidence => [
530 RuntimeFeature::Usage,
531 RuntimeFeature::Attestation,
532 RuntimeFeature::IdentityAttachment,
533 ]
534 .into_iter()
535 .filter(|feature| capabilities.supports_feature(*feature))
536 .map(|feature| format!("feature:{feature:?}"))
537 .collect(),
538 }
539}