1mod artifact;
4mod attestation;
5mod exec;
6mod health;
7mod lifecycle;
8mod logs;
9mod mapping;
10mod metadata;
11mod secret;
12mod service_endpoints;
13mod volume_storage;
14
15use std::future::Future;
16use std::path::PathBuf;
17use std::sync::Arc;
18use std::time::Duration;
19
20use a3s_box_core::config::SevSnpGeneration;
21use a3s_box_core::ExecutionPortConnector;
22use a3s_runtime::contract::{
23 HealthCheckKind, IsolationLevel, MountKind, NetworkMode, ResourceControl, RuntimeActionRequest,
24 RuntimeCapabilities, RuntimeExecRequest, RuntimeExecResult, RuntimeFeature, RuntimeInspection,
25 RuntimeLogChunk, RuntimeLogQuery, RuntimeObservation, RuntimeRemoval, RuntimeUnitClass,
26 RuntimeUnitSpec,
27};
28use a3s_runtime::{ProviderId, RuntimeDriver, RuntimeError, RuntimeResult, RuntimeUnitRecord};
29use async_trait::async_trait;
30use tokio::sync::OnceCell;
31
32use crate::local_execution::TransientRegistryAuthBroker;
33use crate::tee::AttestationPolicy;
34#[cfg(any(test, feature = "runtime-provider-qualification"))]
35use crate::LocalExecutionBackend;
36use crate::{ExecutionIsolation, LocalExecutionManager, VmLocalExecutionBackend};
37
38use self::artifact::ArtifactStorageOwner;
39use self::attestation::AttestationArtifactOwner;
40use self::secret::SecretMaterializationOwner;
41use self::service_endpoints::ServiceEndpointOwner;
42
43pub use self::artifact::{BoxArtifactPort, BoxArtifactPortError};
44pub use self::secret::{
45 BoxRegistryCredential, BoxSecretEnvironmentProjection, BoxSecretMaterial,
46 BoxSecretMaterializationError, BoxSecretMaterializer, BoxTransientSecretStore,
47};
48
49pub(super) const OCI_IMAGE_MANIFEST: &str = "application/vnd.oci.image.manifest.v1+json";
50pub(super) const OCI_IMAGE_INDEX: &str = "application/vnd.oci.image.index.v1+json";
51
52#[derive(Debug, Clone)]
54pub struct BoxRuntimeDriverConfig {
55 pub home_dir: PathBuf,
58 pub control_timeout: Duration,
60 pub task_poll_interval: Duration,
62 pub secret_root: PathBuf,
66}
67
68impl Default for BoxRuntimeDriverConfig {
69 fn default() -> Self {
70 let home_dir = a3s_box_core::dirs_home();
71 Self {
72 secret_root: home_dir.join("runtime-secrets"),
73 home_dir,
74 control_timeout: Duration::from_secs(60),
75 task_poll_interval: Duration::from_millis(50),
76 }
77 }
78}
79
80#[derive(Debug, Clone, Default)]
85pub struct BoxRuntimeSevSnpConfig {
86 pub generation: SevSnpGeneration,
88 pub simulate: bool,
90 pub attestation_policy: AttestationPolicy,
92}
93
94pub struct BoxRuntimeDriver {
96 provider_id: ProviderId,
97 pub(super) config: BoxRuntimeDriverConfig,
98 pub(super) manager: LocalExecutionManager,
99 port_connector: Arc<dyn ExecutionPortConnector>,
100 service_endpoints: ServiceEndpointOwner,
101 execution_isolation: ExecutionIsolation,
102 sev_snp: Option<BoxRuntimeSevSnpConfig>,
103 attestation: AttestationArtifactOwner,
104 artifact_storage: ArtifactStorageOwner,
105 secret_materialization: SecretMaterializationOwner,
106 transient_registry_auth: Option<TransientRegistryAuthBroker>,
107 provider_build: OnceCell<String>,
108}
109
110impl BoxRuntimeDriver {
111 pub fn new(config: BoxRuntimeDriverConfig) -> RuntimeResult<Self> {
115 Self::new_with_isolation(config, ExecutionIsolation::Microvm)
116 }
117
118 pub fn new_confidential(
121 config: BoxRuntimeDriverConfig,
122 sev_snp: BoxRuntimeSevSnpConfig,
123 ) -> RuntimeResult<Self> {
124 validate_sev_snp_config(&sev_snp)?;
125 let mut driver = Self::new(config)?;
126 driver.sev_snp = Some(sev_snp);
127 Ok(driver)
128 }
129
130 pub fn new_with_isolation(
133 config: BoxRuntimeDriverConfig,
134 execution_isolation: ExecutionIsolation,
135 ) -> RuntimeResult<Self> {
136 validate_config(&config)?;
137 let (manager, broker) = production_manager(&config);
138 let connector: Arc<dyn ExecutionPortConnector> = Arc::new(manager.clone());
139 Self::with_manager_connector_and_materializer(
140 config,
141 manager,
142 connector,
143 execution_isolation,
144 None,
145 None,
146 Some(broker),
147 )
148 }
149
150 #[cfg(any(test, feature = "runtime-provider-qualification"))]
164 pub fn new_for_runtime_provider_qualification(
165 config: BoxRuntimeDriverConfig,
166 backend: Arc<dyn LocalExecutionBackend>,
167 port_connector: Arc<dyn ExecutionPortConnector>,
168 execution_isolation: ExecutionIsolation,
169 provider_build: impl Into<String>,
170 ) -> RuntimeResult<Self> {
171 let provider_build = provider_build.into();
172 validate_qualification_provider_build(&provider_build)?;
173 let manager = LocalExecutionManager::new(
174 config.home_dir.join("boxes.json"),
175 &config.home_dir,
176 backend,
177 );
178 let driver = Self::with_manager_connector_and_materializer(
179 config,
180 manager,
181 port_connector,
182 execution_isolation,
183 None,
184 None,
185 Some(TransientRegistryAuthBroker::default()),
186 )?;
187 driver.provider_build.set(provider_build).map_err(|_| {
188 RuntimeError::Protocol(
189 "Box Runtime qualification provider build was initialized twice".into(),
190 )
191 })?;
192 Ok(driver)
193 }
194
195 pub fn with_secret_materializer(
201 mut self,
202 materializer: Arc<dyn BoxSecretMaterializer>,
203 ) -> Self {
204 self.secret_materialization =
205 SecretMaterializationOwner::new(self.config.secret_root.clone(), Some(materializer));
206 self
207 }
208
209 pub fn with_artifact_port(mut self, port: Arc<dyn BoxArtifactPort>) -> Self {
215 self.artifact_storage = ArtifactStorageOwner::new(self.config.home_dir.clone(), Some(port));
216 self
217 }
218
219 fn with_manager_connector_and_materializer(
220 config: BoxRuntimeDriverConfig,
221 manager: LocalExecutionManager,
222 connector: Arc<dyn ExecutionPortConnector>,
223 execution_isolation: ExecutionIsolation,
224 materializer: Option<Arc<dyn BoxSecretMaterializer>>,
225 artifact_port: Option<Arc<dyn BoxArtifactPort>>,
226 transient_registry_auth: Option<TransientRegistryAuthBroker>,
227 ) -> RuntimeResult<Self> {
228 validate_config(&config)?;
229 let endpoint_connector = Arc::clone(&connector);
230 let secret_materialization =
231 SecretMaterializationOwner::new(config.secret_root.clone(), materializer);
232 let artifact_storage = ArtifactStorageOwner::new(config.home_dir.clone(), artifact_port);
233 Ok(Self {
234 provider_id: ProviderId::parse("a3s-box")?,
235 config,
236 manager,
237 port_connector: connector,
238 service_endpoints: ServiceEndpointOwner::new(endpoint_connector),
239 execution_isolation,
240 sev_snp: None,
241 attestation: AttestationArtifactOwner::default(),
242 artifact_storage,
243 secret_materialization,
244 transient_registry_auth,
245 provider_build: OnceCell::new(),
246 })
247 }
248
249 pub const fn execution_isolation(&self) -> ExecutionIsolation {
251 self.execution_isolation
252 }
253
254 pub(super) fn sev_snp_config(&self) -> Option<&BoxRuntimeSevSnpConfig> {
255 self.sev_snp.as_ref()
256 }
257
258 #[cfg(test)]
259 fn with_attestation_transport(
260 mut self,
261 transport: Arc<dyn self::attestation::BoxAttestationTransport>,
262 ) -> Self {
263 self.attestation = AttestationArtifactOwner::with_transport(transport);
264 self
265 }
266
267 #[cfg(test)]
268 fn with_attested_main_starter(
269 mut self,
270 starter: Arc<dyn self::attestation::BoxAttestedMainStarter>,
271 ) -> Self {
272 self.attestation = self.attestation.with_main_starter(starter);
273 self
274 }
275
276 pub(super) async fn provider_build(&self) -> RuntimeResult<String> {
277 self.provider_build
278 .get_or_try_init(|| async {
279 let execution_isolation = self.execution_isolation;
280 let sev_snp_simulated = self.sev_snp.as_ref().map(|config| config.simulate);
281 let provider_build = tokio::time::timeout(
282 self.config.control_timeout,
283 tokio::task::spawn_blocking(move || {
284 probe_provider_build(execution_isolation, sev_snp_simulated)
285 }),
286 )
287 .await
288 .map_err(|_| {
289 RuntimeError::ProviderUnavailable(
290 "Box provider capability probe exceeded the control timeout".into(),
291 )
292 })?
293 .map_err(|error| {
294 RuntimeError::ProviderUnavailable(format!(
295 "Box provider capability probe failed: {error}"
296 ))
297 })?
298 .map_err(RuntimeError::ProviderUnavailable)?;
299 Ok::<String, RuntimeError>(provider_build)
300 })
301 .await
302 .cloned()
303 }
304
305 pub(super) async fn bounded<T, F>(&self, operation: &'static str, future: F) -> RuntimeResult<T>
306 where
307 F: Future<Output = RuntimeResult<T>>,
308 {
309 tokio::time::timeout(self.config.control_timeout, future)
310 .await
311 .map_err(|_| {
312 RuntimeError::ProviderUnavailable(format!(
313 "Box {operation} exceeded the configured control timeout"
314 ))
315 })?
316 }
317}
318
319fn production_manager(
320 config: &BoxRuntimeDriverConfig,
321) -> (LocalExecutionManager, TransientRegistryAuthBroker) {
322 let broker = TransientRegistryAuthBroker::default();
323 let backend =
324 VmLocalExecutionBackend::new(&config.home_dir).with_transient_registry_auth(broker.clone());
325 let manager = LocalExecutionManager::new(
326 config.home_dir.join("boxes.json"),
327 &config.home_dir,
328 Arc::new(backend),
329 );
330 (manager, broker)
331}
332
333fn probe_provider_build(
334 execution_isolation: ExecutionIsolation,
335 sev_snp_simulated: Option<bool>,
336) -> Result<String, String> {
337 let provider_build = match execution_isolation {
338 ExecutionIsolation::Microvm => {
339 let support = crate::host_check::check_virtualization_support()
340 .map_err(|error| format!("microVM unavailable: {error}"))?;
341 if sev_snp_simulated == Some(false) {
342 let tee = crate::tee::check_sev_snp_support()
343 .map_err(|error| format!("SEV-SNP capability probe failed: {error}"))?;
344 if !tee.available {
345 return Err(format!(
346 "SEV-SNP unavailable: {}",
347 tee.reason.unwrap_or_else(|| "unknown reason".into())
348 ));
349 }
350 }
351 format!(
352 "a3s-box/{} isolation/microvm hypervisor/{}",
353 env!("CARGO_PKG_VERSION"),
354 support.backend
355 )
356 }
357 ExecutionIsolation::Sandbox => {
358 let snapshot = crate::sandbox::probe_sandbox_capabilities_for(
359 a3s_box_core::ExecutionBackend::A3sOci,
360 None,
361 None,
362 );
363 snapshot
364 .require_ready()
365 .map_err(|error| format!("shared-kernel backend unavailable: {error}"))?;
366 let runtime = snapshot.a3s_oci.ok_or_else(|| {
367 "shared-kernel capability probe returned no A3S OCI artifacts".to_string()
368 })?;
369 format!(
370 "a3s-box/{} isolation/sandbox a3s-oci/sha256:{} agent/sha256:{}",
371 env!("CARGO_PKG_VERSION"),
372 &runtime.runtime_sha256[..16],
373 &runtime.agent_sha256[..16]
374 )
375 }
376 };
377 Ok(match sev_snp_simulated {
378 Some(true) => format!("{provider_build} tee/sev-snp-simulated"),
379 Some(false) => format!("{provider_build} tee/sev-snp-hardware"),
380 None => provider_build,
381 })
382}
383
384fn validate_config(config: &BoxRuntimeDriverConfig) -> RuntimeResult<()> {
385 if !config.home_dir.is_absolute() {
386 return Err(RuntimeError::InvalidRequest(
387 "Box Runtime home directory must be absolute".into(),
388 ));
389 }
390 if config.control_timeout.is_zero() || config.task_poll_interval.is_zero() {
391 return Err(RuntimeError::InvalidRequest(
392 "Box Runtime timeout and poll interval must be positive".into(),
393 ));
394 }
395 let secret_root = config.secret_root.to_str().ok_or_else(|| {
396 RuntimeError::InvalidRequest(
397 "Box Runtime Secret root must be an encodable UTF-8 Linux path".into(),
398 )
399 })?;
400 let normalized_secret_root = secret_root.strip_prefix('/').is_some_and(|relative| {
401 !relative.is_empty()
402 && relative
403 .split('/')
404 .all(|segment| !segment.is_empty() && !matches!(segment, "." | ".."))
405 });
406 if !normalized_secret_root
407 || secret_root.contains([':', '\0'])
408 || secret_root.bytes().any(|byte| byte.is_ascii_control())
409 || !config.secret_root.is_absolute()
410 || config.secret_root.parent().is_none()
411 || config.secret_root.components().any(|component| {
412 matches!(
413 component,
414 std::path::Component::CurDir
415 | std::path::Component::ParentDir
416 | std::path::Component::Prefix(_)
417 )
418 })
419 {
420 return Err(RuntimeError::InvalidRequest(
421 "Box Runtime Secret root must be an encodable absolute normalized non-root Linux path"
422 .into(),
423 ));
424 }
425 Ok(())
426}
427
428#[cfg(any(test, feature = "runtime-provider-qualification"))]
429fn validate_qualification_provider_build(provider_build: &str) -> RuntimeResult<()> {
430 if provider_build.is_empty()
431 || provider_build.len() > 255
432 || provider_build.trim() != provider_build
433 || provider_build.bytes().any(|byte| byte.is_ascii_control())
434 {
435 return Err(RuntimeError::InvalidRequest(
436 "Box Runtime qualification provider build must be a trimmed non-empty string of at most 255 bytes"
437 .into(),
438 ));
439 }
440 Ok(())
441}
442
443fn validate_sev_snp_config(config: &BoxRuntimeSevSnpConfig) -> RuntimeResult<()> {
444 if config
445 .attestation_policy
446 .expected_measurement
447 .as_ref()
448 .is_some_and(|measurement| {
449 measurement.len() != 96
450 || !measurement
451 .bytes()
452 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
453 })
454 {
455 return Err(RuntimeError::InvalidRequest(
456 "Box Runtime SEV-SNP measurement must be a canonical lowercase SHA-384 hex value"
457 .into(),
458 ));
459 }
460 if config.attestation_policy.max_report_age_secs.is_some() {
461 return Err(RuntimeError::InvalidRequest(
462 "Box Runtime SEV-SNP RA-TLS artifacts do not support report-age policy".into(),
463 ));
464 }
465 Ok(())
466}
467
468#[async_trait]
469impl RuntimeDriver for BoxRuntimeDriver {
470 fn provider_id(&self) -> &ProviderId {
471 &self.provider_id
472 }
473
474 async fn capabilities(&self) -> RuntimeResult<RuntimeCapabilities> {
475 if self.secret_materialization.configured() {
476 self.secret_materialization.require_ready().await?;
477 }
478 let mut features = vec![
479 RuntimeFeature::DurableIdentity,
480 RuntimeFeature::Stop,
481 RuntimeFeature::Remove,
482 RuntimeFeature::ServiceTcp,
483 RuntimeFeature::Logs,
484 RuntimeFeature::Exec,
485 ];
486 if self.secret_materialization.configured() {
487 features.push(RuntimeFeature::SecretReferences);
488 }
489 if self.artifact_storage.artifact_configured() {
490 features.push(RuntimeFeature::OutputArtifacts);
491 }
492 if self.sev_snp.is_some() {
493 features.push(RuntimeFeature::Attestation);
494 }
495 let mut mount_kinds = vec![MountKind::Volume, MountKind::Tmpfs];
496 if self.artifact_storage.artifact_configured() {
497 mount_kinds.insert(0, MountKind::Artifact);
498 }
499 let mut isolation_levels = vec![IsolationLevel::Sandbox];
500 if self.sev_snp.is_some() {
501 isolation_levels.push(IsolationLevel::Confidential);
502 }
503 let capabilities = RuntimeCapabilities {
504 schema: RuntimeCapabilities::SCHEMA.into(),
505 provider_id: self.provider_id.clone(),
506 provider_build: self.provider_build().await?,
507 unit_classes: vec![RuntimeUnitClass::Task, RuntimeUnitClass::Service],
508 artifact_media_types: vec![OCI_IMAGE_MANIFEST.into(), OCI_IMAGE_INDEX.into()],
509 isolation_levels,
512 network_modes: vec![NetworkMode::None, NetworkMode::Service],
513 mount_kinds,
514 health_check_kinds: vec![
515 HealthCheckKind::Http,
516 HealthCheckKind::Tcp,
517 HealthCheckKind::Command,
518 ],
519 resource_controls: vec![
520 ResourceControl::Cpu,
521 ResourceControl::Memory,
522 ResourceControl::Pids,
523 ResourceControl::ExecutionTimeout,
524 ],
525 features,
526 };
527 capabilities.validate().map_err(RuntimeError::Protocol)?;
528 Ok(capabilities)
529 }
530
531 async fn apply(
532 &self,
533 spec: &RuntimeUnitSpec,
534 current: &RuntimeObservation,
535 ) -> RuntimeResult<RuntimeObservation> {
536 self.apply_unit(spec, current).await
537 }
538
539 async fn inspect(&self, unit: &RuntimeUnitRecord) -> RuntimeResult<RuntimeInspection> {
540 self.bounded("inspection", self.inspect_unit(unit)).await
541 }
542
543 async fn stop(
544 &self,
545 unit: &RuntimeUnitRecord,
546 request: &RuntimeActionRequest,
547 ) -> RuntimeResult<RuntimeObservation> {
548 self.bounded("stop", self.stop_unit(unit, request)).await
549 }
550
551 async fn remove(
552 &self,
553 unit: &RuntimeUnitRecord,
554 request: &RuntimeActionRequest,
555 ) -> RuntimeResult<RuntimeRemoval> {
556 self.bounded("remove", self.remove_unit(unit, request))
557 .await
558 }
559
560 async fn logs(
561 &self,
562 unit: &RuntimeUnitRecord,
563 query: &RuntimeLogQuery,
564 ) -> RuntimeResult<Vec<RuntimeLogChunk>> {
565 self.bounded("log read", self.read_runtime_logs(unit, query))
566 .await
567 }
568
569 async fn exec(
570 &self,
571 unit: &RuntimeUnitRecord,
572 request: &RuntimeExecRequest,
573 ) -> RuntimeResult<RuntimeExecResult> {
574 self.execute_runtime_command(unit, request).await
575 }
576}
577
578#[cfg(test)]
579mod artifact_tests;
580#[cfg(test)]
581mod conformance_tests;
582#[cfg(test)]
583mod exec_integration_tests;
584#[cfg(test)]
585mod lifecycle_tests;
586#[cfg(test)]
587mod service_endpoint_tests;
588#[cfg(test)]
589mod test_support;
590#[cfg(test)]
591mod tests;