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::{BoxRecord, 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 supports_ephemeral_storage: bool,
106 sev_snp: Option<BoxRuntimeSevSnpConfig>,
107 attestation: AttestationArtifactOwner,
108 artifact_storage: ArtifactStorageOwner,
109 secret_materialization: SecretMaterializationOwner,
110 transient_registry_auth: Option<TransientRegistryAuthBroker>,
111 provider_build: OnceCell<String>,
112}
113
114impl BoxRuntimeDriver {
115 pub fn new(config: BoxRuntimeDriverConfig) -> RuntimeResult<Self> {
119 Self::new_with_isolation(config, ExecutionIsolation::Microvm)
120 }
121
122 pub fn new_confidential(
125 config: BoxRuntimeDriverConfig,
126 sev_snp: BoxRuntimeSevSnpConfig,
127 ) -> RuntimeResult<Self> {
128 validate_sev_snp_config(&sev_snp)?;
129 let mut driver = Self::new(config)?;
130 driver.sev_snp = Some(sev_snp);
131 Ok(driver)
132 }
133
134 pub fn new_with_isolation(
137 config: BoxRuntimeDriverConfig,
138 execution_isolation: ExecutionIsolation,
139 ) -> RuntimeResult<Self> {
140 validate_config(&config)?;
141 let (manager, broker) = production_manager(&config);
142 let connector: Arc<dyn ExecutionPortConnector> = Arc::new(manager.clone());
143 Self::with_manager_connector_and_materializer(
144 config,
145 manager,
146 connector,
147 execution_isolation,
148 None,
149 None,
150 Some(broker),
151 execution_isolation == ExecutionIsolation::Sandbox
152 && crate::rootfs::writable_layer_quota_supported(),
153 )
154 }
155
156 #[cfg(any(test, feature = "runtime-provider-qualification"))]
170 pub fn new_for_runtime_provider_qualification(
171 config: BoxRuntimeDriverConfig,
172 backend: Arc<dyn LocalExecutionBackend>,
173 port_connector: Arc<dyn ExecutionPortConnector>,
174 execution_isolation: ExecutionIsolation,
175 provider_build: impl Into<String>,
176 ) -> RuntimeResult<Self> {
177 let provider_build = provider_build.into();
178 validate_qualification_provider_build(&provider_build)?;
179 let manager = LocalExecutionManager::new(
180 config.home_dir.join("boxes.json"),
181 &config.home_dir,
182 backend,
183 );
184 let driver = Self::with_manager_connector_and_materializer(
185 config,
186 manager,
187 port_connector,
188 execution_isolation,
189 None,
190 None,
191 Some(TransientRegistryAuthBroker::default()),
192 false,
193 )?;
194 driver.provider_build.set(provider_build).map_err(|_| {
195 RuntimeError::Protocol(
196 "Box Runtime qualification provider build was initialized twice".into(),
197 )
198 })?;
199 Ok(driver)
200 }
201
202 pub fn with_secret_materializer(
208 mut self,
209 materializer: Arc<dyn BoxSecretMaterializer>,
210 ) -> Self {
211 self.secret_materialization =
212 SecretMaterializationOwner::new(self.config.secret_root.clone(), Some(materializer));
213 self
214 }
215
216 pub fn with_artifact_port(mut self, port: Arc<dyn BoxArtifactPort>) -> Self {
222 self.artifact_storage = ArtifactStorageOwner::new(self.config.home_dir.clone(), Some(port));
223 self
224 }
225
226 #[allow(clippy::too_many_arguments)]
227 fn with_manager_connector_and_materializer(
228 config: BoxRuntimeDriverConfig,
229 manager: LocalExecutionManager,
230 connector: Arc<dyn ExecutionPortConnector>,
231 execution_isolation: ExecutionIsolation,
232 materializer: Option<Arc<dyn BoxSecretMaterializer>>,
233 artifact_port: Option<Arc<dyn BoxArtifactPort>>,
234 transient_registry_auth: Option<TransientRegistryAuthBroker>,
235 supports_ephemeral_storage: bool,
236 ) -> RuntimeResult<Self> {
237 validate_config(&config)?;
238 let endpoint_connector = Arc::clone(&connector);
239 let secret_materialization =
240 SecretMaterializationOwner::new(config.secret_root.clone(), materializer);
241 let artifact_storage = ArtifactStorageOwner::new(config.home_dir.clone(), artifact_port);
242 Ok(Self {
243 provider_id: ProviderId::parse("a3s-box")?,
244 config,
245 manager,
246 port_connector: connector,
247 service_endpoints: ServiceEndpointOwner::new(endpoint_connector),
248 execution_isolation,
249 supports_ephemeral_storage,
250 sev_snp: None,
251 attestation: AttestationArtifactOwner::default(),
252 artifact_storage,
253 secret_materialization,
254 transient_registry_auth,
255 provider_build: OnceCell::new(),
256 })
257 }
258
259 pub const fn execution_isolation(&self) -> ExecutionIsolation {
261 self.execution_isolation
262 }
263
264 pub(super) fn sev_snp_config(&self) -> Option<&BoxRuntimeSevSnpConfig> {
265 self.sev_snp.as_ref()
266 }
267
268 #[cfg(test)]
269 fn with_attestation_transport(
270 mut self,
271 transport: Arc<dyn self::attestation::BoxAttestationTransport>,
272 ) -> Self {
273 self.attestation = AttestationArtifactOwner::with_transport(transport);
274 self
275 }
276
277 #[cfg(test)]
278 fn with_attested_main_starter(
279 mut self,
280 starter: Arc<dyn self::attestation::BoxAttestedMainStarter>,
281 ) -> Self {
282 self.attestation = self.attestation.with_main_starter(starter);
283 self
284 }
285
286 pub(super) async fn provider_build(&self) -> RuntimeResult<String> {
287 self.provider_build
288 .get_or_try_init(|| async {
289 let execution_isolation = self.execution_isolation;
290 let sev_snp_simulated = self.sev_snp.as_ref().map(|config| config.simulate);
291 let provider_build = tokio::time::timeout(
292 self.config.control_timeout,
293 tokio::task::spawn_blocking(move || {
294 probe_provider_build(execution_isolation, sev_snp_simulated)
295 }),
296 )
297 .await
298 .map_err(|_| {
299 RuntimeError::ProviderUnavailable(
300 "Box provider capability probe exceeded the control timeout".into(),
301 )
302 })?
303 .map_err(|error| {
304 RuntimeError::ProviderUnavailable(format!(
305 "Box provider capability probe failed: {error}"
306 ))
307 })?
308 .map_err(RuntimeError::ProviderUnavailable)?;
309 Ok::<String, RuntimeError>(provider_build)
310 })
311 .await
312 .cloned()
313 }
314
315 pub(super) async fn bounded<T, F>(&self, operation: &'static str, future: F) -> RuntimeResult<T>
316 where
317 F: Future<Output = RuntimeResult<T>>,
318 {
319 tokio::time::timeout(self.config.control_timeout, future)
320 .await
321 .map_err(|_| {
322 RuntimeError::ProviderUnavailable(format!(
323 "Box {operation} exceeded the configured control timeout"
324 ))
325 })?
326 }
327
328 pub(super) async fn bounded_lifecycle<T, F>(
333 &self,
334 spec: &RuntimeUnitSpec,
335 operation: &'static str,
336 future: F,
337 ) -> RuntimeResult<T>
338 where
339 F: Future<Output = RuntimeResult<T>>,
340 {
341 let timeout = self.lifecycle_control_timeout(spec);
342 tokio::time::timeout(timeout, future).await.map_err(|_| {
343 RuntimeError::ProviderUnavailable(format!(
344 "Box {operation} exceeded the lifecycle-aware control timeout"
345 ))
346 })?
347 }
348
349 fn lifecycle_control_timeout(&self, spec: &RuntimeUnitSpec) -> Duration {
350 let graceful_shutdown_seconds = spec
351 .service_lifecycle
352 .as_ref()
353 .map_or(0, |lifecycle| u64::from(lifecycle.shutdown_grace_seconds));
354 self.control_timeout_with_grace(graceful_shutdown_seconds)
355 }
356
357 pub(super) async fn bounded_record_lifecycle<T, F>(
358 &self,
359 record: &BoxRecord,
360 operation: &'static str,
361 future: F,
362 ) -> RuntimeResult<T>
363 where
364 F: Future<Output = RuntimeResult<T>>,
365 {
366 let timeout = self.control_timeout_with_grace(record.stop_timeout.unwrap_or(0));
367 tokio::time::timeout(timeout, future).await.map_err(|_| {
368 RuntimeError::ProviderUnavailable(format!(
369 "Box {operation} exceeded the lifecycle-aware control timeout"
370 ))
371 })?
372 }
373
374 fn control_timeout_with_grace(&self, graceful_shutdown_seconds: u64) -> Duration {
375 self.config
376 .control_timeout
377 .saturating_add(Duration::from_secs(graceful_shutdown_seconds))
378 }
379}
380
381fn production_manager(
382 config: &BoxRuntimeDriverConfig,
383) -> (LocalExecutionManager, TransientRegistryAuthBroker) {
384 let broker = TransientRegistryAuthBroker::default();
385 let backend =
386 VmLocalExecutionBackend::new(&config.home_dir).with_transient_registry_auth(broker.clone());
387 let manager = LocalExecutionManager::new(
388 config.home_dir.join("boxes.json"),
389 &config.home_dir,
390 Arc::new(backend),
391 );
392 (manager, broker)
393}
394
395fn probe_provider_build(
396 execution_isolation: ExecutionIsolation,
397 sev_snp_simulated: Option<bool>,
398) -> Result<String, String> {
399 let provider_build = match execution_isolation {
400 ExecutionIsolation::Microvm => {
401 let support = crate::host_check::check_virtualization_support()
402 .map_err(|error| format!("microVM unavailable: {error}"))?;
403 if sev_snp_simulated == Some(false) {
404 let tee = crate::tee::check_sev_snp_support()
405 .map_err(|error| format!("SEV-SNP capability probe failed: {error}"))?;
406 if !tee.available {
407 return Err(format!(
408 "SEV-SNP unavailable: {}",
409 tee.reason.unwrap_or_else(|| "unknown reason".into())
410 ));
411 }
412 }
413 format!(
414 "a3s-box/{} isolation/microvm hypervisor/{}",
415 env!("CARGO_PKG_VERSION"),
416 support.backend
417 )
418 }
419 ExecutionIsolation::Sandbox => {
420 let snapshot = crate::sandbox::probe_sandbox_capabilities_for(
421 a3s_box_core::ExecutionBackend::A3sOci,
422 None,
423 None,
424 );
425 snapshot
426 .require_ready()
427 .map_err(|error| format!("shared-kernel backend unavailable: {error}"))?;
428 let runtime = snapshot.a3s_oci.ok_or_else(|| {
429 "shared-kernel capability probe returned no A3S OCI artifacts".to_string()
430 })?;
431 format!(
432 "a3s-box/{} isolation/sandbox a3s-oci/sha256:{} agent/sha256:{}",
433 env!("CARGO_PKG_VERSION"),
434 &runtime.runtime_sha256[..16],
435 &runtime.agent_sha256[..16]
436 )
437 }
438 };
439 Ok(match sev_snp_simulated {
440 Some(true) => format!("{provider_build} tee/sev-snp-simulated"),
441 Some(false) => format!("{provider_build} tee/sev-snp-hardware"),
442 None => provider_build,
443 })
444}
445
446fn validate_config(config: &BoxRuntimeDriverConfig) -> RuntimeResult<()> {
447 if !config.home_dir.is_absolute() {
448 return Err(RuntimeError::InvalidRequest(
449 "Box Runtime home directory must be absolute".into(),
450 ));
451 }
452 if config.control_timeout.is_zero() || config.task_poll_interval.is_zero() {
453 return Err(RuntimeError::InvalidRequest(
454 "Box Runtime timeout and poll interval must be positive".into(),
455 ));
456 }
457 let secret_root = config.secret_root.to_str().ok_or_else(|| {
458 RuntimeError::InvalidRequest(
459 "Box Runtime Secret root must be an encodable UTF-8 Linux path".into(),
460 )
461 })?;
462 let normalized_secret_root = secret_root.strip_prefix('/').is_some_and(|relative| {
463 !relative.is_empty()
464 && relative
465 .split('/')
466 .all(|segment| !segment.is_empty() && !matches!(segment, "." | ".."))
467 });
468 if !normalized_secret_root
469 || secret_root.contains([':', '\0'])
470 || secret_root.bytes().any(|byte| byte.is_ascii_control())
471 || !config.secret_root.is_absolute()
472 || config.secret_root.parent().is_none()
473 || config.secret_root.components().any(|component| {
474 matches!(
475 component,
476 std::path::Component::CurDir
477 | std::path::Component::ParentDir
478 | std::path::Component::Prefix(_)
479 )
480 })
481 {
482 return Err(RuntimeError::InvalidRequest(
483 "Box Runtime Secret root must be an encodable absolute normalized non-root Linux path"
484 .into(),
485 ));
486 }
487 Ok(())
488}
489
490#[cfg(any(test, feature = "runtime-provider-qualification"))]
491fn validate_qualification_provider_build(provider_build: &str) -> RuntimeResult<()> {
492 if provider_build.is_empty()
493 || provider_build.len() > 255
494 || provider_build.trim() != provider_build
495 || provider_build.bytes().any(|byte| byte.is_ascii_control())
496 {
497 return Err(RuntimeError::InvalidRequest(
498 "Box Runtime qualification provider build must be a trimmed non-empty string of at most 255 bytes"
499 .into(),
500 ));
501 }
502 Ok(())
503}
504
505fn validate_sev_snp_config(config: &BoxRuntimeSevSnpConfig) -> RuntimeResult<()> {
506 if config
507 .attestation_policy
508 .expected_measurement
509 .as_ref()
510 .is_some_and(|measurement| {
511 measurement.len() != 96
512 || !measurement
513 .bytes()
514 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
515 })
516 {
517 return Err(RuntimeError::InvalidRequest(
518 "Box Runtime SEV-SNP measurement must be a canonical lowercase SHA-384 hex value"
519 .into(),
520 ));
521 }
522 if config.attestation_policy.max_report_age_secs.is_some() {
523 return Err(RuntimeError::InvalidRequest(
524 "Box Runtime SEV-SNP RA-TLS artifacts do not support report-age policy".into(),
525 ));
526 }
527 Ok(())
528}
529
530#[async_trait]
531impl RuntimeDriver for BoxRuntimeDriver {
532 fn provider_id(&self) -> &ProviderId {
533 &self.provider_id
534 }
535
536 async fn capabilities(&self) -> RuntimeResult<RuntimeCapabilities> {
537 if self.secret_materialization.configured() {
538 self.secret_materialization.require_ready().await?;
539 }
540 let mut features = vec![
541 RuntimeFeature::DurableIdentity,
542 RuntimeFeature::Stop,
543 RuntimeFeature::Remove,
544 RuntimeFeature::ServiceTcp,
545 RuntimeFeature::Logs,
546 RuntimeFeature::Exec,
547 RuntimeFeature::ServiceLifecycle,
548 ];
549 if self.secret_materialization.configured() {
550 features.push(RuntimeFeature::SecretReferences);
551 }
552 if self.artifact_storage.artifact_configured() {
553 features.push(RuntimeFeature::OutputArtifacts);
554 }
555 if self.sev_snp.is_some() {
556 features.push(RuntimeFeature::Attestation);
557 features.push(RuntimeFeature::IdentityAttachment);
558 }
559 let mut mount_kinds = vec![MountKind::Volume, MountKind::Tmpfs];
560 if self.artifact_storage.artifact_configured() {
561 mount_kinds.insert(0, MountKind::Artifact);
562 }
563 let mut isolation_levels = vec![IsolationLevel::Sandbox];
564 if self.sev_snp.is_some() {
565 isolation_levels.push(IsolationLevel::Confidential);
566 }
567 let mut resource_controls = vec![
568 ResourceControl::Cpu,
569 ResourceControl::Memory,
570 ResourceControl::Pids,
571 ];
572 if self.supports_ephemeral_storage {
573 resource_controls.push(ResourceControl::EphemeralStorage);
574 }
575 resource_controls.push(ResourceControl::ExecutionTimeout);
576 let capabilities = RuntimeCapabilities {
577 schema: RuntimeCapabilities::SCHEMA.into(),
578 provider_id: self.provider_id.clone(),
579 provider_build: self.provider_build().await?,
580 unit_classes: vec![RuntimeUnitClass::Task, RuntimeUnitClass::Service],
581 artifact_media_types: vec![OCI_IMAGE_MANIFEST.into(), OCI_IMAGE_INDEX.into()],
582 isolation_levels,
585 network_modes: vec![NetworkMode::None, NetworkMode::Service],
586 mount_kinds,
587 health_check_kinds: vec![
588 HealthCheckKind::Http,
589 HealthCheckKind::Tcp,
590 HealthCheckKind::Command,
591 ],
592 resource_controls,
593 features,
594 };
595 capabilities.validate().map_err(RuntimeError::Protocol)?;
596 Ok(capabilities)
597 }
598
599 async fn apply(
600 &self,
601 spec: &RuntimeUnitSpec,
602 current: &RuntimeObservation,
603 ) -> RuntimeResult<RuntimeObservation> {
604 self.apply_unit(spec, current).await
605 }
606
607 async fn inspect(&self, unit: &RuntimeUnitRecord) -> RuntimeResult<RuntimeInspection> {
608 self.bounded_lifecycle(&unit.spec, "inspection", self.inspect_unit(unit))
609 .await
610 }
611
612 async fn stop(
613 &self,
614 unit: &RuntimeUnitRecord,
615 request: &RuntimeActionRequest,
616 ) -> RuntimeResult<RuntimeObservation> {
617 self.bounded_lifecycle(&unit.spec, "stop", self.stop_unit(unit, request))
618 .await
619 }
620
621 async fn remove(
622 &self,
623 unit: &RuntimeUnitRecord,
624 request: &RuntimeActionRequest,
625 ) -> RuntimeResult<RuntimeRemoval> {
626 self.bounded_lifecycle(&unit.spec, "remove", self.remove_unit(unit, request))
627 .await
628 }
629
630 async fn logs(
631 &self,
632 unit: &RuntimeUnitRecord,
633 query: &RuntimeLogQuery,
634 ) -> RuntimeResult<Vec<RuntimeLogChunk>> {
635 self.bounded("log read", self.read_runtime_logs(unit, query))
636 .await
637 }
638
639 async fn exec(
640 &self,
641 unit: &RuntimeUnitRecord,
642 request: &RuntimeExecRequest,
643 ) -> RuntimeResult<RuntimeExecResult> {
644 self.execute_runtime_command(unit, request).await
645 }
646}
647
648#[cfg(test)]
649mod artifact_tests;
650#[cfg(test)]
651mod conformance_tests;
652#[cfg(test)]
653mod exec_integration_tests;
654#[cfg(test)]
655mod lifecycle_tests;
656#[cfg(test)]
657mod service_endpoint_tests;
658#[cfg(all(test, unix))]
659mod service_lifecycle_tests;
660#[cfg(test)]
661mod test_support;
662#[cfg(test)]
663mod tests;