use async_trait::async_trait;
use serde_json::{json, Value};
use synaptic_core::{SynapticError, Tool};
#[derive(Debug, Clone)]
pub struct E2BConfig {
pub api_key: String,
pub template: String,
pub timeout_secs: u64,
}
impl E2BConfig {
pub fn new(api_key: impl Into<String>) -> Self {
Self {
api_key: api_key.into(),
template: "base".to_string(),
timeout_secs: 30,
}
}
pub fn with_template(mut self, template: impl Into<String>) -> Self {
self.template = template.into();
self
}
pub fn with_timeout(mut self, secs: u64) -> Self {
self.timeout_secs = secs;
self
}
}
pub struct E2BSandboxTool {
config: E2BConfig,
client: reqwest::Client,
}
impl E2BSandboxTool {
pub fn new(config: E2BConfig) -> Self {
Self {
config,
client: reqwest::Client::new(),
}
}
}
#[async_trait]
impl Tool for E2BSandboxTool {
fn name(&self) -> &'static str {
"e2b_code_executor"
}
fn description(&self) -> &'static str {
"Execute code in an isolated E2B cloud sandbox. Supports Python, JavaScript, and other \
languages. Returns stdout, stderr, and exit code."
}
fn parameters(&self) -> Option<Value> {
Some(json!({
"type": "object",
"properties": {
"code": {
"type": "string",
"description": "The code to execute"
},
"language": {
"type": "string",
"enum": ["python", "javascript", "bash"],
"description": "The programming language of the code"
}
},
"required": ["code", "language"]
}))
}
async fn call(&self, args: Value) -> Result<Value, SynapticError> {
let code = args["code"]
.as_str()
.ok_or_else(|| SynapticError::Tool("missing 'code' parameter".to_string()))?;
let language = args["language"].as_str().unwrap_or("python");
let create_resp = self
.client
.post("https://api.e2b.dev/sandboxes")
.header("X-API-Key", &self.config.api_key)
.header("Content-Type", "application/json")
.json(&json!({
"template": self.config.template,
"timeout": self.config.timeout_secs,
}))
.send()
.await
.map_err(|e| SynapticError::Tool(format!("E2B create sandbox: {e}")))?;
let create_status = create_resp.status().as_u16();
let create_body: Value = create_resp
.json()
.await
.map_err(|e| SynapticError::Tool(format!("E2B create parse: {e}")))?;
if create_status != 200 && create_status != 201 {
return Err(SynapticError::Tool(format!(
"E2B create sandbox error ({}): {}",
create_status, create_body
)));
}
let sandbox_id = create_body["sandboxId"]
.as_str()
.or_else(|| create_body["sandbox_id"].as_str())
.ok_or_else(|| SynapticError::Tool("E2B: missing sandbox ID in response".to_string()))?
.to_string();
let exec_resp = self
.client
.post(format!(
"https://api.e2b.dev/sandboxes/{}/process",
sandbox_id
))
.header("X-API-Key", &self.config.api_key)
.header("Content-Type", "application/json")
.json(&json!({
"cmd": get_cmd(language, code),
"timeout": self.config.timeout_secs,
}))
.send()
.await;
let _ = self
.client
.delete(format!("https://api.e2b.dev/sandboxes/{}", sandbox_id))
.header("X-API-Key", &self.config.api_key)
.send()
.await;
let exec_resp = exec_resp.map_err(|e| SynapticError::Tool(format!("E2B execute: {e}")))?;
let exec_body: Value = exec_resp
.json()
.await
.map_err(|e| SynapticError::Tool(format!("E2B execute parse: {e}")))?;
Ok(json!({
"stdout": exec_body["stdout"],
"stderr": exec_body["stderr"],
"exit_code": exec_body["exitCode"]
.as_i64()
.unwrap_or_else(|| exec_body["exit_code"].as_i64().unwrap_or(0)),
}))
}
}
fn get_cmd(language: &str, code: &str) -> Vec<String> {
match language {
"python" => vec!["python3".to_string(), "-c".to_string(), code.to_string()],
"javascript" | "js" => vec!["node".to_string(), "-e".to_string(), code.to_string()],
_ => vec!["bash".to_string(), "-c".to_string(), code.to_string()],
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn config_defaults() {
let config = E2BConfig::new("test-key");
assert_eq!(config.api_key, "test-key");
assert_eq!(config.template, "base");
assert_eq!(config.timeout_secs, 30);
}
#[test]
fn config_builder() {
let config = E2BConfig::new("key")
.with_template("python")
.with_timeout(60);
assert_eq!(config.template, "python");
assert_eq!(config.timeout_secs, 60);
}
#[test]
fn tool_name() {
let tool = E2BSandboxTool::new(E2BConfig::new("key"));
assert_eq!(tool.name(), "e2b_code_executor");
}
#[test]
fn tool_description_contains_sandbox() {
let tool = E2BSandboxTool::new(E2BConfig::new("key"));
assert!(tool.description().contains("sandbox") || tool.description().contains("E2B"));
}
#[test]
fn tool_parameters() {
let tool = E2BSandboxTool::new(E2BConfig::new("key"));
let params = tool.parameters().unwrap();
assert_eq!(params["type"], "object");
assert!(params["properties"]["code"].is_object());
assert!(params["properties"]["language"].is_object());
}
#[test]
fn get_cmd_python() {
let cmd = get_cmd("python", "print('hi')");
assert_eq!(cmd[0], "python3");
assert_eq!(cmd[1], "-c");
assert_eq!(cmd[2], "print('hi')");
}
#[test]
fn get_cmd_javascript() {
let cmd = get_cmd("javascript", "console.log('hi')");
assert_eq!(cmd[0], "node");
assert_eq!(cmd[1], "-e");
}
#[test]
fn get_cmd_bash() {
let cmd = get_cmd("bash", "echo hi");
assert_eq!(cmd[0], "bash");
assert_eq!(cmd[1], "-c");
}
#[tokio::test]
async fn missing_code_returns_error() {
let tool = E2BSandboxTool::new(E2BConfig::new("key"));
let result = tool.call(json!({"language": "python"})).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("code"));
}
}