1use std::ffi::OsString;
4use std::path::{Component, Path, PathBuf};
5use std::sync::Arc;
6
7#[cfg(target_os = "linux")]
8use a3s_box_core::ExecutionBackend;
9use a3s_box_core::{ExecutionManagerError, ExecutionManagerResult};
10#[cfg(target_os = "linux")]
11use sha2::{Digest, Sha256};
12
13use super::LocalExecutionManager;
14#[cfg(target_os = "linux")]
15use super::{NativeLinuxOciBundleProvider, OciLocalExecutionBackend, OciMigrationPolicy};
16
17pub const OCI_MIGRATION_ENV: &str = "A3S_BOX_OCI_MIGRATION";
18pub const OCI_HOST_ROOT_ENV: &str = "A3S_BOX_OCI_HOST_ROOT";
19pub const OCI_RUNTIME_PATH_ENV: &str = "A3S_BOX_OCI_RUNTIME_PATH";
20pub const OCI_AGENT_PATH_ENV: &str = "A3S_BOX_OCI_AGENT_PATH";
21pub const OCI_WHPX_ENDPOINT_ENV: &str = "A3S_BOX_OCI_WHPX_ENDPOINT";
22#[cfg(test)]
23const DEFAULT_OCI_WHPX_ENDPOINT: &str = r"\\.\pipe\a3s-oci-box-qualification";
24
25#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct NativeLinuxOciMigrationConfig {
28 service_root: PathBuf,
29 runtime_path: Option<PathBuf>,
30 agent_path: Option<PathBuf>,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct WindowsWhpxOciMigrationConfig {
36 runtime_root: PathBuf,
37 endpoint: super::OciRuntimeEndpoint,
38}
39
40impl WindowsWhpxOciMigrationConfig {
41 pub fn new(
42 runtime_root: impl Into<PathBuf>,
43 endpoint: impl Into<String>,
44 ) -> ExecutionManagerResult<Self> {
45 let config = Self {
46 runtime_root: runtime_root.into(),
47 endpoint: super::OciRuntimeEndpoint::windows_named_pipe(endpoint)?,
48 };
49 config.validate()?;
50 Ok(config)
51 }
52
53 pub fn runtime_root(&self) -> &Path {
54 &self.runtime_root
55 }
56
57 pub fn endpoint(&self) -> &super::OciRuntimeEndpoint {
58 &self.endpoint
59 }
60
61 pub fn from_environment(home_dir: &Path) -> ExecutionManagerResult<Option<Self>> {
62 parse_windows_environment(
63 std::env::var_os(OCI_MIGRATION_ENV),
64 std::env::var_os(OCI_HOST_ROOT_ENV),
65 std::env::var_os(OCI_WHPX_ENDPOINT_ENV),
66 home_dir,
67 )
68 }
69
70 fn validate(&self) -> ExecutionManagerResult<()> {
71 validate_absolute_normalized(&self.runtime_root, "WHPX OCI runtime root")?;
72 match &self.endpoint {
73 super::OciRuntimeEndpoint::WindowsNamedPipe { .. } => Ok(()),
74 super::OciRuntimeEndpoint::UnixSocket { .. } => {
75 Err(ExecutionManagerError::InvalidRequest(
76 "WHPX OCI qualification requires a Windows named-pipe endpoint".to_string(),
77 ))
78 }
79 }
80 }
81}
82
83impl NativeLinuxOciMigrationConfig {
84 pub fn new(service_root: impl Into<PathBuf>) -> ExecutionManagerResult<Self> {
85 let config = Self {
86 service_root: service_root.into(),
87 runtime_path: None,
88 agent_path: None,
89 };
90 config.validate()?;
91 Ok(config)
92 }
93
94 pub fn with_artifacts(
95 mut self,
96 runtime_path: impl Into<PathBuf>,
97 agent_path: impl Into<PathBuf>,
98 ) -> ExecutionManagerResult<Self> {
99 self.runtime_path = Some(runtime_path.into());
100 self.agent_path = Some(agent_path.into());
101 self.validate()?;
102 Ok(self)
103 }
104
105 pub fn service_root(&self) -> &Path {
106 &self.service_root
107 }
108
109 pub fn runtime_path(&self) -> Option<&Path> {
110 self.runtime_path.as_deref()
111 }
112
113 pub fn agent_path(&self) -> Option<&Path> {
114 self.agent_path.as_deref()
115 }
116
117 pub fn from_environment(home_dir: &Path) -> ExecutionManagerResult<Option<Self>> {
120 parse_environment(
121 std::env::var_os(OCI_MIGRATION_ENV),
122 std::env::var_os(OCI_HOST_ROOT_ENV),
123 std::env::var_os(OCI_RUNTIME_PATH_ENV),
124 std::env::var_os(OCI_AGENT_PATH_ENV),
125 home_dir,
126 )
127 }
128
129 fn validate(&self) -> ExecutionManagerResult<()> {
130 validate_absolute_normalized(&self.service_root, "OCI host root")?;
131 match (&self.runtime_path, &self.agent_path) {
132 (Some(runtime), Some(agent)) => {
133 validate_absolute_normalized(runtime, "OCI runtime path")?;
134 validate_absolute_normalized(agent, "OCI agent path")?;
135 }
136 (None, None) => {}
137 _ => {
138 return Err(ExecutionManagerError::InvalidRequest(
139 "OCI runtime and agent paths must be supplied together".to_string(),
140 ))
141 }
142 }
143 Ok(())
144 }
145}
146
147impl LocalExecutionManager {
148 pub async fn with_native_linux_oci_migration(
151 state_path: impl Into<PathBuf>,
152 home_dir: impl Into<PathBuf>,
153 config: NativeLinuxOciMigrationConfig,
154 ) -> ExecutionManagerResult<Self> {
155 Self::with_native_linux_oci_migration_and_pull_progress(
156 state_path.into(),
157 home_dir.into(),
158 config,
159 None,
160 )
161 .await
162 }
163
164 async fn with_native_linux_oci_migration_and_pull_progress(
165 state_path: PathBuf,
166 home_dir: PathBuf,
167 config: NativeLinuxOciMigrationConfig,
168 pull_progress_fn: Option<crate::PullProgressFn>,
169 ) -> ExecutionManagerResult<Self> {
170 config.validate()?;
171
172 #[cfg(target_os = "linux")]
173 {
174 let capabilities = crate::sandbox::probe_sandbox_capabilities_for(
175 ExecutionBackend::A3sOci,
176 config.runtime_path(),
177 config.agent_path(),
178 );
179 capabilities.require_ready().map_err(|error| {
180 ExecutionManagerError::Unavailable(format!(
181 "native Linux OCI migration preflight failed: {error}"
182 ))
183 })?;
184 let artifacts = capabilities.a3s_oci.as_ref().ok_or_else(|| {
185 ExecutionManagerError::Unavailable(
186 "native Linux OCI migration preflight returned no runtime artifacts"
187 .to_string(),
188 )
189 })?;
190 let endpoint =
191 super::oci_owner::ensure_native_linux_oci_owner(config.service_root(), artifacts)
192 .await?;
193 let mut provider = NativeLinuxOciBundleProvider::new(
194 home_dir.clone(),
195 artifacts.runtime_path.clone(),
196 artifacts.agent_path.clone(),
197 );
198 if let Some(progress) = pull_progress_fn.as_ref() {
199 provider = provider.with_pull_progress_fn(progress.clone());
200 }
201 let provider = Arc::new(provider);
202 let oci = Arc::new(OciLocalExecutionBackend::connect(endpoint, provider).await?);
203 Ok(Self::with_oci_migration_backend_and_pull_progress(
204 state_path,
205 home_dir,
206 oci,
207 OciMigrationPolicy::SandboxViaOci,
208 pull_progress_fn,
209 ))
210 }
211
212 #[cfg(not(target_os = "linux"))]
213 {
214 let _ = (state_path, home_dir, config, pull_progress_fn);
215 Err(ExecutionManagerError::Unavailable(
216 "native Linux OCI migration is supported only on Linux".to_string(),
217 ))
218 }
219 }
220
221 pub async fn with_windows_whpx_oci_qualification(
223 state_path: impl Into<PathBuf>,
224 home_dir: impl Into<PathBuf>,
225 config: WindowsWhpxOciMigrationConfig,
226 ) -> ExecutionManagerResult<Self> {
227 Self::with_windows_whpx_oci_qualification_and_pull_progress(
228 state_path.into(),
229 home_dir.into(),
230 config,
231 None,
232 )
233 .await
234 }
235
236 async fn with_windows_whpx_oci_qualification_and_pull_progress(
237 state_path: PathBuf,
238 home_dir: PathBuf,
239 config: WindowsWhpxOciMigrationConfig,
240 pull_progress_fn: Option<crate::PullProgressFn>,
241 ) -> ExecutionManagerResult<Self> {
242 config.validate()?;
243
244 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
245 {
246 let mut provider =
247 super::WindowsWhpxOciBundleProvider::new(home_dir.clone(), config.runtime_root());
248 if let Some(progress) = pull_progress_fn.as_ref() {
249 provider = provider.with_pull_progress_fn(progress.clone());
250 }
251 let oci = Arc::new(
252 super::OciLocalExecutionBackend::connect(
253 config.endpoint().clone(),
254 Arc::new(provider),
255 )
256 .await?,
257 );
258 Ok(Self::with_oci_migration_backend_and_pull_progress(
259 state_path,
260 home_dir,
261 oci,
262 super::OciMigrationPolicy::AllViaOci,
263 pull_progress_fn,
264 ))
265 }
266
267 #[cfg(not(all(target_os = "windows", target_arch = "x86_64")))]
268 {
269 let _ = (state_path, home_dir, config, pull_progress_fn);
270 Err(ExecutionManagerError::Unavailable(
271 "Box/WHPX OCI qualification requires Windows x86_64".to_string(),
272 ))
273 }
274 }
275
276 pub async fn with_configured_backend(
279 state_path: impl Into<PathBuf>,
280 home_dir: impl Into<PathBuf>,
281 ) -> ExecutionManagerResult<Self> {
282 Self::with_configured_backend_and_pull_progress(state_path, home_dir, None).await
283 }
284
285 pub async fn with_configured_backend_and_pull_progress(
288 state_path: impl Into<PathBuf>,
289 home_dir: impl Into<PathBuf>,
290 pull_progress_fn: Option<crate::PullProgressFn>,
291 ) -> ExecutionManagerResult<Self> {
292 let state_path = state_path.into();
293 let home_dir = home_dir.into();
294
295 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
296 {
297 match WindowsWhpxOciMigrationConfig::from_environment(&home_dir)? {
298 Some(config) => {
299 Self::with_windows_whpx_oci_qualification_and_pull_progress(
300 state_path,
301 home_dir,
302 config,
303 pull_progress_fn,
304 )
305 .await
306 }
307 None => legacy_backend(state_path, home_dir, pull_progress_fn),
308 }
309 }
310
311 #[cfg(not(all(target_os = "windows", target_arch = "x86_64")))]
312 {
313 match NativeLinuxOciMigrationConfig::from_environment(&home_dir)? {
314 Some(config) => {
315 Self::with_native_linux_oci_migration_and_pull_progress(
316 state_path,
317 home_dir,
318 config,
319 pull_progress_fn,
320 )
321 .await
322 }
323 None => legacy_backend(state_path, home_dir, pull_progress_fn),
324 }
325 }
326 }
327}
328
329fn legacy_backend(
330 state_path: PathBuf,
331 home_dir: PathBuf,
332 pull_progress_fn: Option<crate::PullProgressFn>,
333) -> ExecutionManagerResult<LocalExecutionManager> {
334 let mut backend = crate::local_execution::VmLocalExecutionBackend::new(&home_dir);
335 if let Some(progress) = pull_progress_fn {
336 backend = backend.with_pull_progress_fn(progress);
337 }
338 Ok(LocalExecutionManager::new(
339 state_path,
340 home_dir,
341 Arc::new(backend),
342 ))
343}
344
345fn parse_environment(
346 mode: Option<OsString>,
347 service_root: Option<OsString>,
348 runtime_path: Option<OsString>,
349 agent_path: Option<OsString>,
350 home_dir: &Path,
351) -> ExecutionManagerResult<Option<NativeLinuxOciMigrationConfig>> {
352 let Some(mode) = mode.filter(|value| !value.is_empty()) else {
353 return Ok(None);
354 };
355 let mode = mode.to_str().ok_or_else(|| {
356 ExecutionManagerError::InvalidRequest(format!(
357 "{OCI_MIGRATION_ENV} must contain UTF-8 text"
358 ))
359 })?;
360 match mode.trim().to_ascii_lowercase().as_str() {
361 "" | "0" | "false" | "off" | "disabled" | "legacy" => return Ok(None),
362 "1" | "true" | "on" | "sandbox" | "sandbox-via-oci" => {}
363 "all" | "all-via-oci" => {
364 return Err(ExecutionManagerError::InvalidRequest(
365 "all-via-OCI migration is not qualified yet; use sandbox".to_string(),
366 ))
367 }
368 value => {
369 return Err(ExecutionManagerError::InvalidRequest(format!(
370 "unsupported {OCI_MIGRATION_ENV} value {value:?}; expected off or sandbox"
371 )))
372 }
373 }
374
375 let root = service_root
376 .filter(|value| !value.is_empty())
377 .map(PathBuf::from)
378 .unwrap_or_else(|| default_service_root(home_dir));
379 let mut config = NativeLinuxOciMigrationConfig::new(root)?;
380 match (
381 runtime_path.filter(|value| !value.is_empty()),
382 agent_path.filter(|value| !value.is_empty()),
383 ) {
384 (Some(runtime), Some(agent)) => {
385 config = config.with_artifacts(PathBuf::from(runtime), PathBuf::from(agent))?;
386 }
387 (None, None) => {}
388 _ => {
389 return Err(ExecutionManagerError::InvalidRequest(format!(
390 "{OCI_RUNTIME_PATH_ENV} and {OCI_AGENT_PATH_ENV} must be set together"
391 )))
392 }
393 }
394 Ok(Some(config))
395}
396
397fn parse_windows_environment(
398 mode: Option<OsString>,
399 runtime_root: Option<OsString>,
400 endpoint: Option<OsString>,
401 home_dir: &Path,
402) -> ExecutionManagerResult<Option<WindowsWhpxOciMigrationConfig>> {
403 let Some(mode) = mode.filter(|value| !value.is_empty()) else {
404 return Ok(None);
405 };
406 let mode = mode.to_str().ok_or_else(|| {
407 ExecutionManagerError::InvalidRequest(format!(
408 "{OCI_MIGRATION_ENV} must contain UTF-8 text"
409 ))
410 })?;
411 match mode.trim().to_ascii_lowercase().as_str() {
412 "" | "0" | "false" | "off" | "disabled" | "legacy" => return Ok(None),
413 "all" | "all-via-oci" | "microvm" | "microvm-via-oci" | "whpx" => {}
414 "1" | "true" | "on" | "sandbox" | "sandbox-via-oci" => {
415 return Err(ExecutionManagerError::InvalidRequest(
416 "Windows OCI qualification supports only MicroVM/WHPX routing; use all or microvm"
417 .to_string(),
418 ))
419 }
420 value => {
421 return Err(ExecutionManagerError::InvalidRequest(format!(
422 "unsupported {OCI_MIGRATION_ENV} value {value:?}; expected off, microvm, or all"
423 )))
424 }
425 }
426
427 let runtime_root = runtime_root
428 .filter(|value| !value.is_empty())
429 .map(PathBuf::from)
430 .unwrap_or_else(|| default_service_root(home_dir));
431 let endpoint = endpoint
432 .filter(|value| !value.is_empty())
433 .ok_or_else(|| {
434 ExecutionManagerError::InvalidRequest(format!(
435 "{OCI_WHPX_ENDPOINT_ENV} must be set explicitly for the qualification-only WHPX service"
436 ))
437 })?
438 .into_string()
439 .map_err(|_| {
440 ExecutionManagerError::InvalidRequest(format!(
441 "{OCI_WHPX_ENDPOINT_ENV} must contain UTF-8 text"
442 ))
443 })?;
444 WindowsWhpxOciMigrationConfig::new(runtime_root, endpoint).map(Some)
445}
446
447fn default_service_root(home_dir: &Path) -> PathBuf {
448 #[cfg(target_os = "linux")]
449 {
450 use std::os::unix::ffi::OsStrExt as _;
451
452 let digest = Sha256::digest(home_dir.as_os_str().as_bytes());
455 let uid = unsafe { libc::geteuid() };
457 std::env::temp_dir().join(format!("a3s-box-oci-{uid}-{}", hex::encode(&digest[..6])))
458 }
459
460 #[cfg(not(target_os = "linux"))]
461 home_dir.join("run").join("oci-host")
462}
463
464fn validate_absolute_normalized(path: &Path, label: &str) -> ExecutionManagerResult<()> {
465 if !path.is_absolute()
466 || path.parent().is_none()
467 || path
468 .components()
469 .any(|component| matches!(component, Component::CurDir | Component::ParentDir))
470 {
471 return Err(ExecutionManagerError::InvalidRequest(format!(
472 "{label} must be an absolute normalized non-root path: {}",
473 path.display()
474 )));
475 }
476 Ok(())
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482
483 fn absolute(name: &str) -> PathBuf {
484 std::env::temp_dir().join(name)
485 }
486
487 #[test]
488 fn environment_is_opt_in_and_rejects_unqualified_all_policy() {
489 let home = absolute("a3s-oci-config-home");
490 assert_eq!(
491 parse_environment(None, None, None, None, &home).unwrap(),
492 None
493 );
494 assert_eq!(
495 parse_environment(Some(OsString::from("off")), None, None, None, &home).unwrap(),
496 None
497 );
498 assert!(parse_environment(Some(OsString::from("all")), None, None, None, &home).is_err());
499 }
500
501 #[test]
502 fn environment_requires_artifact_pair_and_accepts_sandbox() {
503 let home = absolute("a3s-oci-config-home");
504 let runtime = absolute("a3s-oci");
505 let agent = absolute("a3s-oci-agent");
506 assert!(parse_environment(
507 Some(OsString::from("sandbox")),
508 None,
509 Some(runtime.clone().into_os_string()),
510 None,
511 &home
512 )
513 .is_err());
514 let config = parse_environment(
515 Some(OsString::from("sandbox")),
516 Some(absolute("a3s-oci-root").into_os_string()),
517 Some(runtime.clone().into_os_string()),
518 Some(agent.clone().into_os_string()),
519 &home,
520 )
521 .unwrap()
522 .unwrap();
523 assert_eq!(config.runtime_path(), Some(runtime.as_path()));
524 assert_eq!(config.agent_path(), Some(agent.as_path()));
525 }
526
527 #[test]
528 fn windows_environment_requires_explicit_pipe_and_accepts_microvm() {
529 let home = absolute("a3s-oci-config-home");
530 assert!(
531 parse_windows_environment(Some(OsString::from("all")), None, None, &home,).is_err()
532 );
533
534 let config = parse_windows_environment(
535 Some(OsString::from("microvm")),
536 Some(absolute("a3s-oci-whpx-root").into_os_string()),
537 Some(OsString::from(DEFAULT_OCI_WHPX_ENDPOINT)),
538 &home,
539 )
540 .unwrap()
541 .unwrap();
542 assert_eq!(
543 config.endpoint(),
544 &crate::local_execution::OciRuntimeEndpoint::windows_named_pipe(
545 DEFAULT_OCI_WHPX_ENDPOINT,
546 )
547 .unwrap()
548 );
549 }
550}