Skip to main content

zlayer_agent/
lib.rs

1//! `ZLayer` Agent - Container Runtime
2//!
3//! Manages container lifecycle, health checking, init actions, and proxy integration.
4
5pub mod auth;
6pub mod autoscale_controller;
7pub mod bundle;
8pub mod capability;
9pub mod cdi;
10pub mod cgroups_stats;
11pub mod container_supervisor;
12pub mod cron_scheduler;
13pub mod dependency;
14pub mod env;
15pub mod error;
16pub mod gpu_detector;
17pub mod gpu_metrics;
18pub mod gpu_sharing;
19pub mod health;
20pub mod init;
21pub mod job;
22pub mod kv;
23pub mod metrics_providers;
24pub mod overlay_manager;
25pub mod proxy_manager;
26pub mod runtime;
27pub mod runtimes;
28pub mod service;
29pub mod stabilization;
30pub mod storage_manager;
31pub mod worker_client;
32
33#[cfg(target_os = "windows")]
34pub mod windows;
35
36pub use auth::{ContainerTokenSink, DeploymentDigestSink};
37pub use autoscale_controller::{has_adaptive_scaling, AutoscaleController};
38pub use bundle::*;
39pub use container_supervisor::{
40    ContainerSupervisor, SupervisedContainer, SupervisedState, SupervisorConfig, SupervisorEvent,
41};
42pub use cron_scheduler::{CronJobInfo, CronScheduler};
43pub use dependency::{
44    DependencyConditionChecker, DependencyError, DependencyGraph, DependencyNode, DependencyWaiter,
45    WaitResult,
46};
47pub use env::{
48    resolve_env_value, resolve_env_vars, resolve_env_with_secrets, EnvResolutionError, ResolvedEnv,
49};
50pub use error::*;
51pub use gpu_detector::{detect_gpus, GpuInfo};
52pub use health::*;
53pub use init::{BackoffConfig, InitOrchestrator};
54pub use job::{
55    JobExecution, JobExecutionId, JobExecutor, JobExecutorConfig, JobStatus, JobTrigger,
56};
57pub use kv::{
58    global_kv, set_global_kv, KvBackend, KvEntry, KvError, KvEvent, KvEventKind, KvStore,
59};
60pub use metrics_providers::{
61    LockedServiceManagerContainerProvider, RuntimeStatsProvider, ServiceManagerContainerProvider,
62};
63pub use overlay_manager::{make_interface_name, OverlayManager};
64pub use proxy_manager::{ProxyManager, ProxyManagerConfig};
65pub use runtime::*;
66pub use runtimes::{create_runtime_for_image, detect_image_artifact_type};
67
68// Youki runtime types are only available on Linux with the `youki-runtime` feature.
69#[cfg(all(target_os = "linux", feature = "youki-runtime"))]
70pub use runtimes::{YoukiConfig, YoukiRuntime};
71
72#[cfg(feature = "docker")]
73pub use runtimes::DockerRuntime;
74
75#[cfg(feature = "wasm")]
76pub use runtimes::{WasmConfig, WasmRuntime};
77
78#[cfg(target_os = "macos")]
79pub use runtimes::macos_sandbox::SandboxRuntime;
80#[cfg(target_os = "macos")]
81pub use runtimes::macos_vm::VmRuntime;
82
83pub use service::*;
84pub use stabilization::{
85    wait_for_stabilization, ServiceHealthSummary, StabilizationOutcome, StabilizationResult,
86};
87pub use storage_manager::{StorageError, StorageManager, VolumeInfo};
88pub use worker_client::{
89    WorkerClientError, WorkerClientImpl, WorkerIdentity, WorkerStatusProvider,
90};
91
92#[cfg(target_os = "macos")]
93use std::path::PathBuf;
94use std::sync::Arc;
95
96/// Configuration for macOS sandbox-based container runtime
97///
98/// Uses Apple's sandbox framework (sandbox_init/sandbox-exec) to provide
99/// process isolation on macOS. This is the preferred runtime on macOS
100/// when Docker is not available or not desired.
101#[cfg(target_os = "macos")]
102#[derive(Debug, Clone)]
103pub struct MacSandboxConfig {
104    /// Directory for container data and rootfs
105    pub data_dir: PathBuf,
106    /// Directory for container logs
107    pub log_dir: PathBuf,
108    /// Whether to enable GPU access (Metal/MPS) for containers
109    pub gpu_access: bool,
110    /// Whether containers may opt into macOS keychain / code-signing access via
111    /// the `zlayer.io/keychain` label. Off by default: even with the label, a
112    /// container gets no keychain access unless the daemon operator enables this.
113    /// Defense-in-depth so an untrusted spec can't reach `securityd` or the
114    /// host keychain databases just by setting a label.
115    pub keychain_access_allowed: bool,
116    /// Overlay CIDR (e.g. "10.200.0.0/16") that host-net-shared Seatbelt
117    /// containers are allowed to egress to, so they can reach the node overlay
118    /// IP (daemon) plus sibling overlay IPs. `None` disables overlay egress.
119    pub overlay_cidr: Option<String>,
120}
121
122/// Whether the macOS keychain capability is enabled via the
123/// `ZLAYER_ALLOW_KEYCHAIN_ACCESS` environment variable. This is the bridge the
124/// auto-runtime path uses to honor the `--allow-keychain-access` CLI flag
125/// (which sets this env). Accepts `1`, `true`, `yes`, `on` (case-insensitive).
126#[cfg(target_os = "macos")]
127#[must_use]
128pub fn keychain_access_allowed_from_env() -> bool {
129    std::env::var("ZLAYER_ALLOW_KEYCHAIN_ACCESS")
130        .ok()
131        .is_some_and(|v| {
132            matches!(
133                v.trim().to_ascii_lowercase().as_str(),
134                "1" | "true" | "yes" | "on"
135            )
136        })
137}
138
139#[cfg(target_os = "macos")]
140impl Default for MacSandboxConfig {
141    fn default() -> Self {
142        let dirs = zlayer_paths::ZLayerDirs::system_default();
143        Self {
144            data_dir: dirs.data_dir().to_path_buf(),
145            log_dir: dirs.logs(),
146            gpu_access: true,
147            keychain_access_allowed: false,
148            overlay_cidr: Some(zlayer_overlay::DEFAULT_OVERLAY_CIDR.to_string()),
149        }
150    }
151}
152
153/// Configuration for selecting and configuring a container runtime
154#[derive(Debug, Clone, Default)]
155pub enum RuntimeConfig {
156    /// Automatically select the best available runtime
157    ///
158    /// Selection logic:
159    /// - On Linux: Uses bundled libcontainer runtime (no external binary needed), falls back to Docker
160    /// - On macOS: Uses the Apple-Virtualization / sandbox runtime, falls back to Docker
161    /// - On Windows: Uses the native HCS runtime for Windows containers (Linux workloads run
162    ///   through the WSL2 youki delegate via the composite runtime)
163    /// - If no runtime can be initialized, returns an error
164    #[default]
165    Auto,
166    /// Use the mock runtime for testing and development
167    Mock,
168    /// Use youki/libcontainer as the container runtime (Linux only, requires the `youki-runtime` feature)
169    #[cfg(all(target_os = "linux", feature = "youki-runtime"))]
170    Youki(YoukiConfig),
171    /// Use Docker daemon as the container runtime (cross-platform)
172    #[cfg(feature = "docker")]
173    Docker,
174    /// Use WebAssembly runtime with wasmtime for WASM workloads
175    #[cfg(feature = "wasm")]
176    Wasm(WasmConfig),
177    /// Use macOS sandbox-based container runtime
178    #[cfg(target_os = "macos")]
179    MacSandbox(MacSandboxConfig),
180    /// Use macOS libkrun micro-VMs for Linux-guest isolation.
181    #[cfg(target_os = "macos")]
182    MacVm,
183    /// Use Apple `Virtualization.framework` for ephemeral native-macOS guest
184    /// VMs. Opt-in only (never `Auto`); route via `com.zlayer.isolation=vz`.
185    #[cfg(target_os = "macos")]
186    MacVz,
187    /// WSL2 backend (deprecated).
188    ///
189    /// Preserved for one release for back-compat with existing `runtime: wsl2`
190    /// configs. No real WSL2-specific backend was ever shipped — this variant
191    /// was a stub that suggested using Docker Desktop with the WSL2 backend.
192    #[cfg(target_os = "windows")]
193    #[deprecated(
194        note = "Wsl2 is deprecated in favor of Hcs (native Windows containers via the \
195                Host Compute Service). This variant is preserved for one release and \
196                currently aliases to Hcs with a default config at dispatch time."
197    )]
198    Wsl2,
199    /// Native Windows container runtime via the Host Compute Service (HCS).
200    ///
201    /// Windows-only. Drives containers directly against the Windows HCS API
202    /// (see [`crate::runtimes::hcs`]) without requiring Docker Desktop or a
203    /// WSL2 VM.
204    #[cfg(target_os = "windows")]
205    Hcs(crate::runtimes::hcs::HcsConfig),
206}
207
208/// Check if Docker daemon is available and responsive
209///
210/// This function attempts to connect to the Docker daemon using
211/// platform-specific defaults and pings it to verify connectivity.
212///
213/// # Returns
214/// `true` if Docker is available, `false` otherwise
215#[cfg(feature = "docker")]
216pub async fn is_docker_available() -> bool {
217    use bollard::Docker;
218
219    match Docker::connect_with_local_defaults() {
220        Ok(docker) => match docker.ping().await {
221            Ok(_) => {
222                tracing::debug!("Docker daemon is available");
223                true
224            }
225            Err(e) => {
226                tracing::debug!(error = %e, "Docker daemon ping failed");
227                false
228            }
229        },
230        Err(e) => {
231            tracing::debug!(error = %e, "Failed to connect to Docker daemon");
232            false
233        }
234    }
235}
236
237/// Check if Docker daemon is available (stub when docker feature is disabled)
238#[cfg(not(feature = "docker"))]
239#[allow(clippy::unused_async)]
240pub async fn is_docker_available() -> bool {
241    false
242}
243
244/// Check if the WASM runtime is available (compiled in)
245///
246/// Returns `true` if the `wasm` feature is enabled and the wasmtime
247/// runtime is compiled into this binary.
248///
249/// # Example
250///
251/// ```
252/// use zlayer_agent::is_wasm_available;
253///
254/// if is_wasm_available() {
255///     println!("WASM runtime is available");
256/// } else {
257///     println!("WASM runtime is not compiled in");
258/// }
259/// ```
260#[cfg(feature = "wasm")]
261#[must_use]
262pub fn is_wasm_available() -> bool {
263    true
264}
265
266/// Check if the WASM runtime is available (stub when wasm feature is disabled)
267#[cfg(not(feature = "wasm"))]
268#[must_use]
269pub fn is_wasm_available() -> bool {
270    false
271}
272
273/// Create a runtime based on the provided configuration
274///
275/// # Arguments
276/// * `config` - The runtime configuration specifying which runtime to use
277///
278/// # Returns
279/// An `Arc<dyn Runtime + Send + Sync>` that can be used with `ServiceManager`
280///
281/// # Errors
282/// Returns `AgentError` if the runtime cannot be initialized (e.g., failed to create
283/// required directories, no runtime available for Auto mode)
284///
285/// # Runtime Selection for Auto Mode
286///
287/// When `RuntimeConfig::Auto` is specified:
288/// - **Linux**: Uses bundled libcontainer runtime (no external binary needed), falls back to Docker
289/// - **macOS**: Uses sandbox runtime (native Metal/MPS), falls back to VM runtime (libkrun), then Docker
290/// - **Windows**: Uses Docker directly
291/// - If no runtime can be initialized, returns an error
292///
293/// # Example
294/// ```no_run
295/// use zlayer_agent::{RuntimeConfig, create_runtime};
296///
297/// # async fn example() -> Result<(), zlayer_agent::AgentError> {
298/// let runtime = create_runtime(RuntimeConfig::Auto, None).await?;
299/// # Ok(())
300/// # }
301/// ```
302#[allow(clippy::too_many_lines)]
303pub async fn create_runtime(
304    config: RuntimeConfig,
305    auth_ctx: Option<ContainerAuthContext>,
306) -> Result<Arc<dyn Runtime + Send + Sync>> {
307    match config {
308        RuntimeConfig::Auto => create_auto_runtime(auth_ctx).await,
309        RuntimeConfig::Mock => Ok(Arc::new(MockRuntime::new())),
310        #[cfg(all(target_os = "linux", feature = "youki-runtime"))]
311        RuntimeConfig::Youki(youki_config) => {
312            let runtime = YoukiRuntime::new(youki_config, auth_ctx).await?;
313            Ok(Arc::new(runtime))
314        }
315        #[cfg(feature = "docker")]
316        RuntimeConfig::Docker => {
317            let runtime = DockerRuntime::new(auth_ctx).await?;
318            Ok(Arc::new(runtime))
319        }
320        #[cfg(feature = "wasm")]
321        RuntimeConfig::Wasm(wasm_config) => {
322            let runtime = WasmRuntime::new(wasm_config, auth_ctx).await?;
323            Ok(Arc::new(runtime))
324        }
325        #[cfg(target_os = "macos")]
326        RuntimeConfig::MacSandbox(config) => {
327            let primary: Arc<dyn Runtime> = Arc::new(runtimes::macos_sandbox::SandboxRuntime::new(
328                config,
329                auth_ctx.clone(),
330            )?);
331            let delegate: Option<Arc<dyn Runtime>> = match runtimes::macos_vm::VmRuntime::new(
332                auth_ctx.clone(),
333            ) {
334                Ok(rt) => {
335                    tracing::info!(
336                            "macOS VM (libkrun) delegate available — Linux containers will execute in a micro-VM"
337                        );
338                    Some(Arc::new(rt))
339                }
340                Err(e) => {
341                    tracing::warn!(
342                        error = %e,
343                        "macOS VM delegate unavailable; node will only run mac-native containers"
344                    );
345                    None
346                }
347            };
348            // VZ Linux-guest delegate (the default Linux path on macOS). First
349            // party (no dylib), so this normally succeeds.
350            let vz_linux: Option<Arc<dyn Runtime>> =
351                runtimes::macos_vz_linux::VzLinuxRuntime::new(auth_ctx.clone())
352                    .map(|rt| Arc::new(rt) as Arc<dyn Runtime>)
353                    .ok();
354            // Opt-in VZ delegate (native-macOS guests via `com.zlayer.isolation=vz`).
355            let vz: Option<Arc<dyn Runtime>> = match runtimes::macos_vz::VzRuntime::new(auth_ctx) {
356                Ok(rt) => Some(Arc::new(rt)),
357                Err(e) => {
358                    tracing::warn!(error = %e, "macOS VZ delegate unavailable");
359                    None
360                }
361            };
362            // Point image-OS inspection at BOTH persistent blob caches the
363            // composite's `pull_image` writes into, tried in order:
364            //   1. the VZ-Linux runtime's `{data_dir}/vz/linux/images/blobs.redb`
365            //      (the delegate that actually runs the Linux workload), and
366            //   2. the primary Sandbox runtime's `{data_dir}/images/blobs.redb`.
367            // `pull_image` pulls into BOTH (primary first, then VZ-Linux), and
368            // either pull short-circuits under `IfNotPresent` when its rootfs
369            // already exists — so an already-pulled image's manifest+config may
370            // live in only ONE of the two stores. Probing both (local-only, no
371            // network per cache) lets the composite resolve a locally-cached
372            // Linux image's OS with NO network call — so the workload still
373            // routes to VZ-Linux even when Docker Hub is rate-limiting the
374            // redundant OS re-inspection.
375            let data_dir = zlayer_paths::ZLayerDirs::default_data_dir();
376            let os_inspect_cache_paths = vec![
377                data_dir
378                    .join("vz")
379                    .join("linux")
380                    .join("images")
381                    .join("blobs.redb"),
382                data_dir.join("images").join("blobs.redb"),
383            ];
384            Ok(Arc::new(
385                runtimes::composite::CompositeRuntime::new(primary, delegate)
386                    .with_vz_delegate(vz)
387                    .with_vz_linux_delegate(vz_linux)
388                    .with_os_inspect_cache_paths(os_inspect_cache_paths),
389            ))
390        }
391        #[cfg(target_os = "macos")]
392        RuntimeConfig::MacVm => Ok(Arc::new(runtimes::macos_vm::VmRuntime::new(auth_ctx)?)),
393        #[cfg(target_os = "macos")]
394        RuntimeConfig::MacVz => Ok(Arc::new(runtimes::macos_vz::VzRuntime::new(auth_ctx)?)),
395        #[cfg(target_os = "windows")]
396        #[allow(deprecated)]
397        RuntimeConfig::Wsl2 => {
398            tracing::warn!(
399                "RuntimeConfig::Wsl2 is deprecated; treating as RuntimeConfig::Hcs with default config"
400            );
401            Box::pin(create_runtime(
402                RuntimeConfig::Hcs(crate::runtimes::hcs::HcsConfig::default()),
403                auth_ctx,
404            ))
405            .await
406        }
407        #[cfg(target_os = "windows")]
408        RuntimeConfig::Hcs(hcs_config) => {
409            let primary: Arc<dyn Runtime> = Arc::new(
410                crate::runtimes::hcs::HcsRuntime::new(hcs_config, auth_ctx.clone()).await?,
411            );
412
413            #[cfg(feature = "wsl")]
414            let delegate: Option<Arc<dyn Runtime>> =
415                match runtimes::wsl2_delegate::Wsl2DelegateRuntime::try_new(auth_ctx.clone()).await
416                {
417                    Ok(Some(rt)) => {
418                        tracing::info!(
419                            "WSL2 delegate runtime available — Linux containers will execute inside the zlayer distro"
420                        );
421                        Some(Arc::new(rt))
422                    }
423                    Ok(None) => {
424                        tracing::info!(
425                            "WSL2 not available; node will only run Windows-image containers"
426                        );
427                        None
428                    }
429                    Err(e) => {
430                        tracing::warn!(
431                            error = %e,
432                            "WSL2 delegate setup failed; node will only run Windows-image containers"
433                        );
434                        None
435                    }
436                };
437            #[cfg(not(feature = "wsl"))]
438            let delegate: Option<Arc<dyn Runtime>> = None;
439
440            Ok(Arc::new(runtimes::composite::CompositeRuntime::new(
441                primary, delegate,
442            )))
443        }
444    }
445}
446
447/// Automatically select and create the best available runtime
448///
449/// Selection logic:
450/// - On Linux: Uses bundled libcontainer runtime directly (no external binary needed), falls back to Docker
451/// - On macOS: `SandboxRuntime` (native Metal/MPS) → `VmRuntime` (libkrun Linux compat with GPU) → Docker
452/// - On Windows: native `HcsRuntime` (Windows containers via HCS, no Docker Desktop / WSL2 required),
453///   falling through to Docker only if HCS is unavailable
454/// - Returns an error if no runtime can be initialized
455#[cfg_attr(
456    not(all(target_os = "linux", feature = "youki-runtime")),
457    allow(clippy::unused_async)
458)]
459#[cfg_attr(
460    not(any(
461        all(target_os = "linux", feature = "youki-runtime"),
462        target_os = "macos",
463        feature = "docker"
464    )),
465    allow(unused_variables)
466)]
467#[allow(clippy::too_many_lines)]
468async fn create_auto_runtime(
469    auth_ctx: Option<ContainerAuthContext>,
470) -> Result<Arc<dyn Runtime + Send + Sync>> {
471    tracing::info!("Auto-selecting container runtime");
472
473    // On Linux, use bundled libcontainer runtime (no daemon overhead, no external binary needed)
474    #[cfg(all(target_os = "linux", feature = "youki-runtime"))]
475    {
476        match YoukiRuntime::new(YoukiConfig::default(), auth_ctx.clone()).await {
477            Ok(runtime) => {
478                tracing::info!("Using bundled libcontainer runtime (Linux-native, no daemon)");
479                return Ok(Arc::new(runtime));
480            }
481            Err(e) => {
482                tracing::warn!(error = %e, "Failed to initialize libcontainer runtime, trying Docker");
483            }
484        }
485    }
486
487    // On macOS, build a composite runtime:
488    //   primary  = SandboxRuntime (native Metal/MPS), when available
489    //   delegate = VmRuntime (libkrun Linux compat), when available
490    // If at least the primary is available, return the composite. Otherwise
491    // (e.g. sandbox init failed), fall through to Docker.
492    #[cfg(target_os = "macos")]
493    {
494        // The CLI flag `--allow-keychain-access` is exposed to the auto path via
495        // its `ZLAYER_ALLOW_KEYCHAIN_ACCESS` env var (the explicit
496        // `RuntimeConfig::MacSandbox` arm reads the flag directly). The daemon
497        // runs `--runtime auto` by default, so without this bridge the keychain
498        // capability could never be turned on for the common deployment.
499        let sandbox_config = MacSandboxConfig {
500            keychain_access_allowed: keychain_access_allowed_from_env(),
501            ..MacSandboxConfig::default()
502        };
503        let primary: Option<Arc<dyn Runtime>> =
504            match runtimes::macos_sandbox::SandboxRuntime::new(sandbox_config, auth_ctx.clone()) {
505                Ok(rt) => Some(Arc::new(rt)),
506                Err(e) => {
507                    tracing::warn!("macOS sandbox runtime unavailable: {e}");
508                    None
509                }
510            };
511        let delegate: Option<Arc<dyn Runtime>> = match runtimes::macos_vm::VmRuntime::new(
512            auth_ctx.clone(),
513        ) {
514            Ok(rt) => {
515                tracing::info!(
516                        "macOS VM (libkrun) delegate available — Linux containers will execute in a micro-VM"
517                    );
518                Some(Arc::new(rt))
519            }
520            Err(e) => {
521                tracing::warn!("macOS VM runtime (libkrun) unavailable: {e}");
522                None
523            }
524        };
525        // Opt-in VZ delegate (native-macOS guests via `com.zlayer.isolation=vz`);
526        // never the default, only used when a service requests it.
527        let vz: Option<Arc<dyn Runtime>> = runtimes::macos_vz::VzRuntime::new(auth_ctx.clone())
528            .map(|rt| Arc::new(rt) as Arc<dyn Runtime>)
529            .ok();
530        // VZ Linux-guest delegate — the default Linux path on macOS.
531        let vz_linux: Option<Arc<dyn Runtime>> =
532            runtimes::macos_vz_linux::VzLinuxRuntime::new(auth_ctx.clone())
533                .map(|rt| Arc::new(rt) as Arc<dyn Runtime>)
534                .ok();
535
536        if let Some(p) = primary {
537            // Point image-OS dispatch inspection at BOTH persistent blob caches
538            // the composite's `pull_image` writes into (VZ-Linux first, then the
539            // primary Sandbox store), so an already-pulled image's OS resolves
540            // LOCAL-ONLY with no network round-trip — the cached Linux image
541            // routes to VZ-Linux even when Docker Hub is rate-limiting. Mirrors
542            // the `RuntimeConfig::MacSandbox` arm above.
543            let data_dir = zlayer_paths::ZLayerDirs::default_data_dir();
544            let os_inspect_cache_paths = vec![
545                data_dir
546                    .join("vz")
547                    .join("linux")
548                    .join("images")
549                    .join("blobs.redb"),
550                data_dir.join("images").join("blobs.redb"),
551            ];
552            return Ok(Arc::new(
553                runtimes::composite::CompositeRuntime::new(p, delegate)
554                    .with_vz_delegate(vz)
555                    .with_vz_linux_delegate(vz_linux)
556                    .with_os_inspect_cache_paths(os_inspect_cache_paths),
557            ));
558        }
559        // If sandbox failed but VM succeeded, use the VM runtime on its own —
560        // it's still the best available native macOS path before falling back
561        // to Docker.
562        if let Some(d) = delegate {
563            return Ok(d);
564        }
565    }
566
567    // On Windows, build a composite runtime:
568    //   primary  = HcsRuntime (native Windows containers), when available
569    //   delegate = Wsl2DelegateRuntime (Linux containers via WSL2), when available
570    // If the primary is available, return the composite. Otherwise fall
571    // through to Docker.
572    #[cfg(target_os = "windows")]
573    {
574        let primary: Option<Arc<dyn Runtime>> = match crate::runtimes::hcs::HcsRuntime::new(
575            crate::runtimes::hcs::HcsConfig::default(),
576            auth_ctx.clone(),
577        )
578        .await
579        {
580            Ok(rt) => {
581                tracing::info!(
582                    "Using native Windows HCS runtime (no Docker Desktop / WSL2 required)"
583                );
584                Some(Arc::new(rt))
585            }
586            Err(e) => {
587                tracing::warn!(error = %e, "HCS runtime unavailable, falling back to Docker");
588                None
589            }
590        };
591
592        #[cfg(feature = "wsl")]
593        let delegate: Option<Arc<dyn Runtime>> =
594            match runtimes::wsl2_delegate::Wsl2DelegateRuntime::try_new(auth_ctx.clone()).await {
595                Ok(Some(rt)) => {
596                    tracing::info!(
597                        "WSL2 delegate runtime available — Linux containers will execute inside the zlayer distro"
598                    );
599                    Some(Arc::new(rt))
600                }
601                Ok(None) => {
602                    tracing::info!(
603                        "WSL2 not available; node will only run Windows-image containers"
604                    );
605                    None
606                }
607                Err(e) => {
608                    tracing::warn!(
609                        error = %e,
610                        "WSL2 delegate setup failed; node will only run Windows-image containers"
611                    );
612                    None
613                }
614            };
615        #[cfg(not(feature = "wsl"))]
616        let delegate: Option<Arc<dyn Runtime>> = None;
617
618        if let Some(p) = primary {
619            return Ok(Arc::new(runtimes::composite::CompositeRuntime::new(
620                p, delegate,
621            )));
622        }
623    }
624
625    // On non-Linux or if libcontainer failed, try Docker
626    #[cfg(feature = "docker")]
627    {
628        if is_docker_available().await {
629            tracing::info!("Selected Docker runtime");
630            let runtime = DockerRuntime::new(auth_ctx).await?;
631            return Ok(Arc::new(runtime));
632        }
633        tracing::debug!("Docker daemon not available");
634    }
635
636    // No runtime available
637    #[cfg(all(target_os = "linux", feature = "docker"))]
638    {
639        Err(AgentError::Configuration(
640            "Bundled libcontainer runtime failed to initialize and Docker daemon is not available."
641                .to_string(),
642        ))
643    }
644
645    #[cfg(all(target_os = "linux", not(feature = "docker")))]
646    {
647        Err(AgentError::Configuration(
648            "Bundled libcontainer runtime failed to initialize. Enable the 'docker' feature for an alternative."
649                .to_string(),
650        ))
651    }
652
653    #[cfg(all(not(target_os = "linux"), feature = "docker"))]
654    {
655        Err(AgentError::Configuration(
656            "No container runtime available. Start the Docker daemon.".to_string(),
657        ))
658    }
659
660    #[cfg(all(not(target_os = "linux"), not(feature = "docker")))]
661    {
662        Err(AgentError::Configuration(
663            "No container runtime available. Enable the 'docker' feature and start the Docker daemon.".to_string(),
664        ))
665    }
666}