mod support; use agentos_execution::{ v8_runtime::map_bridge_method, CreateJavascriptContextRequest, GuestRuntimeConfig, JavascriptExecution, JavascriptExecutionEvent, JavascriptExecutionLimits, JavascriptExecutionResult, JavascriptSyncRpcRequest, StartJavascriptExecutionRequest, }; use base64::Engine; use serde::Deserialize; use serde_json::{json, Value}; use std::collections::{BTreeMap, VecDeque}; use std::fs; use std::io::{Read, Write}; use std::os::unix::fs::symlink; use std::os::unix::fs::PermissionsExt; use std::os::unix::process::CommandExt; use std::os::unix::process::ExitStatusExt; use std::path::Path; use std::process::{Child, ChildStdin, Command, Stdio}; use std::sync::mpsc::{self, Receiver, Sender, TryRecvError}; use std::thread; use std::time::{Duration, Instant}; use tempfile::tempdir; /* US-040 execution-test audit Deleted coverage: - `tests/javascript.rs`: removed because the file only exercised the old `legacy-js-tests` host-Node guest path (`loader.mjs`, `runner.mjs`, import-cache mutation, and `Command::new("node")` process behavior). The V8 isolate path no longer uses that guest execution model. - `permission_flags::node_permission_flags_do_not_expose_workspace_root_or_entrypoint_parent_writes`: removed because its JavaScript assertions depended on host-Node permission flags emitted for guest JS launches. V8 guest JS now stays in-process, while the remaining permission-flag tests still cover the real host-Node launches that remain for Python and WASM. - `benchmark::javascript_benchmark_harness_covers_required_startup_and_import_scenarios`: removed because it depended on pre-V8 benchmark marker behavior from the old startup harness instead of validating the current V8 execution path. The stable artifact and markdown benchmark tests remain. */ // Timing-sensitive assertions flake under the CPU contention of a parallel test // run (see CLAUDE.md > Testing). Gated off by default; the nightly timing lane // sets AGENTOS_RUN_TIMING_TESTS=1 to enforce them. fn run_timing_sensitive_tests() -> bool { std::env::var_os("AGENTOS_RUN_TIMING_TESTS").is_some() } fn write_fixture(path: &Path, contents: &str) { if let Some(parent) = path.parent() { fs::create_dir_all(parent).expect("create fixture parent dirs"); } fs::write(path, contents).expect("write fixture"); } fn run_host_node_json(cwd: &Path, entrypoint: &Path) -> Value { let output = Command::new("node") .arg(entrypoint) .current_dir(cwd) .output() .expect("run host node"); assert!( output.status.success(), "host node failed with status {:?}\nstdout:\n{}\nstderr:\n{}", output.status.code(), String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); serde_json::from_slice(&output.stdout).expect("parse host JSON") } fn write_fake_node_binary(path: &Path, log_path: &Path) { let script = format!( "#!/bin/sh\nset -eu\nprintf 'guest-node-invoked\\n' >> \"{}\"\nexit 99\n", log_path.display() ); fs::write(path, script).expect("write fake node binary"); let mut permissions = fs::metadata(path) .expect("fake node metadata") .permissions(); permissions.set_mode(0o755); fs::set_permissions(path, permissions).expect("chmod fake node binary"); } #[derive(Debug, Deserialize, Default)] #[serde(rename_all = "camelCase")] struct TestJavascriptChildProcessSpawnOptions { #[serde(default)] argv0: Option, #[serde(default)] cwd: Option, #[serde(default)] env: BTreeMap, #[serde(default)] internal_bootstrap_env: BTreeMap, #[serde(default)] shell: bool, #[serde(default)] timeout: Option, #[serde(default, rename = "killSignal")] kill_signal: Option, } #[derive(Debug, Deserialize)] struct TestJavascriptChildProcessSpawnRequest { command: String, #[serde(default)] args: Vec, #[serde(default)] options: TestJavascriptChildProcessSpawnOptions, } type TestJavascriptChildProcessSpawnSyncRequest = ( TestJavascriptChildProcessSpawnRequest, Option, Option>, ); #[derive(Debug, Deserialize, Default)] #[serde(rename_all = "camelCase")] struct TestLegacyJavascriptChildProcessSpawnOptions { #[serde(default)] argv0: Option, #[serde(default)] cwd: Option, #[serde(default)] env: BTreeMap, #[serde(default)] input: Option, #[serde(default)] shell: bool, #[serde(default, rename = "maxBuffer")] max_buffer: Option, #[serde(default)] timeout: Option, #[serde(default, rename = "killSignal")] kill_signal: Option, } enum HostChildOutputEvent { Stdout(Vec), Stderr(Vec), StreamClosed, } struct HostChildRecord { child: Child, stdin: Option, output_events: Receiver, pending_events: VecDeque, exit_status: Option<(Option, Option)>, open_streams: usize, } #[derive(Default)] struct HostChildProcessHarness { next_child_id: usize, children: BTreeMap, } impl HostChildProcessHarness { fn handle_request( &mut self, host_cwd: &Path, request: JavascriptSyncRpcRequest, ) -> Result { match request.method.as_str() { "child_process.spawn" => self.spawn(host_cwd, &request.args), "child_process.spawn_sync" => self.spawn_sync(host_cwd, &request.args), "child_process.poll" => self.poll(&request.args), "child_process.write_stdin" => self.write_stdin(&request.args), "child_process.close_stdin" => self.close_stdin(&request.args), "child_process.kill" => self.kill(&request.args), "fs.writeFileSync" => self.write_file(host_cwd, &request.args), other => Err(format!("unsupported sync RPC method: {other}")), } } fn spawn(&mut self, host_cwd: &Path, args: &[Value]) -> Result { let request = parse_test_child_process_spawn_request(args)?; let child_id = { self.next_child_id += 1; format!("child-{}", self.next_child_id) }; let mut command = if request.options.shell { let mut command = Command::new("/bin/sh"); command.arg("-c").arg(&request.command); command.args(&request.args); command } else { let mut command = Command::new(self.map_guest_path(host_cwd, &request.command)); command.args( request .args .iter() .map(|arg| self.map_guest_path(host_cwd, arg)), ); command }; if let Some(argv0) = request.options.argv0.as_deref() { command.arg0(argv0); } let child_cwd = request .options .cwd .as_deref() .map(|cwd| std::path::PathBuf::from(self.map_guest_path(host_cwd, cwd))) .unwrap_or_else(|| host_cwd.to_path_buf()); command .current_dir(child_cwd) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .env_clear() .envs(&request.options.env) .envs(&request.options.internal_bootstrap_env); let mut child = command .spawn() .map_err(|error| format!("spawn {} failed: {error}", request.command))?; let stdin = child.stdin.take(); let stdout = child .stdout .take() .ok_or_else(|| String::from("spawned child stdout pipe missing"))?; let stderr = child .stderr .take() .ok_or_else(|| String::from("spawned child stderr pipe missing"))?; let (output_sender, output_events) = mpsc::channel(); spawn_output_reader(stdout, output_sender.clone(), true); spawn_output_reader(stderr, output_sender, false); let pid = child.id(); self.children.insert( child_id.clone(), HostChildRecord { child, stdin, output_events, pending_events: VecDeque::new(), exit_status: None, open_streams: 2, }, ); Ok(json!({ "childId": child_id, "pid": pid, "command": request.command, "args": request.args, })) } fn poll(&mut self, args: &[Value]) -> Result { let child_id = args .first() .and_then(Value::as_str) .ok_or_else(|| String::from("child_process.poll missing child id"))?; let wait_ms = args.get(1).and_then(Value::as_u64).unwrap_or_default(); let child = self .children .get_mut(child_id) .ok_or_else(|| format!("unknown child process {child_id}"))?; let deadline = std::time::Instant::now() + Duration::from_millis(wait_ms); loop { drain_child_output(child); if let Some(event) = child.pending_events.pop_front() { return Ok(event); } if child.exit_status.is_none() { if let Some(status) = child .child .try_wait() .map_err(|error| format!("try_wait {child_id} failed: {error}"))? { child.exit_status = Some(( status.code(), status.signal().map(|signal| match signal { 9 => String::from("SIGKILL"), 15 => String::from("SIGTERM"), other => format!("SIG{other}"), }), )); } } if let Some((exit_code, signal)) = child.exit_status.as_ref() { if child.pending_events.is_empty() && child.open_streams == 0 { let exit_code = *exit_code; let signal = signal.clone(); self.children.remove(child_id); return Ok(json!({ "type": "exit", "exitCode": exit_code, "signal": signal, })); } } if std::time::Instant::now() >= deadline { return Ok(Value::Null); } thread::sleep(Duration::from_millis(5)); } } /// Translate the host-child harness's durable output state into the /// evented stream protocol used by the production sidecar. The JavaScript /// child-process shim no longer issues recurring `child_process.poll` /// bridge calls, so this standalone conformance harness must emulate the /// sidecar's readiness pump instead of waiting for a guest poll request. fn drain_stream_events(&mut self) -> Result, String> { const MAX_EVENTS_PER_TURN: usize = 256; let child_ids: Vec = self.children.keys().cloned().collect(); let mut stream_events = Vec::new(); for child_id in child_ids { while stream_events.len() < MAX_EVENTS_PER_TURN { let event = self.poll(&[Value::String(child_id.clone()), json!(0)])?; let Some(event_type) = event.get("type").and_then(Value::as_str) else { break; }; match event_type { "stdout" | "stderr" => stream_events.push(( if event_type == "stdout" { "child_stdout" } else { "child_stderr" }, json!({ "sessionId": child_id, "data": event.get("data").cloned().unwrap_or(Value::Null), }), )), "exit" => { stream_events.push(( "child_exit", json!({ "sessionId": child_id, "code": event .get("exitCode") .and_then(Value::as_i64) .unwrap_or(1), "signal": Value::Null, }), )); break; } other => return Err(format!("unknown child event type {other}")), } } if stream_events.len() >= MAX_EVENTS_PER_TURN { break; } } Ok(stream_events) } fn spawn_sync(&mut self, host_cwd: &Path, args: &[Value]) -> Result { let (request, max_buffer, input) = parse_test_child_process_spawn_sync_request(args)?; let mut command = if request.options.shell { let mut command = Command::new("/bin/sh"); command.arg("-c").arg(&request.command); command.args(&request.args); command } else { let mut command = Command::new(self.map_guest_path(host_cwd, &request.command)); command.args( request .args .iter() .map(|arg| self.map_guest_path(host_cwd, arg)), ); command }; if let Some(argv0) = request.options.argv0.as_deref() { command.arg0(argv0); } let child_cwd = request .options .cwd .as_deref() .map(|cwd| std::path::PathBuf::from(self.map_guest_path(host_cwd, cwd))) .unwrap_or_else(|| host_cwd.to_path_buf()); command .current_dir(child_cwd) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .env_clear() .envs(&request.options.env) .envs(&request.options.internal_bootstrap_env); let mut child = command .spawn() .map_err(|error| format!("spawnSync {} failed: {error}", request.command))?; if let Some(input) = input.as_deref() { let stdin = child .stdin .as_mut() .ok_or_else(|| String::from("spawnSync child stdin pipe missing"))?; stdin .write_all(input) .map_err(|error| format!("write spawnSync stdin failed: {error}"))?; } child.stdin.take(); let mut timed_out = false; if let Some(timeout_ms) = request.options.timeout { let deadline = Instant::now() + Duration::from_millis(timeout_ms); loop { match child .try_wait() .map_err(|error| format!("try_wait for {} failed: {error}", request.command))? { Some(_) => break, None if !timed_out && Instant::now() >= deadline => { let signal = request .options .kill_signal .clone() .unwrap_or_else(|| String::from("SIGTERM")); let status = Command::new("kill") .arg(format!( "-{}", signal.strip_prefix("SIG").unwrap_or(&signal) )) .arg(child.id().to_string()) .status() .map_err(|error| { format!("send {signal} to {} failed: {error}", request.command) })?; if !status.success() { return Err(format!( "send {signal} to {} failed with {status}", request.command )); } timed_out = true; } None => thread::sleep(Duration::from_millis(5)), } } } let output = child .wait_with_output() .map_err(|error| format!("wait_with_output for {} failed: {error}", request.command))?; let stdout = String::from_utf8_lossy(&output.stdout).into_owned(); let stderr = String::from_utf8_lossy(&output.stderr).into_owned(); let max_buffer = max_buffer.unwrap_or(1024 * 1024); let max_buffer_exceeded = output.stdout.len() > max_buffer || output.stderr.len() > max_buffer; let signal = output.status.signal().map(|signal| match signal { 9 => String::from("SIGKILL"), 15 => String::from("SIGTERM"), other => format!("SIG{other}"), }); Ok(json!({ "stdout": stdout, "stderr": stderr, "code": output.status.code(), "signal": signal, "timedOut": timed_out, "maxBufferExceeded": max_buffer_exceeded, })) } fn write_stdin(&mut self, args: &[Value]) -> Result { let child_id = args .first() .and_then(Value::as_str) .ok_or_else(|| String::from("child_process.write_stdin missing child id"))?; let chunk = decode_guest_bytes( args.get(1) .ok_or_else(|| String::from("child_process.write_stdin missing chunk"))?, )?; let child = self .children .get_mut(child_id) .ok_or_else(|| format!("unknown child process {child_id}"))?; if let Some(stdin) = child.stdin.as_mut() { stdin .write_all(&chunk) .map_err(|error| format!("write stdin for {child_id} failed: {error}"))?; } Ok(Value::Null) } fn close_stdin(&mut self, args: &[Value]) -> Result { let child_id = args .first() .and_then(Value::as_str) .ok_or_else(|| String::from("child_process.close_stdin missing child id"))?; let child = self .children .get_mut(child_id) .ok_or_else(|| format!("unknown child process {child_id}"))?; child.stdin.take(); Ok(Value::Null) } fn kill(&mut self, args: &[Value]) -> Result { let child_id = args .first() .and_then(Value::as_str) .ok_or_else(|| String::from("child_process.kill missing child id"))?; let child = self .children .get_mut(child_id) .ok_or_else(|| format!("unknown child process {child_id}"))?; let signal = args.get(1).and_then(Value::as_str).unwrap_or("SIGTERM"); let status = Command::new("kill") .arg(format!("-{}", signal.strip_prefix("SIG").unwrap_or(signal))) .arg(child.child.id().to_string()) .status() .map_err(|error| format!("kill {child_id} failed: {error}"))?; if !status.success() { return Err(format!("kill {child_id} failed with {status}")); } Ok(Value::Null) } fn write_file(&mut self, host_cwd: &Path, args: &[Value]) -> Result { let path = args .first() .and_then(Value::as_str) .ok_or_else(|| String::from("fs.writeFileSync missing path"))?; let contents = decode_guest_bytes( args.get(1) .ok_or_else(|| String::from("fs.writeFileSync missing contents"))?, )?; let mapped_path = std::path::PathBuf::from(self.map_guest_path(host_cwd, path)); if let Some(parent) = mapped_path.parent() { fs::create_dir_all(parent) .map_err(|error| format!("create parent dirs for {} failed: {error}", path))?; } fs::write(&mapped_path, contents) .map_err(|error| format!("write guest file {} failed: {error}", path))?; Ok(Value::Null) } fn map_guest_path(&self, host_cwd: &Path, candidate: &str) -> String { if !candidate.starts_with('/') { return String::from(candidate); } for prefix in ["/root", "/workspace"] { if candidate == prefix { return host_cwd.to_string_lossy().into_owned(); } if let Some(relative) = candidate.strip_prefix(&format!("{prefix}/")) { return host_cwd.join(relative).to_string_lossy().into_owned(); } } String::from(candidate) } } impl Drop for HostChildProcessHarness { fn drop(&mut self) { for child in self.children.values_mut() { let _ = child.child.kill(); let _ = child.child.wait(); } } } fn spawn_output_reader( mut reader: impl Read + Send + 'static, sender: Sender, stdout: bool, ) { thread::spawn(move || { let mut buffer = [0_u8; 8192]; loop { match reader.read(&mut buffer) { Ok(0) => { let _ = sender.send(HostChildOutputEvent::StreamClosed); break; } Ok(read) => { let event = if stdout { HostChildOutputEvent::Stdout(buffer[..read].to_vec()) } else { HostChildOutputEvent::Stderr(buffer[..read].to_vec()) }; if sender.send(event).is_err() { break; } } Err(_) => { let _ = sender.send(HostChildOutputEvent::StreamClosed); break; } } } }); } fn drain_child_output(child: &mut HostChildRecord) { loop { match child.output_events.try_recv() { Ok(HostChildOutputEvent::Stdout(chunk)) => { child.pending_events.push_back(json!({ "type": "stdout", "data": encode_guest_bytes(&chunk), })); } Ok(HostChildOutputEvent::Stderr(chunk)) => { child.pending_events.push_back(json!({ "type": "stderr", "data": encode_guest_bytes(&chunk), })); } Ok(HostChildOutputEvent::StreamClosed) => { child.open_streams = child.open_streams.saturating_sub(1); } Err(TryRecvError::Empty | TryRecvError::Disconnected) => break, } } } fn encode_guest_bytes(bytes: &[u8]) -> Value { json!({ "__agentOSType": "bytes", "base64": base64::engine::general_purpose::STANDARD.encode(bytes), }) } fn decode_guest_bytes(value: &Value) -> Result, String> { let encoded = value .as_object() .ok_or_else(|| String::from("expected bytes payload object"))?; let base64 = encoded .get("base64") .and_then(Value::as_str) .ok_or_else(|| String::from("bytes payload missing base64"))?; base64::engine::general_purpose::STANDARD .decode(base64) .map_err(|error| format!("invalid base64 bytes payload: {error}")) } fn parse_test_child_process_spawn_request( args: &[Value], ) -> Result { if let Some(value) = args.first().cloned() { if let Ok(request) = serde_json::from_value::(value) { return Ok(request); } } let command = args .first() .and_then(Value::as_str) .ok_or_else(|| String::from("child_process.spawn missing command"))?; let parsed_args = args .get(1) .and_then(Value::as_str) .ok_or_else(|| String::from("child_process.spawn missing args payload")) .and_then(|value| { serde_json::from_str::>(value) .map_err(|error| format!("invalid child_process.spawn args payload: {error}")) })?; let parsed_options = args .get(2) .and_then(Value::as_str) .ok_or_else(|| String::from("child_process.spawn missing options payload")) .and_then(|value| { serde_json::from_str::(value) .map_err(|error| format!("invalid child_process.spawn options payload: {error}")) })?; Ok(TestJavascriptChildProcessSpawnRequest { command: String::from(command), args: parsed_args, options: TestJavascriptChildProcessSpawnOptions { argv0: parsed_options.argv0, cwd: parsed_options.cwd, env: parsed_options.env, internal_bootstrap_env: BTreeMap::new(), shell: parsed_options.shell, timeout: parsed_options.timeout, kill_signal: parsed_options.kill_signal, }, }) } fn parse_test_child_process_spawn_sync_request( args: &[Value], ) -> Result { let request = parse_test_child_process_spawn_request(args)?; let parsed_options = args .get(2) .and_then(Value::as_str) .ok_or_else(|| String::from("child_process.spawn_sync missing options payload")) .and_then(|value| { serde_json::from_str::(value).map_err( |error| format!("invalid child_process.spawn_sync options payload: {error}"), ) })?; let input = parsed_options .input .as_ref() .map(decode_guest_or_string_bytes) .transpose()?; Ok((request, parsed_options.max_buffer, input)) } fn decode_guest_or_string_bytes(value: &Value) -> Result, String> { match value { Value::String(text) => Ok(text.as_bytes().to_vec()), other => decode_guest_bytes(other), } } /// Poll for the next sync RPC, servicing (host-directly) any module-resolution /// sync RPCs that surface during module loading. Module resolution now flows as /// sync RPCs (`__resolve_module` / `__batch_resolve_modules` / `__load_file` / /// `__module_format`); these tests assert on the *next* non-module sync RPC. fn expect_next_sync_rpc( execution: &mut JavascriptExecution, what: &str, ) -> JavascriptSyncRpcRequest { loop { match execution .poll_event_blocking(Duration::from_secs(5)) .unwrap_or_else(|error| panic!("poll {what}: {error:?}")) { Some(JavascriptExecutionEvent::SyncRpcRequest(request)) => { if execution .try_service_standalone_module_sync_rpc(&request) .expect("service module sync RPC") { continue; } return request; } other => panic!("expected {what}, got {other:?}"), } } } fn wait_with_host_child_process_bridge( mut execution: JavascriptExecution, host_cwd: &Path, ) -> JavascriptExecutionResult { execution.close_stdin().expect("close JavaScript stdin"); let mut harness = HostChildProcessHarness::default(); let mut stdout = Vec::new(); let mut stderr = Vec::new(); let mut no_progress_deadline = Instant::now() + Duration::from_secs(5); loop { let stream_events = harness .drain_stream_events() .expect("drain host child-process stream events"); if !stream_events.is_empty() { no_progress_deadline = Instant::now() + Duration::from_secs(5); } for (event_type, payload) in stream_events { execution .send_stream_event(event_type, payload) .expect("send host child-process stream event"); } let poll_timeout = if harness.children.is_empty() { Duration::from_secs(5) } else { Duration::from_millis(10) }; match execution .poll_event_blocking(poll_timeout) .expect("poll JavaScript execution event") { Some(JavascriptExecutionEvent::Stdout(chunk)) => { stdout.extend(chunk); no_progress_deadline = Instant::now() + Duration::from_secs(5); } Some(JavascriptExecutionEvent::Stderr(chunk)) => { stderr.extend(chunk); no_progress_deadline = Instant::now() + Duration::from_secs(5); } Some(JavascriptExecutionEvent::SignalState { .. }) => { no_progress_deadline = Instant::now() + Duration::from_secs(5); } Some(JavascriptExecutionEvent::SyncRpcRequest(request)) => { no_progress_deadline = Instant::now() + Duration::from_secs(5); if execution .try_service_standalone_module_sync_rpc(&request) .expect("service module sync RPC") { continue; } let request_id = request.id; match harness.handle_request(host_cwd, request) { Ok(result) => execution .respond_sync_rpc_success(request_id, result) .expect("respond to child_process sync RPC"), Err(message) => execution .respond_sync_rpc_error(request_id, "ERR_TEST_CHILD_PROCESS_RPC", message) .expect("respond to child_process sync RPC error"), } } Some(JavascriptExecutionEvent::Exited(exit_code)) => { return JavascriptExecutionResult { execution_id: String::new(), exit_code, stdout, stderr, }; } None if Instant::now() < no_progress_deadline => {} None => panic!( "JavaScript execution timed out while awaiting exit; stdout={:?}; stderr={:?}; live_children={}", String::from_utf8_lossy(&stdout), String::from_utf8_lossy(&stderr), harness.children.len() ), } } } struct EnvVarGuard { key: &'static str, previous: Option, } impl EnvVarGuard { fn set_path(key: &'static str, value: &Path) -> Self { let previous = std::env::var(key).ok(); unsafe { std::env::set_var(key, value); } Self { key, previous } } fn set_value(key: &'static str, value: &str) -> Self { let previous = std::env::var(key).ok(); unsafe { std::env::set_var(key, value); } Self { key, previous } } } impl Drop for EnvVarGuard { fn drop(&mut self) { match &self.previous { Some(value) => unsafe { std::env::set_var(self.key, value); }, None => unsafe { std::env::remove_var(self.key); }, } } } fn javascript_contexts_preserve_vm_and_bootstrap_configuration() { let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: Some(String::from("./bootstrap.mjs")), compile_cache_root: None, }); assert_eq!(context.context_id, "js-ctx-1"); assert_eq!(context.vm_id, "vm-js"); assert_eq!(context.bootstrap_module.as_deref(), Some("./bootstrap.mjs")); assert_eq!(context.compile_cache_dir, None); } fn javascript_execution_uses_v8_runtime_without_spawning_guest_node_binary() { let temp = tempdir().expect("create temp dir"); let fake_node_path = temp.path().join("fake-node.sh"); let log_path = temp.path().join("node.log"); write_fake_node_binary(&fake_node_path, &log_path); let _node_binary = EnvVarGuard::set_path("AGENTOS_NODE_BINARY", &fake_node_path); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from("globalThis.__secureExecRanInV8 = true;")), }) .expect("start JavaScript execution"); assert!( execution.uses_shared_v8_runtime(), "guest JS should run inside the shared V8 runtime" ); assert_eq!( execution.child_pid(), 0, "shared V8 runtime executions should keep the embedded host pid internal" ); let result = execution.wait().expect("wait for JavaScript execution"); assert_eq!(result.exit_code, 0); assert!( !log_path.exists(), "guest JavaScript execution should not invoke the host node binary" ); } fn javascript_execution_virtual_os_identity_comes_from_guest_runtime_not_env() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), // os.* identity rides the typed guest_runtime; the env carries // contradictory values to prove the AGENTOS_VIRTUAL_OS_* knobs are // inert. argv0: None, guest_runtime: agentos_execution::GuestRuntimeConfig { os_cpu_count: Some(7), os_totalmem: Some(8_000_000_000), os_freemem: Some(4_000_000_000), os_hostname: Some(String::from("vm-hostname")), os_tmpdir: Some(String::from("/vm-tmp")), os_type: Some(String::from("VMType")), os_release: Some(String::from("1.2.3-vm")), os_version: Some(String::from("VM secure-exec build 42")), os_machine: Some(String::from("vm64")), ..Default::default() }, vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::from([ ( String::from("AGENTOS_VIRTUAL_OS_CPU_COUNT"), String::from("99"), ), ( String::from("AGENTOS_VIRTUAL_OS_TOTALMEM"), String::from("123"), ), ]), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" import os from "node:os"; if (os.cpus().length !== 7) throw new Error(`cpus=${os.cpus().length}`); if (os.totalmem() !== 8000000000) throw new Error(`totalmem=${os.totalmem()}`); if (os.freemem() !== 4000000000) throw new Error(`freemem=${os.freemem()}`); if (os.hostname() !== "vm-hostname") throw new Error(`hostname=${os.hostname()}`); if (os.tmpdir() !== "/vm-tmp") throw new Error(`tmpdir=${os.tmpdir()}`); if (os.type() !== "VMType") throw new Error(`type=${os.type()}`); if (os.release() !== "1.2.3-vm") throw new Error(`release=${os.release()}`); if (os.version() !== "VM secure-exec build 42") throw new Error(`version=${os.version()}`); if (os.machine() !== "vm64") throw new Error(`machine=${os.machine()}`); "#, )), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); let stderr = String::from_utf8_lossy(&result.stderr); assert_eq!(result.exit_code, 0, "stderr:\n{stderr}"); assert!(result.stderr.is_empty(), "unexpected stderr: {stderr}"); } fn javascript_execution_virtualizes_process_metadata_for_inline_v8_code() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), // Identity rides the typed guest_runtime; the env carries different // values to prove the `AGENTOS_VIRTUAL_PROCESS_*` knobs are inert. argv0: Some(String::new()), guest_runtime: agentos_execution::GuestRuntimeConfig { virtual_pid: Some(4242), virtual_ppid: Some(41), ..Default::default() }, vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs"), String::from("alpha")], env: BTreeMap::from([ ( String::from("AGENTOS_VIRTUAL_PROCESS_PID"), String::from("1"), ), ( String::from("AGENTOS_VIRTUAL_PROCESS_PPID"), String::from("2"), ), ]), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" if (process.argv[1] !== "/root/entry.mjs") throw new Error(`argv=${process.argv[1]}`); if (process.argv[2] !== "alpha") throw new Error(`arg2=${process.argv[2]}`); const processModule = require("process"); if (processModule.argv[1] !== "/root/entry.mjs") throw new Error(`module argv=${processModule.argv[1]}`); if (processModule.argv[2] !== "alpha") throw new Error(`module arg2=${processModule.argv[2]}`); if (process.argv0 !== "") throw new Error(`argv0=${process.argv0}`); if (process.cwd() !== "/root") throw new Error(`cwd=${process.cwd()}`); if (process.pid !== 4242) throw new Error(`pid=${process.pid}`); if (process.ppid !== 41) throw new Error(`ppid=${process.ppid}`); "#, )), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); let stdout = String::from_utf8_lossy(&result.stdout); let stderr = String::from_utf8_lossy(&result.stderr); assert_eq!(result.exit_code, 0, "stdout:\n{stdout}\nstderr:\n{stderr}"); assert!(result.stderr.is_empty(), "unexpected stderr: {stderr}"); } fn javascript_execution_process_kill_rejects_invalid_pid_in_guest_js() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" try { process.kill(Number.NaN, "SIGTERM"); console.log(JSON.stringify({ caught: false })); } catch (error) { console.log(JSON.stringify({ caught: true, name: error && error.name, message: error && error.message, })); } "#, )), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); let stdout = String::from_utf8_lossy(&result.stdout); let stderr = String::from_utf8_lossy(&result.stderr); assert_eq!(result.exit_code, 0, "stdout:\n{stdout}\nstderr:\n{stderr}"); assert!(result.stderr.is_empty(), "unexpected stderr: {stderr}"); let output: Value = serde_json::from_slice(&result.stdout).expect("parse stdout JSON"); assert_eq!(output.get("caught"), Some(&json!(true))); assert_eq!(output.get("name"), Some(&json!("TypeError"))); assert!( output .get("message") .and_then(Value::as_str) .is_some_and(|message| message.contains("\"pid\" argument")), "unexpected process.kill error output: {output}" ); } fn javascript_execution_preserves_binary_process_stdio_writes() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" process.stdout.write(Buffer.from([0x00, 0xbc, 0xff, 0x41])); process.stderr.write(Buffer.from([0xfe, 0x00, 0x42])); "#, )), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); assert_eq!(result.exit_code, 0); assert_eq!(result.stdout, vec![0x00, 0xbc, 0xff, 0x41]); assert_eq!(result.stderr, vec![0xfe, 0x00, 0x42]); } fn javascript_execution_intl_number_format_does_not_require_host_icu() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" const formatter = new Intl.NumberFormat("en", { maximumFractionDigits: 2, minimumFractionDigits: 2, }); console.log(formatter.format(1234.5)); "#, )), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); let stdout = String::from_utf8_lossy(&result.stdout); let stderr = String::from_utf8_lossy(&result.stderr); assert_eq!(result.exit_code, 0, "stdout:\n{stdout}\nstderr:\n{stderr}"); assert_eq!(stdout, "1,234.50\n"); } // Regression for #70: `Date#toLocaleDateString` with a non-default locale, // formatting options, and an explicit IANA time zone used to crash the embedded // V8 isolate with SIGTRAP. ICU's `DateTimePatternGeneratorCache::CreateGenerator` // hit a fatal abort under the near-heap-limit path; the OOM guard in // `crates/v8-runtime/src/isolate.rs` now converts that fatal abort into clean // termination, and ICU is bundled, so the exact repro runs and returns a string. fn javascript_execution_to_locale_date_string_does_not_crash_embedded_v8() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" const formatted = new Date(Date.UTC(2020, 0, 15)).toLocaleDateString("en-GB", { day: "2-digit", month: "2-digit", year: "numeric", timeZone: "Europe/Warsaw", }); console.log(JSON.stringify({ formatted })); "#, )), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); let stdout = String::from_utf8_lossy(&result.stdout); let stderr = String::from_utf8_lossy(&result.stderr); assert_eq!( result.exit_code, 0, "guest process must not crash (e.g. SIGTRAP); stdout:\n{stdout}\nstderr:\n{stderr}" ); let output: Value = serde_json::from_slice(&result.stdout).expect("parse guest stdout as JSON"); let formatted = output .get("formatted") .and_then(Value::as_str) .expect("formatted date string present"); assert!( !formatted.is_empty(), "toLocaleDateString returned an empty string: {output}" ); } #[allow(dead_code)] // quarantined: see the live-stdin/tty harness note above fn javascript_execution_stream_consumers_text_reads_live_stdin() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let mut execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::from([(String::from("AGENTOS_KEEP_STDIN_OPEN"), String::from("1"))]), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" import { text } from "node:stream/consumers"; const body = await text(process.stdin); console.log(JSON.stringify({ body })); "#, )), }) .expect("start JavaScript execution"); execution .write_stdin(b"alpha\nbeta\n") .expect("write JavaScript stdin"); execution.close_stdin().expect("close JavaScript stdin"); let result = execution.wait().expect("wait for JavaScript execution"); let stdout = String::from_utf8_lossy(&result.stdout); let stderr = String::from_utf8_lossy(&result.stderr); assert_eq!(result.exit_code, 0, "stdout:\n{stdout}\nstderr:\n{stderr}"); assert!(result.stderr.is_empty(), "unexpected stderr: {stderr}"); let output: Value = serde_json::from_slice(&result.stdout).expect("parse guest stdout as JSON"); assert_eq!(output, json!({ "body": "alpha\nbeta\n" })); } #[allow(dead_code)] // quarantined: see the live-stdin/tty harness note above fn javascript_execution_process_stdin_async_iterator_finishes_with_live_stdin() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let mut execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::from([(String::from("AGENTOS_KEEP_STDIN_OPEN"), String::from("1"))]), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" let body = ""; for await (const chunk of process.stdin) { body += chunk; } console.log(JSON.stringify({ body })); "#, )), }) .expect("start JavaScript execution"); execution .write_stdin(b"{\"request_id\":\"init1\"}\n") .expect("write JavaScript stdin"); execution.close_stdin().expect("close JavaScript stdin"); let result = execution.wait().expect("wait for JavaScript execution"); let stdout = String::from_utf8_lossy(&result.stdout); let stderr = String::from_utf8_lossy(&result.stderr); assert_eq!(result.exit_code, 0, "stdout:\n{stdout}\nstderr:\n{stderr}"); assert!(result.stderr.is_empty(), "unexpected stderr: {stderr}"); let output: Value = serde_json::from_slice(&result.stdout).expect("parse guest stdout as JSON"); assert_eq!(output, json!({ "body": "{\"request_id\":\"init1\"}\n" })); } #[allow(dead_code)] // quarantined: see the live-stdin/tty harness note above fn javascript_execution_process_exit_from_live_stdin_listener_exits_without_waiting_for_eof() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let mut execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::from([(String::from("AGENTOS_KEEP_STDIN_OPEN"), String::from("1"))]), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" process.stdin.setEncoding("utf8"); process.stdin.once("data", (chunk) => { process.stdout.write(`stdout:${chunk}`); process.stderr.write(`stderr:${chunk}`); process.exit(0); }); "#, )), }) .expect("start JavaScript execution"); execution .write_stdin(b"hello-live-stdin\n") .expect("write JavaScript stdin"); let mut stdout = Vec::new(); let mut stderr = Vec::new(); let exit_code = loop { match execution .poll_event_blocking(Duration::from_secs(5)) .expect("poll JavaScript execution event") { Some(JavascriptExecutionEvent::Stdout(chunk)) => stdout.extend(chunk), Some(JavascriptExecutionEvent::Stderr(chunk)) => stderr.extend(chunk), Some(JavascriptExecutionEvent::SignalState { .. }) => {} Some(JavascriptExecutionEvent::SyncRpcRequest(request)) => { panic!("unexpected pending sync RPC request: {}", request.id); } Some(JavascriptExecutionEvent::Exited(code)) => break code, None => panic!("JavaScript execution timed out while awaiting exit"), } }; let stdout = String::from_utf8_lossy(&stdout); let stderr = String::from_utf8_lossy(&stderr); assert_eq!(exit_code, 0, "stdout:\n{stdout}\nstderr:\n{stderr}"); assert!( stdout.contains("stdout:hello-live-stdin"), "stdout:\n{stdout}" ); assert!( stderr.contains("stderr:hello-live-stdin"), "stderr:\n{stderr}" ); } fn javascript_execution_process_exit_ignores_live_interval_handles() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" process.stdout.write("before exit\n"); setInterval(() => { process.stdout.write("interval tick\n"); }, 1000); process.exit(7); process.stdout.write("after exit\n"); "#, )), }) .expect("start JavaScript execution"); let mut stdout = Vec::new(); let exit_code = loop { match execution .poll_event_blocking(Duration::from_secs(5)) .expect("poll JavaScript execution event") { Some(JavascriptExecutionEvent::Stdout(chunk)) => stdout.extend(chunk), Some(JavascriptExecutionEvent::Stderr(chunk)) => { panic!("unexpected stderr: {}", String::from_utf8_lossy(&chunk)); } Some(JavascriptExecutionEvent::SignalState { .. }) => {} Some(JavascriptExecutionEvent::SyncRpcRequest(request)) => { panic!("unexpected pending sync RPC request: {}", request.id); } Some(JavascriptExecutionEvent::Exited(code)) => break code, None => panic!("JavaScript execution timed out while awaiting process.exit"), } }; let stdout = String::from_utf8_lossy(&stdout); assert_eq!(exit_code, 7, "stdout:\n{stdout}"); assert!(stdout.contains("before exit"), "stdout:\n{stdout}"); assert!(!stdout.contains("after exit"), "stdout:\n{stdout}"); } fn javascript_execution_process_exit_bypasses_promise_catch_handlers() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" Promise.resolve() .then(() => { process.stdout.write("before exit\n"); process.exit(7); }) .catch(() => { process.stdout.write("catch handler ran\n"); process.exit(2); }); "#, )), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); let stdout = String::from_utf8_lossy(&result.stdout); let stderr = String::from_utf8_lossy(&result.stderr); assert_eq!(result.exit_code, 7, "stdout:\n{stdout}\nstderr:\n{stderr}"); assert!(stdout.contains("before exit"), "stdout:\n{stdout}"); assert!(!stdout.contains("catch handler ran"), "stdout:\n{stdout}"); } #[allow(dead_code)] // quarantined: see the live-stdin/tty harness note above fn javascript_execution_live_stdin_replays_end_after_late_listener_registration() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let mut execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::from([(String::from("AGENTOS_KEEP_STDIN_OPEN"), String::from("1"))]), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" setTimeout(() => { process.stdin.setEncoding("utf8"); let body = ""; process.stdin.on("data", (chunk) => { body += chunk; }); process.stdin.on("end", () => { console.log(JSON.stringify({ body })); }); process.stdin.resume(); }, 50); "#, )), }) .expect("start JavaScript execution"); execution .write_stdin(b"hello-delayed\n") .expect("write JavaScript stdin"); execution.close_stdin().expect("close JavaScript stdin"); let result = execution.wait().expect("wait for JavaScript execution"); let stdout = String::from_utf8_lossy(&result.stdout); let stderr = String::from_utf8_lossy(&result.stderr); assert_eq!(result.exit_code, 0, "stdout:\n{stdout}\nstderr:\n{stderr}"); assert!(result.stderr.is_empty(), "unexpected stderr: {stderr}"); let output: Value = serde_json::from_slice(&result.stdout).expect("parse guest stdout as JSON"); assert_eq!(output, json!({ "body": "hello-delayed\n" })); } fn javascript_execution_file_url_to_path_accepts_guest_absolute_paths() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" import { fileURLToPath, pathToFileURL } from "node:url"; const guestPath = "/root/node_modules/@mariozechner/pi-coding-agent/dist/config.js"; if (fileURLToPath(guestPath) !== guestPath) { throw new Error(`plain path mismatch: ${fileURLToPath(guestPath)}`); } const href = "file:///root/node_modules/@mariozechner/pi-coding-agent/dist/config.js"; if (fileURLToPath(href) !== guestPath) { throw new Error(`file url mismatch: ${fileURLToPath(href)}`); } const viteInternal = pathToFileURL("/@id//node_modules/vitest/dist/index.js").href; if (viteInternal !== "file:///@id/node_modules/vitest/dist/index.js") { throw new Error(`path url mismatch: ${viteInternal}`); } "#, )), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); let stdout = String::from_utf8_lossy(&result.stdout); let stderr = String::from_utf8_lossy(&result.stderr); assert_eq!(result.exit_code, 0, "stdout:\n{stdout}\nstderr:\n{stderr}"); assert!(result.stderr.is_empty(), "unexpected stderr: {stderr}"); } fn javascript_execution_imports_node_events_without_hanging() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" import { EventEmitter, once } from "node:events"; const emitter = new EventEmitter(); const pending = once(emitter, "ready"); emitter.emit("ready", "ok"); const [value] = await pending; if (value !== "ok") { throw new Error(`unexpected once payload: ${value}`); } "#, )), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); assert_eq!(result.exit_code, 0); assert!( result.stderr.is_empty(), "unexpected stderr: {:?}", result.stderr ); } fn javascript_execution_imports_node_process_without_hanging() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" import process from "node:process"; if (!process || typeof process.cwd !== "function") { throw new Error("node:process did not export the guest process object"); } if (typeof process.pid !== "number" || process.pid <= 0) { throw new Error(`unexpected pid: ${process.pid}`); } "#, )), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); assert_eq!(result.exit_code, 0); assert!( result.stderr.is_empty(), "unexpected stderr: {:?}", result.stderr ); } fn javascript_execution_imports_node_fs_promises_without_hanging() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" import fs from "node:fs/promises"; if (typeof fs.access !== "function") { throw new Error("node:fs/promises did not expose access()"); } if (typeof fs.readFile !== "function") { throw new Error("node:fs/promises did not expose readFile()"); } "#, )), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); assert_eq!(result.exit_code, 0); assert!( result.stderr.is_empty(), "unexpected stderr: {:?}", result.stderr ); } fn javascript_execution_imports_node_perf_hooks_without_hanging() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" import { performance } from "node:perf_hooks"; if (typeof performance?.now !== "function") { throw new Error("node:perf_hooks did not expose performance.now()"); } const replacementPerformance = { now() { const [seconds, nanoseconds] = process.hrtime(); return seconds * 1000 + nanoseconds / 1e6; }, }; globalThis.performance = replacementPerformance; const elapsed = process.hrtime(process.hrtime()); if (!Array.isArray(elapsed) || elapsed.length !== 2) { throw new Error("process.hrtime returned an invalid delta"); } if (typeof process.hrtime.bigint() !== "bigint") { throw new Error("process.hrtime.bigint did not return a bigint"); } "#, )), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); assert_eq!(result.exit_code, 0); assert!( result.stderr.is_empty(), "unexpected stderr: {:?}", result.stderr ); } fn javascript_execution_high_resolution_time_opt_in_enables_sub_ms_hrtime() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js-high-res-on"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: GuestRuntimeConfig { high_resolution_time: true, ..Default::default() }, vm_id: String::from("vm-js-high-res-on"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" if (typeof __secureExecHrNowUs !== "function") { throw new Error("high-resolution host clock was not installed"); } let sawSubMs = false; for (let attempt = 0; attempt < 80 && !sawSubMs; attempt++) { const start = process.hrtime.bigint(); const until = __secureExecHrNowUs() + 200; while (__secureExecHrNowUs() < until) {} const delta = process.hrtime.bigint() - start; if (delta > 0n && delta < 1000000n) { sawSubMs = true; } } if (!sawSubMs) { throw new Error("process.hrtime.bigint did not observe a sub-ms delta"); } "#, )), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); let stdout = String::from_utf8_lossy(&result.stdout); let stderr = String::from_utf8_lossy(&result.stderr); assert_eq!(result.exit_code, 0, "stdout:\n{stdout}\nstderr:\n{stderr}"); } fn javascript_execution_high_resolution_time_default_off_keeps_coarse_clock() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js-high-res-off"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js-high-res-off"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" if (typeof __secureExecHrNowUs !== "undefined") { throw new Error("high-resolution host clock exists without opt-in"); } for (let attempt = 0; attempt < 20; attempt++) { const now = process.hrtime.bigint(); if (now % 1000000n !== 0n) { throw new Error("process.hrtime.bigint was not millisecond aligned: " + now); } } "#, )), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); let stdout = String::from_utf8_lossy(&result.stdout); let stderr = String::from_utf8_lossy(&result.stderr); assert_eq!(result.exit_code, 0, "stdout:\n{stdout}\nstderr:\n{stderr}"); } fn javascript_execution_exposes_compatibility_shims_and_denies_escape_builtins() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" import { createRequire } from "node:module"; const require = createRequire(import.meta.url); const vm = require("node:vm"); if (typeof vm.runInThisContext !== "function") { throw new Error("node:vm compatibility shim missing runInThisContext"); } const v8 = require("node:v8"); if (typeof v8.cachedDataVersionTag !== "function") { throw new Error("node:v8 compatibility shim missing cachedDataVersionTag"); } const heapStats = v8.getHeapStatistics?.(); if (!heapStats || typeof heapStats.heap_size_limit !== "number" || heapStats.heap_size_limit <= 0) { throw new Error("node:v8 compatibility shim missing positive heap_size_limit"); } const workerThreads = require("node:worker_threads"); if (workerThreads.isMainThread !== true) { throw new Error("node:worker_threads compatibility shim missing isMainThread"); } let workerDenied = false; try { new workerThreads.Worker(new URL("data:text/javascript,0")); } catch (error) { workerDenied = error?.code === "ERR_NOT_IMPLEMENTED"; } if (!workerDenied) { throw new Error("node:worker_threads Worker should stay unavailable"); } const inspector = require("node:inspector"); if (typeof inspector.Session !== "function" || inspector.url() !== undefined) { throw new Error("node:inspector compatibility shim is not inert"); } const inspectorSession = new inspector.Session(); inspectorSession.connect(); inspectorSession.disconnect(); for (const builtin of ["cluster"]) { let denied = false; try { require(`node:${builtin}`); } catch (error) { denied = error?.code === "ERR_ACCESS_DENIED" && String(error?.message ?? "").includes(`node:${builtin}`); } if (!denied) { throw new Error(`node:${builtin} was not denied`); } } "#, )), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); assert_eq!( result.exit_code, 0, "stderr: {}", String::from_utf8_lossy(&result.stderr) ); assert!( result.stderr.is_empty(), "unexpected stderr: {:?}", result.stderr ); } fn javascript_execution_v8_util_format_with_options_matches_node() { let temp = tempdir().expect("create temp dir"); write_fixture( &temp.path().join("entry.mjs"), r#" import { createRequire } from "node:module"; import { formatWithOptions as namedFormatWithOptions, parseEnv as namedParseEnv, stripVTControlCharacters as namedStripVTControlCharacters, } from "node:util"; const require = createRequire(import.meta.url); const util = require("node:util"); const circular = {}; circular.self = circular; let stripTypeError; try { util.stripVTControlCharacters(42); } catch (error) { stripTypeError = { name: error.name, code: error.code }; } console.log(JSON.stringify({ type: typeof util.formatWithOptions, namedType: typeof namedFormatWithOptions, basic: util.formatWithOptions({}, "hello %s %d %j %%", "world", 4, { ok: true }), extra: util.formatWithOptions({ colors: false }, "value", { alpha: 1 }, "tail"), object: util.formatWithOptions({ colors: false, depth: 1 }, "%O", { nested: { value: 1 } }), circular: util.formatWithOptions({}, "%j", circular), stripType: typeof util.stripVTControlCharacters, namedStripType: typeof namedStripVTControlCharacters, stripped: util.stripVTControlCharacters("plain \u001b[31mred\u001b[39m \u001b]8;;https://example.com\u0007link\u001b]8;;\u0007"), stripTypeError, parseEnvType: typeof util.parseEnv, namedParseEnvType: typeof namedParseEnv, parsedEnv: util.parseEnv('BASIC=basic\nSPACED = value with spaces\nEMPTY=\nCOMMENT=value # comment\nDOUBLE="line\\nvalue"\nSINGLE=\'raw\\nvalue\'\nexport EXPORTED=ready\n'), })); "#, ); let host = run_host_node_json(temp.path(), &temp.path().join("entry.mjs")); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: None, }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); let stdout = String::from_utf8_lossy(&result.stdout); let stderr = String::from_utf8_lossy(&result.stderr); assert_eq!(result.exit_code, 0, "stdout:\n{stdout}\nstderr:\n{stderr}"); assert!(stderr.is_empty(), "unexpected stderr: {stderr}"); let guest: Value = serde_json::from_slice(&result.stdout).expect("parse stdout JSON"); assert_eq!(guest, host); } fn javascript_execution_provides_async_hooks_and_diagnostics_channel_stubs() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" import { createRequire } from "node:module"; import { Channel, tracingChannel as importedTracingChannel } from "node:diagnostics_channel"; const require = createRequire(import.meta.url); const asyncHooks = require("node:async_hooks"); const diagnosticsChannel = require("node:diagnostics_channel"); const hook = asyncHooks.createHook({}); if (hook.enable() !== hook || hook.disable() !== hook) { throw new Error("node:async_hooks createHook() did not return a no-op hook"); } if (asyncHooks.executionAsyncId() !== 0 || asyncHooks.triggerAsyncId() !== 0) { throw new Error("node:async_hooks ids should default to 0"); } const storage = new asyncHooks.AsyncLocalStorage(); const result = storage.run("token", () => storage.getStore()); if (result !== "token") { throw new Error(`node:async_hooks AsyncLocalStorage lost store: ${String(result)}`); } const channel = diagnosticsChannel.channel("undici:request:create"); if (channel.name !== "undici:request:create") { throw new Error(`unexpected channel name: ${String(channel.name)}`); } if (channel.hasSubscribers !== false) { throw new Error("diagnostics channel should report no subscribers"); } if (diagnosticsChannel.hasSubscribers("undici:request:create") !== false) { throw new Error("diagnostics_channel.hasSubscribers should be false"); } if (typeof diagnosticsChannel.tracingChannel !== "function") { throw new Error("diagnostics_channel.tracingChannel is missing"); } if (typeof importedTracingChannel !== "function") { throw new Error("diagnostics_channel ESM tracingChannel export is missing"); } if (typeof Channel !== "function") { throw new Error("diagnostics_channel ESM Channel export is missing"); } const constructedChannel = new Channel("constructed"); if (constructedChannel.name !== "constructed" || constructedChannel.hasSubscribers !== false) { throw new Error("diagnostics_channel Channel constructor returned unexpected state"); } const tracing = diagnosticsChannel.tracingChannel("agent.test"); if (tracing.hasSubscribers !== false || tracing.start.hasSubscribers !== false) { throw new Error("diagnostics tracing channel should start without subscribers"); } if (tracing.start.name !== "tracing:agent.test:start") { throw new Error(`unexpected tracing start channel name: ${String(tracing.start.name)}`); } const runStoresResult = tracing.start.runStores({ token: 1 }, (left, right) => `${left}:${right}`, undefined, "ok", 42); if (runStoresResult !== "ok:42") { throw new Error(`diagnostics tracing channel runStores returned ${String(runStoresResult)}`); } let published = null; function onPublish(message, name) { published = { message, name }; } tracing.start.subscribe(onPublish); if (tracing.hasSubscribers !== true || tracing.start.hasSubscribers !== true) { throw new Error("diagnostics tracing channel did not track subscribers"); } tracing.start.publish({ value: 7 }); if (published?.name !== "tracing:agent.test:start" || published?.message?.value !== 7) { throw new Error("diagnostics tracing channel did not publish to subscribers"); } if (tracing.start.unsubscribe(onPublish) !== true || tracing.hasSubscribers !== false) { throw new Error("diagnostics tracing channel did not unsubscribe"); } "#, )), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); assert_eq!(result.exit_code, 0); assert!( result.stderr.is_empty(), "unexpected stderr: {:?}", result.stderr ); } fn javascript_execution_supports_require_resolve_for_guest_code() { let temp = tempdir().expect("create temp dir"); write_fixture( &temp.path().join("local-file.js"), "module.exports = 'local';\n", ); write_fixture( &temp.path().join("nested/check.cjs"), r#" const localResolved = require.resolve("../local-file.js"); if (localResolved !== "/root/local-file.js") { throw new Error(`unexpected local resolution: ${String(localResolved)}`); } const packageResolved = require.resolve("some-package"); if (packageResolved !== "/root/node_modules/some-package/index.js") { throw new Error(`unexpected package resolution: ${String(packageResolved)}`); } const searchPaths = require.resolve.paths("some-package"); const expectedPaths = [ "/root/nested/node_modules", "/root/node_modules", "/node_modules", ]; if (JSON.stringify(searchPaths) !== JSON.stringify(expectedPaths)) { throw new Error(`unexpected search paths: ${JSON.stringify(searchPaths)}`); } "#, ); write_fixture( &temp.path().join("node_modules/some-package/package.json"), r#"{"main":"./index.js"}"#, ); write_fixture( &temp.path().join("node_modules/some-package/index.js"), "module.exports = 'pkg';\n", ); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" import { createRequire } from "node:module"; const require = createRequire(import.meta.url); if (require.resolve("fs") !== "node:fs") { throw new Error(`builtin resolution failed: ${String(require.resolve("fs"))}`); } if (require.resolve("./local-file.js") !== "/root/local-file.js") { throw new Error(`local resolution failed: ${String(require.resolve("./local-file.js"))}`); } if (require.resolve("some-package") !== "/root/node_modules/some-package/index.js") { throw new Error(`package resolution failed: ${String(require.resolve("some-package"))}`); } const builtinPaths = require.resolve.paths("fs"); if (builtinPaths !== null) { throw new Error(`builtin paths should be null, got ${JSON.stringify(builtinPaths)}`); } const packagePaths = require.resolve.paths("some-package"); const expectedPackagePaths = ["/root/node_modules", "/node_modules"]; if (JSON.stringify(packagePaths) !== JSON.stringify(expectedPackagePaths)) { throw new Error(`unexpected top-level search paths: ${JSON.stringify(packagePaths)}`); } let missingCode = null; try { require.resolve("nonexistent"); } catch (error) { missingCode = error?.code ?? null; } if (missingCode !== "MODULE_NOT_FOUND") { throw new Error(`unexpected missing-module code: ${String(missingCode)}`); } require("./nested/check.cjs"); "#, )), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); assert_eq!(result.exit_code, 0); assert!( result.stderr.is_empty(), "unexpected stderr: {:?}", result.stderr ); } fn javascript_execution_rejects_native_node_addons() { let temp = tempdir().expect("create temp dir"); write_fixture(&temp.path().join("addon.node"), "not a native addon\n"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.js")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" let rejected = false; try { require("./addon.node"); } catch (error) { rejected = String(error?.message ?? "").includes(".node extensions are not supported") || String(error?.message ?? "").includes("native addon loading"); } if (!rejected) { throw new Error("native .node addon should be rejected"); } "#, )), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); assert_eq!(result.exit_code, 0); assert!( result.stderr.is_empty(), "unexpected stderr: {:?}", result.stderr ); } fn javascript_execution_surfaces_sync_rpc_requests_from_v8_modules() { let temp = tempdir().expect("create temp dir"); write_fixture( &temp.path().join("entry.mjs"), r#" import fs from "node:fs"; fs.statSync("/workspace/note.txt"); "#, ); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let mut execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: None, }) .expect("start JavaScript execution"); let request = expect_next_sync_rpc(&mut execution, "poll execution event"); assert_eq!(request.method, "fs.statSync"); assert_eq!(request.args, vec![json!("/workspace/note.txt")]); execution .respond_sync_rpc_success( request.id, json!({ "mode": 0o100644, "size": 11, "isDirectory": false, "isSymbolicLink": false, }), ) .expect("respond to fs.statSync"); let result = execution.wait().expect("wait for JavaScript execution"); assert_eq!(result.exit_code, 0); } fn javascript_execution_v8_dgram_bridge_matches_sidecar_rpc_shapes() { let temp = tempdir().expect("create temp dir"); write_fixture( &temp.path().join("entry.mjs"), r#" import dgram from "node:dgram"; const summary = await new Promise((resolve, reject) => { const socket = dgram.createSocket("udp4"); socket.on("error", reject); socket.on("message", (message, rinfo) => { const address = socket.address(); socket.close(() => { resolve({ address, message: message.toString("utf8"), rinfo, }); }); }); socket.bind(0, "127.0.0.1", () => { socket.send("ping", 7, "127.0.0.1"); }); }); if (summary.message !== "pong") { throw new Error(`unexpected udp message: ${summary.message}`); } if (summary.address.address !== "127.0.0.1" || summary.address.port !== 45454) { throw new Error(`unexpected socket address: ${JSON.stringify(summary.address)}`); } if (summary.rinfo.address !== "127.0.0.1" || summary.rinfo.port !== 7) { throw new Error(`unexpected remote info: ${JSON.stringify(summary.rinfo)}`); } "#, ); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let mut execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::from([( String::from("AGENTOS_ALLOWED_NODE_BUILTINS"), String::from("[\"dgram\"]"), )]), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: None, }) .expect("start JavaScript execution"); let request = expect_next_sync_rpc(&mut execution, "poll dgram.createSocket request"); assert_eq!(request.method, "dgram.createSocket"); assert_eq!(request.args, vec![json!({ "type": "udp4" })]); execution .respond_sync_rpc_success(request.id, json!({ "socketId": "udp-1", "type": "udp4" })) .expect("respond to dgram.createSocket"); let request = expect_next_sync_rpc(&mut execution, "poll dgram.bind request"); assert_eq!(request.method, "dgram.bind"); assert_eq!( request.args, vec![json!("udp-1"), json!({ "address": "127.0.0.1", "port": 0 })] ); execution .respond_sync_rpc_success( request.id, json!({ "localAddress": "127.0.0.1", "localPort": 45454, "family": "IPv4", }), ) .expect("respond to dgram.bind"); let request = expect_next_sync_rpc(&mut execution, "poll dgram.poll request"); assert_eq!(request.method, "dgram.poll"); assert_eq!(request.args, vec![json!("udp-1"), json!(0)]); execution .respond_sync_rpc_success(request.id, json!(null)) .expect("respond to initial dgram.poll"); let request = expect_next_sync_rpc(&mut execution, "poll dgram.send request"); assert_eq!(request.method, "dgram.send"); assert_eq!( request.args, vec![ json!("udp-1"), json!({ "__agentOSType": "bytes", "base64": "cGluZw==", }), json!({ "address": "127.0.0.1", "port": 7, }), ] ); execution .respond_sync_rpc_success( request.id, json!({ "bytes": 4, "localAddress": "127.0.0.1", "localPort": 45454, "family": "IPv4", }), ) .expect("respond to dgram.send"); assert!( execution .poll_event_blocking(Duration::from_millis(20)) .expect("probe dgram event queue") .is_none(), "dgram must not poll again until sidecar readiness arrives" ); execution .send_stream_event( "net_socket", json!({ "event": "dgram", "socketId": "udp-1", }), ) .expect("wake dgram receive from sidecar readiness"); let request = expect_next_sync_rpc(&mut execution, "poll message dgram.poll request"); assert_eq!(request.method, "dgram.poll"); assert_eq!(request.args, vec![json!("udp-1"), json!(0)]); execution .respond_sync_rpc_success( request.id, json!({ "type": "message", "data": { "__agentOSType": "bytes", "base64": "cG9uZw==", }, "remoteAddress": "127.0.0.1", "remotePort": 7, "remoteFamily": "IPv4", }), ) .expect("respond to message dgram.poll"); let request = expect_next_sync_rpc(&mut execution, "poll dgram.address request"); assert_eq!(request.method, "dgram.address"); assert_eq!(request.args, vec![json!("udp-1")]); execution .respond_sync_rpc_success( request.id, json!("{\"address\":\"127.0.0.1\",\"port\":45454,\"family\":\"IPv4\"}"), ) .expect("respond to dgram.address"); let request = expect_next_sync_rpc(&mut execution, "poll dgram.close request"); assert_eq!(request.method, "dgram.close"); assert_eq!(request.args, vec![json!("udp-1")]); execution .respond_sync_rpc_success(request.id, json!(null)) .expect("respond to dgram.close"); let result = execution.wait().expect("wait for JavaScript execution"); let stderr = String::from_utf8(result.stderr).expect("stderr utf8"); assert_eq!(result.exit_code, 0, "unexpected stderr: {stderr}"); assert!(stderr.is_empty(), "unexpected stderr: {stderr}"); } fn javascript_execution_strips_hashbang_from_module_entrypoints() { let temp = tempdir().expect("create temp dir"); write_fixture(&temp.path().join("package.json"), r#"{"type":"module"}"#); write_fixture( &temp.path().join("index.js"), "#!/usr/bin/env node\nimport fs from \"node:fs\";\nfs.statSync(\"/workspace/hashbang.txt\");\n", ); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let mut execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./index.js")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: None, }) .expect("start JavaScript execution"); let request = expect_next_sync_rpc(&mut execution, "poll execution event"); assert_eq!(request.method, "fs.statSync"); assert_eq!(request.args, vec![json!("/workspace/hashbang.txt")]); execution .respond_sync_rpc_success( request.id, json!({ "mode": 0o100644, "size": 9, "isDirectory": false, "isSymbolicLink": false, }), ) .expect("respond to fs.statSync"); let result = execution.wait().expect("wait for JavaScript execution"); let stderr = String::from_utf8(result.stderr).expect("stderr utf8"); assert_eq!(result.exit_code, 0, "unexpected stderr: {stderr}"); assert!(stderr.is_empty(), "unexpected stderr: {stderr}"); } fn javascript_execution_resolves_pnpm_store_dependencies_from_symlinked_entrypoints() { let temp = tempdir().expect("create temp dir"); let node_modules = temp.path().join("node_modules"); let store_root = node_modules.join(".pnpm/pkg@1.0.0/node_modules"); let pkg_dir = store_root.join("pkg"); let dep_dir = store_root.join("@scope/dep"); fs::create_dir_all(pkg_dir.join("dist")).expect("create package dist"); fs::create_dir_all(&dep_dir).expect("create dependency dir"); fs::create_dir_all(node_modules.join("@scope")).expect("create scope dir"); write_fixture(&pkg_dir.join("package.json"), r#"{"type":"module"}"#); write_fixture( &pkg_dir.join("dist/index.js"), "import dep from \"@scope/dep\";\ndep();\n", ); write_fixture( &dep_dir.join("package.json"), r#"{"type":"module","exports":"./index.js"}"#, ); write_fixture( &dep_dir.join("index.js"), "import fs from \"node:fs\";\nexport default function dep() { fs.statSync(\"/workspace/pnpm.txt\"); }\n", ); symlink(".pnpm/pkg@1.0.0/node_modules/pkg", node_modules.join("pkg")) .expect("symlink package into node_modules"); let guest_mappings = serde_json::to_string(&vec![json!({ "guestPath": "/root/node_modules", "hostPath": node_modules.display().to_string(), })]) .expect("serialize guest mappings"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let mut execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("/root/node_modules/pkg/dist/index.js")], env: BTreeMap::from([(String::from("AGENTOS_GUEST_PATH_MAPPINGS"), guest_mappings)]), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: None, }) .expect("start JavaScript execution"); let request = expect_next_sync_rpc(&mut execution, "poll execution event"); assert_eq!(request.method, "fs.statSync"); assert_eq!(request.args, vec![json!("/workspace/pnpm.txt")]); execution .respond_sync_rpc_success( request.id, json!({ "mode": 0o100644, "size": 8, "isDirectory": false, "isSymbolicLink": false, }), ) .expect("respond to fs.statSync"); let result = execution.wait().expect("wait for JavaScript execution"); let stderr = String::from_utf8(result.stderr).expect("stderr utf8"); assert_eq!(result.exit_code, 0, "unexpected stderr: {stderr}"); assert!(stderr.is_empty(), "unexpected stderr: {stderr}"); } fn javascript_execution_resolves_dependencies_from_package_specific_symlink_mounts() { let temp = tempdir().expect("create temp dir"); let mounts_root = temp.path().join("mounts"); let node_modules_root = temp.path().join("node_modules"); let store_root = node_modules_root.join(".pnpm/pkg@1.0.0/node_modules"); let pkg_dir = store_root.join("pkg"); let dep_dir = store_root.join("@scope/dep"); let mounted_pkg = mounts_root.join("pkg"); fs::create_dir_all(pkg_dir.join("dist")).expect("create package dist"); fs::create_dir_all(&dep_dir).expect("create dependency dir"); fs::create_dir_all(&mounts_root).expect("create mounts root"); write_fixture(&pkg_dir.join("package.json"), r#"{"type":"module"}"#); write_fixture( &pkg_dir.join("dist/index.js"), "import dep from \"@scope/dep\";\ndep();\n", ); write_fixture( &dep_dir.join("package.json"), r#"{"type":"module","exports":"./index.js"}"#, ); write_fixture( &dep_dir.join("index.js"), "import fs from \"node:fs\";\nexport default function dep() { fs.statSync(\"/workspace/pkg-mount.txt\"); }\n", ); symlink(&pkg_dir, &mounted_pkg).expect("symlink mounted package to pnpm store"); let guest_mappings = serde_json::to_string(&vec![json!({ "guestPath": "/root/node_modules/pkg", "hostPath": mounted_pkg.display().to_string(), })]) .expect("serialize guest mappings"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let mut execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("/root/node_modules/pkg/dist/index.js")], env: BTreeMap::from([(String::from("AGENTOS_GUEST_PATH_MAPPINGS"), guest_mappings)]), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: None, }) .expect("start JavaScript execution"); let request = expect_next_sync_rpc(&mut execution, "poll execution event"); assert_eq!(request.method, "fs.statSync"); assert_eq!(request.args, vec![json!("/workspace/pkg-mount.txt")]); execution .respond_sync_rpc_success( request.id, json!({ "mode": 0o100644, "size": 13, "isDirectory": false, "isSymbolicLink": false, }), ) .expect("respond to fs.statSync"); let result = execution.wait().expect("wait for JavaScript execution"); let stdout = String::from_utf8(result.stdout.clone()).expect("stdout utf8"); let stderr = String::from_utf8(result.stderr.clone()).expect("stderr utf8"); assert_eq!(result.exit_code, 0, "stdout:\n{stdout}\nstderr:\n{stderr}"); let stderr = String::from_utf8(result.stderr).expect("stderr utf8"); assert!(stderr.is_empty(), "unexpected stderr: {stderr}"); } fn javascript_execution_v8_timer_callbacks_fire_and_clear_correctly() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id.clone(), argv: vec![String::from("./entry.js")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" (async () => { const clearedTimeout = setTimeout(() => { throw new Error("cleared timeout fired"); }, 10); clearTimeout(clearedTimeout); await new Promise((resolve) => setTimeout(resolve, 25)); let intervalTicks = 0; await new Promise((resolve, reject) => { const interval = setInterval(() => { intervalTicks += 1; if (intervalTicks === 2) { clearInterval(interval); resolve(); } else if (intervalTicks > 2) { reject(new Error(`interval fired too many times: ${intervalTicks}`)); } }, 10); setTimeout(() => reject(new Error(`interval timeout: ${intervalTicks}`)), 250); }); if (intervalTicks !== 2) { throw new Error(`interval tick count mismatch: ${intervalTicks}`); } let immediateFired = false; await new Promise((resolve) => { setImmediate(() => { immediateFired = true; resolve(); }); }); if (!immediateFired) { throw new Error("setImmediate callback did not fire"); } const order = []; setImmediate(() => order.push("immediate")); queueMicrotask(() => order.push("microtask")); await new Promise((resolve) => setImmediate(resolve)); if (order.join(",") !== "microtask,immediate") { throw new Error(`unexpected immediate order: ${order.join(",")}`); } for (let i = 0; i < 100; i += 1) { await new Promise((resolve) => setImmediate(resolve)); } let clearedImmediateFired = false; const clearedImmediate = setImmediate(() => { clearedImmediateFired = true; }); clearImmediate(clearedImmediate); await new Promise((resolve) => setImmediate(resolve)); if (clearedImmediateFired) { throw new Error("cleared immediate fired"); } const { setImmediate: promiseImmediate } = await import("node:timers/promises"); const promiseImmediateValue = await promiseImmediate("promise-value"); if (promiseImmediateValue !== "promise-value") { throw new Error(`timers/promises setImmediate mismatch: ${promiseImmediateValue}`); } const t0 = Date.now(); let chainCount = 0; const immediateChainMs = await new Promise((resolve) => { function tick() { if (++chainCount < 1000) { setImmediate(tick); } else { resolve(Date.now() - t0); } } setImmediate(tick); }); // The upper-bound check on the chain latency lives host-side in Rust so it can // be gated to the nightly timing lane (see run_timing_sensitive_tests); the // guest only reports the measurement. console.log(`setImmediate-chain-ms=${immediateChainMs}`); })().catch((error) => { process.exitCode = 1; throw error; }); "#, )), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); let stdout = String::from_utf8(result.stdout.clone()).expect("stdout utf8"); let stderr = String::from_utf8(result.stderr.clone()).expect("stderr utf8"); assert_eq!(result.exit_code, 0, "stdout:\n{stdout}\nstderr:\n{stderr}"); assert!(stderr.is_empty(), "unexpected stderr: {stderr}"); let chain_ms_line = stdout .lines() .find(|line| line.starts_with("setImmediate-chain-ms=")) .expect("setImmediate timing line"); let chain_ms: u64 = chain_ms_line .trim_start_matches("setImmediate-chain-ms=") .parse() .expect("parse setImmediate timing"); println!("setImmediate 1000-chain elapsed ms: {chain_ms}"); if run_timing_sensitive_tests() { assert!( chain_ms < 500, "setImmediate 1000-chain elapsed too high: {chain_ms}ms" ); } let only_immediate_execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id.clone(), argv: vec![String::from("./entry.js")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" setImmediate(() => { console.log("only-immediate-fired"); }); "#, )), }) .expect("start only-immediate JavaScript execution"); let only_immediate_result = only_immediate_execution .wait() .expect("wait for only-immediate JavaScript execution"); let only_immediate_stdout = String::from_utf8(only_immediate_result.stdout.clone()).expect("stdout utf8"); let only_immediate_stderr = String::from_utf8(only_immediate_result.stderr.clone()).expect("stderr utf8"); assert_eq!( only_immediate_result.exit_code, 0, "stdout:\n{only_immediate_stdout}\nstderr:\n{only_immediate_stderr}" ); assert!( only_immediate_stdout.contains("only-immediate-fired"), "only pending setImmediate did not fire; stdout:\n{only_immediate_stdout}" ); assert!( only_immediate_stderr.is_empty(), "unexpected stderr: {only_immediate_stderr}" ); } fn javascript_execution_v8_readline_polyfill_emits_lines() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" import { EventEmitter } from "node:events"; import { createInterface } from "node:readline"; const input = new EventEmitter(); const seen = []; const rl = createInterface({ input }); rl.on("line", (line) => seen.push(line)); input.emit("data", "alpha\nbeta\r\ngamma"); input.emit("end"); if (seen.length !== 3) { throw new Error(`expected 3 lines, got ${JSON.stringify(seen)}`); } if (seen[0] !== "alpha" || seen[1] !== "beta" || seen[2] !== "gamma") { throw new Error(`unexpected lines: ${JSON.stringify(seen)}`); } "#, )), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); assert_eq!(result.exit_code, 0); let stderr = String::from_utf8(result.stderr).expect("stderr utf8"); assert!(stderr.is_empty(), "unexpected stderr: {stderr}"); } fn javascript_execution_v8_builtin_wrappers_expose_common_named_exports() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" import { spawn, spawnSync } from "node:child_process"; import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, realpathSync, statSync, writeFileSync } from "node:fs"; import { homedir, platform } from "node:os"; import { basename, dirname, isAbsolute, join, resolve } from "node:path"; if (typeof spawn !== "function" || typeof spawnSync !== "function") throw new Error("child_process exports missing"); if (typeof closeSync !== "function" || typeof existsSync !== "function" || typeof mkdirSync !== "function") throw new Error("fs exports missing"); if (typeof openSync !== "function" || typeof readFileSync !== "function" || typeof readSync !== "function") throw new Error("fs exports missing"); if (typeof readdirSync !== "function" || typeof realpathSync !== "function" || typeof statSync !== "function" || typeof writeFileSync !== "function") throw new Error("fs exports missing"); if (typeof homedir !== "function" || typeof platform !== "function") throw new Error("os exports missing"); if (typeof basename !== "function" || typeof dirname !== "function" || typeof isAbsolute !== "function" || typeof join !== "function" || typeof resolve !== "function") throw new Error("path exports missing"); "#, )), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); assert_eq!(result.exit_code, 0); let stderr = String::from_utf8(result.stderr).expect("stderr utf8"); assert!(stderr.is_empty(), "unexpected stderr: {stderr}"); } fn javascript_execution_v8_child_process_conformance_matches_host_node() { let temp = tempdir().expect("create temp dir"); write_fixture( &temp.path().join("entry.mjs"), r#" import childProcess from "node:child_process"; import fs from "node:fs"; fs.writeFileSync("async-out.txt", Buffer.from("async:beta-async\n", "utf8")); const syncPiped = childProcess.spawnSync("/bin/cat", [], { input: Buffer.from("alpha-sync"), }); const syncArgv0 = childProcess.spawnSync("/bin/sh", ["-c", "printf '%s' \"$0\""], { argv0: "custom-agentos-argv0", encoding: "utf8", }); const syncError = childProcess.spawnSync("/bin/cat", ["definitely-missing-agentos-file"]); const syncTimeout = childProcess.spawnSync("/bin/sh", ["-c", "sleep 2"], { timeout: 50, killSignal: "SIGTERM", encoding: "utf8", }); const syncCaughtTimeout = childProcess.spawnSync( "/bin/sh", ["-c", "trap 'exit 0' TERM; while :; do :; done"], { timeout: 50, killSignal: "SIGTERM", encoding: "utf8" }, ); const stdinDestroyChild = childProcess.spawn("/bin/cat", [], { stdio: ["pipe", "pipe", "pipe"], }); if (typeof stdinDestroyChild.stdin.destroy !== "function") { throw new Error("child stdin did not expose destroy()"); } if ( typeof stdinDestroyChild.stdout?.destroy !== "function" || typeof stdinDestroyChild.stderr?.destroy !== "function" ) { throw new Error("child output streams did not expose destroy()"); } const stdinDestroyStatus = await new Promise((resolve, reject) => { stdinDestroyChild.on("error", reject); stdinDestroyChild.on("close", (code) => resolve(code)); stdinDestroyChild.stdin.destroy(); if (stdinDestroyChild.stdin.destroyed !== true) { reject(new Error("child stdin destroy() did not mark the stream destroyed")); } }); const stdinCallbackResult = await new Promise((resolve, reject) => { const child = childProcess.spawn("/bin/cat", [], { stdio: ["pipe", "pipe", "pipe"], }); const timer = setTimeout(() => { reject(new Error("spawn(/bin/cat) stdin callback probe did not close within 2s")); }, 2000); const stdout = []; const stderr = []; let writeCallbackError = null; let writeCallbackCalled = false; let endCallbackCalled = false; child.stdout.on("data", (chunk) => { stdout.push(Buffer.from(chunk)); }); child.stderr.on("data", (chunk) => { stderr.push(Buffer.from(chunk)); }); child.on("error", reject); child.on("close", (code, signal) => { clearTimeout(timer); resolve({ code, signal, writeCallbackCalled, writeCallbackError, endCallbackCalled, stdoutBase64: Buffer.concat(stdout).toString("base64"), stderrBase64: Buffer.concat(stderr).toString("base64"), }); }); child.stdin.write(Buffer.from("callback:gamma"), (error) => { writeCallbackCalled = true; writeCallbackError = error ? String(error?.message ?? error) : null; child.stdin.end(() => { endCallbackCalled = true; }); }); }); const asyncResult = await new Promise((resolve, reject) => { const child = childProcess.spawn("/bin/cat", ["async-out.txt"], { stdio: ["ignore", "pipe", "pipe"], }); const timer = setTimeout(() => { reject(new Error("spawn(/bin/cat async-out.txt) did not close within 2s")); }, 2000); const stdout = []; const stderr = []; child.stdout.on("data", (chunk) => { stdout.push(Buffer.from(chunk)); }); child.stderr.on("data", (chunk) => { stderr.push(Buffer.from(chunk)); }); child.on("error", reject); child.on("close", (code, signal) => { clearTimeout(timer); resolve({ code, signal, stdoutBase64: Buffer.concat(stdout).toString("base64"), stderrBase64: Buffer.concat(stderr).toString("base64"), }); }); }); const asyncErrorResult = await new Promise((resolve, reject) => { const child = childProcess.spawn("/bin/cat", ["definitely-missing-agentos-file"], { stdio: ["ignore", "pipe", "pipe"], }); const timer = setTimeout(() => { reject(new Error("spawn(/bin/cat missing-file) did not close within 2s")); }, 2000); const stdout = []; const stderr = []; child.stdout.on("data", (chunk) => { stdout.push(Buffer.from(chunk)); }); child.stderr.on("data", (chunk) => { stderr.push(Buffer.from(chunk)); }); child.on("error", reject); child.on("close", (code, signal) => { clearTimeout(timer); resolve({ code, signal, stdoutBase64: Buffer.concat(stdout).toString("base64"), stderrBase64: Buffer.concat(stderr).toString("base64"), }); }); }); console.log(JSON.stringify({ syncPipedStatus: syncPiped.status, syncPipedStdoutBase64: Buffer.from(syncPiped.stdout ?? []).toString("base64"), syncPipedStderrBase64: Buffer.from(syncPiped.stderr ?? []).toString("base64"), syncArgv0Status: syncArgv0.status, syncArgv0Stdout: syncArgv0.stdout, syncErrorStatus: syncError.status, syncErrorStdoutBase64: Buffer.from(syncError.stdout ?? []).toString("base64"), syncErrorStderrBase64: Buffer.from(syncError.stderr ?? []).toString("base64"), syncTimeoutStatus: syncTimeout.status, syncTimeoutSignal: syncTimeout.signal, syncTimeoutErrorCode: syncTimeout.error?.code, syncTimeoutErrorMessage: syncTimeout.error?.message, syncTimeoutStdout: syncTimeout.stdout, syncTimeoutStderr: syncTimeout.stderr, syncCaughtTimeoutStatus: syncCaughtTimeout.status, syncCaughtTimeoutSignal: syncCaughtTimeout.signal, syncCaughtTimeoutErrorCode: syncCaughtTimeout.error?.code, stdinDestroyStatus, stdinCallbackCode: stdinCallbackResult.code, stdinCallbackSignal: stdinCallbackResult.signal, stdinCallbackWriteCallbackCalled: stdinCallbackResult.writeCallbackCalled, stdinCallbackWriteCallbackError: stdinCallbackResult.writeCallbackError, stdinCallbackEndCallbackCalled: stdinCallbackResult.endCallbackCalled, stdinCallbackStdoutBase64: stdinCallbackResult.stdoutBase64, stdinCallbackStderrBase64: stdinCallbackResult.stderrBase64, asyncCode: asyncResult.code, asyncSignal: asyncResult.signal, asyncStdoutBase64: asyncResult.stdoutBase64, asyncStderrBase64: asyncResult.stderrBase64, asyncErrorCode: asyncErrorResult.code, asyncErrorSignal: asyncErrorResult.signal, asyncErrorStdoutBase64: asyncErrorResult.stdoutBase64, asyncErrorStderrBase64: asyncErrorResult.stderrBase64, })); "#, ); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let host = run_host_node_json(temp.path(), &temp.path().join("entry.mjs")); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: None, }) .expect("start JavaScript execution"); let result = wait_with_host_child_process_bridge(execution, temp.path()); let stdout = String::from_utf8(result.stdout).expect("stdout utf8"); let stderr = String::from_utf8(result.stderr).expect("stderr utf8"); assert_eq!(result.exit_code, 0, "unexpected stderr: {stderr}"); assert!(stderr.is_empty(), "unexpected stderr: {stderr}"); let guest: Value = serde_json::from_str(stdout.trim()).expect("parse guest JSON"); assert_eq!( guest, host, "guest child_process result diverged from host Node\nhost: {}\nguest: {}", serde_json::to_string_pretty(&host).expect("pretty host JSON"), serde_json::to_string_pretty(&guest).expect("pretty guest JSON") ); } fn javascript_execution_v8_web_stream_globals_support_basic_io() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" const writes = []; const writable = new WritableStream({ write(chunk) { writes.push(new TextDecoder().decode(chunk)); }, }); const writer = writable.getWriter(); await writer.write(new TextEncoder().encode("hello")); writer.releaseLock(); const readable = new ReadableStream({ start(controller) { controller.enqueue("alpha"); controller.close(); }, }); const reader = readable.getReader(); const first = await reader.read(); const second = await reader.read(); reader.releaseLock(); if (writes.length !== 1 || writes[0] !== "hello") { throw new Error(`unexpected writes: ${JSON.stringify(writes)}`); } if (first.value !== "alpha" || first.done !== false || second.done !== true) { throw new Error(`unexpected reads: ${JSON.stringify({ first, second })}`); } "#, )), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); assert_eq!(result.exit_code, 0); let stderr = String::from_utf8(result.stderr).expect("stderr utf8"); assert!(stderr.is_empty(), "unexpected stderr: {stderr}"); } fn javascript_execution_v8_text_codec_streams_support_pipe_through() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" const { TextEncoderStream: ModuleTextEncoderStream, TextDecoderStream: ModuleTextDecoderStream, } = await import("node:stream/web"); if (ModuleTextEncoderStream !== TextEncoderStream) { throw new Error("node:stream/web TextEncoderStream export diverged from global"); } if (ModuleTextDecoderStream !== TextDecoderStream) { throw new Error("node:stream/web TextDecoderStream export diverged from global"); } if (new TextEncoderStream().encoding !== "utf-8") { throw new Error("unexpected TextEncoderStream encoding"); } const decoder = new ReadableStream({ start(controller) { controller.enqueue(new Uint8Array([0xe2, 0x82])); controller.enqueue(new Uint8Array([0xac, 0x21])); controller.close(); }, }).pipeThrough(new TextDecoderStream()); const decoderReader = decoder.getReader(); const decoded = []; for (;;) { const { done, value } = await decoderReader.read(); if (done) break; decoded.push(value); } decoderReader.releaseLock(); if (decoded.join("") !== "€!") { throw new Error(`unexpected decoded output: ${JSON.stringify(decoded)}`); } const encoded = new ReadableStream({ start(controller) { controller.enqueue("hello"); controller.enqueue(" world"); controller.close(); }, }).pipeThrough(new TextEncoderStream()); const encodedReader = encoded.getReader(); const bytes = []; for (;;) { const { done, value } = await encodedReader.read(); if (done) break; bytes.push(...value); } encodedReader.releaseLock(); const roundTrip = new TextDecoder().decode(new Uint8Array(bytes)); if (roundTrip !== "hello world") { throw new Error(`unexpected encoded output: ${roundTrip}`); } "#, )), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); assert_eq!(result.exit_code, 0); let stderr = String::from_utf8(result.stderr).expect("stderr utf8"); assert!(stderr.is_empty(), "unexpected stderr: {stderr}"); } fn javascript_execution_v8_abort_controller_dispatches_abort() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" const controller = new AbortController(); let seenAbort = false; controller.signal.addEventListener("abort", () => { seenAbort = true; }); controller.abort("stop"); if (!controller.signal.aborted || controller.signal.reason !== "stop" || !seenAbort) { throw new Error("abort controller did not update signal state"); } "#, )), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); assert_eq!(result.exit_code, 0); let stderr = String::from_utf8(result.stderr).expect("stderr utf8"); assert!(stderr.is_empty(), "unexpected stderr: {stderr}"); } fn javascript_execution_v8_request_accepts_abort_signal() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" const controller = new AbortController(); const request = new Request("http://example.com/test", { method: "POST", body: JSON.stringify({ ok: true }), duplex: "half", signal: controller.signal, headers: { "content-type": "application/json" }, }); if (!(request.signal instanceof AbortSignal)) { throw new Error("request signal was not preserved"); } "#, )), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); assert_eq!(result.exit_code, 0); let stderr = String::from_utf8(result.stderr).expect("stderr utf8"); assert!(stderr.is_empty(), "unexpected stderr: {stderr}"); } fn javascript_execution_v8_abort_signal_static_helpers_work() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" if (typeof AbortSignal.timeout !== "function") { throw new Error("AbortSignal.timeout missing"); } if (typeof AbortSignal.any !== "function") { throw new Error("AbortSignal.any missing"); } const timeoutSignal = AbortSignal.timeout(25); let timeoutEventCount = 0; timeoutSignal.addEventListener("abort", () => { timeoutEventCount += 1; }); await new Promise((resolve) => setTimeout(resolve, 60)); if (!timeoutSignal.aborted) { throw new Error("AbortSignal.timeout did not abort"); } if (timeoutEventCount !== 1) { throw new Error(`unexpected timeout event count: ${timeoutEventCount}`); } if (!timeoutSignal.reason || timeoutSignal.reason.name !== "AbortError") { throw new Error(`unexpected timeout reason: ${String(timeoutSignal.reason?.name ?? timeoutSignal.reason)}`); } const controller = new AbortController(); const sibling = new AbortController(); const composite = AbortSignal.any([sibling.signal, controller.signal]); let compositeReason; composite.addEventListener("abort", () => { compositeReason = composite.reason; }); controller.abort("manual-stop"); await new Promise((resolve) => setTimeout(resolve, 0)); if (!composite.aborted) { throw new Error("AbortSignal.any did not abort"); } if (compositeReason !== "manual-stop" || composite.reason !== "manual-stop") { throw new Error(`unexpected composite reason: ${String(composite.reason)}`); } "#, )), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); let stdout = String::from_utf8(result.stdout.clone()).expect("stdout utf8"); let stderr = String::from_utf8(result.stderr.clone()).expect("stderr utf8"); assert_eq!(result.exit_code, 0, "stdout:\n{stdout}\nstderr:\n{stderr}"); assert!(stderr.is_empty(), "unexpected stderr: {stderr}"); } fn javascript_execution_v8_schedule_timer_bridge_resolves() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.js")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" (async () => { let resolved = false; await _scheduleTimer.apply(undefined, [15]).then(() => { resolved = true; }); if (!resolved) { throw new Error("_scheduleTimer did not resolve"); } })().catch((error) => { process.exitCode = 1; throw error; }); "#, )), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); assert_eq!(result.exit_code, 0); let stderr = String::from_utf8(result.stderr).expect("stderr utf8"); assert!(stderr.is_empty(), "unexpected stderr: {stderr}"); } fn javascript_execution_v8_kernel_poll_bridge_requests_multiple_fds() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let mut execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.js")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" const result = globalThis._kernelPollRaw.applySyncPromise(undefined, [[ { fd: 0, events: 1 }, { fd: 1, events: 1 }, ], 250]); if (result.readyCount !== 1) { throw new Error(`readyCount=${result.readyCount}`); } if (result.fds[0]?.revents !== 1 || result.fds[1]?.revents !== 0) { throw new Error(`revents=${JSON.stringify(result.fds)}`); } console.log(JSON.stringify(result)); "#, )), }) .expect("start JavaScript execution"); let request = expect_next_sync_rpc(&mut execution, "poll execution event"); assert_eq!(request.method, "__kernel_poll"); assert_eq!( request.args, vec![ json!([ { "fd": 0, "events": 1 }, { "fd": 1, "events": 1 } ]), json!(250), ] ); execution .respond_sync_rpc_success( request.id, json!({ "readyCount": 1, "fds": [ { "fd": 0, "events": 1, "revents": 1 }, { "fd": 1, "events": 1, "revents": 0 } ] }), ) .expect("respond to __kernel_poll"); let result = execution.wait().expect("wait for JavaScript execution"); let stderr = String::from_utf8(result.stderr).expect("stderr utf8"); assert_eq!(result.exit_code, 0, "stderr: {stderr}"); assert!(stderr.is_empty(), "unexpected stderr: {stderr}"); let stdout: Value = serde_json::from_slice(&result.stdout).expect("parse guest stdout JSON"); assert_eq!( stdout, json!({ "readyCount": 1, "fds": [ { "fd": 0, "events": 1, "revents": 1 }, { "fd": 1, "events": 1, "revents": 0 } ] }) ); } fn javascript_execution_v8_crypto_random_sources_use_local_secure_bridge() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.js")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" const first = new Uint8Array(32); const second = new Uint8Array(32); globalThis.crypto.getRandomValues(first); globalThis.crypto.getRandomValues(second); if (first.every((value) => value === 0)) { throw new Error("first random buffer was all zero"); } if (second.every((value) => value === 0)) { throw new Error("second random buffer was all zero"); } const buffersMatch = first.length === second.length && first.every((value, index) => value === second[index]); if (buffersMatch) { throw new Error("random buffers repeated"); } const uuid = globalThis.crypto.randomUUID(); if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(uuid)) { throw new Error(`invalid uuid: ${uuid}`); } "#, )), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); let stdout = String::from_utf8(result.stdout.clone()).expect("stdout utf8"); let stderr = String::from_utf8(result.stderr.clone()).expect("stderr utf8"); assert_eq!(result.exit_code, 0, "stdout:\n{stdout}\nstderr:\n{stderr}"); assert!(stderr.is_empty(), "unexpected stderr: {stderr}"); } #[test] fn javascript_execution_v8_crypto_basic_operations_emit_expected_sync_rpcs() { assert_eq!( map_bridge_method("_cryptoHashDigest"), ("crypto.hashDigest", false) ); assert_eq!( map_bridge_method("_cryptoHmacDigest"), ("crypto.hmacDigest", false) ); assert_eq!(map_bridge_method("_cryptoPbkdf2"), ("crypto.pbkdf2", false)); assert_eq!(map_bridge_method("_cryptoScrypt"), ("crypto.scrypt", false)); assert_eq!( map_bridge_method("_netSocketConnectRaw"), ("net.connect", false) ); assert_eq!( map_bridge_method("_networkDnsLookupSyncRaw"), ("dns.lookup", false) ); assert_eq!(map_bridge_method("_netSocketPollRaw"), ("net.poll", false)); } fn javascript_execution_v8_load_polyfill_returns_runtime_module_expressions() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" const pathExpr = _loadPolyfill.applySyncPromise(undefined, ["path"]); if (typeof pathExpr !== "string" || !pathExpr.includes("node:path")) { throw new Error(`unexpected path polyfill expression: ${String(pathExpr)}`); } const pathModule = Function('"use strict"; return (' + pathExpr + ');')(); if (pathModule.join("alpha", "beta") !== "alpha/beta") { throw new Error("path polyfill expression did not resolve the runtime module"); } const deniedExpr = _loadPolyfill.applySyncPromise(undefined, ["inspector"]); if (typeof deniedExpr !== "string" || !deniedExpr.includes("ERR_ACCESS_DENIED")) { throw new Error(`unexpected denied polyfill expression: ${String(deniedExpr)}`); } let denied = false; try { Function('"use strict"; return (' + deniedExpr + ');')(); } catch (error) { denied = error?.code === "ERR_ACCESS_DENIED"; } if (!denied) { throw new Error("denied polyfill expression did not raise ERR_ACCESS_DENIED"); } if (_loadPolyfill.applySyncPromise(undefined, ["not-a-real-builtin"]) !== null) { throw new Error("unknown polyfill name should return null"); } "#, )), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); let stdout = String::from_utf8(result.stdout.clone()).expect("stdout utf8"); let stderr = String::from_utf8(result.stderr.clone()).expect("stderr utf8"); assert_eq!(result.exit_code, 0, "stdout:\n{stdout}\nstderr:\n{stderr}"); assert!(stderr.is_empty(), "unexpected stderr: {stderr}"); } fn javascript_execution_v8_stream_wrapper_exports_common_node_classes() { let temp = tempdir().expect("create temp dir"); write_fixture( &temp.path().join("entry.mjs"), r#" import { Duplex, PassThrough, Readable, Transform, Writable, isReadable, isWritable, } from "node:stream"; import { createRequire } from "node:module"; for (const [name, value] of Object.entries({ Duplex, PassThrough, Readable, Transform, Writable })) { if (typeof value !== "function") { throw new Error(`${name} was not exported as a constructor`); } } const require = createRequire(import.meta.url); const cjsStream = require("stream"); if (typeof cjsStream !== "function") { throw new Error("require('stream') should return the legacy Stream constructor"); } if (cjsStream !== cjsStream.Stream) { throw new Error("require('stream').Stream should alias the CommonJS export"); } if (typeof cjsStream.Readable !== "function") { throw new Error("require('stream').Readable should stay available on the constructor export"); } // readable-stream imports the trailing-slash `process/` package internally. // Its browser fallback implements nextTick with setTimeout(0), which inserts a // timer turn before `_read()`. AgentOS must route that dependency to the guest // process nextTick queue so stream demand is visible in the same microtask turn. let readStarted = false; const nextTickReadable = new Readable({ read() { readStarted = true; this.push(null); }, }); nextTickReadable.resume(); await Promise.resolve(); if (!readStarted) { throw new Error("readable-stream deferred _read() through a timer-backed process.nextTick shim"); } const pass = new PassThrough(); let output = ""; pass.on("data", (chunk) => { output += Buffer.from(chunk).toString("utf8"); }); if (!isReadable(pass) || !isWritable(pass)) { throw new Error("stream helpers misreported passthrough readability"); } pass.end("hello"); await new Promise((resolve, reject) => { pass.once("close", resolve); pass.once("error", reject); }); if (output !== "hello") { throw new Error(`unexpected passthrough output: ${output}`); } const lifecycle = []; let writableOutput = ""; const writable = new Writable({ write(chunk, _encoding, callback) { lifecycle.push("write"); writableOutput += Buffer.from(chunk).toString("utf8"); callback(); }, destroy(_error, callback) { lifecycle.push("destroy"); callback(); }, }); writable.on("finish", () => lifecycle.push("finish")); writable.end("hi"); await new Promise((resolve, reject) => { writable.once("close", resolve); writable.once("error", reject); }); if (writableOutput !== "hi") { throw new Error(`unexpected writable output: ${writableOutput}`); } if (lifecycle.join(",") !== "write,finish,destroy") { throw new Error(`unexpected writable lifecycle: ${lifecycle.join(",")}`); } "#, ); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: None, }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); let stderr = String::from_utf8(result.stderr).expect("stderr utf8"); assert_eq!(result.exit_code, 0, "unexpected stderr: {stderr}"); assert!(stderr.is_empty(), "unexpected stderr: {stderr}"); } fn javascript_execution_v8_buffer_wrapper_exposes_commonjs_constants() { let temp = tempdir().expect("create temp dir"); write_fixture( &temp.path().join("entry.mjs"), r#" import { createRequire } from "node:module"; const require = createRequire(import.meta.url); const bufferModule = require("buffer"); if (typeof bufferModule.constants !== "object" || bufferModule.constants === null) { throw new Error("require('buffer').constants was not exported"); } if (typeof bufferModule.constants.MAX_STRING_LENGTH !== "number") { throw new Error("require('buffer').constants.MAX_STRING_LENGTH was not exported"); } if (typeof bufferModule.kMaxLength !== "number") { throw new Error("require('buffer').kMaxLength was not exported"); } if (bufferModule.Buffer?.constants?.MAX_STRING_LENGTH !== bufferModule.constants.MAX_STRING_LENGTH) { throw new Error("buffer module constants diverged from Buffer.constants"); } if (typeof bufferModule.Blob !== "function") { throw new Error("require('buffer').Blob was not exported"); } if (typeof bufferModule.File !== "function") { throw new Error("require('buffer').File was not exported"); } const file = new bufferModule.File(["hello"], "hello.txt", { type: "text/plain" }); if (!(file instanceof bufferModule.Blob)) { throw new Error("buffer module File did not extend Blob"); } "#, ); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: None, }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); let stderr = String::from_utf8(result.stderr).expect("stderr utf8"); assert_eq!(result.exit_code, 0, "unexpected stderr: {stderr}"); assert!(stderr.is_empty(), "unexpected stderr: {stderr}"); } #[allow(dead_code)] // quarantined: see the live-stdin/tty harness note above fn javascript_execution_v8_tty_module_is_backed_by_live_process() { let temp = tempdir().expect("create temp dir"); write_fixture( &temp.path().join("entry.mjs"), r#" import { createRequire } from "node:module"; const require = createRequire(import.meta.url); const tty = require("tty"); // tty.isatty / ReadStream / WriteStream read process.std{in,out,err}. When the // bridge tty stub captured the `process` object at module-load time it snapshotted // `undefined` (process.ts initializes far later in the bundle's module-cycle order), // so these threw "Cannot read properties of undefined". Reading the live binding at // call time fixes it; this test guards against that regression class. if (typeof tty.isatty !== "function") { throw new Error("require('tty').isatty was not exported"); } for (const fd of [0, 1, 2]) { if (typeof tty.isatty(fd) !== "boolean") { throw new Error(`tty.isatty(${fd}) did not return a boolean`); } } if (typeof tty.ReadStream !== "function" || typeof tty.WriteStream !== "function") { throw new Error("require('tty') stream classes were not exported"); } // Constructing the streams exercises the process-backed stdio getters. new tty.ReadStream(0); new tty.WriteStream(1); "#, ); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: None, }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); let stderr = String::from_utf8(result.stderr).expect("stderr utf8"); assert_eq!(result.exit_code, 0, "unexpected stderr: {stderr}"); assert!(stderr.is_empty(), "unexpected stderr: {stderr}"); } fn javascript_execution_v8_sqlite_module_resolves_via_global_install() { let temp = tempdir().expect("create temp dir"); write_fixture( &temp.path().join("entry.mjs"), r#" import { createRequire } from "node:module"; const require = createRequire(import.meta.url); // require("sqlite") routes through the `_sqliteModule` global. That install was // dropped once during the network-module split (it fell off the end of the slice), // making this throw "ReferenceError: _sqliteModule is not defined". Guard it. const sqlite = require("node:sqlite"); if (typeof sqlite.DatabaseSync !== "function") { throw new Error("require('node:sqlite').DatabaseSync was not exported"); } if (typeof sqlite.StatementSync !== "function") { throw new Error("require('node:sqlite').StatementSync was not exported"); } "#, ); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: None, }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); let stderr = String::from_utf8(result.stderr).expect("stderr utf8"); assert_eq!(result.exit_code, 0, "unexpected stderr: {stderr}"); assert!(stderr.is_empty(), "unexpected stderr: {stderr}"); } fn javascript_execution_v8_commonjs_stack_frames_preserve_module_paths() { let temp = tempdir().expect("create temp dir"); write_fixture( &temp.path().join("entry.mjs"), r#" import { createRequire } from "node:module"; const require = createRequire(import.meta.url); require("./probe.cjs"); "#, ); write_fixture( &temp.path().join("probe.cjs"), r#" const previousPrepare = Error.prepareStackTrace; try { Error.prepareStackTrace = (_error, stack) => stack; const stack = new Error("probe").stack ?? []; const frame = stack.find((callsite) => { const path = callsite.getFileName?.() ?? callsite.getScriptNameOrSourceURL?.(); return typeof path === "string" && path.endsWith("/probe.cjs"); }); if (!frame) { const summary = stack.map((callsite) => ({ fileName: callsite.getFileName?.() ?? null, scriptName: callsite.getScriptNameOrSourceURL?.() ?? null, text: String(callsite), })); throw new Error( "CommonJS stack frames did not preserve the module path: " + JSON.stringify(summary), ); } } finally { Error.prepareStackTrace = previousPrepare; } "#, ); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: None, }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); let stderr = String::from_utf8(result.stderr).expect("stderr utf8"); assert_eq!(result.exit_code, 0, "unexpected stderr: {stderr}"); assert!(stderr.is_empty(), "unexpected stderr: {stderr}"); } fn javascript_execution_v8_commonjs_main_entrypoints_preserve_entrypoint_paths() { let temp = tempdir().expect("create temp dir"); write_fixture( &temp.path().join("entry.cjs"), r#" const EVAL_FRAMES = new Set(["[eval]", "[eval]-wrapper"]); const INTERNAL_FRAME_NAMES = new Set([ "readCallsites", "resolveCallerFilePath", "getCurrentFilePath", ]); function readCallsites() { const previousPrepare = Error.prepareStackTrace; try { Error.prepareStackTrace = (_error, stack) => stack; return new Error("probe").stack ?? []; } finally { Error.prepareStackTrace = previousPrepare; } } function readCallsitePath(callsite) { const rawPath = callsite.getFileName?.() ?? callsite.getScriptNameOrSourceURL?.(); if (!rawPath || rawPath.startsWith("node:") || EVAL_FRAMES.has(rawPath)) { return null; } return rawPath; } function isInternalCallsite(callsite) { const functionName = callsite.getFunctionName?.(); if (functionName && INTERNAL_FRAME_NAMES.has(functionName)) { return true; } const methodName = callsite.getMethodName?.(); if (methodName && INTERNAL_FRAME_NAMES.has(methodName)) { return true; } const callsiteString = String(callsite); for (const frameName of INTERNAL_FRAME_NAMES) { if ( callsiteString.includes(`${frameName} (`) || callsiteString.includes(`.${frameName} (`) ) { return true; } } return false; } function resolveCallerFilePath() { for (const callsite of readCallsites()) { const filePath = readCallsitePath(callsite); if (!filePath || isInternalCallsite(callsite)) { continue; } return filePath; } throw new Error("Unable to resolve caller file path."); } const resolved = resolveCallerFilePath(); if (!resolved.endsWith("/entry.cjs")) { throw new Error(`resolved ${resolved} instead of /entry.cjs`); } "#, ); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.cjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: None, }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); let stderr = String::from_utf8(result.stderr).expect("stderr utf8"); assert_eq!(result.exit_code, 0, "unexpected stderr: {stderr}"); assert!(stderr.is_empty(), "unexpected stderr: {stderr}"); } fn javascript_execution_v8_inline_commonjs_entrypoints_preserve_entrypoint_paths() { let temp = tempdir().expect("create temp dir"); let source = String::from( r#" const EVAL_FRAMES = new Set(["[eval]", "[eval]-wrapper"]); const INTERNAL_FRAME_NAMES = new Set([ "readCallsites", "resolveCallerFilePath", "getCurrentFilePath", ]); function readCallsites() { const previousPrepare = Error.prepareStackTrace; try { Error.prepareStackTrace = (_error, stack) => stack; return new Error("probe").stack ?? []; } finally { Error.prepareStackTrace = previousPrepare; } } function readCallsitePath(callsite) { const rawPath = callsite.getFileName?.() ?? callsite.getScriptNameOrSourceURL?.(); if (!rawPath || rawPath.startsWith("node:") || EVAL_FRAMES.has(rawPath)) { return null; } return rawPath; } function isInternalCallsite(callsite) { const functionName = callsite.getFunctionName?.(); if (functionName && INTERNAL_FRAME_NAMES.has(functionName)) { return true; } const methodName = callsite.getMethodName?.(); if (methodName && INTERNAL_FRAME_NAMES.has(methodName)) { return true; } const callsiteString = String(callsite); for (const frameName of INTERNAL_FRAME_NAMES) { if ( callsiteString.includes(`${frameName} (`) || callsiteString.includes(`.${frameName} (`) ) { return true; } } return false; } function resolveCallerFilePath() { for (const callsite of readCallsites()) { const filePath = readCallsitePath(callsite); if (!filePath || isInternalCallsite(callsite)) { continue; } return filePath; } throw new Error("Unable to resolve caller file path."); } const resolved = resolveCallerFilePath(); if (!resolved.endsWith("/entry.cjs")) { throw new Error(`resolved ${resolved} instead of /entry.cjs`); } "#, ); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.cjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(source), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); let stderr = String::from_utf8(result.stderr).expect("stderr utf8"); assert_eq!(result.exit_code, 0, "unexpected stderr: {stderr}"); assert!(stderr.is_empty(), "unexpected stderr: {stderr}"); } fn javascript_execution_v8_inline_commonjs_entrypoints_preserve_commonjs_globals() { let temp = tempdir().expect("create temp dir"); let source = String::from( r#" console.log( JSON.stringify({ filename: __filename, dirname: __dirname, cwd: process.cwd(), }), ); "#, ); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.cjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(source), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); let stderr = String::from_utf8(result.stderr).expect("stderr utf8"); assert_eq!(result.exit_code, 0, "unexpected stderr: {stderr}"); assert!(stderr.is_empty(), "unexpected stderr: {stderr}"); let output: Value = serde_json::from_slice(&result.stdout).expect("parse stdout JSON"); assert_eq!( output, json!({ "filename": "/root/entry.cjs", "dirname": "/root", "cwd": "/root", }) ); } fn javascript_execution_v8_commonjs_require_exposes_node_metadata() { let temp = tempdir().expect("create temp dir"); write_fixture( &temp.path().join("dep.cjs"), r#" const hadSelfBeforeDelete = Object.prototype.hasOwnProperty.call( require.cache, __filename, ); delete require.cache[__filename]; module.exports = { cacheType: typeof require.cache, hadSelfBeforeDelete, hasSelfAfterDelete: Object.prototype.hasOwnProperty.call(require.cache, __filename), extensionsType: typeof require.extensions, }; "#, ); write_fixture( &temp.path().join("entry.cjs"), r#" const dep = require("./dep.cjs"); console.log(JSON.stringify(dep)); "#, ); let mut host = run_host_node_json(temp.path(), &temp.path().join("entry.cjs")); host["hadSelfBeforeDelete"] = json!(true); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.cjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: None, }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); let stderr = String::from_utf8(result.stderr).expect("stderr utf8"); assert_eq!(result.exit_code, 0, "unexpected stderr: {stderr}"); assert!(stderr.is_empty(), "unexpected stderr: {stderr}"); let guest: Value = serde_json::from_slice(&result.stdout).expect("parse stdout JSON"); assert_eq!( guest, host, "guest CommonJS require metadata diverged from host" ); } fn javascript_execution_v8_https_agents_expose_options_objects() { let temp = tempdir().expect("create temp dir"); write_fixture( &temp.path().join("entry.mjs"), r#" import { createRequire } from "node:module"; const require = createRequire(import.meta.url); const http = require("http"); const https = require("https"); for (const [name, module] of Object.entries({ http, https })) { if (!module.globalAgent || typeof module.globalAgent.options !== "object") { throw new Error(`${name}.globalAgent.options was not initialized`); } const agent = new module.Agent({ keepAlive: true }); if (!agent.options || agent.options.keepAlive !== true) { throw new Error(`${name}.Agent did not preserve constructor options`); } } "#, ); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: None, }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); let stderr = String::from_utf8(result.stderr).expect("stderr utf8"); assert_eq!(result.exit_code, 0, "unexpected stderr: {stderr}"); assert!(stderr.is_empty(), "unexpected stderr: {stderr}"); } fn javascript_execution_v8_net_socket_readable_state_tracks_ssh2_writable_shape() { let temp = tempdir().expect("create temp dir"); write_fixture( &temp.path().join("entry.mjs"), r#" import net from "node:net"; import { Duplex } from "node:stream"; import { createRequire } from "node:module"; const require = createRequire(import.meta.url); if (require("stream").Duplex !== Duplex) { throw new Error("bare and node: stream modules returned different Duplex constructors"); } const isWritable = (stream) => Boolean(stream?.writable && stream?._readableState?.ended === false); const socket = new net.Socket(); if (!(socket instanceof Duplex)) { throw new Error("net.Socket did not inherit from the guest Duplex constructor"); } if (Object.getPrototypeOf(net.Socket.prototype) !== Duplex.prototype) { throw new Error("net.Socket prototype is not directly rooted in guest Duplex.prototype"); } if (socket._readableState?.ended !== false) { throw new Error(`expected open socket ended=false, got ${String(socket._readableState?.ended)}`); } if (!isWritable(socket)) { throw new Error("ssh2 writable probe should accept an open socket"); } socket.destroy(); if (socket._readableState?.ended !== true) { throw new Error(`expected destroyed socket ended=true, got ${String(socket._readableState?.ended)}`); } if (isWritable(socket)) { throw new Error("ssh2 writable probe should reject a destroyed socket"); } "#, ); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: None, }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); let stderr = String::from_utf8(result.stderr).expect("stderr utf8"); assert_eq!(result.exit_code, 0, "unexpected stderr: {stderr}"); assert!(stderr.is_empty(), "unexpected stderr: {stderr}"); } // Regression: when the host briefly stops draining the V8 -> host event channel // (capacity = JAVASCRIPT_EVENT_CHANNEL_CAPACITY = 512), a burst of guest events // must apply backpressure, not tear the session down. The original code called // `v8_session.destroy()` the instant `try_send` returned `Full`, turning a // transient backlog into `Exited(1)` with a truncated event stream. // // The guest synchronously logs far more lines than the event channel holds. The // host deliberately does not drain for a window, forcing the bridge onto the // formerly fatal full path, then drains everything. With backpressure every line // survives and the session exits cleanly; with the destroy-on-full bug the stream // is truncated and the session never reaches a clean exit. // // Note: this exercises the V8->host event channel (JAVASCRIPT_EVENT_CHANNEL_CAPACITY). // The upstream per-session v8_session_frames channel does NOT overflow here because // each guest `console.log` is a synchronous `applySync` that blocks until the bridge // drains it — so the guest cannot run ahead and only ~1 frame is ever in flight. That // channel's backpressure (v8_host.rs) only engages under async runtime frame bursts; // it shares the same TrackedSyncSender blocking-send mechanism covered by the bridge // queue_tracker unit tests. fn javascript_execution_v8_event_channel_backpressures_instead_of_destroying_session() { const LINE_COUNT: usize = 1000; let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let mut execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), // Inline code avoids host-serviced module-resolution RPCs, so the // guest runs autonomously and fills the event channel during the // no-drain window below without the host's involvement. wasm_module_bytes: None, inline_code: Some(format!( "for (let i = 0; i < {LINE_COUNT}; i++) {{ console.log('LINE:' + i); }}\n" )), }) .expect("start JavaScript execution"); // Stop draining long enough for the guest burst to overrun the 512-slot // channel and exercise the full path. std::thread::sleep(Duration::from_millis(500)); let mut stdout = Vec::new(); let mut stderr = Vec::new(); let mut exit_code = None; // A poll error (e.g. EventChannelClosed) means the session was torn down // out from under us — exactly the destroy-on-full regression — so stop and // let the assertions below report the truncation rather than panicking. while let Ok(event) = execution.poll_event_blocking(Duration::from_secs(10)) { match event { Some(JavascriptExecutionEvent::Stdout(chunk)) => stdout.extend(chunk), Some(JavascriptExecutionEvent::Stderr(chunk)) => stderr.extend(chunk), Some(JavascriptExecutionEvent::SignalState { .. }) => {} Some(JavascriptExecutionEvent::SyncRpcRequest(request)) => { // No host RPCs are expected on this path; service module RPCs if // any surface and answer anything else with null so a stray RPC // cannot wedge the guest. if execution .try_service_standalone_module_sync_rpc(&request) .expect("service module sync RPC") { continue; } execution .respond_sync_rpc_success(request.id, Value::Null) .expect("respond to unexpected sync RPC"); } Some(JavascriptExecutionEvent::Exited(code)) => { exit_code = Some(code); break; } None => break, } } let stdout = String::from_utf8_lossy(&stdout); let stderr = String::from_utf8_lossy(&stderr); let lines = stdout.matches("LINE:").count(); assert_eq!( exit_code, Some(0), "session must exit cleanly under backpressure (destroy-on-full regression?); \ lines={lines}/{LINE_COUNT}, stderr: {stderr}" ); assert_eq!( lines, LINE_COUNT, "every event must survive backpressure with no truncation; stderr: {stderr}" ); } // Regression: the in-VM bridge socket read loop (`_pumpBridgeReads`) must yield // a macrotask between delivering successive socket chunks instead of draining an // entire response synchronously in one burst. // // `_netSocketReadRaw` is synchronous, so the original loop read every available // byte and emitted `readable`/`data` for the whole HTTP response inside a single // synchronous stack. That collapses the event-loop turn boundaries that undici's // keep-alive socket recycling depends on: it resolves the caller's `fetch` // synchronously and only defers reuse with `setImmediate(client[kResume])`, so // the caller's microtask dispatches the next request while every pooled Client is // still `kNeedDrain`. The pool then allocates a fresh Client + socket per request, // each registering EventEmitter listeners until the guest dies // (MaxListenersExceededWarning + unbounded memory / OOM). // // This reproduces the timing deterministically with two scripted socket chunks. // A `setImmediate` is scheduled the moment the first chunk is delivered (standing // in for `setImmediate(kResume)`); it MUST get a turn before the second chunk // surfaces. With the synchronous-burst bug both chunks arrive before the macrotask // runs and the guest throws; with the per-chunk macrotask yield the macrotask // interleaves between the chunks and the guest prints `ORDER_OK`. fn javascript_execution_v8_net_socket_read_loop_yields_macrotask_between_chunks() { let temp = tempdir().expect("create temp dir"); write_fixture( &temp.path().join("entry.mjs"), r#" import net from "node:net"; const order = []; let scheduledImmediate = false; const socket = net.connect({ host: "127.0.0.1", port: 80 }); socket.on("data", (chunk) => { order.push("data:" + chunk.toString()); if (!scheduledImmediate) { scheduledImmediate = true; // Stand-in for undici's setImmediate(client[kResume]) keep-alive recycle: // a macrotask scheduled the instant the first socket chunk lands. It must // get a turn before the next chunk is delivered. setImmediate(() => { order.push("immediate"); }); } }); await new Promise((resolve, reject) => { socket.once("end", resolve); socket.once("close", resolve); socket.once("error", reject); }); // Flush any macrotask still pending after the stream ends. await new Promise((resolve) => setImmediate(resolve)); const trace = order.join(","); const immediateIdx = order.indexOf("immediate"); const secondChunkIdx = order.indexOf("data:chunk-2"); if (immediateIdx === -1) { throw new Error("macrotask never ran: " + trace); } if (secondChunkIdx === -1) { throw new Error("second chunk never delivered: " + trace); } if (immediateIdx > secondChunkIdx) { throw new Error( "bridge delivered the response in one synchronous burst; the keep-alive " + "macrotask never interleaved between chunks: " + trace, ); } console.log("ORDER_OK:" + trace); "#, ); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let mut execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: None, }) .expect("start JavaScript execution"); let chunk1 = base64::engine::general_purpose::STANDARD.encode("chunk-1"); let chunk2 = base64::engine::general_purpose::STANDARD.encode("chunk-2"); let mut socket_reads = 0usize; let mut stdout = Vec::new(); let mut stderr = Vec::new(); let exit_code = loop { match execution .poll_event_blocking(Duration::from_secs(5)) .expect("poll net socket bridge event") { Some(JavascriptExecutionEvent::Stdout(chunk)) => stdout.extend(chunk), Some(JavascriptExecutionEvent::Stderr(chunk)) => stderr.extend(chunk), Some(JavascriptExecutionEvent::SignalState { .. }) => {} Some(JavascriptExecutionEvent::SyncRpcRequest(request)) => { if execution .try_service_standalone_module_sync_rpc(&request) .expect("service module sync RPC") { continue; } let request_id = request.id; let response = match request.method.as_str() { "net.connect" => json!({ "socketId": 1, "localAddress": "127.0.0.1", "localPort": 12345, "localFamily": "IPv4", "remoteAddress": "127.0.0.1", "remotePort": 80, "remoteFamily": "IPv4", }), "net.socket_wait_connect" => json!( "{\"localAddress\":\"127.0.0.1\",\"localPort\":12345,\ \"remoteAddress\":\"127.0.0.1\",\"remotePort\":80}" ), // First read -> chunk 1, second read -> chunk 2, then EOF (null). "net.socket_read" => { socket_reads += 1; match socket_reads { 1 => json!(chunk1), 2 => json!(chunk2), _ => Value::Null, } } // Any incidental socket RPC (set_no_delay, poll, shutdown, // destroy, ...) gets a benign null so the flow proceeds. _ => Value::Null, }; execution .respond_sync_rpc_success(request_id, response) .expect("respond to net socket bridge RPC"); } Some(JavascriptExecutionEvent::Exited(exit_code)) => break exit_code, None => panic!("net socket bridge execution timed out while awaiting exit"), } }; let stdout = String::from_utf8(stdout).expect("stdout utf8"); let stderr = String::from_utf8(stderr).expect("stderr utf8"); assert_eq!( exit_code, 0, "guest exited non-zero (synchronous-burst regression?)\nstdout: {stdout}\nstderr: {stderr}" ); assert!( stdout.contains("ORDER_OK:data:chunk-1,immediate,data:chunk-2"), "expected macrotask to interleave between chunks; stdout: {stdout}\nstderr: {stderr}" ); assert_eq!( socket_reads, 3, "expected exactly chunk1, chunk2, EOF reads" ); } fn javascript_execution_v8_net_socket_backpressure_stops_and_resumes_transport_reads() { let temp = tempdir().expect("create temp dir"); write_fixture( &temp.path().join("entry.mjs"), r#" import net from "node:net"; const socket = new net.Socket({ readableHighWaterMark: 4 }); socket.connect({ host: "127.0.0.1", port: 80 }); await new Promise((resolve, reject) => { socket.once("readable", resolve); socket.once("error", reject); }); if (socket.readableLength !== 8) { throw new Error(`expected one admitted 8-byte chunk, got ${socket.readableLength}`); } const chunk = socket.read(); if (chunk?.toString() !== "abcdefgh") { throw new Error(`unexpected buffered chunk: ${String(chunk)}`); } socket.resume(); await new Promise((resolve, reject) => { socket.once("end", resolve); socket.once("close", resolve); socket.once("error", reject); }); console.log("BACKPRESSURE_OK"); "#, ); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let mut execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], argv0: None, env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: None, }) .expect("start JavaScript execution"); let chunk = base64::engine::general_purpose::STANDARD.encode("abcdefgh"); let mut socket_reads = 0usize; let mut read_interest = Vec::new(); let mut rpc_trace = Vec::new(); let mut stdout = Vec::new(); let mut stderr = Vec::new(); let exit_code = loop { match execution .poll_event_blocking(Duration::from_secs(5)) .expect("poll net socket backpressure event") { Some(JavascriptExecutionEvent::Stdout(bytes)) => stdout.extend(bytes), Some(JavascriptExecutionEvent::Stderr(bytes)) => stderr.extend(bytes), Some(JavascriptExecutionEvent::SignalState { .. }) => {} Some(JavascriptExecutionEvent::SyncRpcRequest(request)) => { if execution .try_service_standalone_module_sync_rpc(&request) .expect("service module sync RPC") { continue; } let request_id = request.id; rpc_trace.push(request.method.clone()); let response = match request.method.as_str() { "net.connect" => json!({ "socketId": 1, "localAddress": "127.0.0.1", "localPort": 12345, "localFamily": "IPv4", "remoteAddress": "127.0.0.1", "remotePort": 80, "remoteFamily": "IPv4", }), "net.socket_wait_connect" => json!( "{\"localAddress\":\"127.0.0.1\",\"localPort\":12345,\ \"remoteAddress\":\"127.0.0.1\",\"remotePort\":80}" ), "net.socket_set_read_interest" => { read_interest.push( request .args .get(1) .and_then(Value::as_bool) .expect("read-interest boolean"), ); Value::Null } "net.socket_read" => { socket_reads += 1; if socket_reads == 1 { json!(chunk) } else { Value::Null } } _ => Value::Null, }; execution .respond_sync_rpc_success(request_id, response) .expect("respond to net socket backpressure RPC"); } Some(JavascriptExecutionEvent::Exited(exit_code)) => break exit_code, None => panic!( "net socket backpressure execution timed out while awaiting exit; \ read_interest={read_interest:?}, socket_reads={socket_reads}, \ rpcs={rpc_trace:?}, stdout={}, stderr={}", String::from_utf8_lossy(&stdout), String::from_utf8_lossy(&stderr) ), } }; let stdout = String::from_utf8(stdout).expect("stdout utf8"); let stderr = String::from_utf8(stderr).expect("stderr utf8"); assert_eq!( exit_code, 0, "guest exited non-zero\nstdout: {stdout}\nstderr: {stderr}\nrpcs: {rpc_trace:?}" ); assert!( stdout.contains("BACKPRESSURE_OK"), "guest did not complete backpressure fixture: {stdout}\n{stderr}" ); assert_eq!( socket_reads, 2, "the pump must perform one data read, stop at push(false), then read EOF only after resume; rpcs: {rpc_trace:?}" ); let stop_index = read_interest .iter() .position(|enabled| !enabled) .expect("push(false) must disable native application read interest"); assert!( read_interest[..stop_index].iter().any(|enabled| *enabled), "_read() must enable application read interest before the admitted read: {read_interest:?}" ); assert!( read_interest[stop_index + 1..] .iter() .any(|enabled| *enabled), "draining below the HWM must invoke _read() and resume application reads: {read_interest:?}" ); } #[test] fn javascript_execution_v8_net_socket_unref_preserves_read_delivery() { let temp = tempdir().expect("create temp dir"); write_fixture( &temp.path().join("entry.mjs"), r#" import net from "node:net"; const socket = net.connect({ host: "127.0.0.1", port: 80 }); // This referenced timer is deliberately independent of the socket. It proves // unref() changes only socket liveness while the VM still has other live work. const keepAlive = setInterval(() => {}, 1_000); const received = await new Promise((resolve, reject) => { socket.once("error", reject); socket.once("data", (chunk) => resolve(chunk.toString())); socket.once("connect", () => { socket.unref(); setTimeout(() => { if (!globalThis._agentOSReadyDispatch(100n, 1n, 1)) { reject(new Error("socket readiness target was not registered")); } }, 10); }); }); clearInterval(keepAlive); if (received !== "after-unref") { throw new Error(`unexpected data after unref: ${received}`); } socket.destroy(); console.log("NET_UNREF_IO_OK"); "#, ); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let mut execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], argv0: None, env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: None, }) .expect("start JavaScript execution"); let encoded = base64::engine::general_purpose::STANDARD.encode("after-unref"); let mut socket_reads = 0usize; let mut stdout = Vec::new(); let mut stderr = Vec::new(); let exit_code = loop { match execution .poll_event_blocking(Duration::from_secs(5)) .expect("poll unref socket bridge event") { Some(JavascriptExecutionEvent::Stdout(bytes)) => stdout.extend(bytes), Some(JavascriptExecutionEvent::Stderr(bytes)) => stderr.extend(bytes), Some(JavascriptExecutionEvent::SignalState { .. }) => {} Some(JavascriptExecutionEvent::SyncRpcRequest(request)) => { if execution .try_service_standalone_module_sync_rpc(&request) .expect("service module sync RPC") { continue; } let response = match request.method.as_str() { "net.connect" => json!({ "socketId": 1, "capabilityId": 100, "capabilityGeneration": 1, "localAddress": "127.0.0.1", "localPort": 12345, "localFamily": "IPv4", "remoteAddress": "127.0.0.1", "remotePort": 80, "remoteFamily": "IPv4", }), "net.socket_wait_connect" => json!( "{\"localAddress\":\"127.0.0.1\",\"localPort\":12345,\ \"remoteAddress\":\"127.0.0.1\",\"remotePort\":80}" ), "net.socket_read" => { socket_reads += 1; match socket_reads { 1 => json!("__agentos_net_timeout__"), 2 => json!(encoded), _ => json!("__agentos_net_timeout__"), } } "net.socket_set_read_interest" | "net.destroy" => Value::Null, other => panic!("unexpected unref socket RPC {other}: {:?}", request.args), }; execution .respond_sync_rpc_success(request.id, response) .expect("respond to unref socket RPC"); } Some(JavascriptExecutionEvent::Exited(exit_code)) => break exit_code, None => panic!( "unref socket execution timed out; reads={socket_reads}, stdout={}, stderr={}", String::from_utf8_lossy(&stdout), String::from_utf8_lossy(&stderr), ), } }; let stdout = String::from_utf8(stdout).expect("stdout utf8"); let stderr = String::from_utf8(stderr).expect("stderr utf8"); assert_eq!( exit_code, 0, "unref socket guest exited non-zero\nstdout: {stdout}\nstderr: {stderr}" ); assert!( stdout.contains("NET_UNREF_IO_OK"), "unref socket did not receive delayed readiness data: {stdout}\n{stderr}" ); assert_eq!( socket_reads, 3, "expected an empty read, the delayed readiness payload, then a bounded empty probe" ); } #[test] fn javascript_execution_v8_net_socket_serializes_split_writev_batches() { let temp = tempdir().expect("create temp dir"); write_fixture( &temp.path().join("entry.mjs"), r#" import net from "node:net"; globalThis.__agentOSNetBridgeMetrics.enable(); const socket = net.connect({ host: "127.0.0.1", port: 80 }); const callbackOrder = []; await new Promise((resolve, reject) => { socket.once("error", reject); socket.once("data", () => { socket.cork(); socket.write(Buffer.alloc(300 * 1024, 0x41), () => callbackOrder.push("A")); socket.write(Buffer.from("B"), () => callbackOrder.push("B")); socket.uncork(); socket.end(() => { callbackOrder.push("end"); resolve(); }); }); }); if (callbackOrder.join(",") !== "A,B,end") { throw new Error(`write callbacks completed out of order: ${callbackOrder}`); } socket.destroy(); console.log("NET_WRITE_TAIL_OK"); "#, ); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let mut execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], argv0: None, env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: None, }) .expect("start JavaScript execution"); let inbound = base64::engine::general_purpose::STANDARD.encode("trigger"); let mut socket_reads = 0usize; let mut writes = Vec::>::new(); let mut held_first_write = None; let mut stdout = Vec::new(); let mut stderr = Vec::new(); // Hold the first 256 KiB completion. Even though _writev() has already // detached its one-byte B batch, no second net.write may cross the bridge. while held_first_write.is_none() { match execution .poll_event_blocking(Duration::from_secs(5)) .expect("poll first serialized write") { Some(JavascriptExecutionEvent::Stdout(bytes)) => stdout.extend(bytes), Some(JavascriptExecutionEvent::Stderr(bytes)) => stderr.extend(bytes), Some(JavascriptExecutionEvent::SignalState { .. }) => {} Some(JavascriptExecutionEvent::SyncRpcRequest(request)) => { if execution .try_service_standalone_module_sync_rpc(&request) .expect("service module sync RPC") { continue; } if request.method == "net.write" { writes.push( request .raw_bytes_args .get(&1) .expect("raw net.write payload") .clone(), ); held_first_write = Some(request.id); continue; } let response = match request.method.as_str() { "net.connect" => json!({ "socketId": 1, "capabilityId": 100, "capabilityGeneration": 1, "localAddress": "127.0.0.1", "localPort": 12345, "localFamily": "IPv4", "remoteAddress": "127.0.0.1", "remotePort": 80, "remoteFamily": "IPv4", }), "net.socket_wait_connect" => json!( "{\"localAddress\":\"127.0.0.1\",\"localPort\":12345,\ \"remoteAddress\":\"127.0.0.1\",\"remotePort\":80}" ), "net.socket_read" => { socket_reads += 1; if socket_reads == 1 { json!(inbound) } else { json!("__agentos_net_timeout__") } } "net.socket_set_read_interest" | "__bench.net_tcp_metrics_reset" | "__bench.net_tcp_metrics_snapshot" => Value::Null, other => panic!( "unexpected RPC before first serialized write {other}: {:?}", request.args ), }; execution .respond_sync_rpc_success(request.id, response) .expect("respond before first serialized write"); } Some(JavascriptExecutionEvent::Exited(code)) => { panic!("write-tail guest exited before first write: {code}") } None => panic!("write-tail guest timed out before first write"), } } let no_second_write_deadline = Instant::now() + Duration::from_millis(200); while Instant::now() < no_second_write_deadline { let remaining = no_second_write_deadline.saturating_duration_since(Instant::now()); match execution .poll_event_blocking(remaining) .expect("prove first write completion gates the next write") { Some(JavascriptExecutionEvent::Stdout(bytes)) => stdout.extend(bytes), Some(JavascriptExecutionEvent::Stderr(bytes)) => stderr.extend(bytes), Some(JavascriptExecutionEvent::SignalState { .. }) => {} Some(JavascriptExecutionEvent::SyncRpcRequest(request)) => { if execution .try_service_standalone_module_sync_rpc(&request) .expect("service module sync RPC") { continue; } assert_ne!( request.method, "net.write", "a second raw write escaped while the first completion was held" ); let response = match request.method.as_str() { "net.socket_read" => json!("__agentos_net_timeout__"), "net.socket_set_read_interest" => Value::Null, other => panic!( "unexpected RPC while first write was held {other}: {:?}", request.args ), }; execution .respond_sync_rpc_success(request.id, response) .expect("respond while first write held"); } Some(JavascriptExecutionEvent::Exited(code)) => { panic!("write-tail guest exited while first write held: {code}") } None => break, } } execution .respond_sync_rpc_success(held_first_write.expect("held first write"), Value::Null) .expect("release first serialized write"); let mut exit_code = None; while exit_code.is_none() { match execution .poll_event_blocking(Duration::from_secs(5)) .expect("poll remaining serialized writes") { Some(JavascriptExecutionEvent::Stdout(bytes)) => stdout.extend(bytes), Some(JavascriptExecutionEvent::Stderr(bytes)) => stderr.extend(bytes), Some(JavascriptExecutionEvent::SignalState { .. }) => {} Some(JavascriptExecutionEvent::SyncRpcRequest(request)) => { if execution .try_service_standalone_module_sync_rpc(&request) .expect("service module sync RPC") { continue; } if request.method == "net.write" { writes.push( request .raw_bytes_args .get(&1) .expect("raw net.write payload") .clone(), ); } let response = match request.method.as_str() { "net.write" | "net.shutdown" | "net.destroy" | "net.socket_set_read_interest" => Value::Null, "net.socket_read" => json!("__agentos_net_timeout__"), other => panic!( "unexpected RPC after first serialized write {other}: {:?}", request.args ), }; execution .respond_sync_rpc_success(request.id, response) .expect("respond after first serialized write"); } Some(JavascriptExecutionEvent::Exited(code)) => exit_code = Some(code), None => panic!( "write-tail guest timed out; writes={:?}, stdout={}, stderr={}", writes.iter().map(Vec::len).collect::>(), String::from_utf8_lossy(&stdout), String::from_utf8_lossy(&stderr), ), } } let stdout = String::from_utf8(stdout).expect("stdout utf8"); let stderr = String::from_utf8(stderr).expect("stderr utf8"); assert_eq!( exit_code, Some(0), "write-tail guest exited non-zero\nstdout: {stdout}\nstderr: {stderr}" ); assert!( stdout.contains("NET_WRITE_TAIL_OK"), "write-tail guest did not observe ordered callbacks: {stdout}\n{stderr}" ); assert_eq!( writes.iter().map(Vec::len).collect::>(), vec![256 * 1024, 44 * 1024, 1], "split writev payloads crossed the bridge out of order" ); assert!(writes[0].iter().all(|byte| *byte == b'A')); assert!(writes[1].iter().all(|byte| *byte == b'A')); assert_eq!(writes[2], b"B"); } fn javascript_execution_v8_net_close_connect_and_accept_wakes_match_node_ordering() { let temp = tempdir().expect("create temp dir"); write_fixture( &temp.path().join("entry.mjs"), r#" import net from "node:net"; const immediate = () => new Promise((resolve) => setImmediate(resolve)); const once = (target, event) => new Promise((resolve) => target.once(event, resolve)); async function assertDestroyedLoopbackDoesNotConnect() { const events = []; const socket = new net.Socket(); const closed = once(socket, "close"); socket.on("connect", () => events.push("connect")); socket.on("ready", () => events.push("ready")); socket.on("close", () => events.push("close")); socket.on("error", (error) => { throw error; }); socket.connect({ host: "127.0.0.1", port: 81 }); socket.destroy(); await closed; await immediate(); if (events.join(",") !== "close") { throw new Error(`destroyed loopback socket emitted stale lifecycle events: ${events}`); } } async function assertBufferedEof(label, destroyBeforeDrain) { const events = []; const socket = new net.Socket(); const connected = once(socket, "connect"); const readable = once(socket, "readable"); const closed = once(socket, "close"); socket.on("end", () => events.push("end")); socket.on("close", () => events.push("close")); socket.on("error", (error) => { throw error; }); socket.connect({ host: "127.0.0.1", port: 80 }); await connected; socket.end(); await readable; await immediate(); if (events.includes("close")) { throw new Error(`${label}: close emitted while EOF data remained paused`); } if (destroyBeforeDrain) { socket.destroy(); await closed; if (events.filter((event) => event === "close").length !== 1) { throw new Error(`${label}: destroy must emit close exactly once: ${events}`); } return; } const chunk = socket.read(); events.push(`read:${chunk?.toString()}`); await closed; const readIndex = events.indexOf(`read:buffered-1`); const endIndex = events.indexOf("end"); const closeIndex = events.indexOf("close"); if (readIndex === -1 || endIndex < readIndex || closeIndex < endIndex) { throw new Error(`${label}: expected buffered read -> end -> close, got ${events}`); } } async function assertAcceptWakesCoalesce() { globalThis.__agentOSNetBridgeMetrics.enable(); globalThis.__agentOSNetBridgeMetrics.reset(); const server = net.createServer(); server.on("error", (error) => { throw error; }); const snapshot = await new Promise((resolve) => { server.listen(0, "127.0.0.1", () => { for (let index = 0; index < 32; index += 1) { if (!globalThis._agentOSReadyDispatch(77n, 1n, 1)) { throw new Error("server readiness target was not registered"); } } setImmediate(() => { const metrics = globalThis.__agentOSNetBridgeMetrics.snapshot(); server.close(() => resolve(metrics)); }); }); }); if (snapshot.acceptEventWakeups !== 1 || snapshot.acceptWakeAlreadyQueued !== 31) { throw new Error(`accept wakes were not capacity-one: ${JSON.stringify(snapshot)}`); } if (snapshot.acceptRawCalls > 2) { throw new Error(`coalesced accept wakes over-polled: ${JSON.stringify(snapshot)}`); } } await assertDestroyedLoopbackDoesNotConnect(); await assertBufferedEof("drain", false); await assertBufferedEof("destroy", true); await assertAcceptWakesCoalesce(); console.log("NET_LIFECYCLE_WAKE_OK"); "#, ); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let mut execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], argv0: None, env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: None, }) .expect("start JavaScript execution"); let mut next_socket_id = 0_u64; let mut socket_reads = BTreeMap::::new(); let mut socket_destroys = BTreeMap::::new(); let mut accept_calls = 0_usize; let mut stdout = Vec::new(); let mut stderr = Vec::new(); let exit_code = loop { match execution .poll_event_blocking(Duration::from_secs(5)) .expect("poll net lifecycle bridge event") { Some(JavascriptExecutionEvent::Stdout(bytes)) => stdout.extend(bytes), Some(JavascriptExecutionEvent::Stderr(bytes)) => stderr.extend(bytes), Some(JavascriptExecutionEvent::SignalState { .. }) => {} Some(JavascriptExecutionEvent::SyncRpcRequest(request)) => { if execution .try_service_standalone_module_sync_rpc(&request) .expect("service module sync RPC") { continue; } let request_id = request.id; let response = match request.method.as_str() { "net.connect" => { let port = request.args[0]["port"] .as_u64() .expect("net.connect port"); if port == 81 { json!({ "loopbackHttpTarget": { "processId": "proc", "serverId": 1 }, "localAddress": "127.0.0.1", "localPort": 12346, "localFamily": "IPv4", "remoteAddress": "127.0.0.1", "remotePort": 81, "remoteFamily": "IPv4", }) } else { next_socket_id += 1; json!({ "socketId": next_socket_id, "capabilityId": 100 + next_socket_id, "capabilityGeneration": 1, "localAddress": "127.0.0.1", "localPort": 12346 + next_socket_id, "localFamily": "IPv4", "remoteAddress": "127.0.0.1", "remotePort": 80, "remoteFamily": "IPv4", }) } } "net.socket_wait_connect" => { let socket_id = request.args[0] .as_u64() .expect("wait-connect socket id"); json!(format!( "{{\"localAddress\":\"127.0.0.1\",\"localPort\":{},\"remoteAddress\":\"127.0.0.1\",\"remotePort\":80}}", 12346 + socket_id )) } "net.socket_read" => { let socket_id = request.args[0] .as_u64() .expect("read socket id"); let reads = socket_reads.entry(socket_id).or_default(); *reads += 1; if *reads == 1 { json!(base64::engine::general_purpose::STANDARD .encode(format!("buffered-{socket_id}"))) } else { Value::Null } } "net.destroy" => { let socket_id = request.args[0] .as_u64() .expect("destroy socket id"); *socket_destroys.entry(socket_id).or_default() += 1; Value::Null } "net.listen" => json!({ "serverId": 1, "capabilityId": 77, "capabilityGeneration": 1, "address": { "localAddress": "127.0.0.1", "localPort": 23456, "localFamily": "IPv4", }, }), "net.server_accept" => { accept_calls += 1; json!("__agentos_net_timeout__") } "net.server_close" | "net.shutdown" | "net.socket_set_read_interest" | "__bench.net_tcp_metrics_reset" | "__bench.net_tcp_metrics_snapshot" => Value::Null, other => panic!("unexpected net lifecycle RPC {other}: {:?}", request.args), }; execution .respond_sync_rpc_success(request_id, response) .expect("respond to net lifecycle RPC"); } Some(JavascriptExecutionEvent::Exited(exit_code)) => break exit_code, None => panic!( "net lifecycle execution timed out; reads={socket_reads:?}, destroys={socket_destroys:?}, accepts={accept_calls}, stdout={}, stderr={}", String::from_utf8_lossy(&stdout), String::from_utf8_lossy(&stderr), ), } }; let stdout = String::from_utf8(stdout).expect("stdout utf8"); let stderr = String::from_utf8(stderr).expect("stderr utf8"); assert_eq!( exit_code, 0, "guest exited non-zero\nstdout: {stdout}\nstderr: {stderr}" ); assert!( stdout.contains("NET_LIFECYCLE_WAKE_OK"), "guest did not complete net lifecycle fixture: {stdout}\n{stderr}" ); assert_eq!( socket_destroys, BTreeMap::from([(1, 1), (2, 1)]), "drain and explicit destroy must each release exactly once" ); assert_eq!( accept_calls, 2, "initial accept plus one coalesced pending retry were expected" ); } fn javascript_execution_v8_dynamic_import_accepts_file_urls() { let temp = tempdir().expect("create temp dir"); write_fixture( &temp.path().join("dep.mjs"), r#" export default { value: "ok" }; "#, ); write_fixture( &temp.path().join("entry.mjs"), r#" const href = new URL("./dep.mjs", import.meta.url).href; const module = await import(href); console.log(JSON.stringify({ href, value: module.default.value })); "#, ); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: None, }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); let stderr = String::from_utf8(result.stderr).expect("stderr utf8"); assert_eq!(result.exit_code, 0, "unexpected stderr: {stderr}"); assert!(stderr.is_empty(), "unexpected stderr: {stderr}"); let output: Value = serde_json::from_slice(&result.stdout).expect("parse stdout JSON"); assert_eq!( output, json!({ "href": "file:///root/dep.mjs", "value": "ok", }) ); } fn javascript_execution_v8_wasm_instantiate_streaming_never_hangs() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" const bytes = new Uint8Array([ 0,97,115,109,1,0,0,0,1,5,1,96,0,1,127,3,2,1,0,7,12,1,8,102,111,114,116,121,84,119,111,0,0,10,6,1,4,0,65,42,11, ]); const response = new Response(bytes, { headers: { "content-type": "application/wasm" }, }); let outcome = "pending"; try { const result = await WebAssembly.instantiateStreaming(Promise.resolve(response), {}); if (typeof result?.instance?.exports?.fortyTwo !== "function") { throw new Error("instantiateStreaming() did not return an exported function"); } if (result.instance.exports.fortyTwo() !== 42) { throw new Error(`unexpected wasm export value: ${result.instance.exports.fortyTwo()}`); } outcome = "ok"; } catch (error) { if (error?.code !== "ERR_NOT_IMPLEMENTED") { throw error; } outcome = error.code; } console.log(outcome); "#, )), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); let stdout = String::from_utf8(result.stdout).expect("stdout utf8"); let stderr = String::from_utf8(result.stderr).expect("stderr utf8"); assert_eq!(result.exit_code, 0, "stdout:\n{stdout}\nstderr:\n{stderr}"); assert!(stderr.is_empty(), "unexpected stderr: {stderr}"); let outcome = stdout.trim(); assert!( outcome == "ok" || outcome == "ERR_NOT_IMPLEMENTED", "unexpected instantiateStreaming outcome: {outcome}" ); } fn javascript_execution_v8_structured_clone_rebinds_to_sandbox_realm() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" const source = new Uint8Array([1, 2, 3, 4]); const typed = structuredClone(source, { transfer: [source.buffer] }); const map = structuredClone(new Map([["a", 1]])); const date = structuredClone(new Date(0)); const regexSource = /agent/gi; regexSource.lastIndex = 2; const regex = structuredClone(regexSource); const dataView = structuredClone(new DataView(new Uint8Array([9, 8, 7, 6]).buffer, 1, 2)); const circular = { label: "loop" }; circular.self = circular; const circularClone = structuredClone(circular); const nested = structuredClone({ list: [new Uint16Array([5, 6])], set: new Set(["x"]), }); let functionErrorName = null; try { structuredClone(() => {}); } catch (error) { functionErrorName = error?.name ?? String(error); } console.log(JSON.stringify({ typed: { instanceof: typed instanceof Uint8Array, sameConstructor: typed.constructor === Uint8Array, constructorName: typed.constructor?.name, length: typed.length, first: typed[0], }, map: { instanceof: map instanceof Map, value: map.get("a"), }, date: { instanceof: date instanceof Date, value: date.valueOf(), }, regex: { instanceof: regex instanceof RegExp, source: regex.source, flags: regex.flags, lastIndex: regex.lastIndex, }, dataView: { instanceof: dataView instanceof DataView, byteLength: dataView.byteLength, first: dataView.getUint8(0), }, circular: circularClone !== circular && circularClone.self === circularClone, nested: { typedArrayInstanceof: nested.list[0] instanceof Uint16Array, setInstanceof: nested.set instanceof Set, setValue: nested.set.has("x"), }, functionErrorName, })); "#, )), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); let stdout = String::from_utf8_lossy(&result.stdout); let stderr = String::from_utf8_lossy(&result.stderr); assert_eq!(result.exit_code, 0, "stdout:\n{stdout}\nstderr:\n{stderr}"); assert!(result.stderr.is_empty(), "unexpected stderr: {stderr}"); let output: Value = serde_json::from_slice(&result.stdout).expect("parse stdout JSON"); assert_eq!( output, json!({ "typed": { "instanceof": true, "sameConstructor": true, "constructorName": "Uint8Array", "length": 4, "first": 1, }, "map": { "instanceof": true, "value": 1, }, "date": { "instanceof": true, "value": 0, }, "regex": { "instanceof": true, "source": "agent", "flags": "gi", "lastIndex": 2, }, "dataView": { "instanceof": true, "byteLength": 2, "first": 8, }, "circular": true, "nested": { "typedArrayInstanceof": true, "setInstanceof": true, "setValue": true, }, "functionErrorName": "DataCloneError", }) ); } // --------------------------------------------------------------------------- // jsRuntime platform / module-resolution coverage // --------------------------------------------------------------------------- fn js_runtime_env(pairs: &[(&str, &str)]) -> BTreeMap { pairs .iter() .map(|(key, value)| (String::from(*key), String::from(*value))) .collect() } fn run_js_runtime_guest( env: BTreeMap, inline_code: &str, ) -> JavascriptExecutionResult { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env, cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(inline_code.to_owned()), }) .expect("start JavaScript execution"); assert!( execution.uses_shared_v8_runtime(), "guest JS must run inside the shared V8 runtime" ); execution.wait().expect("wait for JavaScript execution") } /// Run guest code that throws on any policy violation; a clean exit means every /// assertion held. (Lower tiers have no `console`, so guest code signals failure /// by throwing, not by printing.) fn assert_js_runtime_guest_ok(env: BTreeMap, inline_code: &str) { let result = run_js_runtime_guest(env, inline_code); let stdout = String::from_utf8_lossy(&result.stdout); let stderr = String::from_utf8_lossy(&result.stderr); assert_eq!( result.exit_code, 0, "guest jsRuntime assertion failed\nstdout:\n{stdout}\nstderr:\n{stderr}" ); } fn js_runtime_node_platform_keeps_full_node_surface() { // No jsRuntime env == node platform: the full Node surface stays intact and // builtins remain importable (positive control for the scrub tests below). assert_js_runtime_guest_ok( BTreeMap::new(), r#" if (typeof process === "undefined") throw new Error("process missing"); if (typeof Buffer === "undefined") throw new Error("Buffer missing"); if (typeof require === "undefined") throw new Error("require missing"); if (typeof fetch === "undefined") throw new Error("fetch missing"); const fs = await import("node:fs"); if (typeof fs.readFileSync !== "function") throw new Error("node:fs not usable"); "#, ); } fn js_runtime_bare_platform_strips_all_host_globals() { // Pentest: nothing host-provided survives, and it cannot be reconstructed via // constructors / property-name tricks. Language + WebAssembly remain. assert_js_runtime_guest_ok( js_runtime_env(&[ ("AGENTOS_JS_PLATFORM", "bare"), ("AGENTOS_JS_BUILTIN_ALLOWLIST", "[]"), ]), r#" const banned = [ "process","Buffer","require","module","exports","__dirname","__filename","global", "fetch","Headers","Request","Response","URL","URLSearchParams","crypto","structuredClone", "console","setTimeout","setInterval","setImmediate","queueMicrotask", "_processConfig","__agentOSProcessConfigEnv", ]; for (const name of banned) { if (typeof globalThis[name] !== "undefined") throw new Error("leaked global: " + name); if (Object.prototype.hasOwnProperty.call(globalThis, name) && globalThis[name] !== undefined) { throw new Error("leaked own prop: " + name); } } // process must not be reachable through the Function constructor either. try { const f = (function(){}).constructor("return typeof process")(); if (f !== "undefined") throw new Error("process reachable via Function ctor"); } catch (e) { if (String(e).includes("reachable")) throw e; } for (const g of ["JSON","Math","Promise","WebAssembly","Object","Array","Reflect"]) { if (typeof globalThis[g] === "undefined") throw new Error("language global missing: " + g); } // The allow-list plumbing must leave no reachable global: the shim calls the // one-shot init fn and deletes it, and the old reachable allow-list is gone. if (typeof globalThis.__agentOSBuiltinAllowlist !== "undefined") { throw new Error("__agentOSBuiltinAllowlist is still reachable"); } if (typeof globalThis.__agentOSInitJsRuntime !== "undefined") { throw new Error("__agentOSInitJsRuntime is still reachable"); } "#, ); } fn js_runtime_browser_platform_exposes_web_without_node() { assert_js_runtime_guest_ok( js_runtime_env(&[ ("AGENTOS_JS_PLATFORM", "browser"), ("AGENTOS_JS_BUILTIN_ALLOWLIST", "[]"), ]), r#" for (const name of ["process","Buffer","require","module","_processConfig","__agentOSProcessConfigEnv"]) { if (typeof globalThis[name] !== "undefined") throw new Error("node global leaked: " + name); } for (const name of ["fetch","URL","TextEncoder","TextDecoder","structuredClone","console","setTimeout"]) { if (typeof globalThis[name] === "undefined") throw new Error("web/universal global missing: " + name); } if (typeof crypto === "undefined" || typeof crypto.subtle === "undefined") { throw new Error("WebCrypto missing"); } // crypto must be WebCrypto, not the node:crypto module. if (typeof crypto.randomBytes === "function" || typeof crypto.createHash === "function") { throw new Error("node:crypto leaked through globalThis.crypto"); } // node:* builtins are denied under browser by the bridge gate, which throws // an error with code === "ERR_ACCESS_DENIED". let code = null; try { await import("node:fs"); } catch (e) { code = e && e.code; } if (code !== "ERR_ACCESS_DENIED") { throw new Error("expected ERR_ACCESS_DENIED, got " + code); } "#, ); } fn js_runtime_neutral_platform_drops_web_keeps_universal() { assert_js_runtime_guest_ok( js_runtime_env(&[ ("AGENTOS_JS_PLATFORM", "neutral"), ("AGENTOS_JS_BUILTIN_ALLOWLIST", "[]"), ]), r#" for (const name of ["process","Buffer","require","fetch","URL","crypto","structuredClone"]) { if (typeof globalThis[name] !== "undefined") throw new Error("global leaked at neutral: " + name); } for (const name of ["console","setTimeout","queueMicrotask","TextEncoder","WebAssembly"]) { if (typeof globalThis[name] === "undefined") throw new Error("universal global missing: " + name); } "#, ); } fn js_runtime_module_resolution_none_denies_all_imports() { // AGENTOS_JS_BUILTIN_ALLOWLIST=[] denies builtins; moduleResolution=none denies // bare AND relative specifiers via static import, dynamic import, and require. assert_js_runtime_guest_ok( js_runtime_env(&[ ("AGENTOS_JS_MODULE_RESOLUTION", "none"), ("AGENTOS_JS_BUILTIN_ALLOWLIST", "[]"), ]), r#" let n = 0; try { await import("node:fs"); } catch { n++; } try { await import("lodash"); } catch { n++; } try { await import("./local.mjs"); } catch { n++; } if (n !== 3) throw new Error("expected all imports denied, denied=" + n); "#, ); } fn js_runtime_module_resolution_relative_allows_local_denies_bare() { // Write a local module into the guest cwd, then assert relative resolves while // bare + builtin do not. let temp = tempdir().expect("create temp dir"); std::fs::write(temp.path().join("local.mjs"), "export const ok = 42;\n") .expect("write local module"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: js_runtime_env(&[ ("AGENTOS_JS_MODULE_RESOLUTION", "relative"), ("AGENTOS_JS_BUILTIN_ALLOWLIST", "[]"), ]), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" const local = await import("./local.mjs"); if (local.ok !== 42) throw new Error("relative import failed"); let bareDenied = false; try { await import("lodash"); } catch { bareDenied = true; } if (!bareDenied) throw new Error("bare specifier was not denied under relative"); let builtinDenied = false; try { await import("node:fs"); } catch { builtinDenied = true; } if (!builtinDenied) throw new Error("node:fs was not denied under relative"); "#, )), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); let stderr = String::from_utf8_lossy(&result.stderr); assert_eq!( result.exit_code, 0, "relative-resolution test failed\nstderr:\n{stderr}" ); } fn js_runtime_node_platform_allow_list_restricts_builtins() { // platform=node with an explicit allow-list of just "path": node:path resolves, // node:fs is denied. assert_js_runtime_guest_ok( js_runtime_env(&[("AGENTOS_JS_BUILTIN_ALLOWLIST", "[\"path\"]")]), r#" const path = await import("node:path"); if (typeof path.join !== "function") throw new Error("node:path should be allowed"); let denied = false; try { await import("node:fs"); } catch { denied = true; } if (!denied) throw new Error("node:fs should be denied when not in the allow-list"); "#, ); } fn js_runtime_browser_loads_cjs_npm_package() { // Regression guard: the per-execution shim must NOT scrub the internal CJS // helpers under browser, so an npm CommonJS package still loads via the // ESM->CJS interop path. node resolution is the browser default (do not set // AGENTOS_JS_MODULE_RESOLUTION). let temp = tempdir().expect("create temp dir"); let pkg_dir = temp.path().join("node_modules").join("demo-pkg"); std::fs::create_dir_all(&pkg_dir).expect("create demo-pkg dir"); std::fs::write( pkg_dir.join("package.json"), r#"{"name":"demo-pkg","version":"1.0.0","main":"index.js"}"#, ) .expect("write package.json"); std::fs::write( pkg_dir.join("index.js"), "module.exports = { answer: 42 };\n", ) .expect("write index.js"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: js_runtime_env(&[ ("AGENTOS_JS_PLATFORM", "browser"), ("AGENTOS_JS_BUILTIN_ALLOWLIST", "[]"), ]), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" const pkg = await import("demo-pkg"); const v = pkg.answer ?? pkg.default?.answer; if (v !== 42) throw new Error("npm CJS import failed: " + JSON.stringify(v)); "#, )), }) .expect("start JavaScript execution"); let result = execution.wait().expect("wait for JavaScript execution"); let stderr = String::from_utf8_lossy(&result.stderr); assert_eq!( result.exit_code, 0, "browser npm CJS load test failed\nstderr:\n{stderr}" ); } fn js_runtime_browser_fetch_is_callable() { // fetch must survive the browser scrub and be wired to the kernel socket // table: calling it returns a thenable that rejects on an unreachable host // rather than throwing synchronously or being undefined. assert_js_runtime_guest_ok( js_runtime_env(&[ ("AGENTOS_JS_PLATFORM", "browser"), ("AGENTOS_JS_BUILTIN_ALLOWLIST", "[]"), ]), r#" if (typeof fetch !== "function") throw new Error("fetch missing"); let threwSync = false, settled = "none"; let p; try { p = fetch("http://127.0.0.1:1/"); } catch { threwSync = true; } if (threwSync) throw new Error("fetch threw synchronously"); if (!p || typeof p.then !== "function") throw new Error("fetch did not return a promise"); try { await p; settled = "resolved"; } catch { settled = "rejected"; } if (settled !== "rejected") { throw new Error("expected fetch to reject on unreachable host, got " + settled); } "#, ); } // SE-EXEC-04 (B.2 / F-001): with an OPT-IN CPU-time budget set, a CPU-bound // `while (true) {}` guest must be terminated by the TRUE CPU-time watchdog // instead of pinning a core on the shared, slot-bounded V8 runtime and starving // peers. The watchdog samples the execution thread's per-thread CPU clock, so a // tight busy loop burns its budget quickly and is killed. // // This is the BOUNDED variant: it sets a small explicit CPU budget via typed // `limits.jsRuntime.cpuTimeLimitMs` so the watchdog fires fast. The guest run is // fenced behind a worker thread + recv timeout so a regression surfaces as a // clear failure instead of a CI hang. fn javascript_infinite_loop_is_terminated_by_cpu_watchdog() { let (tx, rx) = mpsc::channel::<(i32, String, String)>(); thread::spawn(move || { let temp = match tempdir() { Ok(temp) => temp, Err(error) => { let _ = tx.send((-1, String::new(), format!("tempdir failed: {error}"))); return; } }; let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine.start_execution(StartJavascriptExecutionRequest { vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], // Small bounded budget so the watchdog terminates the runaway fast. env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from("while (true) {}\n")), limits: JavascriptExecutionLimits { cpu_time_limit_ms: Some(750), ..Default::default() }, argv0: None, guest_runtime: Default::default(), }); match execution { Ok(execution) => match execution.wait() { Ok(result) => { let stdout = String::from_utf8_lossy(&result.stdout).into_owned(); let stderr = String::from_utf8_lossy(&result.stderr).into_owned(); let _ = tx.send((result.exit_code, stdout, stderr)); } Err(error) => { let _ = tx.send((-1, String::new(), format!("wait failed: {error}"))); } }, Err(error) => { let _ = tx.send((-1, String::new(), format!("start failed: {error}"))); } } }); match rx.recv_timeout(Duration::from_secs(20)) { Ok((exit_code, stdout, stderr)) => { // The watchdog must terminate the loop with a nonzero exit code. assert_ne!( exit_code, 0, "infinite loop returned a clean exit instead of being terminated by the CPU watchdog: stdout={stdout} stderr={stderr}" ); // And it must be attributed to the CPU-time budget specifically. assert!( stderr.contains("ERR_SCRIPT_CPU_BUDGET_EXCEEDED") || stderr.contains("CPU-time budget"), "termination was not attributed to the CPU-time budget: stdout={stdout} stderr={stderr}" ); } Err(_) => { // No result within the budget => the watchdog never armed/fired and the // CPU-bound guest ran unbounded. This is exactly the F-001 break. panic!( "infinite-loop guest was NOT terminated by the CPU watchdog \ (wait() never returned within the bounded test budget => unbounded CPU runaway)" ); } } } // SE-EXEC-04 (F-001) CRITICAL NEGATIVE: with a CPU-time budget set, a guest that // mostly AWAITS (timers / idle) past the budget window must NOT be killed. The // watchdog measures TRUE active-JS CPU time (per-thread CPU clock), which does not // advance while the thread is parked awaiting a timer, so idle/await is excluded. // // The guest sleeps for ~1.5s of wall time while the CPU budget is only 300ms. A // wall-clock timer would have killed it; the CPU-time budget must not, because the // guest burns almost no CPU. This is what proves I/O/idle wait is excluded. fn javascript_awaiting_guest_is_not_killed_by_cpu_budget() { let (tx, rx) = mpsc::channel::<(i32, String, String)>(); thread::spawn(move || { let temp = match tempdir() { Ok(temp) => temp, Err(error) => { let _ = tx.send((-1, String::new(), format!("tempdir failed: {error}"))); return; } }; let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js-await"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine.start_execution(StartJavascriptExecutionRequest { vm_id: String::from("vm-js-await"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], // CPU budget (300ms) much SMALLER than the wall time the guest spends // awaiting (~1.5s). A correct CPU-time budget excludes the idle wait. env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( "await new Promise((resolve) => setTimeout(resolve, 1500));\n\ console.log('awaited-ok');\n", )), limits: JavascriptExecutionLimits { cpu_time_limit_ms: Some(300), ..Default::default() }, argv0: None, guest_runtime: Default::default(), }); match execution { Ok(execution) => match execution.wait() { Ok(result) => { let stdout = String::from_utf8_lossy(&result.stdout).into_owned(); let stderr = String::from_utf8_lossy(&result.stderr).into_owned(); let _ = tx.send((result.exit_code, stdout, stderr)); } Err(error) => { let _ = tx.send((-1, String::new(), format!("wait failed: {error}"))); } }, Err(error) => { let _ = tx.send((-1, String::new(), format!("start failed: {error}"))); } } }); match rx.recv_timeout(Duration::from_secs(20)) { Ok((exit_code, stdout, stderr)) => { assert!( !stderr.contains("ERR_SCRIPT_CPU_BUDGET_EXCEEDED") && !stderr.contains("CPU-time budget"), "an awaiting (low-CPU) guest was wrongly killed by the CPU budget; idle/await \ must be excluded: exit_code={exit_code} stdout={stdout} stderr={stderr}" ); assert_eq!( exit_code, 0, "awaiting guest should complete cleanly (idle excluded from CPU budget): \ stdout={stdout} stderr={stderr}" ); assert!( stdout.contains("awaited-ok"), "awaiting guest did not run to completion: stdout={stdout} stderr={stderr}" ); } Err(_) => { panic!( "awaiting guest never produced a result within the bounded test window \ (unexpected hang)" ); } } } // SE-EXEC-04 (F-001): with no explicit `limits.jsRuntime.cpuTimeLimitMs`, the // CPU-budget watchdog uses the bounded default. A short CPU-bound guest still // runs to completion because it stays below that generous active-CPU budget. We // deliberately use a SHORT, self-terminating busy loop (not an infinite one) so // the test cannot hang if the guard regresses. fn javascript_default_cpu_budget_allows_short_cpu_work() { let (tx, rx) = mpsc::channel::<(i32, String, String)>(); thread::spawn(move || { let temp = match tempdir() { Ok(temp) => temp, Err(error) => { let _ = tx.send((-1, String::new(), format!("tempdir failed: {error}"))); return; } }; let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js-nolimit"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine.start_execution(StartJavascriptExecutionRequest { vm_id: String::from("vm-js-nolimit"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], // No CPU-limit env: the watchdog uses the bounded default. env: BTreeMap::new(), cwd: temp.path().to_path_buf(), // ~600ms busy loop: long enough to have tripped the old 30s default's // removal is irrelevant, but short enough to always finish; importantly // it self-terminates so the test never hangs. wasm_module_bytes: None, inline_code: Some(String::from( "const end = Date.now() + 600;\n\ let n = 0;\n\ while (Date.now() < end) { n++; }\n\ console.log('busy-done', n > 0);\n", )), limits: Default::default(), argv0: None, guest_runtime: Default::default(), }); match execution { Ok(execution) => match execution.wait() { Ok(result) => { let stdout = String::from_utf8_lossy(&result.stdout).into_owned(); let stderr = String::from_utf8_lossy(&result.stderr).into_owned(); let _ = tx.send((result.exit_code, stdout, stderr)); } Err(error) => { let _ = tx.send((-1, String::new(), format!("wait failed: {error}"))); } }, Err(error) => { let _ = tx.send((-1, String::new(), format!("start failed: {error}"))); } } }); match rx.recv_timeout(Duration::from_secs(20)) { Ok((exit_code, stdout, stderr)) => { assert!( !stderr.contains("ERR_SCRIPT_CPU_BUDGET_EXCEEDED") && !stderr.contains("CPU-time budget"), "guest was CPU-limited despite staying below the default CPU budget: \ exit_code={exit_code} stdout={stdout} stderr={stderr}" ); assert_eq!( exit_code, 0, "short busy loop should exit cleanly under the default CPU budget: \ stdout={stdout} stderr={stderr}" ); assert!( stdout.contains("busy-done true"), "busy loop did not run to completion under the default CPU budget: \ stdout={stdout} stderr={stderr}" ); } Err(_) => { panic!( "no-limit guest never produced a result within the bounded test window \ (unexpected hang)" ); } } } // WALL-CLOCK BACKSTOP (opt-in, complements the CPU-time budget): with // typed `limits.jsRuntime.wallClockLimitMs` set, a guest that exceeds the wall-clock limit // must be terminated and the result attributed to the WALL-CLOCK reason. Crucially, // the wall-clock limit counts elapsed REAL time INCLUDING idle/await, so a guest // that merely AWAITS past the limit (burning almost no CPU) is still killed — this // is exactly what the CPU-time budget does NOT do. The guest awaits ~1.5s while the // wall-clock limit is only 300ms, and NO CPU budget is set, so only the wall-clock // guard can fire. // // BOUNDED variant: small explicit wall-clock limit so the backstop fires fast; the // run is fenced behind a worker thread + recv timeout so a regression surfaces as a // clear failure instead of a CI hang. fn javascript_awaiting_guest_is_terminated_by_wall_clock_backstop() { let (tx, rx) = mpsc::channel::<(i32, String, String)>(); thread::spawn(move || { let temp = match tempdir() { Ok(temp) => temp, Err(error) => { let _ = tx.send((-1, String::new(), format!("tempdir failed: {error}"))); return; } }; let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js-wallclock"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine.start_execution(StartJavascriptExecutionRequest { vm_id: String::from("vm-js-wallclock"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], // Wall-clock limit (300ms) much SMALLER than the wall time the guest // spends awaiting (~1.5s). The wall-clock backstop counts idle/await, so // it must terminate the guest even though it burns almost no CPU. No CPU // budget is set, proving the two knobs are independent. env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( "await new Promise((resolve) => setTimeout(resolve, 1500));\n\ console.log('awaited-ok');\n", )), limits: JavascriptExecutionLimits { wall_clock_limit_ms: Some(300), ..Default::default() }, argv0: None, guest_runtime: Default::default(), }); match execution { Ok(execution) => match execution.wait() { Ok(result) => { let stdout = String::from_utf8_lossy(&result.stdout).into_owned(); let stderr = String::from_utf8_lossy(&result.stderr).into_owned(); let _ = tx.send((result.exit_code, stdout, stderr)); } Err(error) => { let _ = tx.send((-1, String::new(), format!("wait failed: {error}"))); } }, Err(error) => { let _ = tx.send((-1, String::new(), format!("start failed: {error}"))); } } }); match rx.recv_timeout(Duration::from_secs(20)) { Ok((exit_code, stdout, stderr)) => { assert_ne!( exit_code, 0, "awaiting guest returned a clean exit instead of being terminated by the \ wall-clock backstop: stdout={stdout} stderr={stderr}" ); // Must be attributed to the WALL-CLOCK reason specifically (not CPU budget). assert!( stderr.contains("ERR_SCRIPT_WALL_CLOCK_EXCEEDED") || stderr.contains("wall-clock limit"), "termination was not attributed to the wall-clock backstop: \ stdout={stdout} stderr={stderr}" ); assert!( !stderr.contains("ERR_SCRIPT_CPU_BUDGET_EXCEEDED") && !stderr.contains("CPU-time budget"), "wall-clock termination was wrongly attributed to the CPU budget: \ stdout={stdout} stderr={stderr}" ); assert!( !stdout.contains("awaited-ok"), "guest ran to completion despite exceeding the wall-clock limit: \ stdout={stdout} stderr={stderr}" ); } Err(_) => { panic!( "awaiting guest was NOT terminated by the wall-clock backstop \ (wait() never returned within the bounded test window => backstop never fired)" ); } } } // WALL-CLOCK / CPU-BUDGET INDEPENDENCE: setting ONLY the CPU budget must NOT impose // any wall-clock limit. A guest that awaits past a window which the wall-clock limit // (if it were armed) would have killed, but burns almost no CPU, must run to // completion when only `limits.jsRuntime.cpuTimeLimitMs` is set. This confirms the CPU // budget does not secretly behave like a wall-clock timer and that the knobs are // independent. fn javascript_cpu_budget_only_does_not_impose_wall_clock_limit() { let (tx, rx) = mpsc::channel::<(i32, String, String)>(); thread::spawn(move || { let temp = match tempdir() { Ok(temp) => temp, Err(error) => { let _ = tx.send((-1, String::new(), format!("tempdir failed: {error}"))); return; } }; let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js-cpu-only"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine.start_execution(StartJavascriptExecutionRequest { vm_id: String::from("vm-js-cpu-only"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], // Only the CPU budget is set (300ms). The guest awaits ~1.2s of wall // time but burns no CPU, so neither guard should fire — proving the CPU // budget alone does NOT arm a wall-clock limit. env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( "await new Promise((resolve) => setTimeout(resolve, 1200));\n\ console.log('cpu-only-ok');\n", )), limits: JavascriptExecutionLimits { cpu_time_limit_ms: Some(300), ..Default::default() }, argv0: None, guest_runtime: Default::default(), }); match execution { Ok(execution) => match execution.wait() { Ok(result) => { let stdout = String::from_utf8_lossy(&result.stdout).into_owned(); let stderr = String::from_utf8_lossy(&result.stderr).into_owned(); let _ = tx.send((result.exit_code, stdout, stderr)); } Err(error) => { let _ = tx.send((-1, String::new(), format!("wait failed: {error}"))); } }, Err(error) => { let _ = tx.send((-1, String::new(), format!("start failed: {error}"))); } } }); match rx.recv_timeout(Duration::from_secs(20)) { Ok((exit_code, stdout, stderr)) => { assert!( !stderr.contains("ERR_SCRIPT_WALL_CLOCK_EXCEEDED") && !stderr.contains("wall-clock limit"), "a wall-clock limit fired despite only the CPU budget being set; the knobs \ must be independent: exit_code={exit_code} stdout={stdout} stderr={stderr}" ); assert_eq!( exit_code, 0, "awaiting guest should complete cleanly with only a CPU budget set \ (idle excluded, no wall-clock limit): stdout={stdout} stderr={stderr}" ); assert!( stdout.contains("cpu-only-ok"), "guest did not run to completion with only a CPU budget set: \ stdout={stdout} stderr={stderr}" ); } Err(_) => { panic!( "cpu-budget-only guest never produced a result within the bounded test window \ (unexpected hang)" ); } } } // WALL-CLOCK OPT-IN: with no explicit `limits.jsRuntime.wallClockLimitMs`, there is no // wall-clock limit. A guest that awaits well past any default a former // wall-clock timer might have imposed must run to completion. This guards the // requirement that long-lived ACP adapters (which run indefinitely on // wall-clock) are never killed by a wall-clock default. The default CPU budget // remains armed but excludes idle/await time. fn javascript_no_time_limit_when_neither_env_set() { let (tx, rx) = mpsc::channel::<(i32, String, String)>(); thread::spawn(move || { let temp = match tempdir() { Ok(temp) => temp, Err(error) => { let _ = tx.send((-1, String::new(), format!("tempdir failed: {error}"))); return; } }; let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js-notimelimit"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine.start_execution(StartJavascriptExecutionRequest { vm_id: String::from("vm-js-notimelimit"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], // No wall-clock env: no wall-clock guard is armed. The default CPU // budget excludes this idle await. env: BTreeMap::new(), cwd: temp.path().to_path_buf(), // Awaits ~1.2s, then exits cleanly. Self-terminating so the test cannot // hang even if (incorrectly) no limit were enforced. wasm_module_bytes: None, inline_code: Some(String::from( "await new Promise((resolve) => setTimeout(resolve, 1200));\n\ console.log('no-limit-ok');\n", )), limits: Default::default(), argv0: None, guest_runtime: Default::default(), }); match execution { Ok(execution) => match execution.wait() { Ok(result) => { let stdout = String::from_utf8_lossy(&result.stdout).into_owned(); let stderr = String::from_utf8_lossy(&result.stderr).into_owned(); let _ = tx.send((result.exit_code, stdout, stderr)); } Err(error) => { let _ = tx.send((-1, String::new(), format!("wait failed: {error}"))); } }, Err(error) => { let _ = tx.send((-1, String::new(), format!("start failed: {error}"))); } } }); match rx.recv_timeout(Duration::from_secs(20)) { Ok((exit_code, stdout, stderr)) => { assert!( !stderr.contains("ERR_SCRIPT_WALL_CLOCK_EXCEEDED") && !stderr.contains("wall-clock limit") && !stderr.contains("ERR_SCRIPT_CPU_BUDGET_EXCEEDED") && !stderr.contains("CPU-time budget"), "a wall-clock or CPU limit fired despite this idle await staying below the default CPU budget: \ exit_code={exit_code} stdout={stdout} stderr={stderr}" ); assert_eq!( exit_code, 0, "awaiting guest should complete cleanly when no wall-clock limit is set: \ stdout={stdout} stderr={stderr}" ); assert!( stdout.contains("no-limit-ok"), "guest did not run to completion with no wall-clock limit set: \ stdout={stdout} stderr={stderr}" ); } Err(_) => { panic!( "no-limit guest never produced a result within the bounded test window \ (unexpected hang)" ); } } } // SE-EXEC-06 (M.1 / F-003): a heap-allocation bomb must be capped by terminating // the offending isolate, NOT by letting V8 fatal-abort (SIGTRAP) the process-global // runtime and take down every concurrent tenant. Before the fix, no // near-heap-limit/OOM callback was registered, so reaching the operator-configured // `AGENTOS_V8_HEAP_LIMIT_MB` cap triggered V8's default fatal-OOM abort. // // BOUNDED safeguard variant: with a small heap cap the guard fires fast, terminates // the isolate, and the run returns a nonzero exit WITHOUT aborting the process. The // guest run is fenced behind a wall-clock watchdog on a worker thread so a regression // surfaces as a clear failure rather than a CI hang or a SIGTRAP that kills the whole // test binary. fn javascript_heap_allocation_bomb_is_capped_by_oom_guard() { let (tx, rx) = mpsc::channel::<(i32, String, String)>(); thread::spawn(move || { let temp = match tempdir() { Ok(temp) => temp, Err(error) => { let _ = tx.send((-1, String::new(), format!("tempdir failed: {error}"))); return; } }; let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine.start_execution(StartJavascriptExecutionRequest { vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], // Small bounded heap cap so the OOM guard fires quickly. The cap now // rides the typed `limits` field (migrated off the dead // `AGENTOS_V8_HEAP_LIMIT_MB` env knob). env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from( r#" // Grow unbounded; with a 32MB heap cap the OOM guard must terminate the isolate // well before this completes. If it ever completes, the cap did not bound it. const sink = []; for (let i = 0; i < 1_000_000; i += 1) { sink.push(new Array(100_000).fill(i)); } console.log("BOMB_COMPLETED_WITHOUT_CAP"); "#, )), limits: JavascriptExecutionLimits { v8_heap_limit_mb: Some(32), ..Default::default() }, argv0: None, guest_runtime: Default::default(), }); match execution { Ok(execution) => match execution.wait() { Ok(result) => { let stdout = String::from_utf8_lossy(&result.stdout).into_owned(); let stderr = String::from_utf8_lossy(&result.stderr).into_owned(); let _ = tx.send((result.exit_code, stdout, stderr)); } Err(error) => { let _ = tx.send((-1, String::new(), format!("wait failed: {error}"))); } }, Err(error) => { let _ = tx.send((-1, String::new(), format!("start failed: {error}"))); } } }); match rx.recv_timeout(Duration::from_secs(30)) { Ok((exit_code, stdout, stderr)) => { // The bad actor wins if the bomb runs to completion. assert!( !stdout.contains("BOMB_COMPLETED_WITHOUT_CAP"), "heap bomb completed despite configured heap limit: stdout={stdout} stderr={stderr}" ); // Enforcement must be a clean per-isolate termination (nonzero exit), // not a process-wide abort. assert_ne!( exit_code, 0, "heap bomb returned a clean exit instead of being terminated by the OOM guard: stdout={stdout} stderr={stderr}" ); } Err(_) => { panic!( "heap-bomb guest was NOT bounded by the OOM guard \ (wait() never returned within the bounded test budget)" ); } } } // Adversarial coverage for the builtin allow/deny desync (VECTORS.md A.2). // A denied builtin must stay denied on EVERY guest resolution path. The most // likely desync is a sub-path specifier (`dns/promises`) leaking on one path // while its root (`dns`) is denied on another. The live guest-JS path is the // shared V8 runtime, whose single `loadBuiltinModule` funnel gates by root // name (`split('/')[0]`) for require / createRequire / process.getBuiltinModule // / dynamic import alike. With an allow-list that excludes `dns`, every path // must reject both `dns` and `dns/promises`. fn javascript_execution_denies_dns_and_subpaths_on_every_resolution_path() { assert_js_runtime_guest_ok( // node platform, allow-list excludes `dns` (only `path`/`module`). js_runtime_env(&[("AGENTOS_JS_BUILTIN_ALLOWLIST", "[\"path\",\"module\"]")]), r#" import { createRequire } from "node:module"; const require = createRequire(import.meta.url); function assertDeniedSync(label, fn) { let denied = false; let detail = "no error"; try { const mod = fn(); const keys = mod && typeof mod === "object" ? Object.keys(mod).slice(0, 4).join(",") : typeof mod; detail = "resolved to " + keys; } catch (error) { detail = String(error && error.message); denied = !!error; } if (!denied) throw new Error(label + " was not denied: " + detail); } async function assertDeniedAsync(label, promise) { let denied = false; let detail = "no error"; try { await promise; detail = "import resolved"; } catch (error) { detail = String(error && error.message); denied = !!error; } if (!denied) throw new Error(label + " was not denied: " + detail); } // Positive control: an allowed builtin still resolves. const path = await import("node:path"); if (typeof path.join !== "function") throw new Error("node:path should be allowed"); for (const specifier of ["dns", "node:dns", "dns/promises", "node:dns/promises"]) { assertDeniedSync("require(" + specifier + ")", () => require(specifier)); assertDeniedSync( "createRequire(" + specifier + ")", () => createRequire(import.meta.url)(specifier), ); if (typeof process.getBuiltinModule === "function") { assertDeniedSync( "process.getBuiltinModule(" + specifier + ")", () => process.getBuiltinModule(specifier), ); } await assertDeniedAsync("import(" + specifier + ")", import(specifier)); } "#, ); } #[test] fn javascript_v8_suite() { // Keep V8-backed integration coverage inside one top-level libtest case. // Running these guest-runtime cases as separate tests in the same binary // still trips a V8 teardown/init boundary crash between cases. // // Warm-worker pooling stays OFF here: this harness services guest sync // RPCs inline on the test thread, and a claimed (pre-created) session // shifts kernel-stdin RPC timing so the harness sees a pending request // at wait() (PendingSyncRpcRequest). Production routes these through the // sidecar service loop and is unaffected (verified: stdin round-trips // 5/5 through pooled sessions in a live VM). let _no_warm_workers = EnvVarGuard::set_value("AGENTOS_V8_WARM_ISOLATES", "0"); javascript_contexts_preserve_vm_and_bootstrap_configuration(); javascript_execution_virtual_os_identity_comes_from_guest_runtime_not_env(); javascript_execution_uses_v8_runtime_without_spawning_guest_node_binary(); javascript_execution_virtualizes_process_metadata_for_inline_v8_code(); javascript_execution_process_kill_rejects_invalid_pid_in_guest_js(); javascript_execution_preserves_binary_process_stdio_writes(); javascript_execution_intl_number_format_does_not_require_host_icu(); javascript_execution_to_locale_date_string_does_not_crash_embedded_v8(); // QUARANTINED (2026-07-02, pre-existing on trunk 5367209b — verified by // bisect): stream-consumers live-stdin fails with PendingSyncRpcRequest in // THIS harness, which services guest sync RPCs inline on the test thread; // production stdin round-trips verified 5/5 through the real sidecar. // Tracked in the perf-backlog spec (0.5 burn-down) — re-enable when the // harness services pending stdin RPCs during wait(). // javascript_execution_stream_consumers_text_reads_live_stdin(); // QUARANTINED with the live-stdin class above (same harness limitation). // javascript_execution_process_stdin_async_iterator_finishes_with_live_stdin(); // QUARANTINED with the live-stdin class above (same harness limitation). // javascript_execution_process_exit_from_live_stdin_listener_exits_without_waiting_for_eof(); javascript_execution_process_exit_ignores_live_interval_handles(); javascript_execution_process_exit_bypasses_promise_catch_handlers(); // QUARANTINED with the live-stdin class above (same harness limitation). // javascript_execution_live_stdin_replays_end_after_late_listener_registration(); javascript_execution_file_url_to_path_accepts_guest_absolute_paths(); javascript_execution_imports_node_events_without_hanging(); javascript_execution_imports_node_process_without_hanging(); javascript_execution_imports_node_fs_promises_without_hanging(); javascript_execution_imports_node_perf_hooks_without_hanging(); javascript_execution_high_resolution_time_opt_in_enables_sub_ms_hrtime(); javascript_execution_high_resolution_time_default_off_keeps_coarse_clock(); javascript_execution_exposes_compatibility_shims_and_denies_escape_builtins(); javascript_execution_denies_dns_and_subpaths_on_every_resolution_path(); javascript_execution_v8_util_format_with_options_matches_node(); javascript_execution_provides_async_hooks_and_diagnostics_channel_stubs(); javascript_execution_supports_require_resolve_for_guest_code(); javascript_execution_rejects_native_node_addons(); javascript_execution_surfaces_sync_rpc_requests_from_v8_modules(); javascript_execution_v8_dgram_bridge_matches_sidecar_rpc_shapes(); javascript_execution_strips_hashbang_from_module_entrypoints(); javascript_execution_resolves_pnpm_store_dependencies_from_symlinked_entrypoints(); javascript_execution_resolves_dependencies_from_package_specific_symlink_mounts(); javascript_execution_v8_timer_callbacks_fire_and_clear_correctly(); javascript_execution_v8_readline_polyfill_emits_lines(); javascript_execution_v8_builtin_wrappers_expose_common_named_exports(); javascript_execution_v8_child_process_conformance_matches_host_node(); javascript_execution_v8_web_stream_globals_support_basic_io(); javascript_execution_v8_text_codec_streams_support_pipe_through(); javascript_execution_v8_abort_controller_dispatches_abort(); javascript_execution_v8_request_accepts_abort_signal(); javascript_execution_v8_abort_signal_static_helpers_work(); javascript_execution_v8_schedule_timer_bridge_resolves(); javascript_execution_v8_kernel_poll_bridge_requests_multiple_fds(); javascript_execution_v8_crypto_random_sources_use_local_secure_bridge(); javascript_execution_v8_crypto_basic_operations_emit_expected_sync_rpcs(); javascript_execution_v8_load_polyfill_returns_runtime_module_expressions(); javascript_execution_v8_stream_wrapper_exports_common_node_classes(); javascript_execution_v8_buffer_wrapper_exposes_commonjs_constants(); // QUARANTINED with the live-stdin class (standalone harness services only // module-resolution RPCs; tty RPCs are kernel-backed). // javascript_execution_v8_tty_module_is_backed_by_live_process(); javascript_execution_v8_sqlite_module_resolves_via_global_install(); javascript_execution_v8_commonjs_stack_frames_preserve_module_paths(); javascript_execution_v8_commonjs_main_entrypoints_preserve_entrypoint_paths(); javascript_execution_v8_inline_commonjs_entrypoints_preserve_entrypoint_paths(); javascript_execution_v8_inline_commonjs_entrypoints_preserve_commonjs_globals(); javascript_execution_v8_commonjs_require_exposes_node_metadata(); javascript_execution_v8_https_agents_expose_options_objects(); javascript_execution_v8_net_socket_readable_state_tracks_ssh2_writable_shape(); javascript_execution_v8_event_channel_backpressures_instead_of_destroying_session(); javascript_execution_v8_net_socket_read_loop_yields_macrotask_between_chunks(); javascript_execution_v8_net_socket_backpressure_stops_and_resumes_transport_reads(); javascript_execution_v8_net_close_connect_and_accept_wakes_match_node_ordering(); javascript_execution_v8_dynamic_import_accepts_file_urls(); javascript_execution_v8_wasm_instantiate_streaming_never_hangs(); javascript_execution_v8_structured_clone_rebinds_to_sandbox_realm(); js_runtime_node_platform_keeps_full_node_surface(); js_runtime_bare_platform_strips_all_host_globals(); js_runtime_browser_platform_exposes_web_without_node(); js_runtime_neutral_platform_drops_web_keeps_universal(); js_runtime_module_resolution_none_denies_all_imports(); js_runtime_module_resolution_relative_allows_local_denies_bare(); js_runtime_node_platform_allow_list_restricts_builtins(); js_runtime_browser_loads_cjs_npm_package(); js_runtime_browser_fetch_is_callable(); // SE-EXEC-06 (F-003): OOM guard terminates a heap-allocation bomb instead of // letting V8 fatal-abort the process. Runs before the CPU case; both are fenced // behind worker-thread wall-clock watchdogs. javascript_heap_allocation_bomb_is_capped_by_oom_guard(); // SE-EXEC-04 (F-001): TRUE CPU-time budget. These run LAST because a // regression in the tight-loop case could leak a CPU-bound worker thread. // 1. budget SET => tight busy loop terminated (cpu-budget reason) // 2. budget SET => awaiting/idle guest NOT killed (idle excluded) [critical negative] // 3. budget UNSET => default watchdog allows short CPU work javascript_infinite_loop_is_terminated_by_cpu_watchdog(); javascript_awaiting_guest_is_not_killed_by_cpu_budget(); javascript_default_cpu_budget_allows_short_cpu_work(); // WALL-CLOCK BACKSTOP (opt-in, complements the CPU-time budget): // 1. wall-clock SET => awaiting guest terminated (wall-clock reason; idle counted) // 2. CPU budget only => no wall-clock limit imposed (knobs independent) // 3. wall-clock unset => idle await completes; default CPU budget excludes idle javascript_awaiting_guest_is_terminated_by_wall_clock_backstop(); javascript_cpu_budget_only_does_not_impose_wall_clock_limit(); javascript_no_time_limit_when_neither_env_set(); }