From cdd38cb1c088b3249f7dd06759e5d80dbb5ae54a Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Thu, 9 Jul 2026 11:57:53 -0700 Subject: [PATCH] poc(runtime): real node lib path/buffer/fs (sync+async) via internalBinding shims + wasm simdutf Stage 1: node v26 lib/path.js + lib/buffer.js load unmodified in the embedded V8 runtime via node's real realm.js loader and JS binding shims. Stage 2: lib/fs.js sync ops pass against an in-memory backend AND the real kernel/ChunkedVFS in a full sidecar VM via the _fs* sync bridge globals. Stage 2.5: buffer validation/length codecs backed by upstream simdutf compiled to wasm32-wasip1 with the patched sysroot, running in V8's own wasm engine. Stage 3: async fs end to end - FSReqCallback/kUsePromises adapter, real lib/timers.js wired to a POC scheduler (getTimerCallbacks/setupTimers), callback+promise APIs, node-exact completion ordering (sync,tick,micro,fs), and fs.createReadStream/createWriteStream/pipe over 1MB through the real internal/streams stack; bridge backend completes via _fs*Async events. --- Cargo.lock | 1 + .../tests/node_stdlib_poc_vfs.rs | 237 +++ crates/v8-runtime/Cargo.toml | 3 + crates/v8-runtime/tests/node_stdlib_poc.rs | 201 ++ .../tests/node_stdlib_poc/bootstrap.js | 1778 +++++++++++++++++ .../tests/node_stdlib_poc/checks.js | 402 ++++ .../tests/node_stdlib_poc/constants.json | 1 + .../tests/node_stdlib_poc/preflight.mjs | 50 + .../tests/node_stdlib_poc/simdutf/.gitignore | 1 + .../simdutf/build-simdutf-poc.sh | 69 + .../tests/node_stdlib_poc/simdutf/wrapper.cpp | 36 + docs-internal/node-stdlib-replacement-spec.md | 433 ++-- 12 files changed, 3034 insertions(+), 178 deletions(-) create mode 100644 crates/native-sidecar/tests/node_stdlib_poc_vfs.rs create mode 100644 crates/v8-runtime/tests/node_stdlib_poc.rs create mode 100644 crates/v8-runtime/tests/node_stdlib_poc/bootstrap.js create mode 100644 crates/v8-runtime/tests/node_stdlib_poc/checks.js create mode 100644 crates/v8-runtime/tests/node_stdlib_poc/constants.json create mode 100644 crates/v8-runtime/tests/node_stdlib_poc/preflight.mjs create mode 100644 crates/v8-runtime/tests/node_stdlib_poc/simdutf/.gitignore create mode 100755 crates/v8-runtime/tests/node_stdlib_poc/simdutf/build-simdutf-poc.sh create mode 100644 crates/v8-runtime/tests/node_stdlib_poc/simdutf/wrapper.cpp diff --git a/Cargo.lock b/Cargo.lock index 0d848d8848..38e82f747f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -323,6 +323,7 @@ dependencies = [ "crossbeam-channel", "libc", "serde", + "serde_json", "sha2 0.10.9", "signal-hook", "v8", diff --git a/crates/native-sidecar/tests/node_stdlib_poc_vfs.rs b/crates/native-sidecar/tests/node_stdlib_poc_vfs.rs new file mode 100644 index 0000000000..5201b10f54 --- /dev/null +++ b/crates/native-sidecar/tests/node_stdlib_poc_vfs.rs @@ -0,0 +1,237 @@ +//! POC: node's REAL lib/fs.js sync ops running inside a full sidecar VM, +//! with `internalBinding('fs')` mapped onto the `_fs*` sync bridge globals — +//! i.e. against the actual kernel/ChunkedVFS, not an in-memory stand-in. +//! +//! Shares the bootstrap/checks assets with +//! crates/v8-runtime/tests/node_stdlib_poc (which runs the same checks against +//! the in-memory backend in a bare embedded-runtime session). + +mod support; + +use agentos_native_sidecar::wire::{ + CreateVmRequest, EventPayload, GuestRuntimeKind, RequestPayload, ResponsePayload, + RootFilesystemDescriptor, RootFilesystemMode, StreamChannel, +}; +use std::collections::{BTreeMap, HashMap}; +use std::path::Path; +use std::time::{Duration, Instant}; +use support::{ + authenticate_wire, dispose_vm_and_close_session_wire, execute_wire, new_sidecar, + open_session_wire, temp_dir, wire_permissions_allow_all, wire_request, wire_session, wire_vm, + write_fixture, +}; + +const BOOTSTRAP_JS: &str = include_str!("../../v8-runtime/tests/node_stdlib_poc/bootstrap.js"); +const CHECKS_JS: &str = include_str!("../../v8-runtime/tests/node_stdlib_poc/checks.js"); +const CONSTANTS_JSON: &str = include_str!("../../v8-runtime/tests/node_stdlib_poc/constants.json"); + +fn node_lib_sources() -> Option { + let dir = std::env::var("NODE_SRC_DIR") + .map(std::path::PathBuf::from) + .unwrap_or_else(|_| std::path::PathBuf::from("/home/nathan/misc/node")); + let lib = dir.join("lib"); + if !lib.is_dir() { + return None; + } + let mut sources = BTreeMap::new(); + collect(&lib, &lib, &mut sources); + Some(serde_json::to_string(&sources).expect("serialize node lib sources")) +} + +fn collect(dir: &Path, base: &Path, out: &mut BTreeMap) { + for entry in std::fs::read_dir(dir).expect("read node lib dir") { + let path = entry.expect("read node lib entry").path(); + if path.is_dir() { + collect(&path, base, out); + } else if path.extension().is_some_and(|ext| ext == "js") { + let id = path + .strip_prefix(base) + .expect("lib path under base") + .with_extension("") + .to_string_lossy() + .into_owned(); + out.insert( + id, + std::fs::read_to_string(&path).expect("read node lib source"), + ); + } + } +} + +/// Stage 2.5 (optional): simdutf wasm compiled with the agentOS patched +/// sysroot; built by crates/v8-runtime/tests/node_stdlib_poc/simdutf/ +/// build-simdutf-poc.sh. Absent → bootstrap keeps its JS codecs. +fn simdutf_wasm_base64() -> String { + let path = std::env::var("AGENTOS_SIMDUTF_POC_WASM") + .map(std::path::PathBuf::from) + .unwrap_or_else(|_| { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../v8-runtime/tests/node_stdlib_poc/simdutf/build/simdutf-poc.wasm") + }); + match std::fs::read(&path) { + Ok(bytes) => base64_encode(&bytes), + Err(_) => { + eprintln!( + "note: simdutf POC wasm not found at {} — running with JS codec fallback", + path.display() + ); + String::new() + } + } +} + +fn base64_encode(bytes: &[u8]) -> String { + const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4); + for chunk in bytes.chunks(3) { + let b = [ + chunk[0], + *chunk.get(1).unwrap_or(&0), + *chunk.get(2).unwrap_or(&0), + ]; + let n = (u32::from(b[0]) << 16) | (u32::from(b[1]) << 8) | u32::from(b[2]); + out.push(ALPHABET[(n >> 18) as usize & 63] as char); + out.push(ALPHABET[(n >> 12) as usize & 63] as char); + out.push(if chunk.len() > 1 { + ALPHABET[(n >> 6) as usize & 63] as char + } else { + '=' + }); + out.push(if chunk.len() > 2 { + ALPHABET[n as usize & 63] as char + } else { + '=' + }); + } + out +} + +fn collect_process_output( + sidecar: &mut agentos_native_sidecar::NativeSidecar, + connection_id: &str, + session_id: &str, + vm_id: &str, + process_id: &str, +) -> (String, String, i32) { + let ownership = wire_session(connection_id, session_id); + let deadline = Instant::now() + Duration::from_secs(60); + let mut stdout = String::new(); + let mut stderr = String::new(); + let mut exit = None; + + loop { + let event = sidecar + .poll_event_wire_blocking(&ownership, Duration::from_millis(100)) + .expect("poll wire event"); + if let Some(event) = event { + assert_eq!(event.ownership, wire_vm(connection_id, session_id, vm_id)); + match event.payload { + EventPayload::ProcessOutputEvent(output) if output.process_id == process_id => { + let chunk = String::from_utf8_lossy(&output.chunk); + match output.channel { + StreamChannel::Stdout => stdout.push_str(&chunk), + StreamChannel::Stderr => stderr.push_str(&chunk), + } + } + EventPayload::ProcessExitedEvent(exited) if exited.process_id == process_id => { + exit = Some((exited.exit_code, Instant::now())); + } + _ => {} + } + } + + if let Some((exit_code, seen_at)) = exit { + // Drain trailing output events before returning. + if Instant::now().duration_since(seen_at) >= Duration::from_millis(200) { + return (stdout, stderr, exit_code); + } + } + + assert!( + Instant::now() < deadline, + "timed out waiting for POC guest process\nstdout:\n{stdout}\nstderr:\n{stderr}" + ); + } +} + +#[test] +fn real_node_fs_sync_ops_hit_the_kernel_vfs() { + let Some(sources_json) = node_lib_sources() else { + eprintln!("skipping: node checkout not found (set NODE_SRC_DIR or clone to /home/nathan/misc/node)"); + return; + }; + + let cwd = temp_dir("node-stdlib-poc-vfs"); + let entrypoint = cwd.join("poc-entrypoint.js"); + let simdutf_b64 = simdutf_wasm_base64(); + write_fixture( + &entrypoint, + &format!( + "globalThis.__nodeSources = {sources_json};\n\ + globalThis.__nodeConstants = {CONSTANTS_JSON};\n\ + globalThis.__pocUseBridgeFs = true;\n\ + globalThis.__pocSimdutfWasmBase64 = \"{simdutf_b64}\";\n\ + {BOOTSTRAP_JS}\n\ + {CHECKS_JS}\n" + ), + ); + + let mut sidecar = new_sidecar("node-stdlib-poc-vfs"); + let connection_id = authenticate_wire(&mut sidecar, "conn-node-stdlib-poc-vfs"); + let session_id = open_session_wire(&mut sidecar, 2, &connection_id); + + let mut metadata: HashMap = HashMap::new(); + metadata.insert(String::from("cwd"), cwd.to_string_lossy().into_owned()); + + let result = sidecar + .dispatch_wire_blocking(wire_request( + 3, + wire_session(&connection_id, &session_id), + RequestPayload::CreateVmRequest(CreateVmRequest::legacy_test_config( + GuestRuntimeKind::JavaScript, + metadata, + RootFilesystemDescriptor { + mode: RootFilesystemMode::Ephemeral, + disable_default_base_layer: false, + lowers: Vec::new(), + bootstrap_entries: Vec::new(), + }, + Some(wire_permissions_allow_all()), + )), + )) + .expect("create sidecar VM through wire"); + let vm_id = match result.response.payload { + ResponsePayload::VmCreatedResponse(response) => response.vm_id, + other => panic!("unexpected wire vm create response: {other:?}"), + }; + + execute_wire( + &mut sidecar, + 4, + &connection_id, + &session_id, + &vm_id, + "proc-node-stdlib-poc-vfs", + GuestRuntimeKind::JavaScript, + &entrypoint, + Vec::new(), + ); + + let (stdout, stderr, exit_code) = collect_process_output( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + "proc-node-stdlib-poc-vfs", + ); + dispose_vm_and_close_session_wire(&mut sidecar, &connection_id, &session_id, &vm_id); + + assert_eq!( + exit_code, 0, + "node stdlib POC failed against the kernel VFS\nstdout:\n{stdout}\nstderr:\n{stderr}" + ); + assert!( + stdout.contains("all sync+async checks passed"), + "expected POC success marker in stdout\nstdout:\n{stdout}\nstderr:\n{stderr}" + ); +} diff --git a/crates/v8-runtime/Cargo.toml b/crates/v8-runtime/Cargo.toml index e9453c60cc..d26fc784f2 100644 --- a/crates/v8-runtime/Cargo.toml +++ b/crates/v8-runtime/Cargo.toml @@ -18,3 +18,6 @@ sha2 = "0.10" [build-dependencies] agentos-build-support = { workspace = true } + +[dev-dependencies] +serde_json = "1.0" diff --git a/crates/v8-runtime/tests/node_stdlib_poc.rs b/crates/v8-runtime/tests/node_stdlib_poc.rs new file mode 100644 index 0000000000..dc69dbb2da --- /dev/null +++ b/crates/v8-runtime/tests/node_stdlib_poc.rs @@ -0,0 +1,201 @@ +//! POC: run Node.js's REAL lib/path.js and lib/buffer.js inside the agentOS +//! embedded V8 runtime, with `internalBinding()` provided by JS shims +//! (tests/node_stdlib_poc/bootstrap.js) instead of node's C++ core. +//! +//! Node lib sources are read from NODE_SRC_DIR (default /home/nathan/misc/node); +//! the test is skipped when the checkout is absent. See +//! tests/node_stdlib_poc/preflight.mjs for a fast host-node iteration loop over +//! the same bootstrap + checks. + +use agentos_v8_runtime::embedded_runtime::EmbeddedV8Runtime; +use agentos_v8_runtime::runtime_protocol::{RuntimeCommand, RuntimeEvent, SessionMessage}; +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; +use std::sync::mpsc; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +const BOOTSTRAP_JS: &str = include_str!("node_stdlib_poc/bootstrap.js"); +const CHECKS_JS: &str = include_str!("node_stdlib_poc/checks.js"); +const CONSTANTS_JSON: &str = include_str!("node_stdlib_poc/constants.json"); + +/// Stage 2.5: simdutf compiled to wasm32-wasip1 with the agentOS patched +/// sysroot (build with tests/node_stdlib_poc/simdutf/build-simdutf-poc.sh). +/// Optional: when absent the bootstrap keeps its JS codec implementations. +fn simdutf_wasm_base64() -> String { + let path = std::env::var("AGENTOS_SIMDUTF_POC_WASM") + .map(PathBuf::from) + .unwrap_or_else(|_| { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/node_stdlib_poc/simdutf/build/simdutf-poc.wasm") + }); + match std::fs::read(&path) { + Ok(bytes) => base64_encode(&bytes), + Err(_) => { + eprintln!( + "note: simdutf POC wasm not found at {} — running with JS codec fallback \ + (build it with tests/node_stdlib_poc/simdutf/build-simdutf-poc.sh)", + path.display() + ); + String::new() + } + } +} + +fn base64_encode(bytes: &[u8]) -> String { + const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4); + for chunk in bytes.chunks(3) { + let b = [ + chunk[0], + *chunk.get(1).unwrap_or(&0), + *chunk.get(2).unwrap_or(&0), + ]; + let n = (u32::from(b[0]) << 16) | (u32::from(b[1]) << 8) | u32::from(b[2]); + out.push(ALPHABET[(n >> 18) as usize & 63] as char); + out.push(ALPHABET[(n >> 12) as usize & 63] as char); + out.push(if chunk.len() > 1 { + ALPHABET[(n >> 6) as usize & 63] as char + } else { + '=' + }); + out.push(if chunk.len() > 2 { + ALPHABET[n as usize & 63] as char + } else { + '=' + }); + } + out +} + +fn node_src_dir() -> Option { + let dir = std::env::var("NODE_SRC_DIR") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from("/home/nathan/misc/node")); + dir.join("lib").is_dir().then_some(dir) +} + +fn collect_lib_sources(lib_dir: &Path, base: &Path, out: &mut BTreeMap) { + for entry in std::fs::read_dir(lib_dir).expect("read node lib dir") { + let path = entry.expect("read node lib entry").path(); + if path.is_dir() { + collect_lib_sources(&path, base, out); + } else if path.extension().is_some_and(|ext| ext == "js") { + let id = path + .strip_prefix(base) + .expect("lib path under base") + .with_extension("") + .to_string_lossy() + .into_owned(); + let source = std::fs::read_to_string(&path).expect("read node lib source"); + out.insert(id, source); + } + } +} + +fn wait_for_execution_result( + receiver: &mpsc::Receiver, + session_id: &str, +) -> RuntimeEvent { + // Generous: the session compiles ~5MB of injected node lib sources. + let deadline = Instant::now() + Duration::from_secs(120); + loop { + let remaining = deadline + .checked_duration_since(Instant::now()) + .expect("timed out waiting for execution result"); + let event = receiver + .recv_timeout(remaining) + .expect("runtime event should arrive before timeout"); + if matches!( + &event, + RuntimeEvent::ExecutionResult { session_id: event_session_id, .. } + if event_session_id == session_id + ) { + return event; + } + } +} + +#[test] +fn real_node_path_and_buffer_run_in_isolate() { + let Some(node_src) = node_src_dir() else { + eprintln!("skipping: node checkout not found (set NODE_SRC_DIR or clone to /home/nathan/misc/node)"); + return; + }; + + let lib_dir = node_src.join("lib"); + let mut sources = BTreeMap::new(); + collect_lib_sources(&lib_dir, &lib_dir, &mut sources); + assert!( + sources.contains_key("path") && sources.contains_key("buffer"), + "node lib sources should include path and buffer" + ); + + let sources_json = serde_json::to_string(&sources).expect("serialize node lib sources"); + let simdutf_b64 = simdutf_wasm_base64(); + // Executed as an ES module (mode 1) so the trailing top-level await makes + // the ExecutionResult wait for — and report failures from — the async fs + // checks (callbacks, promises, streams). An event loop that dropped the + // in-flight work would surface here as a pending/failed evaluation. + let user_code = format!( + "globalThis.__nodeSources = {sources_json};\n\ + globalThis.__nodeConstants = {CONSTANTS_JSON};\n\ + globalThis.__pocSimdutfWasmBase64 = \"{simdutf_b64}\";\n\ + {BOOTSTRAP_JS}\n\ + {CHECKS_JS}\n\ + await globalThis.__pocAsync;\n" + ); + + let runtime = Arc::new(EmbeddedV8Runtime::new(Some(1)).expect("create embedded runtime")); + let session_id = "node-stdlib-poc"; + let receiver = runtime + .register_session(session_id) + .expect("register session"); + runtime + .dispatch(RuntimeCommand::CreateSession { + session_id: session_id.to_owned(), + heap_limit_mb: None, + cpu_time_limit_ms: None, + wall_clock_limit_ms: None, + warm_hint: None, + }) + .expect("create session"); + + runtime + .dispatch(RuntimeCommand::SendToSession { + session_id: session_id.to_owned(), + message: SessionMessage::Execute { + mode: 1, + file_path: String::new(), + bridge_code: "(function() {})();".to_owned(), + post_restore_script: String::new(), + userland_code: String::new(), + high_resolution_time: false, + user_code, + wasm_module_bytes: None, + }, + }) + .expect("dispatch execute"); + + match wait_for_execution_result(&receiver, session_id) { + RuntimeEvent::ExecutionResult { + exit_code, error, .. + } => { + if let Some(error) = &error { + panic!( + "node stdlib POC failed in isolate: {} {}\n{}", + error.error_type, error.message, error.stack + ); + } + assert_eq!(exit_code, 0, "expected successful execution"); + } + other => panic!("expected execution result, got {other:?}"), + } + + runtime + .dispatch(RuntimeCommand::DestroySession { + session_id: session_id.to_owned(), + }) + .expect("destroy session"); + runtime.unregister_session(session_id); +} diff --git a/crates/v8-runtime/tests/node_stdlib_poc/bootstrap.js b/crates/v8-runtime/tests/node_stdlib_poc/bootstrap.js new file mode 100644 index 0000000000..819449d7b5 --- /dev/null +++ b/crates/v8-runtime/tests/node_stdlib_poc/bootstrap.js @@ -0,0 +1,1778 @@ +// POC mini-realm: load Node.js's REAL lib/*.js builtins inside an agentOS V8 +// session, with internalBinding() provided by JS shims instead of node's C++. +// +// Inputs (set by the Rust test before this script runs): +// globalThis.__nodeSources - { "": "" } from node's lib/ +// globalThis.__nodeConstants - the `constants` binding value (harvested from +// a real linux node via internal/test/binding) +// +// Node compiles every regular builtin with the wrapper parameters +// (exports, require, module, process, internalBinding, primordials) +// and per-context scripts with +// (exports, primordials, privateSymbols, perIsolateSymbols) +// (src/builtin_info.h). We replicate exactly that with `new Function`. +(function () { + 'use strict'; + + const sources = globalThis.__nodeSources; + const constantsBinding = globalThis.__nodeConstants; + if (!sources || !constantsBinding) { + throw new Error('POC bootstrap: __nodeSources/__nodeConstants not injected'); + } + + function compileBuiltin(id, params) { + const source = sources[id]; + if (source === undefined) { + throw new Error(`POC: no source for builtin '${id}'`); + } + // eslint-disable-next-line no-new-func + return new Function(...params, `${source}\n//# sourceURL=node:${id}`); + } + + // Private/per-isolate symbols: auto-create a Symbol per name accessed. + function makeLazySymbols(tag) { + const cache = { __proto__: null }; + return new Proxy(Object.create(null), { + get(_t, prop) { + if (typeof prop !== 'string') return undefined; + cache[prop] ??= Symbol(`${tag}:${prop}`); + return cache[prop]; + }, + }); + } + const privateSymbols = makeLazySymbols('private'); + const perIsolateSymbols = makeLazySymbols('perIsolate'); + + // ---- primordials (node's real per-context script, unmodified) ---- + const primordials = { __proto__: null }; + compileBuiltin('internal/per_context/primordials', [ + 'exports', 'primordials', 'privateSymbols', 'perIsolateSymbols', + ])({}, primordials, privateSymbols, perIsolateSymbols); + + // ---- minimal process ---- + const process = { + platform: 'linux', + arch: 'x64', + env: { __proto__: null }, + argv: ['node'], + execArgv: [], + pid: 1, + ppid: 0, + version: 'v26.0.0', + versions: { node: '26.0.0', v8: '13.0', modules: '140', uv: '1.52.1' }, + emitWarning() {}, + // Promise-based so we don't depend on embedder-provided queueMicrotask. + nextTick(cb, ...args) { Promise.resolve().then(() => cb(...args)); }, + cwd() { return '/'; }, + umask() { return 0o22; }, + memoryUsage() { return { rss: 0, heapTotal: 0, heapUsed: 0, external: 0, arrayBuffers: 0 }; }, + }; + + // ---- binding shims ---- + + // Wrap a shim so that destructuring any not-yet-implemented property yields + // a named function that throws on CALL: loads succeed, uses fail loudly. + function withAutoStubs(bindingName, target) { + return new Proxy(target, { + get(t, prop, receiver) { + if (prop in t || typeof prop !== 'string') { + return Reflect.get(t, prop, receiver); + } + return function pocStub() { + throw new Error(`POC: internalBinding('${bindingName}').${prop} is not implemented`); + }; + }, + }); + } + + // A binding we have not written at all: fail loudly on ANY property access, + // so load-time gaps name themselves. + function missingBinding(name) { + return new Proxy(Object.create(null), { + get(_t, prop) { + if (typeof prop !== 'string') return undefined; + throw new Error(`POC: internalBinding('${name}') has no shim (accessed .${String(prop)})`); + }, + }); + } + + // -- util -- + function getOwnNonIndexProperties(obj, filter) { + const ONLY_ENUMERABLE = 2; + const names = Object.getOwnPropertyNames(obj); + const out = []; + for (const name of names) { + if (String(Number(name)) === name && Number(name) >= 0) continue; // index + if (filter & ONLY_ENUMERABLE) { + const desc = Object.getOwnPropertyDescriptor(obj, name); + if (!desc || !desc.enumerable) continue; + } + out.push(name); + } + if (!(filter & 16 /* SKIP_SYMBOLS */)) { + for (const sym of Object.getOwnPropertySymbols(obj)) { + if (filter & 2) { + const desc = Object.getOwnPropertyDescriptor(obj, sym); + if (!desc || !desc.enumerable) continue; + } + out.push(sym); + } + } + return out; + } + + const hiddenValues = new WeakMap(); + const utilBinding = withAutoStubs('util', { + constants: { + ALL_PROPERTIES: 0, ONLY_WRITABLE: 1, ONLY_ENUMERABLE: 2, + ONLY_CONFIGURABLE: 4, SKIP_STRINGS: 8, SKIP_SYMBOLS: 16, + }, + privateSymbols, + getOwnNonIndexProperties, + isInsideNodeModules: () => false, + guessHandleType: () => 'PIPE', + sleep() {}, + constructSharedArrayBuffer: (len) => new SharedArrayBuffer(len), + getConstructorName: (obj) => (obj?.constructor?.name ?? ''), + getPromiseDetails: () => [0], + getProxyDetails: () => undefined, + previewEntries: () => [[], false], + getExternalValue: () => 0, + setHiddenValue(obj, sym, val) { + let map = hiddenValues.get(obj); + if (!map) { map = new Map(); hiddenValues.set(obj, map); } + map.set(sym, val); + return true; + }, + getHiddenValue(obj, sym) { return hiddenValues.get(obj)?.get(sym); }, + defineLazyProperties(target, id, keys, enumerable = true) { + for (const key of keys) { + let set, value, isSet = false; + Object.defineProperty(target, key, { + get() { + if (isSet) return value; + value = requireBuiltin(id)[key]; + isSet = true; + return value; + }, + set(v) { value = v; isSet = true; }, + configurable: true, + enumerable, + }); + } + return target; + }, + shouldAbortOnUncaughtToggle: [true], + WeakReference: class WeakReference { + #ref; + constructor(value) { this.#ref = new WeakRef(value); } + get() { return this.#ref.deref(); } + incRef() {} + decRef() {} + }, + }); + + // -- types -- + const brand = (v) => Object.prototype.toString.call(v); + const typesBinding = withAutoStubs('types', { + isAnyArrayBuffer: (v) => brand(v) === '[object ArrayBuffer]' || brand(v) === '[object SharedArrayBuffer]', + isArrayBuffer: (v) => brand(v) === '[object ArrayBuffer]', + isSharedArrayBuffer: (v) => brand(v) === '[object SharedArrayBuffer]', + isDataView: (v) => brand(v) === '[object DataView]', + isDate: (v) => brand(v) === '[object Date]', + isMap: (v) => brand(v) === '[object Map]', + isSet: (v) => brand(v) === '[object Set]', + isWeakMap: (v) => brand(v) === '[object WeakMap]', + isWeakSet: (v) => brand(v) === '[object WeakSet]', + isMapIterator: (v) => brand(v) === '[object Map Iterator]', + isSetIterator: (v) => brand(v) === '[object Set Iterator]', + isRegExp: (v) => brand(v) === '[object RegExp]', + isNativeError: (v) => v instanceof Error, + isPromise: (v) => brand(v) === '[object Promise]', + isProxy: () => false, + isExternal: () => false, + isModuleNamespaceObject: (v) => brand(v) === '[object Module]', + isArgumentsObject: (v) => brand(v) === '[object Arguments]', + isBoxedPrimitive: (v) => + ['[object Number]', '[object String]', '[object Boolean]', '[object BigInt]', '[object Symbol]'] + .includes(brand(v)) && typeof v === 'object', + isNumberObject: (v) => typeof v === 'object' && brand(v) === '[object Number]', + isStringObject: (v) => typeof v === 'object' && brand(v) === '[object String]', + isBooleanObject: (v) => typeof v === 'object' && brand(v) === '[object Boolean]', + isBigIntObject: (v) => typeof v === 'object' && brand(v) === '[object BigInt]', + isSymbolObject: (v) => typeof v === 'object' && brand(v) === '[object Symbol]', + isGeneratorFunction: (v) => typeof v === 'function' && brand(v) === '[object GeneratorFunction]', + isAsyncFunction: (v) => typeof v === 'function' && brand(v) === '[object AsyncFunction]', + isGeneratorObject: (v) => brand(v) === '[object Generator]', + isCryptoKey: () => false, + isKeyObject: () => false, + }); + + // -- string_decoder (constants from src/string_decoder.h; enum order from + // src/node.h `enum encoding`) -- + const encodings = ['ascii', 'utf8', 'base64', 'utf16le', 'latin1', 'hex', 'buffer', 'base64url']; + const stringDecoderBinding = withAutoStubs('string_decoder', { + encodings, + kIncompleteCharactersStart: 0, + kIncompleteCharactersEnd: 4, + kMissingBytes: 4, + kBufferedBytes: 5, + kEncodingField: 6, + kNumFields: 7, + kSize: 7, + }); + + // -- options -- + const cliOptions = { + __proto__: null, + '--stack-trace-limit': 10, + '--no-deprecation': false, + '--throw-deprecation': false, + '--pending-deprecation': false, + '--trace-deprecation': false, + '--preserve-symlinks': false, + '--verify-base-objects': false, + }; + const optionsBinding = withAutoStubs('options', { + getCLIOptionsValues: () => cliOptions, + getCLIOptionsInfo: () => ({ __proto__: null, options: new Map(), aliases: new Map() }), + getOptionsAsFlags: () => [], + getEmbedderOptions: () => ({ + __proto__: null, + shouldNotRegisterESMLoader: true, + noGlobalSearchPaths: true, + noBrowserGlobals: true, + hasEmbedderPreload: false, + }), + getEnvOptionsInputType: () => ({ __proto__: null }), + getNamespaceOptionsInputType: () => ({ __proto__: null }), + }); + + // -- config -- + const configBinding = { + hasIntl: false, + hasSmallICU: false, + hasOpenSSL: false, + hasQuic: false, + hasInspector: false, + hasSQLite: false, + hasNodeOptions: false, + noBrowserGlobals: true, + bits: 64, + }; + + // -- messaging (only DOMException is reached from util paths) -- + class POCDOMException extends Error { + constructor(message, name) { + super(message); + this.name = typeof name === 'object' ? name?.name ?? 'Error' : (name ?? 'Error'); + } + } + const messagingBinding = withAutoStubs('messaging', { + DOMException: POCDOMException, + // Called at load by internal/worker/js_transferable (pulled in by internal/url). + setDeserializerCreateObjectFunction() {}, + }); + + // -- buffer: pure-JS codecs standing in for node_buffer.cc / simdutf -- + const B64_STD = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + const B64_URL = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; + const B64_REV = new Int8Array(256).fill(-1); + for (let i = 0; i < 64; i++) B64_REV[B64_STD.charCodeAt(i)] = i; + B64_REV['-'.charCodeAt(0)] = 62; + B64_REV['_'.charCodeAt(0)] = 63; + + function utf8ByteLength(str) { + let bytes = 0; + for (let i = 0; i < str.length; i++) { + const c = str.charCodeAt(i); + if (c < 0x80) bytes += 1; + else if (c < 0x800) bytes += 2; + else if (c >= 0xd800 && c < 0xdc00 && i + 1 < str.length && + str.charCodeAt(i + 1) >= 0xdc00 && str.charCodeAt(i + 1) < 0xe000) { + bytes += 4; i++; + } else bytes += 3; + } + return bytes; + } + + // Write as many WHOLE characters as fit in `length` bytes; return bytes written. + function utf8WriteInto(buf, string, offset, length) { + let pos = offset; + const end = offset + length; + for (let i = 0; i < string.length; i++) { + let cp = string.charCodeAt(i); + if (cp >= 0xd800 && cp < 0xdc00 && i + 1 < string.length) { + const lo = string.charCodeAt(i + 1); + if (lo >= 0xdc00 && lo < 0xe000) { cp = 0x10000 + ((cp - 0xd800) << 10) + (lo - 0xdc00); i++; } + else cp = 0xfffd; + } else if (cp >= 0xdc00 && cp < 0xe000) { + cp = 0xfffd; + } + if (cp < 0x80) { + if (pos + 1 > end) break; + buf[pos++] = cp; + } else if (cp < 0x800) { + if (pos + 2 > end) break; + buf[pos++] = 0xc0 | (cp >> 6); + buf[pos++] = 0x80 | (cp & 0x3f); + } else if (cp < 0x10000) { + if (pos + 3 > end) break; + buf[pos++] = 0xe0 | (cp >> 12); + buf[pos++] = 0x80 | ((cp >> 6) & 0x3f); + buf[pos++] = 0x80 | (cp & 0x3f); + } else { + if (pos + 4 > end) break; + buf[pos++] = 0xf0 | (cp >> 18); + buf[pos++] = 0x80 | ((cp >> 12) & 0x3f); + buf[pos++] = 0x80 | ((cp >> 6) & 0x3f); + buf[pos++] = 0x80 | (cp & 0x3f); + } + } + return pos - offset; + } + + // Lossy UTF-8 decode (U+FFFD for invalid), matching node's utf8Slice for + // valid input. + function utf8DecodeRange(buf, start, end) { + const parts = []; + let chunk = []; + const flush = () => { if (chunk.length) { parts.push(String.fromCharCode(...chunk)); chunk = []; } }; + let i = start; + while (i < end) { + const b = buf[i]; + let cp, extra; + if (b < 0x80) { cp = b; extra = 0; } + else if (b >= 0xc2 && b < 0xe0) { cp = b & 0x1f; extra = 1; } + else if (b >= 0xe0 && b < 0xf0) { cp = b & 0x0f; extra = 2; } + else if (b >= 0xf0 && b < 0xf5) { cp = b & 0x07; extra = 3; } + else { chunk.push(0xfffd); i++; continue; } + let ok = true; + let value = cp; + for (let k = 1; k <= extra; k++) { + if (i + k >= end) { ok = false; break; } + const cb = buf[i + k]; + if ((cb & 0xc0) !== 0x80) { ok = false; break; } + value = (value << 6) | (cb & 0x3f); + } + if (!ok || + (extra === 1 && value < 0x80) || + (extra === 2 && (value < 0x800 || (value >= 0xd800 && value < 0xe000))) || + (extra === 3 && (value < 0x10000 || value > 0x10ffff))) { + chunk.push(0xfffd); i++; continue; + } + if (value >= 0x10000) { + value -= 0x10000; + chunk.push(0xd800 + (value >> 10), 0xdc00 + (value & 0x3ff)); + } else { + chunk.push(value); + } + i += extra + 1; + if (chunk.length > 4096) flush(); + } + flush(); + return parts.join(''); + } + + function charSlice(buf, start, end, mask) { + const parts = []; + let chunk = []; + for (let i = start; i < end; i++) { + chunk.push(buf[i] & mask); + if (chunk.length > 4096) { parts.push(String.fromCharCode(...chunk)); chunk = []; } + } + if (chunk.length) parts.push(String.fromCharCode(...chunk)); + return parts.join(''); + } + + function charWrite(buf, string, offset, length) { + const n = Math.min(length, string.length, buf.length - offset); + for (let i = 0; i < n; i++) buf[offset + i] = string.charCodeAt(i) & 0xff; + return n; + } + + function base64EncodeRange(buf, start, end, alphabet, pad) { + let out = ''; + let i = start; + for (; i + 2 < end; i += 3) { + const n = (buf[i] << 16) | (buf[i + 1] << 8) | buf[i + 2]; + out += alphabet[(n >> 18) & 63] + alphabet[(n >> 12) & 63] + alphabet[(n >> 6) & 63] + alphabet[n & 63]; + } + const rem = end - i; + if (rem === 1) { + const n = buf[i] << 16; + out += alphabet[(n >> 18) & 63] + alphabet[(n >> 12) & 63] + (pad ? '==' : ''); + } else if (rem === 2) { + const n = (buf[i] << 16) | (buf[i + 1] << 8); + out += alphabet[(n >> 18) & 63] + alphabet[(n >> 12) & 63] + alphabet[(n >> 6) & 63] + (pad ? '=' : ''); + } + return out; + } + + // Forgiving base64 decode (both alphabets), stops at capacity; returns bytes + // written — matches node's legacy-forgiving binding behavior for valid input. + function base64WriteInto(buf, string, offset, length) { + let bits = 0, acc = 0, pos = offset; + const end = offset + length; + for (let i = 0; i < string.length && pos < end; i++) { + const v = B64_REV[string.charCodeAt(i)]; + if (v === -1) { + if (string[i] === '=') break; + continue; // skip whitespace/invalid, like node + } + acc = (acc << 6) | v; + bits += 6; + if (bits >= 8) { + bits -= 8; + buf[pos++] = (acc >> bits) & 0xff; + } + } + return pos - offset; + } + + function hexWriteInto(buf, string, offset, length) { + const n = Math.min(length, string.length >>> 1, buf.length - offset); + let written = 0; + for (; written < n; written++) { + const hi = parseInt(string[written * 2], 16); + const lo = parseInt(string[written * 2 + 1], 16); + if (Number.isNaN(hi) || Number.isNaN(lo)) break; + buf[offset + written] = (hi << 4) | lo; + } + return written; + } + + function hexSliceRange(buf, start, end) { + let out = ''; + for (let i = start; i < end; i++) out += buf[i].toString(16).padStart(2, '0'); + return out; + } + + function ucs2SliceRange(buf, start, end) { + const parts = []; + let chunk = []; + for (let i = start; i + 1 < end; i += 2) { + chunk.push(buf[i] | (buf[i + 1] << 8)); + if (chunk.length > 4096) { parts.push(String.fromCharCode(...chunk)); chunk = []; } + } + if (chunk.length) parts.push(String.fromCharCode(...chunk)); + return parts.join(''); + } + + function ucs2WriteInto(buf, string, offset, length) { + const maxChars = Math.min(string.length, Math.floor(length / 2), Math.floor((buf.length - offset) / 2)); + for (let i = 0; i < maxChars; i++) { + const c = string.charCodeAt(i); + buf[offset + i * 2] = c & 0xff; + buf[offset + i * 2 + 1] = c >> 8; + } + return maxChars * 2; + } + + function normRange(buf, start, end) { + const len = buf.length; + start = start === undefined ? 0 : Math.max(0, Math.min(start, len)); + end = end === undefined ? len : Math.max(start, Math.min(end, len)); + return [start, end]; + } + + function encodeForSearch(val, encodingNum) { + // encodingNum indexes `encodings` above. + const enc = encodings[encodingNum] ?? 'utf8'; + let tmp; + switch (enc) { + case 'utf8': { + tmp = new Uint8Array(utf8ByteLength(val)); + utf8WriteInto(tmp, val, 0, tmp.length); + return tmp; + } + case 'utf16le': { + tmp = new Uint8Array(val.length * 2); + ucs2WriteInto(tmp, val, 0, tmp.length); + return tmp; + } + case 'latin1': + case 'ascii': { + tmp = new Uint8Array(val.length); + charWrite(tmp, val, 0, tmp.length); + return tmp; + } + case 'hex': { + tmp = new Uint8Array(val.length >>> 1); + return tmp.subarray(0, hexWriteInto(tmp, val, 0, tmp.length)); + } + case 'base64': + case 'base64url': { + tmp = new Uint8Array(Math.ceil(val.length * 3 / 4)); + return tmp.subarray(0, base64WriteInto(tmp, val, 0, tmp.length)); + } + default: + throw new Error(`POC: search encoding ${enc} unsupported`); + } + } + + function searchBytes(haystack, needle, byteOffset, forward) { + if (needle.length === 0) return byteOffset > haystack.length ? haystack.length : byteOffset; + if (forward) { + outer: + for (let i = Math.max(0, byteOffset); i + needle.length <= haystack.length; i++) { + for (let j = 0; j < needle.length; j++) { + if (haystack[i + j] !== needle[j]) continue outer; + } + return i; + } + } else { + outer2: + for (let i = Math.min(byteOffset, haystack.length - needle.length); i >= 0; i--) { + for (let j = 0; j < needle.length; j++) { + if (haystack[i + j] !== needle[j]) continue outer2; + } + return i; + } + } + return -1; + } + + const bufferBinding = withAutoStubs('buffer', { + kMaxLength: 4294967296, + kStringMaxLength: 536870888, + byteLengthUtf8: (string) => utf8ByteLength(string), + copy(source, target, targetStart, sourceStart, nb) { + target.set(source.subarray(sourceStart, sourceStart + nb), targetStart); + return nb; + }, + compare(a, b) { + const len = Math.min(a.length, b.length); + for (let i = 0; i < len; i++) { + if (a[i] !== b[i]) return a[i] < b[i] ? -1 : 1; + } + return a.length === b.length ? 0 : (a.length < b.length ? -1 : 1); + }, + compareOffset(source, target, targetStart, sourceStart, targetEnd, sourceEnd) { + const a = source.subarray(sourceStart, sourceEnd); + const b = target.subarray(targetStart, targetEnd); + return bufferBinding.compare(a, b); + }, + fill(buf, value, offset, end, encoding) { + let bytes; + if (typeof value === 'string') { + bytes = encodeForSearch(value, typeof encoding === 'number' ? encoding : 1); + if (value.length > 0 && bytes.length === 0) return -1; + } else { + bytes = value; // Uint8Array + } + if (bytes.length === 0) return 0; + for (let i = offset; i < end; i++) buf[i] = bytes[(i - offset) % bytes.length]; + return 0; + }, + isAscii(input) { + for (let i = 0; i < input.length; i++) if (input[i] > 0x7f) return false; + return true; + }, + isUtf8(input) { + let i = 0; + while (i < input.length) { + const b = input[i]; + let extra; + if (b < 0x80) { i++; continue; } + else if (b >= 0xc2 && b < 0xe0) extra = 1; + else if (b >= 0xe0 && b < 0xf0) extra = 2; + else if (b >= 0xf0 && b < 0xf5) extra = 3; + else return false; + if (i + extra >= input.length) return false; + let value = extra === 1 ? b & 0x1f : extra === 2 ? b & 0x0f : b & 0x07; + for (let k = 1; k <= extra; k++) { + if ((input[i + k] & 0xc0) !== 0x80) return false; + value = (value << 6) | (input[i + k] & 0x3f); + } + if ((extra === 1 && value < 0x80) || + (extra === 2 && (value < 0x800 || (value >= 0xd800 && value < 0xe000))) || + (extra === 3 && (value < 0x10000 || value > 0x10ffff))) return false; + i += extra + 1; + } + return true; + }, + indexOfBuffer: (buf, val, byteOffset, _encoding, dir) => searchBytes(buf, val, byteOffset, dir), + indexOfNumber(buf, val, byteOffset, dir) { + return searchBytes(buf, Uint8Array.of(val & 0xff), byteOffset, dir); + }, + indexOfString: (buf, val, byteOffset, encoding, dir) => + searchBytes(buf, encodeForSearch(val, encoding), byteOffset, dir), + swap16(buf) { + for (let i = 0; i + 1 < buf.length; i += 2) { + const t = buf[i]; buf[i] = buf[i + 1]; buf[i + 1] = t; + } + return buf; + }, + swap32(buf) { + for (let i = 0; i + 3 < buf.length; i += 4) { + let t = buf[i]; buf[i] = buf[i + 3]; buf[i + 3] = t; + t = buf[i + 1]; buf[i + 1] = buf[i + 2]; buf[i + 2] = t; + } + return buf; + }, + swap64(buf) { + for (let i = 0; i + 7 < buf.length; i += 8) { + for (let j = 0; j < 4; j++) { + const t = buf[i + j]; buf[i + j] = buf[i + 7 - j]; buf[i + 7 - j] = t; + } + } + return buf; + }, + atob(input) { + // Returns decoded latin1 string, or negative error codes like node. + let clean = ''; + for (const ch of input) { + if (' \t\n\f\r'.includes(ch)) continue; + clean += ch; + } + if (clean.length % 4 === 1) return -1; + let padStripped = clean; + if (clean.endsWith('==')) padStripped = clean.slice(0, -2); + else if (clean.endsWith('=')) padStripped = clean.slice(0, -1); + for (let i = 0; i < padStripped.length; i++) { + const v = B64_REV[padStripped.charCodeAt(i)]; + if (v === -1 || padStripped[i] === '-' || padStripped[i] === '_') return -2; + } + const tmp = new Uint8Array(Math.ceil(padStripped.length * 3 / 4)); + const n = base64WriteInto(tmp, padStripped, 0, tmp.length); + return charSlice(tmp, 0, n, 0xff); + }, + btoa(input) { + const bytes = new Uint8Array(input.length); + for (let i = 0; i < input.length; i++) { + const c = input.charCodeAt(i); + if (c > 0xff) return -1; + bytes[i] = c; + } + return base64EncodeRange(bytes, 0, bytes.length, B64_STD, true); + }, + asciiSlice: (buf, start, end) => { const [s, e] = normRange(buf, start, end); return charSlice(buf, s, e, 0x7f); }, + latin1Slice: (buf, start, end) => { const [s, e] = normRange(buf, start, end); return charSlice(buf, s, e, 0xff); }, + utf8Slice: (buf, start, end) => { const [s, e] = normRange(buf, start, end); return utf8DecodeRange(buf, s, e); }, + hexSlice: (buf, start, end) => { const [s, e] = normRange(buf, start, end); return hexSliceRange(buf, s, e); }, + ucs2Slice: (buf, start, end) => { const [s, e] = normRange(buf, start, end); return ucs2SliceRange(buf, s, e); }, + base64Slice: (buf, start, end) => { const [s, e] = normRange(buf, start, end); return base64EncodeRange(buf, s, e, B64_STD, true); }, + base64urlSlice: (buf, start, end) => { const [s, e] = normRange(buf, start, end); return base64EncodeRange(buf, s, e, B64_URL, false); }, + asciiWriteStatic: (buf, string, offset, length) => charWrite(buf, string, offset ?? 0, length ?? buf.length - (offset ?? 0)), + latin1WriteStatic: (buf, string, offset, length) => charWrite(buf, string, offset ?? 0, length ?? buf.length - (offset ?? 0)), + utf8WriteStatic: (buf, string, offset, length) => utf8WriteInto(buf, string, offset ?? 0, length ?? buf.length - (offset ?? 0)), + base64Write: (buf, string, offset, length) => base64WriteInto(buf, string, offset ?? 0, length ?? buf.length - (offset ?? 0)), + base64urlWrite: (buf, string, offset, length) => base64WriteInto(buf, string, offset ?? 0, length ?? buf.length - (offset ?? 0)), + hexWrite: (buf, string, offset, length) => hexWriteInto(buf, string, offset ?? 0, length ?? buf.length - (offset ?? 0)), + ucs2Write: (buf, string, offset, length) => ucs2WriteInto(buf, string, offset ?? 0, length ?? buf.length - (offset ?? 0)), + createUnsafeArrayBuffer: (size) => new ArrayBuffer(size), + setDetachKey() {}, + }); + + // -- stage 2.5: back validation/length codecs with upstream simdutf compiled + // to wasm32-wasip1 against the agentOS patched sysroot, instantiated with + // V8's own WebAssembly engine inside this isolate. The hand-written JS + // implementations above stay as the fallback (and as the differential + // reference for checks.js). Injected by the harness as base64 in + // __pocSimdutfWasmBase64; absent means JS-only mode. -- + (function initSimdutfWasm() { + const b64 = globalThis.__pocSimdutfWasmBase64; + delete globalThis.__pocSimdutfWasmBase64; + if (typeof b64 !== 'string' || b64.length === 0) return; + const wasmBytes = new Uint8Array(Math.ceil(b64.length * 3 / 4)); + const wasmLen = base64WriteInto(wasmBytes, b64, 0, wasmBytes.length); + const module = new WebAssembly.Module(wasmBytes.subarray(0, wasmLen)); + // Only malloc's abort paths import WASI; stub them, but fail loud on exit. + const wasiStub = () => 0; + const instance = new WebAssembly.Instance(module, { + wasi_snapshot_preview1: { + fd_close: wasiStub, + fd_seek: wasiStub, + fd_write: wasiStub, + proc_exit: (code) => { throw new Error(`simdutf wasm called proc_exit(${code})`); }, + }, + }); + const exps = instance.exports; + exps._initialize(); + + function withBytes(input, fn) { + const ptr = exps.poc_alloc(input.length); + if (ptr === 0) throw new Error('simdutf wasm poc_alloc failed'); + try { + // View created after alloc: memory.buffer may detach on growth. + new Uint8Array(exps.memory.buffer, ptr, input.length).set(input); + return fn(ptr, input.length); + } finally { + exps.poc_free(ptr); + } + } + + const jsFallback = { + isUtf8: bufferBinding.isUtf8, + isAscii: bufferBinding.isAscii, + byteLengthUtf8: bufferBinding.byteLengthUtf8, + }; + bufferBinding.isUtf8 = (input) => + input.length === 0 ? true : withBytes(input, (p, n) => exps.poc_is_utf8(p, n) === 1); + bufferBinding.isAscii = (input) => + input.length === 0 ? true : withBytes(input, (p, n) => exps.poc_is_ascii(p, n) === 1); + bufferBinding.byteLengthUtf8 = (string) => { + // simdutf assumes well-formed UTF-16; node replaces lone surrogates with + // U+FFFD. Keep the JS implementation for that (rare) case. + if (string.length === 0) return 0; + if (!string.isWellFormed()) return jsFallback.byteLengthUtf8(string); + const u16 = new Uint8Array(string.length * 2); + for (let i = 0; i < string.length; i++) { + const c = string.charCodeAt(i); + u16[2 * i] = c & 0xff; + u16[2 * i + 1] = c >>> 8; + } + return withBytes(u16, (p) => exps.poc_utf8_len_from_utf16le(p, string.length)); + }; + globalThis.__pocSimdutfBacked = true; + globalThis.__pocSimdutfJsFallback = jsFallback; + })(); + + // -- uv: errno surface used by internal/errors for error translation -- + const UV_ERRNOS = [ + ['EACCES', -13, 'permission denied'], + ['EBADF', -9, 'bad file descriptor'], + ['EEXIST', -17, 'file already exists'], + ['EINVAL', -22, 'invalid argument'], + ['EISDIR', -21, 'illegal operation on a directory'], + ['ENOENT', -2, 'no such file or directory'], + ['ENOTDIR', -20, 'not a directory'], + ['ENOTEMPTY', -39, 'directory not empty'], + ['EPERM', -1, 'operation not permitted'], + ]; + const uvBinding = withAutoStubs('uv', { + getErrorMap: () => new Map(UV_ERRNOS.map(([code, errno, msg]) => [errno, [code, msg]])), + errname(errno) { + const hit = UV_ERRNOS.find(([, num]) => num === errno); + return hit ? hit[0] : `UNKNOWN(${errno})`; + }, + ...Object.fromEntries(UV_ERRNOS.map(([code, errno]) => [`UV_${code}`, errno])), + }); + + // -- permission (permission model off) -- + const permissionBinding = withAutoStubs('permission', { + isEnabled: () => false, + has: () => true, + }); + + // ---- POC scheduler: macrotask FIFO + node-style nextTick queue + wiring + // for internal/timers (setImmediate/setTimeout). Substrate is a chained + // microtask (bare isolates expose no true macrotask API); FIFO order, + // run-after-current-synchronous-code, and nextTick-drain-after-each- + // completion match node. Exact libuv phase parity is NOT claimed. ---- + const nextTickQueue = []; + const macroQueue = []; + let macroPumpScheduled = false; + let tickDrainScheduled = false; + let processImmediateCb = null; + let processTimersCb = null; + let immediatePumpQueued = false; + let libuvClock = 1; + const immediateInfoArr = new Uint32Array(3); // kCount, kRefCount, kHasOutstanding + const timeoutInfoArr = new Int32Array(1); + + function drainNextTicks() { + while (nextTickQueue.length > 0) { + const { fn, args } = nextTickQueue.shift(); + fn(...args); + } + } + + function pumpImmediatesIfNeeded() { + if (immediateInfoArr[0] > 0 && processImmediateCb && !immediatePumpQueued) { + immediatePumpQueued = true; + macroQueue.push(() => { + immediatePumpQueued = false; + processImmediateCb(); + }); + } + } + + function pumpMacro() { + macroPumpScheduled = false; + drainNextTicks(); // ticks queued in the triggering turn run before I/O completions + const task = macroQueue.shift(); + if (task) { + try { + task(); + } finally { + drainNextTicks(); + pumpImmediatesIfNeeded(); + } + } + if (macroQueue.length > 0) ensureMacroPump(); + } + + function ensureMacroPump() { + if (macroPumpScheduled) return; + macroPumpScheduled = true; + // Two-hop deferral: plain microtasks queued in the same turn (promise + // continuations, the nextTick fallback drain) run before I/O completions, + // approximating node's nextTick/microtask-before-poll ordering. + Promise.resolve().then(() => Promise.resolve().then(pumpMacro)); + } + + function scheduleMacro(task) { + macroQueue.push(task); + ensureMacroPump(); + } + + process.nextTick = function nextTick(fn, ...args) { + nextTickQueue.push({ fn, args }); + if (!tickDrainScheduled) { + tickDrainScheduled = true; + // Fallback drain for ticks queued outside a scheduler task. + Promise.resolve().then(() => { + tickDrainScheduled = false; + drainNextTicks(); + // Piggyback: pick up immediates queued from synchronous code. + pumpImmediatesIfNeeded(); + if (macroQueue.length > 0) ensureMacroPump(); + }); + } + }; + globalThis.__pocScheduleMacro = scheduleMacro; // for ordering probes in checks + + // ---- fs: node-shaped errors + a pluggable backend. The in-memory backend + // below is the POC stand-in; a kernel/VFS-bridge backend implements the + // same object surface. ---- + function uvError(code, syscall, path) { + const entry = UV_ERRNOS.find(([c]) => c === code); + const [, errno, msg] = entry ?? [code, -4094, 'unknown error']; + let message = `${code}: ${msg}, ${syscall}`; + if (path !== undefined) message += ` '${path}'`; + const err = new Error(message); + err.errno = errno; + err.code = code; + err.syscall = syscall; + if (path !== undefined) err.path = path; + return err; + } + + // ---- async fs contract: node's lib passes either an FSReqCallback (fire + // req.oncomplete(err, result) later, `this` = req) or the kUsePromises + // sentinel (return a Promise). The req can sit MID-args (e.g. + // stat(path, bigint, req, throwIfNoEntry)), so dispatch scans args. + class FSReqCallback { + constructor(bigint) { + this.bigint = Boolean(bigint); + this.oncomplete = null; + this.context = undefined; + } + } + const kUsePromises = Symbol('fs.promises'); + + function finalizeFsBinding(ops, asyncOverrides = {}) { + const wrapped = {}; + for (const name of Object.keys(ops)) { + const syncFn = ops[name]; + wrapped[name] = function pocFsOp(...args) { + const reqIdx = args.findIndex( + (a) => a === kUsePromises || a instanceof FSReqCallback, + ); + if (reqIdx === -1) return syncFn(...args); + const req = args[reqIdx]; + args[reqIdx] = undefined; // keep positional args (throwIfNoEntry etc.) + const exec = asyncOverrides[name] + ? () => asyncOverrides[name](...args) + : () => syncFn(...args); + if (req === kUsePromises) { + return new Promise((resolve, reject) => { + scheduleMacro(() => { + try { + resolve(exec()); // adopts promises from async overrides + } catch (err) { + reject(err); + } + }); + }); + } + scheduleMacro(() => { + let result; + try { + result = exec(); + } catch (err) { + req.oncomplete.call(req, err); + return; + } + if (result && typeof result.then === 'function') { + result.then( + (value) => scheduleMacro(() => req.oncomplete.call(req, null, value)), + (err) => scheduleMacro(() => req.oncomplete.call(req, err)), + ); + } else { + req.oncomplete.call(req, null, result); + } + }); + return undefined; + }; + } + + // fs.promises open(): binding.openFileHandle(path, flags, mode, kUsePromises) + // resolves a handle exposing fd/close()/closeSync()/getAsyncId(). + wrapped.openFileHandle = function openFileHandle(path, flags, mode, req) { + const open = () => wrapped.open(path, flags, mode, kUsePromises); + const makeHandle = (fd) => ({ + fd, + close: () => wrapped.close(fd, kUsePromises), + closeSync: () => ops.close(fd), + getAsyncId: () => 0, + }); + if (req === kUsePromises) return open().then(makeHandle); + throw new Error('POC: openFileHandle only supports kUsePromises'); + }; + + wrapped.FSReqCallback = FSReqCallback; + wrapped.kUsePromises = kUsePromises; + return wrapped; + } + + function makeMemFsBackend() { + // path -> { type: 'file', data: Uint8Array, mode } | { type: 'dir', mode } + const nodes = new Map(); + const t0 = 1750000000; // fixed epoch seconds; deterministic mtimes + nodes.set('/', { type: 'dir', mode: 0o755, mtime: t0 }); + nodes.set('/tmp', { type: 'dir', mode: 0o777, mtime: t0 }); + + function normalize(p) { + const parts = String(p).split('/'); + const out = []; + for (const part of parts) { + if (part === '' || part === '.') continue; + if (part === '..') out.pop(); + else out.push(part); + } + return '/' + out.join('/'); + } + const parentOf = (p) => (p === '/' ? null : p.slice(0, p.lastIndexOf('/')) || '/'); + + return { + normalize, + get(p) { return nodes.get(normalize(p)); }, + requireDirParent(p, syscall) { + const parent = parentOf(normalize(p)); + const node = parent === null ? null : nodes.get(parent); + if (!node) throw uvError('ENOENT', syscall, p); + if (node.type !== 'dir') throw uvError('ENOTDIR', syscall, p); + return parent; + }, + set(p, node) { nodes.set(normalize(p), node); }, + delete(p) { nodes.delete(normalize(p)); }, + list(p) { + const norm = normalize(p); + const prefix = norm === '/' ? '/' : `${norm}/`; + const names = []; + for (const key of nodes.keys()) { + if (key !== norm && key.startsWith(prefix) && !key.slice(prefix.length).includes('/')) { + names.push(key.slice(prefix.length)); + } + } + return names.sort(); + }, + now() { return t0 + 1000; }, + }; + } + + function makeFsBinding(backend) { + const O = constantsBinding.fs; // real linux O_*/S_* values (harvested) + // fd table: fd -> { path, pos, readable, writable, append } + const fds = new Map(); + let nextFd = 3; + + function resolveFile(path, syscall) { + const node = backend.get(path); + if (!node) throw uvError('ENOENT', syscall, path); + return node; + } + + function openFd(path, flags, mode, syscall = 'open') { + const accMode = flags & (O.O_WRONLY | O.O_RDWR); + const writable = accMode !== 0; + let node = backend.get(path); + if (node?.type === 'dir') { + if (writable) throw uvError('EISDIR', syscall, path); + } else if (!node) { + if (!(flags & O.O_CREAT)) throw uvError('ENOENT', syscall, path); + backend.requireDirParent(path, syscall); + node = { type: 'file', data: new Uint8Array(0), mode: mode ?? 0o666, mtime: backend.now() }; + backend.set(path, node); + } else if ((flags & O.O_CREAT) && (flags & O.O_EXCL)) { + throw uvError('EEXIST', syscall, path); + } + if (node.type === 'file' && (flags & O.O_TRUNC) && writable) { + node.data = new Uint8Array(0); + } + const fd = nextFd++; + fds.set(fd, { + path: backend.normalize(path), + pos: 0, + readable: accMode !== O.O_WRONLY, + writable, + append: Boolean(flags & O.O_APPEND), + }); + return fd; + } + + function fdEntry(fd, syscall) { + const entry = fds.get(fd); + if (!entry) throw uvError('EBADF', syscall); + return entry; + } + + function statArrayFor(node, useBigint) { + const isDir = node.type === 'dir'; + const size = isDir ? 0 : node.data.length; + const mode = (isDir ? 0o040000 : 0o100000) | (node.mode & 0o7777); + const mtime = node.mtime ?? backend.now(); + const fields = [ + 1, mode, 1, 1000, 1000, 0, 4096, 1, size, + Math.ceil(size / 512), + mtime, 0, mtime, 0, mtime, 0, mtime, 0, + ]; + if (useBigint) return new BigInt64Array(fields.map(BigInt)); + return new Float64Array(fields); + } + + function writeAt(entry, node, bytes, position) { + const usesCursor = position === null || position === undefined || position < 0; + const pos = entry.append ? node.data.length : (usesCursor ? entry.pos : position); + const end = pos + bytes.length; + if (end > node.data.length) { + const grown = new Uint8Array(end); + grown.set(node.data); + node.data = grown; + } + node.data.set(bytes, pos); + node.mtime = backend.now(); + if (usesCursor || entry.append) entry.pos = end; + return bytes.length; + } + + return withAutoStubs('fs', finalizeFsBinding({ + open: (path, flags, mode) => openFd(path, flags, mode), + close(fd) { fdEntry(fd, 'close'); fds.delete(fd); }, + read(fd, buffer, offset, length, position) { + const entry = fdEntry(fd, 'read'); + if (!entry.readable) throw uvError('EBADF', 'read'); + const node = resolveFile(entry.path, 'read'); + const usesCursor = position === null || position === undefined || position < 0; + const pos = usesCursor ? entry.pos : position; + const n = Math.max(0, Math.min(length, node.data.length - pos)); + buffer.set(node.data.subarray(pos, pos + n), offset); + if (usesCursor) entry.pos = pos + n; + return n; + }, + writeBuffer(fd, buffer, offset, length, position) { + const entry = fdEntry(fd, 'write'); + if (!entry.writable) throw uvError('EBADF', 'write'); + const node = resolveFile(entry.path, 'write'); + return writeAt(entry, node, buffer.subarray(offset, offset + length), position); + }, + writeString(fd, string, position, _encoding) { + const entry = fdEntry(fd, 'write'); + if (!entry.writable) throw uvError('EBADF', 'write'); + const node = resolveFile(entry.path, 'write'); + const bytes = new Uint8Array(utf8ByteLength(string)); + utf8WriteInto(bytes, string, 0, bytes.length); + return writeAt(entry, node, bytes, position); + }, + writeBuffers(fd, buffers, position) { + const entry = fdEntry(fd, 'write'); + if (!entry.writable) throw uvError('EBADF', 'write'); + const node = resolveFile(entry.path, 'write'); + let total = 0; + let pos = position; + for (const buffer of buffers) { + total += writeAt(entry, node, buffer, pos); + if (pos !== null && pos !== undefined && pos >= 0) pos += buffer.length; + } + return total; + }, + readFileUtf8(pathOrFd, _flags) { + const path = typeof pathOrFd === 'number' ? fdEntry(pathOrFd, 'read').path : pathOrFd; + const node = resolveFile(path, 'open'); + if (node.type === 'dir') throw uvError('EISDIR', 'read', path); + return utf8DecodeRange(node.data, 0, node.data.length); + }, + writeFileUtf8(path, data, flags, mode) { + const fd = openFd(path, flags, mode); + try { + const entry = fds.get(fd); + const node = resolveFile(entry.path, 'write'); + const bytes = new Uint8Array(utf8ByteLength(data)); + utf8WriteInto(bytes, data, 0, bytes.length); + writeAt(entry, node, bytes, null); + } finally { + fds.delete(fd); + } + }, + existsSync(path) { + try { return backend.get(path) !== undefined; } catch { return false; } + }, + stat(path, useBigint, _req, throwIfNoEntry) { + const node = backend.get(path); + if (!node) { + if (throwIfNoEntry === false) return undefined; + throw uvError('ENOENT', 'stat', path); + } + return statArrayFor(node, useBigint); + }, + lstat(path, useBigint, _req, throwIfNoEntry) { + const node = backend.get(path); + if (!node) { + if (throwIfNoEntry === false) return undefined; + throw uvError('ENOENT', 'lstat', path); + } + return statArrayFor(node, useBigint); + }, + fstat(fd, useBigint, _req, shouldNotThrow) { + const entry = fds.get(fd); + const node = entry ? backend.get(entry.path) : undefined; + if (!node) { + if (shouldNotThrow) return undefined; + throw uvError(entry ? 'ENOENT' : 'EBADF', 'fstat', entry?.path); + } + return statArrayFor(node, useBigint); + }, + mkdir(path, mode, recursive) { + const norm = backend.normalize(path); + if (backend.get(norm)) { + if (recursive) return undefined; + throw uvError('EEXIST', 'mkdir', path); + } + if (recursive) { + const parts = norm.split('/').filter(Boolean); + let current = ''; + let first; + for (const part of parts) { + current += `/${part}`; + if (!backend.get(current)) { + backend.set(current, { type: 'dir', mode: mode ?? 0o777, mtime: backend.now() }); + first ??= current; + } + } + return first; + } + backend.requireDirParent(norm, 'mkdir'); + backend.set(norm, { type: 'dir', mode: mode ?? 0o777, mtime: backend.now() }); + return undefined; + }, + readdir(path, _encoding, withFileTypes) { + const node = backend.get(path); + if (!node) throw uvError('ENOENT', 'scandir', path); + if (node.type !== 'dir') throw uvError('ENOTDIR', 'scandir', path); + const names = backend.list(path); + if (!withFileTypes) return names; + const types = names.map((name) => + backend.get(`${backend.normalize(path)}/${name}`).type === 'dir' ? 2 : 1); + return [names, types]; + }, + unlink(path) { + const node = backend.get(path); + if (!node) throw uvError('ENOENT', 'unlink', path); + if (node.type === 'dir') throw uvError('EISDIR', 'unlink', path); + backend.delete(path); + }, + rmdir(path) { + const node = backend.get(path); + if (!node) throw uvError('ENOENT', 'rmdir', path); + if (node.type !== 'dir') throw uvError('ENOTDIR', 'rmdir', path); + if (backend.list(path).length > 0) throw uvError('ENOTEMPTY', 'rmdir', path); + backend.delete(path); + }, + })); + } + + // Real-VFS mode: when the agentOS `_fs*` sync bridge globals are present + // (inside a sidecar VM), implement the SAME internalBinding('fs') surface + // over them, so node's real lib/fs.js talks to the actual kernel/VFS. + // Bridge globals are node-API-shaped (fs.statSync, fs.readFileSync, ...). + // fd-level ops are emulated client-side over whole-file reads/writes for the + // POC; true fd mapping would use the fs.openSync/fs.readSync bridge globals. + function makeBridgeFsBinding() { + const g = globalThis; + const fds = new Map(); + let nextFd = 3; + const O = constantsBinding.fs; + + // Sidecar errors arrive as exceptions whose message leads with the code: + // "ENOENT: no such file or directory, ...". Re-shape into node-style errors + // with the syscall the caller expects. + function translate(err, syscall, path) { + const msg = err?.message ?? ''; + const match = /^([A-Z]+):/.exec(msg); + if (match && UV_ERRNOS.some(([code]) => code === match[1])) { + throw uvError(match[1], syscall, path); + } + // Some kernel errors carry only prose; map the common ones. + if (/entry not found|no such file or directory|not found/i.test(msg)) { + throw uvError('ENOENT', syscall, path); + } + if (/already exists/i.test(msg)) throw uvError('EEXIST', syscall, path); + if (/permission denied/i.test(msg)) throw uvError('EACCES', syscall, path); + if (/not a directory/i.test(msg)) throw uvError('ENOTDIR', syscall, path); + if (/directory not empty/i.test(msg)) throw uvError('ENOTEMPTY', syscall, path); + if (/is a directory/i.test(msg)) throw uvError('EISDIR', syscall, path); + throw err; + } + // Complex bridge results may arrive as JSON strings. + function decodeJson(value) { + return typeof value === 'string' ? JSON.parse(value) : value; + } + function call(fn, syscall, path, ...args) { + try { + return fn(...args); + } catch (err) { + translate(err, syscall, path); + } + } + + // _fsReadFileBinary returns base64; binary writes go raw when + // _fsWriteFileBinaryRaw exists, else base64-tagged. + function toBytes(value) { + if (value instanceof Uint8Array) return value; + if (Array.isArray(value)) return Uint8Array.from(value); + if (typeof value === 'string') { + const out = new Uint8Array(Math.ceil(value.length * 3 / 4)); + return out.subarray(0, base64WriteInto(out, value, 0, out.length)); + } + if (value && value.type === 'Buffer' && Array.isArray(value.data)) { + return Uint8Array.from(value.data); + } + throw new Error(`POC: unexpected bridge byte payload ${Object.prototype.toString.call(value)}`); + } + function writeBytes(path, bytes) { + if (typeof g._fsWriteFileBinaryRaw === 'function') { + call(g._fsWriteFileBinaryRaw, 'write', path, path, bytes); + return; + } + call(g._fsWriteFileBinary, 'write', path, path, { + __agentOSType: 'bytes', + base64: base64EncodeRange(bytes, 0, bytes.length, B64_STD, true), + }); + } + + function statObj(path, syscall, lstat = false) { + return decodeJson(call(lstat ? g._fsLstat : g._fsStat, syscall, path, path)); + } + + function statArrayFromObj(obj, useBigint, sizeOverride) { + const size = sizeOverride ?? obj.size ?? 0; + const mtimeMs = obj.mtimeMs ?? 0; + const sec = Math.floor(mtimeMs / 1000); + const nsec = Math.round((mtimeMs % 1000) * 1e6); + const fields = [ + obj.dev ?? 1, obj.mode ?? 0o100644, obj.nlink ?? 1, obj.uid ?? 1000, + obj.gid ?? 1000, obj.rdev ?? 0, obj.blksize ?? 4096, obj.ino ?? 1, + size, obj.blocks ?? Math.ceil(size / 512), + sec, nsec, sec, nsec, sec, nsec, sec, nsec, + ]; + if (useBigint) return new BigInt64Array(fields.map((v) => BigInt(Math.trunc(v)))); + return new Float64Array(fields); + } + + function readWholeFile(path, syscall) { + return toBytes(call(g._fsReadFileBinary, syscall, path, path)); + } + + function openFd(path, flags, mode, syscall = 'open') { + const accMode = flags & (O.O_WRONLY | O.O_RDWR); + const writable = accMode !== 0; + let data = null; + if ((flags & O.O_CREAT) && (flags & O.O_EXCL) && g._fsExists(path)) { + throw uvError('EEXIST', syscall, path); + } + if ((flags & O.O_TRUNC) && writable) { + data = new Uint8Array(0); + } else { + try { + data = readWholeFile(path, syscall); + } catch (err) { + if (err.code === 'ENOENT' && (flags & O.O_CREAT)) data = new Uint8Array(0); + else throw err; + } + } + const fd = nextFd++; + fds.set(fd, { + path, data, + pos: (flags & O.O_APPEND) ? data.length : 0, + readable: accMode !== O.O_WRONLY, + writable, + append: Boolean(flags & O.O_APPEND), + dirty: writable && Boolean(flags & (O.O_CREAT | O.O_TRUNC)), + }); + return fd; + } + + function fdEntry(fd, syscall) { + const entry = fds.get(fd); + if (!entry) throw uvError('EBADF', syscall); + return entry; + } + + function writeAt(entry, bytes, position) { + const usesCursor = position === null || position === undefined || position < 0; + const pos = entry.append ? entry.data.length : (usesCursor ? entry.pos : position); + const end = pos + bytes.length; + if (end > entry.data.length) { + const grown = new Uint8Array(end); + grown.set(entry.data); + entry.data = grown; + } + entry.data.set(bytes, pos); + entry.dirty = true; + if (usesCursor || entry.append) entry.pos = end; + return bytes.length; + } + + function normalizeReaddir(result, withFileTypes) { + let entries; + if (result instanceof Uint8Array) { + // Raw payload: repeating [kind:u8][nameLen:u32le][nameBytes]. + entries = []; + let offset = 0; + while (offset < result.byteLength) { + const kind = result[offset++]; + const nameLength = result[offset] | (result[offset + 1] << 8) | + (result[offset + 2] << 16) | (result[offset + 3] << 24); + offset += 4; + entries.push({ + name: utf8DecodeRange(result, offset, offset + nameLength), + isDirectory: kind === 1, + }); + offset += nameLength; + } + } else { + entries = decodeJson(result); + } + entries = entries.filter((e) => e.name !== '.' && e.name !== '..'); + const names = entries.map((e) => (typeof e === 'string' ? e : e.name)); + if (!withFileTypes) return names; + return [names, entries.map((e) => (typeof e === 'object' && e.isDirectory ? 2 : 1))]; + } + + // Async overrides route through the real `_fs*Async` bridge globals, so + // completions arrive via the session event stream (true async I/O), then + // get re-shaped exactly like the sync paths. + const asyncCall = (promise, syscall, path) => + promise.catch((err) => translate(err, syscall, path)); + + const statAsync = (asyncFn, syscall) => (path, useBigint, _req, throwIfNoEntry) => + asyncFn(path).then( + (v) => statArrayFromObj(decodeJson(v), useBigint), + (err) => { + try { + translate(err, syscall, path); + } catch (shaped) { + if (shaped.code === 'ENOENT' && throwIfNoEntry === false) return undefined; + throw shaped; + } + }, + ); + + async function openFdAsync(path, flags, mode) { + const accMode = flags & (O.O_WRONLY | O.O_RDWR); + const writable = accMode !== 0; + let data = null; + if ((flags & O.O_CREAT) && (flags & O.O_EXCL)) { + const exists = await g._fsStatAsync(path).then(() => true, () => false); + if (exists) throw uvError('EEXIST', 'open', path); + } + if ((flags & O.O_TRUNC) && writable) { + data = new Uint8Array(0); + } else { + try { + data = toBytes(await asyncCall(g._fsReadFileBinaryAsync(path), 'open', path)); + } catch (err) { + if (err.code === 'ENOENT' && (flags & O.O_CREAT)) data = new Uint8Array(0); + else throw err; + } + } + const fd = nextFd++; + fds.set(fd, { + path, data, + pos: (flags & O.O_APPEND) ? data.length : 0, + readable: accMode !== O.O_WRONLY, + writable, + append: Boolean(flags & O.O_APPEND), + dirty: writable && Boolean(flags & (O.O_CREAT | O.O_TRUNC)), + }); + return fd; + } + + const bridgeAsyncOverrides = { + open: (path, flags, mode) => openFdAsync(path, flags, mode), + close: async (fd) => { + const entry = fdEntry(fd, 'close'); + fds.delete(fd); + if (entry.dirty) { + await asyncCall(g._fsWriteFileBinaryAsync(entry.path, { + __agentOSType: 'bytes', + base64: base64EncodeRange(entry.data, 0, entry.data.length, B64_STD, true), + }), 'write', entry.path); + } + }, + stat: statAsync((p) => g._fsStatAsync(p), 'stat'), + lstat: statAsync((p) => g._fsLstatAsync(p), 'lstat'), + unlink: (path) => asyncCall(g._fsUnlinkAsync(path), 'unlink', path).then(() => undefined), + rmdir: (path) => asyncCall(g._fsRmdirAsync(path), 'rmdir', path).then(() => undefined), + mkdir: (path, mode, recursive) => + asyncCall(g._fsMkdirAsync(path, { recursive: Boolean(recursive), mode }), 'mkdir', path) + .then(() => undefined), + readdir: (path, _encoding, withFileTypes) => + g._fsStatAsync(path).then( + () => asyncCall(g._fsReadDirAsync(path), 'scandir', path), + (err) => translate(err, 'scandir', path), + ).then((result) => normalizeReaddir(result, withFileTypes)), + }; + + return withAutoStubs('fs', finalizeFsBinding({ + open: (path, flags, mode) => openFd(path, flags, mode), + close(fd) { + const entry = fdEntry(fd, 'close'); + if (entry.dirty) writeBytes(entry.path, entry.data); + fds.delete(fd); + }, + read(fd, buffer, offset, length, position) { + const entry = fdEntry(fd, 'read'); + if (!entry.readable) throw uvError('EBADF', 'read'); + const usesCursor = position === null || position === undefined || position < 0; + const pos = usesCursor ? entry.pos : position; + const n = Math.max(0, Math.min(length, entry.data.length - pos)); + buffer.set(entry.data.subarray(pos, pos + n), offset); + if (usesCursor) entry.pos = pos + n; + return n; + }, + writeBuffer(fd, buffer, offset, length, position) { + const entry = fdEntry(fd, 'write'); + if (!entry.writable) throw uvError('EBADF', 'write'); + return writeAt(entry, buffer.subarray(offset, offset + length), position); + }, + writeString(fd, string, position, _encoding) { + const entry = fdEntry(fd, 'write'); + if (!entry.writable) throw uvError('EBADF', 'write'); + const bytes = new Uint8Array(utf8ByteLength(string)); + utf8WriteInto(bytes, string, 0, bytes.length); + return writeAt(entry, bytes, position); + }, + writeBuffers(fd, buffers, position) { + const entry = fdEntry(fd, 'write'); + if (!entry.writable) throw uvError('EBADF', 'write'); + let total = 0; + let pos = position; + for (const buffer of buffers) { + total += writeAt(entry, buffer, pos); + if (pos !== null && pos !== undefined && pos >= 0) pos += buffer.length; + } + return total; + }, + readFileUtf8(pathOrFd, _flags) { + if (typeof pathOrFd === 'number') { + const entry = fdEntry(pathOrFd, 'read'); + return utf8DecodeRange(entry.data, 0, entry.data.length); + } + return call(g._fsReadFile, 'open', pathOrFd, pathOrFd, 'utf8'); + }, + writeFileUtf8(path, data, flags, _mode) { + if (flags & O.O_APPEND) { + let existing = ''; + try { + existing = call(g._fsReadFile, 'open', path, path, 'utf8'); + } catch (err) { + if (err.code !== 'ENOENT') throw err; + } + call(g._fsWriteFile, 'write', path, path, existing + data); + return; + } + call(g._fsWriteFile, 'write', path, path, data); + }, + existsSync: (path) => Boolean(g._fsExists(path)), + stat(path, useBigint, _req, throwIfNoEntry) { + try { + return statArrayFromObj(statObj(path, 'stat'), useBigint); + } catch (err) { + if (err.code === 'ENOENT' && throwIfNoEntry === false) return undefined; + throw err; + } + }, + lstat(path, useBigint, _req, throwIfNoEntry) { + try { + return statArrayFromObj(statObj(path, 'lstat', true), useBigint); + } catch (err) { + if (err.code === 'ENOENT' && throwIfNoEntry === false) return undefined; + throw err; + } + }, + fstat(fd, useBigint, _req, shouldNotThrow) { + const entry = fds.get(fd); + if (!entry) { + if (shouldNotThrow) return undefined; + throw uvError('EBADF', 'fstat'); + } + try { + const obj = entry.dirty + ? { mode: 0o100644, mtimeMs: 0 } + : statObj(entry.path, 'fstat'); + return statArrayFromObj(obj, useBigint, entry.data.length); + } catch (err) { + if (shouldNotThrow) return undefined; + throw err; + } + }, + mkdir(path, mode, recursive) { + call(g._fsMkdir, 'mkdir', path, path, { recursive: Boolean(recursive), mode }); + return undefined; + }, + readdir(path, _encoding, withFileTypes) { + // The kernel returns empty for a missing dir; node needs ENOENT. + if (!g._fsExists(path)) throw uvError('ENOENT', 'scandir', path); + const result = call(g._fsReadDir, 'scandir', path, path); + return normalizeReaddir(result, withFileTypes); + }, + unlink(path) { call(g._fsUnlink, 'unlink', path, path); }, + rmdir(path) { call(g._fsRmdir, 'rmdir', path, path); }, + }, bridgeAsyncOverrides)); + } + + // Explicit opt-in: bridge globals exist as unserviced stubs in light + // harnesses too, so presence-sniffing would deadlock a sync call there. + const fsBinding = globalThis.__pocUseBridgeFs === true + ? makeBridgeFsBinding() + : makeFsBinding(makeMemFsBackend()); + + // -- builtins / module_wrap / errors: what the REAL realm.js loader needs -- + let requireBuiltin = null; // captured from realm.js via setInternalLoaders + let realmInternalBinding = null; + + const builtinsBinding = { + builtinIds: Object.keys(sources), + compileFunction: (id) => compileBuiltin(id, [ + 'exports', 'require', 'module', 'process', 'internalBinding', 'primordials', + ]), + // Node's C++ stores the realm loaders here; we do the same to obtain them. + setInternalLoaders(binding, require) { + realmInternalBinding = binding; + requireBuiltin = require; + }, + }; + class ModuleWrap {} + const errorsBinding = withAutoStubs('errors', { + setPrepareStackTraceCallback() {}, + setEnhanceStackForFatalException() {}, + setSourceMapsSupport() {}, + triggerUncaughtException(err) { throw err; }, + exitCodes: { + kNoFailure: 0, kGenericUserError: 1, kInternalJSParseError: 3, + kInternalJSEvaluationFailure: 4, kV8FatalError: 5, kInvalidFatalExceptionMonkeyPatching: 6, + kExceptionInFatalExceptionHandler: 7, kInvalidCommandLineArgument: 9, + kBootstrapFailure: 10, kInvalidCommandLineArgument2: 12, kUnsettledTopLevelAwait: 13, + kUnCaughtException: 1, + }, + }); + + const bindings = { + __proto__: null, + constants: constantsBinding, + config: configBinding, + util: utilBinding, + types: typesBinding, + string_decoder: stringDecoderBinding, + options: optionsBinding, + messaging: messagingBinding, + buffer: bufferBinding, + builtins: builtinsBinding, + module_wrap: { ModuleWrap }, + errors: errorsBinding, + uv: uvBinding, + permission: permissionBinding, + fs: fsBinding, + encoding_binding: (() => { + const encodeIntoResults = new Uint32Array(2); + return withAutoStubs('encoding_binding', { + encodeIntoResults, + encodeUtf8String(input) { + const out = new Uint8Array(utf8ByteLength(input)); + utf8WriteInto(out, input, 0, out.length); + return out; + }, + encodeInto(input, dest) { + const written = utf8WriteInto(dest, input, 0, dest.length); + // POC: `read` is approximated as chars consumed for fully-written input. + encodeIntoResults[0] = written >= utf8ByteLength(input) ? input.length : 0; + encodeIntoResults[1] = written; + }, + decodeUTF8: (input, _ignoreBom, _fatal) => utf8DecodeRange(input, 0, input.length), + decodeLatin1: (input) => charSlice(input, 0, input.length, 0xff), + }); + })(), + fs_dir: withAutoStubs('fs_dir', {}), + credentials: withAutoStubs('credentials', { getTempDir: () => '/tmp' }), + performance: withAutoStubs('performance', { + constants: { + NODE_PERFORMANCE_MILESTONE_TIME_ORIGIN: 0, + NODE_PERFORMANCE_MILESTONE_TIME_ORIGIN_TIMESTAMP: 1, + NODE_PERFORMANCE_MILESTONE_ENVIRONMENT: 2, + NODE_PERFORMANCE_MILESTONE_NODE_START: 3, + NODE_PERFORMANCE_MILESTONE_V8_START: 4, + NODE_PERFORMANCE_MILESTONE_LOOP_START: 5, + NODE_PERFORMANCE_MILESTONE_LOOP_EXIT: 6, + }, + milestones: new Float64Array(8), + now: () => libuvClock, + markMilestone() {}, + }), + os: withAutoStubs('os', { + getHostname: () => 'agentos-poc', + getHomeDirectory: () => '/root', + getOSInformation: () => ['Linux', 'agentos', '6.1.0', 'x86_64'], + isBigEndian: false, + getAvailableParallelism: () => 1, + getFreeMem: () => 0, + getTotalMem: () => 0, + getUptime: () => 0, + getLoadAvg: (arr) => { arr[0] = 0; arr[1] = 0; arr[2] = 0; }, + getCPUs: () => [], + }), + // fs.watch is out of POC scope; class must exist for load-time destructure. + fs_event_wrap: withAutoStubs('fs_event_wrap', { + FSEvent: class FSEvent { + start() { throw new Error('POC: fs.watch not supported'); } + close() {} + }, + }), + diagnostics_channel: (() => { + const channelIndexes = new Map(); + return withAutoStubs('diagnostics_channel', { + subscribers: [], + linkNativeChannel() {}, + getOrCreateChannelIndex(name) { + if (!channelIndexes.has(name)) channelIndexes.set(name, channelIndexes.size); + return channelIndexes.get(name); + }, + }); + })(), + blob: withAutoStubs('blob', {}), + url: withAutoStubs('url', {}), + url_pattern: withAutoStubs('url_pattern', { URLPattern: class URLPattern {} }), + symbols: perIsolateSymbols, + // Indexes only need to be internally consistent: no C++ side reads them here. + async_wrap: withAutoStubs('async_wrap', { + setCallbackTrampoline() {}, + async_hook_fields: new Uint32Array(16), + async_id_fields: new Float64Array(8), + async_ids_stack: new Float64Array(4096), + execution_async_resources: [], + registerDestroyHook() {}, + queueDestroyAsyncId() {}, + constants: { + kInit: 0, kBefore: 1, kAfter: 2, kDestroy: 3, kPromiseResolve: 4, + kTotals: 5, kCheck: 6, kStackLength: 7, kUsesExecutionAsyncResource: 8, + kExecutionAsyncId: 0, kTriggerAsyncId: 1, kAsyncIdCounter: 2, + kDefaultTriggerAsyncId: 3, + }, + }), + async_context_frame: (() => { + let frame; + return withAutoStubs('async_context_frame', { + getContinuationPreservedEmbedderData: () => frame, + setContinuationPreservedEmbedderData(value) { frame = value; }, + }); + })(), + trace_events: withAutoStubs('trace_events', { + getCategoryEnabledBuffer: () => new Uint8Array(1), + trace() {}, + isTraceCategoryEnabled: () => false, + }), + task_queue: withAutoStubs('task_queue', { + enqueueMicrotask(fn) { Promise.resolve().then(fn); }, + setTickCallback() {}, + setPromiseRejectCallback() {}, + }), + // Wired into the POC scheduler: internal/timers registers its + // processImmediate/processTimers callbacks through setupTimers (exactly + // what node's C++ stores), and scheduleTimer turns into a macrotask that + // advances the fake libuv clock past the requested expiry. + timers: withAutoStubs('timers', { + immediateInfo: immediateInfoArr, + timeoutInfo: timeoutInfoArr, + getLibuvNow: () => libuvClock, + setupTimers(processImmediate, processTimers) { + processImmediateCb = processImmediate; + processTimersCb = processTimers; + }, + scheduleTimer(msecs) { + scheduleMacro(() => { + libuvClock += Math.max(1, msecs); + if (processTimersCb) processTimersCb(libuvClock); + }); + }, + toggleTimerRef() {}, + // Fires on immediate refcount 0->1 (first pending immediate): our cue to + // pump, since there is no per-loop-iteration check phase here. NOTE the + // Immediate constructor calls ref() BEFORE immediateInfo[kCount]++, so + // the count must be checked from a scheduled task, not synchronously. + toggleImmediateRef(on) { + if (on) { + scheduleMacro(() => { + if (immediateInfoArr[0] > 0 && processImmediateCb) processImmediateCb(); + }); + } + }, + }), + process_methods: (() => { + const hrtimeBuffer = new Uint32Array(3); + return withAutoStubs('process_methods', { + hrtimeBuffer, + hrtime() { + const ns = Date.now() * 1e6; + const sec = Math.floor(ns / 1e9); + hrtimeBuffer[0] = Math.floor(sec / 0x100000000); + hrtimeBuffer[1] = sec >>> 0; + hrtimeBuffer[2] = ns % 1e9; + }, + }); + })(), + mksnapshot: withAutoStubs('mksnapshot', { + setSerializeCallback() {}, + setDeserializeCallback() {}, + setDeserializeMainFunction() {}, + isBuildingSnapshotBuffer: new Uint8Array([0]), + }), + }; + + function getInternalBinding(name) { + return bindings[name] ?? missingBinding(name); + } + function getLinkedBinding(name) { + throw new Error(`POC: linked binding '${name}' not supported`); + } + + // ---- run node's REAL realm bootstrap (lib/internal/bootstrap/realm.js) ---- + compileBuiltin('internal/bootstrap/realm', [ + 'process', 'getLinkedBinding', 'getInternalBinding', 'primordials', + ])(process, getLinkedBinding, getInternalBinding, primordials); + + if (typeof requireBuiltin !== 'function') { + throw new Error('POC: realm.js did not hand back its loaders via setInternalLoaders'); + } + + // Wire node's REAL timer processing (normally done by bootstrap/node.js: + // getTimerCallbacks(runNextTicks) + setupTimers). Our scheduler drives + // processImmediate/processTimers instead of libuv's check/timer phases. + { + const internalTimers = requireBuiltin('internal/timers'); + const { processImmediate, processTimers } = + internalTimers.getTimerCallbacks(drainNextTicks); + bindings.timers.setupTimers(processImmediate, processTimers); + // Normally done by internal/process/pre_execution.js. + requireBuiltin('internal/util/debuglog').initializeDebugEnv(process.env.NODE_DEBUG); + } + + globalThis.__poc = { + requireBuiltin, + internalBinding: realmInternalBinding, + process, + primordials, + }; +})(); diff --git a/crates/v8-runtime/tests/node_stdlib_poc/checks.js b/crates/v8-runtime/tests/node_stdlib_poc/checks.js new file mode 100644 index 0000000000..a3a61f26d2 --- /dev/null +++ b/crates/v8-runtime/tests/node_stdlib_poc/checks.js @@ -0,0 +1,402 @@ +// POC stage-1 assertions: node's REAL lib/path.js and lib/buffer.js, loaded by +// bootstrap.js, must behave like node. Throws one aggregate error on failure. +(function () { + 'use strict'; + const { requireBuiltin } = globalThis.__poc; + const failures = []; + + function check(name, actual, expected) { + const a = JSON.stringify(actual); + const e = JSON.stringify(expected); + if (a !== e) failures.push(`${name}: got ${a}, want ${e}`); + } + + // ---------- path ---------- + const path = requireBuiltin('path'); + check('path.join', path.join('/a', 'b', '..', 'c'), '/a/c'); + check('path.join dots', path.join('.', 'x/y', './z'), 'x/y/z'); + check('path.resolve', path.resolve('/foo/bar', './baz'), '/foo/bar/baz'); + check('path.resolve up', path.resolve('/foo/bar', '../qux'), '/foo/qux'); + check('path.normalize', path.normalize('/foo/bar//baz/asdf/quux/..'), '/foo/bar/baz/asdf'); + check('path.dirname', path.dirname('/foo/bar/baz.txt'), '/foo/bar'); + check('path.basename', path.basename('/foo/bar/baz.txt'), 'baz.txt'); + check('path.basename ext', path.basename('/foo/bar/baz.txt', '.txt'), 'baz'); + check('path.extname', path.extname('index.coffee.md'), '.md'); + check('path.extname none', path.extname('Makefile'), ''); + check('path.relative', path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb'), '../../impl/bbb'); + check('path.isAbsolute', [path.isAbsolute('/x'), path.isAbsolute('x')], [true, false]); + check('path.parse', path.parse('/home/user/dir/file.txt'), + { root: '/', dir: '/home/user/dir', base: 'file.txt', ext: '.txt', name: 'file' }); + check('path.format', path.format({ dir: '/a/b', name: 'c', ext: '.d' }), '/a/b/c.d'); + check('path.sep', path.sep, '/'); + check('path.win32.join', path.win32.join('C:\\a', 'b', '..', 'c'), 'C:\\a\\c'); + check('path.win32.basename', path.win32.basename('C:\\temp\\myfile.html'), 'myfile.html'); + check('path.win32.resolve', path.win32.resolve('C:\\foo', 'bar'), 'C:\\foo\\bar'); + check('path.posix identity', path.posix.join('a', 'b'), 'a/b'); + + // ---------- buffer ---------- + const { Buffer, isUtf8, isAscii, btoa, atob } = requireBuiltin('buffer'); + + check('Buffer.from utf8 roundtrip', Buffer.from('hello wörld ✓', 'utf8').toString('utf8'), 'hello wörld ✓'); + check('Buffer.from utf8 bytes', Array.from(Buffer.from('é', 'utf8')), [0xc3, 0xa9]); + check('byteLength utf8', Buffer.byteLength('hello wörld ✓', 'utf8'), 16); + check('byteLength astral', Buffer.byteLength('💩', 'utf8'), 4); + check('toString hex', Buffer.from([0xde, 0xad, 0xbe, 0xef]).toString('hex'), 'deadbeef'); + check('from hex', Array.from(Buffer.from('cafebabe', 'hex')), [0xca, 0xfe, 0xba, 0xbe]); + check('toString base64', Buffer.from('Many hands make light work.').toString('base64'), + 'TWFueSBoYW5kcyBtYWtlIGxpZ2h0IHdvcmsu'); + check('from base64', Buffer.from('TWFueSBoYW5kcyBtYWtlIGxpZ2h0IHdvcmsu', 'base64').toString('utf8'), + 'Many hands make light work.'); + check('base64url value', Buffer.from([0xfb, 0xff]).toString('base64url'), '-_8'); + check('toString latin1', Buffer.from([0x68, 0xe9]).toString('latin1'), 'hé'); + check('toString utf16le', Buffer.from([0x68, 0x00, 0x69, 0x00]).toString('utf16le'), 'hi'); + check('alloc fill num', Array.from(Buffer.alloc(3, 7)), [7, 7, 7]); + check('alloc fill str', Buffer.alloc(6, 'ab').toString('utf8'), 'ababab'); + check('alloc zero', Array.from(Buffer.alloc(4)), [0, 0, 0, 0]); + check('concat', Buffer.concat([Buffer.from('ab'), Buffer.from('cd')]).toString(), 'abcd'); + check('concat totalLength', Buffer.concat([Buffer.from('ab'), Buffer.from('cd')], 3).toString(), 'abc'); + check('compare eq', Buffer.compare(Buffer.from('abc'), Buffer.from('abc')), 0); + check('compare lt', Buffer.compare(Buffer.from('abb'), Buffer.from('abc')), -1); + check('compare len', Buffer.compare(Buffer.from('ab'), Buffer.from('abc')), -1); + check('equals', Buffer.from('x').equals(Buffer.from('x')), true); + check('indexOf str', Buffer.from('hello hello').indexOf('llo'), 2); + check('indexOf str from', Buffer.from('hello hello').indexOf('llo', 3), 8); + check('lastIndexOf str', Buffer.from('hello hello').lastIndexOf('llo'), 8); + check('indexOf num', Buffer.from([1, 2, 3, 2]).indexOf(2), 1); + check('indexOf buf', Buffer.from('abcdef').indexOf(Buffer.from('cd')), 2); + check('indexOf missing', Buffer.from('abc').indexOf('zz'), -1); + check('includes', Buffer.from('abc').includes('b'), true); + check('slice/subarray', Buffer.from('buffer').subarray(1, 4).toString(), 'uff'); + check('subarray shares memory', (() => { + const b = Buffer.from('abcd'); + b.subarray(1, 3)[0] = 0x7a; + return b.toString(); + })(), 'azcd'); + check('write returns count', Buffer.alloc(4).write('abcdef'), 4); + check('write partial multibyte', (() => { + const b = Buffer.alloc(3); + const n = b.write('aé é', 0, 3, 'utf8'); // 'a'(1) + 'é'(2) fits, next ' ' doesn't + return [n, b.toString('utf8', 0, n)]; + })(), [3, 'aé']); + check('readUInt32LE', (() => { + const b = Buffer.alloc(4); + b.writeUInt32LE(0xdeadbeef, 0); + return [b.readUInt32LE(0), Array.from(b)]; + })(), [0xdeadbeef, [0xef, 0xbe, 0xad, 0xde]]); + check('readUInt32BE', (() => { + const b = Buffer.alloc(4); + b.writeUInt32BE(0x01020304, 0); + return [b.readUInt32BE(0), Array.from(b)]; + })(), [0x01020304, [1, 2, 3, 4]]); + check('readBigUInt64LE', (() => { + const b = Buffer.alloc(8); + b.writeBigUInt64LE(0x0102030405060708n, 0); + return b.readBigUInt64LE(0) === 0x0102030405060708n; + })(), true); + check('readDoubleLE', (() => { + const b = Buffer.alloc(8); + b.writeDoubleLE(3.5, 0); + return b.readDoubleLE(0); + })(), 3.5); + check('swap16', Array.from(Buffer.from([1, 2, 3, 4]).swap16()), [2, 1, 4, 3]); + check('swap32', Array.from(Buffer.from([1, 2, 3, 4]).swap32()), [4, 3, 2, 1]); + check('isBuffer', [Buffer.isBuffer(Buffer.alloc(1)), Buffer.isBuffer(new Uint8Array(1))], [true, false]); + check('from arraybuffer view', (() => { + const ab = new ArrayBuffer(4); + new Uint8Array(ab).set([9, 8, 7, 6]); + return Array.from(Buffer.from(ab, 1, 2)); + })(), [8, 7]); + check('from array', Array.from(Buffer.from([256 + 5, 2])), [5, 2]); + check('isUtf8/isAscii', [ + isUtf8(Buffer.from('héllo')), isUtf8(Buffer.from([0xff, 0xfe])), + isAscii(Buffer.from('abc')), isAscii(Buffer.from('é')), + ], [true, false, true, false]); + check('btoa/atob', [btoa('hello'), atob('aGVsbG8=')], ['aGVsbG8=', 'hello']); + check('toJSON', Buffer.from([1, 2]).toJSON(), { type: 'Buffer', data: [1, 2] }); + check('fill on buffer', Array.from(Buffer.from([0, 0, 0, 0, 0]).fill('ab', 1, 4)), [0, 97, 98, 97, 0]); + check('copy', (() => { + const src = Buffer.from('abcdef'); + const dst = Buffer.alloc(4, 0x2e); + const n = src.copy(dst, 1, 2, 4); + return [n, dst.toString()]; + })(), [2, '.cd.']); + + // ---------- stage 2.5: simdutf-backed codecs (wasm compiled with our libc) ---------- + // Fixed expectations hold in both modes (wasm-backed and JS fallback). + check('isUtf8 surrogate-encoded', isUtf8(Buffer.from([0xed, 0xa0, 0x80])), false); + check('isUtf8 overlong', isUtf8(Buffer.from([0xc0, 0xaf])), false); + check('isUtf8 4byte max', isUtf8(Buffer.from([0xf4, 0x8f, 0xbf, 0xbf])), true); + check('isUtf8 beyond U+10FFFF', isUtf8(Buffer.from([0xf4, 0x90, 0x80, 0x80])), false); + check('isUtf8 truncated', isUtf8(Buffer.from([0x61, 0xc3])), false); + check('isUtf8/isAscii empty', [isUtf8(Buffer.alloc(0)), isAscii(Buffer.alloc(0))], [true, true]); + check('byteLength 2byte boundary', Buffer.byteLength('€߿', 'utf8'), 4); + check('byteLength 3byte boundary', Buffer.byteLength('ࠀ￿', 'utf8'), 6); + check('byteLength astral pair', Buffer.byteLength('💩', 'utf8'), 4); + check('byteLength lone surrogate', Buffer.byteLength('a\ud800b', 'utf8'), 5); // U+FFFD replacement + check('byteLength 1MiB ascii', Buffer.byteLength('a'.repeat(1 << 20), 'utf8'), 1 << 20); + + const simdutfBacked = globalThis.__pocSimdutfBacked === true; + if (simdutfBacked) { + // Differential check: the wasm-backed binding must agree with the JS + // reference implementations across tricky inputs. + const { isUtf8: jsIsUtf8, isAscii: jsIsAscii, byteLengthUtf8: jsByteLen } = + globalThis.__pocSimdutfJsFallback; + const trickyBuffers = [ + Buffer.alloc(0), + Buffer.from('plain ascii'), + Buffer.from('héllo wörld ✓ 💩'), + Buffer.from([0xff, 0xfe, 0x80]), + Buffer.from([0x61, 0xc3]), + Buffer.from([0xed, 0xa0, 0x80]), + Buffer.from([0xf4, 0x90, 0x80, 0x80]), + Buffer.alloc(1 << 20, 0x61), + (() => { const b = Buffer.alloc(1 << 20, 0x61); b[(1 << 20) - 1] = 0xff; return b; })(), + ]; + trickyBuffers.forEach((b, i) => { + check(`simdutf diff isUtf8[${i}]`, isUtf8(b), jsIsUtf8(b)); + check(`simdutf diff isAscii[${i}]`, isAscii(b), jsIsAscii(b)); + }); + const trickyStrings = [ + '', 'ascii only', 'héllo', '💩💩💩', 'mixed é 💩 text', + '߿ࠀ￿', 'a'.repeat(100000) + '💩', + ]; + trickyStrings.forEach((s, i) => { + check(`simdutf diff byteLength[${i}]`, Buffer.byteLength(s, 'utf8'), jsByteLen(s)); + }); + } + + // ---------- fs (stage 2: sync ops via internalBinding('fs') shim) ---------- + const fs = requireBuiltin('fs'); + + function checkThrows(name, fn, expectations) { + try { + fn(); + failures.push(`${name}: expected throw, got none`); + } catch (err) { + for (const key of Object.keys(expectations)) { + if (err[key] !== expectations[key]) { + failures.push(`${name}: err.${key} got ${JSON.stringify(err[key])}, want ${JSON.stringify(expectations[key])}`); + } + } + } + } + + // Some VM base layers may lack /tmp; the mem backend pre-creates it. + try { fs.mkdirSync('/tmp'); } catch (err) { if (err.code !== 'EEXIST') throw err; } + + // utf8 write/read roundtrip (fast paths: writeFileUtf8 / readFileUtf8) + fs.writeFileSync('/tmp/poc.txt', 'hello wörld ✓\n'); + check('fs utf8 roundtrip', fs.readFileSync('/tmp/poc.txt', 'utf8'), 'hello wörld ✓\n'); + + // binary roundtrip (open/write/read/close path, no fast path) + const payload = Buffer.from([0, 1, 2, 250, 251, 252, 253, 254, 255]); + fs.writeFileSync('/tmp/poc.bin', payload); + const readBack = fs.readFileSync('/tmp/poc.bin'); + check('fs binary roundtrip', [Buffer.isBuffer(readBack), Array.from(readBack)], + [true, Array.from(payload)]); + + // appending via flag + fs.writeFileSync('/tmp/poc.txt', 'more', { flag: 'a' }); + check('fs append flag', fs.readFileSync('/tmp/poc.txt', 'utf8'), 'hello wörld ✓\nmore'); + + // statSync fields + const st = fs.statSync('/tmp/poc.bin'); + check('stat size', st.size, 9); + check('stat isFile/isDirectory', [st.isFile(), st.isDirectory()], [true, false]); + check('stat dir', (() => { const d = fs.statSync('/tmp'); return [d.isDirectory(), d.isFile()]; })(), + [true, false]); + check('stat mode type bits', (st.mode & 0o170000) === 0o100000, true); + check('stat mtime plausible', st.mtime instanceof Date && st.mtimeMs > 0, true); + check('statSync throwIfNoEntry:false', fs.statSync('/tmp/nope', { throwIfNoEntry: false }), undefined); + check('existsSync', [fs.existsSync('/tmp/poc.bin'), fs.existsSync('/tmp/nope')], [true, false]); + + // ENOENT error shape (code + errno + syscall — exercises error translation) + checkThrows('readFileSync ENOENT', () => fs.readFileSync('/tmp/missing.txt', 'utf8'), + { code: 'ENOENT', errno: -2, syscall: 'open' }); + checkThrows('statSync ENOENT', () => fs.statSync('/tmp/missing.txt'), + { code: 'ENOENT', errno: -2, syscall: 'stat' }); + + // mkdirSync + readdirSync + fs.mkdirSync('/tmp/dir-a'); + fs.mkdirSync('/tmp/dir-a/nested/deep', { recursive: true }); + fs.writeFileSync('/tmp/dir-a/file1.txt', 'x'); + fs.writeFileSync('/tmp/dir-a/file2.txt', 'y'); + check('readdirSync', fs.readdirSync('/tmp/dir-a').sort(), ['file1.txt', 'file2.txt', 'nested']); + checkThrows('mkdirSync EEXIST', () => fs.mkdirSync('/tmp/dir-a'), + { code: 'EEXIST', syscall: 'mkdir' }); + checkThrows('readdirSync ENOENT', () => fs.readdirSync('/tmp/no-dir'), + { code: 'ENOENT', syscall: 'scandir' }); + + // unlinkSync then ENOENT + fs.unlinkSync('/tmp/poc.bin'); + check('unlink removes', fs.existsSync('/tmp/poc.bin'), false); + checkThrows('unlinkSync ENOENT', () => fs.unlinkSync('/tmp/poc.bin'), + { code: 'ENOENT', errno: -2, syscall: 'unlink' }); + + // openSync/readSync positional + const fd = fs.openSync('/tmp/poc.txt', 'r'); + const buf4 = Buffer.alloc(4); + const nRead = fs.readSync(fd, buf4, 0, 4, 6); + fs.closeSync(fd); + // bytes 6..10 of 'hello wörld' are w + ö(2 bytes) + r + check('readSync positional', [nRead, buf4.toString('utf8')], [4, 'wör']); + + if (failures.length > 0) { + throw new Error(`node-stdlib POC sync failures (${failures.length}):\n ${failures.join('\n ')}`); + } + + // ---------- stage 3: ASYNC fs (callbacks, promises, ordering, streams) ---- + const { process } = globalThis.__poc; + const timers = requireBuiltin('timers'); + const fsp = requireBuiltin('fs/promises'); + + // promisify helper for callback-API assertions + const p = (fn, ...args) => new Promise((resolve, reject) => { + fn(...args, (err, value) => (err ? reject(err) : resolve(value))); + }); + + globalThis.__pocAsync = (async () => { + // --- callback API roundtrips --- + await p(fs.writeFile, '/tmp/async.txt', 'async wörld ✓'); + check('cb readFile utf8', await p(fs.readFile, '/tmp/async.txt', 'utf8'), 'async wörld ✓'); + const cbBinary = await p(fs.readFile, '/tmp/async.txt'); // no encoding: open/fstat/read/close chain + check('cb readFile binary', [Buffer.isBuffer(cbBinary), cbBinary.toString('utf8')], + [true, 'async wörld ✓']); + const cbStat = await p(fs.stat, '/tmp/async.txt'); + check('cb stat', [cbStat.isFile(), cbStat.size > 0], [true, true]); + await p(fs.mkdir, '/tmp/async-dir'); + await p(fs.writeFile, '/tmp/async-dir/one.txt', '1'); + check('cb readdir', await p(fs.readdir, '/tmp/async-dir'), ['one.txt']); + await p(fs.unlink, '/tmp/async-dir/one.txt'); + check('cb unlink', fs.existsSync('/tmp/async-dir/one.txt'), false); + + // --- async ENOENT error shape --- + const cbErr = await p(fs.readFile, '/tmp/no-such-async.txt', 'utf8').then( + () => null, (err) => err); + check('cb ENOENT fields', [cbErr?.code, cbErr?.errno, cbErr?.syscall], + ['ENOENT', -2, 'open']); + + // --- concurrency: 10 in flight, all complete, no corruption --- + await Promise.all(Array.from({ length: 10 }, (_, i) => + p(fs.writeFile, `/tmp/conc-${i}.txt`, `payload-${i}-${'x'.repeat(i * 100)}`))); + const concReads = await Promise.all(Array.from({ length: 10 }, (_, i) => + p(fs.readFile, `/tmp/conc-${i}.txt`, 'utf8'))); + check('concurrent integrity', + concReads.every((s, i) => s === `payload-${i}-${'x'.repeat(i * 100)}`), true); + + // --- fs/promises --- + await fsp.writeFile('/tmp/promises.txt', 'via promises'); + check('fsp readFile', await fsp.readFile('/tmp/promises.txt', 'utf8'), 'via promises'); + const pStat = await fsp.stat('/tmp/promises.txt'); + check('fsp stat', [pStat.isFile(), pStat.size], [true, 12]); + await fsp.mkdir('/tmp/promises-dir'); + check('fsp mkdir', fs.statSync('/tmp/promises-dir').isDirectory(), true); + const pErr = await fsp.readFile('/tmp/nope-promises.txt', 'utf8').then(() => null, (e) => e); + check('fsp ENOENT rejection', [pErr?.code, pErr?.errno], ['ENOENT', -2]); + + // --- ordering probes --- + const order1 = []; + const fsDone = new Promise((resolve) => { + fs.stat('/tmp', () => { order1.push('fs'); resolve(); }); + }); + process.nextTick(() => order1.push('tick')); + Promise.resolve().then(() => order1.push('micro')); + order1.push('sync'); + await fsDone; + check('ordering: sync first', order1[0], 'sync'); + check('ordering: fs completion last', order1[order1.length - 1], 'fs'); + check('ordering: tick before fs', order1.indexOf('tick') < order1.indexOf('fs'), true); + // node also guarantees micro before fs; record observed order (see notes) + globalThis.__pocOrder1 = order1.join(','); + + const order2 = []; + await Promise.all([ + new Promise((resolve) => fs.stat('/tmp', () => { + order2.push('cb1'); + process.nextTick(() => order2.push('tick-in-cb1')); + resolve(); + })), + new Promise((resolve) => fs.stat('/tmp', () => { order2.push('cb2'); resolve(); })), + ]); + check('ordering: nextTick in completion before next completion', + order2.join(','), 'cb1,tick-in-cb1,cb2'); + + const order3 = []; + await Promise.all([ + new Promise((resolve) => timers.setImmediate(() => { order3.push('immediate'); resolve(); })), + new Promise((resolve) => fs.stat('/tmp', () => { order3.push('fs'); resolve(); })), + ]); + check('ordering: immediate and fs completion both ran', order3.length, 2); + globalThis.__pocOrder3 = order3.join(','); // observed order recorded, not asserted + + // --- setTimeout sanity through real lib/timers.js --- + check('setTimeout fires', await new Promise((resolve) => + timers.setTimeout(() => resolve('timer'), 5)), 'timer'); + + // --- streams probe: createReadStream / createWriteStream / pipe --- + const MB = 1024 * 1024; + const big = Buffer.alloc(MB); + for (let i = 0; i < MB; i++) big[i] = i % 251; + fs.writeFileSync('/tmp/big.bin', big); + + const chunks = []; + const events = []; + await new Promise((resolve, reject) => { + const rs = fs.createReadStream('/tmp/big.bin'); + rs.on('data', (chunk) => chunks.push(chunk)); + rs.on('end', () => events.push('end')); + rs.on('close', () => { events.push('close'); resolve(); }); + rs.on('error', reject); + }); + const collected = Buffer.concat(chunks); + check('stream read length', collected.length, MB); + check('stream read content', collected.equals(big), true); + check('stream read got multiple chunks', chunks.length > 1, true); + check('stream read events', events, ['end', 'close']); + + await new Promise((resolve, reject) => { + const ws = fs.createWriteStream('/tmp/ws.bin'); + ws.on('error', reject); + ws.on('close', resolve); + ws.write(big.subarray(0, 512 * 1024)); + ws.end(big.subarray(512 * 1024)); + }); + check('write stream content', fs.readFileSync('/tmp/ws.bin').equals(big), true); + + await new Promise((resolve, reject) => { + const rs = fs.createReadStream('/tmp/big.bin'); + const ws = fs.createWriteStream('/tmp/copy.bin'); + ws.on('close', resolve); + rs.on('error', reject); + ws.on('error', reject); + rs.pipe(ws); + }); + check('pipe copy', fs.readFileSync('/tmp/copy.bin').equals(big), true); + + if (failures.length > 0) { + throw new Error(`node-stdlib POC async failures (${failures.length}):\n ${failures.join('\n ')}`); + } + return `ok: all sync+async checks passed (${simdutfBacked ? 'simdutf wasm-backed' : 'js codecs'})`; + })(); + + globalThis.__pocAsync.then( + (result) => { + globalThis.__pocResult = result; + // Guest-probe harnesses read a single JSON line from stdout, printed only + // once async work has genuinely finished (doubles as a liveness proof). + if (typeof console !== 'undefined' && typeof console.log === 'function') { + console.log(JSON.stringify({ + poc: result, + order1: globalThis.__pocOrder1, + order3: globalThis.__pocOrder3, + })); + } + }, + (err) => { + globalThis.__pocResult = `FAILED: ${err?.stack ?? err}`; + if (typeof console !== 'undefined' && typeof console.error === 'function') { + console.error(globalThis.__pocResult); + } + }, + ); +})(); diff --git a/crates/v8-runtime/tests/node_stdlib_poc/constants.json b/crates/v8-runtime/tests/node_stdlib_poc/constants.json new file mode 100644 index 0000000000..0080545d6d --- /dev/null +++ b/crates/v8-runtime/tests/node_stdlib_poc/constants.json @@ -0,0 +1 @@ +{"os":{"UV_UDP_REUSEADDR":4,"dlopen":{"RTLD_LAZY":1,"RTLD_NOW":2,"RTLD_GLOBAL":256,"RTLD_LOCAL":0,"RTLD_DEEPBIND":8},"errno":{"E2BIG":7,"EACCES":13,"EADDRINUSE":98,"EADDRNOTAVAIL":99,"EAFNOSUPPORT":97,"EAGAIN":11,"EALREADY":114,"EBADF":9,"EBADMSG":74,"EBUSY":16,"ECANCELED":125,"ECHILD":10,"ECONNABORTED":103,"ECONNREFUSED":111,"ECONNRESET":104,"EDEADLK":35,"EDESTADDRREQ":89,"EDOM":33,"EDQUOT":122,"EEXIST":17,"EFAULT":14,"EFBIG":27,"EHOSTUNREACH":113,"EIDRM":43,"EILSEQ":84,"EINPROGRESS":115,"EINTR":4,"EINVAL":22,"EIO":5,"EISCONN":106,"EISDIR":21,"ELOOP":40,"EMFILE":24,"EMLINK":31,"EMSGSIZE":90,"EMULTIHOP":72,"ENAMETOOLONG":36,"ENETDOWN":100,"ENETRESET":102,"ENETUNREACH":101,"ENFILE":23,"ENOBUFS":105,"ENODATA":61,"ENODEV":19,"ENOENT":2,"ENOEXEC":8,"ENOLCK":37,"ENOLINK":67,"ENOMEM":12,"ENOMSG":42,"ENOPROTOOPT":92,"ENOSPC":28,"ENOSR":63,"ENOSTR":60,"ENOSYS":38,"ENOTCONN":107,"ENOTDIR":20,"ENOTEMPTY":39,"ENOTSOCK":88,"ENOTSUP":95,"ENOTTY":25,"ENXIO":6,"EOPNOTSUPP":95,"EOVERFLOW":75,"EPERM":1,"EPIPE":32,"EPROTO":71,"EPROTONOSUPPORT":93,"EPROTOTYPE":91,"ERANGE":34,"EROFS":30,"ESPIPE":29,"ESRCH":3,"ESTALE":116,"ETIME":62,"ETIMEDOUT":110,"ETXTBSY":26,"EWOULDBLOCK":11,"EXDEV":18},"signals":{"SIGHUP":1,"SIGINT":2,"SIGQUIT":3,"SIGILL":4,"SIGTRAP":5,"SIGABRT":6,"SIGIOT":6,"SIGBUS":7,"SIGFPE":8,"SIGKILL":9,"SIGUSR1":10,"SIGSEGV":11,"SIGUSR2":12,"SIGPIPE":13,"SIGALRM":14,"SIGTERM":15,"SIGCHLD":17,"SIGSTKFLT":16,"SIGCONT":18,"SIGSTOP":19,"SIGTSTP":20,"SIGTTIN":21,"SIGTTOU":22,"SIGURG":23,"SIGXCPU":24,"SIGXFSZ":25,"SIGVTALRM":26,"SIGPROF":27,"SIGWINCH":28,"SIGIO":29,"SIGPOLL":29,"SIGPWR":30,"SIGSYS":31},"priority":{"PRIORITY_LOW":19,"PRIORITY_BELOW_NORMAL":10,"PRIORITY_NORMAL":0,"PRIORITY_ABOVE_NORMAL":-7,"PRIORITY_HIGH":-14,"PRIORITY_HIGHEST":-20}},"fs":{"UV_FS_SYMLINK_DIR":1,"UV_FS_SYMLINK_JUNCTION":2,"O_RDONLY":0,"O_WRONLY":1,"O_RDWR":2,"UV_DIRENT_UNKNOWN":0,"UV_DIRENT_FILE":1,"UV_DIRENT_DIR":2,"UV_DIRENT_LINK":3,"UV_DIRENT_FIFO":4,"UV_DIRENT_SOCKET":5,"UV_DIRENT_CHAR":6,"UV_DIRENT_BLOCK":7,"S_IFMT":61440,"S_IFREG":32768,"S_IFDIR":16384,"S_IFCHR":8192,"S_IFBLK":24576,"S_IFIFO":4096,"S_IFLNK":40960,"S_IFSOCK":49152,"O_CREAT":64,"O_EXCL":128,"UV_FS_O_FILEMAP":0,"O_NOCTTY":256,"O_TRUNC":512,"O_APPEND":1024,"O_DIRECTORY":65536,"O_NOATIME":262144,"O_NOFOLLOW":131072,"O_SYNC":1052672,"O_DSYNC":4096,"O_DIRECT":16384,"O_NONBLOCK":2048,"S_IRWXU":448,"S_IRUSR":256,"S_IWUSR":128,"S_IXUSR":64,"S_IRWXG":56,"S_IRGRP":32,"S_IWGRP":16,"S_IXGRP":8,"S_IRWXO":7,"S_IROTH":4,"S_IWOTH":2,"S_IXOTH":1,"F_OK":0,"R_OK":4,"W_OK":2,"X_OK":1,"UV_FS_COPYFILE_EXCL":1,"COPYFILE_EXCL":1,"UV_FS_COPYFILE_FICLONE":2,"COPYFILE_FICLONE":2,"UV_FS_COPYFILE_FICLONE_FORCE":4,"COPYFILE_FICLONE_FORCE":4},"crypto":{"OPENSSL_VERSION_NUMBER":810549360,"SSL_OP_ALL":2147485776,"SSL_OP_ALLOW_NO_DHE_KEX":1024,"SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION":262144,"SSL_OP_CIPHER_SERVER_PREFERENCE":4194304,"SSL_OP_CISCO_ANYCONNECT":32768,"SSL_OP_COOKIE_EXCHANGE":8192,"SSL_OP_CRYPTOPRO_TLSEXT_BUG":2147483648,"SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS":2048,"SSL_OP_LEGACY_SERVER_CONNECT":4,"SSL_OP_NO_COMPRESSION":131072,"SSL_OP_NO_ENCRYPT_THEN_MAC":524288,"SSL_OP_NO_QUERY_MTU":4096,"SSL_OP_NO_RENEGOTIATION":1073741824,"SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION":65536,"SSL_OP_NO_SSLv2":0,"SSL_OP_NO_SSLv3":33554432,"SSL_OP_NO_TICKET":16384,"SSL_OP_NO_TLSv1":67108864,"SSL_OP_NO_TLSv1_1":268435456,"SSL_OP_NO_TLSv1_2":134217728,"SSL_OP_NO_TLSv1_3":536870912,"SSL_OP_PRIORITIZE_CHACHA":2097152,"SSL_OP_TLS_ROLLBACK_BUG":8388608,"ENGINE_METHOD_RSA":1,"ENGINE_METHOD_DSA":2,"ENGINE_METHOD_DH":4,"ENGINE_METHOD_RAND":8,"ENGINE_METHOD_EC":2048,"ENGINE_METHOD_CIPHERS":64,"ENGINE_METHOD_DIGESTS":128,"ENGINE_METHOD_PKEY_METHS":512,"ENGINE_METHOD_PKEY_ASN1_METHS":1024,"ENGINE_METHOD_ALL":65535,"ENGINE_METHOD_NONE":0,"DH_CHECK_P_NOT_SAFE_PRIME":2,"DH_CHECK_P_NOT_PRIME":1,"DH_UNABLE_TO_CHECK_GENERATOR":4,"DH_NOT_SUITABLE_GENERATOR":8,"RSA_PKCS1_PADDING":1,"RSA_NO_PADDING":3,"RSA_PKCS1_OAEP_PADDING":4,"RSA_X931_PADDING":5,"RSA_PKCS1_PSS_PADDING":6,"RSA_PSS_SALTLEN_DIGEST":-1,"RSA_PSS_SALTLEN_MAX_SIGN":-2,"RSA_PSS_SALTLEN_AUTO":-2,"defaultCoreCipherList":"TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA256:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA","TLS1_VERSION":769,"TLS1_1_VERSION":770,"TLS1_2_VERSION":771,"TLS1_3_VERSION":772,"POINT_CONVERSION_COMPRESSED":2,"POINT_CONVERSION_UNCOMPRESSED":4,"POINT_CONVERSION_HYBRID":6},"zlib":{"Z_NO_FLUSH":0,"Z_PARTIAL_FLUSH":1,"Z_SYNC_FLUSH":2,"Z_FULL_FLUSH":3,"Z_FINISH":4,"Z_BLOCK":5,"Z_OK":0,"Z_STREAM_END":1,"Z_NEED_DICT":2,"Z_ERRNO":-1,"Z_STREAM_ERROR":-2,"Z_DATA_ERROR":-3,"Z_MEM_ERROR":-4,"Z_BUF_ERROR":-5,"Z_VERSION_ERROR":-6,"Z_NO_COMPRESSION":0,"Z_BEST_SPEED":1,"Z_BEST_COMPRESSION":9,"Z_DEFAULT_COMPRESSION":-1,"Z_FILTERED":1,"Z_HUFFMAN_ONLY":2,"Z_RLE":3,"Z_FIXED":4,"Z_DEFAULT_STRATEGY":0,"ZLIB_VERNUM":4880,"DEFLATE":1,"INFLATE":2,"GZIP":3,"GUNZIP":4,"DEFLATERAW":5,"INFLATERAW":6,"UNZIP":7,"BROTLI_DECODE":8,"BROTLI_ENCODE":9,"ZSTD_DECOMPRESS":11,"ZSTD_COMPRESS":10,"Z_MIN_WINDOWBITS":8,"Z_MAX_WINDOWBITS":15,"Z_DEFAULT_WINDOWBITS":15,"Z_MIN_CHUNK":64,"Z_MAX_CHUNK":null,"Z_DEFAULT_CHUNK":16384,"Z_MIN_MEMLEVEL":1,"Z_MAX_MEMLEVEL":9,"Z_DEFAULT_MEMLEVEL":8,"Z_MIN_LEVEL":-1,"Z_MAX_LEVEL":9,"Z_DEFAULT_LEVEL":-1,"BROTLI_OPERATION_PROCESS":0,"BROTLI_OPERATION_FLUSH":1,"BROTLI_OPERATION_FINISH":2,"BROTLI_OPERATION_EMIT_METADATA":3,"BROTLI_PARAM_MODE":0,"BROTLI_MODE_GENERIC":0,"BROTLI_MODE_TEXT":1,"BROTLI_MODE_FONT":2,"BROTLI_DEFAULT_MODE":0,"BROTLI_PARAM_QUALITY":1,"BROTLI_MIN_QUALITY":0,"BROTLI_MAX_QUALITY":11,"BROTLI_DEFAULT_QUALITY":11,"BROTLI_PARAM_LGWIN":2,"BROTLI_MIN_WINDOW_BITS":10,"BROTLI_MAX_WINDOW_BITS":24,"BROTLI_LARGE_MAX_WINDOW_BITS":30,"BROTLI_DEFAULT_WINDOW":22,"BROTLI_PARAM_LGBLOCK":3,"BROTLI_MIN_INPUT_BLOCK_BITS":16,"BROTLI_MAX_INPUT_BLOCK_BITS":24,"BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING":4,"BROTLI_PARAM_SIZE_HINT":5,"BROTLI_PARAM_LARGE_WINDOW":6,"BROTLI_PARAM_NPOSTFIX":7,"BROTLI_PARAM_NDIRECT":8,"BROTLI_DECODER_RESULT_ERROR":0,"BROTLI_DECODER_RESULT_SUCCESS":1,"BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT":2,"BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT":3,"BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION":0,"BROTLI_DECODER_PARAM_LARGE_WINDOW":1,"BROTLI_DECODER_NO_ERROR":0,"BROTLI_DECODER_SUCCESS":1,"BROTLI_DECODER_NEEDS_MORE_INPUT":2,"BROTLI_DECODER_NEEDS_MORE_OUTPUT":3,"BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE":-1,"BROTLI_DECODER_ERROR_FORMAT_RESERVED":-2,"BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE":-3,"BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET":-4,"BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME":-5,"BROTLI_DECODER_ERROR_FORMAT_CL_SPACE":-6,"BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE":-7,"BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT":-8,"BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1":-9,"BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2":-10,"BROTLI_DECODER_ERROR_FORMAT_TRANSFORM":-11,"BROTLI_DECODER_ERROR_FORMAT_DICTIONARY":-12,"BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS":-13,"BROTLI_DECODER_ERROR_FORMAT_PADDING_1":-14,"BROTLI_DECODER_ERROR_FORMAT_PADDING_2":-15,"BROTLI_DECODER_ERROR_FORMAT_DISTANCE":-16,"BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET":-19,"BROTLI_DECODER_ERROR_INVALID_ARGUMENTS":-20,"BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES":-21,"BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS":-22,"BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP":-25,"BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1":-26,"BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2":-27,"BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES":-30,"BROTLI_DECODER_ERROR_UNREACHABLE":-31,"ZSTD_e_continue":0,"ZSTD_e_flush":1,"ZSTD_e_end":2,"ZSTD_fast":1,"ZSTD_dfast":2,"ZSTD_greedy":3,"ZSTD_lazy":4,"ZSTD_lazy2":5,"ZSTD_btlazy2":6,"ZSTD_btopt":7,"ZSTD_btultra":8,"ZSTD_btultra2":9,"ZSTD_c_compressionLevel":100,"ZSTD_c_windowLog":101,"ZSTD_c_hashLog":102,"ZSTD_c_chainLog":103,"ZSTD_c_searchLog":104,"ZSTD_c_minMatch":105,"ZSTD_c_targetLength":106,"ZSTD_c_strategy":107,"ZSTD_c_enableLongDistanceMatching":160,"ZSTD_c_ldmHashLog":161,"ZSTD_c_ldmMinMatch":162,"ZSTD_c_ldmBucketSizeLog":163,"ZSTD_c_ldmHashRateLog":164,"ZSTD_c_contentSizeFlag":200,"ZSTD_c_checksumFlag":201,"ZSTD_c_dictIDFlag":202,"ZSTD_c_nbWorkers":400,"ZSTD_c_jobSize":401,"ZSTD_c_overlapLog":402,"ZSTD_d_windowLogMax":100,"ZSTD_CLEVEL_DEFAULT":3,"ZSTD_error_no_error":0,"ZSTD_error_GENERIC":1,"ZSTD_error_prefix_unknown":10,"ZSTD_error_version_unsupported":12,"ZSTD_error_frameParameter_unsupported":14,"ZSTD_error_frameParameter_windowTooLarge":16,"ZSTD_error_corruption_detected":20,"ZSTD_error_checksum_wrong":22,"ZSTD_error_literals_headerWrong":24,"ZSTD_error_dictionary_corrupted":30,"ZSTD_error_dictionary_wrong":32,"ZSTD_error_dictionaryCreation_failed":34,"ZSTD_error_parameter_unsupported":40,"ZSTD_error_parameter_combination_unsupported":41,"ZSTD_error_parameter_outOfBound":42,"ZSTD_error_tableLog_tooLarge":44,"ZSTD_error_maxSymbolValue_tooLarge":46,"ZSTD_error_maxSymbolValue_tooSmall":48,"ZSTD_error_stabilityCondition_notRespected":50,"ZSTD_error_stage_wrong":60,"ZSTD_error_init_missing":62,"ZSTD_error_memory_allocation":64,"ZSTD_error_workSpace_tooSmall":66,"ZSTD_error_dstSize_tooSmall":70,"ZSTD_error_srcSize_wrong":72,"ZSTD_error_dstBuffer_null":74,"ZSTD_error_noForwardProgress_destFull":80,"ZSTD_error_noForwardProgress_inputEmpty":82},"trace":{"TRACE_EVENT_PHASE_BEGIN":66,"TRACE_EVENT_PHASE_END":69,"TRACE_EVENT_PHASE_COMPLETE":88,"TRACE_EVENT_PHASE_INSTANT":73,"TRACE_EVENT_PHASE_ASYNC_BEGIN":83,"TRACE_EVENT_PHASE_ASYNC_STEP_INTO":84,"TRACE_EVENT_PHASE_ASYNC_STEP_PAST":112,"TRACE_EVENT_PHASE_ASYNC_END":70,"TRACE_EVENT_PHASE_NESTABLE_ASYNC_BEGIN":98,"TRACE_EVENT_PHASE_NESTABLE_ASYNC_END":101,"TRACE_EVENT_PHASE_NESTABLE_ASYNC_INSTANT":110,"TRACE_EVENT_PHASE_FLOW_BEGIN":115,"TRACE_EVENT_PHASE_FLOW_STEP":116,"TRACE_EVENT_PHASE_FLOW_END":102,"TRACE_EVENT_PHASE_METADATA":77,"TRACE_EVENT_PHASE_COUNTER":67,"TRACE_EVENT_PHASE_SAMPLE":80,"TRACE_EVENT_PHASE_CREATE_OBJECT":78,"TRACE_EVENT_PHASE_SNAPSHOT_OBJECT":79,"TRACE_EVENT_PHASE_DELETE_OBJECT":68,"TRACE_EVENT_PHASE_MEMORY_DUMP":118,"TRACE_EVENT_PHASE_MARK":82,"TRACE_EVENT_PHASE_CLOCK_SYNC":99,"TRACE_EVENT_PHASE_ENTER_CONTEXT":40,"TRACE_EVENT_PHASE_LEAVE_CONTEXT":41,"TRACE_EVENT_PHASE_LINK_IDS":61},"internal":{"EXTENSIONLESS_FORMAT_JAVASCRIPT":0,"EXTENSIONLESS_FORMAT_WASM":1}} \ No newline at end of file diff --git a/crates/v8-runtime/tests/node_stdlib_poc/preflight.mjs b/crates/v8-runtime/tests/node_stdlib_poc/preflight.mjs new file mode 100644 index 0000000000..92e38a7b71 --- /dev/null +++ b/crates/v8-runtime/tests/node_stdlib_poc/preflight.mjs @@ -0,0 +1,50 @@ +// Fast local pre-flight for the POC bootstrap: runs bootstrap.js + checks.js in +// a bare `vm` context (no node globals) to approximate the agentOS isolate. +// Not part of the Rust test — dev iteration aid only. Run: +// node crates/v8-runtime/tests/node_stdlib_poc/preflight.mjs +import { readFileSync, readdirSync, statSync } from 'node:fs'; +import { join, relative } from 'node:path'; +import vm from 'node:vm'; + +const NODE_SRC = process.env.NODE_SRC_DIR ?? '/home/nathan/misc/node'; +const HERE = new URL('.', import.meta.url).pathname; + +const sources = {}; +function walk(dir) { + for (const name of readdirSync(dir)) { + const p = join(dir, name); + if (statSync(p).isDirectory()) walk(p); + else if (name.endsWith('.js')) { + const id = relative(join(NODE_SRC, 'lib'), p).replace(/\.js$/, ''); + sources[id] = readFileSync(p, 'utf8'); + } + } +} +walk(join(NODE_SRC, 'lib')); + +const constants = JSON.parse(readFileSync(join(HERE, 'constants.json'), 'utf8')); +const bootstrap = readFileSync(join(HERE, 'bootstrap.js'), 'utf8'); +const checks = readFileSync(join(HERE, 'checks.js'), 'utf8'); + +const ctx = vm.createContext(Object.create(null)); +ctx.globalThis = ctx; +vm.runInContext('globalThis.__nodeSources = ' + JSON.stringify(sources), ctx); +vm.runInContext('globalThis.__nodeConstants = ' + JSON.stringify(constants), ctx); +// Stage 2.5 (optional): simdutf wasm; absent → JS codec fallback. +const wasmPath = process.env.AGENTOS_SIMDUTF_POC_WASM ?? join(HERE, 'simdutf/build/simdutf-poc.wasm'); +try { + const b64 = readFileSync(wasmPath).toString('base64'); + vm.runInContext(`globalThis.__pocSimdutfWasmBase64 = ${JSON.stringify(b64)}`, ctx); +} catch { + console.error(`note: no simdutf wasm at ${wasmPath}; JS codec fallback`); +} +try { + vm.runInContext(bootstrap, ctx, { filename: 'bootstrap.js' }); + vm.runInContext(checks, ctx, { filename: 'checks.js' }); + // Stage 3: wait for the async checks (fs callbacks/promises/streams). + await vm.runInContext('globalThis.__pocAsync', ctx); + console.log(vm.runInContext('globalThis.__pocResult', ctx)); +} catch (err) { + console.error('PREFLIGHT FAILED:', err.stack ?? err); + process.exit(1); +} diff --git a/crates/v8-runtime/tests/node_stdlib_poc/simdutf/.gitignore b/crates/v8-runtime/tests/node_stdlib_poc/simdutf/.gitignore new file mode 100644 index 0000000000..567609b123 --- /dev/null +++ b/crates/v8-runtime/tests/node_stdlib_poc/simdutf/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/crates/v8-runtime/tests/node_stdlib_poc/simdutf/build-simdutf-poc.sh b/crates/v8-runtime/tests/node_stdlib_poc/simdutf/build-simdutf-poc.sh new file mode 100755 index 0000000000..3f32b8ce65 --- /dev/null +++ b/crates/v8-runtime/tests/node_stdlib_poc/simdutf/build-simdutf-poc.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# Build the stage-2.5 POC wasm: upstream simdutf compiled to wasm32-wasip1 +# against the agentOS patched sysroot, exported behind a flat C ABI +# (wrapper.cpp) as a wasm reactor for instantiation inside the V8 isolate. +# +# The C toolchain (wasi-sdk + patched sysroot) lives on the reg-tests branch, +# not on main, so its location is taken as input: +# AGENTOS_C_TOOLCHAIN root containing vendor/wasi-sdk and sysroot/ +# (default: the reg-tests workspace checkout) +# +# Output: build/simdutf-poc.wasm next to this script. The cargo tests pick it +# up automatically (or via AGENTOS_SIMDUTF_POC_WASM) and skip the wasm-backed +# assertions when it is absent. +set -euo pipefail + +SIMDUTF_VERSION="v9.0.0" +SIMDUTF_URL="https://github.com/simdutf/simdutf/releases/download/${SIMDUTF_VERSION}/singleheader.zip" +SIMDUTF_SHA256="c47c68cd51025ec66509bc36215b4c4f1f0f0a98129139ee55c541531b652526" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BUILD_DIR="$SCRIPT_DIR/build" +TOOLCHAIN="${AGENTOS_C_TOOLCHAIN:-/home/nathan/.herdr/workspaces/agent-os/reg-tests/toolchain/c}" +CLANGXX="$TOOLCHAIN/vendor/wasi-sdk/bin/clang++" +SYSROOT="$TOOLCHAIN/sysroot" + +if [[ ! -x "$CLANGXX" ]]; then + echo "error: wasi-sdk clang++ not found at $CLANGXX (set AGENTOS_C_TOOLCHAIN)" >&2 + exit 1 +fi +if [[ ! -f "$SYSROOT/lib/wasm32-wasi/libc++.a" ]]; then + echo "error: patched sysroot with libc++ not found at $SYSROOT" >&2 + exit 1 +fi + +mkdir -p "$BUILD_DIR" +cd "$BUILD_DIR" + +if [[ ! -f simdutf.cpp || ! -f simdutf.h ]]; then + echo "fetching simdutf $SIMDUTF_VERSION singleheader" + curl -fsSL -o singleheader.zip "$SIMDUTF_URL" + echo "$SIMDUTF_SHA256 singleheader.zip" | sha256sum -c - + unzip -o -q singleheader.zip simdutf.cpp simdutf.h +fi + +echo "compiling simdutf-poc.wasm (simdutf $SIMDUTF_VERSION, sysroot $SYSROOT)" +"$CLANGXX" \ + --target=wasm32-wasip1 \ + --sysroot="$SYSROOT" \ + -isystem "$SYSROOT/include/wasm32-wasi/c++/v1" \ + -isystem "$SYSROOT/include/wasm32-wasi" \ + -I "$BUILD_DIR" \ + -D_WASI_EMULATED_PTHREAD \ + -O2 -fno-exceptions \ + -mexec-model=reactor \ + -lwasi-emulated-pthread \ + -Wl,--export=poc_alloc \ + -Wl,--export=poc_free \ + -Wl,--export=poc_is_utf8 \ + -Wl,--export=poc_is_ascii \ + -Wl,--export=poc_utf8_len_from_utf16le \ + -o simdutf-poc.wasm \ + "$SCRIPT_DIR/wrapper.cpp" simdutf.cpp + +WASM_OPT="$TOOLCHAIN/vendor/wasi-sdk/bin/wasm-opt" +if [[ -x "$WASM_OPT" ]]; then + "$WASM_OPT" -O3 --strip-debug simdutf-poc.wasm -o simdutf-poc.wasm +fi + +ls -la simdutf-poc.wasm diff --git a/crates/v8-runtime/tests/node_stdlib_poc/simdutf/wrapper.cpp b/crates/v8-runtime/tests/node_stdlib_poc/simdutf/wrapper.cpp new file mode 100644 index 0000000000..db64117df3 --- /dev/null +++ b/crates/v8-runtime/tests/node_stdlib_poc/simdutf/wrapper.cpp @@ -0,0 +1,36 @@ +// POC wrapper exporting a flat C ABI over simdutf for the node-stdlib POC. +// Compiled to a wasm32-wasip1 reactor and instantiated inside the V8 isolate. +#include +#include + +#include "simdutf.h" + +extern "C" { + +uint8_t* poc_alloc(size_t len) { + return static_cast(malloc(len)); +} + +void poc_free(uint8_t* ptr) { + free(ptr); +} + +int32_t poc_is_utf8(const uint8_t* buf, size_t len) { + return simdutf::validate_utf8(reinterpret_cast(buf), len) ? 1 + : 0; +} + +int32_t poc_is_ascii(const uint8_t* buf, size_t len) { + return simdutf::validate_ascii(reinterpret_cast(buf), len) ? 1 + : 0; +} + +// Input is UTF-16LE code units; caller guarantees well-formed UTF-16 +// (the JS side falls back to its own implementation for lone surrogates, +// matching node's replacement-character semantics). +double poc_utf8_len_from_utf16le(const uint8_t* buf, size_t u16len) { + return static_cast(simdutf::utf8_length_from_utf16le( + reinterpret_cast(buf), u16len)); +} + +} // extern "C" diff --git a/docs-internal/node-stdlib-replacement-spec.md b/docs-internal/node-stdlib-replacement-spec.md index a2ff59776f..8a2b2c498d 100644 --- a/docs-internal/node-stdlib-replacement-spec.md +++ b/docs-internal/node-stdlib-replacement-spec.md @@ -1,9 +1,9 @@ # Spec: Real Node.js stdlib in agentOS (delete the reimplementation) -Status: DRAFT v3 (post-review + decisions) · Owner: runtime team · Date: 2026-07-09 (PST) +Status: READY FOR IMPLEMENTATION v4 (user decisions resolved; M0 evidence gates) · Owner: runtime team · Date: 2026-07-09 (PST) POC evidence: jj workspace `node-stdlib`, change `wyqxqylo` (`c5888247`); artifacts in `~/progress/agent-os/2026-07-09-node-stdlib-wasm-poc/`. -Review: 4-lens review integrated (dispositions in Appendix A); v3 applies the +Review: 4-lens review integrated (dispositions in Appendix A); v4 applies the 2026-07-09 user decisions (§0). --- @@ -20,22 +20,49 @@ Review: 4-lens review integrated (dispositions in Appendix A); v3 applies the by the same pipeline against the in-repo patched libc (§3.2, §9.4). sha256-pinning of all toolchain inputs stays a hard requirement; prebuilt sysroot artifacts survive only as a CI-cache implementation detail. -2. **Crypto — OpenSSL-family compiled to wasm with our sysroot, in-isolate** - (resolves old OQ 2; deletes old OQ 8 — `process.versions.openssl` - becomes real). Not host-side RustCrypto. §7.4 reworked; the RustCrypto - `_crypto*` bridge surface moves to deletion scope. Pending material - sub-choice: **OpenSSL 3.x vs BoringSSL — recommendation OpenSSL 3.x** - (node's ncrypto wraps the OpenSSL 3.x API; BoringSSL diverges); - proceeding with OpenSSL 3.x absent objection. +2. **Crypto + TLS — one real OpenSSL 3.5.x wasm build, shared in-guest** + (resolves old OQ 2 and old OQ 8). Build the exact OpenSSL source bundled + by the pinned Node 24 tag once against the in-repo sysroot, owned by + `toolchain/c` rather than the Node crate; Node + `crypto`/`tls` and the registry C tools link/use that same wasm + `libcrypto`/`libssl` build and the same VM CA bundle. TLS records flow over + kernel-owned TCP; there is no host TLS termination. The current registry + networking stack's mbedTLS build is proven migration input, not a second + final backend: mbedTLS 3.6 has no OpenSSL 3 compatibility surface capable + of satisfying node's ncrypto/EVP/provider contract. BoringSSL is rejected + for the same API/behavior divergence. §7.4 defines the shared-build and + cross-runtime acceptance gates; the RustCrypto `_crypto*` bridge and host + `rustls-native-certs` paths move to deletion scope. 3. **execPath/self-spawn — real guest `node` command** (resolves old OQ 7). Governing principle, elevated to north star (§1): the guest is *"targeting native linux compatibility and it can't tell it's a different executor."* Current functionality is preserved during migration. -4. **Browser runtime — committed browser profile now** (resolves old OQ 5). - Every HOST binding gets a browser fallback or documented typed - degradation; browser and native migrate together; browser stays in the - deletion gate (§12.3). Acknowledged scope growth (§14). +4. **Browser runtime — disabled and out of scope** (final user decision, + superseding the earlier defer-and-document answer). Keep the browser + runtime source, but remove it from CI, release, and publish build/test + matrices before native cutover work begins. This program does not migrate, + delete, keep buildable, or produce a design doc for the browser runtime; + it cannot block CI or release. Re-enabling it is a separate program. +5. **Node version — Node 24.15.0 LTS.** Pin tag `v24.15.0`, commit + `848430679556aed0bd073f2bc263331ad84fa119`, in the vendor manifest. The + v26-nightly POC is evidence for the seam, not the shipped binding + inventory; M0 reconciles the inventory against this v24 tag. +6. **Workers — unsupported at cutover.** The worker binding is inert data; + `new Worker` throws the typed unsupported error. Workers do not gate + cutover. +7. **Migration parity — measured legacy parity, including performance.** M0 + captures the current legacy implementation with the same suite and bench + harnesses. The real path may be incomplete during staged implementation, + but the default flip and deletion gate require at least the legacy + supported-test set and no unapproved >10% performance regression; new + native-node passes ratchet upward. Guessed runnable percentages are not + acceptance criteria. +8. **Sequencing — dependency-driven, not a user product choice.** Keep the + fs → event loop → net/http → crypto/TLS/child-process order unless measured + implementation evidence requires a change. HTTP depends on the net/event + loop substrate; moving it earlier would only trade schedule against fs + completion, not change the end state. --- @@ -55,8 +82,9 @@ native Linux; anything the guest could observe to distinguish agentOS from a real Linux node process is a defect. **Non-goals (this program):** -- `worker_threads` with real parallelism (§10.4; inert binding, `Worker` - constructor throws typed error — see §4 STUB policy). +- `worker_threads` with real parallelism (§10.4; decided unsupported at + cutover: inert binding, `Worker` constructor throws typed error — see §4 + STUB policy). - Inspector/profiler protocol support (`inspector`, `profiler` stay inert stubs; `config.hasInspector` pinned `false`). - Compiling node's C++ core or libuv to wasm. Ruled out by feasibility study: @@ -90,13 +118,13 @@ a real Linux node process is a defect. │ node lib/**/*.js (vendored verbatim, loaded by node's realm.js) │ │ ── internalBinding(name) + process object ── the seam ────────────────────│ │ binding shims (JS, small, mirror node's C++ binding contract) │ -│ ├─ pure-compute → wasm leaf libs (simdutf/ada/llhttp/zlib…) built with │ +│ ├─ pure-compute → wasm leaf libs (simdutf/ada/llhttp/zlib/OpenSSL…) │ │ │ our patched sysroot, instantiated in-isolate (V8 wasm) │ │ └─ I/O → existing bridge globals (bridge-contract.json) ──sync-RPC──┐ │ └───────────────────────────────────────────────────────────────────────┼────┘ ▼ sidecar (trusted): kernel, fd table, VFS, - sockets, processes, crypto, permissions + sockets, processes, CSPRNG, permissions ``` Principles: @@ -132,18 +160,18 @@ Principles: ## 3. Vendoring, bootstrap, and snapshot -### 3.1 Node version -- **Pin the latest v26.x stable release tag** (exact tag chosen at vendoring - time; the POC ran a v26 nightly, `fbf82766d62`). Rationale: the POC - validated v26's binding surface (which differs from v24 — e.g. static-style - buffer codecs, `encoding_binding`); v26 enters LTS Oct 2026; starting on - v24 LTS pays a major-version binding migration mid-program. **[OQ 1]** +### 3.1 Node version (DECIDED — Decision 5) +- **Pin Node 24.15.0 LTS:** tag `v24.15.0`, peeled commit + `848430679556aed0bd073f2bc263331ad84fa119`. The POC ran a v26 nightly, + `fbf82766d62`; it validates the architecture, not the shipped ABI. Node 24 + is the user-selected stable LTS line. This tag bundles OpenSSL 3.5.5; the + vendor manifest records both source identities. - The internalBinding ABI is unstable across versions. Accepted by the user (we control deployment). Requirements: pinned tag + sha in the vendor manifest, surfaced in docs; upgrades are deliberate PRs gated by the full conformance suite (§10). - **M0 acceptance item:** re-run the `internalBinding(...)` grep diff between - the POC nightly and the pinned tag and reconcile the §4 table (the nightly + the v26 POC nightly and the pinned v24 tag and reconcile the §4 table (the nightly inventory already drifts — `permission` was missed on first pass). ### 3.2 Vendoring mechanics — how we get node's source @@ -168,9 +196,13 @@ patch-the-layer-that-owns-it policy). - `vendor/native/` — **leaf-lib C/C++ sources vendored from the same pinned node tag** where node vendors them: `deps/llhttp` (the *generated C*, not upstream TS), `deps/ada` (amalgamation), `deps/nbytes`; simdutf - from its own pinned release (v26 no longer vendors it); zlib/brotli/zstd + from the dependency version selected by the pinned Node tag (or its own + pinned upstream release if that tag does not vendor it); zlib/brotli/zstd from pinned upstream releases. Version skew between leaf libs and the JS - that drives them is eliminated by construction. + that drives them is eliminated by construction. **OpenSSL is deliberately + not owned here** because it is shared with registry software; its source + identity and build live under `toolchain/c` (§9.4), and this crate records + that shared manifest hash. - `bindings/` (shim JS, one file per binding), `src/` (Rust: source map, snapshot creator, HOST binding functions). - `scripts/vendor-node.mjs` copies from a node checkout at the pinned tag, @@ -187,6 +219,9 @@ patch-the-layer-that-owns-it policy). (§9.4); docker sidecar builds and darwin/linux release legs consume the same job's outputs, so bytes are identical everywhere (required if ever snapshot-adjacent). +- **Ownership boundary:** `crates/node-stdlib/wasm/crypto/` owns only the + Node-facing C/C++ adapter ABI and JS shim. It must not contain a private + OpenSSL source copy, patch set, download, or prebuilt archive. - Builtin IDs mirror node's js2c mapping (`lib/foo.js` → `foo`, `deps/undici/undici.js` → `internal/deps/undici/undici`) so `realm.js` works unchanged. @@ -216,14 +251,15 @@ Spec: Consequence: moving a binding between JS and HOST after M0 is a snapshot-format change — plan the split in M0, not opportunistically. - **The silent degrade path is removed** for the real stdlib: snapshot - creation failure either falls back to a *defined* full `realm.js` - in-context bootstrap (same semantics, slower, logged with reason) or fails - the Execute with a typed error — never a silently different environment. + creation failure falls back to a *defined* full `realm.js` in-context + bootstrap (same semantics, slower, logged with reason). Execute fails with + a typed error only if that fallback also fails; it never enters a silently + different environment. - Non-snapshotted builtins compile lazily from the in-snapshot source map; per-builtin V8 code-cache blobs are a follow-up if the coldstart bench (§11.2's lazy-compile metrics) shows need. -## 4. Binding plan (69 bindings referenced by v26 `lib/` + process object) +## 4. Binding plan (v26-POC inventory; exact v24 count reconciled at M0) Strategies: **JS** (pure JS shim) · **WASM** (JS shim calling in-isolate wasm leaf lib) · **BRIDGE** (kernel/sidecar via bridge globals) · **HOST** (Rust in @@ -236,7 +272,7 @@ and inside `test/common`), so a throwing stub bricks bootstrap and the entire test plan. Pattern: plausible inert values (`isMainThread: true`, `threadId: 0`, `ownsProcessState: true`, …) with only the *action* entry points (`new Worker`, `inspector.open`, …) throwing -`ERR_AGENTOS_UNSUPPORTED(, )`. (Worker stance: [OQ 3].) +`ERR_AGENTOS_UNSUPPORTED(, )`. Workers follow Decision 6. **M0 gate: every binding below loads inert** — `require()` of every public module must succeed at M0 with bindings in at-least-inert form, because @@ -269,11 +305,11 @@ Milestones then make families *behave*, not *exist*. | async_wrap, async_context_frame | HOST | **AsyncLocalStorage requires V8 continuation-preserved embedder data** (`async_context_frame.js:8-11`, `src/async_context_frame.cc:38`) → verify our V8 build has `v8_enable_continuation_preserved_embedder_data` (M0 task); fallback path needs `setPromiseHooks` (`v8::Context::SetPromiseHooks`) — also HOST. `bootstrap/node.js:227` calls `async_wrap.setupHooks` unconditionally. Every callback-dispatching shim follows the MakeCallback discipline (§6.4) | | stream_wrap, tcp_wrap, pipe_wrap, js_stream, stream_pipe | BRIDGE | §7.3 — StreamBase contract incl. sync/async duality | | udp_wrap, cares_wrap | BRIDGE | `_dgram*`, `_networkDns*`; `getaddrinfo(3)`; c-ares behavior as contract | -| tls_wrap | WASM or BRIDGE | reframed by Decision 2 — in-guest OpenSSL TLS over kernel TCP (recommended) vs kernel-owned TLS upgrade; §7.4, RFC 8446 cited **[OQ 2]** | +| tls_wrap | WASM + BRIDGE | **Decision 2:** node's TLS contract over the shared in-guest OpenSSL wasm build; encrypted records use kernel-owned TCP via the socket bridge. RFC 8446 + node `crypto_tls.*` are the implementation authorities; §7.4 | | http_parser | WASM | llhttp **generated C from the pinned node tag** (`deps/llhttp/src`); node `http_parser` binding contract | -| http2 | BRIDGE (M4) | nghttp2-to-wasm vs host decided when reached; `_networkHttp2*` fallback | +| http2 | WASM + BRIDGE (M4) | compile the pinned node tag's nghttp2 to wasm and keep socket I/O in the kernel bridge; a host implementation is allowed only after a measured, documented nghttp2-wasm blocker | | zlib | WASM | node needs BrotliEncoder + zstd compress (`node_zlib.cc:155,256`; `lib/zlib.js:89-92`) — **new build rules required**: toolchain currently builds brotli *decoder*-only and zstd *decompress*-only (built for curl). Add `c/enc` + `lib/compress` objects (zstd keeps `ZSTD_MULTITHREAD` undefined). Not "already built" | -| crypto | WASM | **Decision 2:** OpenSSL-family compiled with our sysroot, in-isolate (edgejs precedent). Binding surface is ~174 properties destructured at module scope — inventory is the checklist. M4 gate: crypto suite categories green with a real denominator (`versions.openssl` is real). §7.4 | +| crypto | WASM | **Decision 2:** the pinned Node 24 tag's real OpenSSL 3.5.x, built once with our sysroot and shared with registry C tools. Binding surface is inventoried from the pinned v24 tag (the v26 POC saw ~174 properties) and is the checklist. M4 gate: crypto suite categories green with a real denominator (`versions.openssl` is real). §7.4 | | sqlite | BRIDGE | existing `_sqlite*` globals | | spawn_sync, process_wrap, signal_wrap | BRIDGE | `_childProcessSpawn*`, `waitpid(2)`, `signal(7)`; self-spawn via the real guest `node` command (Decision 3, §10.5) | | tty_wrap | BRIDGE | `_kernelIsattyRaw`, pty; `tty(4)`, `termios(3)` | @@ -283,9 +319,9 @@ Milestones then make families *behave*, not *exist*. | v8, heap_utils, serdes, internal_only_v8 | HOST | heap stats, Serializer/Deserializer via rusty_v8 | | wasi, wasm_web_api | JS/HOST | expose node's `WASI` class over our existing in-isolate runner where sensible | | diagnostics_channel, trace_events | JS | pure JS / validating no-op sink | -| locks, webstorage | BRIDGE/JS | Web Locks in-JS per spec; localStorage optional | +| locks, webstorage | JS / inert | Web Locks in-JS per spec. Web storage preserves current legacy behavior at cutover; any native-Node expansion beyond that baseline is post-cutover and cannot be advertised until its differential tests pass | | block_list | JS | `node_sockaddr` semantics | -| worker | JS (inert) + STUB actions | inert data (`isMainThread:true`, `threadId:0`, `ownsProcessState:true`, …); only `new Worker` throws typed. Required for bootstrap (`is_main_thread.js:311`, `pre_execution.js:141,778`) and for `test/common` to load **[OQ 3]** | +| worker | JS (inert) + STUB actions | **Decision 6:** inert data (`isMainThread:true`, `threadId:0`, `ownsProcessState:true`, …); only `new Worker` throws typed. Required for bootstrap (`is_main_thread.js:311`, `pre_execution.js:141,778`) and for `test/common` to load | | inspector, profiler, report, watchdog, sea, quic | STUB (inert per policy) | quic revisit post-M4 | | mksnapshot | JS | `startup_snapshot` runtime path (POC-proven); real mode used by our snapshot creator | @@ -412,9 +448,12 @@ Spec: io-completions, watch descriptors + queued events (§7.2), contextify contexts, wasm module cache entries — each in `VmLimits` + `limits-inventory.json` with typed errors. -7. Fidelity budget: exact phase parity required only where node's suite or - real packages observe it; deviations enter the parity ledger (§10.3) - with a linked upstream test. +7. Fidelity budget: during migration, every known phase deviation enters the + parity ledger (§10.3) with a linked upstream test. At cutover, a deviation + may remain only when it is an explicit program non-goal (such as Workers) + or preserves the measured legacy baseline while a linked post-cutover + issue tracks the native-parity gap. The ledger cannot silently redefine + observable Linux behavior as conformant. ## 7. I/O surfaces @@ -470,20 +509,30 @@ Spec: kernel pipes; stdio inheritance and exit/signal codes per `waitpid(2)`; self-spawn story in §10.5. -### 7.4 crypto / zlib (crypto DECIDED — Decision 2) -- **crypto: a real OpenSSL-family library compiled with our sysroot, - running in-isolate**, backing `internalBinding('crypto')` — the same - architecture as every other leaf lib, at larger scale. Build precedent - exists end-to-end: edgejs compiles the wasix-org OpenSSL fork with - documented flags (`~/misc/edgejs/wasix/build-wasix.sh`: `./Configure - linux-generic32`, `_WASI_EMULATED_*`, wasm exceptions); our own toolchain - already builds mbedTLS the same static-lib way. -- **OpenSSL vs BoringSSL — recommendation: OpenSSL 3.x** (pending user - sign-off, Decision 2 note; proceeding with OpenSSL absent objection). - Node's `ncrypto` layer wraps the OpenSSL 3.x EVP/provider API and node's - error strings/`versions.openssl` are OpenSSL-shaped; BoringSSL diverges - (EVP subset, no provider model) and would reintroduce exactly the - surface-chasing this decision eliminates. +### 7.4 crypto / TLS / zlib (DECIDED — Decision 2) +- **One real OpenSSL 3.5.x build:** use the exact OpenSSL source bundled by + the pinned Node 24 tag through the shared `toolchain/c` manifest, compile it + once with the in-repo sysroot, and stage + one content-addressed set of wasm32 static archives (`libcrypto.a` and + `libssl.a`). Node's + `internalBinding('crypto')` and `tls_wrap` adapters use it in-isolate; curl, + wget, git/libcurl, and full-crypto ssh use the same build in their in-guest + wasm processes. All consumers use the VM's + `/etc/ssl/certs/ca-certificates.crt` trust root. +- **Why this supersedes the WIP networking-handoff recommendation:** the + current registry stack proves in-guest TLS, getentropy, CA-bundle, and + socket plumbing with mbedTLS 3.6, but mbedTLS does not expose an OpenSSL 3 + compatibility API. Building such a façade would be a new crypto-abstraction + project and still would not reproduce node's EVP/provider/error behavior. + BoringSSL likewise diverges. Real OpenSSL is therefore the one shared + backend; port gaps are fixed in the sysroot or OpenSSL build patches under + the repo's one-layer-down policy, not with per-tool TLS adapters. +- **Build spike:** start in M0 and gate M1 on compiling `libcrypto` and + `libssl`, loading providers without `dlopen`, seeding from `getentropy`, and + completing an in-guest TLS handshake. If a concrete blocker remains after + sysroot and OpenSSL patches are exhausted, record the failed surface and + evidence as a refuted thesis and return for an architecture decision; do + not silently introduce another backend. - **Entropy:** OpenSSL RAND seeds via `getentropy(2)` → host import → kernel CSPRNG (precedent: the mbedTLS getentropy shim in `toolchain/c`). Seeding must never silently fall back to weak entropy: the getentropy @@ -493,7 +542,7 @@ Spec: deleted — `common.hasCrypto` suite gating works naturally; the M4 crypto acceptance has an honest denominator by construction). The crypto shim marshals through the same wasm import-object contract as other leaf libs - (§9.4); streaming EVP contexts live in wasm memory keyed by handle, + (§9.4); streaming EVP/SSL contexts live in wasm memory keyed by handle, zeroized on free. The RustCrypto-backed `_crypto*` bridge globals + their sidecar handlers move to the **deletion inventory** (§12.1), protocol-lockstep removal at M5. @@ -501,20 +550,19 @@ Spec: crypto-blob budget; instantiation is lazy on first `require('crypto')` (post-restore, §3.3); measured by the §11.2 wasm lifecycle bench. - webcrypto rides the same OpenSSL-wasm backend. -- **TLS ([OQ 2] — reframed by Decision 2):** with OpenSSL in-guest there - are two shapes. (a) **In-guest TLS (recommended):** node's real - `tls_wrap`/`SecureContext` semantics run against real OpenSSL SSL objects - in wasm; TLS records flow over kernel-owned TCP. Highest fidelity — - node's TLS surface (SecureContext options, session reuse, ALPN, - renegotiation, cert chains) is OpenSSL-coupled and transfers wholesale. - (b) Kernel-owned TLS upgrade (today's `_netSocketUpgradeTlsRaw`): smaller - guest, but every TLS API knob must be re-marshalled and will leak - deviations. **Security model is unchanged either way**: the kernel owns - sockets and connect/egress policy in both shapes; in (a) it sees - ciphertext exactly as the Linux kernel does for a native node process, - and in-guest keys are the guest's own credentials (same trust position - as native). Client-chosen dangerous TLS config remains out of scope per - the trust model. +- **TLS is in-guest:** node's `tls_wrap`/`SecureContext` contract runs against + real OpenSSL SSL objects in wasm; TLS records flow over kernel-owned TCP. + The kernel retains socket and egress policy and sees ciphertext exactly as + a Linux kernel does for native node. The host-owned + `_netSocketUpgradeTlsRaw`/`rustls-native-certs` path is deleted at cutover. + Client-chosen TLS credentials and options retain the same trust position as + native Node. +- **Shared-backend acceptance:** build/link manifests prove every Node and C + consumer links the same archive/source hashes (final wasm modules may each + embed their linked copy); a cross-runtime e2e runs curl + and Node `https.get`/`tls.connect` against the same trusted and self-signed + servers, checks the same success/failure class, and verifies `--cacert` and + `NODE_EXTRA_CA_CERTS`; no host trust store or per-tool TLS adapter remains. - **zlib: wasm** with the §4 build-rule additions (brotli encoder, zstd compress). Node `node_zlib.cc` contract (streaming, flush modes, dictionaries). Deletes the bridge zlib payload (`v8-bridge-zlib.js`). @@ -553,6 +601,11 @@ Numbers below are **provisional targets**; M0 replaces each with (`sync-bridge-floor.bench.ts` exists today; native-node codec bench added in M0). If a floor already exceeds a target, the budget is renegotiated in the regression ledger — budgets are not aspirations detached from instruments. +- **Primary migration-parity gate (Decision 7):** at default flip and M5, + every applicable p50 metric is no more than 10% slower than the same-day, + same-machine legacy path. p99 and dispersion are published and any material + tail regression requires a ledger entry. Native-node comparisons remain + optimization targets; they do not excuse a regression from what ships now. - Sync binding RTT: target p50 ≤ max(10µs, 1.2× measured no-op floor); p99 ≤ 5× p50. Metric: **p50/p99 of ≥1000 iterations, ≥5 runs, IQR dispersion reported**. @@ -616,7 +669,16 @@ and the "behave like Linux" north star. They form a **parallel workstream - Ground truth: today nothing on `main` builds a C sysroot — `toolchain/` exists only on the reg-tests branch; the publish "registry WASM commands" job is cargo-only (`publish.yaml:53-78`); sysroot bootstrap = wasi-sdk - download + wasi-libc clone/patch + LLVM-runtimes rebuild (multi-hour). + download + wasi-libc clone/patch + LLVM-runtimes rebuild. **MEASURED + (2026-07-09, 20-core linux, fresh dir, cold caches): 87s total** — + wasi-sdk download+extract 15s, wasi-libc clone 2s, llvm-project tarball + fetch 50s (network-bound), patch + libc build + libc++/libc++abi/libunwind + build ~20s. Leaf-lib (simdutf) build against the fresh sysroot: 2s. The + fresh libc.a verified to contain our host-import surface + (`__host_net_socket`, `__host_proc_spawn`, …). Earlier "multi-hour" + characterizations were wrong. Expect low single-digit minutes on 2–4 core + CI runners (compute step is the only part that scales with cores; the + ~65s of network fetches dominate and don't). - **Decision (user): the toolchain lives in the monorepo and the stdlib wasm components are built by the same pipeline against the same in-repo patched libc.** Landing plan (M0): move `toolchain/` (`c/`, @@ -624,15 +686,42 @@ and the "behave like Linux" north star. They form a **parallel workstream `crates/node-stdlib/wasm/` build rules join `toolchain/c/Makefile`; the release workflow gains a leaf-lib build job ordered before the sidecar builds (staged like pyodide assets). -- **CI caching strategy (the multi-hour cold path):** cache key = content - hash of (wasi-sdk pin, llvm-project pin, `std-patches/**`, - `wasi-libc-overrides/**`, build scripts). Cache hit restores the built - sysroot + LLVM runtimes (minutes); the cold rebuild runs only when those - inputs change, on a scheduled/manual job — never on the PR critical - path. Content-addressed, sha256-pinned prebuilt sysroot artifacts are the - **cache implementation detail**, not the delivery mechanism: the in-repo - toolchain source is the single source of truth, and a cold CI run must be - able to rebuild the identical sysroot from pins alone. +- **Shared OpenSSL ownership and layout (normative):** + - `toolchain/c/openssl/manifest.json` pins Node `v24.15.0`, its peeled + commit, OpenSSL 3.5.5, the Node source archive sha256, the + `deps/openssl/openssl` tree hash, configure flags, and expected outputs. + - `toolchain/c/scripts/build-openssl-upstream.sh` is the only acquisition + and build entry point. It extracts the exact Node-bundled source into the + ignored toolchain cache and builds static, builtin providers without + network access after acquisition. + - `toolchain/c/patches/openssl/` contains numbered source patches only when + a sysroot fix cannot own the gap; each patch states why. + - `toolchain/c/build/openssl/{include,lib/libcrypto.a,lib/libssl.a, + manifest.json}` is ignored build output. The output manifest includes + source, patch-set, sysroot, compiler, flags, and archive hashes. + - `crates/node-stdlib/wasm/crypto/` contains the Node binding adapter and + links the shared archives. curl/libcurl, wget, git, and OpenSSH consume + the same output manifest and archives. No consumer downloads or builds + OpenSSL privately, and no `build.rs` performs a network fetch. +- **CI/dev delivery** (measurement above collapses the problem — the cold + build is ~90s local / minutes on CI, so caching is an optimization, not + load-bearing infrastructure; in-repo toolchain source is the single + source of truth and a cold run reproduces the identical sysroot from + pins alone): + 1. **Recommended: GitHub Actions cache with build fallback.** Content-hash + key (wasi-sdk pin, llvm-project pin, `std-patches/**`, + `wasi-libc-overrides/**`, build scripts); hit restores in seconds, + miss rebuilds in-job in minutes — cheap enough for the PR path. The + network fetches (wasi-sdk + llvm tarballs, ~65s of the 87s) should be + mirrored to R2 with sha256 verification to remove third-party + availability from the critical path. + 2. Optional layer: a sha256-addressed prebuilt sysroot artifact (R2) for + local dev that wants `cargo build` to work without make — nice-to-have + given the from-source path costs ~90s; `just build-sysroot` is the + default local answer. + Local dev is never blocked on a build it didn't ask for; any artifact + fetch failure is a typed hard error naming the expected hash — not a + silent fallback. - **Pin everything (hard requirement, unchanged):** wasi-sdk tarball, llvm-project checkout, zlib/mbedtls/brotli/zstd tarballs, curl fork (currently tracks `main`!), os-test/libc-test — all get sha256/commit @@ -665,13 +754,15 @@ and the "behave like Linux" north star. They form a **parallel workstream Harness requirements: parse `// Flags:` and emulate the relevant ones (`--expose-internals` → provide `internal/test/binding` per node's own mechanism; unsupported flags → skip(flag:) with reason). - **Runnable-fraction trajectory (post-Decisions 2+3):** ~55–65% at M0 - (self-spawn and crypto not yet landed), rising to **~85–90% by M4** — - the execPath category (~16.5%) becomes runnable via the guest `node` - command and the hasCrypto category (~20.7%) gates on a real - `versions.openssl`. Durable exclusions: workers (~8%) and unsupported - flags. The ledger's denominator states the current fraction explicitly - at every milestone **[OQ 4]**. + **The denominator is measured, not guessed (Decision 7):** M0 runs the same + harness against legacy and real paths and publishes both runnable/pass sets. + The real path's M0 gap is implementation work, not an accepted percentage. + Each milestone must preserve every legacy-passing test in the categories it + replaces; by default flip/M5, the real path contains the complete + legacy-passing set except explicit program non-goals, with all additional + native-node passes ratcheted. Workers (~8%) and genuinely unsupported flags + remain reasoned exclusions. The ledger states exact counts and set diffs at + every milestone. - **Expected-state ledger** (checked-in JSON per category): `pass`, `fail-accepted(reason, issue)`, `skip(reason)`. CI fails on regression AND on unexpected pass (forces ledger updates; progress visible in @@ -696,9 +787,10 @@ and the "behave like Linux" north star. They form a **parallel workstream linked upstream tests (§6.7). ### 10.4 worker_threads stance -- Cutover ships without workers; binding is inert data (§4), `new Worker` - throws typed. Suite marks skip(worker). Later design sketch: host-side - multi-isolate + MessagePort over kernel channels — out of scope. **[OQ 3]** +- **Decision 6:** cutover ships without workers; binding is inert data (§4), + `new Worker` throws typed. Suite marks `skip(worker)`. A future host-side + multi-isolate + MessagePort design is a separate program and does not gate + this cutover. ### 10.5 execPath and self-spawn (DECIDED — Decision 3) - 643 suite tests (and real-world tools: npm lifecycle scripts, node-gyp @@ -777,7 +869,7 @@ and the "behave like Linux" north star. They form a **parallel workstream ## 12. Migration, cutover, deletion -### 12.1 Deletion inventory (review-corrected; the definition of "done") +### 12.1 Deletion inventory (review-corrected; native definition of "done") **Deleted:** - `packages/build-tools/bridge-src/**` — 30,486 LOC TS (41 files), its esbuild pipeline `packages/build-tools/scripts/build-v8-bridge.mjs`, the @@ -798,13 +890,14 @@ and the "behave like Linux" north star. They form a **parallel workstream `AGENTOS_ALLOWED_NODE_BUILTINS` host-node path. - CJS export-name extraction + CJS shim generation in `crates/v8-runtime/src/execution.rs`. -- Browser polyfill generators consuming bridge-src: - `build-browser-util-polyfill.mjs` (+path/buffer siblings) and - `packages/runtime-browser/src/generated/*-polyfill.ts` — **in the gate**; - browser migrates together with native (Decision 4, §12.3). -- The RustCrypto-backed `_crypto*` bridge globals and their sidecar - handlers (Decision 2) — protocol-lockstep removal at M5, after the - OpenSSL-wasm binding is the only crypto path. +- Browser runtime sources and its polyfill generators are **kept but disabled, + not migrated** (Decision 4, §12.3). They are removed from CI/release/publish + matrices in M0 and may cease to build after native `bridge-src/**` deletion; + browser buildability is not a gate for this program. +- The RustCrypto-backed `_crypto*`, host TLS-upgrade, and + `rustls-native-certs` bridge globals/handlers (Decision 2) — + protocol-lockstep removal at M5, after shared OpenSSL-wasm is the only + crypto/TLS path. - Collateral retargeted or deleted with owners named in the milestone PRs: `scripts/verify-check-types.mjs` undici-shims reference; native-sidecar suites keyed to the legacy layer (`builtin_conformance`, @@ -826,81 +919,70 @@ substrate), `~/.agents/recovery/secure-exec/` porting stops. per flavor (coldstart budget accounts for it). - Sessions are homogeneous (no per-module runtime mixing) — with the single sanctioned, expiring M0→M1 loader exception (§5). Migration granularity - lives in the suite ledger; the default flips per §13 criteria measured on - the real path only. - -### 12.3 Browser runtime (DECIDED — Decision 4: browser profile, migrate together) -- Ground truth: the browser runtime consumes bridge-src twice — generated - polyfills and the bridge bundle in `packages/runtime-browser/src/worker.ts` - (which also implements `_getPendingTimerCount`/`_waitForActiveHandles` - ~line 1899). **HOST (rusty_v8) bindings cannot exist in a browser - worker.** -- **Decided: browser and native migrate together; browser stays in the - deletion gate.** Committed scope — every HOST binding gets one of: - - **JS fallback** where feasible: `types` via brand checks (POC-proven - adequate), `util` introspection via JS approximations, `process.env` as - a sealed Proxy validated against the same parity fixture as the native - interceptor (§4.5), `task_queue` via the browser engine's own - queueMicrotask/rejection events where semantics allow. - - **Wasm/web-native fallback** where compute: `serdes` structured clone - via the worker's native `structuredClone`; leaf-lib wasm (simdutf, ada, - llhttp, zlib, **OpenSSL**) runs unmodified in the browser's wasm engine - — same blobs, same import contract; the leaf-lib strategy is what makes - Decision 4 tractable. - - **Documented typed degradation** where neither exists: full - `contextify`/`vm` semantics, CPED-backed AsyncLocalStorage (browser - path uses the promise-hook-less best effort or typed unsupported). - - The complete per-binding browser matrix (fallback vs degradation, with - the typed error for each degradation) is an **M0 deliverable** - (`docs-internal/node-stdlib-browser-profile.md`). -- Browser event loop: `worker.ts` loop-accounting hooks reimplemented - against the same HandleRegistry contract (§6.2) so `lib/` sees one loop - semantics in both runtimes. -- Every milestone M1–M5 carries a **browser-parity acceptance item** for - that milestone's module families (same suite categories run under the - browser harness, ledgered separately with the browser-profile - degradations as sanctioned skips). Schedule impact acknowledged in §14. + lives in the suite ledger. The default flips only after the real path + contains the measured legacy supported-test set and meets the same-day + legacy performance gate (Decision 7, §§8.3, 10.1, 12.5). + +### 12.3 Browser runtime (DECIDED — Decision 4: disabled, retained) +- Keep `packages/runtime-browser/**` and browser-specific generators in the + repository; do not delete or migrate them in this program. +- In M0, remove browser build, test, publish, and release jobs from the active + matrices. Browser failures cannot block CI or release, and the browser + package is not shipped while disabled. +- Native cutover may delete shared legacy inputs the disabled browser source + still references. This program makes no browser-buildability promise and + carries no browser profile/design-doc deliverable. Re-enabling the browser + runtime requires a separately approved migration and restores its own CI and + release gates then. ### 12.4 Client-visible changes (review-corrected: NOT "none") - `allowed_node_builtins` denial error shape/timing changes (§5): lockstep same-change updates to Rust client (`crates/client/src/config.rs`), TS options schema, and docs; TS/Rust behavioral identity preserved. -- **Platform tiering:** public `jsRuntime` option maps to +- **Native V8 platform tiering (not the disabled browser runtime):** public `jsRuntime` option maps to `AGENTOS_JS_PLATFORM` (`javascript.rs:3186-3227`) which today *subtractively scrubs* process/Buffer/require per execution. Real node's bootstrap makes `process` load-bearing — subtractive scrubbing does not - transfer. Spec: tiering becomes **bootstrap profiles** (e.g. `browser` - profile = web globals only, node bootstrap not run; `node` profile = - full), implemented at context-build time, with a parity gate asserting - each tier's global surface matches its documented contract. Guest-visible + transfer. Spec: preserve the existing public option values as + **context-build bootstrap profiles**; M0 captures a global-surface fixture + for every current value, and implementation must preserve that contract + while the full-node value runs node bootstrap. This concerns native V8 + sessions only and does not reintroduce browser-runtime scope. Guest-visible behavior change → docs + both clients in the same change. - Everything else (ACP/protocol, bridge globals) unchanged; bridge *additions* follow normal protocol-lockstep rules. ### 12.5 Done means -1. Deletion inventory empty on `main` (browser included, Decision 4); -2. node-suite ledger ≥ agreed per-category thresholds, no `fail-accepted` - without an issue link; 3. §8.3 budgets met with published same-machine +1. Native deletion inventory empty on `main`; the retained, disabled browser + runtime is explicitly outside it (Decision 4); 2. the real node-suite set + includes every legacy-passing test except explicit non-goals, and no + `fail-accepted` lacks an issue link; 3. §8.3 legacy-parity and native + budgets met with published same-machine before/after (§11.2); 4. docs + client schemas current; 5. vendor manifest + upgrade runbook in `docs-internal/`; 6. flag removed. ## 13. Phasing +**Cross-workspace entry gate:** before M0 implementation begins, finish and +forklift the ordered reg-tests handoff work: unskipped git-over-SSH clone/push, +then the procps-driven `/proc` completion. Read the current state and proof +requirements in `docs-internal/registry-networking-handoff.md` in the +`reg-tests` workspace. Do not move either workspace's `@` to inspect the other. + Cutover track: | M | Scope | Acceptance | |---|---|---| -| **M0** | Vendor node (pinned tag) + leaf-lib sources + empty `vendor/patches/` tooling; binding-grep reconcile vs tag; `crates/node-stdlib` skeleton; snapshot integration (flavor-keyed cache, defined fallback); **all 69 bindings + process object load inert** (every public module `require`s clean); interim loader exception documented; CPED build-flag verified; §11.2 measurement deliverables (floors, dual-target runner, A/B + PR-delta tooling, snapshot + wasm microbenches); budgets restated as floor×headroom; suite harness + honest initial ledger (runnable fraction stated); **`toolchain/` lands on main + sysroot CI cache (§9.4)** + policy text on main (§9.5); **browser-profile matrix doc (§12.3)** | real-stdlib session boots eager set from snapshot; `require` of all public modules succeeds; ledger + bench baselines published; toolchain builds green on main | +| **M0** | Vendor pinned Node 24 LTS JS + leaf-lib sources; create the shared `toolchain/c/openssl` manifest/build/patch layout (§9.4) and Node adapter skeleton without a private OpenSSL copy; binding-grep reconcile vs the v26 POC; `crates/node-stdlib` skeleton; snapshot integration (flavor-keyed cache, defined fallback); **every pinned-v24 binding + process object loads inert** (every public module `require`s clean); interim loader exception documented; CPED build-flag verified; §11.2 measurement deliverables (legacy + real floors, dual-target runner, A/B + PR-delta tooling, snapshot + wasm microbenches); budgets restated as floor×headroom; suite harness + exact legacy/real set-diff ledger; **shared OpenSSL build spike (§7.4)**; **`toolchain/` lands on main + sysroot CI cache (§9.4)** + policy text on main (§9.5); **browser runtime removed from active CI/release/publish matrices but source retained (§12.3)** | real-stdlib session boots eager set from snapshot; `require` of all public modules succeeds; exact legacy/real ledger + bench baselines published; shared OpenSSL `libcrypto.a`/`libssl.a` compiles, emits a reproducibility manifest, and completes an in-guest handshake; toolchain builds green on main; browser cannot block CI/release | | **M1** | fs complete: fd-level 1:1 bridge, base64-kill + backing-store transport (A/B in acceptance), stat truth, statfs/access/utimes/mkdtemp/copyFile/opendir, uv errno fixture, blob, permission row final; kernel readdir-ENOENT fix; GC/detach stress test; real CJS loader for user code (module_wrap minimal + compileFunctionForCJSLoader) — loader exception expires | `test-fs-*` sync+async ledger green (watch excluded); fs micro within budget; transport A/B published | | **M2** | Event loop phases + HandleRegistry + explicit-microtask policy + MakeCallback discipline + loop-turn clock; async_wrap/async_context_frame HOST (ALS works); task_queue HOST pieces (unhandledRejection); messaging (DOMException, transferables, structuredClone); streams at suite depth; fs.watch kernel primitive + `fs_event_wrap`; limits table live | streams/timers/ALS suite categories green; ordering parity ledger clean; limits enforced with typed errors | -| **M3** | net (StreamBase contract incl. sync-write fast path, streamBaseState, accepted-handle hydration; kernel readiness push + accept events), dns/dgram, tty (guessHandleType fixture); http via llhttp-wasm + real `_http_*` + real undici fetch | net/http suite categories green (native + browser ledgers); http RPS/p99 + stream-throughput budgets met | -| **M4** | crypto: **OpenSSL-wasm build + binding (Decision 2)**, real `versions.openssl`, entropy host import; zlib/brotli/zstd (wasm, new encoder rules); child_process + **guest `node` command / self-spawn (Decision 3)**; TLS per OQ 2 resolution; os/process_methods, sqlite, http2 | respective categories green with the ~85–90% denominator (§10.1); crypto-blob + lazy-instantiation budgets met | -| **M5** | ESM loader + vm/contextify completeness; platform-tiering bootstrap profiles; flag default→real then **flag removed**; **deletion inventory executed** (incl. split-file surgery + `_crypto*` bridge removal §12.1, browser included); bridge-call census (§8.2); final same-machine before/after report; docs/clients lockstep | §12.5 "done" | +| **M3** | net (StreamBase contract incl. sync-write fast path, streamBaseState, accepted-handle hydration; kernel readiness push + accept events), dns/dgram, tty (guessHandleType fixture); http via llhttp-wasm + real `_http_*` + real undici fetch | net/http suite categories green (native ledger); http RPS/p99 + stream-throughput budgets met | +| **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" | -Every milestone M1–M5 additionally carries the **browser-parity acceptance -item** for its module families (Decision 4, §12.3): same suite categories -under the browser harness, separate ledger, browser-profile degradations as -sanctioned skips. +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. Parallel libc track (does not gate M5): **L1** eventfd (kernel fd object + host import + pollable + conformance), **L2** epoll (libc emulation over @@ -915,40 +997,33 @@ ready; consumed by future registry-software work. | StreamBase sync/async duality subtleties | M3 | contract specced from `stream_base_commons.js`/`stream_wrap.cc`; errno fixtures; sync fast path in design not retrofit | | Bridge chattiness dominates fs/net/loader perf | M1/M3/M5 | floors measured M0; backing-store transport; batching; census at M5 entry; ratio-gated nightly | | CPED flag absent from our V8 build | M0 | verify first (M0 task); fallback = SetPromiseHooks path (slower, HOST already specced) | -| OpenSSL-wasm build + binding scale (Decision 2) | M4 | edgejs build recipe as starting point; mbedTLS static-lib precedent in-toolchain; ~174-prop inventory as checklist; lazy instantiation + blob budget; crypto is the single largest leaf-lib workstream — start the build spike in M1, not M4 | +| Shared OpenSSL 3.5.x wasm build + binding scale (Decision 2) | M0–M4 | edgejs build recipe and the proven mbedTLS/sysroot/socket/CA plumbing as starting points; inventory pinned-v24 binding surface; build + handshake spike in M0; lazy instantiation + blob budget; one content-addressed build and cross-runtime e2e prevent backend drift | | Suite harness realism (Flags emulation, common/ deps) | M0+ | quantified up front (§10.1); Decisions 2+3 remove the two biggest self-skip categories; honest denominator per milestone | | Snapshot bloat / lazy-compile latency | M0+ | size/build-time/lazy-compile metrics + budgets from M0; code-cache follow-up | -| Browser-profile scope (Decision 4) — every HOST binding needs a fallback/degradation, browser ledgers per milestone; **this is the largest scope addition of the decisions and extends every milestone** | M0–M5 | M0 browser matrix makes the cost visible before commitment deepens; leaf-lib wasm runs natively in browser (no port); degradations are typed + documented, not parity-blocking; schedule re-estimate at M1 exit with the matrix in hand | | Toolchain-on-main CI cost (Decision 1) | M0+ | content-hash sysroot cache; cold rebuild off the PR path; pins make cache correct by construction | | Node-version upgrades churn binding ABI | post-cutover | pinned vendor manifest; upgrade runbook = suite ledger diff | | Sysroot/artifact supply chain (unpinned inputs) | M0 | §9.4 pin-everything + content-hash metadata | | Legacy/real dual maintenance drags | M1–M5 | legacy frozen (bugfix-only) at M1; flag removal is M5 exit | -## 15. Open questions for the user - -Resolved 2026-07-09 (see §0 Decisions log): sysroot/toolchain delivery, -crypto backend, execPath/self-spawn, browser runtime, and the old -`versions.openssl` synthesis question (mooted by real OpenSSL). Remaining: - -1. **Node version pin** — latest v26.x stable tag (recommended; POC-aligned, - LTS Oct 2026) vs v24 LTS now. -2. **TLS shape** (reframed by Decision 2) — **in-guest TLS**: node's real - `tls_wrap` over the in-isolate OpenSSL, TLS records over kernel-owned - TCP (recommended: wholesale fidelity for SecureContext/ALPN/session- - reuse semantics; kernel keeps socket + egress policy and sees ciphertext - exactly as a real Linux kernel does) vs **kernel-owned TLS upgrade** - (smaller guest; every TLS API knob re-marshalled, deviations likely). - Also inherits the Decision 2 sub-choice: OpenSSL 3.x (recommended, - proceeding) vs BoringSSL. -3. **worker_threads at cutover** — inert binding + typed `new Worker` error - (recommended) vs making a worker story a cutover requirement. -4. **Initial runnable suite fraction** — accept ~55–65% at M0 ratcheting to - ~85–90% by M4 as Decisions 2+3 land (recommended) vs pulling the guest - `node` command and/or OpenSSL work earlier to raise the denominator - before the M0 baseline. -5. **Sequencing** — M3 net/http as specced (recommended; with the M1 - OpenSSL build spike per §14) vs pulling http earlier at the cost of fs - polish. +## 15. Resolved questions and implementation decision gates + +No user product decision remains open as of 2026-07-09. §0 records the +answers: Node 24.15.0 LTS; one real shared OpenSSL 3.5.x wasm backend with in-guest +TLS; workers unsupported; empirical legacy functional/performance parity; +dependency-driven sequencing; browser source retained but disabled and wholly +outside this program. + +The following are engineering gates, not user preference questions: +1. M0 pins the exact Node 24 tag and reconciles the binding inventory. +2. M0 proves the shared OpenSSL build + handshake. A failure is escalated only + with concrete sysroot/OpenSSL-patch evidence (§7.4), never by silently + adding another TLS backend. +3. M0 measures exact legacy/real suite sets and benchmark floors; those + measurements replace guessed denominators and provisional budgets. +4. URLPattern uses JS-RegExp wasm imports or a HOST provider based on the M0 + prototype; either must pass the same native-node differential fixtures. +5. nghttp2-wasm is the M4 path; a host fallback requires a measured, + documented blocker (§4). --- @@ -1075,13 +1150,15 @@ resolution — in both cases because the resolving input (user repo-strategy preference; M0 floor measurements) does not exist yet; the spec now says exactly how and when each gets resolved. No finding was rejected. -**Post-review update (v3):** the C2 open question was subsequently resolved +**Post-review update (v4):** the C2 open question was subsequently resolved by the user (Decision 1, §0 — toolchain in the monorepo), superseding the prebuilt-artifact recommendation; the reviewer's caching and pinning -requirements are retained in full in §9.4. Decisions 2–4 (§0) also -supersede the v2 recommendations for crypto (was RustCrypto — now -OpenSSL-wasm), browser (was freeze-and-decide — now committed profile), and -self-spawn (recommendation adopted). +requirements are retained in full in §9.4. The final §0 decisions also +supersede the v2 recommendations: crypto/TLS now use one real shared OpenSSL +3.5.x wasm build, browser source is retained but disabled and removed from +this program, self-spawn is adopted, Node is pinned to v24.15.0 LTS, workers +are unsupported at cutover, and functional/performance acceptance is measured +against the legacy implementation rather than a guessed suite fraction. --- *Grounded against: `crates/bridge/bridge-contract.json` (178 globals),