use async_trait::async_trait;
use boa_engine::{Context, JsValue, Source};
use std::time::Instant;
use crate::{
BackendCapabilities, CodeExecutor, ExecutionError, ExecutionIsolation, ExecutionLanguage,
ExecutionPayload, ExecutionRequest, ExecutionResult, ExecutionStatus,
};
#[derive(Debug, Default)]
pub struct EmbeddedJsExecutor;
impl EmbeddedJsExecutor {
pub fn new() -> Self {
Self
}
}
#[async_trait]
impl CodeExecutor for EmbeddedJsExecutor {
fn name(&self) -> &str {
"EmbeddedJsExecutor"
}
fn capabilities(&self) -> BackendCapabilities {
BackendCapabilities {
isolation: ExecutionIsolation::InProcess,
enforce_network_policy: true,
enforce_filesystem_policy: true,
enforce_environment_policy: true,
enforce_timeout: true,
supports_structured_output: true,
supports_process_execution: false,
supports_persistent_workspace: false,
supports_interactive_sessions: false,
}
}
fn supports_language(&self, lang: &ExecutionLanguage) -> bool {
matches!(lang, ExecutionLanguage::JavaScript)
}
async fn execute(&self, request: ExecutionRequest) -> Result<ExecutionResult, ExecutionError> {
crate::validate_request(&self.capabilities(), &[ExecutionLanguage::JavaScript], &request)?;
let code = match &request.payload {
ExecutionPayload::Source { code } => code.clone(),
ExecutionPayload::GuestModule { .. } => {
return Err(ExecutionError::InvalidRequest(
"EmbeddedJsExecutor does not support guest modules".to_string(),
));
}
};
if code.trim().is_empty() {
return Err(ExecutionError::InvalidRequest("empty JavaScript source".to_string()));
}
let timeout = request.sandbox.timeout;
let input = request.input.clone();
let result = tokio::task::spawn_blocking(move || {
let start = Instant::now();
let mut context = Context::default();
let input_str = input
.as_ref()
.map(|v| serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()))
.unwrap_or_else(|| "null".to_string());
let setup = format!("var input = {input_str};");
if let Err(e) = context.eval(Source::from_bytes(&setup)) {
return ExecutionResult {
status: ExecutionStatus::Failed,
stdout: String::new(),
stderr: format!("Failed to inject input: {e:?}"),
output: None,
exit_code: None,
stdout_truncated: false,
stderr_truncated: false,
duration_ms: start.elapsed().as_millis() as u64,
metadata: None,
};
}
let wrapped = format!("(function() {{ {code} }})()");
let eval_result = context.eval(Source::from_bytes(&wrapped));
let duration_ms = start.elapsed().as_millis() as u64;
if start.elapsed() > timeout {
return ExecutionResult {
status: ExecutionStatus::Timeout,
stdout: String::new(),
stderr: format!("execution exceeded timeout of {}ms", timeout.as_millis()),
output: None,
exit_code: None,
stdout_truncated: false,
stderr_truncated: false,
duration_ms,
metadata: None,
};
}
match eval_result {
Ok(val) => {
let json_output = js_value_to_json(&val, &mut context);
ExecutionResult {
status: ExecutionStatus::Success,
stdout: String::new(),
stderr: String::new(),
output: Some(json_output),
exit_code: None,
stdout_truncated: false,
stderr_truncated: false,
duration_ms,
metadata: None,
}
}
Err(e) => ExecutionResult {
status: ExecutionStatus::Failed,
stdout: String::new(),
stderr: format!("JavaScript error: {e:?}"),
output: None,
exit_code: None,
stdout_truncated: false,
stderr_truncated: false,
duration_ms,
metadata: None,
},
}
})
.await
.map_err(|e| ExecutionError::InternalError(format!("JS thread panicked: {e}")))?;
Ok(result)
}
}
fn js_value_to_json(val: &JsValue, context: &mut Context) -> serde_json::Value {
match val {
JsValue::Null | JsValue::Undefined => serde_json::Value::Null,
JsValue::Boolean(b) => serde_json::Value::Bool(*b),
JsValue::Integer(n) => serde_json::json!(*n),
JsValue::Rational(n) => {
if n.is_finite() {
serde_json::json!(*n)
} else {
serde_json::Value::Null
}
}
JsValue::String(s) => serde_json::Value::String(s.to_std_string_escaped()),
JsValue::BigInt(n) => serde_json::Value::String(n.to_string()),
JsValue::Symbol(_) => serde_json::Value::Null,
JsValue::Object(_) => {
let stringify_code = format!(
"JSON.stringify({})",
"__adk_tmp__"
);
let global = context.global_object();
let key = boa_engine::JsString::from("__adk_tmp__");
let _ = global.set(key.clone(), val.clone(), false, context);
let result = context.eval(Source::from_bytes(stringify_code.as_bytes()));
let _ = global.delete_property_or_throw(key, context);
if let Ok(json_val) = result {
if let Some(s) = json_val.as_string() {
let std_str: String = s.to_std_string_escaped();
if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&std_str) {
return parsed;
}
}
}
serde_json::Value::String("[object]".to_string())
}
}
}