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 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 pub(super) async fn bounded_lifecycle<T, F>(
323 &self,
324 spec: &RuntimeUnitSpec,
325 operation: &'static str,
326 future: F,
327 ) -> RuntimeResult<T>
328 where
329 F: Future<Output = RuntimeResult<T>>,
330 {
331 let timeout = self.lifecycle_control_timeout(spec);
332 tokio::time::timeout(timeout, future).await.map_err(|_| {
333 RuntimeError::ProviderUnavailable(format!(
334 "Box {operation} exceeded the lifecycle-aware control timeout"
335 ))
336 })?
337 }
338
339 fn lifecycle_control_timeout(&self, spec: &RuntimeUnitSpec) -> Duration {
340 let graceful_shutdown_seconds = spec
341 .service_lifecycle
342 .as_ref()
343 .map_or(0, |lifecycle| u64::from(lifecycle.shutdown_grace_seconds));
344 self.control_timeout_with_grace(graceful_shutdown_seconds)
345 }
346
347 pub(super) async fn bounded_record_lifecycle<T, F>(
348 &self,
349 record: &BoxRecord,
350 operation: &'static str,
351 future: F,
352 ) -> RuntimeResult<T>
353 where
354 F: Future<Output = RuntimeResult<T>>,
355 {
356 let timeout = self.control_timeout_with_grace(record.stop_timeout.unwrap_or(0));
357 tokio::time::timeout(timeout, future).await.map_err(|_| {
358 RuntimeError::ProviderUnavailable(format!(
359 "Box {operation} exceeded the lifecycle-aware control timeout"
360 ))
361 })?
362 }
363
364 fn control_timeout_with_grace(&self, graceful_shutdown_seconds: u64) -> Duration {
365 self.config
366 .control_timeout
367 .saturating_add(Duration::from_secs(graceful_shutdown_seconds))
368 }
369}
370
371fn production_manager(
372 config: &BoxRuntimeDriverConfig,
373) -> (LocalExecutionManager, TransientRegistryAuthBroker) {
374 let broker = TransientRegistryAuthBroker::default();
375 let backend =
376 VmLocalExecutionBackend::new(&config.home_dir).with_transient_registry_auth(broker.clone());
377 let manager = LocalExecutionManager::new(
378 config.home_dir.join("boxes.json"),
379 &config.home_dir,
380 Arc::new(backend),
381 );
382 (manager, broker)
383}
384
385fn probe_provider_build(
386 execution_isolation: ExecutionIsolation,
387 sev_snp_simulated: Option<bool>,
388) -> Result<String, String> {
389 let provider_build = match execution_isolation {
390 ExecutionIsolation::Microvm => {
391 let support = crate::host_check::check_virtualization_support()
392 .map_err(|error| format!("microVM unavailable: {error}"))?;
393 if sev_snp_simulated == Some(false) {
394 let tee = crate::tee::check_sev_snp_support()
395 .map_err(|error| format!("SEV-SNP capability probe failed: {error}"))?;
396 if !tee.available {
397 return Err(format!(
398 "SEV-SNP unavailable: {}",
399 tee.reason.unwrap_or_else(|| "unknown reason".into())
400 ));
401 }
402 }
403 format!(
404 "a3s-box/{} isolation/microvm hypervisor/{}",
405 env!("CARGO_PKG_VERSION"),
406 support.backend
407 )
408 }
409 ExecutionIsolation::Sandbox => {
410 let snapshot = crate::sandbox::probe_sandbox_capabilities_for(
411 a3s_box_core::ExecutionBackend::A3sOci,
412 None,
413 None,
414 );
415 snapshot
416 .require_ready()
417 .map_err(|error| format!("shared-kernel backend unavailable: {error}"))?;
418 let runtime = snapshot.a3s_oci.ok_or_else(|| {
419 "shared-kernel capability probe returned no A3S OCI artifacts".to_string()
420 })?;
421 format!(
422 "a3s-box/{} isolation/sandbox a3s-oci/sha256:{} agent/sha256:{}",
423 env!("CARGO_PKG_VERSION"),
424 &runtime.runtime_sha256[..16],
425 &runtime.agent_sha256[..16]
426 )
427 }
428 };
429 Ok(match sev_snp_simulated {
430 Some(true) => format!("{provider_build} tee/sev-snp-simulated"),
431 Some(false) => format!("{provider_build} tee/sev-snp-hardware"),
432 None => provider_build,
433 })
434}
435
436fn validate_config(config: &BoxRuntimeDriverConfig) -> RuntimeResult<()> {
437 if !config.home_dir.is_absolute() {
438 return Err(RuntimeError::InvalidRequest(
439 "Box Runtime home directory must be absolute".into(),
440 ));
441 }
442 if config.control_timeout.is_zero() || config.task_poll_interval.is_zero() {
443 return Err(RuntimeError::InvalidRequest(
444 "Box Runtime timeout and poll interval must be positive".into(),
445 ));
446 }
447 let secret_root = config.secret_root.to_str().ok_or_else(|| {
448 RuntimeError::InvalidRequest(
449 "Box Runtime Secret root must be an encodable UTF-8 Linux path".into(),
450 )
451 })?;
452 let normalized_secret_root = secret_root.strip_prefix('/').is_some_and(|relative| {
453 !relative.is_empty()
454 && relative
455 .split('/')
456 .all(|segment| !segment.is_empty() && !matches!(segment, "." | ".."))
457 });
458 if !normalized_secret_root
459 || secret_root.contains([':', '\0'])
460 || secret_root.bytes().any(|byte| byte.is_ascii_control())
461 || !config.secret_root.is_absolute()
462 || config.secret_root.parent().is_none()
463 || config.secret_root.components().any(|component| {
464 matches!(
465 component,
466 std::path::Component::CurDir
467 | std::path::Component::ParentDir
468 | std::path::Component::Prefix(_)
469 )
470 })
471 {
472 return Err(RuntimeError::InvalidRequest(
473 "Box Runtime Secret root must be an encodable absolute normalized non-root Linux path"
474 .into(),
475 ));
476 }
477 Ok(())
478}
479
480#[cfg(any(test, feature = "runtime-provider-qualification"))]
481fn validate_qualification_provider_build(provider_build: &str) -> RuntimeResult<()> {
482 if provider_build.is_empty()
483 || provider_build.len() > 255
484 || provider_build.trim() != provider_build
485 || provider_build.bytes().any(|byte| byte.is_ascii_control())
486 {
487 return Err(RuntimeError::InvalidRequest(
488 "Box Runtime qualification provider build must be a trimmed non-empty string of at most 255 bytes"
489 .into(),
490 ));
491 }
492 Ok(())
493}
494
495fn validate_sev_snp_config(config: &BoxRuntimeSevSnpConfig) -> RuntimeResult<()> {
496 if config
497 .attestation_policy
498 .expected_measurement
499 .as_ref()
500 .is_some_and(|measurement| {
501 measurement.len() != 96
502 || !measurement
503 .bytes()
504 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
505 })
506 {
507 return Err(RuntimeError::InvalidRequest(
508 "Box Runtime SEV-SNP measurement must be a canonical lowercase SHA-384 hex value"
509 .into(),
510 ));
511 }
512 if config.attestation_policy.max_report_age_secs.is_some() {
513 return Err(RuntimeError::InvalidRequest(
514 "Box Runtime SEV-SNP RA-TLS artifacts do not support report-age policy".into(),
515 ));
516 }
517 Ok(())
518}
519
520#[async_trait]
521impl RuntimeDriver for BoxRuntimeDriver {
522 fn provider_id(&self) -> &ProviderId {
523 &self.provider_id
524 }
525
526 async fn capabilities(&self) -> RuntimeResult<RuntimeCapabilities> {
527 if self.secret_materialization.configured() {
528 self.secret_materialization.require_ready().await?;
529 }
530 let mut features = vec![
531 RuntimeFeature::DurableIdentity,
532 RuntimeFeature::Stop,
533 RuntimeFeature::Remove,
534 RuntimeFeature::ServiceTcp,
535 RuntimeFeature::Logs,
536 RuntimeFeature::Exec,
537 RuntimeFeature::ServiceLifecycle,
538 ];
539 if self.secret_materialization.configured() {
540 features.push(RuntimeFeature::SecretReferences);
541 }
542 if self.artifact_storage.artifact_configured() {
543 features.push(RuntimeFeature::OutputArtifacts);
544 }
545 if self.sev_snp.is_some() {
546 features.push(RuntimeFeature::Attestation);
547 features.push(RuntimeFeature::IdentityAttachment);
548 }
549 let mut mount_kinds = vec![MountKind::Volume, MountKind::Tmpfs];
550 if self.artifact_storage.artifact_configured() {
551 mount_kinds.insert(0, MountKind::Artifact);
552 }
553 let mut isolation_levels = vec![IsolationLevel::Sandbox];
554 if self.sev_snp.is_some() {
555 isolation_levels.push(IsolationLevel::Confidential);
556 }
557 let capabilities = RuntimeCapabilities {
558 schema: RuntimeCapabilities::SCHEMA.into(),
559 provider_id: self.provider_id.clone(),
560 provider_build: self.provider_build().await?,
561 unit_classes: vec![RuntimeUnitClass::Task, RuntimeUnitClass::Service],
562 artifact_media_types: vec![OCI_IMAGE_MANIFEST.into(), OCI_IMAGE_INDEX.into()],
563 isolation_levels,
566 network_modes: vec![NetworkMode::None, NetworkMode::Service],
567 mount_kinds,
568 health_check_kinds: vec![
569 HealthCheckKind::Http,
570 HealthCheckKind::Tcp,
571 HealthCheckKind::Command,
572 ],
573 resource_controls: vec![
574 ResourceControl::Cpu,
575 ResourceControl::Memory,
576 ResourceControl::Pids,
577 ResourceControl::ExecutionTimeout,
578 ],
579 features,
580 };
581 capabilities.validate().map_err(RuntimeError::Protocol)?;
582 Ok(capabilities)
583 }
584
585 async fn apply(
586 &self,
587 spec: &RuntimeUnitSpec,
588 current: &RuntimeObservation,
589 ) -> RuntimeResult<RuntimeObservation> {
590 self.apply_unit(spec, current).await
591 }
592
593 async fn inspect(&self, unit: &RuntimeUnitRecord) -> RuntimeResult<RuntimeInspection> {
594 self.bounded_lifecycle(&unit.spec, "inspection", self.inspect_unit(unit))
595 .await
596 }
597
598 async fn stop(
599 &self,
600 unit: &RuntimeUnitRecord,
601 request: &RuntimeActionRequest,
602 ) -> RuntimeResult<RuntimeObservation> {
603 self.bounded_lifecycle(&unit.spec, "stop", self.stop_unit(unit, request))
604 .await
605 }
606
607 async fn remove(
608 &self,
609 unit: &RuntimeUnitRecord,
610 request: &RuntimeActionRequest,
611 ) -> RuntimeResult<RuntimeRemoval> {
612 self.bounded_lifecycle(&unit.spec, "remove", self.remove_unit(unit, request))
613 .await
614 }
615
616 async fn logs(
617 &self,
618 unit: &RuntimeUnitRecord,
619 query: &RuntimeLogQuery,
620 ) -> RuntimeResult<Vec<RuntimeLogChunk>> {
621 self.bounded("log read", self.read_runtime_logs(unit, query))
622 .await
623 }
624
625 async fn exec(
626 &self,
627 unit: &RuntimeUnitRecord,
628 request: &RuntimeExecRequest,
629 ) -> RuntimeResult<RuntimeExecResult> {
630 self.execute_runtime_command(unit, request).await
631 }
632}
633
634#[cfg(test)]
635mod artifact_tests;
636#[cfg(test)]
637mod conformance_tests;
638#[cfg(test)]
639mod exec_integration_tests;
640#[cfg(test)]
641mod lifecycle_tests;
642#[cfg(test)]
643mod service_endpoint_tests;
644#[cfg(all(test, unix))]
645mod service_lifecycle_tests;
646#[cfg(test)]
647mod test_support;
648#[cfg(test)]
649mod tests;