rs_adk/code_executors/
container.rs1use async_trait::async_trait;
7
8use super::base::{CodeExecutor, CodeExecutorError};
9use super::types::{CodeExecutionInput, CodeExecutionResult};
10
11#[derive(Debug, Clone)]
13pub struct ContainerCodeExecutorConfig {
14 pub image: String,
16 pub memory_limit: Option<String>,
18 pub cpu_limit: Option<String>,
20 pub network_mode: Option<String>,
22 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#[derive(Debug, Clone)]
43pub struct ContainerCodeExecutor {
44 config: ContainerCodeExecutorConfig,
45}
46
47impl ContainerCodeExecutor {
48 pub fn new() -> Self {
50 Self {
51 config: ContainerCodeExecutorConfig::default(),
52 }
53 }
54
55 pub fn with_config(config: ContainerCodeExecutorConfig) -> Self {
57 Self { config }
58 }
59
60 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}