1use 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
52pub 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67#[serde(tag = "kind", rename_all = "kebab-case")]
68pub enum OciRuntimeEndpoint {
69 UnixSocket { path: PathBuf },
71 WindowsNamedPipe { name: String },
73}
74
75impl OciRuntimeEndpoint {
76 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 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
143pub struct OciRuntimeBinding {
144 pub schema_version: String,
146 pub endpoint: OciRuntimeEndpoint,
148 pub target: ContainerTarget,
150 pub driver: DriverKind,
152 pub isolation: IsolationClass,
154 pub config_digest: String,
156 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 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 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#[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 pub fn runtime_container_id(&self) -> &ContainerId {
258 &self.runtime_container_id
259 }
260
261 pub fn create_context(&self) -> &OperationContext {
263 &self.create_context
264 }
265
266 pub fn isolation(&self) -> &IsolationRequest {
268 &self.isolation
269 }
270
271 pub fn attachment_capabilities(&self) -> &AttachmentCapabilities {
273 &self.attachment_capabilities
274 }
275
276 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 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#[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 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 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 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 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 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#[async_trait]
518pub trait OciBundleProvider: Send + Sync {
519 fn preflight(
521 &self,
522 _record: &BoxRecord,
523 _context: &OciBundlePreparationContext,
524 ) -> ExecutionManagerResult<()> {
525 Ok(())
526 }
527
528 async fn plan_create_resources(
531 &self,
532 _record: &BoxRecord,
533 ) -> ExecutionManagerResult<LocalExecutionResourcePlan> {
534 Ok(LocalExecutionResourcePlan::default())
535 }
536
537 async fn prepare(
539 &self,
540 record: &BoxRecord,
541 context: &OciBundlePreparationContext,
542 ) -> ExecutionManagerResult<OciPreparedExecution>;
543
544 async fn ensure_log_projection(
547 &self,
548 _record: &BoxRecord,
549 _binding: &OciRuntimeBinding,
550 ) -> ExecutionManagerResult<()> {
551 Ok(())
552 }
553
554 async fn wait_log_projection_drained(
557 &self,
558 _record: &BoxRecord,
559 _binding: &OciRuntimeBinding,
560 ) -> ExecutionManagerResult<()> {
561 Ok(())
562 }
563
564 async fn wait_log_projection_stopped_after_owner_loss(
569 &self,
570 _record: &BoxRecord,
571 _binding: &OciRuntimeBinding,
572 ) -> ExecutionManagerResult<()> {
573 Ok(())
574 }
575
576 async fn cleanup(&self, _record: &BoxRecord) -> ExecutionManagerResult<()> {
578 Ok(())
579 }
580}
581
582#[derive(Debug, Clone)]
584pub struct OciRuntimeLaunch {
585 pub record: ContainerRecord,
586 pub binding: OciRuntimeBinding,
587}
588
589#[derive(Clone)]
591pub struct OciLifecycleAdapter {
592 endpoint: OciRuntimeEndpoint,
593 client: RuntimeClient,
594}
595
596impl OciLifecycleAdapter {
597 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 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 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 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 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 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 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#[derive(Clone)]
1317pub struct OciLocalExecutionBackend {
1318 adapter: OciLifecycleAdapter,
1319 provider: Arc<dyn OciBundleProvider>,
1320}
1321
1322impl OciLocalExecutionBackend {
1323 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 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 resources.disarm();
1654 handle
1655 }
1656 Ok(_) => {
1657 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 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#[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 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 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 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;