Skip to main content

a3s_box_runtime/a3s_runtime_driver/
mod.rs

1//! A3S Runtime provider adapter for Box isolation backends.
2
3mod 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/// Host paths and bounds for one Box Runtime provider instance.
53#[derive(Debug, Clone)]
54pub struct BoxRuntimeDriverConfig {
55    /// Private A3S Box state root. Runtime records share its canonical
56    /// `boxes.json` store with CLI-created records but never adopt them.
57    pub home_dir: PathBuf,
58    /// Independent bound for one provider control-plane operation.
59    pub control_timeout: Duration,
60    /// Poll cadence while waiting for a finite Task to reach a terminal state.
61    pub task_poll_interval: Duration,
62    /// Existing private Linux tmpfs mount used only for transient Runtime
63    /// Secret files. The provider never creates or silently downgrades this
64    /// mount to disk-backed storage.
65    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/// Explicit AMD SEV-SNP policy for a confidential Box Runtime provider.
81///
82/// Hardware mode is the secure default. Simulation is advertised distinctly
83/// and is accepted only when the caller opts in with `simulate: true`.
84#[derive(Debug, Clone, Default)]
85pub struct BoxRuntimeSevSnpConfig {
86    /// CPU generation used to build the guest firmware configuration.
87    pub generation: SevSnpGeneration,
88    /// Accept a simulated SNP report instead of requiring genuine hardware.
89    pub simulate: bool,
90    /// Policy enforced during the RA-TLS attestation handshake.
91    pub attestation_policy: AttestationPolicy,
92}
93
94/// Concrete A3S Runtime driver backed by a configured Box isolation backend.
95pub 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    /// Create a Runtime driver backed by Box MicroVM isolation.
112    ///
113    /// Shared-kernel execution is never selected as an automatic fallback.
114    pub fn new(config: BoxRuntimeDriverConfig) -> RuntimeResult<Self> {
115        Self::new_with_isolation(config, ExecutionIsolation::Microvm)
116    }
117
118    /// Create a Runtime driver that explicitly supports AMD SEV-SNP
119    /// confidential MicroVMs in addition to ordinary MicroVM sandboxes.
120    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    /// Create a Runtime driver with an explicit concrete Box isolation
131    /// backend for Runtime's provider-neutral `IsolationLevel::Sandbox`.
132    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    /// Construct the Box Runtime driver over a caller-owned qualification
151    /// backend and generation-fenced data-plane connector.
152    ///
153    /// This seam exists only for downstream product qualification. It lets a
154    /// host exercise the production Box Runtime mapping, durable lifecycle,
155    /// health, endpoint, stop, and removal code against real child processes
156    /// without requiring a nested hypervisor or privileged OCI runtime on a
157    /// general CI runner. Release builds do not expose this constructor unless
158    /// the explicit `runtime-provider-qualification` feature is enabled.
159    ///
160    /// The supplied provider build is immutable evidence for the complete
161    /// driver instance. Callers must not use this constructor as a production
162    /// capability-probe bypass.
163    #[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    /// Compose the shared Box driver with one caller-owned Secret resolver.
196    ///
197    /// The resolver is normally backed by the node agent's existing
198    /// authenticated control channel. It is not a second lifecycle or Secret
199    /// store, and Box never persists the returned bytes.
200    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    /// Compose the shared Box driver with one caller-owned Artifact boundary.
210    ///
211    /// The caller keeps authenticated transport and Artifact admission. Box
212    /// reuses its existing VolumeStore and lifecycle records for mount wiring,
213    /// Task-output staging, generation fencing, and cleanup.
214    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    /// Concrete Box isolation backend selected for this provider instance.
250    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            // Runtime 0.2 uses `Sandbox` as the provider-neutral isolation
510            // class. `execution_isolation` selects Box's concrete backend.
511            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;