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 Logs,
25 Exec,
26 Usage,
27 Attestation,
28 SecretReferences,
29 OutputArtifacts,
30}
31
32#[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}