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