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