a3s_box_runtime/a3s_runtime_driver/
mod.rs1mod exec;
4mod lifecycle;
5mod logs;
6mod mapping;
7mod metadata;
8
9use std::future::Future;
10use std::path::PathBuf;
11use std::time::Duration;
12
13use a3s_runtime::contract::{
14 IsolationLevel, MountKind, NetworkMode, ResourceControl, RuntimeActionRequest,
15 RuntimeCapabilities, RuntimeExecRequest, RuntimeExecResult, RuntimeFeature, RuntimeInspection,
16 RuntimeLogChunk, RuntimeLogQuery, RuntimeObservation, RuntimeRemoval, RuntimeUnitClass,
17 RuntimeUnitSpec,
18};
19use a3s_runtime::{ProviderId, RuntimeDriver, RuntimeError, RuntimeResult, RuntimeUnitRecord};
20use async_trait::async_trait;
21use tokio::sync::OnceCell;
22
23use crate::LocalExecutionManager;
24
25pub(super) const OCI_IMAGE_MANIFEST: &str = "application/vnd.oci.image.manifest.v1+json";
26pub(super) const DOCKER_IMAGE_MANIFEST: &str =
27 "application/vnd.docker.distribution.manifest.v2+json";
28
29#[derive(Debug, Clone)]
31pub struct BoxRuntimeDriverConfig {
32 pub home_dir: PathBuf,
35 pub control_timeout: Duration,
37 pub task_poll_interval: Duration,
39}
40
41impl Default for BoxRuntimeDriverConfig {
42 fn default() -> Self {
43 Self {
44 home_dir: a3s_box_core::dirs_home(),
45 control_timeout: Duration::from_secs(60),
46 task_poll_interval: Duration::from_millis(50),
47 }
48 }
49}
50
51pub struct BoxRuntimeDriver {
53 provider_id: ProviderId,
54 pub(super) config: BoxRuntimeDriverConfig,
55 pub(super) manager: LocalExecutionManager,
56 provider_build: OnceCell<String>,
57}
58
59impl BoxRuntimeDriver {
60 pub fn new(config: BoxRuntimeDriverConfig) -> RuntimeResult<Self> {
61 validate_config(&config)?;
62 let manager = LocalExecutionManager::with_vm_backend(
63 config.home_dir.join("boxes.json"),
64 &config.home_dir,
65 );
66 Self::with_manager(config, manager)
67 }
68
69 fn with_manager(
70 config: BoxRuntimeDriverConfig,
71 manager: LocalExecutionManager,
72 ) -> RuntimeResult<Self> {
73 validate_config(&config)?;
74 Ok(Self {
75 provider_id: ProviderId::parse("a3s-box")?,
76 config,
77 manager,
78 provider_build: OnceCell::new(),
79 })
80 }
81
82 pub(super) async fn provider_build(&self) -> RuntimeResult<String> {
83 self.provider_build
84 .get_or_try_init(|| async {
85 let snapshot = tokio::time::timeout(
86 self.config.control_timeout,
87 tokio::task::spawn_blocking(|| {
88 crate::sandbox::probe_sandbox_capabilities(None)
89 }),
90 )
91 .await
92 .map_err(|_| {
93 RuntimeError::ProviderUnavailable(
94 "Box Sandbox capability probe exceeded the control timeout".into(),
95 )
96 })?
97 .map_err(|error| {
98 RuntimeError::ProviderUnavailable(format!(
99 "Box Sandbox capability probe failed: {error}"
100 ))
101 })?;
102 snapshot
103 .require_ready()
104 .map_err(|error| RuntimeError::ProviderUnavailable(error.to_string()))?;
105 let runtime = snapshot.runtime.ok_or_else(|| {
106 RuntimeError::ProviderUnavailable(
107 "Box Sandbox capability probe returned no certified crun runtime".into(),
108 )
109 })?;
110 Ok::<String, RuntimeError>(format!(
111 "a3s-box/{} crun/{} sha256:{}",
112 env!("CARGO_PKG_VERSION"),
113 runtime.version,
114 &runtime.sha256[..16]
115 ))
116 })
117 .await
118 .cloned()
119 }
120
121 pub(super) async fn bounded<T, F>(&self, operation: &'static str, future: F) -> RuntimeResult<T>
122 where
123 F: Future<Output = RuntimeResult<T>>,
124 {
125 tokio::time::timeout(self.config.control_timeout, future)
126 .await
127 .map_err(|_| {
128 RuntimeError::ProviderUnavailable(format!(
129 "Box {operation} exceeded the configured control timeout"
130 ))
131 })?
132 }
133}
134
135fn validate_config(config: &BoxRuntimeDriverConfig) -> RuntimeResult<()> {
136 if !config.home_dir.is_absolute() {
137 return Err(RuntimeError::InvalidRequest(
138 "Box Runtime home directory must be absolute".into(),
139 ));
140 }
141 if config.control_timeout.is_zero() || config.task_poll_interval.is_zero() {
142 return Err(RuntimeError::InvalidRequest(
143 "Box Runtime timeout and poll interval must be positive".into(),
144 ));
145 }
146 Ok(())
147}
148
149#[async_trait]
150impl RuntimeDriver for BoxRuntimeDriver {
151 fn provider_id(&self) -> &ProviderId {
152 &self.provider_id
153 }
154
155 async fn capabilities(&self) -> RuntimeResult<RuntimeCapabilities> {
156 let capabilities = RuntimeCapabilities {
157 schema: RuntimeCapabilities::SCHEMA.into(),
158 provider_id: self.provider_id.clone(),
159 provider_build: self.provider_build().await?,
160 unit_classes: vec![RuntimeUnitClass::Task, RuntimeUnitClass::Service],
161 artifact_media_types: vec![OCI_IMAGE_MANIFEST.into(), DOCKER_IMAGE_MANIFEST.into()],
162 isolation_levels: vec![IsolationLevel::Sandbox],
163 network_modes: vec![NetworkMode::None],
164 mount_kinds: vec![MountKind::Tmpfs],
165 health_check_kinds: Vec::new(),
166 resource_controls: vec![
167 ResourceControl::Cpu,
168 ResourceControl::Memory,
169 ResourceControl::Pids,
170 ResourceControl::ExecutionTimeout,
171 ],
172 features: vec![
173 RuntimeFeature::DurableIdentity,
174 RuntimeFeature::Stop,
175 RuntimeFeature::Remove,
176 RuntimeFeature::Logs,
177 RuntimeFeature::Exec,
178 ],
179 };
180 capabilities.validate().map_err(RuntimeError::Protocol)?;
181 Ok(capabilities)
182 }
183
184 async fn apply(
185 &self,
186 spec: &RuntimeUnitSpec,
187 current: &RuntimeObservation,
188 ) -> RuntimeResult<RuntimeObservation> {
189 self.apply_unit(spec, current).await
190 }
191
192 async fn inspect(&self, unit: &RuntimeUnitRecord) -> RuntimeResult<RuntimeInspection> {
193 self.bounded("inspection", self.inspect_unit(unit)).await
194 }
195
196 async fn stop(
197 &self,
198 unit: &RuntimeUnitRecord,
199 request: &RuntimeActionRequest,
200 ) -> RuntimeResult<RuntimeObservation> {
201 self.bounded("stop", self.stop_unit(unit, request)).await
202 }
203
204 async fn remove(
205 &self,
206 unit: &RuntimeUnitRecord,
207 request: &RuntimeActionRequest,
208 ) -> RuntimeResult<RuntimeRemoval> {
209 self.bounded("remove", self.remove_unit(unit, request))
210 .await
211 }
212
213 async fn logs(
214 &self,
215 unit: &RuntimeUnitRecord,
216 query: &RuntimeLogQuery,
217 ) -> RuntimeResult<Vec<RuntimeLogChunk>> {
218 self.bounded("log read", self.read_runtime_logs(unit, query))
219 .await
220 }
221
222 async fn exec(
223 &self,
224 unit: &RuntimeUnitRecord,
225 request: &RuntimeExecRequest,
226 ) -> RuntimeResult<RuntimeExecResult> {
227 self.execute_runtime_command(unit, request).await
228 }
229}
230
231#[cfg(test)]
232mod conformance_tests;
233#[cfg(test)]
234mod exec_integration_tests;
235#[cfg(test)]
236mod lifecycle_tests;
237#[cfg(test)]
238mod test_support;
239#[cfg(test)]
240mod tests;