Skip to main content

a3s_runtime/contract/
capabilities.rs

1use super::{
2    HealthCheckKind, IsolationLevel, MountKind, NetworkMode, RuntimeUnitClass, RuntimeUnitSpec,
3};
4use crate::ProviderId;
5use serde::{Deserialize, Serialize};
6use std::collections::BTreeSet;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum ResourceControl {
11    Cpu,
12    Memory,
13    Pids,
14    EphemeralStorage,
15    ExecutionTimeout,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
19#[serde(rename_all = "snake_case")]
20pub enum RuntimeFeature {
21    DurableIdentity,
22    Stop,
23    Remove,
24    Logs,
25    Exec,
26    Usage,
27    Attestation,
28    SecretReferences,
29    OutputArtifacts,
30}
31
32/// Structured, provider-reported capabilities. Product-specific support
33/// predicates belong to the caller, not this protocol.
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(deny_unknown_fields)]
36pub struct RuntimeCapabilities {
37    pub schema: String,
38    pub provider_id: ProviderId,
39    pub provider_build: String,
40    pub unit_classes: Vec<RuntimeUnitClass>,
41    pub artifact_media_types: Vec<String>,
42    pub isolation_levels: Vec<IsolationLevel>,
43    pub network_modes: Vec<NetworkMode>,
44    pub mount_kinds: Vec<MountKind>,
45    pub health_check_kinds: Vec<HealthCheckKind>,
46    pub resource_controls: Vec<ResourceControl>,
47    pub features: Vec<RuntimeFeature>,
48}
49
50impl RuntimeCapabilities {
51    pub const SCHEMA: &'static str = "a3s.runtime.capabilities.v3";
52
53    pub fn validate(&self) -> Result<(), String> {
54        if self.schema != Self::SCHEMA {
55            return Err(format!(
56                "unsupported Runtime capabilities schema {:?}",
57                self.schema
58            ));
59        }
60        super::validate_nonempty("provider_build", &self.provider_build, 255)?;
61        if self.unit_classes.is_empty()
62            || self.artifact_media_types.is_empty()
63            || self.isolation_levels.is_empty()
64            || self.resource_controls.is_empty()
65        {
66            return Err("Runtime capabilities omit a required capability family".into());
67        }
68        ensure_unique("unit class", &self.unit_classes)?;
69        ensure_unique("artifact media type", &self.artifact_media_types)?;
70        ensure_unique("isolation level", &self.isolation_levels)?;
71        ensure_unique("network mode", &self.network_modes)?;
72        ensure_unique("mount kind", &self.mount_kinds)?;
73        ensure_unique("health check kind", &self.health_check_kinds)?;
74        ensure_unique("resource control", &self.resource_controls)?;
75        ensure_unique("feature", &self.features)?;
76        for media_type in &self.artifact_media_types {
77            super::validate_nonempty("artifact media type", media_type, 255)?;
78        }
79        Ok(())
80    }
81
82    pub fn supports_feature(&self, feature: RuntimeFeature) -> bool {
83        self.features.contains(&feature)
84    }
85
86    pub fn missing_for(&self, spec: &RuntimeUnitSpec) -> Result<Vec<String>, String> {
87        self.validate()?;
88        spec.validate()?;
89        let mut missing = Vec::new();
90        if !self.unit_classes.contains(&spec.class) {
91            missing.push(format!("unit_class:{:?}", spec.class));
92        }
93        if !self
94            .artifact_media_types
95            .contains(&spec.artifact.media_type)
96        {
97            missing.push(format!("artifact_media_type:{}", spec.artifact.media_type));
98        }
99        if !self.isolation_levels.contains(&spec.isolation) {
100            missing.push(format!("isolation:{:?}", spec.isolation));
101        }
102        if !self.network_modes.contains(&spec.network.mode) {
103            missing.push(format!("network_mode:{:?}", spec.network.mode));
104        }
105        for kind in spec.mounts.iter().map(|mount| mount.source.kind()) {
106            if !self.mount_kinds.contains(&kind) {
107                missing.push(format!("mount_kind:{kind:?}"));
108            }
109        }
110        if let Some(health) = &spec.health {
111            let kind = health.probe.kind();
112            if !self.health_check_kinds.contains(&kind) {
113                missing.push(format!("health_check:{kind:?}"));
114            }
115        }
116        for required in [
117            ResourceControl::Cpu,
118            ResourceControl::Memory,
119            ResourceControl::Pids,
120        ] {
121            if !self.resource_controls.contains(&required) {
122                missing.push(format!("resource_control:{required:?}"));
123            }
124        }
125        if spec.resources.ephemeral_storage_bytes.is_some()
126            && !self
127                .resource_controls
128                .contains(&ResourceControl::EphemeralStorage)
129        {
130            missing.push("resource_control:EphemeralStorage".into());
131        }
132        if spec.resources.execution_timeout_ms.is_some()
133            && !self
134                .resource_controls
135                .contains(&ResourceControl::ExecutionTimeout)
136        {
137            missing.push("resource_control:ExecutionTimeout".into());
138        }
139        if !self.supports_feature(RuntimeFeature::DurableIdentity) {
140            missing.push("feature:DurableIdentity".into());
141        }
142        if !spec.secrets.is_empty() && !self.supports_feature(RuntimeFeature::SecretReferences) {
143            missing.push("feature:SecretReferences".into());
144        }
145        if !spec.outputs.is_empty() && !self.supports_feature(RuntimeFeature::OutputArtifacts) {
146            missing.push("feature:OutputArtifacts".into());
147        }
148        if spec.isolation == IsolationLevel::Confidential
149            && !self.supports_feature(RuntimeFeature::Attestation)
150        {
151            missing.push("feature:Attestation".into());
152        }
153        missing.sort();
154        missing.dedup();
155        Ok(missing)
156    }
157}
158
159fn ensure_unique<T>(label: &str, values: &[T]) -> Result<(), String>
160where
161    T: Ord + Clone,
162{
163    let unique = values.iter().cloned().collect::<BTreeSet<_>>();
164    if unique.len() != values.len() {
165        return Err(format!(
166            "Runtime capabilities contain duplicate {label} values"
167        ));
168    }
169    Ok(())
170}