Skip to main content

a3s_box_runtime/a3s_runtime_driver/
mod.rs

1//! A3S Runtime provider adapter for the certified Box Sandbox backend.
2
3mod 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/// Host paths and bounds for one Box Runtime provider instance.
30#[derive(Debug, Clone)]
31pub struct BoxRuntimeDriverConfig {
32    /// Private A3S Box state root. Runtime records share its canonical
33    /// `boxes.json` store with CLI-created records but never adopt them.
34    pub home_dir: PathBuf,
35    /// Independent bound for one provider control-plane operation.
36    pub control_timeout: Duration,
37    /// Poll cadence while waiting for a finite Task to reach a terminal state.
38    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
51/// Concrete A3S Runtime driver backed only by Box's shared-kernel Sandbox.
52pub 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;