mod support;
use hickory_resolver::proto::op::{Message, Query};
use hickory_resolver::proto::rr::domain::Name;
use hickory_resolver::proto::rr::rdata::{A, AAAA, CAA, CNAME, MX, NAPTR, NS, PTR, SOA, SRV, TXT};
use hickory_resolver::proto::rr::{RData, Record, RecordType};
use secure_exec_sidecar::wire::{
CloseStdinRequest, CreateVmRequest, DisposeReason, DisposeVmRequest, EventPayload,
GuestRuntimeKind, PatternPermissionScope, PermissionMode, PermissionsPolicy, RequestPayload,
ResponsePayload, RootFilesystemDescriptor, RootFilesystemMode, StreamChannel,
WriteStdinRequest,
};
use serde_json::{json, Value};
use std::collections::HashMap;
use std::io::{Read, Write};
use std::net::{Shutdown, SocketAddr, TcpListener, TcpStream, UdpSocket};
use std::path::Path;
use std::process::{Command, Stdio};
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use std::thread;
use std::time::{Duration, Instant};
use support::{
assert_node_available, authenticate_wire, dispose_vm_and_close_session_wire, execute_wire,
new_sidecar, open_session_wire, temp_dir, wire_permissions_allow_all, wire_request,
wire_session, wire_vm, write_fixture,
};
const ALLOWED_NODE_BUILTINS: &[&str] = &[
"assert",
"buffer",
"child_process",
"console",
"constants",
"crypto",
"events",
"fs",
"module",
"os",
"path",
"perf_hooks",
"punycode",
"querystring",
"stream",
"string_decoder",
"timers",
"tty",
"url",
"util",
"zlib",
];
const BUILTIN_CONFORMANCE_CASES: &[&str] = &[
"fs",
"console",
"child_process",
"path",
"crypto",
"dns",
"events",
"stream",
"buffer",
"url",
"stdlib_polyfill",
"extended_builtin_polyfills",
];
const PROBE_OUTPUT_BYTE_LIMIT: usize = 1024 * 1024;
fn run_host_probe(cwd: &Path, entrypoint: &Path) -> Value {
run_host_probe_with_env(cwd, entrypoint, &[])
}
fn run_host_probe_with_env(cwd: &Path, entrypoint: &Path, env: &[(&str, &str)]) -> Value {
let mut command = Command::new("node");
command.arg(entrypoint).current_dir(cwd);
for (key, value) in env {
command.env(key, value);
}
let mut child = command
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn host node probe");
let stdout = child.stdout.take().expect("host probe stdout pipe");
let stderr = child.stderr.take().expect("host probe stderr pipe");
let stdout_reader = thread::spawn(move || read_probe_pipe(stdout, "stdout"));
let stderr_reader = thread::spawn(move || read_probe_pipe(stderr, "stderr"));
let status = child.wait().expect("wait host node probe");
let stdout = stdout_reader
.join()
.expect("join host probe stdout reader")
.expect("read bounded host probe stdout");
let stderr = stderr_reader
.join()
.expect("join host probe stderr reader")
.expect("read bounded host probe stderr");
assert!(
status.success(),
"host probe failed with status {:?}\nstdout:\n{}\nstderr:\n{}",
status.code(),
String::from_utf8_lossy(&stdout),
String::from_utf8_lossy(&stderr)
);
serde_json::from_slice(&stdout).expect("parse host probe JSON")
}
fn read_probe_pipe(mut pipe: impl Read, channel: &str) -> Result<Vec<u8>, String> {
let mut output = Vec::new();
let mut chunk = [0_u8; 8192];
loop {
let read = pipe
.read(&mut chunk)
.map_err(|err| format!("read host probe {channel}: {err}"))?;
if read == 0 {
return Ok(output);
}
if output.len().saturating_add(read) > PROBE_OUTPUT_BYTE_LIMIT {
return Err(format!(
"host probe exceeded {PROBE_OUTPUT_BYTE_LIMIT} bytes on {channel}"
));
}
output.extend_from_slice(&chunk[..read]);
}
}
fn run_guest_probe(case_name: &str, cwd: &Path, entrypoint: &Path) -> Value {
run_guest_probe_with_config(
case_name,
cwd,
entrypoint,
HashMap::new(),
wire_permissions_allow_all(),
ALLOWED_NODE_BUILTINS,
)
}
#[allow(clippy::too_many_arguments)]
fn create_vm_with_metadata_and_permissions(
sidecar: &mut secure_exec_sidecar::NativeSidecar<support::RecordingBridge>,
request_id: i64,
connection_id: &str,
session_id: &str,
runtime: GuestRuntimeKind,
cwd: &Path,
mut metadata: HashMap<String, String>,
permissions: PermissionsPolicy,
) -> String {
metadata
.entry(String::from("cwd"))
.or_insert_with(|| cwd.to_string_lossy().into_owned());
let result = sidecar
.dispatch_wire_blocking(wire_request(
request_id,
wire_session(connection_id, session_id),
RequestPayload::CreateVmRequest(CreateVmRequest::legacy_test_config(
runtime,
metadata,
RootFilesystemDescriptor {
mode: RootFilesystemMode::Ephemeral,
disable_default_base_layer: false,
lowers: Vec::new(),
bootstrap_entries: Vec::new(),
},
Some(permissions),
)),
))
.expect("create sidecar VM through wire");
match result.response.payload {
ResponsePayload::VmCreatedResponse(response) => response.vm_id,
other => panic!("unexpected wire vm create response: {other:?}"),
}
}
fn collect_builtin_process_output(
sidecar: &mut secure_exec_sidecar::NativeSidecar<support::RecordingBridge>,
connection_id: &str,
session_id: &str,
vm_id: &str,
process_id: &str,
) -> (String, String, i32) {
collect_builtin_process_output_with_timeout(
sidecar,
connection_id,
session_id,
vm_id,
process_id,
Duration::from_secs(10),
)
}
fn collect_builtin_process_output_with_timeout(
sidecar: &mut secure_exec_sidecar::NativeSidecar<support::RecordingBridge>,
connection_id: &str,
session_id: &str,
vm_id: &str,
process_id: &str,
timeout: Duration,
) -> (String, String, i32) {
let ownership = wire_session(connection_id, session_id);
let deadline = Instant::now() + timeout;
let mut stdout = String::new();
let mut stderr = String::new();
let mut exit = None;
loop {
let event = sidecar
.poll_event_wire_blocking(&ownership, Duration::from_millis(100))
.expect("poll builtin conformance wire event");
if let Some(event) = event {
assert_eq!(event.ownership, wire_vm(connection_id, session_id, vm_id));
match event.payload {
EventPayload::ProcessOutputEvent(output) if output.process_id == process_id => {
match output.channel {
StreamChannel::Stdout => {
append_probe_output(&mut stdout, &output.chunk, process_id, "stdout")
}
StreamChannel::Stderr => {
append_probe_output(&mut stderr, &output.chunk, process_id, "stderr")
}
}
}
EventPayload::ProcessExitedEvent(exited) if exited.process_id == process_id => {
exit = Some((exited.exit_code, Instant::now()));
}
_ => {}
}
}
if let Some((exit_code, seen_at)) = exit {
if Instant::now().duration_since(seen_at) >= Duration::from_millis(200) {
return (stdout, stderr, exit_code);
}
}
assert!(
Instant::now() < deadline,
"timed out waiting for builtin conformance process {process_id}\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
}
}
fn append_probe_output(buffer: &mut String, chunk: &[u8], process_id: &str, channel: &str) {
let text = String::from_utf8_lossy(chunk);
assert!(
buffer.len().saturating_add(text.len()) <= PROBE_OUTPUT_BYTE_LIMIT,
"builtin conformance process {process_id} exceeded {PROBE_OUTPUT_BYTE_LIMIT} bytes on {channel}"
);
buffer.push_str(&text);
}
fn run_guest_probe_with_config(
case_name: &str,
cwd: &Path,
entrypoint: &Path,
mut metadata: HashMap<String, String>,
permissions: PermissionsPolicy,
allowed_builtins: &[&str],
) -> Value {
let mut sidecar = new_sidecar(case_name);
let connection_id = authenticate_wire(&mut sidecar, &format!("conn-{case_name}"));
let session_id = open_session_wire(&mut sidecar, 2, &connection_id);
let allowed_builtins =
serde_json::to_string(allowed_builtins).expect("serialize builtin allowlist");
metadata.insert(
String::from("env.AGENTOS_ALLOWED_NODE_BUILTINS"),
allowed_builtins,
);
let vm_id = create_vm_with_metadata_and_permissions(
&mut sidecar,
3,
&connection_id,
&session_id,
GuestRuntimeKind::JavaScript,
cwd,
metadata,
permissions,
);
execute_wire(
&mut sidecar,
4,
&connection_id,
&session_id,
&vm_id,
&format!("proc-{case_name}"),
GuestRuntimeKind::JavaScript,
entrypoint,
Vec::new(),
);
let (stdout, stderr, exit_code) = collect_builtin_process_output(
&mut sidecar,
&connection_id,
&session_id,
&vm_id,
&format!("proc-{case_name}"),
);
dispose_vm_and_close_session_wire(&mut sidecar, &connection_id, &session_id, &vm_id);
assert_eq!(
exit_code, 0,
"guest probe failed for {case_name}\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
assert!(
stderr.trim().is_empty(),
"guest probe stderr for {case_name}:\n{stderr}"
);
serde_json::from_str(stdout.trim()).expect("parse guest probe JSON")
}
#[allow(clippy::too_many_arguments)]
fn run_guest_probe_in_existing_session(
sidecar: &mut secure_exec_sidecar::NativeSidecar<support::RecordingBridge>,
request_id_base: i64,
connection_id: &str,
session_id: &str,
case_name: &str,
cwd: &Path,
entrypoint: &Path,
mut metadata: HashMap<String, String>,
) -> Value {
let allowed_builtins =
serde_json::to_string(ALLOWED_NODE_BUILTINS).expect("serialize builtin allowlist");
metadata.insert(
String::from("env.AGENTOS_ALLOWED_NODE_BUILTINS"),
allowed_builtins,
);
let vm_id = create_vm_with_metadata_and_permissions(
sidecar,
request_id_base,
connection_id,
session_id,
GuestRuntimeKind::JavaScript,
cwd,
metadata,
wire_permissions_allow_all(),
);
let process_id = format!("proc-{case_name}");
execute_wire(
sidecar,
request_id_base + 1,
connection_id,
session_id,
&vm_id,
&process_id,
GuestRuntimeKind::JavaScript,
entrypoint,
Vec::new(),
);
let (stdout, stderr, exit_code) =
collect_builtin_process_output(sidecar, connection_id, session_id, &vm_id, &process_id);
let result = sidecar
.dispatch_wire_blocking(wire_request(
request_id_base + 2,
wire_vm(connection_id, session_id, &vm_id),
RequestPayload::DisposeVmRequest(DisposeVmRequest {
reason: DisposeReason::Requested,
}),
))
.expect("dispose sidecar VM through wire");
match result.response.payload {
ResponsePayload::VmDisposedResponse(response) => {
assert_eq!(response.vm_id, vm_id);
}
other => panic!("unexpected wire vm dispose response: {other:?}"),
}
assert_eq!(
exit_code, 0,
"guest probe failed for {case_name}\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
assert!(
stderr.trim().is_empty(),
"guest probe stderr for {case_name}:\n{stderr}"
);
serde_json::from_str(stdout.trim()).expect("parse guest probe JSON")
}
fn assert_conformance(case_name: &str, script: &str) {
assert_node_available();
let cwd = temp_dir(&format!("builtin-conformance-{case_name}"));
let entrypoint = cwd.join("entry.mjs");
write_fixture(&entrypoint, script);
let host = run_host_probe(&cwd, &entrypoint);
let guest = run_guest_probe(case_name, &cwd, &entrypoint);
assert_eq!(
guest,
host,
"guest V8 result diverged from host Node for {case_name}\nhost: {}\nguest: {}",
serde_json::to_string_pretty(&host).expect("pretty host JSON"),
serde_json::to_string_pretty(&guest).expect("pretty guest JSON")
);
}
fn run_isolated_builtin_conformance_test(test_name: &str) {
let current_exe = std::env::current_exe().expect("current test binary path");
let status = Command::new(¤t_exe)
.arg("--exact")
.arg("__builtin_conformance_extra_test_runner")
.arg("--nocapture")
.env("AGENTOS_BUILTIN_CONFORMANCE_EXTRA_TEST", test_name)
.status()
.unwrap_or_else(|error| {
panic!("spawn builtin conformance extra runner for {test_name}: {error}")
});
assert!(
status.success(),
"builtin conformance extra test {test_name} failed with status {status}"
);
}
fn write_process_stdin(
sidecar: &mut secure_exec_sidecar::NativeSidecar<support::RecordingBridge>,
request_id: i64,
connection_id: &str,
session_id: &str,
vm_id: &str,
process_id: &str,
chunk: &str,
) {
let result = sidecar
.dispatch_wire_blocking(wire_request(
request_id,
wire_vm(connection_id, session_id, vm_id),
RequestPayload::WriteStdinRequest(WriteStdinRequest {
process_id: process_id.to_owned(),
chunk: chunk.as_bytes().to_vec(),
}),
))
.expect("write builtin conformance stdin through wire");
match result.response.payload {
ResponsePayload::StdinWrittenResponse(response) => {
assert_eq!(response.process_id, process_id);
assert_eq!(response.accepted_bytes, chunk.len() as u64);
}
other => panic!("unexpected wire stdin-written response: {other:?}"),
}
}
fn close_process_stdin(
sidecar: &mut secure_exec_sidecar::NativeSidecar<support::RecordingBridge>,
request_id: i64,
connection_id: &str,
session_id: &str,
vm_id: &str,
process_id: &str,
) {
let result = sidecar
.dispatch_wire_blocking(wire_request(
request_id,
wire_vm(connection_id, session_id, vm_id),
RequestPayload::CloseStdinRequest(CloseStdinRequest {
process_id: process_id.to_owned(),
}),
))
.expect("close builtin conformance stdin through wire");
match result.response.payload {
ResponsePayload::StdinClosedResponse(response) => {
assert_eq!(response.process_id, process_id);
}
other => panic!("unexpected wire stdin-closed response: {other:?}"),
}
}
struct FixtureDnsServer {
addr: SocketAddr,
running: Arc<AtomicBool>,
thread: Option<thread::JoinHandle<()>>,
}
impl FixtureDnsServer {
fn start() -> Self {
let socket = UdpSocket::bind("127.0.0.1:0").expect("bind fixture DNS server");
socket
.set_read_timeout(Some(Duration::from_millis(100)))
.expect("set fixture DNS timeout");
let addr = socket.local_addr().expect("fixture DNS local addr");
let running = Arc::new(AtomicBool::new(true));
let thread_running = Arc::clone(&running);
let thread = thread::spawn(move || {
let mut buffer = [0_u8; 2048];
while thread_running.load(Ordering::SeqCst) {
let Ok((len, peer)) = socket.recv_from(&mut buffer) else {
continue;
};
let Ok(request) = Message::from_vec(&buffer[..len]) else {
continue;
};
let response = fixture_dns_response(&request);
let bytes = response.to_vec().expect("encode fixture DNS response");
let _ = socket.send_to(&bytes, peer);
}
});
Self {
addr,
running,
thread: Some(thread),
}
}
}
impl Drop for FixtureDnsServer {
fn drop(&mut self) {
self.running.store(false, Ordering::SeqCst);
if let Ok(socket) = UdpSocket::bind("127.0.0.1:0") {
let _ = socket.send_to(&[0], self.addr);
}
if let Some(thread) = self.thread.take() {
thread.join().expect("join fixture DNS thread");
}
}
}
fn fixture_dns_response(request: &Message) -> Message {
let mut response = Message::response(request.metadata.id, request.metadata.op_code);
response.metadata.authoritative = true;
response.metadata.recursion_available = true;
response.add_queries(request.queries.iter().cloned());
if let Some(query) = request.queries.first() {
response.add_answers(fixture_dns_answers(query));
}
response
}
fn fixture_dns_answers(query: &Query) -> Vec<Record> {
let name = query.name().to_ascii();
match (name.as_str(), query.query_type()) {
("bundle.example.test.", RecordType::A) => vec![
fixture_dns_record("bundle.example.test.", RData::A(A::new(203, 0, 113, 10))),
fixture_dns_record("bundle.example.test.", RData::A(A::new(203, 0, 113, 11))),
],
("bundle.example.test.", RecordType::AAAA) => vec![
fixture_dns_record(
"bundle.example.test.",
RData::AAAA(AAAA::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0x0010)),
),
fixture_dns_record(
"bundle.example.test.",
RData::AAAA(AAAA::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0x0011)),
),
],
("bundle.example.test.", RecordType::MX) => vec![fixture_dns_record(
"bundle.example.test.",
RData::MX(MX::new(10, fixture_dns_name("mail.example.test."))),
)],
("bundle.example.test.", RecordType::TXT) => vec![
fixture_dns_record(
"bundle.example.test.",
RData::TXT(TXT::new(vec![String::from("v=spf1"), String::from("-all")])),
),
fixture_dns_record(
"bundle.example.test.",
RData::TXT(TXT::new(vec![String::from("secure-exec")])),
),
],
("bundle.example.test.", RecordType::ANY) => vec![
fixture_dns_record("bundle.example.test.", RData::A(A::new(203, 0, 113, 10))),
fixture_dns_record("bundle.example.test.", RData::A(A::new(203, 0, 113, 11))),
fixture_dns_record(
"bundle.example.test.",
RData::AAAA(AAAA::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0x0010)),
),
fixture_dns_record(
"bundle.example.test.",
RData::AAAA(AAAA::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0x0011)),
),
fixture_dns_record(
"bundle.example.test.",
RData::MX(MX::new(10, fixture_dns_name("mail.example.test."))),
),
fixture_dns_record(
"bundle.example.test.",
RData::TXT(TXT::new(vec![String::from("v=spf1"), String::from("-all")])),
),
],
("alias.example.test.", RecordType::CNAME) => vec![fixture_dns_record(
"alias.example.test.",
RData::CNAME(CNAME(fixture_dns_name("bundle.example.test."))),
)],
("ptr.example.test.", RecordType::PTR) => vec![fixture_dns_record(
"ptr.example.test.",
RData::PTR(PTR(fixture_dns_name("host.example.test."))),
)],
("zone.example.test.", RecordType::NS) => vec![fixture_dns_record(
"zone.example.test.",
RData::NS(NS(fixture_dns_name("ns1.example.test."))),
)],
("zone.example.test.", RecordType::SOA) => vec![fixture_dns_record(
"zone.example.test.",
RData::SOA(SOA::new(
fixture_dns_name("ns1.example.test."),
fixture_dns_name("hostmaster.example.test."),
2026041601,
3600,
600,
86400,
60,
)),
)],
("_svc._tcp.example.test.", RecordType::SRV) => vec![fixture_dns_record(
"_svc._tcp.example.test.",
RData::SRV(SRV::new(
1,
5,
8443,
fixture_dns_name("svc-target.example.test."),
)),
)],
("naptr.example.test.", RecordType::NAPTR) => vec![fixture_dns_record(
"naptr.example.test.",
RData::NAPTR(NAPTR::new(
10,
20,
b"s".to_vec().into_boxed_slice(),
b"SIP+D2U".to_vec().into_boxed_slice(),
b"!^.*$!sip:service@example.test!"
.to_vec()
.into_boxed_slice(),
fixture_dns_name("_sip._udp.example.test."),
)),
)],
("caa.example.test.", RecordType::CAA) => vec![
fixture_dns_record(
"caa.example.test.",
RData::CAA(CAA::new_issue(
false,
Some(fixture_dns_name("letsencrypt.org.")),
vec![],
)),
),
fixture_dns_record(
"caa.example.test.",
RData::CAA(CAA::new_iodef(
false,
url::Url::parse("https://iodef.example.test/report")
.expect("fixture CAA iodef URL"),
)),
),
],
_ => Vec::new(),
}
}
fn fixture_dns_record(name: &str, data: RData) -> Record {
Record::from_rdata(fixture_dns_name(name), 60, data)
}
fn fixture_dns_name(name: &str) -> Name {
name.parse().expect("valid fixture DNS name")
}
fn read_http_request(stream: &mut TcpStream) -> String {
let mut request = Vec::new();
let mut buffer = [0_u8; 1024];
loop {
let bytes_read = stream.read(&mut buffer).expect("read http request");
assert!(
bytes_read > 0,
"connection closed before full HTTP request arrived"
);
request.extend_from_slice(&buffer[..bytes_read]);
if request.windows(4).any(|window| window == b"\r\n\r\n") {
break;
}
}
String::from_utf8(request).expect("request utf8")
}
fn http_request_custom_agent_reuses_keepalive_socket_impl() {
assert_node_available();
let listener = TcpListener::bind("127.0.0.1:0").expect("bind host http listener");
let port = listener.local_addr().expect("listener addr").port();
let cwd = temp_dir("builtin-http-agent-keepalive");
let entrypoint = cwd.join("entry.mjs");
write_fixture(
&entrypoint,
format!(
r#"
import http from "node:http";
const agent = new http.Agent({{
keepAlive: true,
maxSockets: 1,
}});
function request(path) {{
return new Promise((resolve, reject) => {{
const req = http.request({{
host: "127.0.0.1",
port: {port},
path,
method: "GET",
agent,
}}, (res) => {{
res.setEncoding("utf8");
let body = "";
res.on("data", (chunk) => {{
body += chunk;
}});
res.on("end", () => {{
resolve({{
body,
reusedSocket: req.reusedSocket,
socketLocalPort: req.socket?.localPort ?? null,
statusCode: res.statusCode ?? null,
}});
}});
}});
req.on("error", reject);
req.end();
}});
}}
const first = await request("/first");
const second = await request("/second");
await new Promise((resolve) => setTimeout(resolve, 0));
const freeSockets = Object.values(agent.freeSockets).reduce(
(total, sockets) => total + sockets.length,
0,
);
console.log(JSON.stringify({{
first,
second,
freeSockets,
totalSocketCount: agent.totalSocketCount,
}}));
agent.destroy();
"#,
),
);
let case_name = "builtin-http-agent-keepalive";
let mut sidecar = new_sidecar(case_name);
let connection_id = authenticate_wire(&mut sidecar, &format!("conn-{case_name}"));
let session_id = open_session_wire(&mut sidecar, 2, &connection_id);
let allowed_builtins = serde_json::to_string(&["http"]).expect("serialize builtin allowlist");
let guest_env = HashMap::from([
(
String::from("env.AGENTOS_ALLOWED_NODE_BUILTINS"),
allowed_builtins,
),
(
String::from("env.AGENTOS_LOOPBACK_EXEMPT_PORTS"),
format!("[{port}]"),
),
]);
let vm_id = create_vm_with_metadata_and_permissions(
&mut sidecar,
3,
&connection_id,
&session_id,
GuestRuntimeKind::JavaScript,
&cwd,
guest_env,
wire_permissions_allow_all(),
);
let server = thread::spawn(move || {
listener
.set_nonblocking(true)
.expect("configure nonblocking listener");
let deadline = Instant::now() + Duration::from_secs(10);
let (mut stream, _) = loop {
match listener.accept() {
Ok(accepted) => break accepted,
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
assert!(
Instant::now() < deadline,
"timed out waiting for guest keep-alive connection"
);
thread::sleep(Duration::from_millis(10));
}
Err(error) => panic!("accept keep-alive connection: {error}"),
}
};
stream
.set_read_timeout(Some(Duration::from_secs(5)))
.expect("set read timeout");
let first_request = read_http_request(&mut stream);
assert!(
first_request.contains("GET /first HTTP/1.1"),
"unexpected first request: {first_request}"
);
stream
.write_all(
b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\nConnection: keep-alive\r\n\r\nfirst",
)
.expect("write first keep-alive response");
stream.flush().expect("flush first keep-alive response");
let second_request = read_http_request(&mut stream);
assert!(
second_request.contains("GET /second HTTP/1.1"),
"unexpected second request: {second_request}"
);
stream
.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 6\r\nConnection: close\r\n\r\nsecond")
.expect("write second keep-alive response");
stream.flush().expect("flush second keep-alive response");
});
execute_wire(
&mut sidecar,
4,
&connection_id,
&session_id,
&vm_id,
&format!("proc-{case_name}"),
GuestRuntimeKind::JavaScript,
&entrypoint,
Vec::new(),
);
let (stdout, stderr, exit_code) = collect_builtin_process_output(
&mut sidecar,
&connection_id,
&session_id,
&vm_id,
"proc-builtin-http-agent-keepalive",
);
dispose_vm_and_close_session_wire(&mut sidecar, &connection_id, &session_id, &vm_id);
server.join().expect("join keep-alive server");
assert_eq!(
exit_code, 0,
"guest probe failed for {case_name}\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
assert!(
stderr.trim().is_empty(),
"guest probe stderr for {case_name}:\n{stderr}"
);
let guest: Value = serde_json::from_str(stdout.trim()).expect("parse guest probe JSON");
assert_eq!(guest["first"]["statusCode"], 200);
assert_eq!(guest["first"]["body"], "first");
assert_eq!(guest["first"]["reusedSocket"], false);
assert_eq!(guest["second"]["statusCode"], 200);
assert_eq!(guest["second"]["body"], "second");
assert_eq!(guest["second"]["reusedSocket"], true);
assert_eq!(
guest["first"]["socketLocalPort"], guest["second"]["socketLocalPort"],
"expected second request to reuse the first socket"
);
assert_eq!(guest["freeSockets"], 1);
}
fn http_request_denied_egress_returns_permission_error_impl() {
assert_node_available();
let cwd = temp_dir("builtin-http-agent-denied");
let entrypoint = cwd.join("entry.mjs");
write_fixture(
&entrypoint,
r#"
import http from "node:http";
const result = await new Promise((resolve) => {
const req = http.get("http://127.0.0.1:9/denied", (res) => {
res.resume();
resolve({
statusCode: res.statusCode ?? null,
unexpected: true,
});
});
req.on("error", (error) => {
resolve({
code: error?.code ?? null,
message: String(error?.message ?? ""),
name: error?.name ?? null,
});
});
});
console.log(JSON.stringify(result));
"#,
);
let allow_all = wire_permissions_allow_all();
let guest = run_guest_probe_with_config(
"builtin-http-agent-denied",
&cwd,
&entrypoint,
HashMap::new(),
PermissionsPolicy {
fs: allow_all.fs,
network: Some(PatternPermissionScope::PermissionMode(PermissionMode::Deny)),
child_process: allow_all.child_process,
process: allow_all.process,
env: allow_all.env,
binding: allow_all.binding,
},
&["http"],
);
assert_eq!(guest["code"], "EACCES");
assert_eq!(guest["unexpected"], Value::Null);
assert!(
guest["message"]
.as_str()
.is_some_and(|message| message.contains("permission denied")),
"unexpected denied-egress payload: {guest}"
);
}
#[test]
fn http_request_custom_agent_reuses_keepalive_socket() {
run_isolated_builtin_conformance_test("http-request-keepalive");
}
#[test]
fn http_request_denied_egress_returns_permission_error() {
run_isolated_builtin_conformance_test("http-request-denied");
}
fn http_socket_writes_do_not_silently_drop_data_impl() {
assert_node_available();
let request_socket_listener =
TcpListener::bind("127.0.0.1:0").expect("bind host request-socket listener");
let request_socket_port = request_socket_listener
.local_addr()
.expect("request-socket listener addr")
.port();
let request_socket_payload = "agent-socket-payload";
let request_socket_server = thread::spawn(move || {
let (mut stream, _) = request_socket_listener
.accept()
.expect("accept request-socket stream");
stream
.set_read_timeout(Some(Duration::from_secs(5)))
.expect("set request-socket read timeout");
let request = read_http_request(&mut stream);
assert!(
request.contains("GET /socket-write HTTP/1.1"),
"unexpected keep-alive request: {request}"
);
stream
.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: keep-alive\r\n\r\nok")
.expect("write keep-alive response");
stream.flush().expect("flush keep-alive response");
let mut payload = vec![0; request_socket_payload.len()];
match stream.read(&mut payload) {
Ok(0) => {}
Ok(bytes_read) => {
let payload = payload[..bytes_read].to_vec();
assert_eq!(
String::from_utf8(payload.clone()).expect("utf8 tunneled payload"),
request_socket_payload
);
stream
.shutdown(Shutdown::Write)
.expect("shutdown request-socket write half");
}
Err(error)
if matches!(
error.kind(),
std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
) => {}
Err(error) => panic!("read request-socket payload: {error}"),
}
});
let cwd = temp_dir("builtin-http-socket-writes");
let entrypoint = cwd.join("entry.mjs");
write_fixture(
&entrypoint,
format!(
r#"
import http from "node:http";
const requestSocketResult = await new Promise((resolve, reject) => {{
const agent = new http.Agent({{ keepAlive: true, maxSockets: 1 }});
const req = http.request({{
host: "127.0.0.1",
port: {request_socket_port},
path: "/socket-write",
method: "GET",
agent,
headers: {{ Connection: "keep-alive" }},
}}, (res) => {{
res.resume();
res.on("end", () => {{
const payload = "{request_socket_payload}";
const finish = (result) => {{
agent.destroy();
resolve(result);
}};
req.socket.once("error", (error) => {{
finish({{
outcome: error?.code ?? error?.name ?? String(error),
sameSocket: !!req.socket,
statusCode: res.statusCode ?? null,
}});
}});
try {{
let writeReturn = null;
writeReturn = req.socket.write(payload, () => {{
finish({{
outcome: "forwarded",
writeReturn,
sameSocket: !!req.socket,
statusCode: res.statusCode ?? null,
}});
}});
}} catch (error) {{
finish({{
outcome: error?.code ?? error?.name ?? String(error),
sameSocket: !!req.socket,
statusCode: res.statusCode ?? null,
}});
}}
}});
}});
req.on("error", reject);
req.end();
}});
const responseResult = (() => {{
const res = new http.ServerResponse({{ method: "GET" }});
const result = {{
hasConnectionAlias: res.connection === res.socket,
socketPresent: !!res.socket,
}};
try {{
result.outcome = "forwarded";
result.returnValue = res.socket.write("socket-body:");
}} catch (error) {{
result.outcome = error?.code ?? error?.name ?? String(error);
}}
res.end("tail");
result.body = Buffer.concat(res._chunks ?? []).toString("utf8");
result.headersSent = res.headersSent;
result.writableFinished = res.writableFinished;
return result;
}})();
console.log(JSON.stringify({{ requestSocketResult, responseResult }}));
await new Promise((resolve) => setTimeout(resolve, 0));
process.exit(0);
"#,
),
);
let guest = run_guest_probe_with_config(
"builtin-http-socket-writes",
&cwd,
&entrypoint,
HashMap::from([(
String::from("env.AGENTOS_LOOPBACK_EXEMPT_PORTS"),
format!("[{request_socket_port}]"),
)]),
wire_permissions_allow_all(),
&["http"],
);
request_socket_server
.join()
.expect("join request-socket fixture server");
assert_eq!(guest["requestSocketResult"]["statusCode"], 200);
assert_eq!(
guest["requestSocketResult"]["sameSocket"],
Value::Bool(true)
);
let connect_outcome = guest["requestSocketResult"]["outcome"]
.as_str()
.expect("req.socket outcome");
assert!(
connect_outcome == "forwarded" || connect_outcome == "ERR_NOT_IMPLEMENTED",
"unexpected req.socket.write outcome: {guest}"
);
if connect_outcome == "forwarded" {
assert_eq!(guest["requestSocketResult"]["statusCode"], 200);
}
assert_eq!(guest["responseResult"]["socketPresent"], Value::Bool(true));
assert_eq!(
guest["responseResult"]["hasConnectionAlias"],
Value::Bool(true)
);
let response_outcome = guest["responseResult"]["outcome"]
.as_str()
.expect("ServerResponse.socket outcome");
assert!(
response_outcome == "forwarded" || response_outcome == "ERR_NOT_IMPLEMENTED",
"unexpected res.socket.write outcome: {guest}"
);
if response_outcome == "forwarded" {
assert_eq!(
guest["responseResult"]["returnValue"],
Value::Bool(true),
"expected res.socket.write to mirror ServerResponse.write return value"
);
assert_eq!(guest["responseResult"]["headersSent"], Value::Bool(true));
assert_eq!(
guest["responseResult"]["writableFinished"],
Value::Bool(true)
);
assert_eq!(
guest["responseResult"]["body"],
Value::String(String::from("socket-body:tail"))
);
} else {
assert_eq!(
guest["responseResult"]["body"],
Value::String(String::from("tail"))
);
}
}
#[test]
fn http_socket_writes_do_not_silently_drop_data() {
run_isolated_builtin_conformance_test("http-socket-writes");
}
fn net_socket_readable_state_tracks_ssh2_writable_shape_impl() {
assert_node_available();
let cwd = temp_dir("builtin-net-socket-readable-state");
let entrypoint = cwd.join("entry.mjs");
write_fixture(
&entrypoint,
r#"
import net from "node:net";
const isWritable = (stream) =>
Boolean(stream?.writable && stream?._readableState?.ended === false);
const socket = new net.Socket();
const open = {
ended: socket._readableState?.ended ?? null,
endEmitted: socket._readableState?.endEmitted ?? null,
writable: socket.writable ?? null,
isWritable: isWritable(socket),
};
socket.destroy();
const closed = {
ended: socket._readableState?.ended ?? null,
endEmitted: socket._readableState?.endEmitted ?? null,
writable: socket.writable ?? null,
isWritable: isWritable(socket),
destroyed: socket.destroyed ?? null,
};
console.log(JSON.stringify({ open, closed }));
"#,
);
let guest = run_guest_probe_with_config(
"net-socket-readable-state",
&cwd,
&entrypoint,
HashMap::new(),
wire_permissions_allow_all(),
&["net"],
);
assert_eq!(guest["open"]["ended"], Value::Bool(false));
assert_eq!(guest["open"]["endEmitted"], Value::Bool(false));
assert_eq!(guest["open"]["isWritable"], Value::Bool(true));
assert_eq!(guest["closed"]["ended"], Value::Bool(true));
assert_eq!(guest["closed"]["endEmitted"], Value::Bool(true));
assert_eq!(guest["closed"]["isWritable"], Value::Bool(false));
}
#[test]
fn net_socket_readable_state_tracks_ssh2_writable_shape() {
run_isolated_builtin_conformance_test("net-socket-readable-state");
}
fn readable_on_data_respects_explicit_pause_matches_host_node_impl() {
assert_conformance(
"readable-on-data-explicit-pause",
r#"
import fs from "node:fs";
const fixturePath = new URL("./fixture.txt", import.meta.url);
fs.writeFileSync(fixturePath, "abcdef");
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function pauseThenOnDataThenResume() {
const stream = fs.createReadStream(fixturePath, { encoding: "utf8", highWaterMark: 2 });
const chunks = [];
stream.pause();
const afterPause = stream.readableFlowing;
stream.on("data", (chunk) => chunks.push(chunk));
const afterOnData = stream.readableFlowing;
await delay(20);
const beforeResumeChunkCount = chunks.length;
stream.resume();
const afterResume = stream.readableFlowing;
await new Promise((resolve, reject) => {
stream.on("end", resolve);
stream.on("error", reject);
});
return {
afterPause,
afterOnData,
beforeResumeChunkCount,
afterResume,
chunks,
};
}
async function onDataAlone() {
const stream = fs.createReadStream(fixturePath, { encoding: "utf8", highWaterMark: 2 });
const chunks = [];
const initialFlowing = stream.readableFlowing;
stream.on("data", (chunk) => chunks.push(chunk));
const afterOnData = stream.readableFlowing;
await new Promise((resolve, reject) => {
stream.on("end", resolve);
stream.on("error", reject);
});
return {
initialFlowing,
afterOnData,
chunks,
};
}
async function multiplePauseResumeCycles() {
const stream = fs.createReadStream(fixturePath, { encoding: "utf8", highWaterMark: 2 });
const chunks = [];
const checkpoints = [];
let firstChunkSeen = false;
stream.pause();
checkpoints.push(["afterInitialPause", stream.readableFlowing]);
stream.on("data", (chunk) => {
chunks.push(chunk);
if (!firstChunkSeen) {
firstChunkSeen = true;
stream.pause();
checkpoints.push(["afterMidStreamPause", stream.readableFlowing]);
setTimeout(() => {
checkpoints.push(["beforeSecondResumeChunkCount", chunks.length]);
stream.resume();
checkpoints.push(["afterSecondResume", stream.readableFlowing]);
}, 20);
}
});
checkpoints.push(["afterOnData", stream.readableFlowing]);
await delay(20);
checkpoints.push(["beforeFirstResumeChunkCount", chunks.length]);
stream.resume();
checkpoints.push(["afterFirstResume", stream.readableFlowing]);
await new Promise((resolve, reject) => {
stream.on("end", resolve);
stream.on("error", reject);
});
return { checkpoints, chunks };
}
console.log(JSON.stringify({
pauseThenOnDataThenResume: await pauseThenOnDataThenResume(),
onDataAlone: await onDataAlone(),
multiplePauseResumeCycles: await multiplePauseResumeCycles(),
}));
"#,
);
}
#[test]
fn readable_on_data_respects_explicit_pause_matches_host_node() {
run_isolated_builtin_conformance_test("readable-on-data-explicit-pause");
}
fn readline_question_reads_real_stdin_impl() {
assert_node_available();
let cwd = temp_dir("builtin-readline-question");
let entrypoint = cwd.join("entry.mjs");
write_fixture(
&entrypoint,
r#"
import readline from "node:readline";
const output = { write() {} };
const rl = readline.createInterface({ input: process.stdin, output });
process.stdout.write("__READY__\n");
const callbackAnswer = await new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error("callback question timed out")), 2000);
rl.question("callback> ", (answer) => {
clearTimeout(timeout);
resolve(answer);
});
});
const promiseAnswer = await rl.question("promise> ");
rl.close();
console.log(JSON.stringify({ callbackAnswer, promiseAnswer }));
"#,
);
let mut sidecar = new_sidecar("builtin-readline-question");
let connection_id = authenticate_wire(&mut sidecar, "conn-readline-question");
let session_id = open_session_wire(&mut sidecar, 2, &connection_id);
let vm_id = create_vm_with_metadata_and_permissions(
&mut sidecar,
3,
&connection_id,
&session_id,
GuestRuntimeKind::JavaScript,
&cwd,
HashMap::from([
(
String::from("env.AGENTOS_ALLOWED_NODE_BUILTINS"),
serde_json::to_string(&["readline"]).expect("serialize builtin allowlist"),
),
(
String::from("env.SECURE_EXEC_KEEP_STDIN_OPEN"),
String::from("1"),
),
]),
wire_permissions_allow_all(),
);
execute_wire(
&mut sidecar,
4,
&connection_id,
&session_id,
&vm_id,
"proc-readline-question",
GuestRuntimeKind::JavaScript,
&entrypoint,
Vec::new(),
);
let ownership = wire_session(&connection_id, &session_id);
let deadline = Instant::now() + Duration::from_secs(10);
let mut stdout = String::new();
let mut stderr = String::new();
let mut exit = None;
let mut stdin_sent = false;
loop {
let event = sidecar
.poll_event_wire_blocking(&ownership, Duration::from_millis(100))
.expect("poll readline question wire event");
if let Some(event) = event {
assert_eq!(
event.ownership,
wire_vm(&connection_id, &session_id, &vm_id)
);
match event.payload {
EventPayload::ProcessOutputEvent(output)
if output.process_id == "proc-readline-question" =>
{
match output.channel {
StreamChannel::Stdout => append_probe_output(
&mut stdout,
&output.chunk,
&output.process_id,
"stdout",
),
StreamChannel::Stderr => append_probe_output(
&mut stderr,
&output.chunk,
&output.process_id,
"stderr",
),
}
}
EventPayload::ProcessExitedEvent(exited)
if exited.process_id == "proc-readline-question" =>
{
exit = Some((exited.exit_code, Instant::now()));
}
_ => {}
}
}
if !stdin_sent && stdout.contains("__READY__\n") {
write_process_stdin(
&mut sidecar,
5,
&connection_id,
&session_id,
&vm_id,
"proc-readline-question",
"hello\nworld\n",
);
close_process_stdin(
&mut sidecar,
6,
&connection_id,
&session_id,
&vm_id,
"proc-readline-question",
);
stdin_sent = true;
}
if let Some((exit_code, seen_at)) = exit {
if Instant::now().duration_since(seen_at) >= Duration::from_millis(200) {
let stdout = stdout.replace("__READY__\n", "");
dispose_vm_and_close_session_wire(
&mut sidecar,
&connection_id,
&session_id,
&vm_id,
);
assert_eq!(
exit_code, 0,
"readline question probe failed\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
assert!(stderr.trim().is_empty(), "unexpected stderr:\n{stderr}");
let payload: Value =
serde_json::from_str(stdout.trim()).expect("parse readline JSON");
assert_eq!(payload["callbackAnswer"], "hello");
assert_eq!(payload["promiseAnswer"], "world");
return;
}
}
assert!(
Instant::now() < deadline,
"timed out waiting for readline question probe\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
}
}
#[test]
fn readline_question_reads_real_stdin() {
run_isolated_builtin_conformance_test("readline-question");
}
fn vm_is_context_only_accepts_create_context_tagged_sandboxes_impl() {
assert_conformance(
"vm-is-context",
r#"
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const vm = require("node:vm");
const safeIsContext = (value) => {
try {
return vm.isContext(value);
} catch {
return false;
}
};
const sandbox = {};
const tagged = vm.createContext(sandbox);
const taggedArray = vm.createContext([]);
console.log(JSON.stringify({
sameReference: tagged === sandbox,
matrix: {
plainObject: safeIsContext({}),
taggedObject: safeIsContext(tagged),
plainArray: safeIsContext([]),
taggedArray: safeIsContext(taggedArray),
functionValue: safeIsContext(function demo() {}),
nullValue: safeIsContext(null),
numberValue: safeIsContext(1),
stringValue: safeIsContext("text"),
},
}));
"#,
);
}
#[test]
fn vm_is_context_only_accepts_create_context_tagged_sandboxes() {
run_isolated_builtin_conformance_test("vm-is-context");
}
fn vm_context_isolation_and_script_options_match_host_node_impl() {
assert_conformance(
"vm-context-isolation",
r#"
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const vm = require("node:vm");
const sandbox = { answer: 41 };
const context = vm.createContext(sandbox);
const runResult = vm.runInContext("answer += 1; typeof globalThis.require", context);
let filenameLine = false;
try {
new vm.Script("throw new Error('boom')", {
filename: "named-vm.js",
lineOffset: 2,
columnOffset: 4,
}).runInNewContext({});
} catch (error) {
filenameLine = String(error?.stack ?? error).includes("named-vm.js:3");
}
let invalidContextType = null;
try {
vm.runInContext("1 + 1", {});
} catch (error) {
invalidContextType = error?.name ?? null;
}
console.log(JSON.stringify({
sameReference: context === sandbox,
sandboxAnswer: sandbox.answer,
newContextRequire: vm.runInNewContext("typeof globalThis.require"),
newContextBuffer: vm.runInNewContext("typeof Buffer"),
contextRequire: runResult,
filenameLine,
invalidContextType,
}));
"#,
);
}
#[test]
fn vm_context_isolation_and_script_options_match_host_node() {
run_isolated_builtin_conformance_test("vm-context-isolation");
}
fn vm_timeout_terminates_within_deadline_impl() {
let cwd = temp_dir("builtin-conformance-vm-timeout");
let entrypoint = cwd.join("entry.mjs");
write_fixture(
&entrypoint,
r#"
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const vm = require("node:vm");
const started = Date.now();
let timeoutCode = null;
let timeoutMessage = null;
try {
vm.runInNewContext("while (true) {}", {}, { timeout: 100 });
} catch (error) {
timeoutCode = error?.code ?? null;
timeoutMessage = String(error?.message ?? error);
}
console.log(JSON.stringify({
elapsedMs: Date.now() - started,
timeoutCode,
timeoutMessage,
}));
"#,
);
let result = run_guest_probe("vm-timeout", &cwd, &entrypoint);
let elapsed_ms = result["elapsedMs"]
.as_u64()
.expect("vm timeout elapsed milliseconds");
assert!(
elapsed_ms <= 200,
"vm timeout exceeded 200ms: {elapsed_ms}ms ({result})"
);
assert_eq!(
result["timeoutCode"],
Value::String(String::from("ERR_SCRIPT_EXECUTION_TIMEOUT"))
);
assert!(
result["timeoutMessage"]
.as_str()
.is_some_and(|message| message.contains("timed out")),
"vm timeout message missing timeout marker: {result}"
);
}
#[test]
fn vm_timeout_terminates_within_deadline() {
run_isolated_builtin_conformance_test("vm-timeout");
}
fn vm_optional_surface_is_implemented_or_explicitly_not_implemented_impl() {
let cwd = temp_dir("builtin-conformance-vm-optional-surface");
let entrypoint = cwd.join("entry.mjs");
write_fixture(
&entrypoint,
r#"
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const vm = require("node:vm");
function capture(label, fn) {
try {
const value = fn();
return typeof value?.then === "function" ? "ok" : "ok";
} catch (error) {
return error?.code ?? `${label}-error`;
}
}
console.log(JSON.stringify({
compileFunction: capture("compileFunction", () => vm.compileFunction("return value;", ["value"])),
measureMemory: capture("measureMemory", () => vm.measureMemory()),
}));
"#,
);
let result = run_guest_probe("vm-optional-surface", &cwd, &entrypoint);
for key in ["compileFunction", "measureMemory"] {
let outcome = result[key].as_str().unwrap_or_default();
assert!(
outcome == "ok" || outcome == "ERR_NOT_IMPLEMENTED",
"vm optional surface {key} returned unexpected outcome: {result}"
);
}
}
#[test]
fn vm_optional_surface_is_implemented_or_explicitly_not_implemented() {
run_isolated_builtin_conformance_test("vm-optional-surface");
}
fn perf_hooks_observer_and_histogram_match_host_node_impl() {
assert_conformance(
"perf-hooks-observer",
r#"
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const { PerformanceObserver, createHistogram, performance } = require("node:perf_hooks");
function sortEntries(entries) {
return [...entries].sort((left, right) => left.localeCompare(right));
}
function toEntryNames(entries) {
return entries.map((entry) => `${entry.entryType}:${entry.name}`);
}
performance.clearMarks?.();
performance.clearMeasures?.();
const callbackEntries = [];
const observer = new PerformanceObserver((list) => {
callbackEntries.push(...toEntryNames(list.getEntries()));
});
observer.observe({ entryTypes: ["mark", "measure"] });
performance.mark("start");
performance.mark("end");
performance.measure("delta", "start", "end");
await new Promise((resolve) => setImmediate(resolve));
const callbackObserved = sortEntries(callbackEntries);
const afterFlush = sortEntries(toEntryNames(observer.takeRecords()));
observer.disconnect();
performance.clearMarks?.();
performance.clearMeasures?.();
const takeRecordsObserver = new PerformanceObserver(() => {});
takeRecordsObserver.observe({ entryTypes: ["mark", "measure"] });
performance.mark("alpha");
performance.mark("omega");
performance.measure("window", "alpha", "omega");
const takeRecordsBeforeFlush = sortEntries(
toEntryNames(takeRecordsObserver.takeRecords()),
);
await new Promise((resolve) => setImmediate(resolve));
const takeRecordsAfterFlush = sortEntries(
toEntryNames(takeRecordsObserver.takeRecords()),
);
takeRecordsObserver.disconnect();
const histogram = createHistogram();
histogram.record(10);
histogram.record(20);
histogram.record(30);
console.log(JSON.stringify({
callbackObserved,
afterFlush,
takeRecordsBeforeFlush,
takeRecordsAfterFlush,
histogram: {
emptyP50: createHistogram().percentile(50),
p50: histogram.percentile(50),
p90: histogram.percentile(90),
},
}));
"#,
);
}
#[test]
fn perf_hooks_observer_and_histogram_match_host_node() {
run_isolated_builtin_conformance_test("perf-hooks-observer");
}
fn run_guest_script(case_name: &str, script: &str) -> Value {
assert_node_available();
let cwd = temp_dir(&format!("builtin-guest-{case_name}"));
let entrypoint = cwd.join("entry.mjs");
write_fixture(&entrypoint, script);
run_guest_probe(case_name, &cwd, &entrypoint)
}
fn current_openssl_version() -> String {
openssl::version::version()
.split_whitespace()
.nth(1)
.unwrap_or_else(openssl::version::version)
.to_string()
}
fn process_runtime_stats_are_live_impl() {
let cwd = temp_dir("process-runtime-stats");
let entrypoint = cwd.join("entry.mjs");
write_fixture(
&entrypoint,
r#"
const before = process.memoryUsage();
const beforeCpu = process.cpuUsage();
const retained = [];
for (let i = 0; i < 25000; i += 1) {
retained.push({
index: i,
text: `${i}-`.padEnd(256, String(i % 10)),
});
}
let cpuAccumulator = 0;
for (let i = 0; i < 500000; i += 1) {
cpuAccumulator += Math.sqrt(i % 1000);
}
globalThis.__retainedProcessStatsFixture = retained;
const after = process.memoryUsage();
const deltaCpu = process.cpuUsage(beforeCpu);
const resource = process.resourceUsage();
console.log(JSON.stringify({
before,
after,
deltaCpu,
resource,
versions: {
node: process.versions.node,
v8: process.versions.v8,
openssl: process.versions.openssl,
},
retainedCount: retained.length,
cpuAccumulator,
}));
"#,
);
let guest = run_guest_probe_with_config(
"process-runtime-stats",
&cwd,
&entrypoint,
HashMap::new(),
wire_permissions_allow_all(),
&[],
);
let before_heap_used = guest["before"]["heapUsed"]
.as_u64()
.expect("before heapUsed should be a number");
let after_heap_used = guest["after"]["heapUsed"]
.as_u64()
.expect("after heapUsed should be a number");
assert!(
after_heap_used > before_heap_used + 512_000,
"expected heapUsed to grow by at least 512KiB after allocation, before={before_heap_used}, after={after_heap_used}, guest={guest}",
);
let user_cpu = guest["deltaCpu"]["user"]
.as_u64()
.expect("cpuUsage.user should be a number");
let system_cpu = guest["deltaCpu"]["system"]
.as_u64()
.expect("cpuUsage.system should be a number");
assert!(
user_cpu + system_cpu > 0,
"expected cpuUsage delta to report live CPU time, guest={guest}",
);
for field in [
"userCPUTime",
"systemCPUTime",
"maxRSS",
"minorPageFault",
"majorPageFault",
"voluntaryContextSwitches",
"involuntaryContextSwitches",
] {
assert!(
guest["resource"][field].is_number(),
"expected resourceUsage.{field} to be numeric, guest={guest}",
);
}
assert_eq!(
guest["versions"]["v8"],
Value::String(v8::V8::get_version().to_string())
);
assert_eq!(
guest["versions"]["openssl"],
Value::String(current_openssl_version())
);
}
#[test]
fn process_runtime_stats_are_live() {
run_isolated_builtin_conformance_test("process-runtime-stats");
}
fn os_resource_limits_are_vm_scoped_impl() {
let cwd = temp_dir("builtin-conformance-os-resource-limits");
let entrypoint = cwd.join("entry.mjs");
write_fixture(
&entrypoint,
r#"
import os from "node:os";
console.log(JSON.stringify({
availableParallelism: os.availableParallelism(),
cpusLength: os.cpus().length,
freemem: os.freemem(),
totalmem: os.totalmem(),
username: os.userInfo().username,
homedir: os.homedir(),
envUser: process.env.USER,
envHome: process.env.HOME,
}));
"#,
);
let mut sidecar = new_sidecar("os-resource-limits");
let connection_id = authenticate_wire(&mut sidecar, "conn-os-resource-limits");
let session_id = open_session_wire(&mut sidecar, 2, &connection_id);
let constrained = run_guest_probe_in_existing_session(
&mut sidecar,
3,
&connection_id,
&session_id,
"os-resource-limits-constrained",
&cwd,
&entrypoint,
HashMap::from([
(String::from("resource.cpu_count"), String::from("2")),
(
String::from("resource.max_wasm_memory_bytes"),
(64_u64 * 1024 * 1024).to_string(),
),
]),
);
let expanded = run_guest_probe_in_existing_session(
&mut sidecar,
5,
&connection_id,
&session_id,
"os-resource-limits-expanded",
&cwd,
&entrypoint,
HashMap::from([
(String::from("resource.cpu_count"), String::from("5")),
(
String::from("resource.max_wasm_memory_bytes"),
(256_u64 * 1024 * 1024).to_string(),
),
]),
);
sidecar
.close_session_blocking(&connection_id, &session_id)
.expect("close sidecar session");
sidecar
.remove_connection_blocking(&connection_id)
.expect("remove sidecar connection");
assert_eq!(constrained["username"], "agentos");
assert_eq!(constrained["homedir"], "/home/agentos");
assert_eq!(expanded["username"], "agentos");
assert_eq!(expanded["homedir"], "/home/agentos");
assert_eq!(constrained["availableParallelism"], 2);
assert_eq!(constrained["cpusLength"], 2);
assert_eq!(constrained["totalmem"], 64_u64 * 1024 * 1024);
assert_eq!(constrained["freemem"], 64_u64 * 1024 * 1024);
assert_eq!(expanded["availableParallelism"], 5);
assert_eq!(expanded["cpusLength"], 5);
assert_eq!(expanded["totalmem"], 256_u64 * 1024 * 1024);
assert_eq!(expanded["freemem"], 256_u64 * 1024 * 1024);
assert_ne!(constrained, expanded);
}
#[test]
fn os_resource_limits_are_vm_scoped() {
run_isolated_builtin_conformance_test("os-resource-limits");
}
fn dns_conformance_matches_host_node() {
assert_node_available();
let dns_server = FixtureDnsServer::start();
let dns_server_addr = dns_server.addr.to_string();
let cwd = temp_dir("builtin-conformance-dns");
let entrypoint = cwd.join("entry.mjs");
write_fixture(
&entrypoint,
r#"
import dns from "node:dns";
if (process.env.AGENTOS_TEST_DNS_SERVER) {
dns.setServers([process.env.AGENTOS_TEST_DNS_SERVER]);
}
function sortStrings(values) {
return [...values].sort((left, right) => left.localeCompare(right));
}
function sortObjects(values) {
return [...values].sort((left, right) =>
JSON.stringify(left).localeCompare(JSON.stringify(right)),
);
}
function resolveWithCallback(hostname, rrtype) {
return new Promise((resolve, reject) => {
dns.resolve(hostname, rrtype, (error, records) => {
if (error) {
reject(error);
return;
}
resolve(records);
});
});
}
const resolveAny = sortObjects(await dns.promises.resolveAny("bundle.example.test"));
const results = {
resolveCallbackA: sortStrings(await resolveWithCallback("bundle.example.test", "A")),
resolve4: sortStrings(await dns.promises.resolve4("bundle.example.test")),
resolve6: sortStrings(await dns.promises.resolve6("bundle.example.test")),
resolveCallbackMx: sortObjects(await resolveWithCallback("bundle.example.test", "MX")),
resolveTxt: sortObjects(await dns.promises.resolveTxt("bundle.example.test")),
resolveSrv: sortObjects(await dns.promises.resolveSrv("_svc._tcp.example.test")),
resolveCname: sortStrings(await dns.promises.resolveCname("alias.example.test")),
resolvePtr: sortStrings(await dns.promises.resolvePtr("ptr.example.test")),
resolveNs: sortStrings(await dns.promises.resolveNs("zone.example.test")),
resolveSoa: await dns.promises.resolveSoa("zone.example.test"),
resolveNaptr: sortObjects(await dns.promises.resolveNaptr("naptr.example.test")),
resolveCaa: sortObjects(await dns.promises.resolveCaa("caa.example.test")),
resolveAny,
};
console.log(JSON.stringify(results));
"#,
);
let host = run_host_probe_with_env(
&cwd,
&entrypoint,
&[("AGENTOS_TEST_DNS_SERVER", dns_server_addr.as_str())],
);
let guest = run_guest_probe_with_config(
"dns",
&cwd,
&entrypoint,
HashMap::from([(String::from("network.dns.servers"), dns_server_addr.clone())]),
wire_permissions_allow_all(),
&["dns"],
);
fn strip_record_type(value: &Value) -> Value {
match value {
Value::Object(map) => Value::Object(
map.iter()
.filter(|(key, _)| key.as_str() != "type")
.map(|(key, val)| (key.clone(), strip_record_type(val)))
.collect(),
),
Value::Array(items) => Value::Array(items.iter().map(strip_record_type).collect()),
other => other.clone(),
}
}
assert_eq!(
strip_record_type(&guest),
strip_record_type(&host),
"guest V8 result diverged from host Node for dns (ignoring host-Node-version-dependent record `type` field)\nhost: {}\nguest: {}",
serde_json::to_string_pretty(&host).expect("pretty host JSON"),
serde_json::to_string_pretty(&guest).expect("pretty guest JSON")
);
let type_at = |key: &str, index: usize| -> Option<String> {
guest[key]
.get(index)
.and_then(|record| record.get("type"))
.and_then(Value::as_str)
.map(str::to_owned)
};
assert_eq!(type_at("resolveSrv", 0).as_deref(), Some("SRV"));
assert_eq!(type_at("resolveCallbackMx", 0).as_deref(), Some("MX"));
assert!(
guest["resolveCaa"].as_array().is_some_and(|records| records
.iter()
.all(|record| record.get("type").and_then(Value::as_str) == Some("CAA"))),
"guest resolveCaa records missing CAA type discriminator: {}",
guest["resolveCaa"]
);
assert!(
guest["resolveAny"].as_array().is_some_and(|records| records
.iter()
.all(|record| record.get("type").and_then(Value::as_str).is_some())),
"guest resolveAny records missing type discriminator: {}",
guest["resolveAny"]
);
let unsupported_cwd = temp_dir("builtin-conformance-dns-unsupported");
let unsupported_entrypoint = unsupported_cwd.join("entry.mjs");
write_fixture(
&unsupported_entrypoint,
r#"
import dns from "node:dns";
try {
await dns.promises.resolve("bundle.example.test", "TLSA");
console.log(JSON.stringify({ unexpected: true }));
} catch (error) {
console.log(JSON.stringify({
code: error?.code ?? null,
message: String(error?.message ?? ""),
}));
}
"#,
);
let unsupported = run_guest_probe_with_config(
"dns-unsupported",
&unsupported_cwd,
&unsupported_entrypoint,
HashMap::new(),
wire_permissions_allow_all(),
&["dns"],
);
assert_eq!(unsupported["code"], "ERR_NOT_IMPLEMENTED");
assert!(
unsupported["message"]
.as_str()
.is_some_and(|message| message.contains("TLSA")),
"unexpected unsupported rrtype payload: {unsupported}"
);
let resolver_cwd = temp_dir("builtin-conformance-dns-resolver");
let resolver_entrypoint = resolver_cwd.join("entry.mjs");
write_fixture(
&resolver_entrypoint,
r#"
import dns, { Resolver as CallbackResolver } from "node:dns";
import dnsPromises, { Resolver as PromisesResolver } from "node:dns/promises";
const callbackResolver = new CallbackResolver();
callbackResolver.setServers(["203.0.113.53:5353"]);
const callbackResult = await new Promise((resolve, reject) => {
callbackResolver.resolve4("bundle.example.test", (error, records) => {
if (error) {
reject(error);
return;
}
resolve(records);
});
});
const promisesResolver = new PromisesResolver();
promisesResolver.setServers(["203.0.113.54", "203.0.113.55:5353"]);
console.log(JSON.stringify({
callbackResolverIsConstructor: typeof CallbackResolver === "function",
promisesResolverIsConstructor: typeof PromisesResolver === "function",
sameCallbackResolverExport: dns.Resolver === CallbackResolver,
samePromisesResolverExport: dnsPromises.Resolver === PromisesResolver,
callbackServers: callbackResolver.getServers(),
promisesServers: promisesResolver.getServers(),
callbackResult: [...callbackResult].sort(),
promisesResult: [...(await promisesResolver.resolve4("bundle.example.test"))].sort(),
}));
"#,
);
let resolver_probe = run_guest_probe_with_config(
"dns-resolver",
&resolver_cwd,
&resolver_entrypoint,
HashMap::from([(String::from("network.dns.servers"), dns_server_addr.clone())]),
wire_permissions_allow_all(),
&["dns"],
);
assert_eq!(
resolver_probe["callbackResolverIsConstructor"],
Value::Bool(true)
);
assert_eq!(
resolver_probe["promisesResolverIsConstructor"],
Value::Bool(true)
);
assert_eq!(
resolver_probe["sameCallbackResolverExport"],
Value::Bool(true)
);
assert_eq!(
resolver_probe["samePromisesResolverExport"],
Value::Bool(true)
);
assert_eq!(
resolver_probe["callbackServers"],
json!([String::from("203.0.113.53:5353")])
);
assert_eq!(
resolver_probe["promisesServers"],
json!([
String::from("203.0.113.54"),
String::from("203.0.113.55:5353"),
])
);
assert_eq!(
resolver_probe["callbackResult"],
json!([String::from("203.0.113.10"), String::from("203.0.113.11"),])
);
assert_eq!(
resolver_probe["promisesResult"],
json!([String::from("203.0.113.10"), String::from("203.0.113.11"),])
);
}
fn fs_conformance_matches_host_node() {
assert_conformance(
"fs",
r#"
import fs from "node:fs";
fs.mkdirSync("scratchdir");
fs.mkdirSync("scratchdir/nested");
fs.writeFileSync("scratchdir/nested/alpha.txt", Buffer.from("alpha-sync", "utf8"));
await new Promise((resolve, reject) => {
fs.writeFile("scratchdir/beta.txt", Buffer.from("beta-async", "utf8"), (error) => {
if (error) {
reject(error);
return;
}
resolve();
});
});
let missingStatCode = null;
try {
fs.statSync("scratchdir/missing.txt");
} catch (error) {
missingStatCode = error?.code ?? null;
}
let missingReadCode = null;
try {
await new Promise((resolve, reject) => {
fs.readFile("scratchdir/missing.txt", "utf8", (error, value) => {
if (error) {
reject(error);
return;
}
resolve(value);
});
});
} catch (error) {
missingReadCode = error?.code ?? null;
}
const asyncRead = await new Promise((resolve, reject) => {
fs.readFile("scratchdir/beta.txt", "utf8", (error, value) => {
if (error) {
reject(error);
return;
}
resolve(value);
});
});
console.log(JSON.stringify({
syncRead: fs.readFileSync("scratchdir/nested/alpha.txt", "utf8"),
asyncRead,
entries: fs.readdirSync("scratchdir").sort(),
statSize: fs.statSync("scratchdir/nested/alpha.txt").size,
existsAlpha: fs.existsSync("scratchdir/nested/alpha.txt"),
existsBeta: fs.existsSync("scratchdir/beta.txt"),
missingStatCode,
missingReadCode,
}));
"#,
);
}
fn console_conformance_matches_host_node() {
assert_conformance(
"console",
r#"
import * as consoleModule from "node:console";
import { Writable } from "node:stream";
const consoleInstance = new consoleModule.Console(process.stdout, process.stderr);
const task = consoleModule.createTask("demo-task");
const detachedChunks = [];
const detachedErrors = [];
const createSink = (target) =>
new Writable({
write(chunk, _encoding, callback) {
target.push(String(chunk));
callback();
},
});
const detachedConsole = new consoleModule.Console(
createSink(detachedChunks),
createSink(detachedErrors),
);
const detachedLog = detachedConsole.log;
const detachedError = detachedConsole.error;
detachedLog("detached-log");
detachedError("detached-error");
console.log(JSON.stringify({
types: {
Console: typeof consoleModule.Console,
context: typeof consoleModule.context,
createTask: typeof consoleModule.createTask,
log: typeof consoleModule.log,
table: typeof consoleModule.table,
},
taskRunType: typeof task.run,
consoleMethods: {
assert: typeof consoleInstance.assert,
clear: typeof consoleInstance.clear,
count: typeof consoleInstance.count,
countReset: typeof consoleInstance.countReset,
debug: typeof consoleInstance.debug,
dir: typeof consoleInstance.dir,
dirxml: typeof consoleInstance.dirxml,
error: typeof consoleInstance.error,
group: typeof consoleInstance.group,
groupCollapsed: typeof consoleInstance.groupCollapsed,
groupEnd: typeof consoleInstance.groupEnd,
info: typeof consoleInstance.info,
log: typeof consoleInstance.log,
profile: typeof consoleInstance.profile,
profileEnd: typeof consoleInstance.profileEnd,
table: typeof consoleInstance.table,
time: typeof consoleInstance.time,
timeEnd: typeof consoleInstance.timeEnd,
timeLog: typeof consoleInstance.timeLog,
timeStamp: typeof consoleInstance.timeStamp,
trace: typeof consoleInstance.trace,
warn: typeof consoleInstance.warn,
},
detachedOutput: detachedChunks.join(""),
detachedErrorOutput: detachedErrors.join(""),
}));
"#,
);
}
fn child_process_conformance_matches_host_node() {
assert_conformance(
"child-process",
r#"
import childProcess from "node:child_process";
const syncStdout = childProcess.spawnSync(
"node",
["-e", "process.stdout.write(process.argv[1] ?? '')", "alpha-sync"],
);
const syncError = childProcess.spawnSync(
"node",
["-e", "process.stderr.write('sync-error'); throw new Error('sync-fail');"],
);
const asyncEchoResult = await new Promise((resolve, reject) => {
const child = childProcess.spawn(
"node",
[
"-e",
"let data=''; let settled = false; const fallback = setTimeout(() => { if (!settled) process.exit(19); }, 50); process.stdin.on('data', (chunk) => { data += chunk; }); process.stdin.on('end', () => { settled = true; clearTimeout(fallback); process.exit(data === 'beta-async' ? 0 : 17); });",
],
);
const timer = setTimeout(() => {
reject(new Error("spawn(node async echo) 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.stdin.write(Buffer.from("beta-async"));
child.stdin.end();
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(
"node",
[
"-e",
"setTimeout(() => { process.stderr.write('async-error'); throw new Error('async-fail'); }, 10);",
],
);
const timer = setTimeout(() => {
reject(new Error("spawn(node async failure) 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({
syncStdoutStatus: syncStdout.status,
syncStdoutTrimmed: Buffer.from(syncStdout.stdout ?? []).toString("utf8").trim(),
syncStdoutStderrBase64: Buffer.from(syncStdout.stderr ?? []).toString("base64"),
syncErrorStatus: syncError.status,
syncErrorStdoutBase64: Buffer.from(syncError.stdout ?? []).toString("base64"),
syncErrorHasMarker: Buffer.from(syncError.stderr ?? []).toString("utf8").includes("sync-error"),
syncErrorHasNonZeroStatus: (syncError.status ?? 0) !== 0,
asyncEchoCode: asyncEchoResult.code,
asyncEchoSignal: asyncEchoResult.signal,
asyncEchoStdoutBase64: asyncEchoResult.stdoutBase64,
asyncEchoStderrBase64: asyncEchoResult.stderrBase64,
asyncErrorCode: asyncErrorResult.code,
asyncErrorSignal: asyncErrorResult.signal,
asyncErrorStdoutBase64: asyncErrorResult.stdoutBase64,
asyncErrorHasNonZeroStatus: (asyncErrorResult.code ?? 0) !== 0,
}));
"#,
);
}
fn child_process_fork_supports_basic_ipc_impl() {
let cwd = temp_dir("builtin-child-process-fork-ipc");
let entrypoint = cwd.join("entry.mjs");
let worker = cwd.join("worker.mjs");
write_fixture(
&worker,
r#"
process.send({
type: "ready",
connected: process.connected,
argv: process.argv.slice(-1),
});
process.on("message", (message) => {
process.send({
type: "pong",
value: message.value + 1,
connected: process.connected,
});
process.exit(0);
});
"#,
);
write_fixture(
&entrypoint,
r#"
import childProcess from "node:child_process";
import { Buffer } from "node:buffer";
const child = childProcess.fork("./worker.mjs", ["worker-arg"]);
const stdout = [];
const messages = [];
const errors = [];
let sendReturn = null;
child.stdout.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
child.on("error", (error) => errors.push({
name: error?.name ?? null,
message: error?.message ?? null,
code: error?.code ?? null,
}));
child.on("message", (message) => {
messages.push(message);
if (message.type === "ready") {
sendReturn = child.send({ type: "ping", value: 41 });
}
});
const exit = await new Promise((resolve) => {
child.on("close", (code, signal) => resolve({ code, signal }));
});
console.log(JSON.stringify({
connectedAfterFork: child.connected,
sendReturn,
messages,
errors,
stdoutBase64: Buffer.concat(stdout).toString("base64"),
exit,
}));
"#,
);
let guest = run_guest_probe_with_config(
"child-process-fork-ipc",
&cwd,
&entrypoint,
HashMap::new(),
wire_permissions_allow_all(),
&["child_process"],
);
let pretty_guest = serde_json::to_string_pretty(&guest).expect("pretty guest JSON");
assert_eq!(
guest["sendReturn"],
Value::Bool(true),
"guest result:\n{pretty_guest}"
);
assert_eq!(
guest["errors"],
Value::Array(Vec::new()),
"guest result:\n{pretty_guest}"
);
assert_eq!(guest["stdoutBase64"], Value::String(String::new()));
assert_eq!(guest["exit"]["code"], Value::from(0));
assert_eq!(guest["exit"]["signal"], Value::Null);
assert_eq!(
guest["messages"][0]["type"],
Value::String(String::from("ready"))
);
assert_eq!(guest["messages"][0]["connected"], Value::Bool(true));
assert_eq!(
guest["messages"][0]["argv"][0],
Value::String(String::from("worker-arg"))
);
assert_eq!(
guest["messages"][1]["type"],
Value::String(String::from("pong"))
);
assert_eq!(guest["messages"][1]["value"], Value::from(42));
assert_eq!(guest["messages"][1]["connected"], Value::Bool(true));
}
#[test]
fn child_process_fork_supports_basic_ipc() {
run_isolated_builtin_conformance_test("child-process-fork-ipc");
}
fn child_process_exec_preserves_spawn_error_codes_impl() {
assert_node_available();
let cwd = temp_dir("builtin-child-process-exec-spawn-error-code");
let entrypoint = cwd.join("entry.mjs");
write_fixture(
&entrypoint,
r#"
import childProcess from "node:child_process";
const result = await new Promise((resolve) => {
const callbacks = [];
const closeEvents = [];
const child = childProcess.exec(
"/definitely/not/a/binary",
(err, stdout, stderr) => {
callbacks.push({
code: err?.code ?? null,
errno: typeof err?.errno === "number" ? err.errno : null,
syscall: err?.syscall ?? null,
path: err?.path ?? null,
stdout,
stderr,
});
setTimeout(() => resolve({ callbacks, closeEvents }), 0);
},
);
child.on("close", (code, signal) => {
closeEvents.push({
code: code ?? null,
signal: signal ?? null,
});
});
child.on("error", () => {});
});
console.log(JSON.stringify(result));
"#,
);
let guest = run_guest_probe_with_config(
"child-process-exec-spawn-error-code",
&cwd,
&entrypoint,
HashMap::new(),
wire_permissions_allow_all(),
&["child_process"],
);
assert_eq!(
guest["callbacks"][0]["code"],
Value::String(String::from("ENOENT")),
"guest exec() callback should preserve the original spawn error code",
);
assert_eq!(
guest["callbacks"].as_array().map(Vec::len),
Some(1),
"guest exec() callback should not be re-fired after a spawn error"
);
assert_eq!(
guest["callbacks"][0]["stdout"],
Value::String(String::new())
);
assert_eq!(
guest["callbacks"][0]["stderr"],
Value::String(String::new())
);
}
#[test]
fn child_process_exec_preserves_spawn_error_codes() {
run_isolated_builtin_conformance_test("child-process-exec-spawn-error-code");
}
fn child_process_rejects_native_elf_binaries_before_wasm_compile_impl() {
let cwd = temp_dir("builtin-child-process-native-elf-reject");
let entrypoint = cwd.join("entry.mjs");
write_fixture(
&entrypoint,
r#"
import childProcess from "node:child_process";
import fs from "node:fs";
const fakeRgPath = "/tmp/fake-rg";
fs.writeFileSync(
fakeRgPath,
Buffer.from([0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00]),
);
fs.chmodSync(fakeRgPath, 0o755);
const syncResult = childProcess.spawnSync(fakeRgPath, ["--version"]);
const asyncResult = await new Promise((resolve) => {
const child = childProcess.spawn(fakeRgPath, ["--version"]);
child.once("error", (error) => {
resolve({
code: error?.code ?? null,
message: error?.message ?? null,
});
});
});
console.log(JSON.stringify({
sync: {
status: syncResult.status,
errorCode: syncResult.error?.code ?? null,
errorMessage: syncResult.error?.message ?? null,
stderr: Buffer.isBuffer(syncResult.stderr)
? syncResult.stderr.toString("utf8")
: String(syncResult.stderr ?? ""),
},
async: asyncResult,
}));
"#,
);
let guest = run_guest_probe_with_config(
"child-process-native-elf-reject",
&cwd,
&entrypoint,
HashMap::new(),
wire_permissions_allow_all(),
&["child_process", "fs"],
);
assert_eq!(guest["sync"]["status"], Value::Number(1.into()));
assert_eq!(
guest["sync"]["errorCode"],
Value::String(String::from("ERR_NATIVE_BINARY_NOT_SUPPORTED"))
);
let sync_stderr = guest["sync"]["stderr"]
.as_str()
.expect("sync stderr string");
assert!(
sync_stderr.contains("ERR_NATIVE_BINARY_NOT_SUPPORTED"),
"sync stderr should expose the explicit native-binary rejection: {sync_stderr}"
);
assert!(
sync_stderr.contains("ELF"),
"sync stderr should name the detected ELF format: {sync_stderr}"
);
assert!(
!sync_stderr.contains("CompileError"),
"sync stderr must not fall back to the WASM compile error: {sync_stderr}"
);
assert_eq!(
guest["async"]["code"],
Value::String(String::from("ERR_NATIVE_BINARY_NOT_SUPPORTED"))
);
let async_message = guest["async"]["message"]
.as_str()
.expect("async error message string");
assert!(
async_message.contains("ERR_NATIVE_BINARY_NOT_SUPPORTED"),
"async spawn error should preserve the explicit native-binary code: {async_message}"
);
assert!(
async_message.contains("ELF"),
"async spawn error should name the detected ELF format: {async_message}"
);
assert!(
!async_message.contains("CompileError"),
"async spawn error must not fall back to the WASM compile error: {async_message}"
);
}
#[test]
fn child_process_rejects_native_elf_binaries_before_wasm_compile() {
run_isolated_builtin_conformance_test("child-process-native-elf-reject");
}
fn child_process_kill_numeric_signals_match_host_node_impl() {
assert_node_available();
let cwd = temp_dir("builtin-child-process-kill-numeric-signal");
let entrypoint = cwd.join("entry.mjs");
write_fixture(
&entrypoint,
r#"
import childProcess from "node:child_process";
async function captureKill(signal) {
const child = childProcess.spawn("node", ["-e", "setInterval(() => {}, 1000)"]);
const killResult = child.kill(signal);
const signalCodeAfterKill = child.signalCode ?? null;
return await new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`spawn(node interval) kill(${String(signal)}) did not exit within 2s`));
}, 2000);
child.on("error", reject);
child.on("exit", (code, exitSignal) => {
clearTimeout(timer);
resolve({
killResult,
signalCodeAfterKill,
code: code ?? null,
signal: exitSignal ?? null,
signalCodeAfterExit: child.signalCode ?? null,
killed: child.killed,
});
});
});
}
console.log(JSON.stringify({
numeric: await captureKill(11),
alias: await captureKill("SIGIOT"),
}));
"#,
);
let host = run_host_probe(&cwd, &entrypoint);
let guest = run_guest_probe_with_config(
"child-process-kill-numeric-signal",
&cwd,
&entrypoint,
HashMap::new(),
wire_permissions_allow_all(),
&["child_process"],
);
assert_eq!(
guest,
host,
"guest child_process.kill signal mapping 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")
);
assert_eq!(
guest["numeric"]["signalCodeAfterExit"],
Value::String(String::from("SIGSEGV"))
);
}
#[test]
fn child_process_kill_numeric_signals_match_host_node() {
run_isolated_builtin_conformance_test("child-process-kill-numeric-signal");
}
fn child_process_abort_reports_sigabrt_impl() {
assert_node_available();
let cwd = temp_dir("builtin-child-process-abort-signal");
let entrypoint = cwd.join("entry.mjs");
write_fixture(
&entrypoint,
r#"
import childProcess from "node:child_process";
const child = childProcess.spawn("node", ["-e", "process.abort();"]);
const result = await new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error("spawn(node abort child) did not exit within 2s"));
}, 2000);
child.on("error", reject);
child.on("exit", (code, signal) => {
clearTimeout(timer);
resolve({
code: code ?? null,
signal: signal ?? null,
signalCodeAfterExit: child.signalCode ?? null,
killed: child.killed,
});
});
});
console.log(JSON.stringify(result));
"#,
);
let host = run_host_probe(&cwd, &entrypoint);
let guest = run_guest_probe_with_config(
"child-process-abort-signal",
&cwd,
&entrypoint,
HashMap::new(),
wire_permissions_allow_all(),
&["child_process"],
);
assert_eq!(guest["code"], host["code"]);
assert_eq!(guest["signal"], host["signal"]);
assert_eq!(guest["signalCodeAfterExit"], host["signalCodeAfterExit"]);
assert_eq!(guest["killed"], host["killed"]);
assert_eq!(guest["signal"], Value::String(String::from("SIGABRT")));
assert_eq!(
guest["signalCodeAfterExit"],
Value::String(String::from("SIGABRT"))
);
}
#[test]
fn child_process_abort_reports_sigabrt() {
run_isolated_builtin_conformance_test("child-process-abort-signal");
}
fn path_conformance_matches_host_node() {
assert_conformance(
"path",
r#"
import * as pathNs from "node:path";
const path = pathNs.default ?? pathNs;
console.log(JSON.stringify({
join: path.join("/virtual", "project", "file.txt"),
resolve: path.resolve("/virtual/root", "alpha", "..", "beta", "file.txt"),
dirname: path.dirname("/virtual/root/beta/file.txt"),
basename: path.basename("/virtual/root/beta/file.txt"),
extname: path.extname("/virtual/root/beta/file.txt"),
isAbsoluteFile: path.isAbsolute("/virtual/root/beta/file.txt"),
isAbsoluteRelative: path.isAbsolute("virtual/root/beta/file.txt"),
relative: path.relative("/virtual/root/alpha", "/virtual/root/beta/file.txt"),
normalize: path.normalize("/virtual//root/alpha/../beta//file.txt"),
}));
"#,
);
}
fn crypto_conformance_matches_host_node() {
assert_conformance(
"crypto",
r#"
import crypto from "node:crypto";
const random = crypto.randomBytes(16);
const uuid = crypto.randomUUID();
const ciphers = crypto.getCiphers();
const curves = crypto.getCurves();
console.log(JSON.stringify({
hashesIncludeSha256: crypto.getHashes().includes("sha256"),
ciphersIncludeAes256Cbc: ciphers.includes("aes-256-cbc"),
ciphersIncludeAes256Gcm: ciphers.includes("aes-256-gcm"),
ciphersSorted: ciphers.join(",") === [...ciphers].sort().join(","),
curvesIncludePrime256v1: curves.includes("prime256v1"),
curvesIncludeSecp384r1: curves.includes("secp384r1"),
curvesSorted: curves.join(",") === [...curves].sort().join(","),
sha256: crypto.createHash("sha256").update("secure-exec").digest("hex"),
hmacSha256: crypto.createHmac("sha256", "shared-secret").update("secure-exec").digest("hex"),
randomBytesLength: random.length,
randomBytesHexLength: random.toString("hex").length,
randomBytesAllZero: Array.from(random).every((value) => value === 0),
randomUuidValid: /^[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),
}));
"#,
);
}
fn crypto_extended_surface_matches_host_node() {
assert_conformance(
"crypto-extended",
r#"
import crypto from "node:crypto";
const cipherKey = Buffer.alloc(32, 7);
const cipherIv = Buffer.alloc(16, 9);
const cipherPlaintext = Buffer.from("secure-exec-crypto-surface", "utf8");
const cipher = crypto.createCipheriv("aes-256-cbc", cipherKey, cipherIv);
const encrypted = Buffer.concat([cipher.update(cipherPlaintext), cipher.final()]);
const decipher = crypto.createDecipheriv("aes-256-cbc", cipherKey, cipherIv);
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]).toString("utf8");
const pbkdf2Hex = await new Promise((resolve, reject) => {
crypto.pbkdf2("password", "salt", 10, 32, "sha256", (error, result) => {
if (error) {
reject(error);
return;
}
resolve(result.toString("hex"));
});
});
const scryptHex = await new Promise((resolve, reject) => {
crypto.scrypt("password", "salt", 32, { N: 1024, r: 8, p: 1 }, (error, result) => {
if (error) {
reject(error);
return;
}
resolve(result.toString("hex"));
});
});
const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", { modulusLength: 1024 });
const privatePem = privateKey.export({ format: "pem", type: "pkcs8" });
const publicPem = publicKey.export({ format: "pem", type: "spki" });
const importedPrivateKey = crypto.createPrivateKey(privatePem);
const importedPublicKey = crypto.createPublicKey(publicPem);
const signer = crypto.createSign("sha256");
signer.update("secure-exec-signature");
const signature = signer.sign(importedPrivateKey);
const verifier = crypto.createVerify("sha256");
verifier.update("secure-exec-signature");
const signatureVerified = verifier.verify(importedPublicKey, signature);
const oneShotSignature = crypto.sign("sha256", Buffer.from("secure-exec-signature"), importedPrivateKey);
const oneShotVerified = crypto.verify(
"sha256",
Buffer.from("secure-exec-signature"),
importedPublicKey,
oneShotSignature,
);
const rsaCiphertext = crypto.publicEncrypt(
{ key: importedPublicKey, padding: crypto.constants.RSA_PKCS1_PADDING },
Buffer.from("secure-exec-rsa", "utf8"),
);
const rsaPlaintext = crypto.privateDecrypt(
{ key: importedPrivateKey, padding: crypto.constants.RSA_PKCS1_PADDING },
rsaCiphertext,
).toString("utf8");
const secretKey = crypto.createSecretKey(Buffer.from("abcd", "utf8"));
const generatedHmacKey = crypto.generateKeySync("hmac", { length: 256 });
const generatedAesKey = crypto.generateKeySync("aes", { length: 128 });
const generatedPrime = crypto.generatePrimeSync(64, { bigint: true });
const groupAlice = crypto.getDiffieHellman("modp14");
const groupBob = crypto.getDiffieHellman("modp14");
groupAlice.generateKeys();
groupBob.generateKeys();
const groupSecretA = groupAlice.computeSecret(groupBob.getPublicKey());
const groupSecretB = groupBob.computeSecret(groupAlice.getPublicKey());
const ecdhAlice = crypto.createECDH("prime256v1");
const ecdhBob = crypto.createECDH("prime256v1");
ecdhAlice.generateKeys();
ecdhBob.generateKeys();
const ecdhSecretA = ecdhAlice.computeSecret(ecdhBob.getPublicKey());
const ecdhSecretB = ecdhBob.computeSecret(ecdhAlice.getPublicKey());
const x25519Alice = crypto.generateKeyPairSync("x25519");
const x25519Bob = crypto.generateKeyPairSync("x25519");
const x25519SecretA = crypto.diffieHellman({
privateKey: x25519Alice.privateKey,
publicKey: x25519Bob.publicKey,
});
const x25519SecretB = crypto.diffieHellman({
privateKey: x25519Bob.privateKey,
publicKey: x25519Alice.publicKey,
});
const generatedAsyncPair = await new Promise((resolve, reject) => {
crypto.generateKeyPair("rsa", { modulusLength: 1024 }, (error, publicKeyValue, privateKeyValue) => {
if (error) {
reject(error);
return;
}
resolve({
publicType: publicKeyValue.type,
privateType: privateKeyValue.type,
publicAsymmetricKeyType: publicKeyValue.asymmetricKeyType,
privateAsymmetricKeyType: privateKeyValue.asymmetricKeyType,
});
});
});
console.log(JSON.stringify({
cipherHex: encrypted.toString("hex"),
decipheredText: decrypted,
pbkdf2SyncHex: crypto.pbkdf2Sync("password", "salt", 10, 32, "sha256").toString("hex"),
pbkdf2Hex,
scryptSyncHex: crypto.scryptSync("password", "salt", 32, { N: 1024, r: 8, p: 1 }).toString("hex"),
scryptHex,
importedPrivateType: importedPrivateKey.type,
importedPrivateAsymmetricKeyType: importedPrivateKey.asymmetricKeyType,
importedPublicType: importedPublicKey.type,
importedPublicAsymmetricKeyType: importedPublicKey.asymmetricKeyType,
importedPrivateEquals: importedPrivateKey.equals(crypto.createPrivateKey(privatePem)),
importedPublicEquals: importedPublicKey.equals(crypto.createPublicKey(publicPem)),
signatureLength: signature.length,
signatureVerified,
oneShotSignatureLength: oneShotSignature.length,
oneShotVerified,
rsaCiphertextLength: rsaCiphertext.length,
rsaPlaintext,
secretKeyType: secretKey.type,
secretKeyExportHex: secretKey.export().toString("hex"),
generatedHmacKeyType: generatedHmacKey.type,
generatedHmacKeyLength: generatedHmacKey.export().length,
generatedAesKeyType: generatedAesKey.type,
generatedAesKeyLength: generatedAesKey.export().length,
generatedPrimeType: typeof generatedPrime,
generatedPrimePositive: generatedPrime > 0n,
groupVerifyError: groupAlice.verifyError,
groupSecretMatches: groupSecretA.equals(groupSecretB),
groupPrimeLength: groupAlice.getPrime().length,
ecdhSecretMatches: ecdhSecretA.equals(ecdhSecretB),
ecdhPublicKeyLength: ecdhAlice.getPublicKey().length,
x25519SecretMatches: x25519SecretA.equals(x25519SecretB),
x25519SecretLength: x25519SecretA.length,
generatedAsyncPair,
}));
"#,
);
}
#[test]
fn crypto_extended_surface_matches_host_node_isolated() {
run_isolated_builtin_conformance_test("crypto-extended");
}
fn events_conformance_matches_host_node() {
assert_conformance(
"events",
r#"
import { EventEmitter } from "node:events";
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const events = require("events");
const nodeEvents = require("node:events");
const emitter = new EventEmitter();
class DerivedEmitter extends require("events") {}
const derived = new DerivedEmitter();
const constructed = new (require("events"))();
const seen = [];
const metaNew = [];
const metaRemove = [];
const constructedSeen = [];
const derivedSeen = [];
const warningEvents = [];
function persistent(value) {
seen.push(`on:${value}`);
}
function onTick() {}
function onceTick() {}
function prependTick() {}
function prependOnceTick() {}
function removeFirst() {}
function removeSecond() {}
function removeThird() {}
function onceVisible() {}
emitter.on("newListener", (eventName, listener) => {
if (eventName === "newListener") {
return;
}
metaNew.push({
eventName,
listenerName: listener.name || "anon",
tickCountBefore: emitter.listenerCount("tick"),
tickListenersBefore: emitter.listeners("tick").map((fn) => fn.name || "anon"),
});
});
const removalEmitter = new EventEmitter();
removalEmitter.on("removeListener", (eventName, listener) => {
if (eventName === "removeListener") {
return;
}
metaRemove.push({
eventName,
listenerName: listener.name || "anon",
tickCountAfter: removalEmitter.listenerCount("tick"),
tickListenersAfter: removalEmitter.listeners("tick").map((fn) => fn.name || "anon"),
eventNamesAfter: removalEmitter.eventNames().sort(),
});
});
emitter.on("tick", persistent);
emitter.once("tick", (value) => {
seen.push(`once:${value}`);
});
emitter.on("tick", onTick);
emitter.once("tick", onceTick);
emitter.prependListener("tick", prependTick);
emitter.prependOnceListener("tick", prependOnceTick);
const listenerViewEmitter = new EventEmitter();
listenerViewEmitter.once("visible", onceVisible);
const visibleListeners = listenerViewEmitter.listeners("visible");
const visibleRawListeners = listenerViewEmitter.rawListeners("visible");
emitter.emit("tick", "alpha");
emitter.removeListener("tick", persistent);
emitter.emit("tick", "beta");
removalEmitter.on("tick", removeFirst);
removalEmitter.on("tick", removeSecond);
removalEmitter.on("pong", removeThird);
removalEmitter.removeListener("tick", removeSecond);
removalEmitter.removeAllListeners("tick");
removalEmitter.removeAllListeners();
constructed.on("ready", (value) => {
constructedSeen.push(`constructed:${value}`);
});
const constructedEmitHandled = constructed.emit("ready", "gamma");
derived.on("tick", (value) => {
derivedSeen.push(`derived:${value}`);
});
const derivedEmitHandled = derived.emit("tick", "delta");
process.on("warning", (warning) => {
warningEvents.push({
name: warning.name,
message: warning.message,
type: warning.type,
count: warning.count,
emitterMatches: warning.emitter === emitter,
});
});
for (let index = 0; index < 11; index += 1) {
emitter.on("warning-check", () => {});
}
emitter.once("warning-check", () => {});
emitter.prependListener("warning-check", () => {});
emitter.prependOnceListener("warning-check", () => {});
const zeroMaxListenersEmitter = new EventEmitter();
zeroMaxListenersEmitter.setMaxListeners(0);
for (let index = 0; index < 12; index += 1) {
zeroMaxListenersEmitter.on("disabled-warning-check", () => {});
}
await new Promise((resolve) => setTimeout(resolve, 0));
console.log(JSON.stringify({
bareEqualsNode: events === nodeEvents,
cjsEqualsEventEmitter: events === EventEmitter,
bareType: typeof events,
nodeType: typeof nodeEvents,
eventEmitterPropEqualsSelf: events.EventEmitter === events,
nodeEventEmitterPropEqualsSelf: nodeEvents.EventEmitter === nodeEvents,
constructedInstanceWorks: constructed instanceof EventEmitter,
constructedEmitHandled,
constructedSeen,
derivedInstanceWorks: derived instanceof EventEmitter,
derivedEmitHandled,
derivedSeen,
visibleListenersIsArray: Array.isArray(visibleListeners),
visibleRawListenersIsArray: Array.isArray(visibleRawListeners),
listenersUnwrapOnce: visibleListeners?.[0] === onceVisible,
rawListenersKeepWrapper: visibleRawListeners?.[0] !== onceVisible,
rawListenerTargetsOriginal: visibleRawListeners?.[0]?.listener === onceVisible,
metaNew,
metaRemove,
warningEvents,
seen,
listenerCount: emitter.listenerCount("tick"),
}));
"#,
);
}
fn stream_conformance_matches_host_node() {
assert_conformance(
"stream",
r#"
import { createRequire } from "node:module";
import * as streamNs from "node:stream";
const stream = streamNs.default ?? streamNs;
const require = createRequire(import.meta.url);
const cjsStream = require("stream");
class Source extends stream.Readable {
constructor() {
super();
this.sent = false;
}
_read() {
if (this.sent) {
return;
}
this.sent = true;
this.push("alpha");
this.push("beta");
this.push(null);
}
}
class Sink extends stream.Writable {
constructor(chunks) {
super();
this.chunks = chunks;
}
_write(chunk, _encoding, callback) {
this.chunks.push(Buffer.from(chunk).toString("utf8"));
callback();
}
}
class Upper extends stream.Transform {
_transform(chunk, _encoding, callback) {
callback(null, Buffer.from(chunk).toString("utf8").toUpperCase());
}
}
class IterableSource extends stream.Readable {
constructor(values) {
super({ objectMode: true });
this.values = [...values];
}
_read() {
if (this.values.length === 0) {
this.push(null);
return;
}
this.push(this.values.shift());
}
}
class RequiredIterableSource extends cjsStream.Readable {
constructor(values) {
super({ objectMode: true });
this.values = [...values];
}
_read() {
if (this.values.length === 0) {
this.push(null);
return;
}
this.push(this.values.shift());
}
}
const chunks = [];
const source = new Source();
const sink = new Sink(chunks);
const upper = new Upper();
let pipelineError = null;
const pipelineResult = stream.pipeline(source, upper, sink, (error) => {
pipelineError = error ? String(error.message || error) : null;
});
source._read();
await new Promise((resolve) => setTimeout(resolve, 0));
const iteratedValues = [];
for await (const chunk of new IterableSource(["gamma", "delta"])) {
iteratedValues.push(
Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk),
);
}
const requiredIteratedValues = [];
for await (const chunk of new RequiredIterableSource(["theta", "lambda"])) {
requiredIteratedValues.push(
Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk),
);
}
const selfCheckReadable = new IterableSource(["self-check"]);
const selfCheckIterator = selfCheckReadable[Symbol.asyncIterator]();
console.log(JSON.stringify({
output: chunks.join("|"),
pipelineReturnedSink: pipelineResult === sink,
pipelineError,
readableIsFunction: typeof stream.Readable === "function",
writableIsFunction: typeof stream.Writable === "function",
transformIsFunction: typeof stream.Transform === "function",
prototypeHasAsyncIterator:
typeof stream.Readable.prototype[Symbol.asyncIterator] === "function",
requiredPrototypeHasAsyncIterator:
typeof cjsStream.Readable.prototype[Symbol.asyncIterator] === "function",
readableIteratorReturnsSelf:
selfCheckIterator[Symbol.asyncIterator]() === selfCheckIterator,
iteratedValues,
requiredIteratedValues,
}));
"#,
);
}
fn buffer_conformance_matches_host_node() {
assert_conformance(
"buffer",
r#"
const text = Buffer.from("hello", "utf8");
const filled = Buffer.alloc(4, 0x61);
const combined = Buffer.concat([text, Buffer.from("-world", "utf8")]);
console.log(JSON.stringify({
fromHex: text.toString("hex"),
allocUtf8: filled.toString("utf8"),
concatUtf8: combined.toString("utf8"),
sliceUtf8: combined.slice(3, 8).toString("utf8"),
}));
"#,
);
}
fn buffer_concat_truncation_matches_host_node_impl() {
assert_conformance(
"buffer-concat-truncation",
r#"
function describeBuffer(value) {
return {
length: value.length,
hex: value.toString("hex"),
};
}
function describeError(fn) {
try {
fn();
return { threw: false };
} catch (error) {
return {
threw: true,
name: error?.name ?? null,
};
}
}
const chunks = [Buffer.from("abc"), Buffer.from("def")];
console.log(JSON.stringify({
smaller: describeBuffer(Buffer.concat(chunks, 4)),
exact: describeBuffer(Buffer.concat(chunks, 6)),
larger: describeBuffer(Buffer.concat(chunks, 8)),
emptyNonZero: describeBuffer(Buffer.concat([], 3)),
invalidEntry: describeError(() => Buffer.concat([Buffer.from("a"), "x"], 1)),
invalidList: describeError(() => Buffer.concat("nope", 1)),
}));
"#,
);
}
#[test]
fn buffer_concat_truncation_matches_host_node() {
run_isolated_builtin_conformance_test("buffer-concat-truncation");
}
fn mkdtemp_sync_collision_safe_matches_host_node_impl() {
let cwd = temp_dir("mkdtemp-sync-collision-safe");
let entrypoint = cwd.join("entry.mjs");
write_fixture(
&entrypoint,
r#"
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const root = fs.mkdtempSync(path.join(os.tmpdir(), "mkdtemp-conformance-"));
const prefix = path.join(root, "x-");
const sampleCount = 32;
const created = await Promise.all(
Array.from({ length: sampleCount }, () => Promise.resolve().then(() => fs.mkdtempSync(prefix)))
);
const result = {
createdCount: created.length,
uniqueCount: new Set(created).size,
basenameLengths: [...new Set(created.map((value) => path.basename(value).length))].sort(
(left, right) => left - right
),
prefixesOk: created.every((value) => value.startsWith(prefix)),
};
fs.rmSync(root, { recursive: true, force: true });
console.log(JSON.stringify(result));
"#,
);
let guest = run_guest_probe("mkdtemp-sync-collision-safe", &cwd, &entrypoint);
assert_eq!(guest["createdCount"], Value::from(32));
assert_eq!(guest["uniqueCount"], Value::from(32));
assert_eq!(guest["basenameLengths"], json!([8]));
assert_eq!(guest["prefixesOk"], Value::Bool(true));
}
#[test]
fn mkdtemp_sync_collision_safe_matches_host_node() {
run_isolated_builtin_conformance_test("mkdtemp-sync-collision-safe");
}
fn url_conformance_matches_host_node() {
assert_conformance(
"url",
r#"
import * as urlNs from "node:url";
const urlModule = urlNs.default ?? urlNs;
const URLSearchParamsCtor = urlNs.URLSearchParams ?? globalThis.URLSearchParams;
const url = new urlModule.URL("https://example.com/a/b?x=1&y=two#frag");
url.searchParams.append("z", "3");
const fileRelative = new urlModule.URL("file:.", "file:///tmp/base/entry.mjs");
const fileRelativeNoBase = new urlModule.URL("file:./child");
const plusDecoded = new URLSearchParamsCtor("?a=foo+bar");
const invalidPercentDecoded = new URLSearchParamsCtor("?a=%&b=%2&c=%GG&d=%E0%A4%A");
const sortable = new URLSearchParamsCtor([
["b", "1"],
["a", "first"],
["ä", "umlaut"],
["a", "second"],
["aa", "x"],
]);
sortable.sort();
const setSemantics = new URLSearchParamsCtor("a=1&b=2&a=3&a=4&c=5");
setSemantics.set("a", "z");
const parsed = urlModule.parse("https://example.com/a/b?x=1&y=two#frag", true);
console.log(JSON.stringify({
href: url.href,
searchParams: Array.from(url.searchParams.entries()),
plusDecoded: Array.from(plusDecoded.entries()),
plusDecodedString: plusDecoded.toString(),
invalidPercentDecoded: Array.from(invalidPercentDecoded.entries()),
invalidPercentDecodedString: invalidPercentDecoded.toString(),
sortedSearchParams: Array.from(sortable.entries()),
setSearchParams: Array.from(setSemantics.entries()),
setSearchParamsSize: setSemantics.size,
fileRelativeHref: fileRelative.href,
fileRelativeNoBaseHref: fileRelativeNoBase.href,
formatted: urlModule.format(parsed),
parsedPathname: parsed.pathname,
parsedQuery: parsed.query,
}));
"#,
);
}
fn stdlib_polyfill_conformance_matches_host_node() {
assert_conformance(
"stdlib-polyfills",
r#"
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const assert = require("node:assert");
const constants = require("node:constants");
const path = require("node:path");
const punycode = require("node:punycode");
const querystring = require("node:querystring");
const stringDecoder = require("node:string_decoder");
const util = require("node:util");
const utilTypes = require("node:util/types");
const zlib = require("node:zlib");
assert.deepStrictEqual(path.normalize?.("/alpha/../beta"), "/beta");
assert.notStrictEqual(1, 2);
assert.strictEqual(typeof assert.fail, "function");
let throwsCode = null;
assert.throws(
() => {
const error = new TypeError("boom");
error.code = "ERR_BOOM";
throw error;
},
(error) => {
throwsCode = error?.code ?? null;
return true;
},
);
let rejectsCode = null;
await assert.rejects(
Promise.reject(Object.assign(new Error("reject"), { code: "ERR_REJECT" })),
(error) => {
rejectsCode = error?.code ?? null;
return true;
},
);
const decoder = new stringDecoder.StringDecoder("utf8");
const textBytes = Buffer.from("Grüße", "utf8");
const decoded =
decoder.write(textBytes.subarray(0, 4)) +
decoder.end(textBytes.subarray(4));
const formatted = util.format("value:%s count:%d json:%j", "alpha", 7, { ok: true });
const promisified = await util.promisify((value, callback) => callback(null, value.toUpperCase()))("beta");
const encodedLength = new util.TextEncoder().encode("Grüße").length;
const decodedText = new util.TextDecoder().decode(textBytes);
const deflated = zlib.deflateSync(Buffer.from("secure-exec", "utf8"));
const inflated = zlib.inflateSync(deflated).toString("utf8");
console.log(JSON.stringify({
constants: {
fOk: constants.F_OK ?? null,
oRdOnly: constants.O_RDONLY ?? null,
rOk: constants.R_OK ?? null,
},
decoded,
decodedText,
deflatedBase64: deflated.toString("base64"),
encodedLength,
formatted,
inflated,
isArrayBufferView: util.types.isArrayBufferView(textBytes),
isDateViaUtilTypes: utilTypes.isDate(new Date("2024-01-01T00:00:00Z")),
isMapViaUtilTypes: utilTypes.isMap(new Map([["alpha", 1]])),
isUint8ArrayViaUtilTypes: utilTypes.isUint8Array(textBytes),
promisified,
punycodeAscii: punycode.toASCII("mañana.com"),
punycodeUnicode: punycode.toUnicode("xn--maana-pta.com"),
querystringParsed: querystring.parse("a=1&b=x&b=y"),
querystringStringified: querystring.stringify({ a: 1, b: ["x", "y"] }),
rejectsCode,
throwsCode,
}));
"#,
);
}
fn extended_builtin_polyfills_work_in_guest_v8() {
let result = run_guest_script(
"extended-builtins",
r#"
import os from "node:os";
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const moduleBuiltin = require("node:module");
const perfHooks = require("node:perf_hooks");
const streamConsumers = require("node:stream/consumers");
const streamPromises = require("node:stream/promises");
const timersPromises = require("node:timers/promises");
const tty = require("node:tty");
const zlib = require("node:zlib");
const { constants: zlibConstants } = await import("node:zlib");
perfHooks.performance.clearMarks?.();
perfHooks.performance.clearMeasures?.();
perfHooks.performance.mark("start");
await timersPromises.setTimeout(5);
perfHooks.performance.mark("end");
const measure = perfHooks.performance.measure("delta", "start", "end");
const immediateValue = await timersPromises.setImmediate("tick");
const timeoutValue = await timersPromises.setTimeout(1, "done");
const intervalValues = [];
const interval = timersPromises.setInterval(1, "pulse");
intervalValues.push((await interval.next()).value);
intervalValues.push((await interval.next()).value);
await interval.return();
function createSink() {
const listeners = new Map();
return {
chunks: [],
write(chunk, callback) {
this.chunks.push(Buffer.from(chunk).toString("utf8"));
callback?.(null);
},
end(callback) {
queueMicrotask(() => {
for (const handler of listeners.get("finish") ?? []) handler();
for (const handler of listeners.get("close") ?? []) handler();
callback?.(null);
});
},
once(event, handler) {
const entries = listeners.get(event) ?? [];
listeners.set(event, [...entries, handler]);
return this;
},
off(event, handler) {
const entries = listeners.get(event) ?? [];
listeners.set(
event,
entries.filter((candidate) => candidate !== handler),
);
return this;
},
};
}
const pipelineWritable = createSink();
await streamPromises.pipeline(
(async function* () {
yield Buffer.from("left");
yield Buffer.from("+");
yield Buffer.from("right");
})(),
pipelineWritable,
);
const finishedWritable = createSink();
const finishedResult = streamPromises.finished(finishedWritable).then(() => "resolved");
finishedWritable.end();
function makeAsyncStream(chunks) {
return (async function* () {
for (const chunk of chunks) {
yield chunk;
}
})();
}
const textValue = await streamConsumers.text(
makeAsyncStream([
Buffer.from("he"),
Buffer.from("llo"),
]),
);
const jsonValue = await streamConsumers.json(
makeAsyncStream([Buffer.from('{"ok":true,"count":2}')]),
);
const arrayBufferValue = await streamConsumers.arrayBuffer(
makeAsyncStream([Buffer.from("AB")]),
);
const blobValue = await streamConsumers.blob(
makeAsyncStream([Buffer.from("blob")]),
);
const bufferValue = await streamConsumers.buffer(
makeAsyncStream([Buffer.from("buf")]),
);
const deflated = zlib.deflateSync(Buffer.from("secure-exec", "utf8"));
const inflated = zlib.inflateSync(deflated).toString("utf8");
process.stdout.write(`${JSON.stringify({
moduleBuiltinHasCreateRequire:
typeof moduleBuiltin.createRequire === "function",
moduleBuiltinHasBuiltinModules:
Array.isArray(moduleBuiltin.builtinModules),
moduleBuiltinHasStreamPromises:
moduleBuiltin.builtinModules.includes("stream/promises"),
os: {
arch: os.arch(),
availableParallelism: os.availableParallelism(),
cpusLength: os.cpus().length,
eol: os.EOL,
freemem: os.freemem(),
hasSignals: typeof os.constants?.signals?.SIGTERM === "number",
homedir: os.homedir(),
hostname: os.hostname(),
networkInterfaceKeys: Object.keys(os.networkInterfaces()),
platform: os.platform(),
release: os.release(),
tmpdir: os.tmpdir(),
totalmem: os.totalmem(),
type: os.type(),
userInfoHomedir: os.userInfo().homedir,
},
perf: {
entriesByType: perfHooks.performance.getEntriesByType?.("measure")?.length ?? 0,
entriesByName: perfHooks.performance.getEntriesByName?.("delta", "measure")?.length ?? 0,
hasNow: typeof perfHooks.performance.now === "function",
hasObserver: typeof perfHooks.PerformanceObserver === "function",
measureDurationFinite: Number.isFinite(measure.duration),
},
streamConsumers: {
arrayBufferLength: arrayBufferValue.byteLength,
blobText: await blobValue.text(),
bufferText: bufferValue.toString("utf8"),
jsonCount: jsonValue.count,
jsonOk: jsonValue.ok,
textValue,
},
streamPromises: {
finishedResult: await finishedResult,
pipelineText: pipelineWritable.chunks.join(""),
},
timersPromises: {
immediateValue,
intervalValues,
timeoutValue,
},
tty: {
isatty0: tty.isatty(0),
isatty1: tty.isatty(1),
isatty2: tty.isatty(2),
readStreamType: typeof tty.ReadStream,
writeStreamType: typeof tty.WriteStream,
},
zlib: {
constantsHasSyncFlush: typeof zlib.constants?.Z_SYNC_FLUSH === "number",
importConstantsHasSyncFlush: typeof zlibConstants?.Z_SYNC_FLUSH === "number",
createDeflateType: typeof zlib.createDeflate,
createInflateType: typeof zlib.createInflate,
inflated,
},
})}\n`);
process.exit(0);
"#,
);
assert_eq!(result["moduleBuiltinHasCreateRequire"], true);
assert_eq!(result["moduleBuiltinHasBuiltinModules"], true);
assert_eq!(result["moduleBuiltinHasStreamPromises"], true);
assert_eq!(result["os"]["platform"], "linux");
assert_eq!(result["os"]["arch"], "x64");
assert_eq!(result["os"]["type"], "Linux");
assert!(result["os"]["homedir"]
.as_str()
.expect("os.homedir string")
.starts_with('/'));
assert_eq!(result["os"]["tmpdir"], "/tmp");
assert_eq!(result["os"]["userInfoHomedir"], result["os"]["homedir"]);
assert_eq!(result["os"]["eol"], "\n");
assert_eq!(result["os"]["availableParallelism"], 1);
assert_eq!(result["os"]["cpusLength"], 1);
assert_eq!(result["os"]["totalmem"], 134_217_728u64);
assert_eq!(result["os"]["freemem"], 134_217_728u64);
assert_eq!(result["os"]["hasSignals"], true);
assert!(result["os"]["networkInterfaceKeys"]
.as_array()
.expect("network interfaces array")
.is_empty());
assert_eq!(result["perf"]["hasNow"], true);
assert_eq!(result["perf"]["hasObserver"], true);
assert_eq!(result["perf"]["measureDurationFinite"], true);
assert_eq!(result["perf"]["entriesByType"], 1);
assert_eq!(result["perf"]["entriesByName"], 1);
assert_eq!(result["timersPromises"]["immediateValue"], "tick");
assert_eq!(result["timersPromises"]["timeoutValue"], "done");
assert_eq!(
result["timersPromises"]["intervalValues"]
.as_array()
.expect("interval values"),
&vec![Value::from("pulse"), Value::from("pulse")]
);
assert_eq!(result["streamPromises"]["pipelineText"], "left+right");
assert_eq!(result["streamPromises"]["finishedResult"], "resolved");
assert_eq!(result["streamConsumers"]["textValue"], "hello");
assert_eq!(result["streamConsumers"]["jsonOk"], true);
assert_eq!(result["streamConsumers"]["jsonCount"], 2);
assert_eq!(result["streamConsumers"]["arrayBufferLength"], 2);
assert_eq!(result["streamConsumers"]["blobText"], "blob");
assert_eq!(result["streamConsumers"]["bufferText"], "buf");
assert_eq!(result["tty"]["readStreamType"], "function");
assert_eq!(result["tty"]["writeStreamType"], "function");
assert_eq!(result["tty"]["isatty0"], false);
assert_eq!(result["tty"]["isatty1"], false);
assert_eq!(result["tty"]["isatty2"], false);
assert_eq!(result["zlib"]["constantsHasSyncFlush"], true);
assert_eq!(result["zlib"]["importConstantsHasSyncFlush"], true);
assert_eq!(result["zlib"]["createDeflateType"], "function");
assert_eq!(result["zlib"]["createInflateType"], "function");
assert_eq!(result["zlib"]["inflated"], "secure-exec");
}
fn timer_handle_ref_refresh_matches_host_node_impl() {
assert_node_available();
let cwd = temp_dir("builtin-timer-handle-ref-refresh");
let entrypoint = cwd.join("entry.mjs");
write_fixture(
&entrypoint,
r#"
import { performance } from "node:perf_hooks";
const timeout = setTimeout(() => {}, 1_000);
const interval = setInterval(() => {}, 1_000);
const initial = {
timeout: timeout.hasRef(),
interval: interval.hasRef(),
};
const unrefReturnSelf = timeout.unref() === timeout && interval.unref() === interval;
const afterUnref = {
timeout: timeout.hasRef(),
interval: interval.hasRef(),
};
const refReturnSelf = timeout.ref() === timeout && interval.ref() === interval;
const afterRef = {
timeout: timeout.hasRef(),
interval: interval.hasRef(),
};
clearTimeout(timeout);
clearInterval(interval);
const refreshDelay = 80;
const refreshWait = 40;
const refreshTolerance = 20;
const refreshStart = performance.now();
let refreshReturnSelf = false;
let refreshedAt = 0;
await new Promise((resolve) => {
const refreshedTimeout = setTimeout(() => {
const elapsed = performance.now() - refreshStart;
console.log(JSON.stringify({
initial,
unrefReturnSelf,
afterUnref,
refReturnSelf,
afterRef,
refreshReturnSelf,
refreshHonored: elapsed >= refreshedAt + refreshDelay - refreshTolerance,
}));
resolve();
}, refreshDelay);
setTimeout(() => {
refreshedAt = performance.now() - refreshStart;
refreshReturnSelf = refreshedTimeout.refresh() === refreshedTimeout;
}, refreshWait);
});
"#,
);
let host = run_host_probe(&cwd, &entrypoint);
let guest = run_guest_probe_with_config(
"timer-handle-ref-refresh",
&cwd,
&entrypoint,
HashMap::new(),
wire_permissions_allow_all(),
&["perf_hooks", "timers"],
);
assert_eq!(
guest,
host,
"guest timer handle behavior 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")
);
assert_eq!(guest["afterUnref"]["timeout"], Value::Bool(false));
assert_eq!(guest["afterUnref"]["interval"], Value::Bool(false));
assert_eq!(guest["afterRef"]["timeout"], Value::Bool(true));
assert_eq!(guest["afterRef"]["interval"], Value::Bool(true));
assert_eq!(guest["refreshHonored"], Value::Bool(true));
}
#[test]
fn timer_handle_ref_refresh_matches_host_node() {
run_isolated_builtin_conformance_test("timer-handle-ref-refresh");
}
fn unrefd_timeout_does_not_keep_guest_process_alive_impl() {
let cwd = temp_dir("builtin-timer-unref-exit");
let entrypoint = cwd.join("entry.mjs");
write_fixture(
&entrypoint,
r#"
const timer = setTimeout(() => {
console.error("timer-fired");
process.exitCode = 1;
}, 10_000);
timer.unref();
console.log(JSON.stringify({ hasRefAfterUnref: timer.hasRef() }));
"#,
);
let mut sidecar = new_sidecar("timer-unref-exit");
let connection_id = authenticate_wire(&mut sidecar, "conn-timer-unref-exit");
let session_id = open_session_wire(&mut sidecar, 2, &connection_id);
let allowed_builtins =
serde_json::to_string(&["timers"]).expect("serialize timer builtin allowlist");
let mut metadata = HashMap::new();
metadata.insert(
String::from("env.AGENTOS_ALLOWED_NODE_BUILTINS"),
allowed_builtins,
);
let vm_id = create_vm_with_metadata_and_permissions(
&mut sidecar,
3,
&connection_id,
&session_id,
GuestRuntimeKind::JavaScript,
&cwd,
metadata,
wire_permissions_allow_all(),
);
let started_at = Instant::now();
execute_wire(
&mut sidecar,
4,
&connection_id,
&session_id,
&vm_id,
"proc-timer-unref-exit",
GuestRuntimeKind::JavaScript,
&entrypoint,
Vec::new(),
);
let (stdout, stderr, exit_code) = collect_builtin_process_output_with_timeout(
&mut sidecar,
&connection_id,
&session_id,
&vm_id,
"proc-timer-unref-exit",
Duration::from_secs(2),
);
let elapsed = started_at.elapsed();
dispose_vm_and_close_session_wire(&mut sidecar, &connection_id, &session_id, &vm_id);
assert_eq!(exit_code, 0, "guest process should exit cleanly: {stderr}");
assert!(
stderr.trim().is_empty(),
"guest process should not wait long enough to fire the timer:\n{stderr}"
);
assert!(
elapsed < Duration::from_millis(1_500),
"guest process waited too long for an unref'd timer: {elapsed:?}"
);
let payload: Value = serde_json::from_str(stdout.trim()).expect("parse timer stdout JSON");
assert_eq!(payload["hasRefAfterUnref"], Value::Bool(false));
}
#[test]
fn unrefd_timeout_does_not_keep_guest_process_alive() {
run_isolated_builtin_conformance_test("timer-unref-exit");
}
fn run_named_case(case_name: &str) {
match case_name {
"fs" => fs_conformance_matches_host_node(),
"console" => console_conformance_matches_host_node(),
"child_process" => child_process_conformance_matches_host_node(),
"path" => path_conformance_matches_host_node(),
"crypto" => crypto_conformance_matches_host_node(),
"dns" => dns_conformance_matches_host_node(),
"events" => events_conformance_matches_host_node(),
"stream" => stream_conformance_matches_host_node(),
"buffer" => buffer_conformance_matches_host_node(),
"url" => url_conformance_matches_host_node(),
"stdlib_polyfill" => stdlib_polyfill_conformance_matches_host_node(),
"extended_builtin_polyfills" => extended_builtin_polyfills_work_in_guest_v8(),
other => panic!("unknown builtin conformance case: {other}"),
}
}
#[test]
fn builtin_conformance_cases() {
let current_exe = std::env::current_exe().expect("current test binary path");
for case_name in BUILTIN_CONFORMANCE_CASES {
let status = Command::new(¤t_exe)
.arg("--exact")
.arg("__builtin_conformance_case_runner")
.arg("--nocapture")
.env("AGENTOS_BUILTIN_CONFORMANCE_CASE", case_name)
.status()
.unwrap_or_else(|error| {
panic!("spawn builtin conformance runner for {case_name}: {error}")
});
assert!(
status.success(),
"builtin conformance case {case_name} failed with status {status}"
);
}
}
#[test]
fn __builtin_conformance_case_runner() {
let Ok(case_name) = std::env::var("AGENTOS_BUILTIN_CONFORMANCE_CASE") else {
return;
};
run_named_case(&case_name);
}
#[test]
fn __builtin_conformance_extra_test_runner() {
let Ok(test_name) = std::env::var("AGENTOS_BUILTIN_CONFORMANCE_EXTRA_TEST") else {
return;
};
match test_name.as_str() {
"http-request-keepalive" => http_request_custom_agent_reuses_keepalive_socket_impl(),
"http-request-denied" => http_request_denied_egress_returns_permission_error_impl(),
"child-process-fork-ipc" => child_process_fork_supports_basic_ipc_impl(),
"http-socket-writes" => http_socket_writes_do_not_silently_drop_data_impl(),
"buffer-concat-truncation" => buffer_concat_truncation_matches_host_node_impl(),
"mkdtemp-sync-collision-safe" => mkdtemp_sync_collision_safe_matches_host_node_impl(),
"crypto-extended" => crypto_extended_surface_matches_host_node(),
"child-process-exec-spawn-error-code" => {
child_process_exec_preserves_spawn_error_codes_impl()
}
"child-process-native-elf-reject" => {
child_process_rejects_native_elf_binaries_before_wasm_compile_impl()
}
"child-process-kill-numeric-signal" => {
child_process_kill_numeric_signals_match_host_node_impl()
}
"child-process-abort-signal" => child_process_abort_reports_sigabrt_impl(),
"net-socket-readable-state" => net_socket_readable_state_tracks_ssh2_writable_shape_impl(),
"readable-on-data-explicit-pause" => {
readable_on_data_respects_explicit_pause_matches_host_node_impl()
}
"readline-question" => readline_question_reads_real_stdin_impl(),
"vm-is-context" => vm_is_context_only_accepts_create_context_tagged_sandboxes_impl(),
"vm-context-isolation" => vm_context_isolation_and_script_options_match_host_node_impl(),
"vm-optional-surface" => {
vm_optional_surface_is_implemented_or_explicitly_not_implemented_impl()
}
"vm-timeout" => vm_timeout_terminates_within_deadline_impl(),
"perf-hooks-observer" => perf_hooks_observer_and_histogram_match_host_node_impl(),
"process-runtime-stats" => process_runtime_stats_are_live_impl(),
"os-resource-limits" => os_resource_limits_are_vm_scoped_impl(),
"timer-handle-ref-refresh" => timer_handle_ref_refresh_matches_host_node_impl(),
"timer-unref-exit" => unrefd_timeout_does_not_keep_guest_process_alive_impl(),
other => panic!("unknown builtin conformance extra test: {other}"),
}
}