Skip to main content

minion_engine/sandbox/
mod.rs

1/// Docker Sandbox Integration
2///
3/// Provides three sandbox modes:
4///
5/// **Mode 1 — Full Workflow** (`--sandbox` CLI flag):
6///   Creates a Docker container, copies workspace, executes all steps inside,
7///   copies results back, and destroys the container.
8///
9/// **Mode 2 — Agent Only** (`config.agent.sandbox: true`):
10///   Only `agent` type steps run inside Docker; all other steps run on the host.
11///
12/// **Mode 3 — Devbox** (`config.global.sandbox.enabled: true`):
13///   Full configuration with custom image, workspace path, network allow/deny
14///   lists, and resource limits (CPUs, memory). Equivalent to Mode 1 with
15///   explicit configuration.
16///
17/// **Requirements**: Docker Desktop 4.40+ must be installed and running.
18pub mod config;
19pub mod docker;
20
21pub use config::{SandboxConfig, SandboxMode};
22pub use docker::DockerSandbox;
23
24use anyhow::{bail, Result};
25
26/// Determine the sandbox mode from workflow config and CLI flags.
27pub fn resolve_mode(
28    sandbox_flag: bool,
29    global_config: &std::collections::HashMap<String, serde_yaml::Value>,
30    agent_config: &std::collections::HashMap<String, serde_yaml::Value>,
31) -> SandboxMode {
32    // CLI --sandbox flag takes priority → Mode 1
33    if sandbox_flag {
34        return SandboxMode::FullWorkflow;
35    }
36
37    // config.global.sandbox.enabled: true → Mode 3 (Devbox)
38    if let Some(serde_yaml::Value::Mapping(m)) = global_config.get("sandbox") {
39        if m.get(serde_yaml::Value::String("enabled".into()))
40            .and_then(|v| v.as_bool())
41            .unwrap_or(false)
42        {
43            return SandboxMode::Devbox;
44        }
45    }
46
47    // config.agent.sandbox: true → Mode 2
48    if agent_config
49        .get("sandbox")
50        .and_then(|v| v.as_bool())
51        .unwrap_or(false)
52    {
53        return SandboxMode::AgentOnly;
54    }
55
56    SandboxMode::Disabled
57}
58
59/// Validate that Docker is available; return a friendly error if not.
60pub async fn require_docker() -> Result<()> {
61    if !DockerSandbox::is_sandbox_available().await {
62        bail!(
63            "Docker Sandbox is not available.\n\
64             \n\
65             Requirements:\n\
66             • Docker Desktop 4.40 or later (https://www.docker.com/products/docker-desktop/)\n\
67             • Docker daemon must be running\n\
68             \n\
69             To disable the sandbox, remove --sandbox flag or set `config.agent.sandbox: false`."
70        );
71    }
72    Ok(())
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use std::collections::HashMap;
79
80    fn make_global_sandbox_enabled() -> HashMap<String, serde_yaml::Value> {
81        serde_yaml::from_str(
82            r#"
83sandbox:
84  enabled: true
85  image: "ubuntu:22.04"
86"#,
87        )
88        .unwrap()
89    }
90
91    fn make_agent_sandbox_true() -> HashMap<String, serde_yaml::Value> {
92        serde_yaml::from_str("sandbox: true").unwrap()
93    }
94
95    #[test]
96    fn cli_flag_wins_over_config() {
97        let mode = resolve_mode(true, &HashMap::new(), &HashMap::new());
98        assert_eq!(mode, SandboxMode::FullWorkflow);
99    }
100
101    #[test]
102    fn global_config_enabled_gives_devbox_mode() {
103        let global = make_global_sandbox_enabled();
104        let mode = resolve_mode(false, &global, &HashMap::new());
105        assert_eq!(mode, SandboxMode::Devbox);
106    }
107
108    #[test]
109    fn agent_config_sandbox_true_gives_agent_only() {
110        let agent = make_agent_sandbox_true();
111        let mode = resolve_mode(false, &HashMap::new(), &agent);
112        assert_eq!(mode, SandboxMode::AgentOnly);
113    }
114
115    #[test]
116    fn no_config_gives_disabled() {
117        let mode = resolve_mode(false, &HashMap::new(), &HashMap::new());
118        assert_eq!(mode, SandboxMode::Disabled);
119    }
120
121    #[test]
122    fn cli_flag_overrides_agent_config() {
123        let agent = make_agent_sandbox_true();
124        let mode = resolve_mode(true, &HashMap::new(), &agent);
125        assert_eq!(mode, SandboxMode::FullWorkflow);
126    }
127}