1use async_trait::async_trait;
25use serde_json::{json, Value};
26use synaptic_core::{SynapticError, Tool};
27
28#[derive(Debug, Clone)]
30pub struct E2BConfig {
31 pub api_key: String,
33 pub template: String,
35 pub timeout_secs: u64,
37}
38
39impl E2BConfig {
40 pub fn new(api_key: impl Into<String>) -> Self {
42 Self {
43 api_key: api_key.into(),
44 template: "base".to_string(),
45 timeout_secs: 30,
46 }
47 }
48
49 pub fn with_template(mut self, template: impl Into<String>) -> Self {
51 self.template = template.into();
52 self
53 }
54
55 pub fn with_timeout(mut self, secs: u64) -> Self {
57 self.timeout_secs = secs;
58 self
59 }
60}
61
62pub struct E2BSandboxTool {
67 config: E2BConfig,
68 client: reqwest::Client,
69}
70
71impl E2BSandboxTool {
72 pub fn new(config: E2BConfig) -> Self {
74 Self {
75 config,
76 client: reqwest::Client::new(),
77 }
78 }
79}
80
81#[async_trait]
82impl Tool for E2BSandboxTool {
83 fn name(&self) -> &'static str {
84 "e2b_code_executor"
85 }
86
87 fn description(&self) -> &'static str {
88 "Execute code in an isolated E2B cloud sandbox. Supports Python, JavaScript, and other \
89 languages. Returns stdout, stderr, and exit code."
90 }
91
92 fn parameters(&self) -> Option<Value> {
93 Some(json!({
94 "type": "object",
95 "properties": {
96 "code": {
97 "type": "string",
98 "description": "The code to execute"
99 },
100 "language": {
101 "type": "string",
102 "enum": ["python", "javascript", "bash"],
103 "description": "The programming language of the code"
104 }
105 },
106 "required": ["code", "language"]
107 }))
108 }
109
110 async fn call(&self, args: Value) -> Result<Value, SynapticError> {
111 let code = args["code"]
112 .as_str()
113 .ok_or_else(|| SynapticError::Tool("missing 'code' parameter".to_string()))?;
114 let language = args["language"].as_str().unwrap_or("python");
115
116 let create_resp = self
118 .client
119 .post("https://api.e2b.dev/sandboxes")
120 .header("X-API-Key", &self.config.api_key)
121 .header("Content-Type", "application/json")
122 .json(&json!({
123 "template": self.config.template,
124 "timeout": self.config.timeout_secs,
125 }))
126 .send()
127 .await
128 .map_err(|e| SynapticError::Tool(format!("E2B create sandbox: {e}")))?;
129
130 let create_status = create_resp.status().as_u16();
131 let create_body: Value = create_resp
132 .json()
133 .await
134 .map_err(|e| SynapticError::Tool(format!("E2B create parse: {e}")))?;
135
136 if create_status != 200 && create_status != 201 {
137 return Err(SynapticError::Tool(format!(
138 "E2B create sandbox error ({}): {}",
139 create_status, create_body
140 )));
141 }
142
143 let sandbox_id = create_body["sandboxId"]
144 .as_str()
145 .or_else(|| create_body["sandbox_id"].as_str())
146 .ok_or_else(|| SynapticError::Tool("E2B: missing sandbox ID in response".to_string()))?
147 .to_string();
148
149 let exec_resp = self
151 .client
152 .post(format!(
153 "https://api.e2b.dev/sandboxes/{}/process",
154 sandbox_id
155 ))
156 .header("X-API-Key", &self.config.api_key)
157 .header("Content-Type", "application/json")
158 .json(&json!({
159 "cmd": get_cmd(language, code),
160 "timeout": self.config.timeout_secs,
161 }))
162 .send()
163 .await;
164
165 let _ = self
167 .client
168 .delete(format!("https://api.e2b.dev/sandboxes/{}", sandbox_id))
169 .header("X-API-Key", &self.config.api_key)
170 .send()
171 .await;
172
173 let exec_resp = exec_resp.map_err(|e| SynapticError::Tool(format!("E2B execute: {e}")))?;
174 let exec_body: Value = exec_resp
175 .json()
176 .await
177 .map_err(|e| SynapticError::Tool(format!("E2B execute parse: {e}")))?;
178
179 Ok(json!({
180 "stdout": exec_body["stdout"],
181 "stderr": exec_body["stderr"],
182 "exit_code": exec_body["exitCode"]
183 .as_i64()
184 .unwrap_or_else(|| exec_body["exit_code"].as_i64().unwrap_or(0)),
185 }))
186 }
187}
188
189fn get_cmd(language: &str, code: &str) -> Vec<String> {
190 match language {
191 "python" => vec!["python3".to_string(), "-c".to_string(), code.to_string()],
192 "javascript" | "js" => vec!["node".to_string(), "-e".to_string(), code.to_string()],
193 _ => vec!["bash".to_string(), "-c".to_string(), code.to_string()],
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn config_defaults() {
203 let config = E2BConfig::new("test-key");
204 assert_eq!(config.api_key, "test-key");
205 assert_eq!(config.template, "base");
206 assert_eq!(config.timeout_secs, 30);
207 }
208
209 #[test]
210 fn config_builder() {
211 let config = E2BConfig::new("key")
212 .with_template("python")
213 .with_timeout(60);
214 assert_eq!(config.template, "python");
215 assert_eq!(config.timeout_secs, 60);
216 }
217
218 #[test]
219 fn tool_name() {
220 let tool = E2BSandboxTool::new(E2BConfig::new("key"));
221 assert_eq!(tool.name(), "e2b_code_executor");
222 }
223
224 #[test]
225 fn tool_description_contains_sandbox() {
226 let tool = E2BSandboxTool::new(E2BConfig::new("key"));
227 assert!(tool.description().contains("sandbox") || tool.description().contains("E2B"));
228 }
229
230 #[test]
231 fn tool_parameters() {
232 let tool = E2BSandboxTool::new(E2BConfig::new("key"));
233 let params = tool.parameters().unwrap();
234 assert_eq!(params["type"], "object");
235 assert!(params["properties"]["code"].is_object());
236 assert!(params["properties"]["language"].is_object());
237 }
238
239 #[test]
240 fn get_cmd_python() {
241 let cmd = get_cmd("python", "print('hi')");
242 assert_eq!(cmd[0], "python3");
243 assert_eq!(cmd[1], "-c");
244 assert_eq!(cmd[2], "print('hi')");
245 }
246
247 #[test]
248 fn get_cmd_javascript() {
249 let cmd = get_cmd("javascript", "console.log('hi')");
250 assert_eq!(cmd[0], "node");
251 assert_eq!(cmd[1], "-e");
252 }
253
254 #[test]
255 fn get_cmd_bash() {
256 let cmd = get_cmd("bash", "echo hi");
257 assert_eq!(cmd[0], "bash");
258 assert_eq!(cmd[1], "-c");
259 }
260
261 #[tokio::test]
262 async fn missing_code_returns_error() {
263 let tool = E2BSandboxTool::new(E2BConfig::new("key"));
264 let result = tool.call(json!({"language": "python"})).await;
265 assert!(result.is_err());
266 assert!(result.unwrap_err().to_string().contains("code"));
267 }
268}