diff --git a/crates/bridge/bridge-contract.json b/crates/bridge/bridge-contract.json index 97e0c20eff..cf0a55bf8e 100644 --- a/crates/bridge/bridge-contract.json +++ b/crates/bridge/bridge-contract.json @@ -119,6 +119,7 @@ "returnType": "unknown", "names": [ "_fsReadFile", + "_fsReadFileRaw", "_fsWriteFile", "_fsReadFileBinary", "_fsWriteFileBinary", @@ -143,11 +144,17 @@ "fs.closeSync", "fs.readSync", "_fsReadRaw", + "_fsReadIntoRaw", "fs.writeSync", "_fsWriteRaw", "_fsWritevRaw", "fs.fstatSync", - "fs.futimesSync" + "fs.futimesSync", + "fs.fchmodSync", + "fs.fchownSync", + "fs.ftruncateSync", + "fs.fsyncSync", + "fs.fdatasyncSync" ] }, { @@ -217,6 +224,7 @@ "process.memoryUsage", "process.resourceUsage", "process.versions", + "_compileFunctionForCJSLoader", "_vmCreateContext", "_vmRunInContext", "_vmRunInThisContext", @@ -614,6 +622,10 @@ "method": "fs.readFileSync", "translateArgs": false }, + "_fsReadFileRaw": { + "method": "fs.readFileSync", + "translateArgs": false + }, "_fsReadFileAsync": { "method": "fs.promises.readFile", "translateArgs": false @@ -1062,6 +1074,26 @@ "method": "fs.fstatSync", "translateArgs": false }, + "fs.fchmodSync": { + "method": "fs.fchmodSync", + "translateArgs": false + }, + "fs.fchownSync": { + "method": "fs.fchownSync", + "translateArgs": false + }, + "fs.ftruncateSync": { + "method": "fs.ftruncateSync", + "translateArgs": false + }, + "fs.fsyncSync": { + "method": "fs.fsyncSync", + "translateArgs": false + }, + "fs.fdatasyncSync": { + "method": "fs.fdatasyncSync", + "translateArgs": false + }, "fs.futimesSync": { "method": "fs.futimesSync", "translateArgs": false @@ -1078,6 +1110,10 @@ "method": "fs.readSync", "translateArgs": false }, + "_fsReadIntoRaw": { + "method": "fs.readSync", + "translateArgs": false + }, "fs.writeSync": { "method": "fs.writeSync", "translateArgs": false diff --git a/crates/execution/src/javascript.rs b/crates/execution/src/javascript.rs index 2bd0f3b7cf..e7a98d121f 100644 --- a/crates/execution/src/javascript.rs +++ b/crates/execution/src/javascript.rs @@ -3393,7 +3393,10 @@ fn spawn_v8_event_bridge( raw_bytes_args.insert(1, bytes); } } - if method == "_fsReadRaw" { + if matches!( + method.as_str(), + "_fsReadRaw" | "_fsReadIntoRaw" | "_fsReadFileRaw" + ) { raw_bytes_args.insert(usize::MAX, Vec::new()); } record_sync_bridge_phase( diff --git a/crates/native-sidecar/src/execution.rs b/crates/native-sidecar/src/execution.rs index aa8d43d83a..d930b47ec4 100644 --- a/crates/native-sidecar/src/execution.rs +++ b/crates/native-sidecar/src/execution.rs @@ -4,8 +4,9 @@ use agentos_vm_config as vm_config; use crate::filesystem::{ handle_python_vfs_rpc_request as filesystem_handle_python_vfs_rpc_request, - service_javascript_fs_read_sync_rpc, service_javascript_fs_readdir_raw_sync_rpc, - service_javascript_fs_sync_rpc, service_javascript_module_sync_rpc, + service_javascript_fs_read_file_raw_sync_rpc, service_javascript_fs_read_sync_rpc, + service_javascript_fs_readdir_raw_sync_rpc, service_javascript_fs_sync_rpc, + service_javascript_module_sync_rpc, }; use crate::protocol::{ CloseStdinRequest, EventFrame, EventPayload, ExecuteRequest, FindBoundUdpRequest, @@ -16715,6 +16716,12 @@ where let bytes = service_javascript_fs_read_sync_rpc(kernel, process, kernel_pid, request)?; return Ok(JavascriptSyncRpcServiceResponse::Raw(bytes)); } + if request.raw_bytes_args.contains_key(&usize::MAX) && request.method == "fs.readFileSync" { + let kernel_pid = process.kernel_pid; + let bytes = + service_javascript_fs_read_file_raw_sync_rpc(kernel, process, kernel_pid, request)?; + return Ok(JavascriptSyncRpcServiceResponse::Raw(bytes)); + } if request.method == "fs.readdirSync" { let kernel_pid = process.kernel_pid; let bytes = diff --git a/crates/native-sidecar/src/filesystem.rs b/crates/native-sidecar/src/filesystem.rs index 989f5b8651..bdafbeb7b6 100644 --- a/crates/native-sidecar/src/filesystem.rs +++ b/crates/native-sidecar/src/filesystem.rs @@ -16,7 +16,7 @@ use crate::service::{ log_stale_process_event, normalize_host_path, normalize_path, path_is_within_root, }; use crate::state::{ - ActiveExecutionEvent, ActiveProcess, BridgeError, SidecarKernel, VmState, + ActiveExecutionEvent, ActiveMappedHostFd, ActiveProcess, BridgeError, SidecarKernel, VmState, EXECUTION_DRIVER_NAME, EXECUTION_SANDBOX_ROOT_ENV, PYTHON_VFS_RPC_GUEST_ROOT, }; use crate::{DispatchResult, NativeSidecar, NativeSidecarBridge, SidecarError}; @@ -44,6 +44,7 @@ use agentos_native_sidecar_core::{ }; use nix::sys::stat::{utimensat, Mode, UtimensatFlags}; use nix::sys::time::TimeSpec; +use nix::unistd::{Gid, Uid}; use serde::Deserialize; use serde_json::{json, Map, Value}; use std::collections::{BTreeMap, BTreeSet}; @@ -166,11 +167,6 @@ impl AnchoredFd { .map_err(errno_to_io) } - /// `futimens` the resolved object. - fn set_times(&self, atime: &TimeSpec, mtime: &TimeSpec) -> std::io::Result<()> { - nix::sys::stat::futimens(self.as_raw_fd(), atime, mtime).map_err(errno_to_io) - } - /// Consume the handle, yielding the owned fd (e.g. to build a persistent /// [`std::fs::File`]). fn into_owned_fd(self) -> OwnedFd { @@ -1253,6 +1249,35 @@ pub(crate) fn service_javascript_fs_read_sync_rpc( .map_err(kernel_error) } +pub(crate) fn service_javascript_fs_read_file_raw_sync_rpc( + kernel: &mut SidecarKernel, + process: &ActiveProcess, + kernel_pid: u32, + request: &JavascriptSyncRpcRequest, +) -> Result, SidecarError> { + let path = javascript_sync_rpc_path_arg(process, &request.args, 0, "filesystem readFile path")?; + let path = path.as_str(); + if let Some(mapped_host) = mapped_runtime_host_path_for_read(process, path) { + materialize_mapped_host_path_from_kernel(kernel, kernel_pid, path, &mapped_host)?; + let opened = open_mapped_runtime_beneath( + &mapped_host, + "fs.readFile", + OFlag::O_RDONLY, + Mode::empty(), + )?; + return opened.handle.read_bytes().map_err(|error| { + SidecarError::Io(format!( + "failed to read mapped guest file {} -> {}: {error}", + path, + opened.host_path.display() + )) + }); + } + kernel + .read_file_for_process(EXECUTION_DRIVER_NAME, kernel_pid, path) + .map_err(kernel_error) +} + pub(crate) fn service_javascript_fs_sync_rpc( kernel: &mut SidecarKernel, process: &mut ActiveProcess, @@ -1489,6 +1514,70 @@ pub(crate) fn service_javascript_fs_sync_rpc( .map(javascript_sync_rpc_stat_value) .map_err(kernel_error) } + "fs.fchmodSync" => { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "filesystem fchmod fd")?; + let mode = javascript_sync_rpc_arg_u32(&request.args, 1, "filesystem fchmod mode")?; + if let Some(mapped) = process.mapped_host_fd(fd) { + nix::sys::stat::fchmod( + mapped.file.as_raw_fd(), + Mode::from_bits_truncate(mode as libc::mode_t), + ) + .map_err(|error| { + SidecarError::Io(format!( + "failed to chmod mapped guest fd {fd} -> {}: {error}", + mapped.path.display() + )) + })?; + if let Some(path) = mapped.guest_path.as_deref() { + match kernel.chmod(path, mode) { + Ok(()) => {} + Err(error) if error.code() == "ENOENT" => {} + Err(error) => return Err(kernel_error(error)), + } + } + return Ok(Value::Null); + } + let path = kernel + .fd_path(EXECUTION_DRIVER_NAME, kernel_pid, fd) + .map_err(kernel_error)?; + kernel + .chmod(&path, mode) + .map(|()| Value::Null) + .map_err(kernel_error) + } + "fs.fchownSync" => { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "filesystem fchown fd")?; + let uid = javascript_sync_rpc_arg_u32(&request.args, 1, "filesystem fchown uid")?; + let gid = javascript_sync_rpc_arg_u32(&request.args, 2, "filesystem fchown gid")?; + if let Some(mapped) = process.mapped_host_fd(fd) { + nix::unistd::fchown( + mapped.file.as_raw_fd(), + (uid != u32::MAX).then(|| Uid::from_raw(uid)), + (gid != u32::MAX).then(|| Gid::from_raw(gid)), + ) + .map_err(|error| { + SidecarError::Io(format!( + "failed to chown mapped guest fd {fd} -> {}: {error}", + mapped.path.display() + )) + })?; + if let Some(path) = mapped.guest_path.as_deref() { + match kernel.chown(path, uid, gid) { + Ok(()) => {} + Err(error) if error.code() == "ENOENT" => {} + Err(error) => return Err(kernel_error(error)), + } + } + return Ok(Value::Null); + } + let path = kernel + .fd_path(EXECUTION_DRIVER_NAME, kernel_pid, fd) + .map_err(kernel_error)?; + kernel + .chown(&path, uid, gid) + .map(|()| Value::Null) + .map_err(kernel_error) + } "fs.fsyncSync" | "fs.fdatasyncSync" => { let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "filesystem sync fd")?; if let Some(mapped) = process.mapped_host_fd(fd) { @@ -1531,11 +1620,6 @@ pub(crate) fn service_javascript_fs_sync_rpc( {MAX_MAPPED_TRUNCATE_BYTES} for mapped guest fd {fd}" ))); } - if let Some(guest_path) = mapped_guest_path.as_deref() { - kernel - .truncate(guest_path, length) - .map_err(|error| kernel_path_error("fs.ftruncate", guest_path, error))?; - } let mapped = process.mapped_host_fd_mut(fd).ok_or_else(|| { SidecarError::Io(format!("mapped guest fd {fd} disappeared during ftruncate")) })?; @@ -1543,7 +1627,20 @@ pub(crate) fn service_javascript_fs_sync_rpc( SidecarError::Io(format!("failed to truncate mapped guest fd {fd}: {error}")) })?; if let Some(guest_path) = mapped_guest_path.as_deref() { - mirror_kernel_path_to_process_shadow(kernel, process, kernel_pid, guest_path)?; + // A file created through this mapped fd may not have a kernel + // mirror until close/exit sync. Update a mirror when present; + // the descriptor-backed host file remains authoritative. + if kernel + .exists_for_process(EXECUTION_DRIVER_NAME, kernel_pid, guest_path) + .map_err(kernel_error)? + { + kernel.truncate(guest_path, length).map_err(|error| { + kernel_path_error("fs.ftruncate", guest_path, error) + })?; + mirror_kernel_path_to_process_shadow( + kernel, process, kernel_pid, guest_path, + )?; + } } return Ok(Value::Null); } @@ -2245,6 +2342,23 @@ pub(crate) fn service_javascript_fs_sync_rpc( let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "filesystem futimes fd")?; let atime = parse_utime_arg(&request.args, 1, "filesystem futimes atime")?; let mtime = parse_utime_arg(&request.args, 2, "filesystem futimes mtime")?; + if let Some(mapped) = process.mapped_host_fd(fd) { + let guest_path = mapped.guest_path.clone(); + apply_mapped_host_fd_utimens( + mapped, + atime, + mtime, + &format!("failed to update mapped guest fd {fd} times"), + )?; + if let Some(path) = guest_path { + match kernel.utimes_spec(&path, atime, mtime) { + Ok(()) => {} + Err(error) if error.code() == "ENOENT" => {} + Err(error) => return Err(kernel_error(error)), + } + } + return Ok(Value::Null); + } kernel .futimes(EXECUTION_DRIVER_NAME, kernel_pid, fd, atime, mtime) .map(|()| Value::Null) @@ -3118,10 +3232,31 @@ fn apply_anchored_fd_utimens( atime: VirtualUtimeSpec, mtime: VirtualUtimeSpec, context: &str, +) -> Result<(), SidecarError> { + apply_raw_fd_utimens(handle.as_raw_fd(), atime, mtime, context) +} + +/// Apply `futimens(3)` semantics to a persistent mapped guest fd. The open +/// descriptor, not its remembered path, is authoritative so rename/unlink after +/// open behaves like Linux: https://man7.org/linux/man-pages/man3/futimens.3.html +fn apply_mapped_host_fd_utimens( + mapped: &ActiveMappedHostFd, + atime: VirtualUtimeSpec, + mtime: VirtualUtimeSpec, + context: &str, +) -> Result<(), SidecarError> { + apply_raw_fd_utimens(mapped.file.as_raw_fd(), atime, mtime, context) +} + +fn apply_raw_fd_utimens( + fd: RawFd, + atime: VirtualUtimeSpec, + mtime: VirtualUtimeSpec, + context: &str, ) -> Result<(), SidecarError> { let existing = match (atime, mtime) { (VirtualUtimeSpec::Omit, _) | (_, VirtualUtimeSpec::Omit) => { - let stat = nix::sys::stat::fstat(handle.as_raw_fd()) + let stat = nix::sys::stat::fstat(fd) .map_err(|error| SidecarError::Io(format!("{context}: failed to stat: {error}")))?; Some(( VirtualTimeSpec { @@ -3148,8 +3283,7 @@ fn apply_anchored_fd_utimens( resolve_host_utime(atime, existing_atime), resolve_host_utime(mtime, existing_mtime), ]; - handle - .set_times(×[0], ×[1]) + nix::sys::stat::futimens(fd, ×[0], ×[1]) .map_err(|error| SidecarError::Io(format!("{context}: failed to set times: {error}"))) } @@ -3883,6 +4017,7 @@ pub(crate) fn service_javascript_fs_readdir_entries( mapped_runtime_host_path(process, path, false) { let mut typed: BTreeMap = BTreeMap::new(); + let mut directory_exists = false; match open_mapped_runtime_beneath( &mapped_host, "fs.readdir", @@ -3890,6 +4025,7 @@ pub(crate) fn service_javascript_fs_readdir_entries( Mode::empty(), ) { Ok(directory) => { + directory_exists = true; let entries = crate::plugins::host_dir::confine::read_dir(directory.handle.fd.as_fd()) .map_err(|error| { @@ -3926,18 +4062,29 @@ pub(crate) fn service_javascript_fs_readdir_entries( .unwrap_or(false) => {} Err(error) => return Err(error), } - match kernel.read_dir_with_types_for_process(EXECUTION_DRIVER_NAME, kernel_pid, path) { - Ok(entries) => { - for entry in entries { - typed.entry(entry.name).or_insert(entry.is_directory); + let missing_kernel_directory_error = + match kernel.read_dir_with_types_for_process(EXECUTION_DRIVER_NAME, kernel_pid, path) { + Ok(entries) => { + directory_exists = true; + for entry in entries { + typed.entry(entry.name).or_insert(entry.is_directory); + } + None } - } - Err(error) if matches!(error.code(), "ENOENT" | "ENOTDIR") => {} - Err(error) => return Err(kernel_error(error)), - } + Err(error) if matches!(error.code(), "ENOENT" | "ENOTDIR") => { + Some(kernel_error(error)) + } + Err(error) => return Err(kernel_error(error)), + }; for name in mapped_runtime_child_mount_basenames(process, path) { + directory_exists = true; typed.entry(name).or_insert(true); } + if !directory_exists { + return Err(missing_kernel_directory_error.expect( + "a missing mapped host directory must have a matching kernel directory error", + )); + } return Ok(typed); } diff --git a/crates/node-stdlib/adapter/inert-loader.js b/crates/node-stdlib/adapter/inert-loader.js index 9e0eef14cd..103944b01a 100644 --- a/crates/node-stdlib/adapter/inert-loader.js +++ b/crates/node-stdlib/adapter/inert-loader.js @@ -1,8 +1,8 @@ 'use strict'; -// M0 bootstraps Node's real realm and compiles every public builtin while the -// native bindings are still inert. Later milestones replace these property- -// readable adapters with real HOST/BRIDGE/WASM providers one binding at a time. +// Bootstrap Node's pinned realm in the guest context. Bindings not yet owned by +// the active migration milestone remain property-readable inert adapters; M1's +// filesystem and CJS-loader dependencies below are live bridge/HOST providers. (function installAgentOSNodeRealm(global) { try { const globalDescriptors = Object.getOwnPropertyDescriptors(global); @@ -178,6 +178,14 @@ '--preserve-symlinks': false, '--preserve-symlinks-main': false, '--pending-deprecation': false, + '--network-family-autoselection': true, + '--network-family-autoselection-attempt-timeout': 250, + '--experimental-detect-module': false, + '--inspect-brk': false, + '--inspect-wait': false, + '--experimental-transform-types': false, + '--experimental-strip-types': false, + '--experimental-import-meta-resolve': false, }); const options = { getCLIOptionsValues: () => cliOptions, @@ -378,12 +386,81 @@ isAscii: (input) => input.every((byte) => byte < 0x80), isUtf8: () => true, }; + const decoderState = new WeakMap(); + const stringDecoderBinding = { + encodings: ['ascii', 'utf8', 'base64', 'utf16le', 'latin1', 'hex', 'buffer', 'base64url'], + kIncompleteCharactersStart: 0, + kIncompleteCharactersEnd: 4, + kMissingBytes: 4, + kBufferedBytes: 5, + kEncodingField: 6, + kNumFields: 7, + kSize: 7, + decode(state, input) { + const encoding = state[6]; + if (encoding === 1) { + let decoder = decoderState.get(state); + if (!decoder) { + decoder = new TextDecoder('utf-8', { fatal: false }); + decoderState.set(state, decoder); + } + return decoder.decode(input, { stream: true }); + } + if (encoding === 0) return bufferBinding.asciiSlice(input, 0, input.length); + if (encoding === 2) return bufferBinding.base64Slice(input, 0, input.length); + if (encoding === 3) return bufferBinding.ucs2Slice(input, 0, input.length); + if (encoding === 4) return bufferBinding.latin1Slice(input, 0, input.length); + if (encoding === 5) return bufferBinding.hexSlice(input, 0, input.length); + if (encoding === 7) return bufferBinding.base64urlSlice(input, 0, input.length); + return bufferBinding.utf8Slice(input, 0, input.length); + }, + flush(state) { + const decoder = decoderState.get(state); + decoderState.delete(state); + return decoder ? decoder.decode() : ''; + }, + }; const util = { constants: { ALL_PROPERTIES: 0, ONLY_WRITABLE: 1, ONLY_ENUMERABLE: 2, ONLY_CONFIGURABLE: 4, - SKIP_STRINGS: 8, SKIP_SYMBOLS: 16 }, + SKIP_STRINGS: 8, SKIP_SYMBOLS: 16, kPending: 0, kRejected: 1 }, privateSymbols, - getOwnNonIndexProperties: (value) => Object.getOwnPropertyNames(value), + getOwnNonIndexProperties(value, filter = 0) { + const isArrayIndex = (property) => { + if (typeof property !== 'string' || property === '') return false; + const number = Number(property); + return Number.isInteger(number) && number >= 0 && number < 0xffff_ffff && String(number) === property; + }; + return Reflect.ownKeys(value).filter((property) => { + if (isArrayIndex(property) || property === 'length') return false; + if (typeof property === 'string' && (filter & 8) !== 0) return false; + if (typeof property === 'symbol' && (filter & 16) !== 0) return false; + const descriptor = Object.getOwnPropertyDescriptor(value, property); + if (!descriptor) return false; + if ((filter & 1) !== 0 && descriptor.writable !== true) return false; + if ((filter & 2) !== 0 && descriptor.enumerable !== true) return false; + if ((filter & 4) !== 0 && descriptor.configurable !== true) return false; + return true; + }); + }, isInsideNodeModules: () => false, + getPromiseDetails: () => undefined, + getProxyDetails: () => undefined, + previewEntries: () => [[], false], + getConstructorName: (value) => value?.constructor?.name, + getExternalValue: () => undefined, + guessHandleType: () => 'UNKNOWN', + sleep() {}, + getCallerLocation: () => undefined, + getCallSites: (frameCount = 10) => Array.from({ length: frameCount }, () => ({ + functionName: '', + scriptName: '', + lineNumber: 0, + columnNumber: 0, + isAsync: false, + isEval: false, + isNative: false, + isConstructor: false, + })), constructSharedArrayBuffer: (size) => new SharedArrayBuffer(size), defineLazyProperties(target, id, keys, enumerable = true) { for (const key of keys) Object.defineProperty(target, key, { @@ -417,22 +494,370 @@ get(target, property) { return Reflect.has(target, property) ? target[property] : 0; }, }); const constants = { - fs: numericConstants(), + fs: global._fsModule?.constants ?? numericConstants(), os: numericConstants(), crypto: numericConstants(), zlib: numericConstants({ BROTLI_PARAM_MODE: 0 }), }; + const UV_ERRNO = Object.freeze({ + EACCES: -13, EBADF: -9, EEXIST: -17, EINVAL: -22, EISDIR: -21, + ENOENT: -2, ENOTDIR: -20, ENOTEMPTY: -39, EPERM: -1, + }); + + function statArray(value, bigint = false) { + const milliseconds = (name) => Number(value[`${name}Ms`] ?? value[name]?.getTime?.() ?? 0); + const time = (name) => { + const ms = milliseconds(name); + return [Math.trunc(ms / 1000), Math.trunc((ms % 1000) * 1_000_000)]; + }; + const fields = [ + value.dev ?? 0, value.mode ?? 0, value.nlink ?? 0, value.uid ?? 0, + value.gid ?? 0, value.rdev ?? 0, value.blksize ?? 4096, value.ino ?? 0, + value.size ?? 0, value.blocks ?? Math.ceil(Number(value.size ?? 0) / 512), + ...time('atime'), ...time('mtime'), ...time('ctime'), ...time('birthtime'), + ]; + return bigint + ? BigInt64Array.from(fields, (entry) => BigInt(Math.trunc(Number(entry)))) + : Float64Array.from(fields, Number); + } + + function statFsArray(value, bigint = false) { + const fields = [ + value.type ?? 0, value.bsize ?? 4096, value.blocks ?? 0, + value.bfree ?? 0, value.bavail ?? 0, value.files ?? 0, value.ffree ?? 0, + ]; + return bigint + ? BigInt64Array.from(fields, (entry) => BigInt(Math.trunc(Number(entry)))) + : Float64Array.from(fields, Number); + } + + class FSReqCallback { + constructor(bigint = false) { + this.bigint = Boolean(bigint); + this.oncomplete = null; + this.context = undefined; + } + } + const kUsePromises = Symbol('agentos.fs.promises'); + + function wrapFsOperations(operations) { + const binding = Object.create(null); + for (const [name, operation] of Object.entries(operations)) { + binding[name] = function agentOSFsBindingOperation(...args) { + const requestIndex = args.findIndex( + (value) => value === kUsePromises || value instanceof FSReqCallback, + ); + if (requestIndex === -1) return operation(...args); + const request = args[requestIndex]; + args[requestIndex] = undefined; + if (request === kUsePromises) { + return new Promise((resolve, reject) => queueMicrotask(() => { + try { resolve(operation(...args)); } catch (error) { reject(error); } + })); + } + queueMicrotask(() => { + try { + const result = operation(...args); + if (result === undefined) request.oncomplete.call(request, null); + else request.oncomplete.call(request, null, result); + } + catch (error) { request.oncomplete.call(request, error); } + }); + return undefined; + }; + } + binding.FSReqCallback = FSReqCallback; + binding.kUsePromises = kUsePromises; + binding.statValues = new Float64Array(36); + binding.bigintStatValues = new BigInt64Array(36); + return binding; + } + + function makeFsBinding() { + const fs = global._fsModule; + if (!fs) return Object.create(makeInert()); + const fdValue = (fd) => { + if (typeof fd === 'number') return fd; + let received; + if (fd == null) received = ` Received ${fd}`; + else if (typeof fd === 'object') received = ` Received an instance of ${fd.constructor?.name ?? 'Object'}`; + else { + const inspected = typeof fd === 'string' ? `'${fd}'` : String(fd); + received = ` Received type ${typeof fd} (${inspected})`; + } + const error = new TypeError(`The "fd" argument must be of type number.${received}`); + error.code = 'ERR_INVALID_ARG_TYPE'; + throw error; + }; + const operations = { + access: (path, mode) => fs.accessSync(path, mode), + chmod: (path, mode) => fs.chmodSync(path, mode), + chown: (path, uid, gid) => fs.chownSync(path, uid, gid), + close: (fd) => fs.closeSync(fdValue(fd)), + copyFile: (source, destination, mode) => fs.copyFileSync(source, destination, mode), + existsSync: (path) => fs.existsSync(path), + fchmod: (fd, mode) => fs.fchmodSync(fdValue(fd), mode), + fchown: (fd, uid, gid) => fs.fchownSync(fdValue(fd), uid, gid), + fdatasync: (fd) => fs.fdatasyncSync(fdValue(fd)), + fstat: (fd, bigint) => statArray(fs.fstatSync(fdValue(fd)), bigint), + fsync: (fd) => fs.fsyncSync(fdValue(fd)), + ftruncate: (fd, length) => fs.ftruncateSync(fdValue(fd), length), + futimes: (fd, atime, mtime) => fs.futimesSync(fdValue(fd), atime, mtime), + lchown: (path, uid, gid) => fs.lchownSync(path, uid, gid), + link: (existingPath, newPath) => fs.linkSync(existingPath, newPath), + lstat: (path, bigint, _request, throwIfNoEntry = true) => { + try { return statArray(fs.lstatSync(path), bigint); } + catch (error) { if (!throwIfNoEntry && error?.code === 'ENOENT') return undefined; throw error; } + }, + lutimes: (path, atime, mtime) => fs.lutimesSync(path, atime, mtime), + mkdir: (path, mode, recursive) => fs.mkdirSync(path, { mode, recursive: Boolean(recursive) }), + mkdtemp: (prefix, encoding) => fs.mkdtempSync(prefix, { encoding: encoding || 'utf8' }), + open: (path, flags, mode) => fs.openSync(path, flags, mode), + read: (fd, buffer, offset, length, position) => fs.readSync(fdValue(fd), buffer, offset, length, position), + readBuffers: (fd, buffers, position) => fs.readvSync(fdValue(fd), buffers, position), + readFileUtf8: (pathOrFd, flags) => fs.readFileSync(pathOrFd, { encoding: 'utf8', flag: flags }), + readdir: (path, encoding, withFileTypes) => { + const entries = fs.readdirSync(path, { encoding: encoding || 'utf8', withFileTypes: true }); + const names = entries.map((entry) => entry.name); + if (!withFileTypes) return names; + return [names, entries.map((entry) => entry.isDirectory() ? 2 : entry.isSymbolicLink() ? 3 : 1)]; + }, + readlink: (path, encoding) => fs.readlinkSync(path, { encoding: encoding || 'utf8' }), + realpath: (path, encoding) => fs.realpathSync(path, { encoding: encoding || 'utf8' }), + rename: (oldPath, newPath) => fs.renameSync(oldPath, newPath), + rmSync: (path, maxRetries, recursive, retryDelay) => + fs.rmSync(path, { force: true, maxRetries, recursive: Boolean(recursive), retryDelay }), + rmdir: (path) => fs.rmdirSync(path), + stat: (path, bigint, _request, throwIfNoEntry = true) => { + try { return statArray(fs.statSync(path), bigint); } + catch (error) { if (!throwIfNoEntry && error?.code === 'ENOENT') return undefined; throw error; } + }, + statfs: (path, bigint) => statFsArray(fs.statfsSync(path), bigint), + symlink: (target, path, type) => fs.symlinkSync(target, path, type), + unlink: (path) => fs.unlinkSync(path), + utimes: (path, atime, mtime) => fs.utimesSync(path, atime, mtime), + writeBuffer: (fd, buffer, offset, length, position) => + fs.writeSync(fdValue(fd), buffer, offset, length, position), + writeBuffers: (fd, buffers, position) => fs.writevSync(fdValue(fd), buffers, position), + writeFileUtf8: (pathOrFd, data, flags, mode) => + fs.writeFileSync(pathOrFd, data, { encoding: 'utf8', flag: flags, mode }), + writeString: (fd, value, position, encoding) => fs.writeSync(fdValue(fd), value, position, encoding), + internalModuleStat(path) { + try { return fs.statSync(path).isDirectory() ? 1 : 0; } + catch (error) { return UV_ERRNO[error?.code] ?? -4094; } + }, + internalModuleReadJSON(path) { + try { return [fs.readFileSync(path, 'utf8'), false]; } + catch { return []; } + }, + }; + const binding = wrapFsOperations(operations); + const close = binding.close; + binding.close = (fd, request) => { + fdValue(fd); + return close(fd, request); + }; + binding.openFileHandle = (path, flags, mode, request) => { + if (request !== kUsePromises) throw new TypeError('openFileHandle requires kUsePromises'); + return binding.open(path, flags, mode, request).then((fd) => ({ + fd, + close: () => binding.close(fd, kUsePromises), + closeSync: () => operations.close(fd), + getAsyncId: () => 0, + })); + }; + return binding; + } + + function makeFsDirBinding() { + const fs = global._fsModule; + if (!fs) return Object.create(makeInert()); + const opendirSync = (path) => { + const entries = fs.readdirSync(path, { withFileTypes: true }); + let offset = 0; + let closed = false; + return { + read(_encoding, bufferSize, request) { + const operation = () => { + if (closed || offset >= entries.length) return null; + const result = []; + for (const entry of entries.slice(offset, offset + bufferSize)) { + result.push(entry.name, entry.isDirectory() ? 2 : entry.isSymbolicLink() ? 3 : 1); + } + offset += bufferSize; + return result; + }; + if (!(request instanceof FSReqCallback)) return operation(); + queueMicrotask(() => { + try { request.oncomplete.call(request, null, operation()); } + catch (error) { request.oncomplete.call(request, error); } + }); + return undefined; + }, + close(request) { + const operation = () => { closed = true; }; + if (!(request instanceof FSReqCallback)) return operation(); + queueMicrotask(() => { operation(); request.oncomplete.call(request, null); }); + return undefined; + }, + }; + }; + return { + opendirSync, + opendir(path, _encoding, request) { + queueMicrotask(() => { + try { request.oncomplete.call(request, null, opendirSync(path)); } + catch (error) { request.oncomplete.call(request, error); } + }); + }, + }; + } + + const fsBinding = makeFsBinding(); + const fsDirBinding = makeFsDirBinding(); + + function makeModulesBinding() { + const fs = global._fsModule; + const filePath = (value) => { + const string = String(value); + if (!string.startsWith('file:')) return string; + return decodeURIComponent(new URL(string).pathname); + }; + const dirname = (value) => { + const normalized = filePath(value).replace(/\/+$/, ''); + const slash = normalized.lastIndexOf('/'); + return slash <= 0 ? '/' : normalized.slice(0, slash); + }; + const serializePackage = (path) => { + if (!fs?.existsSync(path)) return undefined; + let data; + try { data = JSON.parse(fs.readFileSync(path, 'utf8')); } + catch { return undefined; } + const serializeMap = (value) => { + if (value === undefined) return null; + return typeof value === 'string' ? value : JSON.stringify(value); + }; + return [ + data.name ?? null, + data.main ?? null, + data.type ?? null, + serializeMap(data.imports), + serializeMap(data.exports), + path, + ]; + }; + const nearest = (value) => { + let current = dirname(value); + while (true) { + const path = `${current === '/' ? '' : current}/package.json`; + const packageData = serializePackage(path); + if (packageData !== undefined) return packageData; + if (current === '/') return undefined; + current = dirname(current); + } + }; + return { + readPackageJSON: (path) => serializePackage(filePath(path)), + getNearestParentPackageJSON: nearest, + getNearestParentPackageJSONType: (path) => nearest(path)?.[2] ?? 'none', + getPackageScopeConfig: nearest, + getPackageType: (url) => nearest(url)?.[2] ?? 'none', + enableCompileCache: () => 0, + getCompileCacheDir: () => undefined, + compileCacheStatus: Object.freeze({ FAILED: 0, ENABLED: 1, ALREADY_ENABLED: 2, DISABLED: 3 }), + flushCompileCache() {}, + getCompileCacheEntry: () => undefined, + saveCompileCacheEntry() {}, + cachedCodeTypes: Object.freeze({ + kStrippedTypeScript: 0, + kTransformedTypeScript: 1, + kTransformedTypeScriptWithSourceMaps: 2, + }), + moduleFormats: Object.freeze(['builtin', 'commonjs', 'json', 'module', 'wasm']), + setLazyPathHelpers() {}, + }; + } + + const modulesBinding = makeModulesBinding(); + + const moduleWrapEvaluated = Symbol('module_wrap.kEvaluated'); const concreteBindings = { builtins, - module_wrap: { ModuleWrap }, + module_wrap: { + ModuleWrap, + kEvaluated: moduleWrapEvaluated, + createRequiredModuleFacade: (namespace) => namespace, + }, errors, config, options, symbols: perIsolateSymbols, types, buffer: bufferBinding, + string_decoder: stringDecoderBinding, util, + fs: fsBinding, + fs_dir: fsDirBinding, + uv: { + getErrorMap: () => new Map(Object.entries(UV_ERRNO).map(([code, errno]) => [errno, [code, code]])), + errname: (errno) => Object.entries(UV_ERRNO).find(([, value]) => value === errno)?.[0] ?? `UNKNOWN(${errno})`, + ...Object.fromEntries(Object.entries(UV_ERRNO).map(([code, errno]) => [`UV_${code}`, errno])), + }, + permission: { isEnabled: () => false, has: () => true }, + credentials: { safeGetenv: (name) => process.env[name], getTempDir: () => process.env.TMPDIR || '/tmp' }, + contextify: (() => { + const contexts = new WeakSet(); + class ContextifyScript { + constructor(source, filename = 'evalmachine.') { + this.source = String(source); + this.filename = String(filename); + this.sourceMapURL = undefined; + this.sourceURL = undefined; + } + runInContext() { + return (0, eval)(`${this.source}\n//# sourceURL=${this.filename}`); + } + createCachedData() { return new Uint8Array(0); } + } + const compileFunctionForCJSLoader = (source, filename, isSeaMain, shouldDetectModule) => + typeof global._compileFunctionForCJSLoader === 'function' + ? global._compileFunctionForCJSLoader(source, filename, isSeaMain, shouldDetectModule) + : ({ + function: new Function( + 'exports', 'require', 'module', '__filename', '__dirname', + `${source}\n//# sourceURL=${filename}`, + ), + sourceMapURL: undefined, + sourceURL: undefined, + canParseAsESM: false, + }); + return { + ContextifyScript, + compileFunction: (source, _filename, _lineOffset, _columnOffset, _cachedData, + _produceCachedData, _parsingContext, _contextExtensions, params = []) => ({ + function: new Function(...params, source), + }), + containsModuleSyntax: () => false, + compileFunctionForCJSLoader, + makeContext: (context) => { + contexts.add(context); + context[privateSymbols.contextify_context_private_symbol] = context; + return context; + }, + isContext: (context) => contexts.has(context), + constants: Object.freeze({ + measureMemory: Object.freeze({ + mode: Object.freeze({ SUMMARY: 0, DETAILED: 1 }), + execution: Object.freeze({ DEFAULT: 0, EAGER: 1 }), + }), + }), + measureMemory: () => Promise.resolve({ + total: { jsMemoryEstimate: 0, jsMemoryRange: [0, 0] }, + }), + }; + })(), + modules: modulesBinding, serdes: { Serializer, Deserializer }, constants, async_wrap: asyncWrap, @@ -480,9 +905,22 @@ EventEmitter.call(process); } requireBuiltin('internal/util/debuglog').initializeDebugEnv(process.env.NODE_DEBUG); - if (global.__agentOSNodeLoadAll === true) { - requireBuiltin('internal/modules/cjs/loader').initializeCJS(); - } + requireBuiltin('internal/modules/cjs/loader').initializeCJS(); + + const publicFs = requireBuiltin('fs'); + const upstreamReadFileSync = publicFs.readFileSync; + const NodeBuffer = requireBuiltin('buffer').Buffer; + publicFs.readFileSync = function agentOSReadFileSync(path, options) { + const flag = options && typeof options === 'object' ? options.flag : undefined; + const encoding = typeof options === 'string' ? options : options?.encoding; + if (process.env.AGENTOS_BENCH_FS_READFILE_FAST_PATH === '0' || + typeof path !== 'string' || path.includes('\0') || + (flag !== undefined && flag !== 'r') || encoding != null) { + return upstreamReadFileSync.call(this, path, options); + } + const bytes = global._fsReadFileRaw(path); + return NodeBuffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength); + }; const excluded = new Set(['quic', 'sqlite']); const publicIds = builtinIds.filter((id) => !id.startsWith('internal/') && !excluded.has(id)); @@ -502,10 +940,9 @@ } } - // M0's sanctioned dual-loader window must not let Node's realm bootstrap - // replace the legacy bridge's live process methods. The real builtin loader - // retains its closure over this process object, while the guest-facing - // process surface is restored byte-for-byte for legacy execution. + // Node's realm bootstrap must not replace the bridge's live process methods. + // The builtin loader retains its closure over this process object, while the + // guest-facing process surface is restored byte-for-byte after installation. for (const key of Reflect.ownKeys(process)) { if (!Object.prototype.hasOwnProperty.call(processDescriptors, key)) { Reflect.deleteProperty(process, key); diff --git a/crates/node-stdlib/suite/ledger.json b/crates/node-stdlib/suite/ledger.json index a81ff580ab..14194f5b6f 100644 --- a/crates/node-stdlib/suite/ledger.json +++ b/crates/node-stdlib/suite/ledger.json @@ -1,128 +1,128 @@ { "schema": 1, "node": "24.15.0", - "scope": "M0 guest user-loader compatibility ledger; the isolated real realm require-all gate is separately 71/71", - "generated_at_pst": "2026-07-09T19:36:00-07:00", + "scope": "M1 guest user-loader compatibility ledger; real CJS loader requires every pinned public builtin (scheme-only modules via node:)", + "generated_at_pst": "2026-07-10T00:40:00-07:00", "cases": [ { "id": "_http_agent", "category": "public-builtin", "legacy": "fail-accepted", - "real": "fail-accepted", - "reason": "M0 sanctioned legacy user-loader exception; native realm load is green and M1 ports the user CJS loader", - "issue": "docs-internal/node-stdlib-replacement-spec.md#M1" + "real": "pass", + "reason": "Legacy user loader does not expose the full pinned Node public surface; the M1 real CJS loader is authoritative", + "issue": "docs-internal/node-stdlib-replacement-spec.md#M5" }, { "id": "_http_client", "category": "public-builtin", "legacy": "fail-accepted", - "real": "fail-accepted", - "reason": "M0 sanctioned legacy user-loader exception; native realm load is green and M1 ports the user CJS loader", - "issue": "docs-internal/node-stdlib-replacement-spec.md#M1" + "real": "pass", + "reason": "Legacy user loader does not expose the full pinned Node public surface; the M1 real CJS loader is authoritative", + "issue": "docs-internal/node-stdlib-replacement-spec.md#M5" }, { "id": "_http_common", "category": "public-builtin", "legacy": "fail-accepted", - "real": "fail-accepted", - "reason": "M0 sanctioned legacy user-loader exception; native realm load is green and M1 ports the user CJS loader", - "issue": "docs-internal/node-stdlib-replacement-spec.md#M1" + "real": "pass", + "reason": "Legacy user loader does not expose the full pinned Node public surface; the M1 real CJS loader is authoritative", + "issue": "docs-internal/node-stdlib-replacement-spec.md#M5" }, { "id": "_http_incoming", "category": "public-builtin", "legacy": "fail-accepted", - "real": "fail-accepted", - "reason": "M0 sanctioned legacy user-loader exception; native realm load is green and M1 ports the user CJS loader", - "issue": "docs-internal/node-stdlib-replacement-spec.md#M1" + "real": "pass", + "reason": "Legacy user loader does not expose the full pinned Node public surface; the M1 real CJS loader is authoritative", + "issue": "docs-internal/node-stdlib-replacement-spec.md#M5" }, { "id": "_http_outgoing", "category": "public-builtin", "legacy": "fail-accepted", - "real": "fail-accepted", - "reason": "M0 sanctioned legacy user-loader exception; native realm load is green and M1 ports the user CJS loader", - "issue": "docs-internal/node-stdlib-replacement-spec.md#M1" + "real": "pass", + "reason": "Legacy user loader does not expose the full pinned Node public surface; the M1 real CJS loader is authoritative", + "issue": "docs-internal/node-stdlib-replacement-spec.md#M5" }, { "id": "_http_server", "category": "public-builtin", "legacy": "fail-accepted", - "real": "fail-accepted", - "reason": "M0 sanctioned legacy user-loader exception; native realm load is green and M1 ports the user CJS loader", - "issue": "docs-internal/node-stdlib-replacement-spec.md#M1" + "real": "pass", + "reason": "Legacy user loader does not expose the full pinned Node public surface; the M1 real CJS loader is authoritative", + "issue": "docs-internal/node-stdlib-replacement-spec.md#M5" }, { "id": "_stream_duplex", "category": "public-builtin", "legacy": "fail-accepted", - "real": "fail-accepted", - "reason": "M0 sanctioned legacy user-loader exception; native realm load is green and M1 ports the user CJS loader", - "issue": "docs-internal/node-stdlib-replacement-spec.md#M1" + "real": "pass", + "reason": "Legacy user loader does not expose the full pinned Node public surface; the M1 real CJS loader is authoritative", + "issue": "docs-internal/node-stdlib-replacement-spec.md#M5" }, { "id": "_stream_passthrough", "category": "public-builtin", "legacy": "fail-accepted", - "real": "fail-accepted", - "reason": "M0 sanctioned legacy user-loader exception; native realm load is green and M1 ports the user CJS loader", - "issue": "docs-internal/node-stdlib-replacement-spec.md#M1" + "real": "pass", + "reason": "Legacy user loader does not expose the full pinned Node public surface; the M1 real CJS loader is authoritative", + "issue": "docs-internal/node-stdlib-replacement-spec.md#M5" }, { "id": "_stream_readable", "category": "public-builtin", "legacy": "fail-accepted", - "real": "fail-accepted", - "reason": "M0 sanctioned legacy user-loader exception; native realm load is green and M1 ports the user CJS loader", - "issue": "docs-internal/node-stdlib-replacement-spec.md#M1" + "real": "pass", + "reason": "Legacy user loader does not expose the full pinned Node public surface; the M1 real CJS loader is authoritative", + "issue": "docs-internal/node-stdlib-replacement-spec.md#M5" }, { "id": "_stream_transform", "category": "public-builtin", "legacy": "fail-accepted", - "real": "fail-accepted", - "reason": "M0 sanctioned legacy user-loader exception; native realm load is green and M1 ports the user CJS loader", - "issue": "docs-internal/node-stdlib-replacement-spec.md#M1" + "real": "pass", + "reason": "Legacy user loader does not expose the full pinned Node public surface; the M1 real CJS loader is authoritative", + "issue": "docs-internal/node-stdlib-replacement-spec.md#M5" }, { "id": "_stream_wrap", "category": "public-builtin", "legacy": "fail-accepted", - "real": "fail-accepted", - "reason": "M0 sanctioned legacy user-loader exception; native realm load is green and M1 ports the user CJS loader", - "issue": "docs-internal/node-stdlib-replacement-spec.md#M1" + "real": "pass", + "reason": "Legacy user loader does not expose the full pinned Node public surface; the M1 real CJS loader is authoritative", + "issue": "docs-internal/node-stdlib-replacement-spec.md#M5" }, { "id": "_stream_writable", "category": "public-builtin", "legacy": "fail-accepted", - "real": "fail-accepted", - "reason": "M0 sanctioned legacy user-loader exception; native realm load is green and M1 ports the user CJS loader", - "issue": "docs-internal/node-stdlib-replacement-spec.md#M1" + "real": "pass", + "reason": "Legacy user loader does not expose the full pinned Node public surface; the M1 real CJS loader is authoritative", + "issue": "docs-internal/node-stdlib-replacement-spec.md#M5" }, { "id": "_tls_common", "category": "public-builtin", "legacy": "fail-accepted", - "real": "fail-accepted", - "reason": "M0 sanctioned legacy user-loader exception; native realm load is green and M1 ports the user CJS loader", - "issue": "docs-internal/node-stdlib-replacement-spec.md#M1" + "real": "pass", + "reason": "Legacy user loader does not expose the full pinned Node public surface; the M1 real CJS loader is authoritative", + "issue": "docs-internal/node-stdlib-replacement-spec.md#M5" }, { "id": "_tls_wrap", "category": "public-builtin", "legacy": "fail-accepted", - "real": "fail-accepted", - "reason": "M0 sanctioned legacy user-loader exception; native realm load is green and M1 ports the user CJS loader", - "issue": "docs-internal/node-stdlib-replacement-spec.md#M1" + "real": "pass", + "reason": "Legacy user loader does not expose the full pinned Node public surface; the M1 real CJS loader is authoritative", + "issue": "docs-internal/node-stdlib-replacement-spec.md#M5" }, { "id": "assert/strict", "category": "public-builtin", "legacy": "fail-accepted", - "real": "fail-accepted", - "reason": "M0 sanctioned legacy user-loader exception; native realm load is green and M1 ports the user CJS loader", - "issue": "docs-internal/node-stdlib-replacement-spec.md#M1" + "real": "pass", + "reason": "Legacy user loader does not expose the full pinned Node public surface; the M1 real CJS loader is authoritative", + "issue": "docs-internal/node-stdlib-replacement-spec.md#M5" }, { "id": "assert", @@ -152,9 +152,9 @@ "id": "cluster", "category": "public-builtin", "legacy": "fail-accepted", - "real": "fail-accepted", - "reason": "M0 sanctioned legacy user-loader exception; native realm load is green and M1 ports the user CJS loader", - "issue": "docs-internal/node-stdlib-replacement-spec.md#M1" + "real": "pass", + "reason": "Legacy user loader does not expose the full pinned Node public surface; the M1 real CJS loader is authoritative", + "issue": "docs-internal/node-stdlib-replacement-spec.md#M5" }, { "id": "console", @@ -202,9 +202,9 @@ "id": "domain", "category": "public-builtin", "legacy": "fail-accepted", - "real": "fail-accepted", - "reason": "M0 sanctioned legacy user-loader exception; native realm load is green and M1 ports the user CJS loader", - "issue": "docs-internal/node-stdlib-replacement-spec.md#M1" + "real": "pass", + "reason": "Legacy user loader does not expose the full pinned Node public surface; the M1 real CJS loader is authoritative", + "issue": "docs-internal/node-stdlib-replacement-spec.md#M5" }, { "id": "events", @@ -246,17 +246,17 @@ "id": "inspector/promises", "category": "public-builtin", "legacy": "fail-accepted", - "real": "fail-accepted", - "reason": "M0 sanctioned legacy user-loader exception; native realm load is green and M1 ports the user CJS loader", - "issue": "docs-internal/node-stdlib-replacement-spec.md#M1" + "real": "pass", + "reason": "Legacy user loader does not expose the full pinned Node public surface; the M1 real CJS loader is authoritative", + "issue": "docs-internal/node-stdlib-replacement-spec.md#M5" }, { "id": "inspector", "category": "public-builtin", "legacy": "fail-accepted", - "real": "fail-accepted", - "reason": "M0 sanctioned legacy user-loader exception; native realm load is green and M1 ports the user CJS loader", - "issue": "docs-internal/node-stdlib-replacement-spec.md#M1" + "real": "pass", + "reason": "Legacy user loader does not expose the full pinned Node public surface; the M1 real CJS loader is authoritative", + "issue": "docs-internal/node-stdlib-replacement-spec.md#M5" }, { "id": "module", @@ -322,9 +322,9 @@ "id": "readline/promises", "category": "public-builtin", "legacy": "fail-accepted", - "real": "fail-accepted", - "reason": "M0 sanctioned legacy user-loader exception; native realm load is green and M1 ports the user CJS loader", - "issue": "docs-internal/node-stdlib-replacement-spec.md#M1" + "real": "pass", + "reason": "Legacy user loader does not expose the full pinned Node public surface; the M1 real CJS loader is authoritative", + "issue": "docs-internal/node-stdlib-replacement-spec.md#M5" }, { "id": "readline", @@ -336,17 +336,17 @@ "id": "repl", "category": "public-builtin", "legacy": "fail-accepted", - "real": "fail-accepted", - "reason": "M0 sanctioned legacy user-loader exception; native realm load is green and M1 ports the user CJS loader", - "issue": "docs-internal/node-stdlib-replacement-spec.md#M1" + "real": "pass", + "reason": "Legacy user loader does not expose the full pinned Node public surface; the M1 real CJS loader is authoritative", + "issue": "docs-internal/node-stdlib-replacement-spec.md#M5" }, { "id": "sea", "category": "public-builtin", "legacy": "fail-accepted", - "real": "fail-accepted", - "reason": "M0 sanctioned legacy user-loader exception; native realm load is green and M1 ports the user CJS loader", - "issue": "docs-internal/node-stdlib-replacement-spec.md#M1" + "real": "pass", + "reason": "Legacy user loader does not expose the full pinned Node public surface; the M1 real CJS loader is authoritative", + "issue": "docs-internal/node-stdlib-replacement-spec.md#M5" }, { "id": "stream/consumers", @@ -388,17 +388,17 @@ "id": "test/reporters", "category": "public-builtin", "legacy": "fail-accepted", - "real": "fail-accepted", - "reason": "M0 sanctioned legacy user-loader exception; native realm load is green and M1 ports the user CJS loader", - "issue": "docs-internal/node-stdlib-replacement-spec.md#M1" + "real": "pass", + "reason": "Legacy user loader does not expose the full pinned Node public surface; the M1 real CJS loader is authoritative", + "issue": "docs-internal/node-stdlib-replacement-spec.md#M5" }, { "id": "test", "category": "public-builtin", "legacy": "fail-accepted", - "real": "fail-accepted", - "reason": "M0 sanctioned legacy user-loader exception; native realm load is green and M1 ports the user CJS loader", - "issue": "docs-internal/node-stdlib-replacement-spec.md#M1" + "real": "pass", + "reason": "Legacy user loader does not expose the full pinned Node public surface; the M1 real CJS loader is authoritative", + "issue": "docs-internal/node-stdlib-replacement-spec.md#M5" }, { "id": "timers/promises", @@ -422,9 +422,9 @@ "id": "trace_events", "category": "public-builtin", "legacy": "fail-accepted", - "real": "fail-accepted", - "reason": "M0 sanctioned legacy user-loader exception; native realm load is green and M1 ports the user CJS loader", - "issue": "docs-internal/node-stdlib-replacement-spec.md#M1" + "real": "pass", + "reason": "Legacy user loader does not expose the full pinned Node public surface; the M1 real CJS loader is authoritative", + "issue": "docs-internal/node-stdlib-replacement-spec.md#M5" }, { "id": "tty", @@ -466,9 +466,9 @@ "id": "wasi", "category": "public-builtin", "legacy": "fail-accepted", - "real": "fail-accepted", - "reason": "M0 sanctioned legacy user-loader exception; native realm load is green and M1 ports the user CJS loader", - "issue": "docs-internal/node-stdlib-replacement-spec.md#M1" + "real": "pass", + "reason": "Legacy user loader does not expose the full pinned Node public surface; the M1 real CJS loader is authoritative", + "issue": "docs-internal/node-stdlib-replacement-spec.md#M5" }, { "id": "worker_threads", diff --git a/crates/v8-runtime/src/bridge.rs b/crates/v8-runtime/src/bridge.rs index 92591e0d77..b0eb352918 100644 --- a/crates/v8-runtime/src/bridge.rs +++ b/crates/v8-runtime/src/bridge.rs @@ -610,6 +610,49 @@ fn bridge_response_payload_to_v8<'s>( raw_bytes_to_uint8array(scope, payload) } +fn copy_raw_response_into_guest_view<'s>( + scope: &mut v8::HandleScope<'s>, + args: &v8::FunctionCallbackArguments<'s>, + payload: &[u8], +) -> Result, String> { + let value = args.get(3); + let view = v8::Local::::try_from(value) + .map_err(|_| "fs read destination must be an ArrayBufferView".to_string())?; + let offset = usize::try_from(args.get(4).uint32_value(scope).unwrap_or(0)) + .map_err(|_| "fs read destination offset must fit usize".to_string())?; + let buffer = view + .buffer(scope) + .ok_or_else(|| "fs read destination has no ArrayBuffer".to_string())?; + if buffer.was_detached() { + return Err("fs read destination ArrayBuffer is detached".to_string()); + } + if offset > view.byte_length() || payload.len() > view.byte_length() - offset { + return Err(format!( + "fs read response of {} bytes exceeds destination view length {} at offset {offset}", + payload.len(), + view.byte_length() + )); + } + if !payload.is_empty() { + let destination = view.data(); + if destination.is_null() { + return Err("fs read destination backing store is unavailable".to_string()); + } + // Node's synchronous fs binding parks the isolate for the duration of + // the host call, so V8 cannot detach or resize this backing store while + // the trusted runtime writes into it. Node's native contract is + // src/node_file.cc Read; backing-store API: V8 ArrayBuffer::GetBackingStore. + unsafe { + std::ptr::copy_nonoverlapping( + payload.as_ptr(), + destination.cast::().add(offset), + payload.len(), + ); + } + } + Ok(v8::Integer::new_from_unsigned(scope, payload.len() as u32).into()) +} + /// Serialize a V8 value to CBOR bytes. pub fn serialize_cbor_value( scope: &mut v8::HandleScope, @@ -1694,6 +1737,64 @@ fn vm_run_in_this_context_value<'s>( vm_run_script_in_context(scope, isolate_handle, context, &code, &options) } +fn compile_function_for_cjs_loader_value<'s>( + scope: &mut v8::HandleScope<'s>, + args: &mut v8::FunctionCallbackArguments<'s>, +) -> Result, String> { + let source_text = args.get(0).to_rust_string_lossy(scope); + let filename = args.get(1).to_rust_string_lossy(scope); + let source = v8::String::new(scope, &source_text) + .ok_or_else(|| String::from("CJS source exceeds the V8 string limit"))?; + let resource_name = v8::String::new(scope, &filename) + .ok_or_else(|| String::from("CJS filename exceeds the V8 string limit"))?; + let origin = v8::ScriptOrigin::new( + scope, + resource_name.into(), + 0, + 0, + false, + -1, + None, + false, + false, + false, + None, + ); + let mut compiler_source = v8::script_compiler::Source::new(source, Some(&origin)); + let parameter_names = ["exports", "require", "module", "__filename", "__dirname"] + .into_iter() + .map(|name| v8::String::new(scope, name).expect("static CJS parameter name")) + .collect::>(); + + // Node's native binding compiles the CommonJS body as a V8 function with + // the five wrapper parameters, then returns it in the result record consumed + // by lib/internal/modules/cjs/loader.js. Contract: Node v24 + // src/node_contextify.cc CompileFunctionForCJSLoader. + let function = v8::script_compiler::compile_function( + scope, + &mut compiler_source, + ¶meter_names, + &[], + v8::script_compiler::CompileOptions::NoCompileOptions, + v8::script_compiler::NoCacheReason::NoReason, + ) + .ok_or_else(|| String::from("V8 failed to compile the CommonJS module"))?; + + let result = v8::Object::new(scope); + for (name, value) in [ + ("function", function.into()), + ("sourceMapURL", v8::undefined(scope).into()), + ("sourceURL", v8::undefined(scope).into()), + ("canParseAsESM", v8::Boolean::new(scope, false).into()), + ] { + let key = v8::String::new(scope, name).expect("static CJS result key"); + if result.set(scope, key.into(), value) != Some(true) { + return Err(format!("failed to set CJS compile result field {name}")); + } + } + Ok(result.into()) +} + fn handle_local_bridge_call<'s>( scope: &mut v8::HandleScope<'s>, method: &str, @@ -1707,6 +1808,9 @@ fn handle_local_bridge_call<'s>( "_vmCreateContext" => vm_create_context_value(scope, args).map(Some), "_vmRunInContext" => vm_run_in_context_value(scope, args).map(Some), "_vmRunInThisContext" => vm_run_in_this_context_value(scope, args).map(Some), + "_compileFunctionForCJSLoader" => { + compile_function_for_cjs_loader_value(scope, args).map(Some) + } _ => Ok(None), } } @@ -1801,7 +1905,7 @@ fn sync_bridge_callback<'s>( } // Serialize V8 arguments using the Vec released by V8's serializer directly. - let encoded_args = match serialize_v8_args(scope, &args) { + let encoded_args = match serialize_v8_args_for_method(scope, &args, &data.method) { Ok(encoded_args) => encoded_args, Err(err) => { let msg = @@ -1815,6 +1919,17 @@ fn sync_bridge_callback<'s>( // Perform sync-blocking bridge call match ctx.sync_call_response(&data.method, encoded_args) { Ok(Some(response)) => { + if data.method == "_fsReadIntoRaw" && response.status == 2 { + match copy_raw_response_into_guest_view(scope, &args, &response.payload) { + Ok(value) => rv.set(value), + Err(error) => { + let msg = v8::String::new(scope, &error).unwrap(); + let exc = v8::Exception::type_error(scope, msg); + scope.throw_exception(exc); + } + } + return; + } let v8_val = bridge_response_payload_to_v8(scope, response.status, &response.payload); if let Some(val) = v8_val { rv.set(val); @@ -2087,6 +2202,24 @@ fn serialize_v8_args( serialize_v8_value(scope, array.into()) } +fn serialize_v8_args_for_method( + scope: &mut v8::HandleScope, + args: &v8::FunctionCallbackArguments, + method: &str, +) -> Result, String> { + if method != "_fsReadIntoRaw" { + return serialize_v8_args(scope, args); + } + // The destination view and offset are V8-local write targets, not request + // data. Omitting them prevents an O(n) serialization copy of the existing + // guest buffer before the sidecar performs the read. + let array = v8::Array::new(scope, 3); + for index in 0..3 { + array.set_index(scope, index, args.get(index as i32)); + } + serialize_v8_value(scope, array.into()) +} + /// Resolve or reject a pending async bridge promise by call_id. /// /// Called when a BridgeResponse arrives during the session event loop. diff --git a/crates/v8-runtime/src/session.rs b/crates/v8-runtime/src/session.rs index 1333e19ba7..2293a36f74 100644 --- a/crates/v8-runtime/src/session.rs +++ b/crates/v8-runtime/src/session.rs @@ -1673,28 +1673,44 @@ fn session_thread( ); } - // M0's sanctioned dual-loader window boots the inert real - // stdlib in an isolated realm. This proves the eager graph - // without letting Node's realm bootstrap mutate the legacy - // user-execution context; M1 replaces this with the real CJS - // loader and removes the isolation exception. + // The real stdlib must live in the user-execution context so + // Node's own CJS loader and builtin cache are the ones user + // code observes. The bootstrap snapshots/restores the legacy + // bridge globals it temporarily mutates while installing the + // pinned realm, then publishes __agentOSNodeStdlib here. if stdlib_flavor == StdlibFlavor::Real { - let stdlib_context = isolate::create_context(iso); let scope = &mut v8::HandleScope::new(iso); - let ctx = v8::Local::new(scope, &stdlib_context); + let ctx = v8::Local::new(scope, &exec_context); let scope = &mut v8::ContextScope::new(scope, ctx); - let fresh_bootstrap; - let stdlib_bootstrap = if from_snapshot { - agentos_node_stdlib::REAL_STDLIB_BOOTSTRAP_SOURCE - } else { - fresh_bootstrap = format!( - "{}\n{}\n{}", - agentos_node_stdlib::PROCESS_BOOTSTRAP_SOURCE, - agentos_node_stdlib::INERT_BINDING_BOOTSTRAP_SOURCE, - agentos_node_stdlib::REAL_STDLIB_BOOTSTRAP_SOURCE, - ); - &fresh_bootstrap - }; + // A restored snapshot already contains the bridge IIFE. + // Fresh fallback contexts must install it before the Node + // adapter so live fd facades are available during realm + // bootstrap. Do not run it again with user code below. + if !from_snapshot { + let (bridge_code, bridge_error) = + execution::run_init_script(scope, &snapshot_bridge_code); + if bridge_code != 0 { + let result_frame = RuntimeEvent::ExecutionResult { + session_id, + exit_code: bridge_code, + exports: None, + error: bridge_error.map(|error| ExecutionErrorBin { + error_type: error.error_type, + message: error.message, + stack: error.stack, + code: error.code.unwrap_or_default(), + }), + }; + send_event_with_generation( + &event_tx, + output_generation, + result_frame, + ); + continue; + } + } + let stdlib_bootstrap = + agentos_node_stdlib::REAL_STDLIB_BOOTSTRAP_SOURCE; let (stdlib_code, stdlib_error) = execution::run_init_script(scope, stdlib_bootstrap); if stdlib_code != 0 { @@ -1898,11 +1914,12 @@ fn session_thread( // On snapshot-restored context, skip bridge IIFE (already in // snapshot) and run user code only. On fresh context, run full // bridge code + user code as before. - let bridge_code_for_exec = if from_snapshot { - Cow::Borrowed("") - } else { - snapshot_bridge_code - }; + let bridge_code_for_exec = + if from_snapshot || stdlib_flavor == StdlibFlavor::Real { + Cow::Borrowed("") + } else { + snapshot_bridge_code + }; let file_path_opt = if file_path.is_empty() { None } else { diff --git a/docs-internal/node-stdlib-regression-ledger.json b/docs-internal/node-stdlib-regression-ledger.json index cb46e80497..8f6328119d 100644 --- a/docs-internal/node-stdlib-regression-ledger.json +++ b/docs-internal/node-stdlib-regression-ledger.json @@ -1,7 +1,25 @@ { "schema": 1, - "measured_at_pst": "2026-07-09T22:45:27-07:00", + "measured_at_pst": "2026-07-10T01:01:19-07:00", "entries": [ + { + "bench": "node-stdlib-m1-fs-transport", + "metric": "fs/readFileSync 4KiB backing-store p50", + "delta": "0.07ms measured vs 0.03ms budget", + "cause": "the one-call raw-byte path is bounded by the embedded sync-RPC round trip, whose independent 0.048ms target already exceeds this 0.03ms budget", + "disposition": "fix", + "issue": "docs-internal/node-stdlib-replacement-spec.md#M1", + "approver": "runtime lead" + }, + { + "bench": "node-stdlib-m1-fs-transport", + "metric": "fs/readFileSync 1MiB backing-store p50", + "delta": "0.58ms measured vs 0.42ms budget", + "cause": "the one-call raw-byte path removes open/stat/read/close chattiness and base64, but the host read plus response-channel and host-to-guest copy remain above the native-floor budget", + "disposition": "fix", + "issue": "docs-internal/node-stdlib-replacement-spec.md#M1", + "approver": "runtime lead" + }, { "bench": "node-stdlib-ab", "metric": "modules/import_npm_package guest p50", diff --git a/docs-internal/node-stdlib-replacement-spec.md b/docs-internal/node-stdlib-replacement-spec.md index cfe4b734d8..d5318dfcd5 100644 --- a/docs-internal/node-stdlib-replacement-spec.md +++ b/docs-internal/node-stdlib-replacement-spec.md @@ -993,6 +993,25 @@ Cutover track: | **M4** | crypto + TLS adapters over the **one shared OpenSSL 3.5.x wasm build (Decision 2)**, real `versions.openssl`, entropy host import, shared VM CA; migrate registry C consumers and delete per-tool TLS adapters; zlib/brotli/zstd (wasm, new encoder rules); child_process + **guest `node` command / self-spawn (Decision 3)**; os/process_methods, sqlite, nghttp2-wasm | respective categories preserve the complete legacy-passing set and ratchet native passes (§10.1); cross-runtime shared-TLS e2e green; crypto-blob + lazy-instantiation budgets met | | **M5** | ESM loader + vm/contextify completeness; native-V8 platform-tiering bootstrap profiles; flag default→real only after legacy functional/perf parity, then **flag removed**; **native deletion inventory executed** (incl. split-file surgery + `_crypto*`, host TLS, and host trust-store removal §12.1); bridge-call census (§8.2); final same-machine before/after report; docs/clients lockstep | §12.5 "done" | +### 13.1 M1 implementation evidence (2026-07-10 PST) + +- The exact non-watch filesystem slice is 77 pass / 100 fail-accepted / 40 + skip (217 total), up from 18 real passes at M0 with no ratcheted filesystem + pass lost. The public user-loader surface is 71/71 pass. +- The fd-level transport writes raw read responses into the parked guest + backing store and passes the 1,000-iteration allocation/detach/shared/ + resizable stress gate. The published five-run A/B is + `packages/runtime-benchmarks/results/node-stdlib-m1-fs-transport.json`. +- The one-call raw-byte `readFileSync` path removes base64 and fd-RPC + chattiness, but its measured 0.07ms / 0.58ms p50 remains above the + 0.03ms / 0.42ms native-floor budgets. Those two misses are explicit in + `node-stdlib-regression-ledger.json`; targets were not moved. +- A diagnostic full 3,950-test sweep produced 900 passes (+431 newly passing, + 230 earlier shim-backed passes awaiting their M2-M4 bindings), 1,762 + accepted failures, and 1,288 policy/resource skips. The full ledger refuses + to ratchet those later-category passes backward; this sweep is the ordered + restoration worklist for M2-M4, not an M1 ledger update. + Browser (Decision 4): source is retained but disabled from CI, release, and publish in M0. It has no migration deliverable or acceptance gate in this program and cannot block any milestone. diff --git a/packages/build-tools/bridge-src/builtins/fs.ts b/packages/build-tools/bridge-src/builtins/fs.ts index fbb387b8ea..3f4f595cc3 100644 --- a/packages/build-tools/bridge-src/builtins/fs.ts +++ b/packages/build-tools/bridge-src/builtins/fs.ts @@ -2216,13 +2216,17 @@ var _fdOpen = createBridgeSyncFacade("fs.openSync"); var _fdClose = createBridgeSyncFacade("fs.closeSync"); var _fdRead = createBridgeSyncFacade("fs.readSync"); var _fsReadRaw = createBridgeSyncFacade("_fsReadRaw"); +var _fsReadIntoRaw = createBridgeSyncFacade("_fsReadIntoRaw"); var _fdWrite = createBridgeSyncFacade("fs.writeSync"); var _fsWriteRaw = createBridgeSyncFacade("_fsWriteRaw"); var _fsWritevRaw = createBridgeSyncFacade("_fsWritevRaw"); var _fdFstat = createBridgeSyncFacade("fs.fstatSync"); var _fdFtruncate = createBridgeSyncFacade("fs.ftruncateSync"); var _fdFsync = createBridgeSyncFacade("fs.fsyncSync"); +var _fdFdatasync = createBridgeSyncFacade("fs.fdatasyncSync"); var _fdFutimes = createBridgeSyncFacade("fs.futimesSync"); +var _fdFchmod = createBridgeSyncFacade("fs.fchmodSync"); +var _fdFchown = createBridgeSyncFacade("fs.fchownSync"); var _fdGetPath = createBridgeSyncFacade("fs._getPathSync"); var _processUmask = createBridgeSyncFacade("process.umask"); var _processMemoryUsage = createBridgeSyncFacade("process.memoryUsage"); @@ -2924,10 +2928,33 @@ var fs = { } }, readSync(fd, buffer, offset, length, position) { + if (buffer?.buffer?.byteLength === 0 && Number(length ?? 0) > 0) { + const error = new TypeError("fs.readSync cannot write into a detached ArrayBuffer"); + error.code = "ERR_INVALID_ARG_TYPE"; + throw error; + } const normalized = normalizeReadSyncArgs(buffer, offset, length, position); + const backing = normalized.buffer.buffer; + const unstableBacking = backing?.detached === true || backing?.resizable === true || + (typeof SharedArrayBuffer !== "undefined" && backing instanceof SharedArrayBuffer); + if (unstableBacking) { + const error = new TypeError("fs.readSync requires a non-detached, fixed ArrayBuffer backing store"); + error.code = "ERR_INVALID_ARG_TYPE"; + throw error; + } + const transport = globalThis.process?.env?.AGENTOS_BENCH_FS_SYNC_READ_TRANSPORT; let bytes; try { - if (hasBridgeSyncFn("_fsReadRaw")) { + if (transport !== "base64" && transport !== "raw" && hasBridgeSyncFn("_fsReadIntoRaw")) { + return _fsReadIntoRaw.applySyncPromise(void 0, [ + fd, + normalized.length, + normalized.position ?? null, + normalized.buffer, + normalized.offset + ]); + } + if (transport !== "base64" && hasBridgeSyncFn("_fsReadRaw")) { const rawBytes = _fsReadRaw.applySyncPromise(void 0, [fd, normalized.length, normalized.position ?? null]); bytes = rawBytes instanceof Uint8Array ? rawBytes : import_buffer.Buffer.from(rawBytes); } else { @@ -3008,7 +3035,7 @@ var fs = { fdatasyncSync(fd) { normalizeFdInteger(fd); try { - _fdFsync.applySyncPromise(void 0, [fd]); + _fdFdatasync.applySyncPromise(void 0, [fd]); } catch (e) { const msg = e?.message ?? String(e); if (msg.includes("EBADF")) throw createFsError("EBADF", "EBADF: bad file descriptor, fdatasync", "fdatasync"); @@ -3079,19 +3106,18 @@ var fs = { }, fchmodSync(fd, mode) { const normalizedFd = normalizeFdInteger(fd); - const pathStr = _fdGetPath.applySync(void 0, [normalizedFd]); - if (!pathStr) { - throw createFsError("EBADF", "EBADF: bad file descriptor", "chmod"); - } - fs.chmodSync(pathStr, normalizeModeArgument(mode)); + bridgeCall(() => _fdFchmod.applySyncPromise(void 0, [ + normalizedFd, + normalizeModeArgument(mode) + ]), "fchmod"); }, fchownSync(fd, uid, gid) { const normalizedFd = normalizeFdInteger(fd); - const pathStr = _fdGetPath.applySync(void 0, [normalizedFd]); - if (!pathStr) { - throw createFsError("EBADF", "EBADF: bad file descriptor", "chown"); - } - fs.chownSync(pathStr, uid, gid); + bridgeCall(() => _fdFchown.applySyncPromise(void 0, [ + normalizedFd, + normalizeNumberArgument("uid", uid, { min: -1, max: 4294967295, allowNegativeOne: true }), + normalizeNumberArgument("gid", gid, { min: -1, max: 4294967295, allowNegativeOne: true }) + ]), "fchown"); }, lchownSync(path, uid, gid) { const pathStr = normalizePathLike(path); diff --git a/packages/build-tools/bridge-src/builtins/module-loader.ts b/packages/build-tools/bridge-src/builtins/module-loader.ts index 302a86aaab..fe0b657471 100644 --- a/packages/build-tools/bridge-src/builtins/module-loader.ts +++ b/packages/build-tools/bridge-src/builtins/module-loader.ts @@ -279,6 +279,18 @@ var moduleModule = Object.assign(Module, { exposeCustomGlobal("_moduleModule", moduleModule); function requireFrom(request, parentDir) { const parentPath = typeof parentDir === "string" ? parentDir : "/"; + const realStdlib = globalThis.__agentOSNodeStdlib; + if (realStdlib && typeof realStdlib.requireBuiltin === "function") { + if (Module.isBuiltin(request)) { + rejectRestrictedBuiltinRequest(request); + return realStdlib.requireBuiltin(request.replace(/^node:/, "")); + } + const realModule = realStdlib.requireBuiltin("module"); + const parentFilename = parentPath.endsWith("/") + ? parentPath + "__agentos_require__.cjs" + : parentPath + "/__agentos_require__.cjs"; + return realModule.createRequire(parentFilename)(request); + } if (Module.isBuiltin(request)) { try { return loadBuiltinModule(request); diff --git a/packages/build-tools/bridge-src/builtins/process.ts b/packages/build-tools/bridge-src/builtins/process.ts index f07df4e596..50b4169929 100644 --- a/packages/build-tools/bridge-src/builtins/process.ts +++ b/packages/build-tools/bridge-src/builtins/process.ts @@ -743,7 +743,7 @@ var process2 = { // Module info (will be set by createRequire) mainModule: void 0, // No-op methods for compatibility - emitWarning(warning) { + emitWarning(warning, typeOrOptions, code) { if (warning && typeof warning === "object") { if (typeof warning.message !== "string") { warning.message = String(warning.message ?? ""); @@ -754,9 +754,15 @@ var process2 = { _emit("warning", warning); return; } + const options = typeOrOptions && typeof typeOrOptions === "object" + ? typeOrOptions + : { type: typeOrOptions, code }; _emit("warning", { message: String(warning ?? ""), - name: "Warning" + name: typeof options.type === "string" && options.type.length > 0 + ? options.type + : "Warning", + ...(options.code === void 0 ? {} : { code: options.code }) }); }, binding(_name) { diff --git a/packages/build-tools/bridge-src/global-exposure.ts b/packages/build-tools/bridge-src/global-exposure.ts index 57fee81cb5..97d579325f 100644 --- a/packages/build-tools/bridge-src/global-exposure.ts +++ b/packages/build-tools/bridge-src/global-exposure.ts @@ -130,6 +130,11 @@ var NODE_CUSTOM_GLOBAL_INVENTORY = [ classification: "hardened", rationale: "Bridge-owned tls module handle for require resolution." }, + { + name: "_compileFunctionForCJSLoader", + classification: "hardened", + rationale: "V8-hosted CommonJS compilation bridge reference.", + }, { name: "_vmCreateContext", classification: "hardened", @@ -395,6 +400,11 @@ var NODE_CUSTOM_GLOBAL_INVENTORY = [ classification: "hardened", rationale: "Host filesystem bridge reference." }, + { + name: "_fsReadFileRaw", + classification: "hardened", + rationale: "Raw-byte host file read bridge reference." + }, { name: "_fsReadFileAsync", classification: "hardened", @@ -600,6 +610,31 @@ var NODE_CUSTOM_GLOBAL_INVENTORY = [ classification: "hardened", rationale: "Host filesystem bridge reference." }, + { + name: "fs.fchmodSync", + classification: "hardened", + rationale: "Host file-descriptor mode bridge reference." + }, + { + name: "fs.fchownSync", + classification: "hardened", + rationale: "Host file-descriptor ownership bridge reference." + }, + { + name: "fs.ftruncateSync", + classification: "hardened", + rationale: "Host file-descriptor truncate bridge reference." + }, + { + name: "fs.fsyncSync", + classification: "hardened", + rationale: "Host file-descriptor sync bridge reference." + }, + { + name: "fs.fdatasyncSync", + classification: "hardened", + rationale: "Host file-descriptor data-sync bridge reference." + }, { name: "fs.openSync", classification: "hardened", @@ -620,6 +655,11 @@ var NODE_CUSTOM_GLOBAL_INVENTORY = [ classification: "hardened", rationale: "Raw-byte host file-descriptor read bridge reference." }, + { + name: "_fsReadIntoRaw", + classification: "hardened", + rationale: "Direct guest-backing-store file-descriptor read bridge reference." + }, { name: "fs.writeSync", classification: "hardened", diff --git a/packages/runtime-benchmarks/package.json b/packages/runtime-benchmarks/package.json index fc995298ff..c4eb036e58 100644 --- a/packages/runtime-benchmarks/package.json +++ b/packages/runtime-benchmarks/package.json @@ -12,6 +12,7 @@ "bench:matrix": "tsx src/run-all.ts", "bench:node-stdlib-floor": "tsx src/node-stdlib/native-codec-floor.ts", "bench:node-stdlib-wasm": "node src/node-stdlib/wasm-lifecycle.mjs", + "bench:node-stdlib-fs-transport": "tsx src/node-stdlib/fs-transport-ab.ts", "bench:node-stdlib-ab": "tsx src/node-stdlib/ab-runner.ts", "bench:node-stdlib-delta": "tsx src/node-stdlib/delta-report.ts", "check-types": "pnpm --dir ../runtime-core build && tsc --noEmit" diff --git a/packages/runtime-benchmarks/results/node-stdlib-m1-fs-transport.json b/packages/runtime-benchmarks/results/node-stdlib-m1-fs-transport.json new file mode 100644 index 0000000000..c5badb9b50 --- /dev/null +++ b/packages/runtime-benchmarks/results/node-stdlib-m1-fs-transport.json @@ -0,0 +1,439 @@ +{ + "schema": 1, + "generatedAt": "2026-07-10T01:01:19.342-07:00", + "hardware": { + "cpu": "12th Gen Intel(R) Core(TM) i7-12700KF", + "cores": 20, + "ram": "62.6 GB", + "node": "v24.17.0", + "os": "Linux 6.1.0-41-amd64", + "arch": "x64" + }, + "protocol": { + "iterations": 25, + "warmup": 5, + "runs": 5, + "stdlib": "real", + "comparison": "fd-level CBOR/base64 response versus direct write into the guest backing store", + "budgetLane": "one-call raw-byte path-based readFileSync fast path" + }, + "lanes": { + "base64": { + "name": "base64-fd", + "transport": "base64", + "readFileFastPath": false, + "runs": [ + [ + { + "op": "fs_read_small", + "p50Ms": 0.06, + "p99Ms": 0.4, + "minMs": 0.06, + "maxMs": 0.4 + }, + { + "op": "fs_read_big", + "p50Ms": 0.62, + "p99Ms": 0.84, + "minMs": 0.49, + "maxMs": 0.84 + } + ], + [ + { + "op": "fs_read_small", + "p50Ms": 0.06, + "p99Ms": 0.08, + "minMs": 0.06, + "maxMs": 0.08 + }, + { + "op": "fs_read_big", + "p50Ms": 0.5, + "p99Ms": 0.73, + "minMs": 0.39, + "maxMs": 0.73 + } + ], + [ + { + "op": "fs_read_small", + "p50Ms": 0.08, + "p99Ms": 0.14, + "minMs": 0.06, + "maxMs": 0.14 + }, + { + "op": "fs_read_big", + "p50Ms": 0.53, + "p99Ms": 0.77, + "minMs": 0.42, + "maxMs": 0.77 + } + ], + [ + { + "op": "fs_read_small", + "p50Ms": 0.06, + "p99Ms": 0.08, + "minMs": 0.06, + "maxMs": 0.08 + }, + { + "op": "fs_read_big", + "p50Ms": 0.57, + "p99Ms": 1.06, + "minMs": 0.38, + "maxMs": 1.06 + } + ], + [ + { + "op": "fs_read_small", + "p50Ms": 0.06, + "p99Ms": 0.08, + "minMs": 0.06, + "maxMs": 0.08 + }, + { + "op": "fs_read_big", + "p50Ms": 0.51, + "p99Ms": 0.83, + "minMs": 0.47, + "maxMs": 0.83 + } + ] + ], + "rows": [ + { + "op": "fs_read_small", + "p50Ms": 0.06, + "p99Ms": 0.4, + "p50IqrMs": 0, + "runP50Ms": [ + 0.06, + 0.06, + 0.06, + 0.06, + 0.08 + ], + "runP99Ms": [ + 0.08, + 0.08, + 0.08, + 0.14, + 0.4 + ] + }, + { + "op": "fs_read_big", + "p50Ms": 0.53, + "p99Ms": 1.06, + "p50IqrMs": 0.06, + "runP50Ms": [ + 0.5, + 0.51, + 0.53, + 0.57, + 0.62 + ], + "runP99Ms": [ + 0.73, + 0.77, + 0.83, + 0.84, + 1.06 + ] + } + ] + }, + "backingStore": { + "name": "backing-store-fd", + "transport": "backing-store", + "readFileFastPath": false, + "runs": [ + [ + { + "op": "fs_read_small", + "p50Ms": 0.07, + "p99Ms": 0.26, + "minMs": 0.06, + "maxMs": 0.26 + }, + { + "op": "fs_read_big", + "p50Ms": 0.58, + "p99Ms": 0.88, + "minMs": 0.27, + "maxMs": 0.88 + } + ], + [ + { + "op": "fs_read_small", + "p50Ms": 0.07, + "p99Ms": 0.17, + "minMs": 0.06, + "maxMs": 0.17 + }, + { + "op": "fs_read_big", + "p50Ms": 0.5, + "p99Ms": 0.91, + "minMs": 0.37, + "maxMs": 0.91 + } + ], + [ + { + "op": "fs_read_small", + "p50Ms": 0.06, + "p99Ms": 0.08, + "minMs": 0.06, + "maxMs": 0.08 + }, + { + "op": "fs_read_big", + "p50Ms": 0.52, + "p99Ms": 0.95, + "minMs": 0.37, + "maxMs": 0.95 + } + ], + [ + { + "op": "fs_read_small", + "p50Ms": 0.1, + "p99Ms": 0.15, + "minMs": 0.06, + "maxMs": 0.15 + }, + { + "op": "fs_read_big", + "p50Ms": 0.51, + "p99Ms": 0.79, + "minMs": 0.46, + "maxMs": 0.79 + } + ], + [ + { + "op": "fs_read_small", + "p50Ms": 0.11, + "p99Ms": 0.13, + "minMs": 0.1, + "maxMs": 0.13 + }, + { + "op": "fs_read_big", + "p50Ms": 0.51, + "p99Ms": 1.27, + "minMs": 0.41, + "maxMs": 1.27 + } + ] + ], + "rows": [ + { + "op": "fs_read_small", + "p50Ms": 0.07, + "p99Ms": 0.26, + "p50IqrMs": 0.03, + "runP50Ms": [ + 0.06, + 0.07, + 0.07, + 0.1, + 0.11 + ], + "runP99Ms": [ + 0.08, + 0.13, + 0.15, + 0.17, + 0.26 + ] + }, + { + "op": "fs_read_big", + "p50Ms": 0.51, + "p99Ms": 1.27, + "p50IqrMs": 0.01, + "runP50Ms": [ + 0.5, + 0.51, + 0.51, + 0.52, + 0.58 + ], + "runP99Ms": [ + 0.79, + 0.88, + 0.91, + 0.95, + 1.27 + ] + } + ] + }, + "optimizedReadFile": { + "name": "optimized-read-file", + "transport": "backing-store", + "readFileFastPath": true, + "runs": [ + [ + { + "op": "fs_read_small", + "p50Ms": 0.06, + "p99Ms": 0.09, + "minMs": 0.06, + "maxMs": 0.09 + }, + { + "op": "fs_read_big", + "p50Ms": 0.58, + "p99Ms": 0.95, + "minMs": 0.47, + "maxMs": 0.95 + } + ], + [ + { + "op": "fs_read_small", + "p50Ms": 0.06, + "p99Ms": 0.08, + "minMs": 0.06, + "maxMs": 0.08 + }, + { + "op": "fs_read_big", + "p50Ms": 0.5, + "p99Ms": 0.68, + "minMs": 0.43, + "maxMs": 0.68 + } + ], + [ + { + "op": "fs_read_small", + "p50Ms": 0.1, + "p99Ms": 0.23, + "minMs": 0.1, + "maxMs": 0.23 + }, + { + "op": "fs_read_big", + "p50Ms": 0.5, + "p99Ms": 0.84, + "minMs": 0.39, + "maxMs": 0.84 + } + ], + [ + { + "op": "fs_read_small", + "p50Ms": 0.07, + "p99Ms": 0.08, + "minMs": 0.06, + "maxMs": 0.08 + }, + { + "op": "fs_read_big", + "p50Ms": 0.59, + "p99Ms": 0.89, + "minMs": 0.38, + "maxMs": 0.89 + } + ], + [ + { + "op": "fs_read_small", + "p50Ms": 0.1, + "p99Ms": 0.17, + "minMs": 0.06, + "maxMs": 0.17 + }, + { + "op": "fs_read_big", + "p50Ms": 0.59, + "p99Ms": 0.75, + "minMs": 0.39, + "maxMs": 0.75 + } + ] + ], + "rows": [ + { + "op": "fs_read_small", + "p50Ms": 0.07, + "p99Ms": 0.23, + "p50IqrMs": 0.04, + "runP50Ms": [ + 0.06, + 0.06, + 0.07, + 0.1, + 0.1 + ], + "runP99Ms": [ + 0.08, + 0.08, + 0.09, + 0.17, + 0.23 + ] + }, + { + "op": "fs_read_big", + "p50Ms": 0.58, + "p99Ms": 0.95, + "p50IqrMs": 0.09, + "runP50Ms": [ + 0.5, + 0.5, + 0.58, + 0.59, + 0.59 + ], + "runP99Ms": [ + 0.68, + 0.75, + 0.84, + 0.89, + 0.95 + ] + } + ] + } + }, + "transportDeltas": [ + { + "op": "fs_read_small", + "base64P50Ms": 0.06, + "backingStoreP50Ms": 0.07, + "ratio": 1.1667, + "reductionPercent": -16.67 + }, + { + "op": "fs_read_big", + "base64P50Ms": 0.53, + "backingStoreP50Ms": 0.51, + "ratio": 0.9623, + "reductionPercent": 3.77 + } + ], + "budgets": [ + { + "op": "fs_read_small", + "optimizedP50Ms": 0.07, + "budgetMs": 0.03, + "budgetMet": false + }, + { + "op": "fs_read_big", + "optimizedP50Ms": 0.58, + "budgetMs": 0.42, + "budgetMet": false + } + ] +} diff --git a/packages/runtime-benchmarks/src/node-stdlib/fs-transport-ab.ts b/packages/runtime-benchmarks/src/node-stdlib/fs-transport-ab.ts new file mode 100644 index 0000000000..160e45c0a7 --- /dev/null +++ b/packages/runtime-benchmarks/src/node-stdlib/fs-transport-ab.ts @@ -0,0 +1,165 @@ +import { execFileSync } from "node:child_process"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { getHardware, percentile, round } from "../lib/perf-utils.js"; +import { formatPacificIso } from "../lib/vm.js"; + +const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); +const iterations = Math.max( + 5, + Number(process.env.BENCH_NODE_STDLIB_ITERATIONS ?? 9), +); +const warmup = Math.max(1, Number(process.env.BENCH_NODE_STDLIB_WARMUP ?? 3)); +const runs = Math.max(5, Number(process.env.BENCH_NODE_STDLIB_RUNS ?? 5)); +const POST_TRANSPORT_BUDGET_MS = new Map([ + ["fs_read_small", 0.03], + ["fs_read_big", 0.42], +]); + +function lane( + name: "base64-fd" | "backing-store-fd" | "optimized-read-file", + transport: "base64" | "backing-store", + readFileFastPath: boolean, +) { + const stdout = execFileSync( + "pnpm", + [ + "--silent", + "--dir", + packageRoot, + "exec", + "tsx", + resolve(packageRoot, "src/run-all.ts"), + ], + { + cwd: packageRoot, + env: { + ...process.env, + AGENTOS_JS_STDLIB: "real", + AGENTOS_BENCH_FS_SYNC_READ_TRANSPORT: transport, + AGENTOS_BENCH_FS_READFILE_FAST_PATH: readFileFastPath ? "1" : "0", + BENCH_ITERATIONS: String(iterations), + BENCH_WARMUP: String(warmup), + BENCH_SHARED_VM: "1", + BENCH_NO_WRITE: "1", + BENCH_FAMILIES: "fs", + BENCH_OP_FILTER: "fs_read_small,fs_read_big", + }, + encoding: "utf8", + maxBuffer: 128 * 1024 * 1024, + stdio: ["ignore", "pipe", "inherit"], + }, + ); + const report = JSON.parse(stdout); + return { + name, + transport, + readFileFastPath, + rows: report.latency.map((row: any) => ({ + op: row.op, + payloadBytes: row.payloadBytes, + p50Ms: row.layers.guest.p50, + p99Ms: row.layers.guest.p99, + minMs: row.layers.guest.min, + maxMs: row.layers.guest.max, + })), + }; +} + +function aggregate(name: string, laneRuns: Array>) { + const operations = laneRuns[0].rows.map((row: any) => row.op); + return { + name, + transport: laneRuns[0].transport, + readFileFastPath: laneRuns[0].readFileFastPath, + runs: laneRuns.map((run) => run.rows), + rows: operations.map((op: string) => { + const rows = laneRuns.map((run) => + run.rows.find((row: any) => row.op === op), + ); + const p50s = rows + .map((row: any) => row.p50Ms) + .sort((a: number, b: number) => a - b); + const p99s = rows + .map((row: any) => row.p99Ms) + .sort((a: number, b: number) => a - b); + return { + op, + p50Ms: percentile(p50s, 50), + p99Ms: p99s.at(-1), + p50IqrMs: round(percentile(p50s, 75) - percentile(p50s, 25), 4), + runP50Ms: p50s, + runP99Ms: p99s, + }; + }), + }; +} + +const base64Runs: Array> = []; +const backingStoreRuns: Array> = []; +const optimizedReadFileRuns: Array> = []; +for (let run = 0; run < runs; run++) { + const first = run % 2 === 0 ? base64Runs : backingStoreRuns; + const second = run % 2 === 0 ? backingStoreRuns : base64Runs; + first.push( + run % 2 === 0 + ? lane("base64-fd", "base64", false) + : lane("backing-store-fd", "backing-store", false), + ); + second.push( + run % 2 === 0 + ? lane("backing-store-fd", "backing-store", false) + : lane("base64-fd", "base64", false), + ); + optimizedReadFileRuns.push( + lane("optimized-read-file", "backing-store", true), + ); +} +const base64 = aggregate("base64-fd", base64Runs); +const backingStore = aggregate("backing-store-fd", backingStoreRuns); +const optimizedReadFile = aggregate( + "optimized-read-file", + optimizedReadFileRuns, +); +const base64Rows = new Map(base64.rows.map((row: any) => [row.op, row])); + +console.log( + JSON.stringify( + { + schema: 1, + generatedAt: formatPacificIso(new Date()), + hardware: getHardware(), + protocol: { + iterations, + warmup, + runs, + stdlib: "real", + comparison: + "fd-level CBOR/base64 response versus direct write into the guest backing store", + budgetLane: "one-call raw-byte path-based readFileSync fast path", + }, + lanes: { base64, backingStore, optimizedReadFile }, + transportDeltas: backingStore.rows.map((row: any) => { + const before = base64Rows.get(row.op) as any; + return { + op: row.op, + base64P50Ms: before.p50Ms, + backingStoreP50Ms: row.p50Ms, + ratio: round(row.p50Ms / before.p50Ms, 4), + reductionPercent: round((1 - row.p50Ms / before.p50Ms) * 100, 2), + }; + }), + budgets: optimizedReadFile.rows.map((row: any) => { + const budgetMs = POST_TRANSPORT_BUDGET_MS.get(row.op); + return { + op: row.op, + optimizedP50Ms: row.p50Ms, + budgetMs, + budgetMet: budgetMs === undefined ? null : row.p50Ms <= budgetMs, + }; + }), + }, + null, + 2, + ), +); diff --git a/packages/runtime-benchmarks/src/run-all.ts b/packages/runtime-benchmarks/src/run-all.ts index 5e81ddb2f1..3204f0dbb4 100644 --- a/packages/runtime-benchmarks/src/run-all.ts +++ b/packages/runtime-benchmarks/src/run-all.ts @@ -50,16 +50,15 @@ import { pathToFileURL } from "node:url"; const RESULTS_DIR = new URL("../results/", import.meta.url).pathname; const ITERATIONS = Number(process.env.BENCH_ITERATIONS ?? 20); const WARMUP = Number(process.env.BENCH_WARMUP ?? 5); -const FAMILY_FILTER = process.env.BENCH_FAMILIES - ?.split(",") +const FAMILY_FILTER = process.env.BENCH_FAMILIES?.split(",") .map((family) => family.trim()) .filter(Boolean); -const OP_FILTER = process.env.BENCH_OP_FILTER - ?.split(",") +const OP_FILTER = process.env.BENCH_OP_FILTER?.split(",") .map((op) => op.trim()) .filter(Boolean); const COLD_MODE = process.env.BENCH_COLD === "1"; const SHARED_VM_MODE = process.env.BENCH_SHARED_VM === "1"; +const WRITE_RESULTS = process.env.BENCH_NO_WRITE !== "1"; interface LatencyMatrixRun { results: LatencyResult[]; @@ -76,38 +75,59 @@ export async function runLatencyMatrix(): Promise { const sidecar = resolveBenchSidecarProvenance(); console.error(formatSidecarProvenance(sidecar)); if (COLD_MODE) { - console.error("BENCH_COLD=1: VM prewarm is disabled; samples include first-use VM costs."); + console.error( + "BENCH_COLD=1: VM prewarm is disabled; samples include first-use VM costs.", + ); } if (SHARED_VM_MODE) { - console.error("BENCH_SHARED_VM=1: reusing a VM where op-specific VM options are not required."); + console.error( + "BENCH_SHARED_VM=1: reusing a VM where op-specific VM options are not required.", + ); } const wasmOptions = wasmLayerOptions(); if (!wasmOptions) { - console.error("vm-wasm lane disabled: native-baseline wasm artifact was not found"); + console.error( + "vm-wasm lane disabled: native-baseline wasm artifact was not found", + ); } const commandDirs = ecosystemWasmCommandDirs(); - const baseVmOptions = mergeBenchVmOptions({}, { - mounts: wasmOptions?.mounts, - wasmCommandDirs: [ - ...(wasmOptions?.wasmCommandDirs ?? []), - ...commandDirs, - ], - }); - const sharedVm = SHARED_VM_MODE ? await createBenchVm(baseVmOptions) : undefined; + const baseVmOptions = mergeBenchVmOptions( + {}, + { + mounts: wasmOptions?.mounts, + wasmCommandDirs: [ + ...(wasmOptions?.wasmCommandDirs ?? []), + ...commandDirs, + ], + }, + ); + const sharedVm = SHARED_VM_MODE + ? await createBenchVm(baseVmOptions) + : undefined; try { const results: LatencyResult[] = []; const ops = FAMILY_FILTER ? allOps.filter((op) => FAMILY_FILTER.includes(op.family)) : allOps; const filteredOps = OP_FILTER - ? ops.filter((op) => OP_FILTER.includes(op.name) || OP_FILTER.includes(`${op.family}/${op.name}`)) + ? ops.filter( + (op) => + OP_FILTER.includes(op.name) || + OP_FILTER.includes(`${op.family}/${op.name}`), + ) : ops; await primeMemoryBaselines(filteredOps); await runIdleVmMemorySelfCheck(baseVmOptions); for (const op of filteredOps) { console.error(`latency ${op.family}/${op.name}`); - if (!("runHostCmd" in op) && op.nativeOp && supportsWasmLayer(op.nativeOp)) { - console.error(" wasm lane: guest JS measured first, wasm native-baseline after"); + if ( + !("runHostCmd" in op) && + op.nativeOp && + supportsWasmLayer(op.nativeOp) + ) { + console.error( + " wasm lane: guest JS measured first, wasm native-baseline after", + ); } results.push(await runOneOp(op, baseVmOptions, sharedVm)); } @@ -129,7 +149,9 @@ async function main(): Promise { const matrix = await runLatencyMatrix(); const latency = matrix.results; const layerLatency = latency.filter(isLayerOpResult); - const nonPermissionsLatency = layerLatency.filter((result) => result.family !== "permissions"); + const nonPermissionsLatency = layerLatency.filter( + (result) => result.family !== "permissions", + ); const findings = findingsFromLatency(nonPermissionsLatency); const refuted = refutedFromLatency(nonPermissionsLatency); const permissionPolicyTax = permissionPolicyTaxFromLatency(layerLatency); @@ -138,7 +160,9 @@ async function main(): Promise { const fuzz = FAMILY_FILTER ? { programs: [], findings: [], refuted: [] } : await runFuzz({ iterations: ITERATIONS, warmup: WARMUP }); - const leak = FAMILY_FILTER ? { findings: [], streams: [] } : await runLeakSuite(); + const leak = FAMILY_FILTER + ? { findings: [], streams: [] } + : await runLeakSuite(); const footprint = FAMILY_FILTER ? { findings: [], components: [] } : await runFootprint(); @@ -169,23 +193,30 @@ async function main(): Promise { { family: "net", op: "udp_echo_small", - reason: "guest UDP datagrams are unsupported in the current kernel-backed V8 bridge", - evidence: "ERR_NOT_IMPLEMENTED: external UDP datagrams are not yet supported by the kernel-backed V8 bridge", + reason: + "guest UDP datagrams are unsupported in the current kernel-backed V8 bridge", + evidence: + "ERR_NOT_IMPLEMENTED: external UDP datagrams are not yet supported by the kernel-backed V8 bridge", }, ], critic_gaps: criticGaps(latency, fuzz, leak, footprint), }; - writeJson(`${RESULTS_DIR}/latency-matrix.json`, { - sidecar: matrix.sidecar, - matrixMode: matrix.mode, - wallTimeMs: matrix.wallTimeMs, - latency, - permissionPolicyTax, - }); - writeJson(`${RESULTS_DIR}/findings.json`, findingsJson); - const baselinePath = `${RESULTS_DIR}/baseline/findings-baseline.json`; - const diff = compareBaselineFile(`${RESULTS_DIR}/findings.json`, baselinePath); - writeJson(`${RESULTS_DIR}/regression-diff.json`, diff); + if (WRITE_RESULTS) { + writeJson(`${RESULTS_DIR}/latency-matrix.json`, { + sidecar: matrix.sidecar, + matrixMode: matrix.mode, + wallTimeMs: matrix.wallTimeMs, + latency, + permissionPolicyTax, + }); + writeJson(`${RESULTS_DIR}/findings.json`, findingsJson); + const baselinePath = `${RESULTS_DIR}/baseline/findings-baseline.json`; + const diff = compareBaselineFile( + `${RESULTS_DIR}/findings.json`, + baselinePath, + ); + writeJson(`${RESULTS_DIR}/regression-diff.json`, diff); + } printTable( [ @@ -308,7 +339,8 @@ async function withOpVm( sharedVm: BenchVm | undefined, callback: (vm: BenchVm, context?: unknown) => Promise, ): Promise { - const prepared = "prepareVm" in op && op.prepareVm ? await op.prepareVm() : undefined; + const prepared = + "prepareVm" in op && op.prepareVm ? await op.prepareVm() : undefined; const options = mergeBenchVmOptions(baseVmOptions, prepared?.options ?? {}); const canUseSharedVm = sharedVm && !prepared?.options; const vm = canUseSharedVm ? sharedVm : await createBenchVm(options); @@ -353,11 +385,15 @@ async function primeMemoryBaselines( } else { if (ops.some((op) => !("runHostCmd" in op) && op.nativeOp)) { const nativeBaseline = await primeNativeMemoryBaseline(); - console.error(`native memory startup baseline: ${formatBytes(nativeBaseline)}`); + console.error( + `native memory startup baseline: ${formatBytes(nativeBaseline)}`, + ); } if (ops.some((op) => !("runHostCmd" in op) && !op.runNode)) { const nodeBaseline = await primeNodeMemoryBaseline(); - console.error(`node memory startup baseline: ${formatBytes(nodeBaseline)}`); + console.error( + `node memory startup baseline: ${formatBytes(nodeBaseline)}`, + ); } } @@ -381,11 +417,14 @@ async function runIdleVmMemorySelfCheck( family: "self_check", name: "idle_prewarmed_vm", fileLine: "packages/benchmarks/src/run-all.ts", - reproducer: "prewarm VM, clear_refs=5, wait 2s, read VmHWM - baseline VmRSS", + reproducer: + "prewarm VM, clear_refs=5, wait 2s, read VmHWM - baseline VmRSS", }); const sampler = SidecarPeakMemorySampler.forVm(vm); if (!sampler) { - console.error("idle prewarmed VM memory self-check skipped: sidecar pid unavailable"); + console.error( + "idle prewarmed VM memory self-check skipped: sidecar pid unavailable", + ); return; } const memory = await sampler.measureIdle(2_000); @@ -406,7 +445,9 @@ function criticGaps( ): string[] { const gaps: string[] = []; const covered = new Set( - latency.filter(isLayerOpResult).map((result) => `${result.family}/${result.op}`), + latency + .filter(isLayerOpResult) + .map((result) => `${result.family}/${result.op}`), ); for (const required of [ "process/fanout_spawn_8", @@ -425,7 +466,9 @@ function criticGaps( gaps.push("fuzz did not confirm the non-P2 stdout fanout slow path"); } if (leak.streams.some((stream) => stream.idleMs < 61_000)) { - gaps.push("leak suite was run in smoke mode without waiting past 60s ZOMBIE_TTL"); + gaps.push( + "leak suite was run in smoke mode without waiting past 60s ZOMBIE_TTL", + ); } if (footprint.components?.length === 0) { gaps.push("footprint run did not emit component attribution"); diff --git a/packages/runtime-core/tests/integration/node-stdlib-fs-m1.test.ts b/packages/runtime-core/tests/integration/node-stdlib-fs-m1.test.ts new file mode 100644 index 0000000000..083b136a1d --- /dev/null +++ b/packages/runtime-core/tests/integration/node-stdlib-fs-m1.test.ts @@ -0,0 +1,280 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { + createIntegrationKernel, + type IntegrationKernelResult, +} from "@rivet-dev/agentos-vm-test-harness"; + +describe("Node stdlib M1 filesystem parity", () => { + let context: IntegrationKernelResult | undefined; + + afterEach(async () => { + await context?.dispose(); + }); + + it("reports ENOENT for sync and async readdir of a missing mapped path", async () => { + context = await createIntegrationKernel({ runtimes: ["wasmvm", "node"] }); + const script = String.raw` +const fs = require('node:fs'); +const missing = '/tmp/agentos-m1-missing-directory'; +const observed = {}; +try { fs.readdirSync(missing); } catch (error) { + observed.sync = { code: error.code, syscall: error.syscall, path: error.path }; +} +try { await fs.promises.readdir(missing); } catch (error) { + observed.async = { code: error.code, syscall: error.syscall, path: error.path }; +} +process.stdout.write(JSON.stringify(observed)); +`; + await context.vfs.writeFile( + "/tmp/node-stdlib-fs-readdir-missing.mjs", + script, + ); + const result = await context.kernel.exec( + "node /tmp/node-stdlib-fs-readdir-missing.mjs", + ); + + expect(result.exitCode, result.stderr).toBe(0); + expect(JSON.parse(result.stdout)).toEqual({ + sync: { + code: "ENOENT", + syscall: "scandir", + path: "/tmp/agentos-m1-missing-directory", + }, + async: { + code: "ENOENT", + syscall: "scandir", + path: "/tmp/agentos-m1-missing-directory", + }, + }); + }); + + it("uses the real Node CJS loader and fd-backed filesystem ABI", async () => { + context = await createIntegrationKernel({ runtimes: ["wasmvm", "node"] }); + await context.vfs.writeFile( + "/tmp/node-stdlib-m1-dependency.cjs", + ` +const fs = require('node:fs'); +module.exports = { + fs, + read(path) { return fs.readFileSync(path, 'utf8'); }, +}; +`, + ); + await context.vfs.writeFile( + "/tmp/node-stdlib-m1-acceptance.cjs", + ` +const fs = require('node:fs'); +const fsp = require('node:fs/promises'); +const dependency = require('./node-stdlib-m1-dependency.cjs'); + +(async () => { + const root = fs.mkdtempSync('/tmp/agentos-node-m1-'); + const source = root + '/source.bin'; + const copy = root + '/copy.bin'; + const fd = fs.openSync(source, 'w+'); + const first = Buffer.from([0, 1, 2, 3, 254, 255]); + fs.writeSync(fd, first, 0, first.length, 0); + fs.writevSync(fd, [Buffer.from([4, 5]), Buffer.from([6, 7])], 6); + const positional = Buffer.alloc(4); + fs.readSync(fd, positional, 0, positional.length, 2); + const vectors = [Buffer.alloc(3), Buffer.alloc(3)]; + fs.readvSync(fd, vectors, 0); + fs.ftruncateSync(fd, 8); + fs.fsyncSync(fd); + fs.fdatasyncSync(fd); + fs.futimesSync(fd, new Date(1_700_000_000_111), new Date(1_700_000_000_222)); + fs.fchmodSync(fd, 0o640); + const ownership = fs.fstatSync(fd); + fs.fchownSync(fd, ownership.uid, ownership.gid); + const beforeClose = fs.fstatSync(fd); + fs.closeSync(fd); + + fs.accessSync(source, fs.constants.R_OK | fs.constants.W_OK); + fs.utimesSync(source, new Date(1_700_000_000_333), new Date(1_700_000_000_444)); + fs.copyFileSync(source, copy, fs.constants.COPYFILE_EXCL); + const statfs = fs.statfsSync(root); + const stat = fs.statSync(copy); + const directory = fs.opendirSync(root); + const names = []; + for (let entry; (entry = directory.readSync()) !== null;) names.push(entry.name); + directory.closeSync(); + + const asyncFile = root + '/async.txt'; + const handle = await fsp.open(asyncFile, 'w+'); + await handle.write(Buffer.from('async-node-fs'), 0, 13, 0); + await handle.sync(); + await handle.truncate(5); + const asyncStat = await handle.stat(); + await handle.close(); + const asyncText = await fsp.readFile(asyncFile, 'utf8'); + const asyncNames = (await fsp.readdir(root)).sort(); + const closeCallback = await new Promise((resolve, reject) => { + fs.open(copy, 'r', (openError, closeFd) => { + if (openError) return reject(openError); + fs.close(closeFd, (...args) => { + const expected = [null]; + resolve({ + arrayPrototype: Object.getPrototypeOf(args) === Array.prototype, + expectedArrayPrototype: Object.getPrototypeOf(expected) === Array.prototype, + samePrototype: Object.getPrototypeOf(args) === Object.getPrototypeOf(expected), + constructorEqual: args.constructor === expected.constructor, + }); + }); + }); + }); + let invalidMode; + try { fs.openSync(source, 'r', 'boom'); } + catch (error) { invalidMode = { name: error.name, code: error.code, message: error.message }; } + let invalidAsyncMode; + try { fs.open(source, 'r', 'boom', () => {}); } + catch (error) { invalidAsyncMode = { name: error.name, code: error.code, message: error.message }; } + + process.stdout.write(JSON.stringify({ + dependencyFsSame: dependency.fs === fs, + dependencyRead: dependency.read(asyncFile), + positional: [...positional], + vectors: vectors.map((value) => [...value]), + bytes: [...fs.readFileSync(copy)], + beforeClose: { size: beforeClose.size, mode: beforeClose.mode & 0o777 }, + stat: { + size: stat.size, + inoPositive: stat.ino > 0, + nlinkPositive: stat.nlink > 0, + blksizePositive: stat.blksize > 0, + blocksNonNegative: stat.blocks >= 0, + }, + statfs: { bsizePositive: statfs.bsize > 0, blocksNonNegative: statfs.blocks >= 0 }, + realpath: fs.realpathSync(copy), + names: names.sort(), + asyncText, + asyncSize: asyncStat.size, + asyncNames, + invalidMode, + invalidAsyncMode, + closeCallback, + })); +})().catch((error) => { console.error(error); process.exitCode = 1; }); +`, + ); + + const result = await context.kernel.exec( + "node /tmp/node-stdlib-m1-acceptance.cjs", + ); + expect(result.exitCode, result.stderr).toBe(0); + const actual = JSON.parse(result.stdout); + expect(actual).toMatchObject({ + dependencyFsSame: true, + dependencyRead: "async", + positional: [2, 3, 254, 255], + vectors: [ + [0, 1, 2], + [3, 254, 255], + ], + bytes: [0, 1, 2, 3, 254, 255, 4, 5], + beforeClose: { size: 8, mode: 0o640 }, + stat: { + size: 8, + inoPositive: true, + nlinkPositive: true, + blksizePositive: true, + blocksNonNegative: true, + }, + statfs: { bsizePositive: true, blocksNonNegative: true }, + asyncText: "async", + asyncSize: 5, + invalidMode: { + name: "TypeError", + code: "ERR_INVALID_ARG_VALUE", + }, + invalidAsyncMode: { + name: "TypeError", + code: "ERR_INVALID_ARG_VALUE", + }, + closeCallback: { + arrayPrototype: true, + expectedArrayPrototype: true, + samePrototype: true, + constructorEqual: true, + }, + }); + expect(actual.realpath).toMatch(/^\/tmp\/agentos-node-m1-/); + expect(actual.names).toEqual(["copy.bin", "source.bin"]); + expect(actual.asyncNames).toEqual(["async.txt", "copy.bin", "source.bin"]); + }); + + it("keeps sync backing-store reads stable across allocation and detach stress", async () => { + context = await createIntegrationKernel({ runtimes: ["wasmvm", "node"] }); + const fixture = Uint8Array.from( + { length: 64 * 1024 }, + (_, index) => index & 0xff, + ); + await context.vfs.writeFile( + "/tmp/node-stdlib-m1-backing-store.bin", + fixture, + ); + await context.vfs.writeFile( + "/tmp/node-stdlib-m1-backing-store.cjs", + ` +const assert = require('node:assert'); +const fs = require('node:fs'); +const fd = fs.openSync('/tmp/node-stdlib-m1-backing-store.bin', 'r'); +const target = Buffer.alloc(1024, 0xaa); +let checksum = 0; +for (let iteration = 0; iteration < 1000; iteration++) { + const position = (iteration * 257) % (64 * 1024 - 257); + const bytesRead = fs.readSync(fd, target, 13, 257, position); + assert.strictEqual(bytesRead, 257); + assert.strictEqual(target[12], 0xaa); + assert.strictEqual(target[270], 0xaa); + assert.strictEqual(target[13], position & 0xff); + checksum = (checksum + target[13] + target[269]) >>> 0; + // Create collection pressure between parked sync reads. None of these + // allocations may move/detach the destination backing store mid-RPC. + Array.from({ length: 32 }, () => new Uint8Array(4096)); +} +fs.closeSync(fd); + +const detached = new Uint8Array(16); +if (typeof detached.buffer.transfer === 'function') detached.buffer.transfer(); +else structuredClone(detached.buffer, { transfer: [detached.buffer] }); +let detachedError; +try { fs.readSync(fs.openSync('/tmp/node-stdlib-m1-backing-store.bin', 'r'), detached, 0, 1, 0); } +catch (error) { detachedError = { name: error.name, code: error.code }; } + +let sharedError; +if (typeof SharedArrayBuffer === 'function') { + try { + const shared = new Uint8Array(new SharedArrayBuffer(16)); + fs.readSync(fs.openSync('/tmp/node-stdlib-m1-backing-store.bin', 'r'), shared, 0, 1, 0); + } catch (error) { sharedError = { name: error.name, code: error.code }; } +} + +let resizableError; +try { + const resizable = new Uint8Array(new ArrayBuffer(16, { maxByteLength: 32 })); + if (resizable.buffer.resizable) { + fs.readSync(fs.openSync('/tmp/node-stdlib-m1-backing-store.bin', 'r'), resizable, 0, 1, 0); + } +} catch (error) { resizableError = { name: error.name, code: error.code }; } + +process.stdout.write(JSON.stringify({ checksum, detachedError, sharedError, resizableError })); +`, + ); + + const result = await context.kernel.exec( + "node /tmp/node-stdlib-m1-backing-store.cjs", + ); + expect(result.exitCode, result.stderr).toBe(0); + const actual = JSON.parse(result.stdout); + expect(actual.checksum).toBeGreaterThan(0); + expect(actual.detachedError).toMatchObject({ name: "TypeError" }); + expect(actual.sharedError).toMatchObject({ + name: "TypeError", + code: "ERR_INVALID_ARG_TYPE", + }); + expect(actual.resizableError).toMatchObject({ + name: "TypeError", + code: "ERR_INVALID_ARG_TYPE", + }); + }, 30_000); +}); diff --git a/packages/runtime-core/tests/integration/node-stdlib-public-modules.test.ts b/packages/runtime-core/tests/integration/node-stdlib-public-modules.test.ts index 1c24785b2a..b828fbf010 100644 --- a/packages/runtime-core/tests/integration/node-stdlib-public-modules.test.ts +++ b/packages/runtime-core/tests/integration/node-stdlib-public-modules.test.ts @@ -1,46 +1,56 @@ -import { readFile } from 'node:fs/promises'; -import { resolve } from 'node:path'; -import { afterEach, describe, expect, it } from 'vitest'; +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; import { - createIntegrationKernel, - type IntegrationKernelResult, -} from '@rivet-dev/agentos-vm-test-harness'; + createIntegrationKernel, + type IntegrationKernelResult, +} from "@rivet-dev/agentos-vm-test-harness"; -const repoRoot = resolve(import.meta.dirname, '../../../..'); -const ledgerPath = resolve(repoRoot, 'crates/node-stdlib/suite/ledger.json'); +const repoRoot = resolve(import.meta.dirname, "../../../.."); +const ledgerPath = resolve(repoRoot, "crates/node-stdlib/suite/ledger.json"); -describe('pinned Node public builtin ledger', () => { - let context: IntegrationKernelResult | undefined; +describe("pinned Node public builtin ledger", () => { + let context: IntegrationKernelResult | undefined; - afterEach(async () => { - await context?.dispose(); - }); + afterEach(async () => { + await context?.dispose(); + }); - it('matches the exact expected pass set for the selected stdlib flavor', async () => { - const ledger = JSON.parse(await readFile(ledgerPath, 'utf8')); - const flavor = process.env.AGENTOS_JS_STDLIB === 'real' ? 'real' : 'legacy'; - const expected = ledger.cases - .filter((entry: { legacy: string; real: string }) => entry[flavor] === 'pass') - .map((entry: { id: string }) => entry.id); - const expectedAccepted = ledger.cases - .filter((entry: { legacy: string; real: string }) => entry[flavor] === 'fail-accepted') - .map((entry: { id: string }) => entry.id); - context = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] }); - const script = ` + it("matches the exact expected pass set for the selected stdlib flavor", async () => { + const ledger = JSON.parse(await readFile(ledgerPath, "utf8")); + const flavor = process.env.AGENTOS_JS_STDLIB === "real" ? "real" : "legacy"; + const expected = ledger.cases + .filter( + (entry: { legacy: string; real: string }) => entry[flavor] === "pass", + ) + .map((entry: { id: string }) => entry.id); + const expectedAccepted = ledger.cases + .filter( + (entry: { legacy: string; real: string }) => + entry[flavor] === "fail-accepted", + ) + .map((entry: { id: string }) => entry.id); + context = await createIntegrationKernel({ runtimes: ["wasmvm", "node"] }); + const script = ` const ids = ${JSON.stringify(ledger.cases.map((entry: { id: string }) => entry.id))}; +const schemeOnly = new Set(['sea', 'test', 'test/reporters']); const passed = []; const failed = []; for (const id of ids) { - try { require(id); passed.push(id); } + try { require(schemeOnly.has(id) ? 'node:' + id : id); passed.push(id); } catch (error) { failed.push({ id, code: error?.code, message: error?.message }); } } process.stdout.write(JSON.stringify({ passed, failed })); `; - await context.vfs.writeFile('/tmp/node-stdlib-public-ledger.cjs', script); - const result = await context.kernel.exec('node /tmp/node-stdlib-public-ledger.cjs'); - expect(result.exitCode, result.stderr).toBe(0); - const actual = JSON.parse(result.stdout); - expect(actual.passed).toEqual(expected); - expect(actual.failed.map((entry: { id: string }) => entry.id)).toEqual(expectedAccepted); - }, 30_000); + await context.vfs.writeFile("/tmp/node-stdlib-public-ledger.cjs", script); + const result = await context.kernel.exec( + "node /tmp/node-stdlib-public-ledger.cjs", + ); + expect(result.exitCode, result.stderr).toBe(0); + const actual = JSON.parse(result.stdout); + expect(actual.passed, JSON.stringify(actual.failed)).toEqual(expected); + expect(actual.failed.map((entry: { id: string }) => entry.id)).toEqual( + expectedAccepted, + ); + }, 30_000); }); diff --git a/test-harness/node-suite/ledger.json b/test-harness/node-suite/ledger.json index 5ea242784d..3c181ab9dd 100644 --- a/test-harness/node-suite/ledger.json +++ b/test-harness/node-suite/ledger.json @@ -5941,14 +5941,14 @@ { "id": "parallel/test-fs-append-file.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, { "id": "parallel/test-fs-assert-encoding-error.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -5988,7 +5988,7 @@ { "id": "parallel/test-fs-chown-type-check.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -6032,7 +6032,7 @@ { "id": "parallel/test-fs-empty-readStream.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -6110,7 +6110,7 @@ { "id": "parallel/test-fs-internal-assertencoding.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -6186,7 +6186,7 @@ { "id": "parallel/test-fs-mkdtemp-prefix-check.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -6206,14 +6206,14 @@ { "id": "parallel/test-fs-non-number-arguments-throw.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, { "id": "parallel/test-fs-null-bytes.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -6226,28 +6226,28 @@ { "id": "parallel/test-fs-open-mode-mask.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, { "id": "parallel/test-fs-open-no-close.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, { "id": "parallel/test-fs-open-numeric-flags.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, { "id": "parallel/test-fs-open.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -6275,7 +6275,7 @@ { "id": "parallel/test-fs-promises-exists.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -6395,7 +6395,7 @@ { "id": "parallel/test-fs-promises-readfile-empty.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -6415,7 +6415,7 @@ { "id": "parallel/test-fs-promises-statfs-validate-path.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -6450,7 +6450,7 @@ { "id": "parallel/test-fs-promises-writefile-with-fd.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -6471,7 +6471,7 @@ { "id": "parallel/test-fs-promisified.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -6485,7 +6485,7 @@ { "id": "parallel/test-fs-read-file-assert-encoding.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -6513,21 +6513,21 @@ { "id": "parallel/test-fs-read-optional-params.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, { "id": "parallel/test-fs-read-promises-optional-params.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, { "id": "parallel/test-fs-read-stream-autoClose.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -6555,7 +6555,7 @@ { "id": "parallel/test-fs-read-stream-err.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -6569,14 +6569,14 @@ { "id": "parallel/test-fs-read-stream-fd.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, { "id": "parallel/test-fs-read-stream-file-handle.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -6590,7 +6590,7 @@ { "id": "parallel/test-fs-read-stream-patch-open.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -6604,14 +6604,14 @@ { "id": "parallel/test-fs-read-stream-resume.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, { "id": "parallel/test-fs-read-stream-throw-type-error.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -6624,7 +6624,7 @@ { "id": "parallel/test-fs-read-type.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -6636,7 +6636,7 @@ { "id": "parallel/test-fs-read.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -6660,14 +6660,14 @@ { "id": "parallel/test-fs-readdir-stack-overflow.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, { "id": "parallel/test-fs-readdir-types-symlinks.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -6718,7 +6718,7 @@ { "id": "parallel/test-fs-readfile-flags.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -6737,7 +6737,7 @@ { "id": "parallel/test-fs-readfile-unlink.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -6767,7 +6767,7 @@ { "id": "parallel/test-fs-readlink-type-check.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -6786,7 +6786,7 @@ { "id": "parallel/test-fs-readv-promisify.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -6800,14 +6800,14 @@ { "id": "parallel/test-fs-readv.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, { "id": "parallel/test-fs-ready-event-stream.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -6846,7 +6846,7 @@ { "id": "parallel/test-fs-rename-type-check.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -6859,7 +6859,7 @@ { "id": "parallel/test-fs-rmdir-recursive-sync-warns-not-found.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -6873,7 +6873,7 @@ { "id": "parallel/test-fs-rmdir-recursive-throws-not-found.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -6887,7 +6887,7 @@ { "id": "parallel/test-fs-rmdir-recursive-warns-not-found.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -6907,28 +6907,28 @@ { "id": "parallel/test-fs-rmdir-type-check.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, { "id": "parallel/test-fs-rmSync-special-char.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, { "id": "parallel/test-fs-sir-writes-alot.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, { "id": "parallel/test-fs-stat-bigint.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -6942,7 +6942,7 @@ { "id": "parallel/test-fs-statfs.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -6984,14 +6984,14 @@ { "id": "parallel/test-fs-stream-double-close.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, { "id": "parallel/test-fs-stream-fs-options.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -7057,7 +7057,7 @@ { "id": "parallel/test-fs-timestamp-parsing-error.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -7085,7 +7085,7 @@ { "id": "parallel/test-fs-unlink-type-check.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -7490,21 +7490,21 @@ { "id": "parallel/test-fs-write-stream-err.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, { "id": "parallel/test-fs-write-stream-file-handle-2.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, { "id": "parallel/test-fs-write-stream-file-handle.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -7518,7 +7518,7 @@ { "id": "parallel/test-fs-write-stream-fs.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -7531,28 +7531,28 @@ { "id": "parallel/test-fs-write-stream-throw-type-error.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, { "id": "parallel/test-fs-write-stream.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, { "id": "parallel/test-fs-write-sync-optional-params.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, { "id": "parallel/test-fs-write-sync.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, @@ -7572,7 +7572,7 @@ { "id": "parallel/test-fs-writestream-open-write.js", "legacy": "fail-accepted", - "real": "fail-accepted", + "real": "pass", "reason": "node-test-failure", "issue": "node-stdlib-m1-fs" }, diff --git a/test-harness/node-suite/run.ts b/test-harness/node-suite/run.ts index 4bde96194b..b044233831 100644 --- a/test-harness/node-suite/run.ts +++ b/test-harness/node-suite/run.ts @@ -198,6 +198,7 @@ const { values } = parseArgs({ 'update-ledger': { type: 'boolean', default: false }, resume: { type: 'boolean', default: false }, retry: { type: 'string' }, + match: { type: 'string' }, write: { type: 'string' }, }, }); @@ -205,7 +206,9 @@ const flavor = (values.flavor ?? process.env.AGENTOS_JS_STDLIB ?? 'legacy') as F const slice = values.slice as Slice; if (!['legacy', 'real'].includes(flavor)) throw new Error(`invalid flavor: ${flavor}`); if (!['sanity', 'smoke', 'full'].includes(slice)) throw new Error(`invalid slice: ${slice}`); -const ids = await discover(slice); +const discoveredIds = await discover(slice); +const match = values.match ? new RegExp(values.match) : undefined; +const ids = match ? discoveredIds.filter((id) => match.test(id)) : discoveredIds; const configuredLedger = values.ledger ? JSON.parse(await readFile(resolve(values.ledger), 'utf8')) as { cases: LedgerEntry[] } : undefined;