Skip to main content

a3s_runtime/conformance/
profiles.rs

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/// Composable provider conformance profiles owned by the shared Runtime suite.
10#[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/// Exact shared case IDs and capability claims required for one profile.
44#[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/// Provider evidence returned for one non-Base profile.
52#[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/// Canonical pre/post provider inventory. Keys can represent provider
60/// resources, volumes, ports, mounts, processes, or other retained objects;
61/// values are provider-owned stable state digests.
62#[derive(Debug, Clone, Default, PartialEq, Eq)]
63pub struct RuntimeConformanceInventory {
64    pub entries: BTreeMap<String, String>,
65}
66
67/// Complete report for one profile-driven provider certification run.
68#[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/// Provider integration boundary for destructive, capability-specific
77/// fixtures. No method has a success-by-default implementation: a production
78/// provider must explicitly declare, execute, clean up, and inventory every
79/// activated profile.
80#[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
98/// Derives mandatory and capability-triggered profiles from source-reported
99/// capabilities. Base and Recovery are unconditional.
100pub 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
143/// Returns the stable shared requirements a provider evidence record must
144/// cover for the selected profile and capability set.
145pub 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
184/// Runs Base internally, activates every required/declared profile, validates
185/// typed evidence, always requests provider cleanup, and rejects any inventory
186/// delta.
187pub 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}