Skip to main content

a3s_box_runtime/local_execution/
oci_backend.rs

1//! Public-SDK-only A3S OCI lifecycle boundary for managed local execution.
2
3use std::collections::BTreeSet;
4use std::future::Future;
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7
8use a3s_box_core::{
9    pty::PtyRequest, ExecOutput, ExecRequest as BoxExecRequest, ExecutionCpuStats,
10    ExecutionEventBatch, ExecutionEventKind, ExecutionEventsRequest, ExecutionGeneration,
11    ExecutionId, ExecutionIsolation, ExecutionManagerError, ExecutionManagerResult,
12    ExecutionMemoryStats, ExecutionProcess, ExecutionProcessInfo, ExecutionProcessInventory,
13    ExecutionResourceUpdate, ExecutionRuntimeEvent, ExecutionState, ExecutionStats,
14    FileOp as BoxFileOp, FileRequest as BoxFileRequest, FileResponse as BoxFileResponse,
15    FilesystemEntry as BoxFilesystemEntry, FilesystemEntryKind as BoxFilesystemEntryKind,
16    FilesystemOp as BoxFilesystemOp, FilesystemRequest as BoxFilesystemRequest,
17    FilesystemResponse as BoxFilesystemResponse, KillOutcome, OperationId as BoxOperationId,
18    MAX_BOUNDED_FILE_BYTES,
19};
20use a3s_oci_sdk::oci_spec::runtime::{
21    ContainerState, LinuxCpuBuilder, LinuxMemoryBuilder, LinuxPidsBuilder, LinuxResourcesBuilder,
22};
23use a3s_oci_sdk::{
24    runtime_bundle_handoff_directory as sdk_runtime_bundle_handoff_directory,
25    AttachmentCapabilities, ContainerId, ContainerOperationRequest, ContainerRecord,
26    ContainerTarget, CreateAttachments, CreateRequest, DeleteMode, DeleteRequest, DriverKind,
27    ErrorCode, EventsRequest, ExitStatus, FileOp as OciFileOp, FileRequest as OciFileRequest,
28    FileResponse as OciFileResponse, FilesystemEntry as OciFilesystemEntry,
29    FilesystemEntryKind as OciFilesystemEntryKind, FilesystemOp as OciFilesystemOp,
30    FilesystemRequest as OciFilesystemRequest, FilesystemResponse as OciFilesystemResponse, IoMode,
31    IsolationClass, IsolationRequest, KillRequest, LinuxResources, LocalIpcEndpoint, OciBundle,
32    OperationContext, OperationId as OciOperationId, ProcessIo, ProcessRecord, ProcessesRequest,
33    RuntimeClient, RuntimeEventKind, RuntimeInfo, RuntimeOperation, Signal, StartRequest,
34    StateRequest, StatsRequest, UpdateRequest, WaitRequest, ATTACHMENT_SCHEMA_V1,
35    MAX_FILE_TRANSFER_BYTES, RUNTIME_BUNDLE_HANDOFF_BUNDLE_DIRECTORY,
36    RUNTIME_BUNDLE_HANDOFF_EXTENSION, RUNTIME_BUNDLE_HANDOFF_EXTENSION_VERSION,
37    RUNTIME_BUNDLE_HANDOFF_ROOT_DIRECTORY,
38};
39use async_trait::async_trait;
40use base64::{engine::general_purpose::STANDARD, Engine as _};
41use chrono::Utc;
42use serde::{Deserialize, Serialize};
43use sha2::{Digest, Sha256};
44
45use super::resources::ExecutionResourceGuard;
46use super::{
47    LocalExecutionBackend, LocalExecutionHandle, LocalExecutionObservation,
48    LocalExecutionTermination,
49};
50use crate::{BoxRecord, ManagedExecutionState};
51
52/// Durable schema for one Box-to-OCI runtime attachment.
53pub const OCI_RUNTIME_BINDING_SCHEMA_VERSION: &str = "a3s.box.oci-runtime-binding.v2";
54
55const REQUIRED_LIFECYCLE_OPERATIONS: &[RuntimeOperation] = &[
56    RuntimeOperation::Create,
57    RuntimeOperation::State,
58    RuntimeOperation::Start,
59    RuntimeOperation::Kill,
60    RuntimeOperation::Delete,
61    RuntimeOperation::Wait,
62];
63const DEFAULT_KILL_SIGNAL: i32 = 9;
64
65/// Serializable platform-local endpoint retained for runtime reconciliation.
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67#[serde(tag = "kind", rename_all = "kebab-case")]
68pub enum OciRuntimeEndpoint {
69    /// Absolute Unix-domain socket owned by the local runtime service.
70    UnixSocket { path: PathBuf },
71    /// Local Windows named pipe below `\\.\pipe\`.
72    WindowsNamedPipe { name: String },
73}
74
75impl OciRuntimeEndpoint {
76    /// Construct an absolute Unix-domain socket endpoint.
77    pub fn unix_socket(path: impl Into<PathBuf>) -> ExecutionManagerResult<Self> {
78        let endpoint = Self::UnixSocket { path: path.into() };
79        endpoint.validate()?;
80        Ok(endpoint)
81    }
82
83    /// Construct a local Windows named-pipe endpoint.
84    pub fn windows_named_pipe(name: impl Into<String>) -> ExecutionManagerResult<Self> {
85        let endpoint = Self::WindowsNamedPipe { name: name.into() };
86        endpoint.validate()?;
87        Ok(endpoint)
88    }
89
90    fn validate(&self) -> ExecutionManagerResult<()> {
91        match self {
92            Self::UnixSocket { path } if path.is_absolute() => Ok(()),
93            Self::UnixSocket { path } => Err(ExecutionManagerError::InvalidRequest(format!(
94                "A3S OCI Unix endpoint must be absolute: {}",
95                path.display()
96            ))),
97            Self::WindowsNamedPipe { name }
98                if name.to_ascii_lowercase().starts_with(r"\\.\pipe\")
99                    && name.len() > r"\\.\pipe\".len()
100                    && !name.as_bytes().contains(&0) =>
101            {
102                Ok(())
103            }
104            Self::WindowsNamedPipe { name } => Err(ExecutionManagerError::InvalidRequest(format!(
105                "A3S OCI named-pipe endpoint is not local and non-empty: {name:?}"
106            ))),
107        }
108    }
109
110    fn to_sdk(&self) -> ExecutionManagerResult<LocalIpcEndpoint> {
111        self.validate()?;
112        #[cfg(unix)]
113        {
114            return match self {
115                Self::UnixSocket { path } => LocalIpcEndpoint::unix_socket(path.clone())
116                    .map_err(|error| sdk_error("connect", error)),
117                Self::WindowsNamedPipe { .. } => Err(ExecutionManagerError::Unavailable(
118                    "a Windows A3S OCI endpoint cannot be opened on this host".to_string(),
119                )),
120            };
121        }
122        #[cfg(windows)]
123        {
124            return match self {
125                Self::WindowsNamedPipe { name } => {
126                    LocalIpcEndpoint::windows_named_pipe(name.clone())
127                        .map_err(|error| sdk_error("connect", error))
128                }
129                Self::UnixSocket { .. } => Err(ExecutionManagerError::Unavailable(
130                    "a Unix A3S OCI endpoint cannot be opened on this host".to_string(),
131                )),
132            };
133        }
134        #[allow(unreachable_code)]
135        Err(ExecutionManagerError::Unavailable(
136            "A3S OCI local IPC is unsupported on this host".to_string(),
137        ))
138    }
139}
140
141/// Exact runtime identity persisted separately from the Box execution identity.
142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
143pub struct OciRuntimeBinding {
144    /// Version of this durable binding schema.
145    pub schema_version: String,
146    /// Local service endpoint used to reopen the runtime.
147    pub endpoint: OciRuntimeEndpoint,
148    /// Exact container ID and runtime generation.
149    pub target: ContainerTarget,
150    /// Runtime-selected driver; Box never persists a requested hypervisor.
151    pub driver: DriverKind,
152    /// Effective isolation returned by the runtime.
153    pub isolation: IsolationClass,
154    /// Immutable OCI configuration evidence returned by the runtime.
155    pub config_digest: String,
156    /// Complete versioned create-time attachment evidence returned by the runtime.
157    pub attachments_digest: String,
158}
159
160impl OciRuntimeBinding {
161    fn from_record(
162        endpoint: OciRuntimeEndpoint,
163        expected_id: &ContainerId,
164        record: &ContainerRecord,
165    ) -> ExecutionManagerResult<Self> {
166        validate_record_id(record, expected_id, "bind")?;
167        if record.generation.0 == 0 {
168            return Err(ExecutionManagerError::Internal(
169                "A3S OCI returned runtime generation zero".to_string(),
170            ));
171        }
172        let binding = Self {
173            schema_version: OCI_RUNTIME_BINDING_SCHEMA_VERSION.to_string(),
174            endpoint,
175            target: ContainerTarget::exact(expected_id.clone(), record.generation),
176            driver: record.driver,
177            isolation: record.isolation,
178            config_digest: record.config_digest.clone(),
179            attachments_digest: record.attachments_digest.clone().ok_or_else(|| {
180                ExecutionManagerError::Internal(
181                    "A3S OCI returned no versioned attachment evidence".to_string(),
182                )
183            })?,
184        };
185        binding.validate()?;
186        Ok(binding)
187    }
188
189    /// Validate the versioned endpoint, exact target, and immutable evidence.
190    pub fn validate(&self) -> ExecutionManagerResult<()> {
191        if self.schema_version != OCI_RUNTIME_BINDING_SCHEMA_VERSION {
192            return Err(ExecutionManagerError::Internal(format!(
193                "unsupported A3S OCI binding schema {:?}",
194                self.schema_version
195            )));
196        }
197        self.endpoint.validate()?;
198        let generation = self.target.generation.ok_or_else(|| {
199            ExecutionManagerError::Internal(
200                "A3S OCI binding does not contain an exact runtime generation".to_string(),
201            )
202        })?;
203        if generation.0 == 0 {
204            return Err(ExecutionManagerError::Internal(
205                "A3S OCI binding contains runtime generation zero".to_string(),
206            ));
207        }
208        validate_config_digest(&self.config_digest)?;
209        validate_attachments_digest(&self.attachments_digest)?;
210        Ok(())
211    }
212
213    /// Validate that this runtime identity belongs to one Box execution.
214    pub fn validate_for(&self, execution_id: &ExecutionId) -> ExecutionManagerResult<()> {
215        self.validate()?;
216        let expected = runtime_container_id(execution_id)?;
217        if self.target.id != expected {
218            return Err(ExecutionManagerError::Internal(format!(
219                "A3S OCI binding target {} does not belong to Box execution {execution_id}",
220                self.target.id
221            )));
222        }
223        Ok(())
224    }
225
226    fn validate_record(&self, record: &ContainerRecord) -> ExecutionManagerResult<()> {
227        self.validate()?;
228        validate_record_id(record, &self.target.id, "reconcile")?;
229        if Some(record.generation) != self.target.generation
230            || record.driver != self.driver
231            || record.isolation != self.isolation
232            || record.config_digest != self.config_digest
233            || record.attachments_digest.as_deref() != Some(self.attachments_digest.as_str())
234        {
235            return Err(ExecutionManagerError::Internal(format!(
236                "A3S OCI runtime evidence drifted for {} generation {:?}",
237                self.target.id, self.target.generation
238            )));
239        }
240        Ok(())
241    }
242}
243
244/// Stable runtime identities and negotiated capabilities available to product preparation.
245#[derive(Debug, Clone)]
246pub struct OciBundlePreparationContext {
247    runtime_container_id: ContainerId,
248    create_context: OperationContext,
249    isolation: IsolationRequest,
250    attachment_capabilities: AttachmentCapabilities,
251    execution_generation: ExecutionGeneration,
252    operation_seed: BoxOperationId,
253}
254
255impl OciBundlePreparationContext {
256    /// Exact runtime container identity derived from the Box execution identity.
257    pub fn runtime_container_id(&self) -> &ContainerId {
258        &self.runtime_container_id
259    }
260
261    /// Exact create operation context that will be sent to OCI Runtime.
262    pub fn create_context(&self) -> &OperationContext {
263        &self.create_context
264    }
265
266    /// Minimum isolation requested from OCI Runtime.
267    pub fn isolation(&self) -> &IsolationRequest {
268        &self.isolation
269    }
270
271    /// Attachment extensions advertised by the connected runtime service.
272    pub fn attachment_capabilities(&self) -> &AttachmentCapabilities {
273        &self.attachment_capabilities
274    }
275
276    /// Resolve the only operation-scoped directory accepted for bundle ownership handoff.
277    pub fn runtime_bundle_handoff_directory(
278        &self,
279        runtime_root: impl AsRef<Path>,
280    ) -> ExecutionManagerResult<PathBuf> {
281        if !self.attachment_capabilities.supports_extension(
282            RUNTIME_BUNDLE_HANDOFF_EXTENSION,
283            RUNTIME_BUNDLE_HANDOFF_EXTENSION_VERSION,
284        ) {
285            return Err(ExecutionManagerError::Unavailable(format!(
286                "A3S OCI Runtime does not advertise {RUNTIME_BUNDLE_HANDOFF_EXTENSION} version {RUNTIME_BUNDLE_HANDOFF_EXTENSION_VERSION}"
287            )));
288        }
289        sdk_runtime_bundle_handoff_directory(
290            runtime_root,
291            &self.runtime_container_id,
292            &self.create_context.operation_id,
293        )
294        .map_err(|error| sdk_error("resolve bundle handoff", error))
295    }
296
297    /// Verify that a claimed handoff directory has the exact negotiated suffix.
298    pub fn validate_runtime_bundle_handoff_directory(
299        &self,
300        directory: &Path,
301    ) -> ExecutionManagerResult<()> {
302        let operation_directory = directory.parent();
303        let container_directory = operation_directory.and_then(Path::parent);
304        let handoff_root = container_directory.and_then(Path::parent);
305        let runtime_root = handoff_root.and_then(Path::parent);
306        let exact_shape = directory.is_absolute()
307            && directory.file_name().and_then(|name| name.to_str())
308                == Some(RUNTIME_BUNDLE_HANDOFF_BUNDLE_DIRECTORY)
309            && operation_directory
310                .and_then(Path::file_name)
311                .and_then(|name| name.to_str())
312                == Some(self.create_context.operation_id.as_str())
313            && container_directory
314                .and_then(Path::file_name)
315                .and_then(|name| name.to_str())
316                == Some(self.runtime_container_id.as_str())
317            && handoff_root
318                .and_then(Path::file_name)
319                .and_then(|name| name.to_str())
320                == Some(RUNTIME_BUNDLE_HANDOFF_ROOT_DIRECTORY);
321        let Some(runtime_root) = runtime_root.filter(|_| exact_shape) else {
322            return Err(ExecutionManagerError::InvalidRequest(format!(
323                "A3S OCI bundle handoff does not match the exact runtime/container/create-operation layout: {}",
324                directory.display()
325            )));
326        };
327        let expected = self.runtime_bundle_handoff_directory(runtime_root)?;
328        if directory != expected {
329            return Err(ExecutionManagerError::InvalidRequest(format!(
330                "A3S OCI bundle handoff path is not normalized: {}",
331                directory.display()
332            )));
333        }
334        Ok(())
335    }
336}
337
338/// Product-owned OCI bundle and host bookkeeping required before lifecycle launch.
339#[derive(Debug, Clone)]
340pub struct OciPreparedExecution {
341    pub bundle: OciBundle,
342    pub attachments: CreateAttachments,
343    pub console_log: PathBuf,
344    pub anonymous_volumes: Vec<String>,
345}
346
347impl OciPreparedExecution {
348    /// Build a non-interactive prepared execution with a caller-owned console.
349    pub fn new(bundle: OciBundle, console_log: impl Into<PathBuf>) -> ExecutionManagerResult<Self> {
350        let io = ProcessIo {
351            stdin: IoMode::Null,
352            stdout: IoMode::Null,
353            stderr: IoMode::Null,
354            terminal_size: None,
355        };
356        let attachments = CreateAttachments::from_bundle(&bundle, io)
357            .map_err(|error| sdk_error("prepare attachments", error))?;
358        Self::with_attachments(bundle, attachments, console_log)
359    }
360
361    /// Build a prepared execution with explicit, already-authorized classifications.
362    pub fn with_attachments(
363        bundle: OciBundle,
364        attachments: CreateAttachments,
365        console_log: impl Into<PathBuf>,
366    ) -> ExecutionManagerResult<Self> {
367        attachments
368            .validate(&bundle)
369            .map_err(|error| sdk_error("prepare attachments", error))?;
370        Ok(Self {
371            bundle,
372            attachments,
373            console_log: console_log.into(),
374            anonymous_volumes: Vec::new(),
375        })
376    }
377
378    /// Bind a portable bundle prepared at the negotiated operation-scoped handoff path.
379    pub fn with_runtime_bundle_handoff(
380        mut self,
381        context: &OciBundlePreparationContext,
382        runtime_root: impl AsRef<Path>,
383    ) -> ExecutionManagerResult<Self> {
384        let expected = context.runtime_bundle_handoff_directory(runtime_root)?;
385        if !exact_handoff_directory_matches(self.bundle.directory(), &expected) {
386            return Err(ExecutionManagerError::InvalidRequest(format!(
387                "A3S OCI bundle handoff must use exact operation path {}: {}",
388                expected.display(),
389                self.bundle.directory().display()
390            )));
391        }
392        self.attachments = self
393            .attachments
394            .clone()
395            .with_runtime_bundle_handoff(&self.bundle)
396            .map_err(|error| sdk_error("prepare bundle handoff", error))?;
397        self.attachments
398            .validate(&self.bundle)
399            .map_err(|error| sdk_error("validate bundle handoff", error))?;
400        Ok(self)
401    }
402
403    fn validate_for(
404        &self,
405        record: &BoxRecord,
406        context: &OciBundlePreparationContext,
407    ) -> ExecutionManagerResult<()> {
408        self.attachments
409            .validate(&self.bundle)
410            .map_err(|error| sdk_error("validate attachments", error))?;
411        if self.attachments.uses_runtime_bundle_handoff() {
412            context.validate_runtime_bundle_handoff_directory(self.bundle.directory())?;
413        }
414        if self.console_log != record.console_log {
415            return Err(ExecutionManagerError::InvalidRequest(format!(
416                "A3S OCI preparation changed the durable console path for {}",
417                record.id
418            )));
419        }
420        if self.anonymous_volumes != record.anonymous_volumes {
421            return Err(ExecutionManagerError::InvalidRequest(format!(
422                "A3S OCI preparation introduced uncommitted anonymous-volume state for {}",
423                record.id
424            )));
425        }
426        Ok(())
427    }
428}
429
430#[cfg(not(windows))]
431fn exact_handoff_directory_matches(directory: &Path, expected: &Path) -> bool {
432    directory == expected
433}
434
435#[cfg(windows)]
436fn exact_handoff_directory_matches(directory: &Path, expected: &Path) -> bool {
437    // `OciBundle::load` resolves an existing directory with
438    // `std::fs::canonicalize`. Windows returns that same directory in the
439    // verbatim `\\?\C:\...` namespace, while the operation-scoped SDK path is
440    // intentionally constructed before publication as `C:\...`. Strip only
441    // that namespace spelling (including its UNC form); do not resolve either
442    // input here, because accepting a symlink alias would weaken the exact
443    // container/operation ownership boundary.
444    windows_path_without_verbatim_prefix(directory)
445        == windows_path_without_verbatim_prefix(expected)
446}
447
448#[cfg(windows)]
449fn windows_path_without_verbatim_prefix(path: &Path) -> Vec<u16> {
450    use std::os::windows::ffi::OsStrExt as _;
451
452    const BACKSLASH: u16 = b'\\' as u16;
453    const QUESTION_MARK: u16 = b'?' as u16;
454    const COLON: u16 = b':' as u16;
455    const UNC: [u16; 4] = [b'U' as u16, b'N' as u16, b'C' as u16, BACKSLASH];
456
457    let encoded = path.as_os_str().encode_wide().collect::<Vec<_>>();
458    let Some(rest) = encoded.strip_prefix(&[BACKSLASH, BACKSLASH, QUESTION_MARK, BACKSLASH]) else {
459        return encoded;
460    };
461    let has_drive_prefix = rest.len() >= 3
462        && ((b'A' as u16..=b'Z' as u16).contains(&rest[0])
463            || (b'a' as u16..=b'z' as u16).contains(&rest[0]))
464        && rest[1] == COLON;
465    if has_drive_prefix {
466        return rest.to_vec();
467    }
468    if let Some(unc_rest) = rest.strip_prefix(&UNC) {
469        let mut normalized = Vec::with_capacity(unc_rest.len() + 2);
470        normalized.extend([BACKSLASH, BACKSLASH]);
471        normalized.extend_from_slice(unc_rest);
472        return normalized;
473    }
474    encoded
475}
476
477/// Product-owned preparation boundary consumed by the OCI lifecycle backend.
478#[async_trait]
479pub trait OciBundleProvider: Send + Sync {
480    /// Reject a runtime/provider mismatch before product or runtime mutation.
481    fn preflight(
482        &self,
483        _record: &BoxRecord,
484        _context: &OciBundlePreparationContext,
485    ) -> ExecutionManagerResult<()> {
486        Ok(())
487    }
488
489    /// Prepare one immutable OCI bundle after runtime capability preflight.
490    async fn prepare(
491        &self,
492        record: &BoxRecord,
493        context: &OciBundlePreparationContext,
494    ) -> ExecutionManagerResult<OciPreparedExecution>;
495
496    /// Ensure the Box-owned init-output projection is ready before OCI start.
497    /// Test and embedding providers may retain raw runtime output only.
498    async fn ensure_log_projection(
499        &self,
500        _record: &BoxRecord,
501        _binding: &OciRuntimeBinding,
502    ) -> ExecutionManagerResult<()> {
503        Ok(())
504    }
505
506    /// Wait until the exact init stdout/stderr streams have reached EOF and
507    /// Box's configured log driver has consumed them.
508    async fn wait_log_projection_drained(
509        &self,
510        _record: &BoxRecord,
511        _binding: &OciRuntimeBinding,
512    ) -> ExecutionManagerResult<()> {
513        Ok(())
514    }
515
516    /// Wait until the exact projection worker has stopped after the runtime
517    /// owner was lost. This path must not claim that output was fully drained:
518    /// owner-death recovery deliberately preserves an unknown exit status and
519    /// may also have lost the tail of the init output stream.
520    async fn wait_log_projection_stopped_after_owner_loss(
521        &self,
522        _record: &BoxRecord,
523        _binding: &OciRuntimeBinding,
524    ) -> ExecutionManagerResult<()> {
525        Ok(())
526    }
527
528    /// Remove only Box-owned preparation artifacts after runtime cleanup.
529    async fn cleanup(&self, _record: &BoxRecord) -> ExecutionManagerResult<()> {
530        Ok(())
531    }
532}
533
534/// Result of a create/start composition through the public OCI SDK.
535#[derive(Debug, Clone)]
536pub struct OciRuntimeLaunch {
537    pub record: ContainerRecord,
538    pub binding: OciRuntimeBinding,
539}
540
541/// SDK-only lifecycle adapter. It contains no Box image, VM, or driver internals.
542#[derive(Clone)]
543pub struct OciLifecycleAdapter {
544    endpoint: OciRuntimeEndpoint,
545    client: RuntimeClient,
546}
547
548impl OciLifecycleAdapter {
549    /// Connect to an out-of-process A3S OCI host service.
550    pub async fn connect(endpoint: OciRuntimeEndpoint) -> ExecutionManagerResult<Self> {
551        let sdk_endpoint = endpoint.to_sdk()?;
552        let client = RuntimeClient::connect(&sdk_endpoint)
553            .await
554            .map_err(|error| sdk_error("connect", error))?;
555        Self::from_client(endpoint, client)
556    }
557
558    /// Inject a public SDK client, primarily for an in-process host service or test.
559    pub fn from_client(
560        endpoint: OciRuntimeEndpoint,
561        client: RuntimeClient,
562    ) -> ExecutionManagerResult<Self> {
563        endpoint.validate()?;
564        Ok(Self { endpoint, client })
565    }
566
567    pub(super) fn client(&self) -> RuntimeClient {
568        self.client.clone()
569    }
570
571    /// Fail before product preparation unless one launch-ready driver can meet
572    /// the requested isolation and the exact lifecycle surface is advertised.
573    pub async fn require_isolation(
574        &self,
575        isolation: ExecutionIsolation,
576    ) -> ExecutionManagerResult<RuntimeInfo> {
577        let info = self
578            .client
579            .features()
580            .await
581            .map_err(|error| sdk_error("features", error))?;
582        for operation in REQUIRED_LIFECYCLE_OPERATIONS {
583            if !info.operations.contains(operation) {
584                return Err(ExecutionManagerError::Unavailable(format!(
585                    "A3S OCI Runtime does not advertise {operation:?}"
586                )));
587            }
588        }
589        if !info.attachments.supports_schema(ATTACHMENT_SCHEMA_V1) {
590            return Err(ExecutionManagerError::Unavailable(format!(
591                "A3S OCI Runtime does not advertise attachment schema {ATTACHMENT_SCHEMA_V1}"
592            )));
593        }
594        let required = oci_isolation_request(isolation).class();
595        if !info.drivers.drivers.iter().any(|capability| {
596            capability.can_launch() && capability.isolation_classes.contains(&required)
597        }) {
598            return Err(ExecutionManagerError::Unavailable(format!(
599                "A3S OCI Runtime has no launch-ready driver for {required:?}"
600            )));
601        }
602        Ok(info)
603    }
604
605    async fn require_operation(
606        &self,
607        operation: RuntimeOperation,
608        label: &str,
609    ) -> ExecutionManagerResult<()> {
610        let info = self
611            .client
612            .features()
613            .await
614            .map_err(|error| sdk_error("features", error))?;
615        if !info.operations.contains(&operation) {
616            return Err(ExecutionManagerError::Unavailable(format!(
617                "A3S OCI Runtime does not advertise {label}"
618            )));
619        }
620        Ok(())
621    }
622
623    async fn launch_preflighted<BeforeStart, BeforeStartFuture>(
624        &self,
625        info: &RuntimeInfo,
626        execution_id: &ExecutionId,
627        preparation: OciBundlePreparationContext,
628        prepared: OciPreparedExecution,
629        before_start: BeforeStart,
630    ) -> ExecutionManagerResult<OciRuntimeLaunch>
631    where
632        BeforeStart: FnOnce(OciRuntimeBinding) -> BeforeStartFuture,
633        BeforeStartFuture: Future<Output = ExecutionManagerResult<()>>,
634    {
635        let id = preparation.runtime_container_id.clone();
636        let requested = preparation.isolation.clone();
637        let expected_config_digest = prepared.bundle.config_digest().to_string();
638        let expected_attachments_digest = prepared
639            .attachments
640            .digest()
641            .map_err(|error| sdk_error("digest attachments", error))?;
642        let created = self
643            .client
644            .create(CreateRequest {
645                context: preparation.create_context,
646                id: id.clone(),
647                bundle: prepared.bundle,
648                isolation: requested.clone(),
649                attachments: prepared.attachments,
650            })
651            .await
652            .map_err(|error| sdk_error("create", error))?;
653        if let Err(error) = validate_created_record(
654            info,
655            &created,
656            &id,
657            requested.class(),
658            Some(&expected_config_digest),
659            Some(&expected_attachments_digest),
660        ) {
661            self.cleanup_failed_launch(
662                execution_id,
663                preparation.execution_generation,
664                &created,
665                requested.class(),
666            )
667            .await;
668            return Err(error);
669        }
670
671        let created_binding = OciRuntimeBinding::from_record(self.endpoint.clone(), &id, &created)?;
672        before_start(created_binding).await?;
673
674        let target = ContainerTarget::exact(id.clone(), created.generation);
675        let started = match self
676            .client
677            .start(StartRequest {
678                context: start_operation_context(
679                    preparation.operation_seed.as_str(),
680                    preparation.execution_generation,
681                    &target,
682                    requested.class(),
683                )?,
684                target: target.clone(),
685            })
686            .await
687        {
688            Ok(record) => record,
689            // A failed response is not proof that start had no effect. Leave
690            // the exact generation intact so Box can reconcile it through
691            // state with the same durable Starting claim.
692            Err(error) => return Err(sdk_error("start", error)),
693        };
694        if let Err(error) =
695            validate_started_record(info, &created, &started, &target, requested.class())
696        {
697            self.cleanup_failed_launch(
698                execution_id,
699                preparation.execution_generation,
700                &created,
701                requested.class(),
702            )
703            .await;
704            return Err(error);
705        }
706        let binding = OciRuntimeBinding::from_record(self.endpoint.clone(), &id, &started)?;
707        Ok(OciRuntimeLaunch {
708            record: started,
709            binding,
710        })
711    }
712
713    async fn cleanup_failed_launch(
714        &self,
715        execution_id: &ExecutionId,
716        execution_generation: ExecutionGeneration,
717        created: &ContainerRecord,
718        required_isolation: IsolationClass,
719    ) {
720        let Ok(id) = runtime_container_id(execution_id) else {
721            return;
722        };
723        if created.state.id() != id.as_str() || created.generation.0 == 0 {
724            return;
725        }
726        // Never trust a malformed response to select a cleanup target.
727        let target = ContainerTarget::exact(id, created.generation);
728        let context = operation_context(
729            execution_id.as_str(),
730            execution_generation,
731            "failed-launch-delete",
732            required_isolation,
733        );
734        if let Ok(context) = context {
735            let _ = self
736                .client
737                .delete(DeleteRequest {
738                    context,
739                    target,
740                    mode: DeleteMode::Force,
741                })
742                .await;
743        }
744    }
745
746    async fn state_current(
747        &self,
748        execution_id: &ExecutionId,
749    ) -> ExecutionManagerResult<Option<ContainerRecord>> {
750        let id = runtime_container_id(execution_id)?;
751        match self
752            .client
753            .state(StateRequest {
754                target: ContainerTarget::current(id.clone()),
755            })
756            .await
757        {
758            Ok(record) => {
759                validate_record_id(&record, &id, "state")?;
760                Ok(Some(record))
761            }
762            Err(error) if error.code == ErrorCode::NotFound => Ok(None),
763            Err(error) => Err(sdk_error("state", error)),
764        }
765    }
766
767    async fn state_exact(
768        &self,
769        binding: &OciRuntimeBinding,
770    ) -> ExecutionManagerResult<Option<ContainerRecord>> {
771        binding.validate()?;
772        match self
773            .client
774            .state(StateRequest {
775                target: binding.target.clone(),
776            })
777            .await
778        {
779            Ok(record) => {
780                binding.validate_record(&record)?;
781                Ok(Some(record))
782            }
783            Err(error) if error.code == ErrorCode::NotFound => Ok(None),
784            Err(error) => Err(sdk_error("state", error)),
785        }
786    }
787
788    async fn start_existing(
789        &self,
790        info: &RuntimeInfo,
791        execution_id: &ExecutionId,
792        execution_generation: ExecutionGeneration,
793        operation_seed: &BoxOperationId,
794        created: &ContainerRecord,
795        isolation: ExecutionIsolation,
796    ) -> ExecutionManagerResult<OciRuntimeLaunch> {
797        let id = runtime_container_id(execution_id)?;
798        let required = oci_isolation_request(isolation).class();
799        validate_created_record(info, created, &id, required, None, None)?;
800        let target = ContainerTarget::exact(id.clone(), created.generation);
801        let started = self
802            .client
803            .start(StartRequest {
804                context: start_operation_context(
805                    operation_seed.as_str(),
806                    execution_generation,
807                    &target,
808                    required,
809                )?,
810                target: target.clone(),
811            })
812            .await
813            .map_err(|error| sdk_error("start", error))?;
814        validate_started_record(info, created, &started, &target, required)?;
815        let binding = OciRuntimeBinding::from_record(self.endpoint.clone(), &id, &started)?;
816        Ok(OciRuntimeLaunch {
817            record: started,
818            binding,
819        })
820    }
821
822    async fn wait(
823        &self,
824        binding: &OciRuntimeBinding,
825        timeout_ms: Option<u64>,
826    ) -> ExecutionManagerResult<ExitStatus> {
827        binding.validate()?;
828        let status = self
829            .client
830            .wait(WaitRequest {
831                target: binding.target.clone(),
832                timeout_ms,
833            })
834            .await
835            .map_err(|error| sdk_error("wait", error))?;
836        status
837            .validate()
838            .map_err(|error| sdk_error("wait", error))?;
839        Ok(status)
840    }
841
842    /// Read terminal evidence only after the runtime has authoritatively
843    /// reported this exact generation as stopped. A recovered runtime is
844    /// allowed to refuse an exact exit result when no authenticated reaper
845    /// survived owner death; Box must preserve that uncertainty rather than
846    /// inventing an exit code.
847    async fn wait_stopped(
848        &self,
849        record: &ContainerRecord,
850        binding: &OciRuntimeBinding,
851    ) -> ExecutionManagerResult<Option<ExitStatus>> {
852        binding.validate_record(record)?;
853        if *record.state.status() != ContainerState::Stopped {
854            return Err(ExecutionManagerError::Internal(format!(
855                "A3S OCI terminal wait was requested for non-stopped container {}",
856                binding.target.id
857            )));
858        }
859        match self
860            .client
861            .wait(WaitRequest {
862                target: binding.target.clone(),
863                timeout_ms: Some(0),
864            })
865            .await
866        {
867            Ok(status) => {
868                status
869                    .validate()
870                    .map_err(|error| sdk_error("wait", error))?;
871                Ok(Some(status))
872            }
873            Err(error) if error.code == ErrorCode::FailedPrecondition => Ok(None),
874            Err(error) => Err(sdk_error("wait", error)),
875        }
876    }
877
878    async fn wait_until(
879        &self,
880        binding: &OciRuntimeBinding,
881        timeout_ms: u64,
882    ) -> ExecutionManagerResult<Option<ExitStatus>> {
883        binding.validate()?;
884        match self
885            .client
886            .wait(WaitRequest {
887                target: binding.target.clone(),
888                timeout_ms: Some(timeout_ms),
889            })
890            .await
891        {
892            Ok(status) => {
893                status
894                    .validate()
895                    .map_err(|error| sdk_error("wait", error))?;
896                Ok(Some(status))
897            }
898            Err(error) if error.code == ErrorCode::DeadlineExceeded => Ok(None),
899            Err(error) => Err(sdk_error("wait", error)),
900        }
901    }
902
903    async fn set_paused(
904        &self,
905        execution_id: &ExecutionId,
906        execution_generation: ExecutionGeneration,
907        operation_seed: &str,
908        binding: &OciRuntimeBinding,
909        paused: bool,
910    ) -> ExecutionManagerResult<ContainerRecord> {
911        binding.validate_for(execution_id)?;
912        let (operation, label) = if paused {
913            (RuntimeOperation::Pause, "pause")
914        } else {
915            (RuntimeOperation::Resume, "resume")
916        };
917        self.require_operation(operation, label).await?;
918        let request = ContainerOperationRequest {
919            context: operation_context(
920                operation_seed,
921                execution_generation,
922                label,
923                (&binding.target, paused),
924            )?,
925            target: binding.target.clone(),
926        };
927        let result = if paused {
928            self.client.pause(request).await
929        } else {
930            self.client.resume(request).await
931        };
932        let record = match result {
933            Ok(record) => record,
934            Err(error) if error.code == ErrorCode::NotFound => {
935                return Err(ExecutionManagerError::NotFound(execution_id.clone()))
936            }
937            Err(error) => return Err(sdk_error(label, error)),
938        };
939        validate_freezer_record(binding, &record, paused, label)?;
940        Ok(record)
941    }
942
943    async fn update_resources(
944        &self,
945        execution_id: &ExecutionId,
946        execution_generation: ExecutionGeneration,
947        operation_seed: &BoxOperationId,
948        binding: &OciRuntimeBinding,
949        resources: LinuxResources,
950    ) -> ExecutionManagerResult<ContainerRecord> {
951        binding.validate_for(execution_id)?;
952        self.require_operation(RuntimeOperation::Update, "update")
953            .await?;
954        let updated = match self
955            .client
956            .update(UpdateRequest {
957                // The caller key and exact target define mutation identity.
958                // The runtime journals the full request and rejects reuse of
959                // this ID with changed resource content.
960                context: operation_context(
961                    operation_seed.as_str(),
962                    execution_generation,
963                    "update",
964                    &binding.target,
965                )?,
966                target: binding.target.clone(),
967                resources,
968            })
969            .await
970        {
971            Ok(record) => record,
972            Err(error) if error.code == ErrorCode::NotFound => {
973                return Err(ExecutionManagerError::NotFound(execution_id.clone()))
974            }
975            Err(error) => return Err(sdk_error("update", error)),
976        };
977        validate_updated_record(binding, &updated)?;
978        Ok(updated)
979    }
980
981    async fn processes(
982        &self,
983        execution_id: &ExecutionId,
984        binding: &OciRuntimeBinding,
985    ) -> ExecutionManagerResult<Vec<ProcessRecord>> {
986        binding.validate_for(execution_id)?;
987        self.require_operation(RuntimeOperation::Processes, "processes")
988            .await?;
989        let processes = match self
990            .client
991            .processes(ProcessesRequest {
992                target: binding.target.clone(),
993            })
994            .await
995        {
996            Ok(processes) => processes,
997            Err(error) if error.code == ErrorCode::NotFound => {
998                return Err(ExecutionManagerError::NotFound(execution_id.clone()))
999            }
1000            Err(error) => return Err(sdk_error("processes", error)),
1001        };
1002        validate_process_records(binding, &processes)?;
1003        Ok(processes)
1004    }
1005
1006    async fn stats(
1007        &self,
1008        execution_id: &ExecutionId,
1009        binding: &OciRuntimeBinding,
1010    ) -> ExecutionManagerResult<a3s_oci_sdk::ContainerStats> {
1011        binding.validate_for(execution_id)?;
1012        self.require_operation(RuntimeOperation::Stats, "stats")
1013            .await?;
1014        let stats = match self
1015            .client
1016            .stats(StatsRequest {
1017                target: binding.target.clone(),
1018            })
1019            .await
1020        {
1021            Ok(stats) => stats,
1022            Err(error) if error.code == ErrorCode::NotFound => {
1023                return Err(ExecutionManagerError::NotFound(execution_id.clone()))
1024            }
1025            Err(error) => return Err(sdk_error("stats", error)),
1026        };
1027        stats
1028            .validate()
1029            .map_err(|error| sdk_error("stats", error))?;
1030        if stats.target != binding.target {
1031            return Err(ExecutionManagerError::Internal(format!(
1032                "A3S OCI stats returned a different target for {execution_id}"
1033            )));
1034        }
1035        Ok(stats)
1036    }
1037
1038    async fn events(
1039        &self,
1040        execution_id: &ExecutionId,
1041        binding: &OciRuntimeBinding,
1042        request: &ExecutionEventsRequest,
1043    ) -> ExecutionManagerResult<a3s_oci_sdk::EventBatch> {
1044        binding.validate_for(execution_id)?;
1045        self.require_operation(RuntimeOperation::Events, "events")
1046            .await?;
1047        let batch = match self
1048            .client
1049            .events(EventsRequest {
1050                container: Some(binding.target.clone()),
1051                after_sequence: request.after_sequence,
1052                limit: request.limit,
1053                wait_timeout_ms: request.wait_timeout_ms,
1054            })
1055            .await
1056        {
1057            Ok(batch) => batch,
1058            Err(error) if error.code == ErrorCode::NotFound => {
1059                return Err(ExecutionManagerError::NotFound(execution_id.clone()))
1060            }
1061            Err(error) => return Err(sdk_error("events", error)),
1062        };
1063        validate_event_batch(binding, request.after_sequence, &batch)?;
1064        Ok(batch)
1065    }
1066
1067    async fn file(
1068        &self,
1069        execution_id: &ExecutionId,
1070        execution_generation: ExecutionGeneration,
1071        binding: &OciRuntimeBinding,
1072        request: BoxFileRequest,
1073    ) -> ExecutionManagerResult<BoxFileResponse> {
1074        binding.validate_for(execution_id)?;
1075        self.require_operation(RuntimeOperation::File, "file")
1076            .await?;
1077        let maximum_download_size = match (request.op, request.max_bytes) {
1078            (BoxFileOp::Upload, Some(_)) => {
1079                return Err(ExecutionManagerError::InvalidRequest(
1080                    "max_bytes is only valid for file downloads".to_string(),
1081                ))
1082            }
1083            (BoxFileOp::Download, Some(limit)) if limit == 0 || limit > MAX_BOUNDED_FILE_BYTES => {
1084                return Err(ExecutionManagerError::InvalidRequest(format!(
1085                    "download max_bytes must be between 1 and {MAX_BOUNDED_FILE_BYTES}"
1086                )))
1087            }
1088            (BoxFileOp::Download, limit) => limit,
1089            (BoxFileOp::Upload, None) => None,
1090        };
1091        let expected_upload_size = match request.op {
1092            BoxFileOp::Upload => {
1093                let encoded = request.data.as_deref().ok_or_else(|| {
1094                    ExecutionManagerError::InvalidRequest(
1095                        "file upload requires base64 data".to_string(),
1096                    )
1097                })?;
1098                let maximum_encoded = MAX_FILE_TRANSFER_BYTES.div_ceil(3) * 4;
1099                if encoded.len() > maximum_encoded {
1100                    return Err(ExecutionManagerError::InvalidRequest(format!(
1101                        "file upload payload exceeds {MAX_FILE_TRANSFER_BYTES} decoded bytes"
1102                    )));
1103                }
1104                let decoded = STANDARD.decode(encoded).map_err(|error| {
1105                    ExecutionManagerError::InvalidRequest(format!(
1106                        "file upload data is not valid base64: {error}"
1107                    ))
1108                })?;
1109                if decoded.len() > MAX_FILE_TRANSFER_BYTES {
1110                    return Err(ExecutionManagerError::InvalidRequest(format!(
1111                        "file upload payload exceeds {MAX_FILE_TRANSFER_BYTES} decoded bytes"
1112                    )));
1113                }
1114                Some(decoded.len() as u64)
1115            }
1116            BoxFileOp::Download => None,
1117        };
1118        let operation = match request.op {
1119            BoxFileOp::Upload => OciFileOp::Upload,
1120            BoxFileOp::Download => OciFileOp::Download,
1121        };
1122        let context = if request.op == BoxFileOp::Upload {
1123            Some(operation_context(
1124                &format!("session-{}", uuid::Uuid::new_v4().simple()),
1125                execution_generation,
1126                "file",
1127                (&binding.target, &request),
1128            )?)
1129        } else {
1130            None
1131        };
1132        let sdk_request = OciFileRequest {
1133            target: binding.target.clone(),
1134            op: operation,
1135            path: request.guest_path,
1136            data: request.data,
1137            user: request.user,
1138            context,
1139        };
1140        let response = match self.client.file(sdk_request.clone()).await {
1141            Err(error) if error.retryable => self.client.file(sdk_request).await,
1142            result => result,
1143        }
1144        .map_err(|error| sdk_error("file", error))?;
1145        validate_file_response(
1146            binding,
1147            &response,
1148            operation,
1149            expected_upload_size,
1150            maximum_download_size,
1151        )?;
1152        Ok(BoxFileResponse {
1153            success: true,
1154            data: response.data,
1155            size: response.size,
1156            error: None,
1157        })
1158    }
1159
1160    async fn filesystem(
1161        &self,
1162        execution_id: &ExecutionId,
1163        execution_generation: ExecutionGeneration,
1164        binding: &OciRuntimeBinding,
1165        request: BoxFilesystemRequest,
1166    ) -> ExecutionManagerResult<BoxFilesystemResponse> {
1167        binding.validate_for(execution_id)?;
1168        self.require_operation(RuntimeOperation::Filesystem, "filesystem")
1169            .await?;
1170        let operation = match request.op {
1171            BoxFilesystemOp::Stat => OciFilesystemOp::Stat,
1172            BoxFilesystemOp::MakeDir => OciFilesystemOp::MakeDir,
1173            BoxFilesystemOp::Move => OciFilesystemOp::Move,
1174            BoxFilesystemOp::ListDir => OciFilesystemOp::ListDir,
1175            BoxFilesystemOp::Remove => OciFilesystemOp::Remove,
1176        };
1177        let context = if operation.is_mutating() {
1178            Some(operation_context(
1179                &format!("session-{}", uuid::Uuid::new_v4().simple()),
1180                execution_generation,
1181                "filesystem",
1182                (&binding.target, &request),
1183            )?)
1184        } else {
1185            None
1186        };
1187        let sdk_request = OciFilesystemRequest {
1188            target: binding.target.clone(),
1189            op: operation,
1190            path: request.path,
1191            destination: request.destination,
1192            depth: request.depth,
1193            user: request.user,
1194            context,
1195        };
1196        let response = match self.client.filesystem(sdk_request.clone()).await {
1197            Err(error) if error.retryable => self.client.filesystem(sdk_request).await,
1198            result => result,
1199        }
1200        .map_err(|error| sdk_error("filesystem", error))?;
1201        validate_filesystem_response(binding, &response, operation)?;
1202        Ok(BoxFilesystemResponse {
1203            success: true,
1204            entry: response.entry.map(map_filesystem_entry),
1205            entries: response
1206                .entries
1207                .into_iter()
1208                .map(map_filesystem_entry)
1209                .collect(),
1210            error: None,
1211        })
1212    }
1213
1214    async fn kill(
1215        &self,
1216        execution_id: &ExecutionId,
1217        execution_generation: ExecutionGeneration,
1218        binding: &OciRuntimeBinding,
1219        signal: Signal,
1220    ) -> ExecutionManagerResult<ContainerRecord> {
1221        binding.validate_for(execution_id)?;
1222        let killed = self
1223            .client
1224            .kill(KillRequest {
1225                context: operation_context(
1226                    execution_id.as_str(),
1227                    execution_generation,
1228                    "kill",
1229                    signal.get(),
1230                )?,
1231                target: binding.target.clone(),
1232                signal,
1233                all: true,
1234            })
1235            .await
1236            .map_err(|error| sdk_error("kill", error))?;
1237        binding.validate_record(&killed)?;
1238        Ok(killed)
1239    }
1240
1241    async fn delete(
1242        &self,
1243        execution_id: &ExecutionId,
1244        execution_generation: ExecutionGeneration,
1245        binding: &OciRuntimeBinding,
1246        mode: DeleteMode,
1247    ) -> ExecutionManagerResult<()> {
1248        binding.validate_for(execution_id)?;
1249        let request = DeleteRequest {
1250            context: operation_context(
1251                execution_id.as_str(),
1252                execution_generation,
1253                "delete",
1254                mode,
1255            )?,
1256            target: binding.target.clone(),
1257            mode,
1258        };
1259        match self.client.delete(request).await {
1260            Ok(()) => Ok(()),
1261            Err(error) if error.code == ErrorCode::NotFound => Ok(()),
1262            Err(error) => Err(sdk_error("delete", error)),
1263        }
1264    }
1265}
1266
1267/// Opt-in canonical local-execution backend over one A3S OCI host service.
1268#[derive(Clone)]
1269pub struct OciLocalExecutionBackend {
1270    adapter: OciLifecycleAdapter,
1271    provider: Arc<dyn OciBundleProvider>,
1272}
1273
1274impl OciLocalExecutionBackend {
1275    /// Construct from an already connected public SDK client.
1276    pub fn from_client(
1277        endpoint: OciRuntimeEndpoint,
1278        client: RuntimeClient,
1279        provider: Arc<dyn OciBundleProvider>,
1280    ) -> ExecutionManagerResult<Self> {
1281        Ok(Self {
1282            adapter: OciLifecycleAdapter::from_client(endpoint, client)?,
1283            provider,
1284        })
1285    }
1286
1287    /// Connect to a local host service and retain only its public SDK boundary.
1288    pub async fn connect(
1289        endpoint: OciRuntimeEndpoint,
1290        provider: Arc<dyn OciBundleProvider>,
1291    ) -> ExecutionManagerResult<Self> {
1292        Ok(Self {
1293            adapter: OciLifecycleAdapter::connect(endpoint).await?,
1294            provider,
1295        })
1296    }
1297
1298    pub(super) fn metadata<'a>(
1299        &self,
1300        record: &'a BoxRecord,
1301    ) -> ExecutionManagerResult<&'a crate::ManagedExecutionMetadata> {
1302        let metadata = record.managed_execution.as_ref().ok_or_else(|| {
1303            ExecutionManagerError::Internal(format!(
1304                "execution {} has no managed lifecycle metadata",
1305                record.id
1306            ))
1307        })?;
1308        metadata
1309            .validate()
1310            .map_err(|error| ExecutionManagerError::Internal(error.to_string()))?;
1311        if metadata.runtime_route == crate::ManagedRuntimeRoute::BoxVm {
1312            return Err(ExecutionManagerError::Internal(format!(
1313                "managed execution {} is pinned to the Box VM route",
1314                record.id
1315            )));
1316        }
1317        Ok(metadata)
1318    }
1319
1320    pub(super) fn execution_id(&self, record: &BoxRecord) -> ExecutionManagerResult<ExecutionId> {
1321        ExecutionId::new(record.id.clone())
1322            .map_err(|error| ExecutionManagerError::Internal(error.to_string()))
1323    }
1324
1325    pub(super) fn binding(
1326        &self,
1327        record: &BoxRecord,
1328    ) -> ExecutionManagerResult<Option<OciRuntimeBinding>> {
1329        let execution_id = self.execution_id(record)?;
1330        let binding = self.metadata(record)?.oci_runtime.clone();
1331        if let Some(binding) = &binding {
1332            binding.validate_for(&execution_id)?;
1333            if binding.endpoint != self.adapter.endpoint {
1334                return Err(ExecutionManagerError::Internal(format!(
1335                    "execution {execution_id} belongs to a different A3S OCI endpoint"
1336                )));
1337            }
1338        }
1339        Ok(binding)
1340    }
1341
1342    pub(super) fn client(&self) -> RuntimeClient {
1343        self.adapter.client()
1344    }
1345
1346    fn preparation_context(
1347        &self,
1348        record: &BoxRecord,
1349        info: &RuntimeInfo,
1350    ) -> ExecutionManagerResult<OciBundlePreparationContext> {
1351        let execution_id = self.execution_id(record)?;
1352        let metadata = self.metadata(record)?;
1353        let isolation = oci_isolation_request(record.isolation);
1354        let runtime_container_id = runtime_container_id(&execution_id)?;
1355        let create_context = create_operation_context(
1356            metadata.operation_id.as_str(),
1357            metadata.generation,
1358            &runtime_container_id,
1359            isolation.class(),
1360        )?;
1361        Ok(OciBundlePreparationContext {
1362            runtime_container_id,
1363            create_context,
1364            isolation,
1365            attachment_capabilities: info.attachments.clone(),
1366            execution_generation: metadata.generation,
1367            operation_seed: metadata.operation_id.clone(),
1368        })
1369    }
1370
1371    fn handle(
1372        &self,
1373        record: &BoxRecord,
1374        runtime: &ContainerRecord,
1375        binding: OciRuntimeBinding,
1376        console_log: PathBuf,
1377        anonymous_volumes: Vec<String>,
1378    ) -> ExecutionManagerResult<LocalExecutionHandle> {
1379        let (pid, pid_start_time) = runtime_process_identity(&binding, runtime)?;
1380        Ok(LocalExecutionHandle {
1381            started_at: record.started_at.unwrap_or_else(Utc::now),
1382            pid,
1383            pid_start_time,
1384            exec_socket_path: PathBuf::new(),
1385            console_log,
1386            anonymous_volumes,
1387            oci_runtime: Some(binding),
1388        })
1389    }
1390
1391    async fn current_runtime(
1392        &self,
1393        record: &BoxRecord,
1394    ) -> ExecutionManagerResult<Option<(ContainerRecord, OciRuntimeBinding)>> {
1395        let execution_id = self.execution_id(record)?;
1396        if let Some(binding) = self.binding(record)? {
1397            return Ok(self
1398                .adapter
1399                .state_exact(&binding)
1400                .await?
1401                .map(|runtime| (runtime, binding)));
1402        }
1403        let Some(runtime) = self.adapter.state_current(&execution_id).await? else {
1404            return Ok(None);
1405        };
1406        let info = self.adapter.require_isolation(record.isolation).await?;
1407        validate_recovered_record(
1408            &info,
1409            &runtime,
1410            &runtime_container_id(&execution_id)?,
1411            oci_isolation_request(record.isolation).class(),
1412        )?;
1413        let binding = OciRuntimeBinding::from_record(
1414            self.adapter.endpoint.clone(),
1415            &runtime_container_id(&execution_id)?,
1416            &runtime,
1417        )?;
1418        Ok(Some((runtime, binding)))
1419    }
1420
1421    async fn terminal_observation(
1422        &self,
1423        record: &BoxRecord,
1424        runtime: &ContainerRecord,
1425        binding: &OciRuntimeBinding,
1426    ) -> ExecutionManagerResult<LocalExecutionObservation> {
1427        let execution_id = self.execution_id(record)?;
1428        let generation = self.metadata(record)?.generation;
1429        let status = self.adapter.wait_stopped(runtime, binding).await?;
1430        if status.is_some() {
1431            self.provider.ensure_log_projection(record, binding).await?;
1432            self.provider
1433                .wait_log_projection_drained(record, binding)
1434                .await?;
1435        } else {
1436            self.provider
1437                .wait_log_projection_stopped_after_owner_loss(record, binding)
1438                .await?;
1439        }
1440        self.adapter
1441            .delete(&execution_id, generation, binding, DeleteMode::StoppedOnly)
1442            .await?;
1443        self.provider.cleanup(record).await?;
1444        Ok(LocalExecutionObservation {
1445            state: ExecutionState::Stopped,
1446            handle: None,
1447            exit_code: status.as_ref().map(exit_code).transpose()?,
1448        })
1449    }
1450}
1451
1452fn managed_resource_home(record: &BoxRecord) -> ExecutionManagerResult<PathBuf> {
1453    let boxes = record.box_dir.parent().ok_or_else(|| {
1454        ExecutionManagerError::Internal(format!(
1455            "managed OCI execution {} has no boxes directory",
1456            record.id
1457        ))
1458    })?;
1459    let home = boxes.parent().ok_or_else(|| {
1460        ExecutionManagerError::Internal(format!(
1461            "managed OCI execution {} has no runtime home directory",
1462            record.id
1463        ))
1464    })?;
1465    if boxes.file_name().and_then(|name| name.to_str()) != Some("boxes")
1466        || home.join("boxes").join(&record.id) != record.box_dir
1467    {
1468        return Err(ExecutionManagerError::Internal(format!(
1469            "managed OCI execution {} has an unexpected host directory {}",
1470            record.id,
1471            record.box_dir.display()
1472        )));
1473    }
1474    Ok(home.to_path_buf())
1475}
1476
1477async fn rollback_execution_resources(resources: ExecutionResourceGuard, record: &BoxRecord) {
1478    let execution_id = record.id.clone();
1479    if let Err(error) = tokio::task::spawn_blocking(move || resources.rollback()).await {
1480        tracing::warn!(
1481            %execution_id,
1482            %error,
1483            "Managed OCI resource rollback task failed"
1484        );
1485    }
1486}
1487
1488#[async_trait]
1489impl LocalExecutionBackend for OciLocalExecutionBackend {
1490    fn route_for_create(
1491        &self,
1492        _record: &BoxRecord,
1493    ) -> ExecutionManagerResult<crate::ManagedRuntimeRoute> {
1494        Ok(crate::ManagedRuntimeRoute::OciSdk)
1495    }
1496
1497    async fn preflight(&self, record: &BoxRecord) -> ExecutionManagerResult<()> {
1498        self.metadata(record)?;
1499        let info = self.adapter.require_isolation(record.isolation).await?;
1500        let context = self.preparation_context(record, &info)?;
1501        self.provider.preflight(record, &context)
1502    }
1503
1504    async fn start(&self, record: &BoxRecord) -> ExecutionManagerResult<LocalExecutionHandle> {
1505        let execution_id = self.execution_id(record)?;
1506        self.metadata(record)?;
1507        if let Some((runtime, binding)) = self.current_runtime(record).await? {
1508            if *runtime.state.status() == ContainerState::Running {
1509                self.provider
1510                    .ensure_log_projection(record, &binding)
1511                    .await?;
1512                return self.handle(
1513                    record,
1514                    &runtime,
1515                    binding,
1516                    record.console_log.clone(),
1517                    record.anonymous_volumes.clone(),
1518                );
1519            }
1520            return Err(ExecutionManagerError::Conflict {
1521                execution_id,
1522                message: format!(
1523                    "A3S OCI already contains runtime state {:?} for this generation",
1524                    runtime.state.status()
1525                ),
1526            });
1527        }
1528
1529        let info = self.adapter.require_isolation(record.isolation).await?;
1530        let preparation = self.preparation_context(record, &info)?;
1531        self.provider.preflight(record, &preparation)?;
1532        let resource_home = managed_resource_home(record)?;
1533        let resource_record = record.clone();
1534        let resources = tokio::task::spawn_blocking(move || {
1535            ExecutionResourceGuard::prepare(&resource_home, &resource_record)
1536        })
1537        .await
1538        .map_err(|error| {
1539            ExecutionManagerError::Internal(format!(
1540                "managed OCI resource preparation task failed for {}: {error}",
1541                record.id
1542            ))
1543        })??;
1544        let prepared = match self.provider.prepare(record, &preparation).await {
1545            Ok(prepared) => prepared,
1546            Err(error) => {
1547                rollback_execution_resources(resources, record).await;
1548                return Err(error);
1549            }
1550        };
1551        if let Err(error) = prepared.validate_for(record, &preparation) {
1552            let cleanup = self.provider.cleanup(record).await;
1553            rollback_execution_resources(resources, record).await;
1554            return match cleanup {
1555                Ok(()) => Err(error),
1556                Err(cleanup) => Err(ExecutionManagerError::Internal(format!(
1557                    "{error}; Box OCI preparation cleanup also failed: {cleanup}"
1558                ))),
1559            };
1560        }
1561        let console_log = prepared.console_log.clone();
1562        let anonymous_volumes = prepared.anonymous_volumes.clone();
1563        let provider = Arc::clone(&self.provider);
1564        let projection_record = record.clone();
1565        let launch = self
1566            .adapter
1567            .launch_preflighted(
1568                &info,
1569                &execution_id,
1570                preparation,
1571                prepared,
1572                move |binding| async move {
1573                    provider
1574                        .ensure_log_projection(&projection_record, &binding)
1575                        .await
1576                },
1577            )
1578            .await;
1579        match launch {
1580            Ok(launch) if *launch.record.state.status() == ContainerState::Running => {
1581                let handle = self.handle(
1582                    record,
1583                    &launch.record,
1584                    launch.binding,
1585                    console_log,
1586                    anonymous_volumes,
1587                );
1588                // The runtime generation owns the prepared rootfs even when
1589                // publishing its host-process identity fails closed.
1590                resources.disarm();
1591                handle
1592            }
1593            Ok(_) => {
1594                // A runtime generation exists and owns the prepared rootfs even
1595                // when it completed before Box could publish Running. Terminal
1596                // reconciliation performs the normal provider/resource cleanup.
1597                resources.disarm();
1598                Err(ExecutionManagerError::Unavailable(format!(
1599                    "execution {execution_id} completed while A3S OCI startup was being published"
1600                )))
1601            }
1602            Err(error) => {
1603                // Unknown create/start outcomes must be reconciled, not erased.
1604                // Cleanup product preparation only when the runtime proves no
1605                // current generation exists for the deterministic ID.
1606                match self.adapter.state_current(&execution_id).await {
1607                    Ok(Some(_)) => {
1608                        resources.disarm();
1609                        return Err(error);
1610                    }
1611                    Err(reconcile_error) => {
1612                        resources.disarm();
1613                        return Err(ExecutionManagerError::Unavailable(format!(
1614                            "{error}; A3S OCI launch ownership could not be reconciled and Box preparation was retained: {reconcile_error}"
1615                        )));
1616                    }
1617                    Ok(None) => {}
1618                }
1619                let cleanup = self.provider.cleanup(record).await;
1620                rollback_execution_resources(resources, record).await;
1621                match cleanup {
1622                    Ok(()) => Err(error),
1623                    Err(cleanup) => Err(ExecutionManagerError::Internal(format!(
1624                        "{error}; Box OCI preparation cleanup also failed: {cleanup}"
1625                    ))),
1626                }
1627            }
1628        }
1629    }
1630
1631    async fn inspect(
1632        &self,
1633        record: &BoxRecord,
1634    ) -> ExecutionManagerResult<LocalExecutionObservation> {
1635        let execution_id = self.execution_id(record)?;
1636        let metadata = self.metadata(record)?;
1637        let had_binding = self.binding(record)?.is_some();
1638        let Some((mut runtime, mut binding)) = self.current_runtime(record).await? else {
1639            if had_binding {
1640                self.provider.cleanup(record).await?;
1641            }
1642            return Err(ExecutionManagerError::NotFound(execution_id));
1643        };
1644
1645        if *runtime.state.status() == ContainerState::Created
1646            && matches!(
1647                record.managed_state(),
1648                Ok(Some(
1649                    ManagedExecutionState::Starting | ManagedExecutionState::RestartStarting
1650                ))
1651            )
1652        {
1653            self.provider
1654                .ensure_log_projection(record, &binding)
1655                .await?;
1656            let info = self.adapter.require_isolation(record.isolation).await?;
1657            let launch = self
1658                .adapter
1659                .start_existing(
1660                    &info,
1661                    &execution_id,
1662                    metadata.generation,
1663                    &metadata.operation_id,
1664                    &runtime,
1665                    record.isolation,
1666                )
1667                .await?;
1668            runtime = launch.record;
1669            binding = launch.binding;
1670        }
1671
1672        if *runtime.state.status() == ContainerState::Running {
1673            self.provider
1674                .ensure_log_projection(record, &binding)
1675                .await?;
1676        }
1677
1678        match *runtime.state.status() {
1679            ContainerState::Creating | ContainerState::Created => Ok(LocalExecutionObservation {
1680                state: ExecutionState::Creating,
1681                handle: None,
1682                exit_code: None,
1683            }),
1684            ContainerState::Running if runtime.is_paused() => Ok(LocalExecutionObservation {
1685                state: ExecutionState::Paused,
1686                handle: Some(self.handle(
1687                    record,
1688                    &runtime,
1689                    binding,
1690                    record.console_log.clone(),
1691                    record.anonymous_volumes.clone(),
1692                )?),
1693                exit_code: None,
1694            }),
1695            ContainerState::Running => Ok(LocalExecutionObservation {
1696                state: ExecutionState::Running,
1697                handle: Some(self.handle(
1698                    record,
1699                    &runtime,
1700                    binding,
1701                    record.console_log.clone(),
1702                    record.anonymous_volumes.clone(),
1703                )?),
1704                exit_code: None,
1705            }),
1706            ContainerState::Stopped => self.terminal_observation(record, &runtime, &binding).await,
1707        }
1708    }
1709
1710    async fn pause(
1711        &self,
1712        record: &BoxRecord,
1713        keep_memory: bool,
1714    ) -> ExecutionManagerResult<LocalExecutionHandle> {
1715        if !keep_memory {
1716            return Err(ExecutionManagerError::InvalidRequest(format!(
1717                "A3S OCI in-place pause requires memory retention for {}",
1718                record.id
1719            )));
1720        }
1721        let execution_id = self.execution_id(record)?;
1722        let generation = self.metadata(record)?.generation;
1723        let operation_seed = freezer_operation_seed(record, &execution_id, true)?;
1724        let binding = self.binding(record)?.ok_or_else(|| {
1725            ExecutionManagerError::Internal(format!(
1726                "execution {execution_id} has no exact A3S OCI binding to pause"
1727            ))
1728        })?;
1729        let runtime = self
1730            .adapter
1731            .set_paused(&execution_id, generation, &operation_seed, &binding, true)
1732            .await?;
1733        self.handle(
1734            record,
1735            &runtime,
1736            binding,
1737            record.console_log.clone(),
1738            record.anonymous_volumes.clone(),
1739        )
1740    }
1741
1742    async fn resume(&self, record: &BoxRecord) -> ExecutionManagerResult<LocalExecutionHandle> {
1743        let execution_id = self.execution_id(record)?;
1744        let generation = self.metadata(record)?.generation;
1745        let operation_seed = freezer_operation_seed(record, &execution_id, false)?;
1746        let binding = self.binding(record)?.ok_or_else(|| {
1747            ExecutionManagerError::Internal(format!(
1748                "execution {execution_id} has no exact A3S OCI binding to resume"
1749            ))
1750        })?;
1751        let runtime = self
1752            .adapter
1753            .set_paused(&execution_id, generation, &operation_seed, &binding, false)
1754            .await?;
1755        self.handle(
1756            record,
1757            &runtime,
1758            binding,
1759            record.console_log.clone(),
1760            record.anonymous_volumes.clone(),
1761        )
1762    }
1763
1764    async fn preflight_resource_update(
1765        &self,
1766        record: &BoxRecord,
1767        update: &ExecutionResourceUpdate,
1768    ) -> ExecutionManagerResult<()> {
1769        update.validate()?;
1770        let execution_id = self.execution_id(record)?;
1771        self.binding(record)?.ok_or_else(|| {
1772            ExecutionManagerError::Internal(format!(
1773                "execution {execution_id} has no exact A3S OCI binding to update"
1774            ))
1775        })?;
1776        self.adapter
1777            .require_operation(RuntimeOperation::Update, "update")
1778            .await?;
1779        compile_resource_update(record, update)?;
1780        Ok(())
1781    }
1782
1783    async fn update_resources(
1784        &self,
1785        record: &BoxRecord,
1786        operation_id: &BoxOperationId,
1787        update: &ExecutionResourceUpdate,
1788    ) -> ExecutionManagerResult<()> {
1789        let execution_id = self.execution_id(record)?;
1790        let generation = self.metadata(record)?.generation;
1791        let binding = self.binding(record)?.ok_or_else(|| {
1792            ExecutionManagerError::Internal(format!(
1793                "execution {execution_id} has no exact A3S OCI binding to update"
1794            ))
1795        })?;
1796        let resources = compile_resource_update(record, update)?;
1797        self.adapter
1798            .update_resources(&execution_id, generation, operation_id, &binding, resources)
1799            .await?;
1800        Ok(())
1801    }
1802
1803    async fn list_processes(
1804        &self,
1805        record: &BoxRecord,
1806    ) -> ExecutionManagerResult<ExecutionProcessInventory> {
1807        let execution_id = self.execution_id(record)?;
1808        let generation = self.metadata(record)?.generation;
1809        let binding = self.binding(record)?.ok_or_else(|| {
1810            ExecutionManagerError::Internal(format!(
1811                "execution {execution_id} has no exact A3S OCI binding for process inventory"
1812            ))
1813        })?;
1814        let mut processes = self
1815            .adapter
1816            .processes(&execution_id, &binding)
1817            .await?
1818            .into_iter()
1819            .map(|process| ExecutionProcessInfo {
1820                process_id: process.target.process_id.to_string(),
1821                pid: process.pid,
1822                terminal: process.terminal,
1823            })
1824            .collect::<Vec<_>>();
1825        processes.sort_by(|left, right| left.process_id.cmp(&right.process_id));
1826        let inventory = ExecutionProcessInventory {
1827            execution_id,
1828            generation,
1829            processes,
1830        };
1831        inventory.validate()?;
1832        Ok(inventory)
1833    }
1834
1835    async fn stats(&self, record: &BoxRecord) -> ExecutionManagerResult<ExecutionStats> {
1836        let execution_id = self.execution_id(record)?;
1837        let generation = self.metadata(record)?.generation;
1838        let binding = self.binding(record)?.ok_or_else(|| {
1839            ExecutionManagerError::Internal(format!(
1840                "execution {execution_id} has no exact A3S OCI binding for stats"
1841            ))
1842        })?;
1843        let stats = self.adapter.stats(&execution_id, &binding).await?;
1844        let stats = ExecutionStats {
1845            execution_id,
1846            generation,
1847            timestamp_unix_ns: stats.timestamp_unix_ns,
1848            cpu: ExecutionCpuStats {
1849                usage_ns: stats.cpu.usage_ns,
1850                user_ns: stats.cpu.user_ns,
1851                system_ns: stats.cpu.system_ns,
1852                throttled_ns: stats.cpu.throttled_ns,
1853            },
1854            memory: ExecutionMemoryStats {
1855                usage_bytes: stats.memory.usage_bytes,
1856                limit_bytes: stats.memory.limit_bytes,
1857                peak_bytes: stats.memory.peak_bytes,
1858            },
1859            process_count: stats.process_count,
1860            metrics: stats.metrics,
1861        };
1862        stats.validate()?;
1863        Ok(stats)
1864    }
1865
1866    async fn events(
1867        &self,
1868        record: &BoxRecord,
1869        request: ExecutionEventsRequest,
1870    ) -> ExecutionManagerResult<ExecutionEventBatch> {
1871        request.validate()?;
1872        let execution_id = self.execution_id(record)?;
1873        let generation = self.metadata(record)?.generation;
1874        let binding = self.binding(record)?.ok_or_else(|| {
1875            ExecutionManagerError::Internal(format!(
1876                "execution {execution_id} has no exact A3S OCI binding for events"
1877            ))
1878        })?;
1879        let batch = self
1880            .adapter
1881            .events(&execution_id, &binding, &request)
1882            .await?;
1883        let events = batch
1884            .events
1885            .into_iter()
1886            .map(|event| {
1887                Ok(ExecutionRuntimeEvent {
1888                    sequence: event.sequence,
1889                    timestamp_unix_ns: event.timestamp_unix_ns,
1890                    process_id: event.process_id.map(|process_id| process_id.to_string()),
1891                    kind: map_event_kind(event.kind)?,
1892                    attributes: event.attributes,
1893                })
1894            })
1895            .collect::<ExecutionManagerResult<Vec<_>>>()?;
1896        let batch = ExecutionEventBatch {
1897            execution_id,
1898            generation,
1899            events,
1900            next_sequence: batch.next_sequence,
1901        };
1902        batch.validate_after(request.after_sequence)?;
1903        Ok(batch)
1904    }
1905
1906    async fn execute(
1907        &self,
1908        record: &BoxRecord,
1909        request: BoxExecRequest,
1910    ) -> ExecutionManagerResult<ExecOutput> {
1911        super::oci_session::execute(self, record, request).await
1912    }
1913
1914    async fn start_process(
1915        &self,
1916        record: &BoxRecord,
1917        request: BoxExecRequest,
1918    ) -> ExecutionManagerResult<ExecutionProcess> {
1919        super::oci_session::start_process(self, record, request).await
1920    }
1921
1922    async fn start_pty(
1923        &self,
1924        record: &BoxRecord,
1925        request: PtyRequest,
1926    ) -> ExecutionManagerResult<ExecutionProcess> {
1927        super::oci_session::start_pty(self, record, request).await
1928    }
1929
1930    async fn transfer_file(
1931        &self,
1932        record: &BoxRecord,
1933        request: BoxFileRequest,
1934    ) -> ExecutionManagerResult<BoxFileResponse> {
1935        let execution_id = self.execution_id(record)?;
1936        let generation = self.metadata(record)?.generation;
1937        let binding = self.binding(record)?.ok_or_else(|| {
1938            ExecutionManagerError::Internal(format!(
1939                "execution {execution_id} has no exact A3S OCI binding for file transfer"
1940            ))
1941        })?;
1942        self.adapter
1943            .file(&execution_id, generation, &binding, request)
1944            .await
1945    }
1946
1947    async fn filesystem(
1948        &self,
1949        record: &BoxRecord,
1950        request: BoxFilesystemRequest,
1951    ) -> ExecutionManagerResult<BoxFilesystemResponse> {
1952        let execution_id = self.execution_id(record)?;
1953        let generation = self.metadata(record)?.generation;
1954        let binding = self.binding(record)?.ok_or_else(|| {
1955            ExecutionManagerError::Internal(format!(
1956                "execution {execution_id} has no exact A3S OCI binding for filesystem access"
1957            ))
1958        })?;
1959        self.adapter
1960            .filesystem(&execution_id, generation, &binding, request)
1961            .await
1962    }
1963
1964    async fn kill(&self, record: &BoxRecord) -> ExecutionManagerResult<KillOutcome> {
1965        Ok(self.kill_with_status(record).await?.outcome)
1966    }
1967
1968    async fn kill_with_status(
1969        &self,
1970        record: &BoxRecord,
1971    ) -> ExecutionManagerResult<LocalExecutionTermination> {
1972        let execution_id = self.execution_id(record)?;
1973        let generation = self.metadata(record)?.generation;
1974        let Some((runtime, binding)) = self.current_runtime(record).await? else {
1975            self.provider.cleanup(record).await?;
1976            return Ok(LocalExecutionTermination {
1977                outcome: KillOutcome::AlreadyStopped,
1978                exit_code: record.exit_code,
1979            });
1980        };
1981        self.provider
1982            .ensure_log_projection(record, &binding)
1983            .await?;
1984        let was_stopped = *runtime.state.status() == ContainerState::Stopped;
1985        let mut status = None;
1986        if !was_stopped {
1987            let signal_number = record
1988                .stop_signal
1989                .as_deref()
1990                .map(a3s_box_core::vmm::parse_signal_name)
1991                .unwrap_or(DEFAULT_KILL_SIGNAL);
1992            let signal = Signal::new(signal_number).map_err(|error| sdk_error("kill", error))?;
1993            let graceful_timeout_ms = if signal_number == DEFAULT_KILL_SIGNAL {
1994                None
1995            } else {
1996                record
1997                    .stop_timeout
1998                    .map(|timeout_secs| {
1999                        timeout_secs.checked_mul(1_000).ok_or_else(|| {
2000                            ExecutionManagerError::InvalidRequest(format!(
2001                                "stop timeout is too large for execution {execution_id}"
2002                            ))
2003                        })
2004                    })
2005                    .transpose()?
2006            };
2007            self.adapter
2008                .kill(&execution_id, generation, &binding, signal)
2009                .await?;
2010            if let Some(timeout_ms) = graceful_timeout_ms {
2011                status = self.adapter.wait_until(&binding, timeout_ms).await?;
2012                if status.is_none() {
2013                    let force = Signal::new(DEFAULT_KILL_SIGNAL)
2014                        .map_err(|error| sdk_error("kill", error))?;
2015                    self.adapter
2016                        .kill(&execution_id, generation, &binding, force)
2017                        .await?;
2018                }
2019            }
2020        }
2021        let status = match status {
2022            Some(status) => status,
2023            None => self.adapter.wait(&binding, None).await?,
2024        };
2025        self.provider
2026            .wait_log_projection_drained(record, &binding)
2027            .await?;
2028        self.adapter
2029            .delete(&execution_id, generation, &binding, DeleteMode::StoppedOnly)
2030            .await?;
2031        self.provider.cleanup(record).await?;
2032        Ok(LocalExecutionTermination {
2033            outcome: if was_stopped {
2034                KillOutcome::AlreadyStopped
2035            } else {
2036                KillOutcome::Killed
2037            },
2038            exit_code: Some(exit_code(&status)?),
2039        })
2040    }
2041}
2042
2043/// Map product isolation to an OCI isolation requirement without selecting a driver.
2044#[must_use]
2045pub const fn oci_isolation_request(isolation: ExecutionIsolation) -> IsolationRequest {
2046    match isolation {
2047        ExecutionIsolation::Microvm => IsolationRequest::DedicatedVm,
2048        ExecutionIsolation::Sandbox => IsolationRequest::SharedHostKernel,
2049    }
2050}
2051
2052fn runtime_container_id(execution_id: &ExecutionId) -> ExecutionManagerResult<ContainerId> {
2053    ContainerId::new(format!("a3s-box-{execution_id}"))
2054        .map_err(|error| ExecutionManagerError::Internal(error.to_string()))
2055}
2056
2057fn freezer_operation_seed(
2058    record: &BoxRecord,
2059    execution_id: &ExecutionId,
2060    paused: bool,
2061) -> ExecutionManagerResult<String> {
2062    let operation = record
2063        .managed_execution
2064        .as_ref()
2065        .and_then(|metadata| metadata.pending_operation.as_ref());
2066    match (paused, operation) {
2067        (true, Some(crate::ManagedExecutionOperation::Pause { operation_id, .. }))
2068        | (false, Some(crate::ManagedExecutionOperation::Resume { operation_id })) => {
2069            // Legacy transitional records predate explicit freezer mutation
2070            // IDs. OCI routing did not exist when they were written, so the
2071            // exact Box identity remains a safe one-time recovery seed.
2072            Ok(operation_id
2073                .as_ref()
2074                .map(|operation_id| operation_id.as_str())
2075                .unwrap_or_else(|| execution_id.as_str())
2076                .to_string())
2077        }
2078        (
2079            _,
2080            Some(crate::ManagedExecutionOperation::Snapshot {
2081                snapshot_id,
2082                source_state: ManagedExecutionState::Running,
2083                operation_id,
2084                ..
2085            }),
2086        ) => Ok(operation_id
2087            .as_ref()
2088            .map(|operation_id| operation_id.as_str().to_string())
2089            .unwrap_or_else(|| format!("legacy-snapshot-{execution_id}-{snapshot_id}"))),
2090        _ => Err(ExecutionManagerError::Internal(format!(
2091            "execution {execution_id} has no matching durable OCI freezer claim"
2092        ))),
2093    }
2094}
2095
2096pub(super) fn operation_context(
2097    seed: &str,
2098    generation: ExecutionGeneration,
2099    operation: &str,
2100    payload: impl Serialize,
2101) -> ExecutionManagerResult<OperationContext> {
2102    let payload = serde_json::to_vec(&payload).map_err(|error| {
2103        ExecutionManagerError::Internal(format!(
2104            "failed to encode A3S OCI {operation} operation identity: {error}"
2105        ))
2106    })?;
2107    let mut digest = Sha256::new();
2108    digest.update(seed.as_bytes());
2109    digest.update([0]);
2110    digest.update(generation.get().to_be_bytes());
2111    digest.update([0]);
2112    digest.update(operation.as_bytes());
2113    digest.update([0]);
2114    digest.update(payload);
2115    let id = format!("a3s-box-{operation}-{}", hex::encode(digest.finalize()));
2116    OciOperationId::new(id)
2117        .map(OperationContext::new)
2118        .map_err(|error| sdk_error(operation, error))
2119}
2120
2121fn create_operation_context(
2122    seed: &str,
2123    generation: ExecutionGeneration,
2124    runtime_container_id: &ContainerId,
2125    isolation: IsolationClass,
2126) -> ExecutionManagerResult<OperationContext> {
2127    operation_context(
2128        seed,
2129        generation,
2130        "create",
2131        (runtime_container_id, isolation),
2132    )
2133}
2134
2135fn start_operation_context(
2136    seed: &str,
2137    generation: ExecutionGeneration,
2138    target: &ContainerTarget,
2139    isolation: IsolationClass,
2140) -> ExecutionManagerResult<OperationContext> {
2141    operation_context(seed, generation, "start", (target, isolation))
2142}
2143
2144fn validate_created_record(
2145    info: &RuntimeInfo,
2146    record: &ContainerRecord,
2147    expected_id: &ContainerId,
2148    required_isolation: IsolationClass,
2149    expected_config_digest: Option<&str>,
2150    expected_attachments_digest: Option<&str>,
2151) -> ExecutionManagerResult<()> {
2152    validate_record_id(record, expected_id, "create")?;
2153    if *record.state.status() != ContainerState::Created
2154        || record.generation.0 == 0
2155        || record.isolation != required_isolation
2156    {
2157        return Err(ExecutionManagerError::Internal(format!(
2158            "A3S OCI create returned an invalid state, generation, or isolation for {expected_id}"
2159        )));
2160    }
2161    validate_config_digest(&record.config_digest)?;
2162    let attachments_digest = validate_record_attachments(record, "create")?;
2163    if expected_config_digest.is_some_and(|expected| record.config_digest != expected) {
2164        return Err(ExecutionManagerError::Internal(format!(
2165            "A3S OCI create returned configuration evidence that differs from the submitted bundle for {expected_id}"
2166        )));
2167    }
2168    if expected_attachments_digest.is_some_and(|expected| attachments_digest != expected) {
2169        return Err(ExecutionManagerError::Internal(format!(
2170            "A3S OCI create returned attachment evidence that differs from the submitted manifest for {expected_id}"
2171        )));
2172    }
2173    validate_selected_driver(info, record)
2174}
2175
2176fn validate_recovered_record(
2177    info: &RuntimeInfo,
2178    record: &ContainerRecord,
2179    expected_id: &ContainerId,
2180    required_isolation: IsolationClass,
2181) -> ExecutionManagerResult<()> {
2182    validate_record_id(record, expected_id, "recover")?;
2183    if record.generation.0 == 0 || record.isolation != required_isolation {
2184        return Err(ExecutionManagerError::Internal(format!(
2185            "A3S OCI recovery returned an invalid generation or isolation for {expected_id}"
2186        )));
2187    }
2188    validate_config_digest(&record.config_digest)?;
2189    validate_record_attachments(record, "recover")?;
2190    validate_selected_driver(info, record)
2191}
2192
2193fn validate_started_record(
2194    info: &RuntimeInfo,
2195    created: &ContainerRecord,
2196    started: &ContainerRecord,
2197    target: &ContainerTarget,
2198    required_isolation: IsolationClass,
2199) -> ExecutionManagerResult<()> {
2200    validate_record_id(started, &target.id, "start")?;
2201    if Some(started.generation) != target.generation
2202        || started.driver != created.driver
2203        || started.isolation != required_isolation
2204        || started.config_digest != created.config_digest
2205        || started.attachments_digest != created.attachments_digest
2206        || !matches!(
2207            started.state.status(),
2208            ContainerState::Running | ContainerState::Stopped
2209        )
2210    {
2211        return Err(ExecutionManagerError::Internal(format!(
2212            "A3S OCI start changed exact runtime evidence for {} generation {:?}",
2213            target.id, target.generation
2214        )));
2215    }
2216    validate_record_attachments(started, "start")?;
2217    validate_selected_driver(info, started)
2218}
2219
2220fn validate_freezer_record(
2221    binding: &OciRuntimeBinding,
2222    record: &ContainerRecord,
2223    paused: bool,
2224    operation: &str,
2225) -> ExecutionManagerResult<()> {
2226    binding.validate_record(record)?;
2227    if *record.state.status() != ContainerState::Running || record.is_paused() != paused {
2228        return Err(ExecutionManagerError::Internal(format!(
2229            "A3S OCI {operation} returned an invalid freezer state for {} generation {:?}",
2230            binding.target.id, binding.target.generation
2231        )));
2232    }
2233    Ok(())
2234}
2235
2236fn compile_resource_update(
2237    record: &BoxRecord,
2238    update: &ExecutionResourceUpdate,
2239) -> ExecutionManagerResult<LinuxResources> {
2240    update.validate()?;
2241    let metadata = record.managed_execution.as_ref().ok_or_else(|| {
2242        ExecutionManagerError::Internal(format!(
2243            "execution {} has no managed resource intent",
2244            record.id
2245        ))
2246    })?;
2247    if record.resource_limits != metadata.request.config.resource_limits {
2248        return Err(ExecutionManagerError::Internal(format!(
2249            "execution {} has divergent compatibility and managed resource limits",
2250            record.id
2251        )));
2252    }
2253    let mut config = metadata.request.config.clone();
2254    update.apply_to(&mut config.resource_limits);
2255    let effective = crate::sandbox::oci::SandboxResources::from_box_config(&config)
2256        .map_err(|error| ExecutionManagerError::InvalidRequest(error.to_string()))?;
2257
2258    // OCI Runtime treats linux.resources as a partial live update. Recompiling
2259    // the full launch profile here would resend immutable device policy and
2260    // overwrite unrelated cgroup values. Validate the complete resulting Box
2261    // intent above, then emit only fields selected by this operation. CPU quota
2262    // and period are coupled because they share one atomic cgroup setting.
2263    let mut resources = LinuxResourcesBuilder::default();
2264
2265    if update.memory_reservation.is_some() || update.memory_swap.is_some() {
2266        let mut memory = LinuxMemoryBuilder::default();
2267        if update.memory_reservation.is_some() {
2268            let reservation = effective.memory_reservation.ok_or_else(|| {
2269                ExecutionManagerError::Internal(format!(
2270                    "execution {} lost its validated memory reservation update",
2271                    record.id
2272                ))
2273            })?;
2274            memory = memory.reservation(reservation);
2275        }
2276        if update.memory_swap.is_some() {
2277            let swap = effective.memory_swap.ok_or_else(|| {
2278                ExecutionManagerError::Internal(format!(
2279                    "execution {} lost its validated memory swap update",
2280                    record.id
2281                ))
2282            })?;
2283            memory = memory.swap(swap);
2284        }
2285        resources = resources.memory(memory.build().map_err(resource_update_compilation_error)?);
2286    }
2287
2288    let changes_cpu_max = update.cpu_quota.is_some() || update.cpu_period.is_some();
2289    if update.cpu_shares.is_some() || changes_cpu_max || update.cpuset_cpus.is_some() {
2290        let mut cpu = LinuxCpuBuilder::default();
2291        if update.cpu_shares.is_some() {
2292            let shares = effective.cpu_shares.ok_or_else(|| {
2293                ExecutionManagerError::Internal(format!(
2294                    "execution {} lost its validated CPU shares update",
2295                    record.id
2296                ))
2297            })?;
2298            cpu = cpu.shares(shares);
2299        }
2300        if changes_cpu_max {
2301            cpu = cpu.quota(effective.cpu_quota).period(effective.cpu_period);
2302        }
2303        if update.cpuset_cpus.is_some() {
2304            let cpuset = effective.cpuset_cpus.clone().ok_or_else(|| {
2305                ExecutionManagerError::Internal(format!(
2306                    "execution {} lost its validated CPU set update",
2307                    record.id
2308                ))
2309            })?;
2310            cpu = cpu.cpus(cpuset);
2311        }
2312        resources = resources.cpu(cpu.build().map_err(resource_update_compilation_error)?);
2313    }
2314
2315    if update.pids_limit.is_some() {
2316        resources = resources.pids(
2317            LinuxPidsBuilder::default()
2318                .limit(effective.pids_limit)
2319                .build()
2320                .map_err(resource_update_compilation_error)?,
2321        );
2322    }
2323
2324    resources.build().map_err(resource_update_compilation_error)
2325}
2326
2327fn resource_update_compilation_error(error: impl std::fmt::Display) -> ExecutionManagerError {
2328    ExecutionManagerError::InvalidRequest(format!("failed to compile OCI resource update: {error}"))
2329}
2330
2331fn validate_updated_record(
2332    binding: &OciRuntimeBinding,
2333    record: &ContainerRecord,
2334) -> ExecutionManagerResult<()> {
2335    binding.validate_record(record)?;
2336    if *record.state.status() != ContainerState::Running || record.is_paused() {
2337        return Err(ExecutionManagerError::Internal(format!(
2338            "A3S OCI update returned an invalid running state for {} generation {:?}",
2339            binding.target.id, binding.target.generation
2340        )));
2341    }
2342    Ok(())
2343}
2344
2345fn runtime_process_identity(
2346    binding: &OciRuntimeBinding,
2347    record: &ContainerRecord,
2348) -> ExecutionManagerResult<(Option<u32>, Option<u64>)> {
2349    binding.validate_record(record)?;
2350    if *record.state.status() != ContainerState::Running {
2351        return Err(ExecutionManagerError::Internal(format!(
2352            "A3S OCI returned runtime state {:?} while publishing the active handle for {} generation {:?}",
2353            record.state.status(),
2354            binding.target.id,
2355            binding.target.generation
2356        )));
2357    }
2358
2359    match record.isolation {
2360        IsolationClass::SharedHostKernel => {
2361            let pid = record
2362                .state
2363                .pid()
2364                .and_then(|pid| u32::try_from(pid).ok())
2365                .filter(|pid| *pid != 0)
2366                .ok_or_else(|| {
2367                    ExecutionManagerError::Internal(format!(
2368                        "A3S OCI returned no valid host init PID for {} generation {:?}",
2369                        binding.target.id, binding.target.generation
2370                    ))
2371                })?;
2372            let pid_start_time = crate::process::pid_start_time(pid);
2373            #[cfg(target_os = "linux")]
2374            if pid_start_time.is_none() {
2375                return Err(ExecutionManagerError::Unavailable(format!(
2376                    "A3S OCI host init PID {pid} for {} generation {:?} has no live Linux process identity",
2377                    binding.target.id, binding.target.generation
2378                )));
2379            }
2380            Ok((Some(pid), pid_start_time))
2381        }
2382        IsolationClass::DedicatedVm | IsolationClass::SharedGuestKernel => {
2383            // OCI init PIDs for VM-backed isolation are guest identities. They
2384            // must never authorize access to a host process or network namespace.
2385            Ok((None, None))
2386        }
2387    }
2388}
2389
2390fn validate_file_response(
2391    binding: &OciRuntimeBinding,
2392    response: &OciFileResponse,
2393    operation: OciFileOp,
2394    expected_upload_size: Option<u64>,
2395    maximum_download_size: Option<u64>,
2396) -> ExecutionManagerResult<()> {
2397    if response.target != binding.target || response.size > MAX_FILE_TRANSFER_BYTES as u64 {
2398        return Err(ExecutionManagerError::Internal(
2399            "A3S OCI returned invalid file target or size evidence".to_string(),
2400        ));
2401    }
2402    match operation {
2403        OciFileOp::Upload
2404            if response.data.is_some() || Some(response.size) != expected_upload_size =>
2405        {
2406            Err(ExecutionManagerError::Internal(
2407                "A3S OCI returned an invalid file upload acknowledgement".to_string(),
2408            ))
2409        }
2410        OciFileOp::Download => {
2411            if maximum_download_size.is_some_and(|limit| response.size > limit) {
2412                return Err(ExecutionManagerError::Internal(
2413                    "A3S OCI file response exceeds the requested download limit".to_string(),
2414                ));
2415            }
2416            let data = response.data.as_deref().ok_or_else(|| {
2417                ExecutionManagerError::Internal(
2418                    "A3S OCI omitted the downloaded file payload".to_string(),
2419                )
2420            })?;
2421            let maximum_encoded = MAX_FILE_TRANSFER_BYTES.div_ceil(3) * 4;
2422            if data.len() > maximum_encoded {
2423                return Err(ExecutionManagerError::Internal(format!(
2424                    "A3S OCI file payload exceeds {MAX_FILE_TRANSFER_BYTES} decoded bytes"
2425                )));
2426            }
2427            let decoded = STANDARD.decode(data).map_err(|error| {
2428                ExecutionManagerError::Internal(format!(
2429                    "A3S OCI returned invalid base64 file data: {error}"
2430                ))
2431            })?;
2432            if decoded.len() as u64 != response.size {
2433                return Err(ExecutionManagerError::Internal(
2434                    "A3S OCI file size does not match its decoded payload".to_string(),
2435                ));
2436            }
2437            Ok(())
2438        }
2439        OciFileOp::Upload => Ok(()),
2440    }
2441}
2442
2443fn validate_filesystem_response(
2444    binding: &OciRuntimeBinding,
2445    response: &OciFilesystemResponse,
2446    operation: OciFilesystemOp,
2447) -> ExecutionManagerResult<()> {
2448    const MAX_ENTRIES: usize = 4_096;
2449    const MAX_RESPONSE_BYTES: usize = 12 * 1024 * 1024;
2450    let valid_shape = match operation {
2451        OciFilesystemOp::Stat | OciFilesystemOp::MakeDir | OciFilesystemOp::Move => {
2452            response.entry.is_some() && response.entries.is_empty()
2453        }
2454        OciFilesystemOp::ListDir => response.entry.is_none(),
2455        OciFilesystemOp::Remove => response.entry.is_none() && response.entries.is_empty(),
2456    };
2457    if response.target != binding.target || !valid_shape || response.entries.len() > MAX_ENTRIES {
2458        return Err(ExecutionManagerError::Internal(
2459            "A3S OCI returned invalid filesystem target or response shape".to_string(),
2460        ));
2461    }
2462    let encoded = serde_json::to_vec(response).map_err(|error| {
2463        ExecutionManagerError::Internal(format!(
2464            "failed to size A3S OCI filesystem response: {error}"
2465        ))
2466    })?;
2467    if encoded.len() > MAX_RESPONSE_BYTES {
2468        return Err(ExecutionManagerError::Internal(format!(
2469            "A3S OCI filesystem response exceeds {MAX_RESPONSE_BYTES} bytes"
2470        )));
2471    }
2472    Ok(())
2473}
2474
2475fn map_filesystem_entry(entry: OciFilesystemEntry) -> BoxFilesystemEntry {
2476    BoxFilesystemEntry {
2477        name: entry.name,
2478        kind: match entry.kind {
2479            OciFilesystemEntryKind::Unspecified => BoxFilesystemEntryKind::Unspecified,
2480            OciFilesystemEntryKind::File => BoxFilesystemEntryKind::File,
2481            OciFilesystemEntryKind::Directory => BoxFilesystemEntryKind::Directory,
2482        },
2483        path: entry.path,
2484        size: entry.size,
2485        mode: entry.mode,
2486        permissions: entry.permissions,
2487        owner: entry.owner,
2488        group: entry.group,
2489        modified_seconds: entry.modified_seconds,
2490        modified_nanos: entry.modified_nanos,
2491        symlink_target: entry.symlink_target,
2492        metadata: entry.metadata,
2493    }
2494}
2495
2496fn validate_process_records(
2497    binding: &OciRuntimeBinding,
2498    records: &[ProcessRecord],
2499) -> ExecutionManagerResult<()> {
2500    let mut process_ids = BTreeSet::new();
2501    for record in records {
2502        if record.target.container != binding.target {
2503            return Err(ExecutionManagerError::Internal(format!(
2504                "A3S OCI process inventory crossed the exact target for {}",
2505                binding.target.id
2506            )));
2507        }
2508        if record.pid == Some(0) {
2509            return Err(ExecutionManagerError::Internal(format!(
2510                "A3S OCI process {} returned PID zero",
2511                record.target.process_id
2512            )));
2513        }
2514        if !process_ids.insert(record.target.process_id.as_str()) {
2515            return Err(ExecutionManagerError::Internal(format!(
2516                "A3S OCI process inventory returned duplicate process {}",
2517                record.target.process_id
2518            )));
2519        }
2520    }
2521    Ok(())
2522}
2523
2524fn validate_event_batch(
2525    binding: &OciRuntimeBinding,
2526    after_sequence: u64,
2527    batch: &a3s_oci_sdk::EventBatch,
2528) -> ExecutionManagerResult<()> {
2529    if batch.next_sequence < after_sequence {
2530        return Err(ExecutionManagerError::Internal(
2531            "A3S OCI event cursor regressed".to_string(),
2532        ));
2533    }
2534    let mut previous = after_sequence;
2535    for event in &batch.events {
2536        if event.container != binding.target {
2537            return Err(ExecutionManagerError::Internal(format!(
2538                "A3S OCI events crossed the exact target for {}",
2539                binding.target.id
2540            )));
2541        }
2542        if event.sequence == 0 || event.sequence <= previous {
2543            return Err(ExecutionManagerError::Internal(
2544                "A3S OCI events are not strictly ordered after the requested cursor".to_string(),
2545            ));
2546        }
2547        if event.timestamp_unix_ns == 0 {
2548            return Err(ExecutionManagerError::Internal(format!(
2549                "A3S OCI event {} has timestamp zero",
2550                event.sequence
2551            )));
2552        }
2553        previous = event.sequence;
2554    }
2555    if batch.next_sequence < previous {
2556        return Err(ExecutionManagerError::Internal(
2557            "A3S OCI event next cursor precedes the returned batch".to_string(),
2558        ));
2559    }
2560    Ok(())
2561}
2562
2563fn map_event_kind(kind: RuntimeEventKind) -> ExecutionManagerResult<ExecutionEventKind> {
2564    match kind {
2565        RuntimeEventKind::ContainerCreating => Ok(ExecutionEventKind::ContainerCreating),
2566        RuntimeEventKind::ContainerCreated => Ok(ExecutionEventKind::ContainerCreated),
2567        RuntimeEventKind::ContainerStarted => Ok(ExecutionEventKind::ContainerStarted),
2568        RuntimeEventKind::ContainerStopped => Ok(ExecutionEventKind::ContainerStopped),
2569        RuntimeEventKind::ContainerDeleted => Ok(ExecutionEventKind::ContainerDeleted),
2570        RuntimeEventKind::ContainerPaused => Ok(ExecutionEventKind::ContainerPaused),
2571        RuntimeEventKind::ContainerResumed => Ok(ExecutionEventKind::ContainerResumed),
2572        RuntimeEventKind::ResourcesUpdated => Ok(ExecutionEventKind::ResourcesUpdated),
2573        RuntimeEventKind::ProcessCreated => Ok(ExecutionEventKind::ProcessCreated),
2574        RuntimeEventKind::ProcessStarted => Ok(ExecutionEventKind::ProcessStarted),
2575        RuntimeEventKind::ProcessExited => Ok(ExecutionEventKind::ProcessExited),
2576        RuntimeEventKind::OutputDropped => Ok(ExecutionEventKind::OutputDropped),
2577        RuntimeEventKind::RuntimeWarning => Ok(ExecutionEventKind::RuntimeWarning),
2578        _ => Err(ExecutionManagerError::Unavailable(
2579            "A3S OCI Runtime returned an event kind unsupported by this Box build".to_string(),
2580        )),
2581    }
2582}
2583
2584fn validate_selected_driver(
2585    info: &RuntimeInfo,
2586    record: &ContainerRecord,
2587) -> ExecutionManagerResult<()> {
2588    let valid = info.drivers.drivers.iter().any(|capability| {
2589        capability.driver == record.driver
2590            && capability.can_launch()
2591            && capability.isolation_classes.contains(&record.isolation)
2592    });
2593    if !valid {
2594        return Err(ExecutionManagerError::Internal(format!(
2595            "A3S OCI selected unadvertised or non-launch-ready driver {:?}",
2596            record.driver
2597        )));
2598    }
2599    Ok(())
2600}
2601
2602fn validate_record_id(
2603    record: &ContainerRecord,
2604    expected_id: &ContainerId,
2605    operation: &str,
2606) -> ExecutionManagerResult<()> {
2607    if record.state.id() != expected_id.as_str() {
2608        return Err(ExecutionManagerError::Internal(format!(
2609            "A3S OCI {operation} returned container {}, expected {expected_id}",
2610            record.state.id()
2611        )));
2612    }
2613    Ok(())
2614}
2615
2616fn validate_config_digest(config_digest: &str) -> ExecutionManagerResult<()> {
2617    validate_sha256_evidence(config_digest, "configuration")
2618}
2619
2620fn validate_attachments_digest(attachments_digest: &str) -> ExecutionManagerResult<()> {
2621    validate_sha256_evidence(attachments_digest, "attachment")
2622}
2623
2624fn validate_record_attachments<'a>(
2625    record: &'a ContainerRecord,
2626    operation: &str,
2627) -> ExecutionManagerResult<&'a str> {
2628    let digest = record.attachments_digest.as_deref().ok_or_else(|| {
2629        ExecutionManagerError::Internal(format!(
2630            "A3S OCI {operation} returned no versioned attachment evidence"
2631        ))
2632    })?;
2633    validate_attachments_digest(digest)?;
2634    Ok(digest)
2635}
2636
2637fn validate_sha256_evidence(value: &str, label: &str) -> ExecutionManagerResult<()> {
2638    let valid = value.strip_prefix("sha256:").is_some_and(|digest| {
2639        digest.len() == 64
2640            && digest
2641                .bytes()
2642                .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
2643    });
2644    if !valid {
2645        return Err(ExecutionManagerError::Internal(format!(
2646            "A3S OCI binding contains invalid {label} evidence"
2647        )));
2648    }
2649    Ok(())
2650}
2651
2652pub(super) fn exit_code(status: &ExitStatus) -> ExecutionManagerResult<i32> {
2653    if let Some(exit_code) = status.exit_code {
2654        return Ok(exit_code);
2655    }
2656    let signal = status.signal.ok_or_else(|| {
2657        ExecutionManagerError::Internal(
2658            "A3S OCI terminal status contains neither an exit code nor a signal".to_string(),
2659        )
2660    })?;
2661    128_i32.checked_add(signal).ok_or_else(|| {
2662        ExecutionManagerError::Internal(format!(
2663            "A3S OCI terminal signal {signal} cannot be represented as a Box exit code"
2664        ))
2665    })
2666}
2667
2668pub(super) fn sdk_error(operation: &str, error: a3s_oci_sdk::Error) -> ExecutionManagerError {
2669    match error.code {
2670        ErrorCode::InvalidArgument => {
2671            ExecutionManagerError::InvalidRequest(format!("A3S OCI {operation}: {error}"))
2672        }
2673        ErrorCode::Conflict | ErrorCode::FailedPrecondition => ExecutionManagerError::Unavailable(
2674            format!("A3S OCI {operation} rejected the lifecycle boundary: {error}"),
2675        ),
2676        _ => ExecutionManagerError::Unavailable(format!("A3S OCI {operation}: {error}")),
2677    }
2678}
2679
2680#[cfg(test)]
2681#[path = "oci_backend_tests.rs"]
2682mod tests;