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