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::{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/// 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    /// Reserves the complete caller-declared graceful shutdown interval plus
319    /// the ordinary provider-control budget. A provider-local timeout must not
320    /// truncate a valid Runtime lifecycle policy; an outer request deadline
321    /// may still bound the complete public operation.
322    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            // Runtime uses `Sandbox` as the provider-neutral isolation
564            // class. `execution_isolation` selects Box's concrete backend.
565            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;