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