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    {
137        profiles.insert(RuntimeConformanceProfile::Evidence);
138    }
139    Ok(profiles)
140}
141
142/// Returns the stable shared requirements a provider evidence record must
143/// cover for the selected profile and capability set.
144pub 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
178/// Runs Base internally, activates every required/declared profile, validates
179/// typed evidence, always requests provider cleanup, and rejects any inventory
180/// delta.
181pub 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}