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;
20pub mod proxy;
21
22pub use config::{SandboxConfig, SandboxMode};
23pub use docker::DockerSandbox;
24pub use proxy::ApiProxy;
25
26use anyhow::{bail, Result};
27
28/// Determine the sandbox mode from workflow config and CLI flags.
29pub fn resolve_mode(
30    sandbox_flag: bool,
31    global_config: &std::collections::HashMap<String, serde_yaml::Value>,
32    agent_config: &std::collections::HashMap<String, serde_yaml::Value>,
33) -> SandboxMode {
34    // CLI --sandbox flag takes priority → Mode 1
35    if sandbox_flag {
36        return SandboxMode::FullWorkflow;
37    }
38
39    // config.global.sandbox.enabled: true → Mode 3 (Devbox)
40    if let Some(serde_yaml::Value::Mapping(m)) = global_config.get("sandbox") {
41        if m.get(serde_yaml::Value::String("enabled".into()))
42            .and_then(|v| v.as_bool())
43            .unwrap_or(false)
44        {
45            return SandboxMode::Devbox;
46        }
47    }
48
49    // config.agent.sandbox: true → Mode 2
50    if agent_config
51        .get("sandbox")
52        .and_then(|v| v.as_bool())
53        .unwrap_or(false)
54    {
55        return SandboxMode::AgentOnly;
56    }
57
58    SandboxMode::Disabled
59}
60
61/// Validate that Docker is available; return a friendly error if not.
62pub async fn require_docker() -> Result<()> {
63    if !DockerSandbox::is_sandbox_available().await {
64        bail!(
65            "Docker Sandbox is not available.\n\
66             \n\
67             Requirements:\n\
68             • Docker Desktop 4.40 or later (https://www.docker.com/products/docker-desktop/)\n\
69             • Docker daemon must be running\n\
70             \n\
71             To disable the sandbox, remove --sandbox flag or set `config.agent.sandbox: false`."
72        );
73    }
74    Ok(())
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use std::collections::HashMap;
81
82    fn make_global_sandbox_enabled() -> HashMap<String, serde_yaml::Value> {
83        serde_yaml::from_str(
84            r#"
85sandbox:
86  enabled: true
87  image: "ubuntu:22.04"
88"#,
89        )
90        .unwrap()
91    }
92
93    fn make_agent_sandbox_true() -> HashMap<String, serde_yaml::Value> {
94        serde_yaml::from_str("sandbox: true").unwrap()
95    }
96
97    #[test]
98    fn cli_flag_wins_over_config() {
99        let mode = resolve_mode(true, &HashMap::new(), &HashMap::new());
100        assert_eq!(mode, SandboxMode::FullWorkflow);
101    }
102
103    #[test]
104    fn global_config_enabled_gives_devbox_mode() {
105        let global = make_global_sandbox_enabled();
106        let mode = resolve_mode(false, &global, &HashMap::new());
107        assert_eq!(mode, SandboxMode::Devbox);
108    }
109
110    #[test]
111    fn agent_config_sandbox_true_gives_agent_only() {
112        let agent = make_agent_sandbox_true();
113        let mode = resolve_mode(false, &HashMap::new(), &agent);
114        assert_eq!(mode, SandboxMode::AgentOnly);
115    }
116
117    #[test]
118    fn no_config_gives_disabled() {
119        let mode = resolve_mode(false, &HashMap::new(), &HashMap::new());
120        assert_eq!(mode, SandboxMode::Disabled);
121    }
122
123    #[test]
124    fn cli_flag_overrides_agent_config() {
125        let agent = make_agent_sandbox_true();
126        let mode = resolve_mode(true, &HashMap::new(), &agent);
127        assert_eq!(mode, SandboxMode::FullWorkflow);
128    }
129}