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    /// Whether this concrete provider instance can enforce a byte-precise
103    /// Sandbox writable-layer quota. Qualification backends deliberately do
104    /// not inherit the production host probe.
105    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    /// Create a Runtime driver backed by Box MicroVM isolation.
116    ///
117    /// Shared-kernel execution is never selected as an automatic fallback.
118    pub fn new(config: BoxRuntimeDriverConfig) -> RuntimeResult<Self> {
119        Self::new_with_isolation(config, ExecutionIsolation::Microvm)
120    }
121
122    /// Create a Runtime driver that explicitly supports AMD SEV-SNP
123    /// confidential MicroVMs in addition to ordinary MicroVM sandboxes.
124    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    /// Create a Runtime driver with an explicit concrete Box isolation
135    /// backend for Runtime's provider-neutral `IsolationLevel::Sandbox`.
136    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    /// Construct the Box Runtime driver over a caller-owned qualification
157    /// backend and generation-fenced data-plane connector.
158    ///
159    /// This seam exists only for downstream product qualification. It lets a
160    /// host exercise the production Box Runtime mapping, durable lifecycle,
161    /// health, endpoint, stop, and removal code against real child processes
162    /// without requiring a nested hypervisor or privileged OCI runtime on a
163    /// general CI runner. Release builds do not expose this constructor unless
164    /// the explicit `runtime-provider-qualification` feature is enabled.
165    ///
166    /// The supplied provider build is immutable evidence for the complete
167    /// driver instance. Callers must not use this constructor as a production
168    /// capability-probe bypass.
169    #[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    /// Compose the shared Box driver with one caller-owned Secret resolver.
203    ///
204    /// The resolver is normally backed by the node agent's existing
205    /// authenticated control channel. It is not a second lifecycle or Secret
206    /// store, and Box never persists the returned bytes.
207    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    /// Compose the shared Box driver with one caller-owned Artifact boundary.
217    ///
218    /// The caller keeps authenticated transport and Artifact admission. Box
219    /// reuses its existing VolumeStore and lifecycle records for mount wiring,
220    /// Task-output staging, generation fencing, and cleanup.
221    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    /// Concrete Box isolation backend selected for this provider instance.
260    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    /// Reserves the complete caller-declared graceful shutdown interval plus
329    /// the ordinary provider-control budget. A provider-local timeout must not
330    /// truncate a valid Runtime lifecycle policy; an outer request deadline
331    /// may still bound the complete public operation.
332    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            // Runtime uses `Sandbox` as the provider-neutral isolation
583            // class. `execution_isolation` selects Box's concrete backend.
584            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;