Skip to main content

forge_engine/exec/
backend.rs

1use std::path::Path;
2
3use async_trait::async_trait;
4
5use crate::config::ForgeConfig;
6use crate::error::{ForgeError, ForgeResult};
7
8pub use check_runner::{
9    CheckCommand, CheckKind, CheckResult, CommandOutput, CommandTimings, ExecutionBackendKind,
10    LogBundle, ParsedCheckOutput,
11};
12pub use effect_signature::{EffectSignature, LocatedEffect};
13pub use sandbox_workspace::{PatchedWorkspace, Workspace};
14
15/// Execution backend trait.
16#[async_trait]
17pub trait ExecutionBackend: Send + Sync {
18    fn kind(&self) -> ExecutionBackendKind;
19
20    async fn prepare_workspace(&self, fixture: &Path) -> ForgeResult<Workspace>;
21
22    async fn run_command(
23        &self,
24        workspace: &Path,
25        program: &str,
26        args: &[&str],
27        env: &[(&str, &str)],
28        timeout_secs: u64,
29    ) -> ForgeResult<CommandOutput>;
30
31    async fn collect_logs(
32        &self,
33        fmt: &CommandOutput,
34        clippy: &CommandOutput,
35        test: &CommandOutput,
36    ) -> ForgeResult<LogBundle>;
37}
38
39pub(crate) fn runner_config(config: &ForgeConfig) -> check_runner::BackendConfig {
40    check_runner::BackendConfig {
41        mode: config.mode.clone(),
42        execution_backend_preference: config.execution_backend_preference.clone(),
43        container_runtime_preference: config.container_runtime_preference.clone(),
44        sealed_allow_host_backend: config.sealed_allow_host_backend,
45        rust_image: config.container.rust_image.clone(),
46        command_timeout_secs: config.container.command_timeout_secs,
47        memory_limit: config.container.memory_limit.clone(),
48        cpu_limit: config.container.cpu_limit.clone(),
49    }
50}
51
52/// Select the best available backend based on config.
53///
54/// Sealed mode enforcement:
55/// - `"host"` explicit: rejected unless `sealed_allow_host_backend = true`.
56/// - `"container"` explicit: never falls back to host; errors if container unavailable.
57/// - `"auto"`: tries container first; only falls back to host if not sealed or
58///   `sealed_allow_host_backend = true`.
59pub fn select_backend(config: &ForgeConfig) -> ForgeResult<Box<dyn ExecutionBackend>> {
60    let sealed = config.mode == "sealed_local";
61
62    match config.execution_backend_preference.as_str() {
63        "host" => {
64            if sealed && !config.sealed_allow_host_backend {
65                return Err(ForgeError::SealedModeUnsupported {
66                    runtime: "host backend requested in sealed_local mode".into(),
67                });
68            }
69            if sealed {
70                tracing::warn!("sealed_local mode with host backend — no network isolation");
71            }
72            Ok(Box::new(crate::exec::host::HostBackend::new(config)))
73        }
74        "container" => crate::exec::container::ContainerBackend::new(config)
75            .map(|backend| Box::new(backend) as Box<dyn ExecutionBackend>)
76            .map_err(|error| {
77                if sealed {
78                    ForgeError::SealedModeUnsupported {
79                        runtime: format!("container unavailable in sealed mode: {error}"),
80                    }
81                } else {
82                    error
83                }
84            }),
85        _ => match crate::exec::container::ContainerBackend::new(config) {
86            Ok(backend) => Ok(Box::new(backend)),
87            Err(_) => {
88                if sealed && !config.sealed_allow_host_backend {
89                    return Err(ForgeError::SealedModeUnsupported {
90                        runtime:
91                            "container unavailable and host fallback not allowed in sealed mode"
92                                .into(),
93                    });
94                }
95                if sealed {
96                    tracing::warn!(
97                            "sealed_local: container unavailable, falling back to host — isolation not guaranteed"
98                        );
99                }
100                Ok(Box::new(crate::exec::host::HostBackend::new(config)))
101            }
102        },
103    }
104}