a3s_runtime/contract/
capabilities.rs1use 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#[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}