Skip to main content

gemini_adk_rs/code_executors/
container.rs

1//! Container-based code executor — runs code in Docker containers.
2//!
3//! Mirrors ADK-Python's `container_code_executor`. Provides sandboxed
4//! code execution by running code inside Docker containers.
5
6use async_trait::async_trait;
7
8use super::base::{CodeExecutor, CodeExecutorError};
9use super::types::{CodeExecutionInput, CodeExecutionResult};
10
11/// Configuration for container-based code execution.
12#[derive(Debug, Clone)]
13pub struct ContainerCodeExecutorConfig {
14    /// Docker image to use for code execution.
15    pub image: String,
16    /// Container memory limit (e.g., "256m").
17    pub memory_limit: Option<String>,
18    /// Container CPU limit (e.g., "0.5").
19    pub cpu_limit: Option<String>,
20    /// Network mode (e.g., "none" for no network access).
21    pub network_mode: Option<String>,
22    /// Execution timeout in seconds.
23    pub timeout_secs: u64,
24}
25
26impl Default for ContainerCodeExecutorConfig {
27    fn default() -> Self {
28        Self {
29            image: "python:3.12-slim".into(),
30            memory_limit: Some("256m".into()),
31            cpu_limit: Some("0.5".into()),
32            network_mode: Some("none".into()),
33            timeout_secs: 30,
34        }
35    }
36}
37
38/// Code executor that runs code in Docker containers.
39///
40/// Provides strong isolation by executing code in disposable
41/// Docker containers with configurable resource limits.
42#[derive(Debug, Clone)]
43pub struct ContainerCodeExecutor {
44    config: ContainerCodeExecutorConfig,
45}
46
47impl ContainerCodeExecutor {
48    /// Create a new container code executor with default configuration.
49    pub fn new() -> Self {
50        Self {
51            config: ContainerCodeExecutorConfig::default(),
52        }
53    }
54
55    /// Create with a custom configuration.
56    pub fn with_config(config: ContainerCodeExecutorConfig) -> Self {
57        Self { config }
58    }
59
60    /// Returns the configured Docker image.
61    pub fn image(&self) -> &str {
62        &self.config.image
63    }
64}
65
66impl Default for ContainerCodeExecutor {
67    fn default() -> Self {
68        Self::new()
69    }
70}
71
72#[async_trait]
73impl CodeExecutor for ContainerCodeExecutor {
74    async fn execute_code(
75        &self,
76        input: CodeExecutionInput,
77    ) -> Result<CodeExecutionResult, CodeExecutorError> {
78        let mut cmd = tokio::process::Command::new("docker");
79        cmd.arg("run").arg("--rm").arg("--read-only");
80
81        if let Some(ref mem) = self.config.memory_limit {
82            cmd.arg("--memory").arg(mem);
83        }
84        if let Some(ref cpu) = self.config.cpu_limit {
85            cmd.arg("--cpus").arg(cpu);
86        }
87        if let Some(ref net) = self.config.network_mode {
88            cmd.arg("--network").arg(net);
89        }
90
91        cmd.arg(&self.config.image)
92            .arg("python3")
93            .arg("-c")
94            .arg(&input.code);
95
96        let output = tokio::time::timeout(
97            std::time::Duration::from_secs(self.config.timeout_secs),
98            cmd.output(),
99        )
100        .await
101        .map_err(|_| CodeExecutorError::Timeout(self.config.timeout_secs))?
102        .map_err(|e| CodeExecutorError::ExecutionFailed(format!("Docker execution failed: {e}")))?;
103
104        Ok(CodeExecutionResult {
105            stdout: String::from_utf8_lossy(&output.stdout).to_string(),
106            stderr: String::from_utf8_lossy(&output.stderr).to_string(),
107            output_files: vec![],
108        })
109    }
110
111    fn stateful(&self) -> bool {
112        false
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn default_config() {
122        let config = ContainerCodeExecutorConfig::default();
123        assert_eq!(config.image, "python:3.12-slim");
124        assert_eq!(config.timeout_secs, 30);
125    }
126
127    #[test]
128    fn executor_metadata() {
129        let exec = ContainerCodeExecutor::new();
130        assert_eq!(exec.image(), "python:3.12-slim");
131        assert!(!exec.stateful());
132    }
133
134    #[test]
135    fn custom_config() {
136        let exec = ContainerCodeExecutor::with_config(ContainerCodeExecutorConfig {
137            image: "node:20-slim".into(),
138            memory_limit: Some("512m".into()),
139            cpu_limit: None,
140            network_mode: None,
141            timeout_secs: 60,
142        });
143        assert_eq!(exec.image(), "node:20-slim");
144    }
145}