Skip to main content

agentos_execution/
javascript.rs

1use crate::common::stable_hash64;
2use crate::node_import_cache::{
3    NodeImportCache, NodeImportCacheCleanup, NODE_IMPORT_CACHE_ASSET_ROOT_ENV,
4};
5use crate::runtime_support::{
6    NODE_COMPILE_CACHE_ENV, NODE_DISABLE_COMPILE_CACHE_ENV, NODE_FROZEN_TIME_ENV,
7    NODE_SANDBOX_ROOT_ENV,
8};
9use crate::signal::NodeSignalHandlerRegistration;
10use crate::v8_host::{V8RuntimeHost, V8SessionHandle};
11use crate::v8_ipc::BinaryFrame;
12use crate::v8_runtime;
13use agentos_bridge::queue_tracker::{register_queue, TrackedLimit, TrackedReceiver};
14use agentos_v8_runtime::runtime_protocol::{RuntimeCommand, WarmSessionHint};
15use getrandom::getrandom;
16use serde::Deserialize;
17use serde::Serialize;
18use serde_json::{json, Value};
19use std::cmp::Reverse;
20use std::collections::{BTreeMap, BinaryHeap, HashMap, HashSet, VecDeque};
21use std::fmt;
22use std::fs::{self, File};
23use std::io::{BufRead, BufReader, BufWriter, Write};
24use std::os::fd::OwnedFd;
25use std::panic::{catch_unwind, AssertUnwindSafe};
26use std::path::{Path, PathBuf};
27use std::sync::{
28    atomic::{AtomicU64, Ordering},
29    mpsc::{self, Receiver, SyncSender, TrySendError},
30    Arc, Condvar, Mutex, OnceLock,
31};
32use std::thread;
33use std::time::{Duration, Instant};
34use tokio::sync::{
35    mpsc::{channel, error::TryRecvError as TokioTryRecvError, Receiver as TokioReceiver},
36    Notify,
37};
38use tokio::time;
39
40const NODE_ENTRYPOINT_ENV: &str = "AGENTOS_ENTRYPOINT";
41const NODE_BOOTSTRAP_ENV: &str = "AGENTOS_BOOTSTRAP_MODULE";
42const NODE_GUEST_ARGV_ENV: &str = "AGENTOS_GUEST_ARGV";
43const NODE_PREWARM_IMPORTS_ENV: &str = "AGENTOS_NODE_PREWARM_IMPORTS";
44const NODE_IMPORT_COMPILE_CACHE_NAMESPACE_VERSION: &str = "3";
45const NODE_IMPORT_CACHE_LOADER_PATH_ENV: &str = "AGENTOS_NODE_IMPORT_CACHE_LOADER_PATH";
46const NODE_IMPORT_CACHE_PATH_ENV: &str = "AGENTOS_NODE_IMPORT_CACHE_PATH";
47const NODE_KEEP_STDIN_OPEN_ENV: &str = "AGENTOS_KEEP_STDIN_OPEN";
48const NODE_GUEST_ENTRYPOINT_ENV: &str = "AGENTOS_GUEST_ENTRYPOINT";
49const NODE_GUEST_ENTRYPOINT_MODULE_MODE_ENV: &str = "AGENTOS_GUEST_ENTRYPOINT_MODULE_MODE";
50const NODE_GUEST_PATH_MAPPINGS_ENV: &str = "AGENTOS_GUEST_PATH_MAPPINGS";
51const NODE_VIRTUAL_PROCESS_EXEC_PATH_ENV: &str = "AGENTOS_VIRTUAL_PROCESS_EXEC_PATH";
52const NODE_VIRTUAL_PROCESS_PID_ENV: &str = "AGENTOS_VIRTUAL_PROCESS_PID";
53const NODE_VIRTUAL_PROCESS_PPID_ENV: &str = "AGENTOS_VIRTUAL_PROCESS_PPID";
54const NODE_VIRTUAL_PROCESS_UID_ENV: &str = "AGENTOS_VIRTUAL_PROCESS_UID";
55const NODE_VIRTUAL_PROCESS_GID_ENV: &str = "AGENTOS_VIRTUAL_PROCESS_GID";
56const NODE_PARENT_ALLOW_CHILD_PROCESS_ENV: &str = "AGENTOS_PARENT_NODE_ALLOW_CHILD_PROCESS";
57const NODE_PARENT_ALLOW_WORKER_ENV: &str = "AGENTOS_PARENT_NODE_ALLOW_WORKER";
58const NODE_EXTRA_FS_READ_PATHS_ENV: &str = "AGENTOS_EXTRA_FS_READ_PATHS";
59const NODE_EXTRA_FS_WRITE_PATHS_ENV: &str = "AGENTOS_EXTRA_FS_WRITE_PATHS";
60const NODE_ALLOWED_BUILTINS_ENV: &str = "AGENTOS_ALLOWED_NODE_BUILTINS";
61const NODE_LOOPBACK_EXEMPT_PORTS_ENV: &str = "AGENTOS_LOOPBACK_EXEMPT_PORTS";
62const NODE_SYNC_RPC_ENABLE_ENV: &str = "AGENTOS_NODE_SYNC_RPC_ENABLE";
63const NODE_SYNC_RPC_REQUEST_FD_ENV: &str = "AGENTOS_NODE_SYNC_RPC_REQUEST_FD";
64const NODE_SYNC_RPC_RESPONSE_FD_ENV: &str = "AGENTOS_NODE_SYNC_RPC_RESPONSE_FD";
65const NODE_SYNC_RPC_DATA_BYTES_ENV: &str = "AGENTOS_NODE_SYNC_RPC_DATA_BYTES";
66const NODE_SYNC_RPC_WAIT_TIMEOUT_MS_ENV: &str = "AGENTOS_NODE_SYNC_RPC_WAIT_TIMEOUT_MS";
67static NEXT_V8_SESSION_ID: AtomicU64 = AtomicU64::new(1);
68static JAVASCRIPT_TIMER_WHEEL: OnceLock<Arc<TimerWheel>> = OnceLock::new();
69
70#[derive(Default)]
71struct JsStartPhaseStats {
72    calls: u64,
73    total_ns: u128,
74    max_ns: u128,
75}
76
77static JS_START_PHASES: OnceLock<Mutex<BTreeMap<String, JsStartPhaseStats>>> = OnceLock::new();
78static JS_EVENT_PHASES: OnceLock<Mutex<BTreeMap<String, JsStartPhaseStats>>> = OnceLock::new();
79
80fn js_start_phases_enabled() -> bool {
81    std::env::var("AGENTOS_JS_START_PHASES").as_deref() == Ok("1")
82}
83
84fn js_event_phases_enabled() -> bool {
85    std::env::var("AGENTOS_JS_EVENT_PHASES").as_deref() == Ok("1")
86}
87
88fn record_js_start_phase(stage: &str, elapsed: Duration) {
89    if !js_start_phases_enabled() {
90        return;
91    }
92    record_js_phase_stats(
93        &JS_START_PHASES,
94        "AGENTOS_JS_START_PHASES_FILE",
95        stage,
96        elapsed,
97    );
98}
99
100fn record_js_event_phase(stage: &str, elapsed: Duration) {
101    if !js_event_phases_enabled() {
102        return;
103    }
104    record_js_phase_stats(
105        &JS_EVENT_PHASES,
106        "AGENTOS_JS_EVENT_PHASES_FILE",
107        stage,
108        elapsed,
109    );
110}
111
112fn record_js_phase_stats(
113    phases: &OnceLock<Mutex<BTreeMap<String, JsStartPhaseStats>>>,
114    path_env: &str,
115    stage: &str,
116    elapsed: Duration,
117) {
118    let phases = phases.get_or_init(|| Mutex::new(BTreeMap::new()));
119    let Ok(mut phases) = phases.lock() else {
120        return;
121    };
122    let stats = phases.entry(stage.to_string()).or_default();
123    stats.calls += 1;
124    let elapsed_ns = elapsed.as_nanos();
125    stats.total_ns += elapsed_ns;
126    stats.max_ns = stats.max_ns.max(elapsed_ns);
127
128    let Some(path) = std::env::var_os(path_env) else {
129        return;
130    };
131    let mut output = String::new();
132    for (stage, stats) in phases.iter() {
133        let total_us = stats.total_ns / 1_000;
134        let avg_us = if stats.calls == 0 {
135            0
136        } else {
137            total_us / u128::from(stats.calls)
138        };
139        let max_us = stats.max_ns / 1_000;
140        output.push_str(&format!(
141            "stage={stage} calls={} total_us={total_us} avg_us={avg_us} max_us={max_us}\n",
142            stats.calls
143        ));
144    }
145    let _ = fs::write(path, output);
146}
147
148const DEFAULT_V8_CPU_TIME_LIMIT_MS: u32 = 30_000;
149const DEFAULT_V8_WALL_CLOCK_LIMIT_MS: u32 = 0;
150const DEFAULT_NODE_IMPORT_CACHE_MATERIALIZE_TIMEOUT_MS: u64 = 30_000;
151const NODE_SYNC_RPC_DEFAULT_DATA_BYTES: usize = 4 * 1024 * 1024;
152const NODE_SYNC_RPC_DEFAULT_WAIT_TIMEOUT_MS: u64 = 30_000;
153const NODE_SYNC_RPC_RESPONSE_QUEUE_CAPACITY: usize = 1;
154const FORWARD_KERNEL_STDIN_RPC_ENV: &str = "AGENTOS_FORWARD_KERNEL_STDIN_RPC";
155// Defense-in-depth headroom: a transient burst of guest events (e.g. a chatty
156// tool/skill turn) should be absorbed by the buffer, so the producer only ever
157// hits backpressure under a genuinely stuck consumer rather than on every spike.
158const JAVASCRIPT_EVENT_CHANNEL_CAPACITY: usize = 512;
159const JAVASCRIPT_EVENT_PAYLOAD_LIMIT_BYTES: usize = 1024 * 1024;
160const JAVASCRIPT_CAPTURED_OUTPUT_LIMIT_BYTES: usize = 16 * 1024 * 1024;
161const KERNEL_STDIN_BUFFER_LIMIT_BYTES: usize = 16 * 1024 * 1024;
162const NODE_WARMUP_MARKER_VERSION: &str = "1";
163const NODE_WARMUP_SPECIFIERS: &[&str] = &[
164    "secure-exec:builtin/path",
165    "secure-exec:builtin/url",
166    "secure-exec:builtin/fs-promises",
167    "secure-exec:polyfill/path",
168];
169
170#[derive(Debug, Default, Clone)]
171struct SyncBridgePhaseStats {
172    calls: u64,
173    total_us: u64,
174    max_us: u64,
175}
176
177static SYNC_BRIDGE_PHASES: OnceLock<Mutex<BTreeMap<String, SyncBridgePhaseStats>>> =
178    OnceLock::new();
179static SYNC_BRIDGE_REQUEST_ENQUEUED: OnceLock<Mutex<HashMap<u64, (String, Instant)>>> =
180    OnceLock::new();
181
182fn sync_bridge_phases_enabled() -> bool {
183    std::env::var("AGENTOS_SYNC_BRIDGE_PHASES").as_deref() == Ok("1")
184}
185
186fn record_sync_bridge_phase(method: &str, stage: &str, elapsed: Duration) {
187    if !sync_bridge_phases_enabled() {
188        return;
189    }
190    let stats = SYNC_BRIDGE_PHASES.get_or_init(|| Mutex::new(BTreeMap::new()));
191    let Ok(mut stats) = stats.lock() else {
192        return;
193    };
194    let elapsed_us = elapsed.as_micros() as u64;
195    let key = format!("{method}:{stage}");
196    let entry = stats.entry(key).or_default();
197    entry.calls += 1;
198    entry.total_us = entry.total_us.wrapping_add(elapsed_us);
199    entry.max_us = entry.max_us.max(elapsed_us);
200
201    if let Ok(path) = std::env::var("AGENTOS_SYNC_BRIDGE_PHASES_FILE") {
202        let mut lines = String::new();
203        for (key, value) in stats.iter() {
204            let Some((method, stage)) = key.split_once(':') else {
205                continue;
206            };
207            let avg_us = value.total_us.checked_div(value.calls).unwrap_or(0);
208            lines.push_str(&format!(
209                "method={method} stage={stage} calls={} total_us={} avg_us={} max_us={}\n",
210                value.calls, value.total_us, avg_us, value.max_us
211            ));
212        }
213        let _ = fs::write(path, lines);
214    }
215}
216
217pub fn record_sync_bridge_request_enqueued(call_id: u64, method: &str) {
218    if !sync_bridge_phases_enabled() {
219        return;
220    }
221    let requests = SYNC_BRIDGE_REQUEST_ENQUEUED.get_or_init(|| Mutex::new(HashMap::new()));
222    let Ok(mut requests) = requests.lock() else {
223        return;
224    };
225    if requests.len() > 4096 {
226        requests.clear();
227    }
228    requests.insert(call_id, (method.to_owned(), Instant::now()));
229}
230
231pub fn record_sync_bridge_request_observed(call_id: u64, fallback_method: &str) {
232    if !sync_bridge_phases_enabled() {
233        return;
234    }
235    let Some(requests) = SYNC_BRIDGE_REQUEST_ENQUEUED.get() else {
236        return;
237    };
238    let Ok(mut requests) = requests.lock() else {
239        return;
240    };
241    let Some((method, started)) = requests.remove(&call_id) else {
242        return;
243    };
244    let method = if method.is_empty() {
245        fallback_method
246    } else {
247        method.as_str()
248    };
249    record_sync_bridge_phase(method, "request_service_observed", started.elapsed());
250}
251const CONTROLLED_STDERR_PREFIXES: &[&str] =
252    &[crate::node_import_cache::NODE_IMPORT_CACHE_METRICS_PREFIX];
253const RESERVED_NODE_ENV_KEYS: &[&str] = &[
254    NODE_BOOTSTRAP_ENV,
255    NODE_COMPILE_CACHE_ENV,
256    NODE_DISABLE_COMPILE_CACHE_ENV,
257    NODE_ENTRYPOINT_ENV,
258    NODE_EXTRA_FS_READ_PATHS_ENV,
259    NODE_EXTRA_FS_WRITE_PATHS_ENV,
260    NODE_SANDBOX_ROOT_ENV,
261    NODE_FROZEN_TIME_ENV,
262    NODE_GUEST_ENTRYPOINT_ENV,
263    NODE_GUEST_ENTRYPOINT_MODULE_MODE_ENV,
264    NODE_GUEST_ARGV_ENV,
265    NODE_GUEST_PATH_MAPPINGS_ENV,
266    NODE_VIRTUAL_PROCESS_EXEC_PATH_ENV,
267    NODE_VIRTUAL_PROCESS_PID_ENV,
268    NODE_VIRTUAL_PROCESS_PPID_ENV,
269    NODE_VIRTUAL_PROCESS_UID_ENV,
270    NODE_VIRTUAL_PROCESS_GID_ENV,
271    NODE_PARENT_ALLOW_CHILD_PROCESS_ENV,
272    NODE_PARENT_ALLOW_WORKER_ENV,
273    NODE_IMPORT_CACHE_ASSET_ROOT_ENV,
274    NODE_IMPORT_CACHE_LOADER_PATH_ENV,
275    NODE_IMPORT_CACHE_PATH_ENV,
276    NODE_KEEP_STDIN_OPEN_ENV,
277    NODE_ALLOWED_BUILTINS_ENV,
278    NODE_LOOPBACK_EXEMPT_PORTS_ENV,
279    NODE_SYNC_RPC_ENABLE_ENV,
280    NODE_SYNC_RPC_REQUEST_FD_ENV,
281    NODE_SYNC_RPC_RESPONSE_FD_ENV,
282    NODE_SYNC_RPC_DATA_BYTES_ENV,
283    NODE_SYNC_RPC_WAIT_TIMEOUT_MS_ENV,
284];
285
286#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
287#[serde(tag = "type", rename_all = "snake_case")]
288enum NodeControlMessage {
289    NodeImportCacheMetrics {
290        metrics: serde_json::Value,
291    },
292    PythonExit {
293        #[serde(rename = "exitCode")]
294        exit_code: i32,
295    },
296    SignalState {
297        signal: u32,
298        registration: NodeSignalHandlerRegistration,
299    },
300}
301
302#[derive(Debug, Default)]
303struct LinePrefixFilter {
304    pending: Vec<u8>,
305}
306
307#[derive(Debug, Clone, PartialEq, Eq)]
308pub struct JavascriptSyncRpcRequest {
309    pub id: u64,
310    pub method: String,
311    pub args: Vec<Value>,
312    pub raw_bytes_args: HashMap<usize, Vec<u8>>,
313}
314
315#[derive(Debug, Deserialize)]
316struct JavascriptSyncRpcRequestWire {
317    id: u64,
318    method: String,
319    #[serde(default)]
320    args: Vec<Value>,
321}
322
323struct JavascriptSyncRpcChannels {
324    parent_request_reader: File,
325    parent_response_writer: File,
326    child_request_writer: OwnedFd,
327    child_response_reader: OwnedFd,
328}
329
330impl LinePrefixFilter {
331    fn filter_chunk(&mut self, chunk: &[u8], prefixes: &[&str]) -> Vec<u8> {
332        self.pending.extend_from_slice(chunk);
333        let mut filtered = Vec::new();
334
335        while let Some(newline_index) = self.pending.iter().position(|byte| *byte == b'\n') {
336            let line = self.pending.drain(..=newline_index).collect::<Vec<_>>();
337            if !has_control_prefix(&line, prefixes) {
338                filtered.extend_from_slice(&line);
339            }
340        }
341
342        filtered
343    }
344}
345
346fn has_control_prefix(line: &[u8], prefixes: &[&str]) -> bool {
347    let text = String::from_utf8_lossy(line);
348    let trimmed = text.trim_end_matches(['\r', '\n']);
349    prefixes.iter().any(|prefix| trimmed.starts_with(prefix))
350}
351
352#[derive(Debug)]
353struct JavascriptSyncRpcResponseWriter {
354    sender: SyncSender<Vec<u8>>,
355    timeout: Duration,
356}
357
358impl JavascriptSyncRpcResponseWriter {
359    fn new(writer: File, timeout: Duration) -> Self {
360        let (sender, receiver) = mpsc::sync_channel(NODE_SYNC_RPC_RESPONSE_QUEUE_CAPACITY);
361        spawn_javascript_sync_rpc_response_writer(writer, receiver);
362        Self { sender, timeout }
363    }
364
365    fn send(&self, payload: Vec<u8>) -> Result<(), JavascriptExecutionError> {
366        let started = Instant::now();
367        let mut payload = Some(payload);
368
369        loop {
370            match self
371                .sender
372                .try_send(payload.take().expect("payload should be present"))
373            {
374                Ok(()) => return Ok(()),
375                Err(TrySendError::Disconnected(_)) => {
376                    return Err(JavascriptExecutionError::RpcResponse(String::from(
377                        "JavaScript sync RPC response channel closed unexpectedly",
378                    )));
379                }
380                Err(TrySendError::Full(returned_payload)) => {
381                    if started.elapsed() >= self.timeout {
382                        return Err(JavascriptExecutionError::RpcResponse(format!(
383                            "timed out after {}ms while queueing JavaScript sync RPC response",
384                            self.timeout.as_millis()
385                        )));
386                    }
387                    payload = Some(returned_payload);
388                    thread::sleep(Duration::from_millis(5));
389                }
390            }
391        }
392    }
393}
394
395impl Clone for JavascriptSyncRpcResponseWriter {
396    fn clone(&self) -> Self {
397        Self {
398            sender: self.sender.clone(),
399            timeout: self.timeout,
400        }
401    }
402}
403
404#[derive(Debug, Clone, Copy, PartialEq, Eq)]
405enum PendingSyncRpcState {
406    Pending(u64),
407    TimedOut(u64),
408}
409
410#[derive(Debug, Clone, Copy, PartialEq, Eq)]
411enum PendingSyncRpcResolution {
412    Pending,
413    TimedOut,
414    Missing,
415}
416
417#[derive(Debug, Clone, PartialEq, Eq)]
418pub struct CreateJavascriptContextRequest {
419    pub vm_id: String,
420    pub bootstrap_module: Option<String>,
421    pub compile_cache_root: Option<PathBuf>,
422}
423
424#[derive(Debug, Clone, PartialEq, Eq)]
425pub struct JavascriptContext {
426    pub context_id: String,
427    pub vm_id: String,
428    pub bootstrap_module: Option<String>,
429    pub compile_cache_dir: Option<PathBuf>,
430}
431
432/// Per-execution JavaScript runtime limits, carried as typed fields on the
433/// execution request rather than `AGENTOS_*` env vars. The sidecar populates
434/// these from the per-VM `VmLimits` (which originate from `CreateVmConfig` on
435/// the BARE wire); `None` selects the engine default. See the env-vs-wire rule
436/// in `crates/sidecar/CLAUDE.md`.
437#[derive(Debug, Clone, Default, PartialEq, Eq)]
438pub struct JavascriptExecutionLimits {
439    /// V8 heap cap in MB. `None`/`Some(0)` keeps the engine default heap.
440    pub v8_heap_limit_mb: Option<u32>,
441    /// Sync-RPC blocking-wait ceiling in ms. `None` keeps the engine default.
442    pub sync_rpc_wait_timeout_ms: Option<u64>,
443    /// Active JavaScript CPU-time budget in ms. `None` keeps the engine default;
444    /// `Some(0)` disables the CPU watchdog.
445    pub cpu_time_limit_ms: Option<u32>,
446    /// JavaScript wall-clock backstop in ms. `None` keeps the engine default;
447    /// `Some(0)` disables the wall-clock watchdog.
448    pub wall_clock_limit_ms: Option<u32>,
449    /// Timeout for materializing the per-VM Node import cache.
450    pub import_cache_materialize_timeout_ms: Option<u64>,
451}
452
453/// Per-execution guest-runtime config carried as typed fields rather than
454/// `AGENTOS_*` env vars. The sidecar populates these from kernel state
455/// (`user_profile()`, `resource_limits()`) and `CreateVmConfig`; the runtime
456/// shim interpolates them into a `_processConfig` object the guest reads, so the
457/// guest's virtual identity no longer rides the ambient env channel. `None`
458/// keeps the guest-runtime default. See the env-vs-wire rule in
459/// `crates/sidecar/CLAUDE.md`.
460#[derive(Debug, Clone, Default, PartialEq, Eq)]
461pub struct GuestRuntimeConfig {
462    /// Virtual `process.pid`.
463    pub virtual_pid: Option<u64>,
464    /// Virtual `process.ppid`.
465    pub virtual_ppid: Option<u64>,
466    /// Virtual `process.uid` / `process.euid`.
467    pub virtual_uid: Option<u64>,
468    /// Virtual `process.gid` / `process.egid` / `process.groups`.
469    pub virtual_gid: Option<u64>,
470    /// Virtual `process.execPath`.
471    pub virtual_exec_path: Option<String>,
472    /// `os.cpus().length`.
473    pub os_cpu_count: Option<u64>,
474    /// `os.totalmem()` in bytes.
475    pub os_totalmem: Option<u64>,
476    /// `os.freemem()` in bytes.
477    pub os_freemem: Option<u64>,
478    /// `os.homedir()`.
479    pub os_homedir: Option<String>,
480    /// `os.hostname()`.
481    pub os_hostname: Option<String>,
482    /// `os.tmpdir()`.
483    pub os_tmpdir: Option<String>,
484    /// `os.type()`.
485    pub os_type: Option<String>,
486    /// `os.release()`.
487    pub os_release: Option<String>,
488    /// `os.version()`.
489    pub os_version: Option<String>,
490    /// `os.machine()`.
491    pub os_machine: Option<String>,
492    /// Default login shell.
493    pub os_shell: Option<String>,
494    /// `os.userInfo().username`.
495    pub os_user: Option<String>,
496    /// Opt-in high-resolution monotonic guest clock. Default false preserves
497    /// the security-oriented coarse clock.
498    pub high_resolution_time: bool,
499    /// Optional agent-SDK bundle (esbuild IIFE) to evaluate into the per-sidecar
500    /// V8 snapshot alongside the bridge, so the SDK is loaded once per sidecar and
501    /// reused across sessions instead of re-imported on every execution. `None`
502    /// keeps the bridge-only snapshot (unchanged behavior). The runtime caches the
503    /// snapshot process-wide keyed by sha256(bridge_code + this bundle).
504    pub snapshot_userland_code: Option<String>,
505}
506
507#[derive(Debug, Clone, PartialEq, Eq)]
508pub struct StartJavascriptExecutionRequest {
509    pub vm_id: String,
510    pub context_id: String,
511    pub argv: Vec<String>,
512    pub env: BTreeMap<String, String>,
513    pub cwd: PathBuf,
514    /// Per-execution runtime limits (see [`JavascriptExecutionLimits`]).
515    pub limits: JavascriptExecutionLimits,
516    /// Per-execution guest-runtime config (see [`GuestRuntimeConfig`]).
517    pub guest_runtime: GuestRuntimeConfig,
518    /// Optional inline JavaScript code supplied by the sidecar.
519    /// Eval entrypoints always execute this source directly. Module-mode file
520    /// entrypoints may also use it so the isolate can evaluate the original
521    /// source without re-reading through the host. CommonJS file entrypoints
522    /// still go through the normal require() wrapper so Node-style globals such
523    /// as __filename and __dirname are initialized correctly.
524    pub inline_code: Option<String>,
525    /// Optional raw WASM module bytes to expose to the runner isolate for this
526    /// execution.
527    pub wasm_module_bytes: Option<Arc<Vec<u8>>>,
528}
529
530#[derive(Debug, Clone, PartialEq, Eq)]
531pub enum JavascriptExecutionEvent {
532    Stdout(Vec<u8>),
533    Stderr(Vec<u8>),
534    SyncRpcRequest(JavascriptSyncRpcRequest),
535    SignalState {
536        signal: u32,
537        registration: NodeSignalHandlerRegistration,
538    },
539    Exited(i32),
540}
541
542#[derive(Debug, Clone, PartialEq, Eq)]
543enum JavascriptProcessEvent {
544    Stdout(Vec<u8>),
545    RawStderr(Vec<u8>),
546    SyncRpcRequest(JavascriptSyncRpcRequest),
547    Control(NodeControlMessage),
548    Exited(i32),
549}
550
551#[derive(Debug, Clone, PartialEq, Eq)]
552pub struct JavascriptExecutionResult {
553    pub execution_id: String,
554    pub exit_code: i32,
555    pub stdout: Vec<u8>,
556    pub stderr: Vec<u8>,
557}
558
559#[derive(Debug, Clone, PartialEq, Eq)]
560struct GuestPathMapping {
561    guest_path: String,
562    host_path: PathBuf,
563}
564
565#[derive(Debug, Deserialize)]
566struct GuestPathMappingWire {
567    #[serde(rename = "guestPath")]
568    guest_path: String,
569    #[serde(rename = "hostPath")]
570    host_path: String,
571}
572
573#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
574pub enum ModuleResolveMode {
575    Require,
576    Import,
577}
578
579#[derive(Debug, Clone, Copy, PartialEq, Eq)]
580pub enum LocalResolvedModuleFormat {
581    Module,
582    Commonjs,
583    Json,
584}
585
586impl LocalResolvedModuleFormat {
587    pub fn as_str(self) -> &'static str {
588        match self {
589            Self::Module => "module",
590            Self::Commonjs => "commonjs",
591            Self::Json => "json",
592        }
593    }
594}
595
596#[derive(Debug, Clone, Default)]
597pub struct LocalModuleResolutionCache {
598    resolve_results: HashMap<(String, String, ModuleResolveMode), Option<String>>,
599    module_format_results: HashMap<String, Option<LocalResolvedModuleFormat>>,
600    package_json_results: HashMap<String, Option<LocalPackageJson>>,
601    exists_results: HashMap<String, bool>,
602    stat_results: HashMap<String, Option<bool>>,
603}
604
605/// Read-only filesystem primitives the module resolver needs. The resolution
606/// algorithm itself is pure path algebra over these four operations; pointing
607/// it at a different backing store (host files vs. the kernel VFS) is purely a
608/// matter of supplying a different `ModuleFsReader`.
609///
610/// All paths are guest paths (e.g. `/root/node_modules/foo/index.js`). Symlink
611/// following is the reader's responsibility: `canonical_guest_path` must return
612/// the fully-resolved guest path (realpath), and `path_is_dir`/`path_exists`
613/// must follow symlinks the way real Node's `fs.stat`/`fs.existsSync` do.
614pub trait ModuleFsReader {
615    /// Realpath of `guest_path`, expressed as a guest path. `None` if the path
616    /// does not resolve (does not exist / escapes the addressable tree).
617    fn canonical_guest_path(&mut self, guest_path: &str) -> Option<String>;
618
619    /// Read the file at `guest_path` as a UTF-8 string, following symlinks.
620    fn read_to_string(&mut self, guest_path: &str) -> Option<String>;
621
622    /// `Some(true)` if `guest_path` is a directory, `Some(false)` if it exists
623    /// but is not a directory, `None` if it does not exist. Follows symlinks.
624    fn path_is_dir(&mut self, guest_path: &str) -> Option<bool>;
625
626    /// Whether `guest_path` exists, following symlinks.
627    fn path_exists(&mut self, guest_path: &str) -> bool;
628}
629
630/// Guest JavaScript module-resolution mode (the `moduleResolution` axis of
631/// `jsRuntime`). Defaults to full Node.js resolution.
632#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
633enum GuestModuleResolution {
634    /// node_modules ancestor-walk + exports/conditions + realpath.
635    #[default]
636    Node,
637    /// Relative/absolute ESM only; bare specifiers do not resolve.
638    Relative,
639    /// No resolution at all; every specifier (relative included) is denied.
640    None,
641}
642
643impl GuestModuleResolution {
644    fn from_env(env: &BTreeMap<String, String>) -> Self {
645        match env.get("AGENTOS_JS_MODULE_RESOLUTION").map(String::as_str) {
646            Some("relative") => Self::Relative,
647            Some("none") => Self::None,
648            _ => Self::Node,
649        }
650    }
651}
652
653#[derive(Default)]
654struct LocalBridgeState {
655    translator: GuestPathTranslator,
656    resolution_cache: LocalModuleResolutionCache,
657    /// jsRuntime module-resolution mode for this execution.
658    module_resolution: GuestModuleResolution,
659    handle_descriptions: HashMap<String, String>,
660    next_timer_id: u64,
661    timers: Arc<Mutex<HashMap<u64, LocalTimerEntry>>>,
662    kernel_stdin: Arc<LocalKernelStdinBridge>,
663    forward_kernel_stdin_rpc: bool,
664    v8_session: Option<V8SessionHandle>,
665    /// Optional read-only reader over the mounted `node_modules` VFS, supplied by
666    /// the sidecar. When present, the bridge thread resolves module-resolution
667    /// RPCs (`_resolveModule` / `_loadFile` / `_moduleFormat` /
668    /// `_batchResolveModules`) inline against this reader, concurrently with the
669    /// service loop — so a large cold-start module graph does not serialize
670    /// behind / starve the ACP bootstrap on the single service-loop thread.
671    /// `None` means "route module resolution to the service loop" (the kernel-VFS
672    /// fallback for callers that supply no reader).
673    module_reader: Option<Box<dyn ModuleFsReader + Send>>,
674}
675
676impl Drop for LocalBridgeState {
677    /// Tear down all tracked timers when the bridge state is dropped (which
678    /// happens when the event-bridge service loop exits on session termination —
679    /// success, error, or shutdown). Clearing the shared `timers` map cancels both
680    /// kernel and bridge timers: any in-flight wheel action that wakes afterwards
681    /// finds its entry gone and suppresses its callback via `timer_should_fire`,
682    /// so a destroyed session's timers do not fire after the fact.
683    fn drop(&mut self) {
684        if let Ok(mut timers) = self.timers.lock() {
685            timers.clear();
686        }
687    }
688}
689
690#[derive(Debug, Default)]
691struct LocalKernelStdinBridge {
692    state: Mutex<LocalKernelStdinState>,
693    ready: Condvar,
694}
695
696#[derive(Debug, Default)]
697struct LocalKernelStdinState {
698    bytes: VecDeque<u8>,
699    closed: bool,
700}
701
702#[derive(Debug, Clone, Default)]
703struct GuestPathTranslator {
704    implicit_guest_cwd: String,
705    implicit_host_cwd: PathBuf,
706    sandbox_root: Option<PathBuf>,
707    mappings: Vec<GuestPathMapping>,
708}
709
710#[derive(Debug, Clone, Deserialize, Default)]
711struct LocalPackageJson {
712    #[serde(default)]
713    name: Option<String>,
714    #[serde(default)]
715    main: Option<String>,
716    #[serde(default)]
717    #[serde(rename = "type")]
718    package_type: Option<String>,
719    #[serde(default)]
720    exports: Option<Value>,
721    #[serde(default)]
722    imports: Option<Value>,
723}
724
725#[derive(Debug, Clone, PartialEq, Eq)]
726struct LocalTimerEntry {
727    delay_ms: u64,
728    generation: u64,
729    repeat: bool,
730}
731
732#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
733#[serde(rename_all = "kebab-case")]
734enum PolyfillSourceKind {
735    NodeStdlibBrowser,
736    CustomBridge,
737    Denied,
738}
739
740#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
741#[serde(rename_all = "camelCase")]
742struct PolyfillRegistryGroup {
743    source: PolyfillSourceKind,
744    #[serde(default)]
745    error_code: Option<String>,
746    names: Vec<String>,
747}
748
749#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
750#[serde(rename_all = "camelCase")]
751struct PolyfillRegistry {
752    version: u32,
753    groups: Vec<PolyfillRegistryGroup>,
754}
755
756static POLYFILL_REGISTRY: OnceLock<PolyfillRegistry> = OnceLock::new();
757
758fn polyfill_registry() -> &'static PolyfillRegistry {
759    POLYFILL_REGISTRY.get_or_init(|| {
760        serde_json::from_str(include_str!("../assets/polyfill-registry.json"))
761            .expect("polyfill-registry.json must be valid")
762    })
763}
764
765#[derive(Debug, Clone, PartialEq)]
766enum LocalBridgeCallResult {
767    Immediate(Value),
768    Deferred,
769}
770
771/// Upper bound on guest-supplied timer delays, matching the JS `TIMEOUT_MAX`
772/// ceiling (`2**31 - 1` ms, ~24.8 days). Guest code can pass a delay up to
773/// `u64::MAX` ms; clamping keeps the timer wheel's deadline math and session
774/// handle lifetime within Node-compatible bounds.
775const MAX_TIMER_DELAY_MS: u64 = 2_147_483_647;
776
777fn timer_delay_ms(value: Option<&Value>) -> u64 {
778    let delay = match value {
779        Some(Value::Number(number)) => number.as_f64().unwrap_or(0.0),
780        Some(Value::String(text)) => text.parse::<f64>().unwrap_or(0.0),
781        _ => 0.0,
782    };
783
784    if !delay.is_finite() || delay <= 0.0 {
785        0
786    } else {
787        delay.floor().min(MAX_TIMER_DELAY_MS as f64) as u64
788    }
789}
790
791/// Decide whether a woken timer action should fire, and reclaim its tracking
792/// entry. Returns `false` (suppressing the callback) when the timer is gone from
793/// the map (cleared, or wiped on session teardown) or its generation no longer
794/// matches the one captured at scheduling time (re-armed/cancelled). A one-shot
795/// (`repeat == false`) timer that does fire is removed from the map so its id is
796/// reclaimed. Shared by the kernel-timer and bridge-timer paths so both honor the
797/// same cancellation semantics.
798fn timer_should_fire(
799    timers: &Arc<Mutex<HashMap<u64, LocalTimerEntry>>>,
800    timer_id: u64,
801    generation: u64,
802) -> bool {
803    timers
804        .lock()
805        .ok()
806        .and_then(|mut timers| {
807            let (current_generation, repeat) = timers
808                .get(&timer_id)
809                .map(|entry| (entry.generation, entry.repeat))?;
810            if current_generation != generation {
811                return Some(false);
812            }
813            if !repeat {
814                timers.remove(&timer_id);
815            }
816            Some(true)
817        })
818        .unwrap_or(false)
819}
820
821struct TimerWheel {
822    state: Mutex<TimerWheelState>,
823    ready: Condvar,
824}
825
826#[derive(Default)]
827struct TimerWheelState {
828    heap: BinaryHeap<Reverse<(Instant, u64)>>,
829    entries: HashMap<u64, TimerAction>,
830    next_seq: u64,
831}
832
833enum TimerAction {
834    StreamEvent {
835        session: V8SessionHandle,
836        timer_id: u64,
837        generation: u64,
838        timers: Arc<Mutex<HashMap<u64, LocalTimerEntry>>>,
839    },
840    BridgeResponse {
841        session: V8SessionHandle,
842        call_id: u64,
843        timer_id: u64,
844        generation: u64,
845        timers: Arc<Mutex<HashMap<u64, LocalTimerEntry>>>,
846    },
847}
848
849impl TimerAction {
850    fn execute(self) {
851        match self {
852            Self::StreamEvent {
853                session,
854                timer_id,
855                generation,
856                timers,
857            } => {
858                if !timer_should_fire(&timers, timer_id, generation) {
859                    return;
860                }
861
862                let payload =
863                    v8_runtime::json_to_cbor_payload(&json!(timer_id)).unwrap_or_default();
864                let _ = session.send_stream_event("timer", payload);
865            }
866            Self::BridgeResponse {
867                session,
868                call_id,
869                timer_id,
870                generation,
871                timers,
872            } => {
873                if !timer_should_fire(&timers, timer_id, generation) {
874                    return;
875                }
876                let _ = session.send_bridge_response(call_id, 0, Vec::new());
877            }
878        }
879    }
880}
881
882impl TimerWheel {
883    fn get() -> &'static Arc<Self> {
884        JAVASCRIPT_TIMER_WHEEL.get_or_init(Self::start)
885    }
886
887    fn start() -> Arc<Self> {
888        let wheel = Arc::new(Self {
889            state: Mutex::new(TimerWheelState::default()),
890            ready: Condvar::new(),
891        });
892        let worker = Arc::clone(&wheel);
893        // Detached daemon: queued entries carry generation-checked session handles,
894        // matching the previous fire-and-forget timer threads without one OS
895        // thread per guest timer.
896        if let Err(error) = thread::Builder::new()
897            .name(String::from("secure-exec-js-timer-wheel"))
898            .spawn(move || worker.run())
899        {
900            tracing::warn!(?error, "failed to start JavaScript timer wheel thread");
901        }
902        wheel
903    }
904
905    fn schedule(&self, delay_ms: u64, action: TimerAction) {
906        let now = Instant::now();
907        let deadline = now
908            .checked_add(Duration::from_millis(delay_ms))
909            .unwrap_or(now);
910        let mut state = self.lock_state();
911        let old_earliest = state.heap.peek().map(|Reverse((deadline, _))| *deadline);
912        let seq = state.next_seq;
913        state.next_seq = state.next_seq.wrapping_add(1);
914        state.heap.push(Reverse((deadline, seq)));
915        state.entries.insert(seq, action);
916        if old_earliest.is_none_or(|old| deadline < old) {
917            self.ready.notify_one();
918        }
919    }
920
921    fn run(&self) {
922        loop {
923            let due = {
924                let mut state = self.lock_state();
925                loop {
926                    match state.heap.peek().copied() {
927                        Some(Reverse((deadline, _))) => {
928                            let now = Instant::now();
929                            if deadline <= now {
930                                break;
931                            }
932                            let timeout = deadline.saturating_duration_since(now);
933                            state = match self.ready.wait_timeout(state, timeout) {
934                                Ok((state, _)) => state,
935                                Err(poisoned) => poisoned.into_inner().0,
936                            };
937                        }
938                        None => {
939                            state = match self.ready.wait(state) {
940                                Ok(state) => state,
941                                Err(poisoned) => poisoned.into_inner(),
942                            };
943                        }
944                    }
945                }
946
947                let now = Instant::now();
948                let mut due = Vec::new();
949                while let Some(Reverse((deadline, seq))) = state.heap.peek().copied() {
950                    if deadline > now {
951                        break;
952                    }
953                    state.heap.pop();
954                    if let Some(action) = state.entries.remove(&seq) {
955                        due.push(action);
956                    }
957                }
958                due
959            };
960
961            for action in due {
962                if catch_unwind(AssertUnwindSafe(|| action.execute())).is_err() {
963                    tracing::warn!("JavaScript timer wheel action panicked");
964                }
965            }
966        }
967    }
968
969    fn lock_state(&self) -> std::sync::MutexGuard<'_, TimerWheelState> {
970        match self.state.lock() {
971            Ok(state) => state,
972            Err(poisoned) => poisoned.into_inner(),
973        }
974    }
975}
976
977impl GuestPathTranslator {
978    fn from_host_context(
979        env: &BTreeMap<String, String>,
980        host_cwd: PathBuf,
981        guest_cwd: String,
982    ) -> Self {
983        let mut mappings = parse_guest_path_mappings_from_env(env)
984            .into_iter()
985            .filter(|mapping| mapping.guest_path.starts_with('/'))
986            .collect::<Vec<_>>();
987
988        if !mappings
989            .iter()
990            .any(|mapping| mapping.guest_path == guest_cwd && mapping.host_path == host_cwd)
991        {
992            mappings.push(GuestPathMapping {
993                guest_path: guest_cwd.clone(),
994                host_path: host_cwd.clone(),
995            });
996        }
997
998        sort_guest_path_mappings(&mut mappings);
999
1000        Self {
1001            implicit_guest_cwd: guest_cwd,
1002            implicit_host_cwd: host_cwd,
1003            sandbox_root: env
1004                .get(NODE_SANDBOX_ROOT_ENV)
1005                .filter(|value| Path::new(value.as_str()).is_absolute())
1006                .map(PathBuf::from),
1007            mappings,
1008        }
1009    }
1010
1011    fn is_known_host_path(&self, host_path: &Path) -> bool {
1012        if host_path.starts_with(&self.implicit_host_cwd) {
1013            return true;
1014        }
1015
1016        if let Some(sandbox_root) = &self.sandbox_root {
1017            if host_path.starts_with(sandbox_root) {
1018                return true;
1019            }
1020        }
1021
1022        self.mappings.iter().any(|mapping| {
1023            host_path.starts_with(&mapping.host_path)
1024                || fs::canonicalize(&mapping.host_path)
1025                    .map(|real_path| host_path.starts_with(real_path))
1026                    .unwrap_or(false)
1027        })
1028    }
1029
1030    fn from_request(request: &StartJavascriptExecutionRequest) -> Self {
1031        let implicit_guest_cwd = request
1032            .env
1033            .get("PWD")
1034            .filter(|value| value.starts_with('/'))
1035            .cloned()
1036            .or_else(|| {
1037                request
1038                    .env
1039                    .get("HOME")
1040                    .filter(|value| value.starts_with('/'))
1041                    .cloned()
1042            })
1043            .unwrap_or_else(|| String::from("/root"));
1044        let mut translator = Self::from_host_context(
1045            &request.env,
1046            request.cwd.clone(),
1047            implicit_guest_cwd.clone(),
1048        );
1049        translator.mappings.sort_by(|left, right| {
1050            let left_is_implicit =
1051                left.guest_path == implicit_guest_cwd && left.host_path == request.cwd;
1052            let right_is_implicit =
1053                right.guest_path == implicit_guest_cwd && right.host_path == request.cwd;
1054            right
1055                .guest_path
1056                .len()
1057                .cmp(&left.guest_path.len())
1058                .then_with(|| right_is_implicit.cmp(&left_is_implicit))
1059                .then_with(|| {
1060                    right
1061                        .host_path
1062                        .components()
1063                        .count()
1064                        .cmp(&left.host_path.components().count())
1065                })
1066        });
1067        translator
1068    }
1069
1070    fn guest_cwd(&self) -> &str {
1071        &self.implicit_guest_cwd
1072    }
1073
1074    fn resolve_host_entrypoint(&self, cwd: &Path, entrypoint: &str) -> PathBuf {
1075        if entrypoint == "-e" || entrypoint == "--eval" {
1076            return PathBuf::from(entrypoint);
1077        }
1078
1079        let path = Path::new(entrypoint);
1080        if path.is_absolute() {
1081            if self.is_known_host_path(path) {
1082                return path.to_path_buf();
1083            }
1084            self.guest_to_host(entrypoint)
1085                .unwrap_or_else(|| path.to_path_buf())
1086        } else {
1087            cwd.join(path)
1088        }
1089    }
1090
1091    fn host_to_guest_string(&self, host_path: &Path) -> String {
1092        if !host_path.is_absolute() {
1093            return normalize_guest_path(&host_path.to_string_lossy());
1094        }
1095
1096        for mapping in &self.mappings {
1097            if let Ok(stripped) = host_path.strip_prefix(&mapping.host_path) {
1098                return join_guest_path(
1099                    &mapping.guest_path,
1100                    &stripped.to_string_lossy().replace('\\', "/"),
1101                );
1102            }
1103            if let Ok(real_mapping_path) = fs::canonicalize(&mapping.host_path) {
1104                if let Ok(stripped) = host_path.strip_prefix(&real_mapping_path) {
1105                    return join_guest_path(
1106                        &mapping.guest_path,
1107                        &stripped.to_string_lossy().replace('\\', "/"),
1108                    );
1109                }
1110            }
1111        }
1112
1113        if let Ok(stripped) = host_path.strip_prefix(&self.implicit_host_cwd) {
1114            return join_guest_path(
1115                &self.implicit_guest_cwd,
1116                &stripped.to_string_lossy().replace('\\', "/"),
1117            );
1118        }
1119
1120        if let Some(sandbox_root) = &self.sandbox_root {
1121            if let Ok(stripped) = host_path.strip_prefix(sandbox_root) {
1122                return join_guest_path("/", &stripped.to_string_lossy().replace('\\', "/"));
1123            }
1124        }
1125
1126        let basename = host_path
1127            .file_name()
1128            .and_then(|value| value.to_str())
1129            .unwrap_or("unknown");
1130        join_guest_path("/unknown", basename)
1131    }
1132
1133    fn guest_to_host(&self, guest_path: &str) -> Option<PathBuf> {
1134        let normalized = normalize_guest_path(guest_path);
1135        let mut fallback_candidate = None;
1136
1137        for mapping in &self.mappings {
1138            if let Some(suffix) = strip_guest_prefix(&normalized, &mapping.guest_path) {
1139                let candidate = join_host_path(&mapping.host_path, suffix);
1140                if candidate.exists() {
1141                    return self.confine_host_path(candidate);
1142                }
1143                if let Ok(real_mapping_path) = fs::canonicalize(&mapping.host_path) {
1144                    let real_candidate = join_host_path(&real_mapping_path, suffix);
1145                    if real_candidate.exists() {
1146                        return self.confine_host_path(real_candidate);
1147                    }
1148                    if let Some(sibling_candidate) =
1149                        resolve_pnpm_sibling_host_path(&real_mapping_path, suffix)
1150                    {
1151                        return self.confine_host_path(sibling_candidate);
1152                    }
1153                }
1154                fallback_candidate.get_or_insert(candidate);
1155            }
1156        }
1157        if let Some(suffix) = strip_guest_prefix(&normalized, &self.implicit_guest_cwd) {
1158            return self.confine_host_path(join_host_path(&self.implicit_host_cwd, suffix));
1159        }
1160
1161        if let Some(candidate) = fallback_candidate {
1162            return self.confine_host_path(candidate);
1163        }
1164
1165        if let Some(sandbox_root) = &self.sandbox_root {
1166            return self.confine_host_path(join_host_path(
1167                sandbox_root,
1168                normalized.trim_start_matches('/'),
1169            ));
1170        }
1171
1172        None
1173    }
1174
1175    fn confine_host_path(&self, host_path: PathBuf) -> Option<PathBuf> {
1176        let allowed_roots = self.allowed_canonical_host_roots();
1177        if allowed_roots.is_empty() {
1178            return None;
1179        }
1180
1181        if let Ok(canonical_path) = fs::canonicalize(&host_path) {
1182            return canonical_path_is_allowed(&canonical_path, &allowed_roots).then_some(host_path);
1183        }
1184
1185        let existing_ancestor = nearest_existing_host_ancestor(&host_path)?;
1186        let canonical_ancestor = fs::canonicalize(existing_ancestor).ok()?;
1187        canonical_path_is_allowed(&canonical_ancestor, &allowed_roots).then_some(host_path)
1188    }
1189
1190    fn allowed_canonical_host_roots(&self) -> Vec<PathBuf> {
1191        let mut roots = Vec::new();
1192        for root in self
1193            .mappings
1194            .iter()
1195            .map(|mapping| mapping.host_path.as_path())
1196            .chain(std::iter::once(self.implicit_host_cwd.as_path()))
1197            .chain(self.sandbox_root.as_deref())
1198        {
1199            if let Ok(canonical_root) = fs::canonicalize(root) {
1200                if !roots.iter().any(|existing| existing == &canonical_root) {
1201                    roots.push(canonical_root);
1202                }
1203            }
1204        }
1205        roots
1206    }
1207
1208    fn canonical_guest_path(&self, guest_path: &str) -> Option<String> {
1209        let host_path = self.guest_to_host(guest_path)?;
1210        let canonical = fs::canonicalize(host_path).ok()?;
1211        for mapping in &self.mappings {
1212            if strip_guest_prefix(guest_path, &mapping.guest_path).is_none() {
1213                continue;
1214            }
1215            if let Ok(stripped) = canonical.strip_prefix(&mapping.host_path) {
1216                return Some(join_guest_path(
1217                    &mapping.guest_path,
1218                    &stripped.to_string_lossy().replace('\\', "/"),
1219                ));
1220            }
1221            if let Ok(real_mapping_path) = fs::canonicalize(&mapping.host_path) {
1222                if let Ok(stripped) = canonical.strip_prefix(&real_mapping_path) {
1223                    return Some(join_guest_path(
1224                        &mapping.guest_path,
1225                        &stripped.to_string_lossy().replace('\\', "/"),
1226                    ));
1227                }
1228            }
1229        }
1230        if let Some(node_modules_root) = self
1231            .mappings
1232            .iter()
1233            .find(|mapping| mapping.guest_path == "/root/node_modules")
1234        {
1235            if let Ok(stripped) = canonical.strip_prefix(&node_modules_root.host_path) {
1236                return Some(join_guest_path(
1237                    &node_modules_root.guest_path,
1238                    &stripped.to_string_lossy().replace('\\', "/"),
1239                ));
1240            }
1241            if let Ok(real_root) = fs::canonicalize(&node_modules_root.host_path) {
1242                if let Ok(stripped) = canonical.strip_prefix(&real_root) {
1243                    return Some(join_guest_path(
1244                        &node_modules_root.guest_path,
1245                        &stripped.to_string_lossy().replace('\\', "/"),
1246                    ));
1247                }
1248            }
1249        }
1250        let guest = self.host_to_guest_string(&canonical);
1251        (!guest.starts_with("/unknown/")).then_some(normalize_guest_path(&guest))
1252    }
1253}
1254
1255fn sort_guest_path_mappings(mappings: &mut [GuestPathMapping]) {
1256    mappings.sort_by(|left, right| {
1257        right
1258            .guest_path
1259            .len()
1260            .cmp(&left.guest_path.len())
1261            .then_with(|| {
1262                right
1263                    .host_path
1264                    .components()
1265                    .count()
1266                    .cmp(&left.host_path.components().count())
1267            })
1268    });
1269}
1270
1271fn canonical_path_is_allowed(path: &Path, allowed_roots: &[PathBuf]) -> bool {
1272    allowed_roots
1273        .iter()
1274        .any(|root| path == root || path.starts_with(root))
1275}
1276
1277fn nearest_existing_host_ancestor(path: &Path) -> Option<&Path> {
1278    let mut candidate = Some(path);
1279    while let Some(current) = candidate {
1280        if fs::symlink_metadata(current).is_ok() {
1281            return Some(current);
1282        }
1283        candidate = current.parent();
1284    }
1285    None
1286}
1287
1288#[doc(hidden)]
1289pub struct ModuleResolutionTestHarness {
1290    local_bridge: LocalBridgeState,
1291}
1292
1293impl ModuleResolutionTestHarness {
1294    pub fn new(host_root: impl Into<PathBuf>) -> Self {
1295        let host_root = host_root.into();
1296        let mut mappings = vec![
1297            GuestPathMapping {
1298                guest_path: String::from("/root/node_modules"),
1299                host_path: host_root.join("node_modules"),
1300            },
1301            GuestPathMapping {
1302                guest_path: String::from("/root"),
1303                host_path: host_root.clone(),
1304            },
1305        ];
1306        sort_guest_path_mappings(&mut mappings);
1307
1308        // Build via default + in-place assignment rather than `..default()`:
1309        // LocalBridgeState implements Drop (to cancel timers on session teardown),
1310        // and functional-record-update would move fields out of a Drop type (E0509).
1311        let mut local_bridge = LocalBridgeState::default();
1312        local_bridge.translator = GuestPathTranslator {
1313            implicit_guest_cwd: String::from("/root"),
1314            implicit_host_cwd: host_root,
1315            sandbox_root: None,
1316            mappings,
1317        };
1318        Self { local_bridge }
1319    }
1320
1321    pub fn resolve_import(&mut self, specifier: &str, from_path: &str) -> Option<String> {
1322        self.local_bridge
1323            .resolve_module(specifier, from_path, ModuleResolveMode::Import)
1324    }
1325
1326    pub fn resolve_require(&mut self, specifier: &str, from_path: &str) -> Option<String> {
1327        self.local_bridge
1328            .resolve_module(specifier, from_path, ModuleResolveMode::Require)
1329    }
1330
1331    pub fn module_format(&mut self, path: &str) -> Option<&'static str> {
1332        self.local_bridge
1333            .module_format(path)
1334            .map(LocalResolvedModuleFormat::as_str)
1335    }
1336}
1337
1338#[doc(hidden)]
1339pub fn handle_internal_bridge_call_from_host_context(
1340    host_cwd: &Path,
1341    guest_cwd: &str,
1342    env: &BTreeMap<String, String>,
1343    method: &str,
1344    args: &[Value],
1345) -> Option<Value> {
1346    // default + in-place assign: LocalBridgeState is Drop, so `..default()` (E0509)
1347    // is not allowed.
1348    let mut local_bridge = LocalBridgeState::default();
1349    local_bridge.translator =
1350        GuestPathTranslator::from_host_context(env, host_cwd.to_path_buf(), guest_cwd.to_owned());
1351
1352    match local_bridge.handle_internal_bridge_call(0, method, args) {
1353        Some(LocalBridgeCallResult::Immediate(value)) => Some(value),
1354        _ => None,
1355    }
1356}
1357
1358fn resolve_pnpm_sibling_host_path(real_mapping_path: &Path, suffix: &str) -> Option<PathBuf> {
1359    let trimmed = suffix.strip_prefix("node_modules/")?;
1360    let mut current = Some(real_mapping_path);
1361    while let Some(path) = current {
1362        if path.file_name().and_then(|name| name.to_str()) == Some("node_modules") {
1363            let candidate = join_host_path(path, trimmed);
1364            if candidate.exists() {
1365                return Some(candidate);
1366            }
1367            break;
1368        }
1369        current = path.parent();
1370    }
1371    None
1372}
1373
1374fn parse_guest_path_mappings(request: &StartJavascriptExecutionRequest) -> Vec<GuestPathMapping> {
1375    parse_guest_path_mappings_from_env(&request.env)
1376}
1377
1378fn parse_guest_path_mappings_from_env(env: &BTreeMap<String, String>) -> Vec<GuestPathMapping> {
1379    env.get(NODE_GUEST_PATH_MAPPINGS_ENV)
1380        .and_then(|value| serde_json::from_str::<Vec<GuestPathMappingWire>>(value).ok())
1381        .into_iter()
1382        .flatten()
1383        .map(|mapping| GuestPathMapping {
1384            guest_path: normalize_guest_path(&mapping.guest_path),
1385            host_path: PathBuf::from(mapping.host_path),
1386        })
1387        .collect()
1388}
1389
1390fn normalize_guest_path(path: &str) -> String {
1391    let mut segments = Vec::new();
1392    let absolute = path.starts_with('/');
1393    for segment in path.split('/') {
1394        match segment {
1395            "" | "." => {}
1396            ".." => {
1397                segments.pop();
1398            }
1399            other => segments.push(other),
1400        }
1401    }
1402    if !absolute {
1403        return segments.join("/");
1404    }
1405    if segments.is_empty() {
1406        String::from("/")
1407    } else {
1408        format!("/{}", segments.join("/"))
1409    }
1410}
1411
1412fn join_guest_path(base: &str, suffix: &str) -> String {
1413    if suffix.is_empty() || suffix == "." {
1414        return normalize_guest_path(base);
1415    }
1416    let trimmed = suffix.trim_start_matches('/');
1417    normalize_guest_path(&format!("{}/{}", base.trim_end_matches('/'), trimmed))
1418}
1419
1420fn strip_guest_prefix<'a>(path: &'a str, prefix: &str) -> Option<&'a str> {
1421    if prefix == "/" {
1422        return path.strip_prefix('/');
1423    }
1424    if path == prefix {
1425        return Some("");
1426    }
1427    path.strip_prefix(prefix)
1428        .and_then(|suffix| suffix.strip_prefix('/'))
1429}
1430
1431fn join_host_path(base: &Path, suffix: &str) -> PathBuf {
1432    if suffix.is_empty() {
1433        return base.to_path_buf();
1434    }
1435    let mut joined = base.to_path_buf();
1436    for segment in suffix.split('/') {
1437        if segment.is_empty() || segment == "." {
1438            continue;
1439        }
1440        if segment == ".." {
1441            joined.pop();
1442        } else {
1443            joined.push(segment);
1444        }
1445    }
1446    joined
1447}
1448
1449fn translate_v8_bridge_value_to_legacy(value: &Value) -> Value {
1450    match value {
1451        Value::Array(values) => Value::Array(
1452            values
1453                .iter()
1454                .map(translate_v8_bridge_value_to_legacy)
1455                .collect(),
1456        ),
1457        Value::Object(map) if map.get("__type").and_then(Value::as_str) == Some("Buffer") => {
1458            json!({
1459                "__agentOSType": "bytes",
1460                "base64": map.get("data").cloned().unwrap_or(Value::String(String::new())),
1461            })
1462        }
1463        Value::Object(map) => Value::Object(
1464            map.iter()
1465                .map(|(key, value)| (key.clone(), translate_v8_bridge_value_to_legacy(value)))
1466                .collect(),
1467        ),
1468        other => other.clone(),
1469    }
1470}
1471
1472fn translate_request_args_for_legacy(method: &str, args: &[Value]) -> Vec<Value> {
1473    let mut translated = args
1474        .iter()
1475        .map(translate_v8_bridge_value_to_legacy)
1476        .collect::<Vec<_>>();
1477
1478    if matches!(method, "fs.writeFileSync" | "fs.promises.writeFile") {
1479        if let Some(Value::String(data)) = translated.get(1) {
1480            translated[1] = json!({
1481                "__agentOSType": "bytes",
1482                "base64": v8_runtime::base64_encode_pub(data.as_bytes()),
1483            });
1484        }
1485    }
1486
1487    translated
1488}
1489
1490fn translate_legacy_bridge_value_to_v8(value: &Value) -> Value {
1491    match value {
1492        Value::Array(values) => Value::Array(
1493            values
1494                .iter()
1495                .map(translate_legacy_bridge_value_to_v8)
1496                .collect(),
1497        ),
1498        Value::Object(map) if map.get("__agentOSType").and_then(Value::as_str) == Some("bytes") => {
1499            json!({
1500                "__type": "Buffer",
1501                "data": map.get("base64").cloned().unwrap_or(Value::String(String::new())),
1502            })
1503        }
1504        Value::Object(map) => Value::Object(
1505            map.iter()
1506                .map(|(key, value)| (key.clone(), translate_legacy_bridge_value_to_v8(value)))
1507                .collect(),
1508        ),
1509        other => other.clone(),
1510    }
1511}
1512
1513fn decode_bridge_output_arg(value: &Value) -> Vec<u8> {
1514    match value {
1515        Value::String(s) => s.as_bytes().to_vec(),
1516        Value::Object(map)
1517            if map.get("__type").and_then(Value::as_str) == Some("Buffer")
1518                || map.get("__agentOSType").and_then(Value::as_str) == Some("bytes") =>
1519        {
1520            let base64_value = map
1521                .get("data")
1522                .or_else(|| map.get("base64"))
1523                .and_then(Value::as_str);
1524            if let Some(base64_value) = base64_value {
1525                if let Some(bytes) = v8_runtime::base64_decode_pub(base64_value) {
1526                    return bytes;
1527                }
1528            }
1529            value.to_string().into_bytes()
1530        }
1531        other => other.to_string().into_bytes(),
1532    }
1533}
1534
1535fn decode_bridge_output_args(args: &[Value]) -> Vec<u8> {
1536    let mut output = Vec::new();
1537    for (index, arg) in args.iter().enumerate() {
1538        if index > 0 {
1539            output.push(b' ');
1540        }
1541        output.extend(decode_bridge_output_arg(arg));
1542    }
1543    output
1544}
1545
1546#[derive(Debug)]
1547pub enum JavascriptExecutionError {
1548    EmptyArgv,
1549    MissingContext(String),
1550    VmMismatch { expected: String, found: String },
1551    PrepareImportCache(std::io::Error),
1552    Spawn(std::io::Error),
1553    PendingSyncRpcRequest(u64),
1554    ExpiredSyncRpcRequest(u64),
1555    RpcResponse(String),
1556    Terminate(std::io::Error),
1557    StdinClosed,
1558    Stdin(std::io::Error),
1559    OutputBufferExceeded { stream: &'static str, limit: usize },
1560    EventChannelClosed,
1561}
1562
1563impl fmt::Display for JavascriptExecutionError {
1564    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1565        match self {
1566            Self::EmptyArgv => f.write_str("guest JavaScript execution requires argv[0]"),
1567            Self::MissingContext(context_id) => {
1568                write!(f, "unknown guest JavaScript context: {context_id}")
1569            }
1570            Self::VmMismatch { expected, found } => {
1571                write!(
1572                    f,
1573                    "guest JavaScript context belongs to vm {expected}, not {found}"
1574                )
1575            }
1576            Self::PrepareImportCache(err) => {
1577                write!(
1578                    f,
1579                    "failed to prepare sidecar-scoped Node import cache: {err}"
1580                )
1581            }
1582            Self::Spawn(err) => write!(f, "failed to start guest JavaScript runtime: {err}"),
1583            Self::PendingSyncRpcRequest(id) => {
1584                write!(
1585                    f,
1586                    "guest JavaScript execution requires servicing pending sync RPC request {id}"
1587                )
1588            }
1589            Self::ExpiredSyncRpcRequest(id) => {
1590                write!(f, "sync RPC request {id} is no longer pending")
1591            }
1592            Self::RpcResponse(message) => {
1593                write!(
1594                    f,
1595                    "failed to reply to guest JavaScript sync RPC request: {message}"
1596                )
1597            }
1598            Self::Terminate(err) => {
1599                write!(f, "failed to terminate guest JavaScript runtime: {err}")
1600            }
1601            Self::StdinClosed => f.write_str("guest JavaScript stdin is already closed"),
1602            Self::Stdin(err) => write!(f, "failed to write guest stdin: {err}"),
1603            Self::OutputBufferExceeded { stream, limit } => {
1604                write!(
1605                    f,
1606                    "guest JavaScript {stream} exceeded the captured output limit of {limit} bytes"
1607                )
1608            }
1609            Self::EventChannelClosed => {
1610                f.write_str("guest JavaScript event channel closed unexpectedly")
1611            }
1612        }
1613    }
1614}
1615
1616impl std::error::Error for JavascriptExecutionError {}
1617
1618#[derive(Debug)]
1619pub struct JavascriptExecution {
1620    execution_id: String,
1621    child_pid: u32,
1622    events: tokio::sync::Mutex<TokioReceiver<JavascriptExecutionEvent>>,
1623    pending_sync_rpc: Arc<Mutex<Option<PendingSyncRpcState>>>,
1624    kernel_stdin: Arc<LocalKernelStdinBridge>,
1625    _import_cache_guard: Arc<NodeImportCacheCleanup>,
1626    v8_session: V8SessionHandle,
1627    /// Host-direct module resolver state, used ONLY by the standalone `wait()`
1628    /// loop. The real VM runtime resolves modules against the kernel VFS on the
1629    /// sidecar service loop and never reaches this; but `wait()` runs without a
1630    /// kernel (dev/test harness), so it services module-resolution sync RPCs
1631    /// host-directly from the request's path translator.
1632    module_resolution: Mutex<(GuestPathTranslator, LocalModuleResolutionCache)>,
1633}
1634
1635impl JavascriptExecution {
1636    pub fn execution_id(&self) -> &str {
1637        &self.execution_id
1638    }
1639
1640    pub fn child_pid(&self) -> u32 {
1641        self.child_pid
1642    }
1643
1644    pub fn v8_session_handle(&self) -> V8SessionHandle {
1645        self.v8_session.clone()
1646    }
1647
1648    pub fn uses_shared_v8_runtime(&self) -> bool {
1649        true
1650    }
1651
1652    pub fn write_stdin(&mut self, chunk: &[u8]) -> Result<(), JavascriptExecutionError> {
1653        self.kernel_stdin.write(chunk)?;
1654        let payload = v8_runtime::json_to_cbor_payload(&json!({
1655            "dataBase64": v8_runtime::base64_encode_pub(chunk),
1656        }))
1657        .map_err(JavascriptExecutionError::Stdin)?;
1658        self.v8_session
1659            .send_stream_event("stdin", payload)
1660            .map_err(JavascriptExecutionError::Stdin)
1661    }
1662
1663    pub fn close_stdin(&mut self) -> Result<(), JavascriptExecutionError> {
1664        self.kernel_stdin.close();
1665        let _ = self.v8_session.send_stream_event("stdin_end", vec![]);
1666        Ok(())
1667    }
1668
1669    pub(crate) fn write_kernel_stdin_only(
1670        &mut self,
1671        chunk: &[u8],
1672    ) -> Result<(), JavascriptExecutionError> {
1673        self.kernel_stdin.write(chunk)
1674    }
1675
1676    pub(crate) fn close_kernel_stdin_only(&mut self) {
1677        self.kernel_stdin.close();
1678    }
1679
1680    pub fn read_kernel_stdin_sync_rpc(
1681        &self,
1682        request: &JavascriptSyncRpcRequest,
1683    ) -> Result<Value, JavascriptExecutionError> {
1684        if request.method != "__kernel_stdin_read" {
1685            return Ok(Value::Null);
1686        }
1687
1688        Ok(self.kernel_stdin.read(&request.args))
1689    }
1690
1691    pub(crate) fn handle_kernel_stdin_sync_rpc(
1692        &mut self,
1693        request: &JavascriptSyncRpcRequest,
1694    ) -> Result<bool, JavascriptExecutionError> {
1695        if request.method != "__kernel_stdin_read" {
1696            return Ok(false);
1697        }
1698
1699        let response = self.kernel_stdin.read(&request.args);
1700        self.respond_sync_rpc_success(request.id, response)?;
1701        Ok(true)
1702    }
1703
1704    pub fn terminate(&self) -> Result<(), JavascriptExecutionError> {
1705        self.v8_session
1706            .terminate()
1707            .map_err(JavascriptExecutionError::Terminate)
1708    }
1709
1710    pub fn send_stream_event(
1711        &self,
1712        event_type: &str,
1713        payload: Value,
1714    ) -> Result<(), JavascriptExecutionError> {
1715        let payload = v8_runtime::json_to_cbor_payload(&payload)
1716            .map_err(|error| JavascriptExecutionError::RpcResponse(error.to_string()))?;
1717        self.v8_session
1718            .send_stream_event(event_type, payload)
1719            .map_err(|error| JavascriptExecutionError::RpcResponse(error.to_string()))
1720    }
1721
1722    pub fn respond_sync_rpc_success(
1723        &mut self,
1724        id: u64,
1725        result: Value,
1726    ) -> Result<(), JavascriptExecutionError> {
1727        let phase_start = Instant::now();
1728        match self.clear_pending_sync_rpc(id)? {
1729            PendingSyncRpcResolution::Pending => {}
1730            PendingSyncRpcResolution::TimedOut => {
1731                return Err(JavascriptExecutionError::ExpiredSyncRpcRequest(id));
1732            }
1733            PendingSyncRpcResolution::Missing => {}
1734        }
1735        record_sync_bridge_phase(
1736            "sync_rpc_response",
1737            "response_clear_pending",
1738            phase_start.elapsed(),
1739        );
1740
1741        let phase_start = Instant::now();
1742        let payload = translate_legacy_bridge_value_to_v8(&result);
1743        record_sync_bridge_phase(
1744            "sync_rpc_response",
1745            "response_translate_value",
1746            phase_start.elapsed(),
1747        );
1748        let phase_start = Instant::now();
1749        let payload = v8_runtime::json_to_cbor_payload(&payload)
1750            .map_err(|e| JavascriptExecutionError::RpcResponse(e.to_string()))?;
1751        record_sync_bridge_phase(
1752            "sync_rpc_response",
1753            "response_encode_cbor",
1754            phase_start.elapsed(),
1755        );
1756        let phase_start = Instant::now();
1757        let result = self
1758            .v8_session
1759            .send_bridge_response(id, 0, payload)
1760            .map_err(|e| JavascriptExecutionError::RpcResponse(e.to_string()));
1761        record_sync_bridge_phase("sync_rpc_response", "response_send", phase_start.elapsed());
1762        result
1763    }
1764
1765    pub fn respond_sync_rpc_raw_success(
1766        &mut self,
1767        id: u64,
1768        payload: Vec<u8>,
1769    ) -> Result<(), JavascriptExecutionError> {
1770        let phase_start = Instant::now();
1771        match self.clear_pending_sync_rpc(id)? {
1772            PendingSyncRpcResolution::Pending => {}
1773            PendingSyncRpcResolution::TimedOut => {
1774                return Err(JavascriptExecutionError::ExpiredSyncRpcRequest(id));
1775            }
1776            PendingSyncRpcResolution::Missing => {}
1777        }
1778        record_sync_bridge_phase(
1779            "sync_rpc_raw_response",
1780            "response_clear_pending",
1781            phase_start.elapsed(),
1782        );
1783
1784        let phase_start = Instant::now();
1785        let result = self
1786            .v8_session
1787            .send_bridge_response(id, 2, payload)
1788            .map_err(|e| JavascriptExecutionError::RpcResponse(e.to_string()));
1789        record_sync_bridge_phase(
1790            "sync_rpc_raw_response",
1791            "response_send",
1792            phase_start.elapsed(),
1793        );
1794        result
1795    }
1796
1797    pub fn respond_sync_rpc_error(
1798        &mut self,
1799        id: u64,
1800        code: impl Into<String>,
1801        message: impl Into<String>,
1802    ) -> Result<(), JavascriptExecutionError> {
1803        match self.clear_pending_sync_rpc(id)? {
1804            PendingSyncRpcResolution::Pending => {}
1805            PendingSyncRpcResolution::TimedOut => {
1806                return Err(JavascriptExecutionError::ExpiredSyncRpcRequest(id));
1807            }
1808            PendingSyncRpcResolution::Missing => {}
1809        }
1810
1811        let error_msg = format!("{}: {}", code.into(), message.into());
1812        self.v8_session
1813            .send_bridge_response(id, 1, error_msg.into_bytes())
1814            .map_err(|e| JavascriptExecutionError::RpcResponse(e.to_string()))
1815    }
1816
1817    pub async fn poll_event(
1818        &self,
1819        timeout: Duration,
1820    ) -> Result<Option<JavascriptExecutionEvent>, JavascriptExecutionError> {
1821        if timeout.is_zero() {
1822            let mut events = self.events.lock().await;
1823            return match events.try_recv() {
1824                Ok(event) => Ok(Some(event)),
1825                Err(TokioTryRecvError::Empty) => Ok(None),
1826                Err(TokioTryRecvError::Disconnected) => {
1827                    Err(JavascriptExecutionError::EventChannelClosed)
1828                }
1829            };
1830        }
1831
1832        let mut events = self.events.lock().await;
1833        match time::timeout(timeout, events.recv()).await {
1834            Ok(Some(event)) => Ok(Some(event)),
1835            Ok(None) => Err(JavascriptExecutionError::EventChannelClosed),
1836            Err(_) => Ok(None),
1837        }
1838    }
1839
1840    pub fn poll_event_blocking(
1841        &self,
1842        timeout: Duration,
1843    ) -> Result<Option<JavascriptExecutionEvent>, JavascriptExecutionError> {
1844        let deadline = Instant::now() + timeout;
1845        loop {
1846            if let Ok(mut events) = self.events.try_lock() {
1847                match events.try_recv() {
1848                    Ok(event) => return Ok(Some(event)),
1849                    Err(TokioTryRecvError::Disconnected) => {
1850                        return Err(JavascriptExecutionError::EventChannelClosed);
1851                    }
1852                    Err(TokioTryRecvError::Empty) => {
1853                        if Instant::now() >= deadline {
1854                            return Ok(None);
1855                        }
1856                    }
1857                }
1858            }
1859
1860            if Instant::now() >= deadline {
1861                return Ok(None);
1862            }
1863            thread::sleep(Duration::from_millis(1));
1864        }
1865    }
1866
1867    pub fn wait(mut self) -> Result<JavascriptExecutionResult, JavascriptExecutionError> {
1868        self.close_stdin()?;
1869        let mut events = std::mem::replace(
1870            self.events.get_mut(),
1871            channel(JAVASCRIPT_EVENT_CHANNEL_CAPACITY).1,
1872        );
1873        let execution_id = std::mem::take(&mut self.execution_id);
1874
1875        let mut stdout = Vec::new();
1876        let mut stderr = Vec::new();
1877
1878        loop {
1879            match events.blocking_recv() {
1880                Some(JavascriptExecutionEvent::Stdout(chunk)) => {
1881                    append_captured_output(&mut stdout, chunk, "stdout")?;
1882                }
1883                Some(JavascriptExecutionEvent::Stderr(chunk)) => {
1884                    append_captured_output(&mut stderr, chunk, "stderr")?;
1885                }
1886                Some(JavascriptExecutionEvent::SyncRpcRequest(request)) => {
1887                    // The standalone engine has no kernel/service loop. Service
1888                    // module-resolution RPCs host-directly (the only FS source
1889                    // available here) so `wait()` does not deadlock; everything
1890                    // else is unsupported off the VM path.
1891                    if self.try_service_standalone_module_sync_rpc(&request)? {
1892                        continue;
1893                    }
1894                    return Err(JavascriptExecutionError::PendingSyncRpcRequest(request.id));
1895                }
1896                Some(JavascriptExecutionEvent::SignalState { .. }) => {}
1897                Some(JavascriptExecutionEvent::Exited(exit_code)) => {
1898                    return Ok(JavascriptExecutionResult {
1899                        execution_id,
1900                        exit_code,
1901                        stdout,
1902                        stderr,
1903                    });
1904                }
1905                None => return Err(JavascriptExecutionError::EventChannelClosed),
1906            }
1907        }
1908    }
1909
1910    /// Service a module-resolution sync RPC host-directly, for consumers that
1911    /// drive the V8 bridge without a kernel/service loop (the standalone
1912    /// `wait()` loop and the Python/WASM prewarm loops). Uses this execution's
1913    /// own path translator (captured at start, including any runtime path
1914    /// mappings) and a persistent cache. Returns `Ok(true)` if the request was a
1915    /// module method and was answered, `Ok(false)` if it should fall through.
1916    ///
1917    /// The real VM runtime resolves modules against the kernel VFS on the
1918    /// sidecar service loop and never calls this.
1919    pub fn try_service_standalone_module_sync_rpc(
1920        &mut self,
1921        request: &JavascriptSyncRpcRequest,
1922    ) -> Result<bool, JavascriptExecutionError> {
1923        let result = {
1924            let mut guard = self.module_resolution.lock().map_err(|_| {
1925                JavascriptExecutionError::RpcResponse(String::from(
1926                    "standalone module resolution state poisoned",
1927                ))
1928            })?;
1929            let (translator, cache) = &mut *guard;
1930            let mut resolver = ModuleResolver::new(translator, cache);
1931            match request.method.as_str() {
1932                "__resolve_module" | "_resolveModule" | "_resolveModuleSync" => {
1933                    let specifier = request.args.first().and_then(Value::as_str).unwrap_or("");
1934                    let parent = request.args.get(1).and_then(Value::as_str).unwrap_or("/");
1935                    let mode = match request.args.get(2).and_then(Value::as_str) {
1936                        Some("import") => ModuleResolveMode::Import,
1937                        Some("require") => ModuleResolveMode::Require,
1938                        _ if request.method == "_resolveModuleSync" => ModuleResolveMode::Require,
1939                        _ => ModuleResolveMode::Import,
1940                    };
1941                    resolver
1942                        .resolve_module(specifier, parent, mode)
1943                        .map(Value::String)
1944                        .unwrap_or(Value::Null)
1945                }
1946                "__load_file" | "_loadFile" | "_loadFileSync" => resolver
1947                    .load_file(request.args.first().and_then(Value::as_str).unwrap_or(""))
1948                    .map(Value::String)
1949                    .unwrap_or(Value::Null),
1950                "__module_format" | "_moduleFormat" => resolver
1951                    .module_format(request.args.first().and_then(Value::as_str).unwrap_or(""))
1952                    .map(|format| Value::String(String::from(format.as_str())))
1953                    .unwrap_or(Value::Null),
1954                "__batch_resolve_modules" | "_batchResolveModules" => {
1955                    resolver.batch_resolve_modules(&request.args)
1956                }
1957                _ => return Ok(false),
1958            }
1959        };
1960        self.respond_sync_rpc_success(request.id, result)?;
1961        Ok(true)
1962    }
1963
1964    fn clear_pending_sync_rpc(
1965        &self,
1966        id: u64,
1967    ) -> Result<PendingSyncRpcResolution, JavascriptExecutionError> {
1968        let mut pending = self.pending_sync_rpc.lock().map_err(|_| {
1969            JavascriptExecutionError::RpcResponse(String::from(
1970                "sync RPC pending-request state lock poisoned",
1971            ))
1972        })?;
1973        match *pending {
1974            Some(PendingSyncRpcState::Pending(current)) if current == id => {
1975                *pending = None;
1976                Ok(PendingSyncRpcResolution::Pending)
1977            }
1978            Some(PendingSyncRpcState::TimedOut(current)) if current == id => {
1979                Ok(PendingSyncRpcResolution::TimedOut)
1980            }
1981            _ => Ok(PendingSyncRpcResolution::Missing),
1982        }
1983    }
1984}
1985
1986impl Drop for JavascriptExecution {
1987    fn drop(&mut self) {
1988        let _ = self.v8_session.destroy();
1989    }
1990}
1991
1992fn append_captured_output(
1993    target: &mut Vec<u8>,
1994    chunk: Vec<u8>,
1995    stream: &'static str,
1996) -> Result<(), JavascriptExecutionError> {
1997    let next_len = target.len().checked_add(chunk.len()).ok_or(
1998        JavascriptExecutionError::OutputBufferExceeded {
1999            stream,
2000            limit: JAVASCRIPT_CAPTURED_OUTPUT_LIMIT_BYTES,
2001        },
2002    )?;
2003    if next_len > JAVASCRIPT_CAPTURED_OUTPUT_LIMIT_BYTES {
2004        return Err(JavascriptExecutionError::OutputBufferExceeded {
2005            stream,
2006            limit: JAVASCRIPT_CAPTURED_OUTPUT_LIMIT_BYTES,
2007        });
2008    }
2009
2010    target.extend(chunk);
2011    Ok(())
2012}
2013
2014struct V8SessionRegistrationGuard<'a> {
2015    v8_host: &'a V8RuntimeHost,
2016    session_id: String,
2017    active: bool,
2018}
2019
2020impl<'a> V8SessionRegistrationGuard<'a> {
2021    fn new(v8_host: &'a V8RuntimeHost, session_id: String) -> Self {
2022        Self {
2023            v8_host,
2024            session_id,
2025            active: true,
2026        }
2027    }
2028
2029    fn disarm(&mut self) {
2030        self.active = false;
2031    }
2032}
2033
2034impl Drop for V8SessionRegistrationGuard<'_> {
2035    fn drop(&mut self) {
2036        if self.active {
2037            self.v8_host.unregister_session(&self.session_id);
2038        }
2039    }
2040}
2041
2042struct PendingV8SessionRegistration<'a> {
2043    frame_receiver: TrackedReceiver<BinaryFrame>,
2044    registration_guard: V8SessionRegistrationGuard<'a>,
2045}
2046
2047fn register_v8_session<F>(
2048    v8_host: &V8RuntimeHost,
2049    session_id: String,
2050    heap_limit_mb: u32,
2051    cpu_time_limit_ms: u32,
2052    wall_clock_limit_ms: u32,
2053    warm_hint: Option<WarmSessionHint>,
2054    create_session: F,
2055) -> Result<PendingV8SessionRegistration<'_>, JavascriptExecutionError>
2056where
2057    F: FnOnce(RuntimeCommand) -> std::io::Result<()>,
2058{
2059    let frame_receiver = v8_host
2060        .register_session(&session_id)
2061        .map_err(JavascriptExecutionError::Spawn)?;
2062    let registration_guard = V8SessionRegistrationGuard::new(v8_host, session_id.clone());
2063
2064    create_session(RuntimeCommand::CreateSession {
2065        session_id,
2066        heap_limit_mb: (heap_limit_mb > 0).then_some(heap_limit_mb),
2067        cpu_time_limit_ms: (cpu_time_limit_ms > 0).then_some(cpu_time_limit_ms),
2068        wall_clock_limit_ms: (wall_clock_limit_ms > 0).then_some(wall_clock_limit_ms),
2069        warm_hint,
2070    })
2071    .map_err(JavascriptExecutionError::Spawn)?;
2072
2073    Ok(PendingV8SessionRegistration {
2074        frame_receiver,
2075        registration_guard,
2076    })
2077}
2078
2079#[derive(Default)]
2080pub struct JavascriptExecutionEngine {
2081    next_context_id: usize,
2082    next_execution_id: usize,
2083    contexts: BTreeMap<String, JavascriptContext>,
2084    import_caches: BTreeMap<String, NodeImportCache>,
2085    v8_host: Option<V8RuntimeHost>,
2086    event_notify: Option<Arc<Notify>>,
2087}
2088
2089impl std::fmt::Debug for JavascriptExecutionEngine {
2090    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2091        f.debug_struct("JavascriptExecutionEngine")
2092            .field("next_context_id", &self.next_context_id)
2093            .field("next_execution_id", &self.next_execution_id)
2094            .field("contexts", &self.contexts)
2095            .field("v8_host", &self.v8_host.is_some())
2096            .finish()
2097    }
2098}
2099
2100impl JavascriptExecutionEngine {
2101    #[doc(hidden)]
2102    pub fn set_event_notify(&mut self, notify: Option<Arc<Notify>>) {
2103        self.event_notify = notify;
2104    }
2105
2106    #[doc(hidden)]
2107    pub fn set_import_cache_base_dir(&mut self, vm_id: impl Into<String>, base_dir: PathBuf) {
2108        self.import_caches
2109            .insert(vm_id.into(), NodeImportCache::new_in(base_dir));
2110    }
2111
2112    pub fn create_context(&mut self, request: CreateJavascriptContextRequest) -> JavascriptContext {
2113        self.next_context_id += 1;
2114        self.import_caches.entry(request.vm_id.clone()).or_default();
2115
2116        let context = JavascriptContext {
2117            context_id: format!("js-ctx-{}", self.next_context_id),
2118            vm_id: request.vm_id,
2119            bootstrap_module: request.bootstrap_module,
2120            compile_cache_dir: request
2121                .compile_cache_root
2122                .map(resolve_node_import_compile_cache_dir),
2123        };
2124        self.contexts
2125            .insert(context.context_id.clone(), context.clone());
2126        context
2127    }
2128
2129    pub fn start_execution(
2130        &mut self,
2131        request: StartJavascriptExecutionRequest,
2132    ) -> Result<JavascriptExecution, JavascriptExecutionError> {
2133        self.start_execution_with_module_reader(request, None, None)
2134    }
2135
2136    fn ensure_v8_host(&mut self) -> Result<(), JavascriptExecutionError> {
2137        let should_spawn_v8_host = match self.v8_host.as_mut() {
2138            Some(v8_host) => !v8_host
2139                .is_alive()
2140                .map_err(JavascriptExecutionError::Spawn)?,
2141            None => true,
2142        };
2143        if should_spawn_v8_host {
2144            self.v8_host = Some(V8RuntimeHost::spawn().map_err(JavascriptExecutionError::Spawn)?);
2145        }
2146        Ok(())
2147    }
2148
2149    pub(crate) fn snapshot_userland_ready(
2150        &mut self,
2151        userland_code: &str,
2152    ) -> Result<bool, JavascriptExecutionError> {
2153        self.ensure_v8_host()?;
2154        Ok(self
2155            .v8_host
2156            .as_ref()
2157            .expect("V8 host initialized")
2158            .snapshot_ready(userland_code))
2159    }
2160
2161    pub(crate) fn pre_warm_snapshot(
2162        &mut self,
2163        userland_code: &str,
2164    ) -> Result<(), JavascriptExecutionError> {
2165        self.ensure_v8_host()?;
2166        self.v8_host
2167            .as_ref()
2168            .expect("V8 host initialized")
2169            .pre_warm_snapshot(userland_code)
2170            .map_err(JavascriptExecutionError::Spawn)
2171    }
2172
2173    pub(crate) fn pre_warm_workers(
2174        &mut self,
2175        userland_code: &str,
2176        heap_limit_mb: u32,
2177        count: usize,
2178    ) -> Result<(), JavascriptExecutionError> {
2179        self.ensure_v8_host()?;
2180        self.v8_host
2181            .as_ref()
2182            .expect("V8 host initialized")
2183            .pre_warm_workers(userland_code, heap_limit_mb, count);
2184        Ok(())
2185    }
2186
2187    /// Like [`start_execution`](Self::start_execution) but with an optional
2188    /// read-only VFS reader over the mounted `node_modules` tree. When supplied,
2189    /// the bridge thread resolves module-resolution RPCs inline against this
2190    /// reader (off the service loop, concurrently with it) instead of routing
2191    /// them through the service loop. The reader must be `Send` because it is
2192    /// moved onto the bridge thread; it must read the same mount the guest sees.
2193    pub fn start_execution_with_module_reader(
2194        &mut self,
2195        request: StartJavascriptExecutionRequest,
2196        module_reader: Option<Box<dyn ModuleFsReader + Send>>,
2197        guest_reader: Option<Box<dyn agentos_v8_runtime::execution::GuestModuleReader>>,
2198    ) -> Result<JavascriptExecution, JavascriptExecutionError> {
2199        let context = self
2200            .contexts
2201            .get(&request.context_id)
2202            .cloned()
2203            .ok_or_else(|| JavascriptExecutionError::MissingContext(request.context_id.clone()))?;
2204
2205        if context.vm_id != request.vm_id {
2206            return Err(JavascriptExecutionError::VmMismatch {
2207                expected: context.vm_id,
2208                found: request.vm_id,
2209            });
2210        }
2211
2212        if request.argv.is_empty() {
2213            return Err(JavascriptExecutionError::EmptyArgv);
2214        }
2215
2216        let phase_start = Instant::now();
2217        // Ensure import cache is materialized (still needed for module resolution)
2218        let import_cache = self.import_caches.entry(context.vm_id.clone()).or_default();
2219        import_cache
2220            .ensure_materialized_with_timeout(javascript_import_cache_materialize_timeout(&request))
2221            .map_err(JavascriptExecutionError::PrepareImportCache)?;
2222        let import_cache_guard = import_cache.cleanup_guard();
2223        record_js_start_phase("js_start_import_cache", phase_start.elapsed());
2224
2225        self.next_execution_id += 1;
2226        let execution_id = format!("exec-{}", self.next_execution_id);
2227        let sync_rpc_timeout = javascript_sync_rpc_timeout(&request);
2228
2229        let phase_start = Instant::now();
2230        self.ensure_v8_host()?;
2231        let v8_host = self.v8_host.as_ref().unwrap();
2232        record_js_start_phase("js_start_v8_host_ready", phase_start.elapsed());
2233
2234        let phase_start = Instant::now();
2235        // Create a V8 session
2236        let session_id = format!(
2237            "v8-{execution_id}-{}",
2238            NEXT_V8_SESSION_ID.fetch_add(1, Ordering::Relaxed)
2239        );
2240        let heap_limit_mb = javascript_heap_limit_mb(&request);
2241        let cpu_time_limit_ms = javascript_cpu_time_limit_ms(&request);
2242        let wall_clock_limit_ms = javascript_wall_clock_limit_ms(&request);
2243        let snapshot_userland_code = request
2244            .guest_runtime
2245            .snapshot_userland_code
2246            .clone()
2247            .unwrap_or_default();
2248        let warm_hint = Some(WarmSessionHint {
2249            bridge_code: V8RuntimeHost::bridge_code().to_owned(),
2250            userland_code: snapshot_userland_code.clone(),
2251            heap_limit_mb: (heap_limit_mb > 0).then_some(heap_limit_mb),
2252        });
2253        if snapshot_userland_code.is_empty() && heap_limit_mb == 0 {
2254            v8_host.seed_default_warm_workers_async();
2255        }
2256        let PendingV8SessionRegistration {
2257            frame_receiver,
2258            mut registration_guard,
2259        } = register_v8_session(
2260            v8_host,
2261            session_id.clone(),
2262            heap_limit_mb,
2263            cpu_time_limit_ms,
2264            wall_clock_limit_ms,
2265            warm_hint,
2266            |command| v8_host.create_session_from_command(command),
2267        )?;
2268        record_js_start_phase("js_start_v8_session_register", phase_start.elapsed());
2269
2270        let phase_start = Instant::now();
2271        // Build user code: prefer inline code, fall back to entrypoint-based
2272        let translator = GuestPathTranslator::from_request(&request);
2273        let host_entrypoint = translator.resolve_host_entrypoint(&request.cwd, &request.argv[0]);
2274        let guest_entrypoint = if request.argv[0] == "-e" || request.argv[0] == "--eval" {
2275            request.argv[0].clone()
2276        } else if let Some(explicit_guest_entrypoint) = request
2277            .env
2278            .get(NODE_GUEST_ENTRYPOINT_ENV)
2279            .filter(|value| value.starts_with('/'))
2280        {
2281            // Part B (guest-VFS adapter launch): the sidecar already resolved the
2282            // GUEST entrypoint path (AGENTOS_GUEST_ENTRYPOINT). Use it directly as
2283            // the sourceURL / module-resolution base instead of translating the
2284            // host entrypoint — `host_to_guest_string` misses for guest-native
2285            // mounts (`agentos_packages`, whose host staging dir is not in the
2286            // translation map) and falls back to `/unknown/<cmd>`, which then
2287            // poisons the adapter's own relative/bare imports. For host-backed
2288            // mounts the two values are equal, so this is a no-op there. Applies
2289            // to child launches too (they set AGENTOS_GUEST_ENTRYPOINT as well),
2290            // which is the child-process `/unknown` case.
2291            explicit_guest_entrypoint.clone()
2292        } else {
2293            translator.host_to_guest_string(&host_entrypoint)
2294        };
2295        let process_argv = if matches!(guest_entrypoint.as_str(), "-e" | "--eval") {
2296            std::iter::once(String::from("node"))
2297                .chain(request.argv.iter().skip(1).cloned())
2298                .collect::<Vec<_>>()
2299        } else {
2300            std::iter::once(String::from("node"))
2301                .chain(std::iter::once(guest_entrypoint.clone()))
2302                .chain(request.argv.iter().skip(1).cloned())
2303                .collect::<Vec<_>>()
2304        };
2305        let inline_code = request
2306            .inline_code
2307            .clone()
2308            .map(|inline_code| strip_javascript_hashbang(&inline_code));
2309        let use_module_mode = request
2310            .env
2311            .get(NODE_GUEST_ENTRYPOINT_MODULE_MODE_ENV)
2312            .is_some_and(|value| value == "1" || value.eq_ignore_ascii_case("true"))
2313            || host_entrypoint_uses_module_mode(&host_entrypoint)
2314            || inline_code
2315                .as_deref()
2316                .is_some_and(inline_code_uses_module_mode);
2317        if !matches!(guest_entrypoint.as_str(), "-e" | "--eval") && !use_module_mode {
2318            if let Some(inline_code) = inline_code.as_ref() {
2319                if let Some(parent) = host_entrypoint.parent() {
2320                    fs::create_dir_all(parent)
2321                        .map_err(JavascriptExecutionError::PrepareImportCache)?;
2322                }
2323                fs::write(&host_entrypoint, inline_code)
2324                    .map_err(JavascriptExecutionError::PrepareImportCache)?;
2325            }
2326        }
2327        let user_code = if matches!(guest_entrypoint.as_str(), "-e" | "--eval") {
2328            inline_code.unwrap_or_else(|| build_v8_user_code(&guest_entrypoint, &request.env))
2329        } else if use_module_mode {
2330            if let Some(inline_code) = inline_code {
2331                format!("{inline_code}\n//# sourceURL={guest_entrypoint}")
2332            } else {
2333                strip_javascript_hashbang(&fs::read_to_string(&host_entrypoint).map_err(
2334                    |error| {
2335                        JavascriptExecutionError::PrepareImportCache(std::io::Error::new(
2336                            error.kind(),
2337                            format!(
2338                                "failed to read JavaScript entrypoint {}: {error}",
2339                                host_entrypoint.display()
2340                            ),
2341                        ))
2342                    },
2343                )?)
2344            }
2345        } else {
2346            build_v8_user_code(&guest_entrypoint, &request.env)
2347        };
2348        let user_code = prepend_v8_runtime_shim(
2349            user_code,
2350            &guest_entrypoint,
2351            &process_argv,
2352            translator.guest_cwd(),
2353            &request.env,
2354            heap_limit_mb,
2355            &request.guest_runtime,
2356        );
2357        record_js_start_phase("js_start_build_user_code", phase_start.elapsed());
2358
2359        let phase_start = Instant::now();
2360        // Create session handle for sending bridge responses
2361        let v8_session = v8_host.session_handle(session_id.clone());
2362
2363        // Start the event bridge before execution so early sync bridge calls
2364        // made during module instantiation/evaluation cannot deadlock waiting
2365        // for a response while no host thread is draining session frames yet.
2366        let pending_sync_rpc = Arc::new(Mutex::new(None));
2367        let kernel_stdin = Arc::new(LocalKernelStdinBridge::default());
2368        let standalone_translator = translator.clone();
2369        // default + in-place assign: LocalBridgeState is Drop, so `..Default::default()`
2370        // (E0509) is not allowed.
2371        let mut local_bridge = LocalBridgeState::default();
2372        local_bridge.translator = translator;
2373        local_bridge.kernel_stdin = kernel_stdin.clone();
2374        local_bridge.v8_session = Some(v8_session.clone());
2375        local_bridge.module_reader = module_reader;
2376        local_bridge.module_resolution = GuestModuleResolution::from_env(&request.env);
2377        local_bridge.forward_kernel_stdin_rpc = request
2378            .env
2379            .get(FORWARD_KERNEL_STDIN_RPC_ENV)
2380            .is_some_and(|value| value == "1" || value.eq_ignore_ascii_case("true"));
2381        let events = spawn_v8_event_bridge(
2382            frame_receiver,
2383            pending_sync_rpc.clone(),
2384            sync_rpc_timeout,
2385            v8_session.clone(),
2386            local_bridge,
2387            self.event_notify.clone(),
2388        );
2389        record_js_start_phase("js_start_event_bridge", phase_start.elapsed());
2390
2391        let phase_start = Instant::now();
2392        // Install the direct module reader on the session thread BEFORE the Execute
2393        // frame so the SetModuleReader command (routed through the same dispatch
2394        // queue) arrives first; module loads then read source directly on the V8
2395        // thread instead of round-tripping the bridge.
2396        if let Some(guest_reader) = guest_reader {
2397            v8_session
2398                .set_module_reader(guest_reader)
2399                .map_err(JavascriptExecutionError::Spawn)?;
2400        }
2401        record_js_start_phase("js_start_install_module_reader", phase_start.elapsed());
2402
2403        let phase_start = Instant::now();
2404        // Execute bridge code + user code in the V8 isolate
2405        v8_session
2406            .execute(
2407                if use_module_mode { 1 } else { 0 },
2408                guest_entrypoint.clone(),
2409                V8RuntimeHost::bridge_code().to_owned(),
2410                String::new(),
2411                snapshot_userland_code,
2412                request.guest_runtime.high_resolution_time,
2413                user_code,
2414                request.wasm_module_bytes.clone(),
2415            )
2416            .map_err(JavascriptExecutionError::Spawn)?;
2417        registration_guard.disarm();
2418        record_js_start_phase("js_start_send_execute", phase_start.elapsed());
2419
2420        Ok(JavascriptExecution {
2421            execution_id,
2422            child_pid: v8_host.child_pid(),
2423            events: tokio::sync::Mutex::new(events),
2424            pending_sync_rpc,
2425            kernel_stdin,
2426            _import_cache_guard: import_cache_guard,
2427            v8_session,
2428            module_resolution: Mutex::new((
2429                standalone_translator,
2430                LocalModuleResolutionCache::default(),
2431            )),
2432        })
2433    }
2434
2435    pub fn dispose_vm(&mut self, vm_id: &str) {
2436        self.contexts.retain(|_, context| context.vm_id != vm_id);
2437        self.import_caches.remove(vm_id);
2438    }
2439
2440    #[doc(hidden)]
2441    #[allow(dead_code)]
2442    pub fn materialize_import_cache_for_vm(
2443        &mut self,
2444        vm_id: &str,
2445    ) -> Result<&std::path::Path, std::io::Error> {
2446        let import_cache = self.import_caches.entry(vm_id.to_owned()).or_default();
2447        import_cache.ensure_materialized()?;
2448        Ok(import_cache.cache_path())
2449    }
2450
2451    #[doc(hidden)]
2452    #[allow(dead_code)]
2453    pub fn import_cache_path_for_vm(&self, vm_id: &str) -> Option<&std::path::Path> {
2454        self.import_caches
2455            .get(vm_id)
2456            .map(NodeImportCache::cache_path)
2457    }
2458}
2459
2460fn set_pending_sync_rpc_state(
2461    pending_sync_rpc: &Arc<Mutex<Option<PendingSyncRpcState>>>,
2462    id: u64,
2463) -> Result<(), JavascriptExecutionError> {
2464    let mut pending = pending_sync_rpc.lock().map_err(|_| {
2465        JavascriptExecutionError::RpcResponse(String::from(
2466            "sync RPC pending-request state lock poisoned",
2467        ))
2468    })?;
2469    *pending = Some(PendingSyncRpcState::Pending(id));
2470    Ok(())
2471}
2472
2473fn resolve_node_import_compile_cache_dir(root_dir: PathBuf) -> PathBuf {
2474    root_dir.join(format!(
2475        "node-imports-v{NODE_IMPORT_COMPILE_CACHE_NAMESPACE_VERSION}-{:016x}",
2476        stable_compile_cache_namespace_hash()
2477    ))
2478}
2479
2480fn stable_compile_cache_namespace_hash() -> u64 {
2481    stable_hash64(
2482        [
2483            env!("CARGO_PKG_NAME"),
2484            env!("CARGO_PKG_VERSION"),
2485            NODE_ENTRYPOINT_ENV,
2486            NODE_BOOTSTRAP_ENV,
2487            NODE_GUEST_ARGV_ENV,
2488            NODE_PREWARM_IMPORTS_ENV,
2489            NODE_WARMUP_MARKER_VERSION,
2490        ]
2491        .into_iter()
2492        .chain(NODE_WARMUP_SPECIFIERS.iter().copied())
2493        .collect::<Vec<_>>()
2494        .join("\n")
2495        .as_bytes(),
2496    )
2497}
2498
2499fn javascript_sync_rpc_timeout(request: &StartJavascriptExecutionRequest) -> Duration {
2500    let timeout_ms = request
2501        .limits
2502        .sync_rpc_wait_timeout_ms
2503        .filter(|value| *value > 0)
2504        .unwrap_or(NODE_SYNC_RPC_DEFAULT_WAIT_TIMEOUT_MS);
2505    Duration::from_millis(timeout_ms)
2506}
2507
2508fn javascript_heap_limit_mb(request: &StartJavascriptExecutionRequest) -> u32 {
2509    request
2510        .limits
2511        .v8_heap_limit_mb
2512        .filter(|value| *value > 0)
2513        .unwrap_or(0)
2514}
2515
2516fn javascript_import_cache_materialize_timeout(
2517    request: &StartJavascriptExecutionRequest,
2518) -> Duration {
2519    let timeout_ms = request
2520        .limits
2521        .import_cache_materialize_timeout_ms
2522        .filter(|value| *value > 0)
2523        .unwrap_or(DEFAULT_NODE_IMPORT_CACHE_MATERIALIZE_TIMEOUT_MS);
2524    Duration::from_millis(timeout_ms)
2525}
2526
2527/// Resolve the TRUE CPU-time budget (ms) for a JavaScript execution.
2528///
2529/// Read from typed `limits.jsRuntime.cpuTimeLimitMs`, falling back to a bounded
2530/// default when unset. `0` remains an explicit trusted opt-out and is normalized
2531/// to `None` by the V8 session.
2532fn javascript_cpu_time_limit_ms(request: &StartJavascriptExecutionRequest) -> u32 {
2533    request
2534        .limits
2535        .cpu_time_limit_ms
2536        // Generous active-CPU budget: long-lived adapters are still not capped
2537        // on wall-clock, but CPU-bound runaways no longer pin a core forever by
2538        // default.
2539        .unwrap_or(DEFAULT_V8_CPU_TIME_LIMIT_MS)
2540}
2541
2542/// Resolve the opt-in WALL-CLOCK backstop (ms) for a JavaScript execution.
2543///
2544/// Read from typed `limits.jsRuntime.wallClockLimitMs`, falling back to `0` (no
2545/// limit). `0` is normalized to `None` by the V8 session, so the wall-clock
2546/// `TimeoutGuard` is NOT armed and the guest runs without a wall-clock limit.
2547/// This is INDEPENDENT of the CPU-time budget: setting only one arms only that
2548/// guard.
2549fn javascript_wall_clock_limit_ms(request: &StartJavascriptExecutionRequest) -> u32 {
2550    request
2551        .limits
2552        .wall_clock_limit_ms
2553        .unwrap_or(DEFAULT_V8_WALL_CLOCK_LIMIT_MS)
2554}
2555
2556fn spawn_javascript_sync_rpc_timeout(
2557    id: u64,
2558    timeout: Duration,
2559    pending_state: Arc<Mutex<Option<PendingSyncRpcState>>>,
2560    responses: Option<JavascriptSyncRpcResponseWriter>,
2561) {
2562    let Some(responses) = responses else {
2563        return;
2564    };
2565
2566    thread::spawn(move || {
2567        thread::sleep(timeout);
2568
2569        let should_timeout = match pending_state.lock() {
2570            Ok(mut guard) if *guard == Some(PendingSyncRpcState::Pending(id)) => {
2571                *guard = Some(PendingSyncRpcState::TimedOut(id));
2572                true
2573            }
2574            Ok(_) => false,
2575            Err(_) => false,
2576        };
2577
2578        if !should_timeout {
2579            return;
2580        }
2581
2582        let _ = write_javascript_sync_rpc_response(
2583            &responses,
2584            json!({
2585                "id": id,
2586                "ok": false,
2587                "error": {
2588                    "code": "ERR_AGENTOS_NODE_SYNC_RPC_TIMEOUT",
2589                    "message": format!(
2590                        "guest JavaScript sync RPC request {id} timed out after {}ms",
2591                        timeout.as_millis()
2592                    ),
2593                },
2594            }),
2595        );
2596    });
2597}
2598
2599fn spawn_javascript_sync_rpc_reader(
2600    reader: File,
2601    sender: mpsc::Sender<JavascriptProcessEvent>,
2602) -> std::thread::JoinHandle<()> {
2603    std::thread::spawn(move || {
2604        let mut reader = BufReader::new(reader);
2605        let mut line = String::new();
2606
2607        loop {
2608            line.clear();
2609            match reader.read_line(&mut line) {
2610                Ok(0) => return,
2611                Ok(_) => {
2612                    let trimmed = line.trim();
2613                    if trimmed.is_empty() {
2614                        continue;
2615                    }
2616
2617                    match parse_javascript_sync_rpc_request(trimmed) {
2618                        Ok(request) => {
2619                            if sender
2620                                .send(JavascriptProcessEvent::SyncRpcRequest(request))
2621                                .is_err()
2622                            {
2623                                return;
2624                            }
2625                        }
2626                        Err(message) => {
2627                            if sender
2628                                .send(JavascriptProcessEvent::RawStderr(
2629                                    format!("{message}\n").into_bytes(),
2630                                ))
2631                                .is_err()
2632                            {
2633                                return;
2634                            }
2635                        }
2636                    }
2637                }
2638                Err(error) => {
2639                    let _ = sender.send(JavascriptProcessEvent::RawStderr(
2640                        format!("failed to read JavaScript sync RPC request: {error}\n")
2641                            .into_bytes(),
2642                    ));
2643                    return;
2644                }
2645            }
2646        }
2647    })
2648}
2649
2650fn parse_javascript_sync_rpc_request(line: &str) -> Result<JavascriptSyncRpcRequest, String> {
2651    let wire: JavascriptSyncRpcRequestWire =
2652        serde_json::from_str(line).map_err(|error| error.to_string())?;
2653    Ok(JavascriptSyncRpcRequest {
2654        id: wire.id,
2655        method: wire.method,
2656        args: wire.args,
2657        raw_bytes_args: HashMap::new(),
2658    })
2659}
2660
2661fn write_javascript_sync_rpc_response(
2662    writer: &JavascriptSyncRpcResponseWriter,
2663    response: Value,
2664) -> Result<(), JavascriptExecutionError> {
2665    let mut payload = serde_json::to_vec(&response)
2666        .map_err(|error| JavascriptExecutionError::RpcResponse(error.to_string()))?;
2667    payload.push(b'\n');
2668    writer.send(payload)
2669}
2670
2671fn spawn_javascript_sync_rpc_response_writer(
2672    writer: File,
2673    receiver: Receiver<Vec<u8>>,
2674) -> thread::JoinHandle<()> {
2675    thread::spawn(move || {
2676        let mut writer = BufWriter::new(writer);
2677        while let Ok(payload) = receiver.recv() {
2678            if writer
2679                .write_all(&payload)
2680                .and_then(|()| writer.flush())
2681                .is_err()
2682            {
2683                return;
2684            }
2685        }
2686    })
2687}
2688
2689/// Build the user code wrapper for V8 execution.
2690/// This wraps the entrypoint in a way that the V8 bridge can execute it.
2691fn build_v8_user_code(entrypoint: &str, env: &BTreeMap<String, String>) -> String {
2692    // The bridge code (polyfills) sets up the module system and globals.
2693    // User code is executed after the bridge completes.
2694    // For file-based entrypoints, we load and execute them through the module system.
2695    // For inline code (-e flag), we execute directly.
2696    if entrypoint == "-e" || entrypoint == "--eval" {
2697        // Inline code from NODE_EVAL or similar
2698        env.get("AGENTOS_NODE_EVAL").cloned().unwrap_or_default()
2699    } else {
2700        // Module entrypoint - use require to load it
2701        format!(
2702            "require({});\n//# sourceURL={}",
2703            serde_json::to_string(entrypoint).unwrap_or_else(|_| format!("\"{}\"", entrypoint)),
2704            entrypoint
2705        )
2706    }
2707}
2708
2709fn host_entrypoint_uses_module_mode(entrypoint: &Path) -> bool {
2710    // Agent adapters are launched via an extensionless `/opt/agentos/bin/<cmd>`
2711    // symlink into the packed package's `node_modules/<name>/<entry>`. Resolve it
2712    // to the real file so the extension check and the nearest-`package.json` walk
2713    // reflect the package (which carries `"type": "module"`), not the symlink farm
2714    // (which has neither an extension nor a package.json).
2715    let resolved = fs::canonicalize(entrypoint).unwrap_or_else(|_| entrypoint.to_path_buf());
2716    match resolved.extension().and_then(|ext| ext.to_str()) {
2717        Some("mjs" | "mts") => true,
2718        Some("js") => nearest_package_json_type(&resolved).as_deref() == Some("module"),
2719        _ => false,
2720    }
2721}
2722
2723fn inline_code_uses_module_mode(source: &str) -> bool {
2724    let sanitized = strip_non_code_segments(source);
2725    let tokens = tokenize_inline_module_source(&sanitized);
2726    let has_commonjs_signal = tokens.windows(3).any(|window| {
2727        matches!(
2728            window,
2729            [
2730                InlineModuleToken::Identifier("module"),
2731                InlineModuleToken::Punct('.'),
2732                InlineModuleToken::Identifier("exports")
2733            ]
2734        )
2735    }) || tokens.windows(2).any(|window| {
2736        matches!(
2737            window,
2738            [
2739                InlineModuleToken::Identifier("exports"),
2740                InlineModuleToken::Punct('.' | '[')
2741            ] | [
2742                InlineModuleToken::Identifier("require"),
2743                InlineModuleToken::Punct('(')
2744            ]
2745        )
2746    });
2747
2748    if has_commonjs_signal {
2749        return false;
2750    }
2751
2752    tokens.windows(2).any(|window| match window {
2753        [InlineModuleToken::Identifier("import"), InlineModuleToken::Punct('.')] => true,
2754        [InlineModuleToken::Identifier("import"), InlineModuleToken::Punct('(' | ':')] => false,
2755        [InlineModuleToken::Identifier("import"), InlineModuleToken::Identifier(_)
2756        | InlineModuleToken::Punct('{')
2757        | InlineModuleToken::Punct('*')
2758        | InlineModuleToken::StringLiteral] => true,
2759        [InlineModuleToken::Identifier("export"), InlineModuleToken::Identifier(
2760            "default" | "const" | "let" | "var" | "function" | "class" | "async" | "enum" | "type"
2761            | "interface",
2762        )
2763        | InlineModuleToken::Punct('{')
2764        | InlineModuleToken::Punct('*')] => true,
2765        _ => false,
2766    })
2767}
2768
2769#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2770enum InlineModuleToken<'a> {
2771    Identifier(&'a str),
2772    StringLiteral,
2773    Punct(char),
2774}
2775
2776const INLINE_MODULE_STRING_PLACEHOLDER: char = '\u{1F}';
2777
2778fn strip_non_code_segments(source: &str) -> String {
2779    let mut sanitized = String::with_capacity(source.len());
2780    let bytes = source.as_bytes();
2781    let mut index = 0;
2782    sanitize_javascript_code(bytes, &mut index, &mut sanitized, None);
2783    sanitized
2784}
2785
2786fn sanitize_javascript_code(
2787    bytes: &[u8],
2788    index: &mut usize,
2789    output: &mut String,
2790    until_brace_depth: Option<usize>,
2791) {
2792    let mut brace_depth = 0usize;
2793
2794    while *index < bytes.len() {
2795        let current = bytes[*index];
2796
2797        if let Some(target_depth) = until_brace_depth {
2798            match current {
2799                b'{' => brace_depth += 1,
2800                b'}' => {
2801                    if brace_depth == target_depth {
2802                        output.push(' ');
2803                        *index += 1;
2804                        return;
2805                    }
2806                    brace_depth = brace_depth.saturating_sub(1);
2807                }
2808                _ => {}
2809            }
2810        }
2811
2812        match current {
2813            b'/' if bytes.get(*index + 1) == Some(&b'/') => {
2814                output.push(' ');
2815                output.push(' ');
2816                *index += 2;
2817                while *index < bytes.len() {
2818                    let comment_byte = bytes[*index];
2819                    *index += 1;
2820                    if comment_byte == b'\n' {
2821                        output.push('\n');
2822                        break;
2823                    }
2824                    output.push(' ');
2825                }
2826            }
2827            b'/' if bytes.get(*index + 1) == Some(&b'*') => {
2828                output.push(' ');
2829                output.push(' ');
2830                *index += 2;
2831                while *index < bytes.len() {
2832                    let comment_byte = bytes[*index];
2833                    if comment_byte == b'*' && bytes.get(*index + 1) == Some(&b'/') {
2834                        output.push(' ');
2835                        output.push(' ');
2836                        *index += 2;
2837                        break;
2838                    }
2839                    output.push(if comment_byte == b'\n' { '\n' } else { ' ' });
2840                    *index += 1;
2841                }
2842            }
2843            b'\'' | b'"' => sanitize_string_literal(bytes, index, output, current),
2844            b'`' => sanitize_template_literal(bytes, index, output),
2845            _ => {
2846                output.push(char::from(current));
2847                *index += 1;
2848            }
2849        }
2850    }
2851}
2852
2853fn sanitize_string_literal(bytes: &[u8], index: &mut usize, output: &mut String, quote: u8) {
2854    output.push(INLINE_MODULE_STRING_PLACEHOLDER);
2855    *index += 1;
2856
2857    while *index < bytes.len() {
2858        let current = bytes[*index];
2859        *index += 1;
2860        match current {
2861            b'\\' => {
2862                if *index < bytes.len() {
2863                    *index += 1;
2864                }
2865            }
2866            c if c == quote => break,
2867            _ => {}
2868        }
2869    }
2870}
2871
2872fn sanitize_template_literal(bytes: &[u8], index: &mut usize, output: &mut String) {
2873    output.push(INLINE_MODULE_STRING_PLACEHOLDER);
2874    *index += 1;
2875
2876    while *index < bytes.len() {
2877        let current = bytes[*index];
2878        match current {
2879            b'\\' => {
2880                *index += 1;
2881                if *index < bytes.len() {
2882                    *index += 1;
2883                }
2884            }
2885            b'`' => {
2886                *index += 1;
2887                break;
2888            }
2889            b'$' if bytes.get(*index + 1) == Some(&b'{') => {
2890                output.push(' ');
2891                output.push(' ');
2892                *index += 2;
2893                sanitize_javascript_code(bytes, index, output, Some(0));
2894                output.push(INLINE_MODULE_STRING_PLACEHOLDER);
2895            }
2896            b'\n' => {
2897                output.push('\n');
2898                *index += 1;
2899            }
2900            _ => {
2901                *index += 1;
2902            }
2903        }
2904    }
2905}
2906
2907fn tokenize_inline_module_source(source: &str) -> Vec<InlineModuleToken<'_>> {
2908    let mut tokens = Vec::new();
2909    let bytes = source.as_bytes();
2910    let mut index = 0;
2911
2912    while index < bytes.len() {
2913        let current = bytes[index];
2914        match current {
2915            b if b.is_ascii_whitespace() => index += 1,
2916            b if char::from(b) == INLINE_MODULE_STRING_PLACEHOLDER => {
2917                tokens.push(InlineModuleToken::StringLiteral);
2918                index += 1;
2919            }
2920            b'a'..=b'z' | b'A'..=b'Z' | b'_' | b'$' => {
2921                let start = index;
2922                index += 1;
2923                while index < bytes.len()
2924                    && matches!(bytes[index], b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' | b'$')
2925                {
2926                    index += 1;
2927                }
2928                tokens.push(InlineModuleToken::Identifier(&source[start..index]));
2929            }
2930            _ => {
2931                tokens.push(InlineModuleToken::Punct(char::from(current)));
2932                index += 1;
2933            }
2934        }
2935    }
2936
2937    tokens
2938}
2939
2940fn nearest_package_json_type(entrypoint: &Path) -> Option<String> {
2941    let mut current = entrypoint.parent();
2942    while let Some(dir) = current {
2943        let package_json = dir.join("package.json");
2944        if let Ok(contents) = fs::read_to_string(&package_json) {
2945            if let Ok(pkg) = serde_json::from_str::<LocalPackageJson>(&contents) {
2946                return pkg.package_type;
2947            }
2948        }
2949        current = dir.parent();
2950    }
2951    None
2952}
2953
2954fn resolve_v8_entrypoint(cwd: &Path, entrypoint: &str) -> String {
2955    if entrypoint == "-e" || entrypoint == "--eval" {
2956        return entrypoint.to_owned();
2957    }
2958
2959    let path = Path::new(entrypoint);
2960    let resolved = if path.is_absolute() {
2961        path.to_path_buf()
2962    } else {
2963        cwd.join(path)
2964    };
2965    resolved.to_string_lossy().into_owned()
2966}
2967
2968fn prepend_v8_runtime_shim(
2969    user_code: String,
2970    entrypoint: &str,
2971    argv: &[String],
2972    cwd: &str,
2973    env: &BTreeMap<String, String>,
2974    // V8 heap cap in MB (`0` = engine default). Threaded from the typed wire
2975    // limit and interpolated into the shim so guest heap-stats reporting no
2976    // longer depends on an `AGENTOS_V8_HEAP_LIMIT_MB` env var.
2977    heap_limit_mb: u32,
2978    // Typed guest-runtime identity, interpolated into the shim so virtual
2979    // `process.*` identity no longer rides `AGENTOS_VIRTUAL_PROCESS_*` env vars.
2980    guest_runtime: &GuestRuntimeConfig,
2981) -> String {
2982    let argv_json = serde_json::to_string(argv).unwrap_or_else(|_| String::from("[\"node\"]"));
2983    let entry_json =
2984        serde_json::to_string(entrypoint).unwrap_or_else(|_| String::from("\"/<entry>\""));
2985    let cwd_json = serde_json::to_string(cwd).unwrap_or_else(|_| String::from("\"/\""));
2986    let env_json = serde_json::to_string(env).unwrap_or_else(|_| String::from("{}"));
2987    // Virtual process identity object. `Option` fields serialize to `null`, which
2988    // the shim treats as "unset" (leaving the V8-baked default) — matching the
2989    // prior behavior when the env var was absent.
2990    let identity_json = serde_json::json!({
2991        "pid": guest_runtime.virtual_pid,
2992        "ppid": guest_runtime.virtual_ppid,
2993        "uid": guest_runtime.virtual_uid,
2994        "gid": guest_runtime.virtual_gid,
2995        "execPath": guest_runtime.virtual_exec_path,
2996    })
2997    .to_string();
2998    // Virtual OS identity (os.cpus/totalmem/freemem/homedir/userInfo/...). Read
2999    // by the bridge + node-import-cache os polyfill from the `__agentOSVirtualOs`
3000    // global instead of `AGENTOS_VIRTUAL_OS_*` env vars. Absent fields stay
3001    // `null`, so the consumers fall back to their built-in defaults.
3002    let virtual_os_json = serde_json::json!({
3003        "cpuCount": guest_runtime.os_cpu_count,
3004        "totalmem": guest_runtime.os_totalmem,
3005        "freemem": guest_runtime.os_freemem,
3006        "homedir": guest_runtime.os_homedir,
3007        "hostname": guest_runtime.os_hostname,
3008        "tmpdir": guest_runtime.os_tmpdir,
3009        "type": guest_runtime.os_type,
3010        "release": guest_runtime.os_release,
3011        "version": guest_runtime.os_version,
3012        "machine": guest_runtime.os_machine,
3013        "shell": guest_runtime.os_shell,
3014        "user": guest_runtime.os_user,
3015    })
3016    .to_string();
3017    let high_resolution_time = guest_runtime.high_resolution_time;
3018
3019    format!(
3020        r#"(function () {{
3021  const __guestIdentity = {identity_json};
3022  Object.defineProperty(globalThis, "__agentOSVirtualOs", {{
3023    configurable: true,
3024    enumerable: false,
3025    value: {virtual_os_json},
3026    writable: true,
3027  }});
3028  const nextArgv = {argv_json};
3029  const entryFile = {entry_json};
3030  const nextCwd = {cwd_json};
3031  const nextEnv = {env_json};
3032  const nextHighResolutionTime = {high_resolution_time};
3033  try {{
3034    const previousProcessConfig =
3035      typeof globalThis._processConfig === "object" && globalThis._processConfig !== null
3036        ? globalThis._processConfig
3037        : {{}};
3038    Object.defineProperty(globalThis, "_processConfig", {{
3039      configurable: true,
3040      enumerable: false,
3041      value: Object.freeze({{
3042        ...previousProcessConfig,
3043        cwd: nextCwd,
3044        env: nextEnv,
3045        argv: nextArgv,
3046        high_resolution_time: nextHighResolutionTime,
3047      }}),
3048      writable: false,
3049    }});
3050  }} catch (_e) {{}}
3051  Object.defineProperty(globalThis, "__agentOSProcessConfigEnv", {{
3052    configurable: true,
3053    enumerable: false,
3054    value: nextEnv,
3055    writable: true,
3056  }});
3057  const visibleEnv = Object.fromEntries(
3058    Object.entries(nextEnv).filter(([key]) => !key.startsWith("AGENTOS_"))
3059  );
3060
3061  if (typeof process !== "undefined") {{
3062    process.argv = nextArgv;
3063    process.argv0 = nextArgv[0] || "node";
3064    process.env = {{
3065      ...(process.env || {{}}),
3066      ...visibleEnv,
3067    }};
3068    const configuredHeapLimitMb = {heap_limit_mb};
3069    if (Number.isFinite(configuredHeapLimitMb) && configuredHeapLimitMb > 0) {{
3070      Object.defineProperty(globalThis, "__agentOSV8HeapLimitBytes", {{
3071        configurable: true,
3072        enumerable: false,
3073        value: configuredHeapLimitMb * 1024 * 1024,
3074        writable: true,
3075      }});
3076    }}
3077    if (nextEnv.AGENTOS_ALLOW_PROCESS_BINDINGS === "1" && typeof process.binding === "function") {{
3078      const originalProcessBinding = process.binding.bind(process);
3079      process.binding = (name) => {{
3080        const bindingName = String(name);
3081        if (
3082          bindingName === "constants" &&
3083          typeof __agentOSConstantsBinding !== "undefined"
3084        ) {{
3085          const constantsBinding =
3086            __agentOSConstantsBinding.default ?? __agentOSConstantsBinding;
3087          return {{
3088            fs: constantsBinding,
3089            crypto: constantsBinding,
3090            zlib: constantsBinding,
3091            trace: constantsBinding,
3092            internal: constantsBinding,
3093            os: {{
3094              UV_UDP_REUSEADDR: constantsBinding.UV_UDP_REUSEADDR,
3095              dlopen: constantsBinding.dlopen,
3096              errno: constantsBinding.errno,
3097              signals: constantsBinding.signals,
3098              priority: constantsBinding.priority,
3099            }},
3100          }};
3101        }}
3102        try {{
3103          return originalProcessBinding(name);
3104        }} catch (error) {{
3105          const originalMessage =
3106            error && typeof error === "object" && typeof error.message === "string"
3107              ? error.message
3108              : String(error);
3109          throw new Error(
3110            `process.binding(${{bindingName}}) failed: ${{originalMessage}}`
3111          );
3112        }}
3113      }};
3114    }}
3115    const nextPid = Number(__guestIdentity.pid);
3116    if (Number.isFinite(nextPid) && nextPid > 0) {{
3117      process.pid = nextPid;
3118    }}
3119    const nextPpid = Number(__guestIdentity.ppid);
3120    if (Number.isFinite(nextPpid) && nextPpid >= 0) {{
3121      process.ppid = nextPpid;
3122    }}
3123    const nextUid = Number(__guestIdentity.uid);
3124    if (Number.isFinite(nextUid) && nextUid >= 0) {{
3125      process.uid = nextUid;
3126      process.euid = nextUid;
3127    }}
3128    const nextGid = Number(__guestIdentity.gid);
3129    if (Number.isFinite(nextGid) && nextGid >= 0) {{
3130      process.gid = nextGid;
3131      process.egid = nextGid;
3132      process.groups = [nextGid];
3133    }}
3134    if (typeof __guestIdentity.execPath === "string" && __guestIdentity.execPath.length > 0) {{
3135      process.execPath = __guestIdentity.execPath;
3136    }}
3137    if (nextEnv.AGENTOS_NODE_IPC === "1" && typeof __runtimeInstallProcessIpcBridge === "function") {{
3138      process.connected = true;
3139      __runtimeInstallProcessIpcBridge();
3140    }}
3141    process.cwd = () => nextCwd;
3142    process._cwd = nextCwd;
3143    if (typeof process.getBuiltinModule !== "function") {{
3144      process.getBuiltinModule = function(specifier) {{
3145        return globalThis.require ? globalThis.require(specifier) : undefined;
3146      }};
3147    }}
3148  }}
3149
3150  globalThis.__runtimeStreamStdin = nextEnv.AGENTOS_KEEP_STDIN_OPEN === "1";
3151
3152  if (
3153    typeof globalThis.WebAssembly === "object" &&
3154    globalThis.WebAssembly !== null &&
3155    typeof globalThis.WebAssembly.instantiateStreaming !== "function"
3156  ) {{
3157    globalThis.WebAssembly.instantiateStreaming = async function instantiateStreaming(source, imports) {{
3158      const response = await source;
3159      if (response == null || typeof response.arrayBuffer !== "function") {{
3160        throw new TypeError(
3161          "WebAssembly.instantiateStreaming requires a Response or promise for one",
3162        );
3163      }}
3164      const bytes = new Uint8Array(await response.arrayBuffer());
3165      return globalThis.WebAssembly.instantiate(bytes, imports);
3166    }};
3167  }}
3168
3169  if (
3170    typeof globalThis.require === "undefined" &&
3171    typeof globalThis._moduleModule?.createRequire === "function"
3172  ) {{
3173    const requireEntryFile =
3174      entryFile === "-e" || entryFile === "--eval"
3175        ? nextCwd === "/"
3176          ? "/__agentos_eval__.js"
3177          : `${{nextCwd.replace(/\/+$/, "")}}/__agentos_eval__.js`
3178        : entryFile;
3179    globalThis.require =
3180      globalThis._moduleModule.createRequire(requireEntryFile);
3181  }}
3182
3183  // jsRuntime platform tiering: the guest JS host surface is baked into the
3184  // shared V8 snapshot, so non-node platforms are produced by subtractively
3185  // scrubbing baked globals here, per execution.
3186  const __jsPlatform = nextEnv.AGENTOS_JS_PLATFORM || "node";
3187  // Install the builtin allow-list gate consulted by the bridge's
3188  // rejectRestrictedBuiltinRequest (covers require + ESM builtin loads). Present
3189  // for non-node platforms (empty => deny all) and node + explicit allow-list;
3190  // absent => unrestricted (node default).
3191  {{
3192    const __builtinAllowRaw = nextEnv.AGENTOS_JS_BUILTIN_ALLOWLIST;
3193    if (typeof __builtinAllowRaw === "string") {{
3194      let __allowList = [];
3195      try {{ __allowList = JSON.parse(__builtinAllowRaw); }} catch (_e) {{ __allowList = []; }}
3196      if (typeof globalThis.__agentOSInitJsRuntime === "function") {{
3197        globalThis.__agentOSInitJsRuntime(Array.isArray(__allowList) ? __allowList : []);
3198      }}
3199      try {{ delete globalThis.__agentOSInitJsRuntime; }} catch (_e) {{}}
3200    }}
3201  }}
3202  if (__jsPlatform !== "node") {{
3203    const __dropGlobal = (name) => {{
3204      try {{ delete globalThis[name]; }} catch (_e) {{}}
3205      if (
3206        Object.prototype.hasOwnProperty.call(globalThis, name) ||
3207        typeof globalThis[name] !== "undefined"
3208      ) {{
3209        try {{ globalThis[name] = undefined; }} catch (_e) {{}}
3210        try {{
3211          Object.defineProperty(globalThis, name, {{
3212            value: undefined,
3213            configurable: true,
3214            writable: true,
3215          }});
3216        }} catch (_e) {{}}
3217        try {{ delete globalThis[name]; }} catch (_e) {{}}
3218      }}
3219    }};
3220    // Node host surface + identity channels — removed on every non-node platform.
3221    [
3222      "process", "Buffer", "require", "module", "exports",
3223      "__dirname", "__filename", "global",
3224      "_processConfig", "__agentOSProcessConfigEnv", "__agentOSVirtualOs",
3225    ].forEach(__dropGlobal);
3226    if (__jsPlatform === "browser") {{
3227      // Narrow `crypto` from the full node:crypto module to the WebCrypto object
3228      // (drops randomBytes/createHash/... while keeping subtle/getRandomValues).
3229      try {{
3230        const __wc = globalThis.crypto && globalThis.crypto.webcrypto;
3231        if (__wc) {{ globalThis.crypto = __wc; }}
3232      }} catch (_e) {{}}
3233    }}
3234    if (__jsPlatform === "neutral" || __jsPlatform === "bare") {{
3235      // Web-platform globals removed at neutral and below.
3236      [
3237        "fetch", "Headers", "Request", "Response", "FormData",
3238        "URL", "URLSearchParams", "Blob", "File", "crypto",
3239        "atob", "btoa", "structuredClone", "performance",
3240        "AbortController", "AbortSignal", "Event", "EventTarget",
3241        "MessageChannel", "MessagePort", "MessageEvent",
3242        "ReadableStream", "WritableStream", "TransformStream",
3243      ].forEach(__dropGlobal);
3244    }}
3245    if (__jsPlatform === "bare") {{
3246      // Universal host primitives removed only at the language-only tier.
3247      [
3248        "console", "queueMicrotask",
3249        "setTimeout", "clearTimeout", "setInterval", "clearInterval",
3250        "setImmediate", "clearImmediate",
3251      ].forEach(__dropGlobal);
3252    }}
3253  }}
3254}})();
3255{user_code}"#
3256    )
3257}
3258
3259/// Spawn a V8 event bridge thread that converts V8 BinaryFrame messages
3260/// into JavascriptExecutionEvent for the sidecar event loop.
3261///
3262/// Internal bridge calls (module loading, logging, timers) are handled locally
3263/// by the event bridge. Kernel operations (fs, net, child_process, dns) are
3264/// forwarded to the sidecar via SyncRpcRequest events.
3265fn spawn_v8_event_bridge(
3266    frame_receiver: TrackedReceiver<BinaryFrame>,
3267    pending_sync_rpc: Arc<Mutex<Option<PendingSyncRpcState>>>,
3268    _sync_rpc_timeout: Duration,
3269    v8_session: V8SessionHandle,
3270    mut local_bridge: LocalBridgeState,
3271    event_notify: Option<Arc<Notify>>,
3272) -> TokioReceiver<JavascriptExecutionEvent> {
3273    let (sender, receiver) = channel(JAVASCRIPT_EVENT_CHANNEL_CAPACITY);
3274    let event_gauge = register_queue(
3275        TrackedLimit::JavascriptEventChannel,
3276        JAVASCRIPT_EVENT_CHANNEL_CAPACITY,
3277    );
3278
3279    thread::spawn(move || {
3280        let mut emitted_exit = false;
3281        loop {
3282            let frame_recv_start = Instant::now();
3283            let Ok(frame) = frame_receiver.recv() else {
3284                break;
3285            };
3286            let frame_recv_wait = frame_recv_start.elapsed();
3287            let mut exit_frame_start = None;
3288            let event = match frame {
3289                BinaryFrame::BridgeCall {
3290                    call_id,
3291                    method,
3292                    payload,
3293                    ..
3294                } => {
3295                    // Convert CBOR payload to JSON args
3296                    let phase_start = Instant::now();
3297                    let args = v8_runtime::cbor_payload_to_json_args(&payload).unwrap_or_default();
3298                    record_sync_bridge_phase(&method, "event_decode_args", phase_start.elapsed());
3299
3300                    // Module resolution / loading must read the mounted
3301                    // `node_modules` VFS, not host files directly. When the
3302                    // sidecar supplied a read-only VFS module reader, resolve
3303                    // these inline on this bridge thread (off the service loop) so
3304                    // a large cold-start module graph runs concurrently with — and
3305                    // never serializes behind / starves — the ACP bootstrap that
3306                    // is itself awaiting the adapter's `session/new` response on
3307                    // the single service-loop thread. Without a reader (no mount),
3308                    // they flow to the service loop as SyncRpcRequests (mapped to
3309                    // `__resolve_module` / `__load_file` / `__module_format` /
3310                    // `__batch_resolve_modules`) and resolve against `vm.kernel`.
3311                    let is_module_method = matches!(
3312                        method.as_str(),
3313                        "_resolveModule"
3314                            | "_resolveModuleSync"
3315                            | "_loadFile"
3316                            | "_loadFileSync"
3317                            | "_moduleFormat"
3318                            | "_batchResolveModules"
3319                    );
3320                    let resolve_on_service_loop =
3321                        is_module_method && !local_bridge.has_module_reader();
3322
3323                    // Check if this is an internal bridge call we handle locally
3324                    if !resolve_on_service_loop {
3325                        if let Some(response) =
3326                            local_bridge.handle_internal_bridge_call(call_id, &method, &args)
3327                        {
3328                            if let LocalBridgeCallResult::Immediate(response) = response {
3329                                let cbor_payload =
3330                                    v8_runtime::json_to_cbor_payload(&response).unwrap_or_default();
3331                                let _ = v8_session.send_bridge_response(call_id, 0, cbor_payload);
3332                            }
3333                            continue;
3334                        }
3335                    }
3336
3337                    // Handle logging locally (produce stdout/stderr events)
3338                    if method == "_log" || method == "_error" {
3339                        let output = decode_bridge_output_args(&args);
3340                        // Respond to the bridge call
3341                        let _ = v8_session.send_bridge_response(
3342                            call_id,
3343                            0,
3344                            v8_runtime::json_to_cbor_payload(&Value::Null).unwrap_or_default(),
3345                        );
3346                        if method == "_log" {
3347                            if !send_javascript_event(
3348                                &sender,
3349                                &v8_session,
3350                                &event_gauge,
3351                                event_notify.as_deref(),
3352                                JavascriptExecutionEvent::Stdout(output),
3353                            ) {
3354                                break;
3355                            }
3356                        } else {
3357                            if !send_javascript_event(
3358                                &sender,
3359                                &v8_session,
3360                                &event_gauge,
3361                                event_notify.as_deref(),
3362                                JavascriptExecutionEvent::Stderr(output),
3363                            ) {
3364                                break;
3365                            }
3366                        }
3367                        continue;
3368                    }
3369
3370                    // Map the bridge method name to the sidecar sync RPC method name
3371                    let phase_start = Instant::now();
3372                    let (sidecar_method, _needs_translation) =
3373                        v8_runtime::map_bridge_method(&method);
3374                    record_sync_bridge_phase(&method, "event_map_method", phase_start.elapsed());
3375
3376                    // Track pending sync RPC
3377                    let phase_start = Instant::now();
3378                    if let Ok(mut pending) = pending_sync_rpc.lock() {
3379                        *pending = Some(PendingSyncRpcState::Pending(call_id));
3380                    }
3381                    record_sync_bridge_phase(&method, "event_mark_pending", phase_start.elapsed());
3382
3383                    let phase_start = Instant::now();
3384                    let request_args = translate_request_args_for_legacy(sidecar_method, &args);
3385                    let mut raw_bytes_args = HashMap::new();
3386                    if sidecar_method == "net.write"
3387                        || sidecar_method == "fs.writeSync"
3388                        || sidecar_method == "fs.writevSync"
3389                        || sidecar_method == "fs.writeFileSync"
3390                    {
3391                        if let Ok(Some(bytes)) = v8_runtime::cbor_payload_raw_byte_arg(&payload, 1)
3392                        {
3393                            raw_bytes_args.insert(1, bytes);
3394                        }
3395                    }
3396                    if method == "_fsReadRaw" {
3397                        raw_bytes_args.insert(usize::MAX, Vec::new());
3398                    }
3399                    record_sync_bridge_phase(
3400                        &method,
3401                        "event_translate_args",
3402                        phase_start.elapsed(),
3403                    );
3404                    Some(JavascriptExecutionEvent::SyncRpcRequest(
3405                        JavascriptSyncRpcRequest {
3406                            id: call_id,
3407                            method: sidecar_method.to_owned(),
3408                            args: request_args,
3409                            raw_bytes_args,
3410                        },
3411                    ))
3412                }
3413                BinaryFrame::Log {
3414                    channel, message, ..
3415                } => {
3416                    if channel == 0 {
3417                        Some(JavascriptExecutionEvent::Stdout(message.into_bytes()))
3418                    } else {
3419                        Some(JavascriptExecutionEvent::Stderr(message.into_bytes()))
3420                    }
3421                }
3422                BinaryFrame::ExecutionResult {
3423                    exit_code, error, ..
3424                } => {
3425                    let phase_start = Instant::now();
3426                    exit_frame_start = Some(phase_start);
3427                    record_js_event_phase("js_exit_frame_recv_wait", frame_recv_wait);
3428                    let is_process_exit_error = error.as_ref().is_some_and(|err| {
3429                        err.error_type == "ProcessExitError"
3430                            || err.message.starts_with("process.exit(")
3431                    });
3432                    let resolved_exit_code = error
3433                        .as_ref()
3434                        .and_then(|err| {
3435                            if is_process_exit_error {
3436                                parse_process_exit_code_message(&err.message)
3437                            } else {
3438                                None
3439                            }
3440                        })
3441                        .unwrap_or(exit_code);
3442                    let should_emit_error = error.is_some() && !is_process_exit_error;
3443                    if should_emit_error {
3444                        let err = error.as_ref().expect("checked above");
3445                        let error_msg = if err.stack.is_empty() {
3446                            format!("{}: {}\n", err.error_type, err.message)
3447                        } else {
3448                            format!("{}\n", err.stack)
3449                        };
3450                        if !send_javascript_event(
3451                            &sender,
3452                            &v8_session,
3453                            &event_gauge,
3454                            event_notify.as_deref(),
3455                            JavascriptExecutionEvent::Stderr(error_msg.into_bytes()),
3456                        ) {
3457                            break;
3458                        }
3459                    }
3460                    emitted_exit = true;
3461                    record_js_event_phase(
3462                        "js_exit_frame_to_event_construct",
3463                        phase_start.elapsed(),
3464                    );
3465                    Some(JavascriptExecutionEvent::Exited(resolved_exit_code))
3466                }
3467                BinaryFrame::StreamCallback { .. } => None,
3468                _ => None,
3469            };
3470
3471            if let Some(event) = event {
3472                let sync_rpc = match &event {
3473                    JavascriptExecutionEvent::SyncRpcRequest(request) => {
3474                        Some((request.id, request.method.clone()))
3475                    }
3476                    _ => None,
3477                };
3478                if let Some((call_id, method)) = sync_rpc.as_ref() {
3479                    record_sync_bridge_request_enqueued(*call_id, method);
3480                }
3481                let phase_start = sync_rpc.as_ref().map(|_| Instant::now());
3482                let exit_send_start = if matches!(&event, JavascriptExecutionEvent::Exited(_)) {
3483                    Some(Instant::now())
3484                } else {
3485                    None
3486                };
3487                if !send_javascript_event(
3488                    &sender,
3489                    &v8_session,
3490                    &event_gauge,
3491                    event_notify.as_deref(),
3492                    event,
3493                ) {
3494                    break;
3495                }
3496                if let (Some((_, method)), Some(start)) = (sync_rpc, phase_start) {
3497                    record_sync_bridge_phase(&method, "event_enqueue", start.elapsed());
3498                }
3499                if let Some(start) = exit_send_start {
3500                    record_js_event_phase("js_exit_event_send", start.elapsed());
3501                }
3502                if let Some(start) = exit_frame_start {
3503                    record_js_event_phase("js_exit_frame_to_event_sent", start.elapsed());
3504                }
3505            }
3506        }
3507
3508        if !emitted_exit {
3509            let phase_start = Instant::now();
3510            let sent = send_javascript_event(
3511                &sender,
3512                &v8_session,
3513                &event_gauge,
3514                event_notify.as_deref(),
3515                JavascriptExecutionEvent::Exited(1),
3516            );
3517            if sent {
3518                record_js_event_phase("js_exit_fallback_event_send", phase_start.elapsed());
3519            }
3520        }
3521    });
3522
3523    receiver
3524}
3525
3526fn send_javascript_event(
3527    sender: &tokio::sync::mpsc::Sender<JavascriptExecutionEvent>,
3528    v8_session: &V8SessionHandle,
3529    gauge: &agentos_bridge::queue_tracker::QueueGauge,
3530    notify: Option<&Notify>,
3531    event: JavascriptExecutionEvent,
3532) -> bool {
3533    if javascript_event_payload_len(&event) > JAVASCRIPT_EVENT_PAYLOAD_LIMIT_BYTES {
3534        let _ = v8_session.destroy();
3535        return false;
3536    }
3537
3538    // Apply backpressure instead of self-destructing when the host consumer is
3539    // slow. A burst of guest events that briefly outpaces the host draining this
3540    // channel is normal; previously a single `try_send` returning `Full` tore the
3541    // whole session down (`destroy()` -> Shutdown -> `Exited(1)`), turning a
3542    // transient backlog into a fatal crash. This bridge runs on its own dedicated
3543    // OS thread (never a Tokio runtime worker), so a blocking send is safe: it
3544    // parks this thread — and, in turn, the guest producing into it — until the
3545    // host drains capacity. The only terminal condition is the receiver being
3546    // dropped (host gone for good), which surfaces as `Closed` and stops the loop.
3547    match sender.blocking_send(event) {
3548        Ok(()) => {
3549            // Sample the live channel depth so the centralized queue tracker can
3550            // warn before the host consumer falls far enough behind to stall the
3551            // session (and surface the high-water mark for debugging).
3552            gauge.observe_depth(sender.max_capacity().saturating_sub(sender.capacity()));
3553            if let Some(notify) = notify {
3554                notify.notify_one();
3555            }
3556            true
3557        }
3558        Err(_closed) => false,
3559    }
3560}
3561
3562fn javascript_event_payload_len(event: &JavascriptExecutionEvent) -> usize {
3563    match event {
3564        JavascriptExecutionEvent::Stdout(chunk) | JavascriptExecutionEvent::Stderr(chunk) => {
3565            chunk.len()
3566        }
3567        JavascriptExecutionEvent::SyncRpcRequest(_)
3568        | JavascriptExecutionEvent::SignalState { .. }
3569        | JavascriptExecutionEvent::Exited(_) => 0,
3570    }
3571}
3572
3573/// Handle internal bridge calls that don't need to go to the sidecar.
3574/// Returns Some(response) if handled locally, None if it should be forwarded.
3575impl LocalBridgeState {
3576    fn handle_internal_bridge_call(
3577        &mut self,
3578        call_id: u64,
3579        method: &str,
3580        args: &[Value],
3581    ) -> Option<LocalBridgeCallResult> {
3582        match method {
3583            "_resolveModule" | "_resolveModuleSync" => {
3584                let specifier = args.first().and_then(Value::as_str).unwrap_or("");
3585                let parent = args.get(1).and_then(Value::as_str).unwrap_or("/");
3586                let mode = match args.get(2).and_then(Value::as_str) {
3587                    Some("import") => ModuleResolveMode::Import,
3588                    Some("require") => ModuleResolveMode::Require,
3589                    _ if method == "_resolveModule" => ModuleResolveMode::Import,
3590                    _ => ModuleResolveMode::Require,
3591                };
3592                if self.js_runtime_denies_specifier(specifier) {
3593                    return Some(LocalBridgeCallResult::Immediate(Value::Null));
3594                }
3595                let resolved = self.with_module_resolver(|resolver| {
3596                    resolver.resolve_module(specifier, parent, mode)
3597                });
3598                if resolved.is_none() && self.has_module_reader() {
3599                    return None;
3600                }
3601                Some(LocalBridgeCallResult::Immediate(
3602                    resolved.map(Value::String).unwrap_or(Value::Null),
3603                ))
3604            }
3605            "_moduleFormat" => {
3606                let format = self.module_format(args.first().and_then(Value::as_str).unwrap_or(""));
3607                if format.is_none() && self.has_module_reader() {
3608                    return None;
3609                }
3610                Some(LocalBridgeCallResult::Immediate(
3611                    format
3612                        .map(|format| Value::String(String::from(format.as_str())))
3613                        .unwrap_or(Value::Null),
3614                ))
3615            }
3616            "_loadFile" | "_loadFileSync" => {
3617                let source = self.load_file(args.first().and_then(Value::as_str).unwrap_or(""));
3618                if source.is_none() && self.has_module_reader() {
3619                    return None;
3620                }
3621                Some(LocalBridgeCallResult::Immediate(
3622                    source.map(Value::String).unwrap_or(Value::Null),
3623                ))
3624            }
3625            "_batchResolveModules" => {
3626                let resolved = self.batch_resolve_modules(args);
3627                if self.has_module_reader()
3628                    && resolved
3629                        .as_array()
3630                        .is_some_and(|items| items.iter().any(Value::is_null))
3631                {
3632                    return None;
3633                }
3634                Some(LocalBridgeCallResult::Immediate(resolved))
3635            }
3636            "_loadPolyfill" => Some(LocalBridgeCallResult::Immediate(
3637                self.handle_polyfill_dispatch(args),
3638            )),
3639            "_cryptoRandomFill" => {
3640                let size = args.first().and_then(Value::as_u64).unwrap_or(16) as usize;
3641                let mut bytes = vec![0u8; size];
3642                if getrandom(&mut bytes).is_err() {
3643                    return Some(LocalBridgeCallResult::Immediate(Value::Null));
3644                }
3645                Some(LocalBridgeCallResult::Immediate(Value::String(
3646                    v8_runtime::base64_encode_pub(&bytes),
3647                )))
3648            }
3649            "_cryptoRandomUUID" => {
3650                let mut bytes = [0u8; 16];
3651                if getrandom(&mut bytes).is_err() {
3652                    return Some(LocalBridgeCallResult::Immediate(Value::Null));
3653                }
3654                bytes[6] = (bytes[6] & 0x0f) | 0x40;
3655                bytes[8] = (bytes[8] & 0x3f) | 0x80;
3656
3657                Some(LocalBridgeCallResult::Immediate(Value::String(format!(
3658                    "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
3659                    bytes[0],
3660                    bytes[1],
3661                    bytes[2],
3662                    bytes[3],
3663                    bytes[4],
3664                    bytes[5],
3665                    bytes[6],
3666                    bytes[7],
3667                    bytes[8],
3668                    bytes[9],
3669                    bytes[10],
3670                    bytes[11],
3671                    bytes[12],
3672                    bytes[13],
3673                    bytes[14],
3674                    bytes[15],
3675                ))))
3676            }
3677            "_kernelStdinRead" | "_kernelStdinReadRaw" if self.forward_kernel_stdin_rpc => None,
3678            "_kernelStdinRead" | "_kernelStdinReadRaw" => Some(LocalBridgeCallResult::Immediate(
3679                self.kernel_stdin.read(args),
3680            )),
3681            "_pythonStdinRead" => Some(LocalBridgeCallResult::Immediate(
3682                self.kernel_stdin.read_python_raw(args),
3683            )),
3684            "_scheduleTimer" => {
3685                self.schedule_bridge_timer_response(call_id, timer_delay_ms(args.first()));
3686                Some(LocalBridgeCallResult::Deferred)
3687            }
3688            _ => None,
3689        }
3690    }
3691
3692    fn handle_polyfill_dispatch(&mut self, args: &[Value]) -> Value {
3693        let Some(dispatch) = args.first().and_then(Value::as_str) else {
3694            return Value::Null;
3695        };
3696        if !dispatch.starts_with("__bd:") {
3697            return polyfill_expression(dispatch)
3698                .map(Value::String)
3699                .unwrap_or(Value::Null);
3700        }
3701        let (dispatch_method, payload_json) = dispatch
3702            .strip_prefix("__bd:")
3703            .and_then(|value| value.split_once(':'))
3704            .unwrap_or(("", "[]"));
3705        let payload = serde_json::from_str::<Value>(payload_json).unwrap_or_else(|_| json!([]));
3706        let args = payload.as_array().cloned().unwrap_or_default();
3707        let result = match dispatch_method {
3708            "kernelHandleRegister" => {
3709                if let (Some(id), Some(description)) = (
3710                    args.first().and_then(Value::as_str),
3711                    args.get(1).and_then(Value::as_str),
3712                ) {
3713                    self.handle_descriptions
3714                        .insert(id.to_owned(), description.to_owned());
3715                }
3716                Value::Null
3717            }
3718            "kernelHandleUnregister" => {
3719                if let Some(id) = args.first().and_then(Value::as_str) {
3720                    self.handle_descriptions.remove(id);
3721                }
3722                Value::Null
3723            }
3724            "kernelHandleList" => Value::Array(
3725                self.handle_descriptions
3726                    .iter()
3727                    .map(|(id, description)| {
3728                        json!({
3729                            "id": id,
3730                            "description": description,
3731                        })
3732                    })
3733                    .collect(),
3734            ),
3735            "kernelTimerCreate" => {
3736                let delay_ms = timer_delay_ms(args.first());
3737                let repeat = args.get(1).and_then(Value::as_bool).unwrap_or(false);
3738                json!(self.create_kernel_timer(delay_ms, repeat))
3739            }
3740            "kernelTimerArm" => {
3741                if let Some(timer_id) = args.first().and_then(Value::as_u64) {
3742                    self.arm_kernel_timer(timer_id);
3743                }
3744                Value::Null
3745            }
3746            "kernelTimerClear" => {
3747                if let Some(timer_id) = args.first().and_then(Value::as_u64) {
3748                    self.clear_kernel_timer(timer_id);
3749                }
3750                Value::Null
3751            }
3752            _ => json!({
3753                "__bd_error": {
3754                    "name": "Error",
3755                    "message": format!("No handler: {dispatch_method}"),
3756                }
3757            }),
3758        };
3759
3760        if dispatch_method.starts_with("kernel") {
3761            Value::String(
3762                serde_json::to_string(&json!({ "__bd_result": result }))
3763                    .unwrap_or_else(|_| String::from("{\"__bd_result\":null}")),
3764            )
3765        } else {
3766            Value::String(
3767                serde_json::to_string(&json!({
3768                    "__bd_error": {
3769                        "name": "Error",
3770                        "message": format!("No handler: {dispatch_method}"),
3771                    }
3772                }))
3773                .unwrap_or_else(|_| {
3774                    String::from(
3775                        "{\"__bd_error\":{\"name\":\"Error\",\"message\":\"dispatch failed\"}}",
3776                    )
3777                }),
3778            )
3779        }
3780    }
3781
3782    fn create_kernel_timer(&mut self, delay_ms: u64, repeat: bool) -> u64 {
3783        self.next_timer_id += 1;
3784        if let Ok(mut timers) = self.timers.lock() {
3785            timers.insert(
3786                self.next_timer_id,
3787                LocalTimerEntry {
3788                    delay_ms,
3789                    generation: 0,
3790                    repeat,
3791                },
3792            );
3793        }
3794        self.next_timer_id
3795    }
3796
3797    /// Allocate a fresh timer id and register a one-shot (`repeat == false`)
3798    /// tracking entry at generation 0. Used by the bridge-timer path so the
3799    /// queued wheel action can be cancelled (its entry removed) on `clear`/teardown.
3800    fn register_oneshot_timer(&mut self, delay_ms: u64) -> u64 {
3801        self.next_timer_id += 1;
3802        let timer_id = self.next_timer_id;
3803        if let Ok(mut timers) = self.timers.lock() {
3804            timers.insert(
3805                timer_id,
3806                LocalTimerEntry {
3807                    delay_ms,
3808                    generation: 0,
3809                    repeat: false,
3810                },
3811            );
3812        }
3813        timer_id
3814    }
3815
3816    fn arm_kernel_timer(&self, timer_id: u64) {
3817        let Some(session) = self.v8_session.clone() else {
3818            return;
3819        };
3820
3821        let Some((delay_ms, generation, timers)) =
3822            self.timers.lock().ok().and_then(|mut timers| {
3823                let entry = timers.get_mut(&timer_id)?;
3824                entry.generation = entry.generation.wrapping_add(1);
3825                Some((entry.delay_ms, entry.generation, self.timers.clone()))
3826            })
3827        else {
3828            return;
3829        };
3830
3831        TimerWheel::get().schedule(
3832            delay_ms,
3833            TimerAction::StreamEvent {
3834                session,
3835                timer_id,
3836                generation,
3837                timers,
3838            },
3839        );
3840    }
3841
3842    fn clear_kernel_timer(&self, timer_id: u64) {
3843        if let Ok(mut timers) = self.timers.lock() {
3844            timers.remove(&timer_id);
3845        }
3846    }
3847
3848    fn schedule_bridge_timer_response(&mut self, call_id: u64, delay_ms: u64) {
3849        let Some(session) = self.v8_session.clone() else {
3850            return;
3851        };
3852
3853        // Register the bridge timer in the shared `timers` map with a generation,
3854        // mirroring the kernel-timer cancellation path. Tracking it means that
3855        // when `LocalBridgeState` is dropped on session teardown (which clears the
3856        // map) or the entry is otherwise removed, the timer wheel observes the
3857        // missing/mismatched generation via `timer_should_fire` and suppresses the
3858        // response instead of touching the torn-down session.
3859        let timer_id = self.register_oneshot_timer(delay_ms);
3860        let generation = 0;
3861        let timers = self.timers.clone();
3862
3863        TimerWheel::get().schedule(
3864            delay_ms,
3865            TimerAction::BridgeResponse {
3866                session,
3867                call_id,
3868                timer_id,
3869                generation,
3870                timers,
3871            },
3872        );
3873    }
3874
3875    fn has_module_reader(&self) -> bool {
3876        self.module_reader.is_some()
3877    }
3878
3879    fn batch_resolve_modules(&mut self, args: &[Value]) -> Value {
3880        self.with_module_resolver(|resolver| resolver.batch_resolve_modules(args))
3881    }
3882
3883    fn resolve_module(
3884        &mut self,
3885        specifier: &str,
3886        from_dir: &str,
3887        mode: ModuleResolveMode,
3888    ) -> Option<String> {
3889        if self.js_runtime_denies_specifier(specifier) {
3890            if std::env::var("AGENTOS_MODULE_READER_TRACE").is_ok() {
3891                eprintln!("resolve DENIED: {specifier} from {from_dir}");
3892            }
3893            return None;
3894        }
3895        let resolved =
3896            self.with_module_resolver(|resolver| resolver.resolve_module(specifier, from_dir, mode));
3897        if resolved.is_none() && std::env::var("AGENTOS_MODULE_READER_TRACE").is_ok() {
3898            eprintln!("resolve MISS: {specifier} from {from_dir} mode={mode:?}");
3899        }
3900        resolved
3901    }
3902
3903    /// jsRuntime resolution gate. Denies builtin and bare/relative imports per the
3904    /// configured `moduleResolution` and builtin allow-list, before the resolver
3905    /// touches the VFS. This is the authoritative chokepoint for the live
3906    /// shared-V8 path (both `import`/`import()` and `require`/`createRequire`
3907    /// route through `_resolveModule` -> here).
3908    fn js_runtime_denies_specifier(&self, specifier: &str) -> bool {
3909        let is_local = specifier.starts_with("./")
3910            || specifier.starts_with("../")
3911            || specifier == "."
3912            || specifier == ".."
3913            || specifier.starts_with('/')
3914            || specifier.starts_with("file:");
3915        match self.module_resolution {
3916            GuestModuleResolution::Node => false,
3917            // Relative permits local files only; bare specifiers and package
3918            // imports (`#...`) do not resolve.
3919            GuestModuleResolution::Relative => !is_local,
3920            // None denies every specifier, local included.
3921            GuestModuleResolution::None => true,
3922        }
3923    }
3924
3925    fn module_format(&mut self, path: &str) -> Option<LocalResolvedModuleFormat> {
3926        self.with_module_resolver(|resolver| resolver.module_format(path))
3927    }
3928
3929    fn load_file(&mut self, path: &str) -> Option<String> {
3930        self.with_module_resolver(|resolver| resolver.load_file(path))
3931    }
3932
3933    /// Run `f` against a resolver bound to this bridge's resolution cache, reading
3934    /// through the supplied VFS `module_reader` when present (the live VM path:
3935    /// resolution executes here on the bridge thread, reading the mounted
3936    /// `node_modules` filesystem in parallel with the service loop), or through
3937    /// the host-backed path translator otherwise (the legacy host-direct path
3938    /// used by `handle_internal_bridge_call_from_host_context`).
3939    fn with_module_resolver<T>(
3940        &mut self,
3941        f: impl FnOnce(&mut ModuleResolver<'_, &mut dyn ModuleFsReader>) -> T,
3942    ) -> T {
3943        let cache = &mut self.resolution_cache;
3944        if let Some(reader) = self.module_reader.as_deref_mut() {
3945            let reader: &mut dyn ModuleFsReader = reader;
3946            let mut resolver = ModuleResolver { reader, cache };
3947            f(&mut resolver)
3948        } else {
3949            let mut translator = &mut self.translator;
3950            let reader: &mut dyn ModuleFsReader = &mut translator;
3951            let mut resolver = ModuleResolver { reader, cache };
3952            f(&mut resolver)
3953        }
3954    }
3955}
3956
3957impl ModuleFsReader for &mut dyn ModuleFsReader {
3958    fn canonical_guest_path(&mut self, guest_path: &str) -> Option<String> {
3959        (**self).canonical_guest_path(guest_path)
3960    }
3961
3962    fn read_to_string(&mut self, guest_path: &str) -> Option<String> {
3963        (**self).read_to_string(guest_path)
3964    }
3965
3966    fn path_is_dir(&mut self, guest_path: &str) -> Option<bool> {
3967        (**self).path_is_dir(guest_path)
3968    }
3969
3970    fn path_exists(&mut self, guest_path: &str) -> bool {
3971        (**self).path_exists(guest_path)
3972    }
3973}
3974
3975impl ModuleFsReader for &mut GuestPathTranslator {
3976    fn canonical_guest_path(&mut self, guest_path: &str) -> Option<String> {
3977        GuestPathTranslator::canonical_guest_path(self, guest_path)
3978    }
3979
3980    fn read_to_string(&mut self, guest_path: &str) -> Option<String> {
3981        let host_path = self.guest_to_host(guest_path)?;
3982        fs::read_to_string(host_path).ok()
3983    }
3984
3985    fn path_is_dir(&mut self, guest_path: &str) -> Option<bool> {
3986        self.guest_to_host(guest_path)
3987            .and_then(|host_path| fs::metadata(host_path).ok())
3988            .map(|metadata| metadata.is_dir())
3989    }
3990
3991    fn path_exists(&mut self, guest_path: &str) -> bool {
3992        self.guest_to_host(guest_path)
3993            .map(|host_path| host_path.exists())
3994            .unwrap_or(false)
3995    }
3996}
3997
3998/// Standard Node module resolution executed as pure path algebra over a
3999/// [`ModuleFsReader`]. The same algorithm backs both the legacy host-direct
4000/// path (reader = host path translator) and the live VM path (reader = kernel
4001/// VFS), guaranteeing they resolve identically.
4002pub struct ModuleResolver<'a, R: ModuleFsReader> {
4003    reader: R,
4004    cache: &'a mut LocalModuleResolutionCache,
4005}
4006
4007impl<'a, R: ModuleFsReader> ModuleResolver<'a, R> {
4008    /// Construct a resolver over `reader`, reusing `cache` across calls. The
4009    /// cache must be persisted per-VM so cold-start resolution does not rebuild
4010    /// it on every dispatch.
4011    pub fn new(reader: R, cache: &'a mut LocalModuleResolutionCache) -> Self {
4012        Self { reader, cache }
4013    }
4014
4015    pub fn batch_resolve_modules(&mut self, args: &[Value]) -> Value {
4016        let requests = args
4017            .first()
4018            .and_then(Value::as_array)
4019            .cloned()
4020            .unwrap_or_default();
4021        Value::Array(
4022            requests
4023                .into_iter()
4024                .map(|request| {
4025                    let pair = request.as_array().cloned().unwrap_or_default();
4026                    let specifier = pair.first().and_then(Value::as_str).unwrap_or("");
4027                    let referrer = pair.get(1).and_then(Value::as_str).unwrap_or("/");
4028                    self.resolve_module(specifier, referrer, ModuleResolveMode::Import)
4029                        .and_then(|resolved| {
4030                            self.load_file(&resolved).map(|source| {
4031                                json!({
4032                                    "resolved": resolved,
4033                                    "source": source,
4034                                })
4035                            })
4036                        })
4037                        .unwrap_or(Value::Null)
4038                })
4039                .collect(),
4040        )
4041    }
4042
4043    pub fn resolve_module(
4044        &mut self,
4045        specifier: &str,
4046        from_dir: &str,
4047        mode: ModuleResolveMode,
4048    ) -> Option<String> {
4049        let normalized_from_path = self
4050            .reader
4051            .canonical_guest_path(from_dir)
4052            .unwrap_or_else(|| normalize_guest_path(from_dir));
4053        let normalized_from = if self.cached_stat(&normalized_from_path) == Some(false) {
4054            dirname_guest_path(&normalized_from_path)
4055        } else {
4056            normalize_module_resolve_context(&normalized_from_path)
4057        };
4058        let cache_key = (specifier.to_owned(), normalized_from.clone(), mode);
4059        if let Some(cached) = self.cache.resolve_results.get(&cache_key) {
4060            return cached.clone();
4061        }
4062
4063        let resolved = if let Some(builtin) = normalize_builtin_specifier(specifier) {
4064            Some(builtin)
4065        } else if specifier.starts_with("file:") {
4066            guest_path_from_file_url(specifier)
4067                .and_then(|file_path| self.resolve_path(&file_path, mode))
4068        } else if specifier.starts_with('/') {
4069            self.resolve_path(specifier, mode)
4070        } else if specifier.starts_with("./")
4071            || specifier.starts_with("../")
4072            || specifier == "."
4073            || specifier == ".."
4074        {
4075            self.resolve_path(&join_guest_path(&normalized_from, specifier), mode)
4076        } else if specifier.starts_with('#') {
4077            self.resolve_package_imports(specifier, &normalized_from, mode)
4078        } else {
4079            self.resolve_package_self_reference(specifier, &normalized_from, mode)
4080                .or_else(|| self.resolve_node_modules(specifier, &normalized_from, mode))
4081        };
4082
4083        if resolved.is_some() || module_resolution_miss_is_stable(&normalized_from) {
4084            self.cache
4085                .resolve_results
4086                .insert(cache_key, resolved.clone());
4087        }
4088        resolved
4089    }
4090
4091    pub fn load_file(&mut self, path: &str) -> Option<String> {
4092        let bare = path.trim_start_matches("node:");
4093        if is_builtin_specifier(path) {
4094            return Some(build_builtin_module_wrapper(bare));
4095        }
4096
4097        let source = self.reader.read_to_string(path)?;
4098        Some(
4099            if matches!(
4100                Path::new(path).extension().and_then(|ext| ext.to_str()),
4101                Some("js" | "mjs" | "cjs")
4102            ) {
4103                strip_javascript_hashbang(&source)
4104            } else {
4105                source
4106            },
4107        )
4108    }
4109
4110    pub fn module_format(&mut self, path: &str) -> Option<LocalResolvedModuleFormat> {
4111        if let Some(cached) = self.cache.module_format_results.get(path) {
4112            return *cached;
4113        }
4114
4115        let format = self.detect_module_format(path);
4116        self.cache
4117            .module_format_results
4118            .insert(path.to_owned(), format);
4119        format
4120    }
4121
4122    fn detect_module_format(&mut self, path: &str) -> Option<LocalResolvedModuleFormat> {
4123        if is_builtin_specifier(path) {
4124            return Some(LocalResolvedModuleFormat::Module);
4125        }
4126
4127        let normalized = normalize_guest_path(path);
4128        match Path::new(&normalized)
4129            .extension()
4130            .and_then(|ext| ext.to_str())
4131        {
4132            Some("mjs" | "mts") => Some(LocalResolvedModuleFormat::Module),
4133            Some("cjs" | "cts") => Some(LocalResolvedModuleFormat::Commonjs),
4134            Some("json") => Some(LocalResolvedModuleFormat::Json),
4135            Some("js") => Some(
4136                if self
4137                    .nearest_package_json_type_for_guest_path(&normalized)
4138                    .as_deref()
4139                    == Some("module")
4140                {
4141                    LocalResolvedModuleFormat::Module
4142                } else {
4143                    LocalResolvedModuleFormat::Commonjs
4144                },
4145            ),
4146            _ => None,
4147        }
4148    }
4149
4150    fn nearest_package_json_type_for_guest_path(&mut self, guest_path: &str) -> Option<String> {
4151        let mut dir = dirname_guest_path(guest_path);
4152        loop {
4153            let package_json_path = join_guest_path(&dir, "package.json");
4154            if let Some(package_json) = self.read_package_json(&package_json_path) {
4155                return package_json.package_type;
4156            }
4157            if dir == "/" {
4158                break;
4159            }
4160            dir = dirname_guest_path(&dir);
4161        }
4162        None
4163    }
4164
4165    fn resolve_package_imports(
4166        &mut self,
4167        request: &str,
4168        from_dir: &str,
4169        mode: ModuleResolveMode,
4170    ) -> Option<String> {
4171        let mut dir = normalize_guest_path(from_dir);
4172        loop {
4173            let pkg_json_path = join_guest_path(&dir, "package.json");
4174            if let Some(pkg_json) = self.read_package_json(&pkg_json_path) {
4175                if let Some(imports) = &pkg_json.imports {
4176                    if let Some(target) = resolve_imports_target(imports, request, mode) {
4177                        let target_path = if target.starts_with('/') {
4178                            target
4179                        } else {
4180                            join_guest_path(&dir, &target)
4181                        };
4182                        return self.resolve_path(&target_path, mode);
4183                    }
4184                    return None;
4185                }
4186            }
4187            if dir == "/" {
4188                break;
4189            }
4190            dir = dirname_guest_path(&dir);
4191        }
4192        None
4193    }
4194
4195    fn resolve_package_self_reference(
4196        &mut self,
4197        request: &str,
4198        from_dir: &str,
4199        mode: ModuleResolveMode,
4200    ) -> Option<String> {
4201        let (package_name, subpath) = split_package_request(request)?;
4202        let mut dir = normalize_guest_path(from_dir);
4203        loop {
4204            let pkg_json_path = join_guest_path(&dir, "package.json");
4205            if let Some(pkg_json) = self.read_package_json(&pkg_json_path) {
4206                if pkg_json.name.as_deref() == Some(package_name) {
4207                    return self.resolve_package_entry_from_dir(&dir, subpath, mode);
4208                }
4209            }
4210            if dir == "/" {
4211                break;
4212            }
4213            dir = dirname_guest_path(&dir);
4214        }
4215        None
4216    }
4217
4218    fn resolve_node_modules(
4219        &mut self,
4220        request: &str,
4221        from_dir: &str,
4222        mode: ModuleResolveMode,
4223    ) -> Option<String> {
4224        let (package_name, subpath) = split_package_request(request)?;
4225
4226        // Standard Node resolution over the faithful VFS: walk ancestor
4227        // `node_modules` directories (following symlinks via the importer's
4228        // realpath). pnpm/yarn layouts resolve because the VFS exposes their
4229        // symlinks, not because the resolver understands package-manager
4230        // internals (see CLAUDE.md npm Compatibility).
4231        let mut dir = normalize_guest_path(from_dir);
4232        loop {
4233            for package_dir in node_modules_direct_candidate_dirs(&dir, package_name) {
4234                if let Some(entry) =
4235                    self.resolve_package_entry_from_dir(&package_dir, subpath, mode)
4236                {
4237                    return Some(entry);
4238                }
4239            }
4240            if dir == "/" {
4241                break;
4242            }
4243            dir = dirname_guest_path(&dir);
4244        }
4245
4246        ["/root/node_modules", "/node_modules"]
4247            .into_iter()
4248            .find_map(|root| {
4249                self.resolve_package_entry_from_dir(
4250                    &join_guest_path(root, package_name),
4251                    subpath,
4252                    mode,
4253                )
4254            })
4255    }
4256
4257    fn resolve_package_entry_from_dir(
4258        &mut self,
4259        package_dir: &str,
4260        subpath: &str,
4261        mode: ModuleResolveMode,
4262    ) -> Option<String> {
4263        let package_json_path = join_guest_path(package_dir, "package.json");
4264        let pkg_json = self.read_package_json(&package_json_path);
4265        if pkg_json.is_none() && !self.cached_exists(package_dir) {
4266            return None;
4267        }
4268
4269        if let Some(pkg_json) = pkg_json.as_ref() {
4270            if let Some(exports) = &pkg_json.exports {
4271                let exports_subpath = if subpath.is_empty() {
4272                    String::from(".")
4273                } else {
4274                    format!("./{subpath}")
4275                };
4276                let exports_target = resolve_exports_target(exports, &exports_subpath, mode)?;
4277                let target_path = join_guest_path(package_dir, &exports_target);
4278                return self.resolve_path(&target_path, mode).or(Some(target_path));
4279            }
4280        }
4281
4282        if !subpath.is_empty() {
4283            return self.resolve_path(&join_guest_path(package_dir, subpath), mode);
4284        }
4285
4286        let entry_field = pkg_json
4287            .as_ref()
4288            .and_then(|pkg_json| pkg_json.main.as_deref())
4289            .unwrap_or("index.js");
4290        let entry_path = join_guest_path(package_dir, entry_field);
4291        self.resolve_path(&entry_path, mode)
4292            .or_else(|| self.resolve_path(&join_guest_path(package_dir, "index"), mode))
4293    }
4294
4295    fn resolve_path(&mut self, base_path: &str, mode: ModuleResolveMode) -> Option<String> {
4296        if self.cached_stat(base_path) == Some(false) {
4297            return Some(normalize_guest_path(base_path));
4298        }
4299
4300        for extension in [".js", ".json", ".mjs", ".cjs"] {
4301            let candidate = format!("{}{}", normalize_guest_path(base_path), extension);
4302            if self.cached_exists(&candidate) {
4303                return Some(candidate);
4304            }
4305        }
4306
4307        if self.cached_stat(base_path) == Some(true) {
4308            let pkg_json_path = join_guest_path(base_path, "package.json");
4309            if let Some(pkg_json) = self.read_package_json(&pkg_json_path) {
4310                if let Some(main) = pkg_json.main.as_deref() {
4311                    let entry_path = join_guest_path(base_path, main);
4312                    if entry_path != normalize_guest_path(base_path) {
4313                        if let Some(entry) = self.resolve_path(&entry_path, mode) {
4314                            return Some(entry);
4315                        }
4316                    }
4317                }
4318                if mode == ModuleResolveMode::Import
4319                    && pkg_json.package_type.as_deref() == Some("module")
4320                    && self.cached_exists(&join_guest_path(base_path, "index.js"))
4321                {
4322                    return Some(join_guest_path(base_path, "index.js"));
4323                }
4324            }
4325
4326            for extension in [".js", ".json", ".mjs", ".cjs"] {
4327                let index_path = join_guest_path(base_path, &format!("index{extension}"));
4328                if self.cached_exists(&index_path) {
4329                    return Some(index_path);
4330                }
4331            }
4332        }
4333
4334        None
4335    }
4336
4337    fn read_package_json(&mut self, guest_path: &str) -> Option<LocalPackageJson> {
4338        if let Some(cached) = self.cache.package_json_results.get(guest_path).cloned() {
4339            return cached;
4340        }
4341
4342        let parsed = self
4343            .reader
4344            .read_to_string(guest_path)
4345            .and_then(|contents| serde_json::from_str::<LocalPackageJson>(&contents).ok());
4346        if parsed.is_some() || module_path_miss_is_stable(guest_path) {
4347            self.cache
4348                .package_json_results
4349                .insert(guest_path.to_owned(), parsed.clone());
4350        }
4351        parsed
4352    }
4353
4354    fn cached_exists(&mut self, guest_path: &str) -> bool {
4355        if let Some(cached) = self.cache.exists_results.get(guest_path) {
4356            return *cached;
4357        }
4358        let exists = self.reader.path_exists(guest_path);
4359        if exists || module_path_miss_is_stable(guest_path) {
4360            self.cache
4361                .exists_results
4362                .insert(guest_path.to_owned(), exists);
4363        }
4364        exists
4365    }
4366
4367    fn cached_stat(&mut self, guest_path: &str) -> Option<bool> {
4368        if let Some(cached) = self.cache.stat_results.get(guest_path) {
4369            return *cached;
4370        }
4371        let result = self.reader.path_is_dir(guest_path);
4372        if result.is_some() || module_path_miss_is_stable(guest_path) {
4373            self.cache
4374                .stat_results
4375                .insert(guest_path.to_owned(), result);
4376        }
4377        result
4378    }
4379}
4380
4381fn module_resolution_miss_is_stable(from_dir: &str) -> bool {
4382    module_path_miss_is_stable(from_dir)
4383}
4384
4385fn module_path_miss_is_stable(guest_path: &str) -> bool {
4386    guest_path == "/node_modules"
4387        || guest_path.ends_with("/node_modules")
4388        || guest_path.contains("/node_modules/")
4389}
4390
4391fn guest_path_from_file_url(specifier: &str) -> Option<String> {
4392    if !specifier.starts_with("file:") {
4393        return None;
4394    }
4395
4396    let mut pathname = if let Some(stripped) = specifier.strip_prefix("file://") {
4397        stripped
4398    } else {
4399        specifier.strip_prefix("file:")?
4400    };
4401
4402    if let Some(terminator_index) = pathname.find(['?', '#']) {
4403        pathname = &pathname[..terminator_index];
4404    }
4405
4406    if !pathname.starts_with('/') {
4407        let slash_index = pathname.find('/')?;
4408        let host = &pathname[..slash_index];
4409        if !host.is_empty() && host != "localhost" {
4410            return None;
4411        }
4412        pathname = &pathname[slash_index..];
4413    }
4414
4415    Some(normalize_guest_path(&percent_decode(pathname)?))
4416}
4417
4418fn percent_decode(raw: &str) -> Option<String> {
4419    let bytes = raw.as_bytes();
4420    let mut index = 0;
4421    let mut decoded = Vec::with_capacity(bytes.len());
4422    while index < bytes.len() {
4423        match bytes[index] {
4424            b'%' if index + 2 < bytes.len() => {
4425                if let (Some(high), Some(low)) =
4426                    (hex_digit(bytes[index + 1]), hex_digit(bytes[index + 2]))
4427                {
4428                    let value = (high << 4) | low;
4429                    decoded.push(value);
4430                    index += 3;
4431                } else {
4432                    decoded.push(bytes[index]);
4433                    index += 1;
4434                }
4435            }
4436            byte => {
4437                decoded.push(byte);
4438                index += 1;
4439            }
4440        }
4441    }
4442    String::from_utf8(decoded).ok()
4443}
4444
4445fn hex_digit(byte: u8) -> Option<u8> {
4446    match byte {
4447        b'0'..=b'9' => Some(byte - b'0'),
4448        b'a'..=b'f' => Some(byte - b'a' + 10),
4449        b'A'..=b'F' => Some(byte - b'A' + 10),
4450        _ => None,
4451    }
4452}
4453
4454impl LocalKernelStdinBridge {
4455    fn write(&self, chunk: &[u8]) -> Result<(), JavascriptExecutionError> {
4456        let mut state = self.state.lock().expect("kernel stdin state poisoned");
4457        if state.closed {
4458            return Err(JavascriptExecutionError::StdinClosed);
4459        }
4460        let next_len = state.bytes.len().checked_add(chunk.len()).ok_or_else(|| {
4461            JavascriptExecutionError::Stdin(std::io::Error::new(
4462                std::io::ErrorKind::InvalidData,
4463                format!("guest stdin buffer exceeded {KERNEL_STDIN_BUFFER_LIMIT_BYTES} bytes"),
4464            ))
4465        })?;
4466        if next_len > KERNEL_STDIN_BUFFER_LIMIT_BYTES {
4467            return Err(JavascriptExecutionError::Stdin(std::io::Error::new(
4468                std::io::ErrorKind::InvalidData,
4469                format!("guest stdin buffer exceeded {KERNEL_STDIN_BUFFER_LIMIT_BYTES} bytes"),
4470            )));
4471        }
4472
4473        state.bytes.extend(chunk.iter().copied());
4474        self.ready.notify_all();
4475        Ok(())
4476    }
4477
4478    fn close(&self) {
4479        let mut state = self.state.lock().expect("kernel stdin state poisoned");
4480        state.closed = true;
4481        self.ready.notify_all();
4482    }
4483
4484    fn read(&self, args: &[Value]) -> Value {
4485        let max_bytes = args
4486            .first()
4487            .and_then(Value::as_u64)
4488            .map(|value| value.clamp(1, 64 * 1024) as usize)
4489            .unwrap_or(64 * 1024);
4490        let timeout = Duration::from_millis(args.get(1).and_then(Value::as_u64).unwrap_or(100));
4491        let deadline = Instant::now() + timeout;
4492        let mut state = self.state.lock().expect("kernel stdin state poisoned");
4493
4494        while state.bytes.is_empty() && !state.closed {
4495            let remaining = deadline.saturating_duration_since(Instant::now());
4496            if remaining.is_zero() {
4497                return Value::Null;
4498            }
4499            let (next_state, wait_result) = self
4500                .ready
4501                .wait_timeout(state, remaining)
4502                .expect("kernel stdin wait poisoned");
4503            state = next_state;
4504            if wait_result.timed_out() && state.bytes.is_empty() && !state.closed {
4505                return Value::Null;
4506            }
4507        }
4508
4509        if !state.bytes.is_empty() {
4510            let read_len = state.bytes.len().min(max_bytes);
4511            let bytes = state.bytes.drain(..read_len).collect::<Vec<_>>();
4512            return json!({
4513                "dataBase64": v8_runtime::base64_encode_pub(&bytes),
4514            });
4515        }
4516
4517        json!({
4518            "done": true,
4519        })
4520    }
4521
4522    fn read_python_raw(&self, args: &[Value]) -> Value {
4523        const PYTHON_STDIN_DONE_SENTINEL: &str = "__AGENTOS_PYTHON_STDIN_DONE__";
4524
4525        let max_bytes = args
4526            .first()
4527            .and_then(Value::as_u64)
4528            .map(|value| value.clamp(1, 64 * 1024) as usize)
4529            .unwrap_or(64 * 1024);
4530        let timeout = Duration::from_millis(args.get(1).and_then(Value::as_u64).unwrap_or(100));
4531        let deadline = Instant::now() + timeout;
4532        let mut state = self.state.lock().expect("kernel stdin state poisoned");
4533
4534        while state.bytes.is_empty() && !state.closed {
4535            let remaining = deadline.saturating_duration_since(Instant::now());
4536            if remaining.is_zero() {
4537                return Value::Null;
4538            }
4539            let (next_state, wait_result) = self
4540                .ready
4541                .wait_timeout(state, remaining)
4542                .expect("kernel stdin wait poisoned");
4543            state = next_state;
4544            if wait_result.timed_out() && state.bytes.is_empty() && !state.closed {
4545                return Value::Null;
4546            }
4547        }
4548
4549        if !state.bytes.is_empty() {
4550            let read_len = state.bytes.len().min(max_bytes);
4551            let bytes = state.bytes.drain(..read_len).collect::<Vec<_>>();
4552            return Value::String(v8_runtime::base64_encode_pub(&bytes));
4553        }
4554
4555        Value::String(String::from(PYTHON_STDIN_DONE_SENTINEL))
4556    }
4557}
4558
4559fn normalize_module_resolve_context(path: &str) -> String {
4560    let normalized = normalize_guest_path(path);
4561    if normalized.ends_with(".js")
4562        || normalized.ends_with(".mjs")
4563        || normalized.ends_with(".cjs")
4564        || normalized.ends_with(".json")
4565        || normalized.ends_with(".ts")
4566        || normalized.ends_with(".mts")
4567        || normalized.ends_with(".cts")
4568    {
4569        dirname_guest_path(&normalized)
4570    } else {
4571        normalized
4572    }
4573}
4574
4575fn strip_javascript_hashbang(source: &str) -> String {
4576    if let Some(stripped) = source.strip_prefix("#!") {
4577        match stripped.find('\n') {
4578            Some(index) => format!("\n{}", &stripped[index + 1..]),
4579            None => String::new(),
4580        }
4581    } else {
4582        source.to_owned()
4583    }
4584}
4585
4586fn parse_process_exit_code_message(message: &str) -> Option<i32> {
4587    let code = message.strip_prefix("process.exit(")?.strip_suffix(')')?;
4588    code.parse::<i32>().ok()
4589}
4590
4591fn dirname_guest_path(path: &str) -> String {
4592    let normalized = normalize_guest_path(path);
4593    if normalized == "/" {
4594        return normalized;
4595    }
4596    normalized
4597        .rsplit_once('/')
4598        .map(|(parent, _)| {
4599            if parent.is_empty() {
4600                String::from("/")
4601            } else {
4602                parent.to_owned()
4603            }
4604        })
4605        .unwrap_or_else(|| String::from("/"))
4606}
4607
4608fn normalize_builtin_specifier(specifier: &str) -> Option<String> {
4609    let bare = specifier.trim_start_matches("node:");
4610    match bare {
4611        "assert"
4612        | "async_hooks"
4613        | "buffer"
4614        | "child_process"
4615        | "cluster"
4616        | "console"
4617        | "constants"
4618        | "crypto"
4619        | "dgram"
4620        | "diagnostics_channel"
4621        | "dns"
4622        | "dns/promises"
4623        | "events"
4624        | "fs"
4625        | "fs/promises"
4626        | "http"
4627        | "http2"
4628        | "https"
4629        | "inspector"
4630        | "module"
4631        | "net"
4632        | "os"
4633        | "path"
4634        | "path/posix"
4635        | "path/win32"
4636        | "perf_hooks"
4637        | "process"
4638        | "punycode"
4639        | "querystring"
4640        | "readline"
4641        | "repl"
4642        | "sqlite"
4643        | "stream"
4644        | "stream/consumers"
4645        | "stream/promises"
4646        | "stream/web"
4647        | "string_decoder"
4648        | "sys"
4649        | "timers"
4650        | "tls"
4651        | "timers/promises"
4652        | "trace_events"
4653        | "tty"
4654        | "url"
4655        | "util"
4656        | "util/types"
4657        | "domain"
4658        | "vm"
4659        | "v8"
4660        | "wasi"
4661        | "worker_threads"
4662        | "zlib" => Some(format!("node:{bare}")),
4663        _ => None,
4664    }
4665}
4666
4667fn is_builtin_specifier(specifier: &str) -> bool {
4668    normalize_builtin_specifier(specifier).is_some()
4669}
4670
4671fn polyfill_expression(request: &str) -> Option<String> {
4672    let normalized = request.trim_start_matches("node:");
4673    let entry = polyfill_registry()
4674        .groups
4675        .iter()
4676        .find(|group| group.names.iter().any(|name| name == normalized))?;
4677
4678    Some(match entry.source {
4679        PolyfillSourceKind::NodeStdlibBrowser | PolyfillSourceKind::CustomBridge => format!(
4680            "globalThis._requireFrom({}, \"/\")",
4681            serde_json::to_string(&format!("node:{normalized}"))
4682                .unwrap_or_else(|_| format!("\"node:{normalized}\""))
4683        ),
4684        PolyfillSourceKind::Denied => {
4685            let error_code = entry.error_code.as_deref().unwrap_or("ERR_ACCESS_DENIED");
4686            format!(
4687                "(() => {{ const error = new Error({message}); error.code = {code}; throw error; }})()",
4688                message = serde_json::to_string(&format!(
4689                    "node:{normalized} is not available in the secure-exec guest runtime"
4690                ))
4691                .unwrap_or_else(|_| format!(
4692                    "\"node:{normalized} is not available in the secure-exec guest runtime\""
4693                )),
4694                code = serde_json::to_string(error_code)
4695                    .unwrap_or_else(|_| "\"ERR_ACCESS_DENIED\"".to_owned())
4696            )
4697        }
4698    })
4699}
4700
4701fn build_builtin_module_wrapper(module_name: &str) -> String {
4702    if module_name == "assert" {
4703        return String::from(
4704            r#"class AssertionError extends Error {
4705  constructor(message = "Assertion failed") {
4706    super(message);
4707    this.name = "AssertionError";
4708  }
4709}
4710
4711function fail(message) {
4712  throw new AssertionError(message);
4713}
4714
4715function ok(value, message) {
4716  if (!value) fail(message);
4717}
4718
4719function equal(actual, expected, message) {
4720  if (actual != expected) fail(message ?? `Expected ${actual} == ${expected}`);
4721}
4722
4723function notEqual(actual, expected, message) {
4724  if (actual == expected) fail(message ?? `Expected ${actual} != ${expected}`);
4725}
4726
4727function strictEqual(actual, expected, message) {
4728  if (actual !== expected) fail(message ?? `Expected ${actual} === ${expected}`);
4729}
4730
4731function notStrictEqual(actual, expected, message) {
4732  if (actual === expected) fail(message ?? `Expected ${actual} !== ${expected}`);
4733}
4734
4735function serialize(value) {
4736  return JSON.stringify(value);
4737}
4738
4739function deepEqual(actual, expected, message) {
4740  if (serialize(actual) !== serialize(expected)) {
4741    fail(message ?? "Expected values to be deeply equal");
4742  }
4743}
4744
4745function deepStrictEqual(actual, expected, message) {
4746  return deepEqual(actual, expected, message);
4747}
4748
4749function match(actual, expected, message) {
4750  if (!(expected instanceof RegExp) || !expected.test(String(actual))) {
4751    fail(message ?? `Expected ${actual} to match ${expected}`);
4752  }
4753}
4754
4755function matchesExpectedError(error, expected) {
4756  if (expected == null) return true;
4757  if (expected instanceof RegExp) {
4758    return expected.test(String(error?.message ?? error));
4759  }
4760  if (typeof expected === "function") {
4761    if (error instanceof expected) return true;
4762    return expected(error) === true;
4763  }
4764  if (typeof expected === "object") {
4765    return Object.entries(expected).every(([key, value]) => serialize(error?.[key]) === serialize(value));
4766  }
4767  return false;
4768}
4769
4770function throws(fn, expected, message) {
4771  if (typeof fn !== "function") {
4772    fail(message ?? "assert.throws requires a function");
4773  }
4774
4775  try {
4776    fn();
4777  } catch (error) {
4778    if (!matchesExpectedError(error, expected)) {
4779      throw error;
4780    }
4781    return error;
4782  }
4783
4784  fail(message ?? "Missing expected exception");
4785}
4786
4787async function rejects(promiseOrFn, expected, message) {
4788  let promise;
4789  if (typeof promiseOrFn === "function") {
4790    promise = promiseOrFn();
4791  } else {
4792    promise = promiseOrFn;
4793  }
4794
4795  try {
4796    await promise;
4797  } catch (error) {
4798    if (!matchesExpectedError(error, expected)) {
4799      throw error;
4800    }
4801    return error;
4802  }
4803
4804  fail(message ?? "Missing expected rejection");
4805}
4806
4807function ifError(error) {
4808  if (error != null) {
4809    throw error;
4810  }
4811}
4812
4813function assert(value, message) {
4814  ok(value, message);
4815}
4816
4817Object.assign(assert, {
4818  AssertionError,
4819  deepEqual,
4820  deepStrictEqual,
4821  equal,
4822  fail,
4823  ifError,
4824  match,
4825  notEqual,
4826  notStrictEqual,
4827  ok,
4828  rejects,
4829  strict: assert,
4830  strictEqual,
4831  throws,
4832});
4833
4834export {
4835  AssertionError,
4836  assert as default,
4837  deepEqual,
4838  deepStrictEqual,
4839  equal,
4840  fail,
4841  ifError,
4842  match,
4843  notEqual,
4844  notStrictEqual,
4845  ok,
4846  rejects,
4847  assert as strict,
4848  strictEqual,
4849  throws,
4850};
4851"#,
4852        );
4853    }
4854
4855    if module_name == "path" || module_name == "path/posix" || module_name == "path/win32" {
4856        return String::from(
4857            r#"const sep = "/";
4858const delimiter = ":";
4859
4860function normalizeSegments(parts) {
4861  const output = [];
4862  for (const part of parts) {
4863    if (!part || part === ".") continue;
4864    if (part === "..") {
4865      if (output.length > 0) output.pop();
4866      continue;
4867    }
4868    output.push(part);
4869  }
4870  return output;
4871}
4872
4873function isAbsolute(path) {
4874  return String(path || "").startsWith(sep);
4875}
4876
4877function join(...parts) {
4878  const absolute = parts.some((part, index) => index === 0 && isAbsolute(part));
4879  const normalized = normalizeSegments(parts.flatMap((part) => String(part || "").split(sep)));
4880  const joined = normalized.join(sep);
4881  if (!joined) return absolute ? sep : ".";
4882  return absolute ? `${sep}${joined}` : joined;
4883}
4884
4885function dirname(path) {
4886  const normalized = String(path || "");
4887  if (!normalized || normalized === sep) return sep;
4888  const parts = normalizeSegments(normalized.split(sep));
4889  if (parts.length <= 1) return isAbsolute(normalized) ? sep : ".";
4890  const dir = parts.slice(0, -1).join(sep);
4891  return isAbsolute(normalized) ? `${sep}${dir}` : dir;
4892}
4893
4894function basename(path) {
4895  const normalized = normalizeSegments(String(path || "").split(sep));
4896  return normalized.length === 0 ? "" : normalized[normalized.length - 1];
4897}
4898
4899function extname(path) {
4900  const base = basename(path);
4901  const index = base.lastIndexOf(".");
4902  if (index <= 0) return "";
4903  return base.slice(index);
4904}
4905
4906function resolve(...parts) {
4907  const absoluteParts = [];
4908  for (let index = parts.length - 1; index >= 0; index -= 1) {
4909    const part = String(parts[index] || "");
4910    if (!part) continue;
4911    absoluteParts.unshift(part);
4912    if (isAbsolute(part)) break;
4913  }
4914  if (absoluteParts.length === 0 || !isAbsolute(absoluteParts[0])) {
4915    absoluteParts.unshift(typeof process?.cwd === "function" ? process.cwd() : sep);
4916  }
4917  return join(...absoluteParts);
4918}
4919
4920function relative(from, to) {
4921  const fromResolved = resolve(from);
4922  const toResolved = resolve(to);
4923  if (fromResolved === toResolved) return "";
4924
4925  const fromParts = normalizeSegments(fromResolved.split(sep));
4926  const toParts = normalizeSegments(toResolved.split(sep));
4927  let shared = 0;
4928  while (
4929    shared < fromParts.length &&
4930    shared < toParts.length &&
4931    fromParts[shared] === toParts[shared]
4932  ) {
4933    shared += 1;
4934  }
4935
4936  const up = new Array(fromParts.length - shared).fill("..");
4937  const down = toParts.slice(shared);
4938  const result = [...up, ...down].join(sep);
4939  return result || ".";
4940}
4941
4942function parse(path) {
4943  const root = isAbsolute(path) ? sep : "";
4944  const dir = dirname(path);
4945  const base = basename(path);
4946  const ext = extname(path);
4947  const name = ext ? base.slice(0, -ext.length) : base;
4948  return { root, dir, base, ext, name };
4949}
4950
4951function format(pathObject = {}) {
4952  const dir = pathObject.dir || pathObject.root || "";
4953  const base =
4954    pathObject.base ||
4955    `${pathObject.name || ""}${pathObject.ext || ""}`;
4956  if (!dir) return base;
4957  if (!base) return dir;
4958  return dir.endsWith(sep) ? `${dir}${base}` : `${dir}${sep}${base}`;
4959}
4960
4961function normalize(path) {
4962  return join(String(path || ""));
4963}
4964
4965const pathModule = {
4966  basename,
4967  delimiter,
4968  dirname,
4969  extname,
4970  format,
4971  isAbsolute,
4972  join,
4973  normalize,
4974  parse,
4975  relative,
4976  resolve,
4977  sep,
4978};
4979const posix = pathModule;
4980const win32 = pathModule;
4981pathModule.posix = posix;
4982pathModule.win32 = win32;
4983
4984export { basename, delimiter, dirname, extname, format, isAbsolute, join, normalize, parse, posix, relative, resolve, sep, win32 };
4985export default pathModule;
4986"#,
4987        );
4988    }
4989
4990    if module_name == "url" {
4991        return String::from(
4992            r#"const NativeURL = globalThis.URL;
4993
4994function normalizeFilePath(value) {
4995  const path = String(value ?? "");
4996  if (path.length === 0) {
4997    return "/";
4998  }
4999  return path.startsWith("/") ? path : `/${path}`;
5000}
5001
5002function encodeFilePath(path) {
5003  return path
5004    .split("/")
5005    .map((segment, index) =>
5006      index === 0
5007        ? ""
5008        : encodeURIComponent(segment).replace(/[!'()*]/g, (char) =>
5009            `%${char.charCodeAt(0).toString(16).toUpperCase()}`
5010          )
5011    )
5012    .join("/");
5013}
5014
5015function buildFileUrlRecord(href, pathname) {
5016  const searchParams = new URLSearchParams();
5017  return {
5018    href,
5019    origin: "null",
5020    protocol: "file:",
5021    username: "",
5022    password: "",
5023    host: "",
5024    hostname: "",
5025    port: "",
5026    pathname,
5027    search: "",
5028    searchParams,
5029    hash: "",
5030    toString() {
5031      return href;
5032    },
5033    toJSON() {
5034      return href;
5035    },
5036    valueOf() {
5037      return href;
5038    },
5039    [Symbol.toPrimitive]() {
5040      return href;
5041    },
5042  };
5043}
5044
5045function fileURLToPath(value) {
5046  const raw =
5047    typeof value === "string"
5048      ? value
5049      : value && typeof value.href === "string"
5050        ? value.href
5051        : String(value ?? "");
5052  if (raw.startsWith("/")) {
5053    return raw;
5054  }
5055  if (raw.startsWith("file:")) {
5056    let pathname = raw.startsWith("file://")
5057      ? raw.slice("file://".length)
5058      : raw.slice("file:".length);
5059    const terminatorIndex = pathname.search(/[?#]/);
5060    if (terminatorIndex >= 0) {
5061      pathname = pathname.slice(0, terminatorIndex);
5062    }
5063    if (!pathname.startsWith("/")) {
5064      const slashIndex = pathname.indexOf("/");
5065      if (slashIndex === -1) {
5066        return "/";
5067      }
5068      const host = pathname.slice(0, slashIndex);
5069      if (host && host !== "localhost") {
5070        throw new Error(`Expected file URL with an empty host, received ${host}`);
5071      }
5072      pathname = pathname.slice(slashIndex);
5073    }
5074    return decodeURIComponent(pathname || "/");
5075  }
5076  const url = value instanceof NativeURL ? value : new NativeURL(raw);
5077  if (url.protocol !== "file:") {
5078    throw new Error(`Expected file URL, received ${url.protocol}`);
5079  }
5080  return decodeURIComponent(url.pathname);
5081}
5082
5083function pathToFileURL(path) {
5084  const absolute = normalizeFilePath(path);
5085  const pathname = encodeFilePath(absolute);
5086  const href = `file://${pathname}`;
5087
5088  try {
5089    return new NativeURL(href);
5090  } catch {}
5091
5092  return buildFileUrlRecord(href, pathname);
5093}
5094
5095function parse(input, parseQueryString = false) {
5096  const parsed = new NativeURL(String(input ?? ""));
5097  const queryString = parsed.search.length > 0 ? parsed.search.slice(1) : null;
5098  const auth =
5099    parsed.username || parsed.password
5100      ? `${decodeURIComponent(parsed.username)}${parsed.password ? `:${decodeURIComponent(parsed.password)}` : ""}`
5101      : null;
5102  return {
5103    href: parsed.href,
5104    protocol: parsed.protocol,
5105    slashes: true,
5106    auth,
5107    host: parsed.host,
5108    port: parsed.port || null,
5109    hostname: parsed.hostname,
5110    hash: parsed.hash || null,
5111    search: parsed.search || null,
5112    query: parseQueryString ? Object.fromEntries(parsed.searchParams.entries()) : queryString,
5113    pathname: parsed.pathname,
5114    path: `${parsed.pathname}${parsed.search}`,
5115  };
5116}
5117
5118function format(value) {
5119  if (value == null) return "";
5120  if (typeof value === "string") return value;
5121  if (typeof value.href === "string") return value.href;
5122
5123  const protocol = typeof value.protocol === "string" ? value.protocol : "http:";
5124  const slashes = value.slashes === false ? "" : "//";
5125  const auth =
5126    typeof value.auth === "string" && value.auth.length > 0 ? `${value.auth}@` : "";
5127  const host =
5128    typeof value.host === "string" && value.host.length > 0
5129      ? value.host
5130      : `${value.hostname || ""}${value.port ? `:${value.port}` : ""}`;
5131  const pathname =
5132    typeof value.pathname === "string"
5133      ? value.pathname
5134      : typeof value.path === "string"
5135        ? value.path
5136        : "";
5137
5138  let search = "";
5139  if (typeof value.search === "string") {
5140    search = value.search;
5141  } else if (typeof value.query === "string" && value.query.length > 0) {
5142    search = value.query.startsWith("?") ? value.query : `?${value.query}`;
5143  } else if (value.query && typeof value.query === "object") {
5144    const params = new URLSearchParams();
5145    for (const [key, entry] of Object.entries(value.query)) {
5146      if (Array.isArray(entry)) {
5147        for (const item of entry) {
5148          params.append(key, String(item));
5149        }
5150      } else if (entry != null) {
5151        params.append(key, String(entry));
5152      }
5153    }
5154    const encoded = params.toString();
5155    search = encoded ? `?${encoded}` : "";
5156  }
5157
5158  const hash = typeof value.hash === "string" ? value.hash : "";
5159  return `${protocol}${slashes}${auth}${host}${pathname}${search}${hash}`;
5160}
5161
5162export { NativeURL as URL, fileURLToPath, format, parse, pathToFileURL };
5163export default { URL: NativeURL, fileURLToPath, format, parse, pathToFileURL };
5164"#,
5165        );
5166    }
5167
5168    if module_name == "readline" {
5169        return String::from(
5170            r#"class MiniEmitter {
5171  constructor() {
5172    this.listeners = new Map();
5173  }
5174
5175  on(event, listener) {
5176    const listeners = this.listeners.get(event) ?? [];
5177    listeners.push(listener);
5178    this.listeners.set(event, listeners);
5179    return this;
5180  }
5181
5182  addListener(event, listener) {
5183    return this.on(event, listener);
5184  }
5185
5186  once(event, listener) {
5187    const wrapped = (...args) => {
5188      this.off(event, wrapped);
5189      listener(...args);
5190    };
5191    return this.on(event, wrapped);
5192  }
5193
5194  off(event, listener) {
5195    const listeners = this.listeners.get(event) ?? [];
5196    this.listeners.set(
5197      event,
5198      listeners.filter((candidate) => candidate !== listener),
5199    );
5200    return this;
5201  }
5202
5203  removeListener(event, listener) {
5204    return this.off(event, listener);
5205  }
5206
5207  emit(event, ...args) {
5208    const listeners = this.listeners.get(event) ?? [];
5209    for (const listener of listeners) {
5210      listener(...args);
5211    }
5212    return listeners.length > 0;
5213  }
5214}
5215
5216export function createInterface(options = {}) {
5217  const input = options.input ?? null;
5218  const output = options.output ?? null;
5219  const emitter = new MiniEmitter();
5220  let buffer = "";
5221  let closed = false;
5222  let ended = false;
5223  const queuedLines = [];
5224  let pendingResolve = null;
5225  const pendingQuestionResolves = [];
5226
5227  const enqueueLine = (line) => {
5228    if (pendingQuestionResolves.length > 0) {
5229      const resolve = pendingQuestionResolves.shift();
5230      resolve(line);
5231      return;
5232    }
5233    if (pendingResolve) {
5234      const resolve = pendingResolve;
5235      pendingResolve = null;
5236      resolve({ done: false, value: line });
5237      return;
5238    }
5239    queuedLines.push(line);
5240  };
5241
5242  const flush = () => {
5243    if (buffer.length > 0) {
5244      emitter.emit("line", buffer);
5245      enqueueLine(buffer);
5246      buffer = "";
5247    }
5248  };
5249
5250  const onData = (chunk) => {
5251    buffer += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
5252    while (true) {
5253      const index = buffer.indexOf("\n");
5254      if (index < 0) break;
5255      const line = buffer.slice(0, index).replace(/\r$/, "");
5256      buffer = buffer.slice(index + 1);
5257      emitter.emit("line", line);
5258      enqueueLine(line);
5259    }
5260  };
5261
5262  const onEnd = () => {
5263    if (ended) return;
5264    ended = true;
5265    flush();
5266    emitter.emit("close");
5267    while (pendingQuestionResolves.length > 0) {
5268      const resolve = pendingQuestionResolves.shift();
5269      resolve("");
5270    }
5271    if (pendingResolve) {
5272      const resolve = pendingResolve;
5273      pendingResolve = null;
5274      resolve({ done: true, value: void 0 });
5275    }
5276  };
5277
5278  if (input && typeof input.on === "function") {
5279    input.on("data", onData);
5280    input.on("end", onEnd);
5281    if (typeof input.resume === "function") {
5282      input.resume();
5283    }
5284  }
5285
5286  emitter.close = () => {
5287    if (closed) return;
5288    closed = true;
5289    if (input && typeof input.off === "function") {
5290      input.off("data", onData);
5291      input.off("end", onEnd);
5292    }
5293    flush();
5294    emitter.emit("close");
5295    while (pendingQuestionResolves.length > 0) {
5296      const resolve = pendingQuestionResolves.shift();
5297      resolve("");
5298    }
5299    if (pendingResolve) {
5300      const resolve = pendingResolve;
5301      pendingResolve = null;
5302      resolve({ done: true, value: void 0 });
5303    }
5304  };
5305
5306  emitter.question = (prompt, callback) => {
5307    if (output && typeof output.write === "function" && prompt) {
5308      output.write(String(prompt));
5309    }
5310    const readLine = () => {
5311      if (queuedLines.length > 0) {
5312        return Promise.resolve(queuedLines.shift());
5313      }
5314      if (closed || ended) {
5315        return Promise.resolve("");
5316      }
5317      return new Promise((resolve) => {
5318        pendingQuestionResolves.push(resolve);
5319      });
5320    };
5321    if (typeof callback === "function") {
5322      void readLine().then((line) => {
5323        callback(line);
5324      });
5325      return;
5326    }
5327    return readLine();
5328  };
5329
5330  emitter[Symbol.asyncIterator] = () => ({
5331    next() {
5332      if (queuedLines.length > 0) {
5333        return Promise.resolve({ done: false, value: queuedLines.shift() });
5334      }
5335      if (closed || ended) {
5336        return Promise.resolve({ done: true, value: void 0 });
5337      }
5338      return new Promise((resolve) => {
5339        pendingResolve = resolve;
5340      });
5341    },
5342    return() {
5343      emitter.close();
5344      return Promise.resolve({ done: true, value: void 0 });
5345    },
5346    [Symbol.asyncIterator]() {
5347      return this;
5348    },
5349  });
5350
5351  return emitter;
5352}
5353
5354export default { createInterface };
5355"#,
5356        );
5357    }
5358
5359    if module_name == "stream" {
5360        return String::from(
5361            r#"class MiniEmitter {
5362  constructor() {
5363    this._listeners = new Map();
5364    this._onceListeners = new Map();
5365  }
5366
5367  on(event, listener) {
5368    const listeners = this._listeners.get(event) ?? [];
5369    listeners.push(listener);
5370    this._listeners.set(event, listeners);
5371    return this;
5372  }
5373
5374  once(event, listener) {
5375    const listeners = this._onceListeners.get(event) ?? [];
5376    listeners.push(listener);
5377    this._onceListeners.set(event, listeners);
5378    return this;
5379  }
5380
5381  off(event, listener) {
5382    for (const map of [this._listeners, this._onceListeners]) {
5383      const listeners = map.get(event) ?? [];
5384      map.set(
5385        event,
5386        listeners.filter((candidate) => candidate !== listener),
5387      );
5388    }
5389    return this;
5390  }
5391
5392  removeListener(event, listener) {
5393    return this.off(event, listener);
5394  }
5395
5396  emit(event, ...args) {
5397    const persistent = [...(this._listeners.get(event) ?? [])];
5398    const once = [...(this._onceListeners.get(event) ?? [])];
5399    this._onceListeners.delete(event);
5400    for (const listener of persistent) {
5401      listener(...args);
5402    }
5403    for (const listener of once) {
5404      listener(...args);
5405    }
5406    return persistent.length + once.length > 0;
5407  }
5408}
5409
5410function getCallback(encodingOrCallback, callback) {
5411  if (typeof encodingOrCallback === "function") return encodingOrCallback;
5412  if (typeof callback === "function") return callback;
5413  return null;
5414}
5415
5416function queueResult(callback, error = null) {
5417  if (typeof callback !== "function") return;
5418  queueMicrotask(() => callback(error));
5419}
5420
5421function createReadableAsyncIterator(stream) {
5422  const queuedChunks = [];
5423  let pendingResolve = null;
5424  let pendingReject = null;
5425  let done = stream?.readableEnded === true;
5426  let error = stream?.errored ?? null;
5427
5428  const cleanup = () => {
5429    stream?.off?.("data", onData);
5430    stream?.off?.("end", onEnd);
5431    stream?.off?.("close", onEnd);
5432    stream?.off?.("error", onError);
5433  };
5434
5435  const settlePending = (result) => {
5436    if (pendingResolve) {
5437      const resolve = pendingResolve;
5438      pendingResolve = null;
5439      pendingReject = null;
5440      resolve(result);
5441    }
5442  };
5443
5444  const rejectPending = (reason) => {
5445    if (pendingReject) {
5446      const reject = pendingReject;
5447      pendingResolve = null;
5448      pendingReject = null;
5449      reject(reason);
5450    }
5451  };
5452
5453  const onData = (chunk) => {
5454    if (pendingResolve) {
5455      settlePending({ done: false, value: chunk });
5456      return;
5457    }
5458    queuedChunks.push(chunk);
5459  };
5460
5461  const onEnd = () => {
5462    if (done) return;
5463    done = true;
5464    cleanup();
5465    settlePending({ done: true, value: void 0 });
5466  };
5467
5468  const onError = (reason) => {
5469    error = reason;
5470    done = true;
5471    cleanup();
5472    rejectPending(reason);
5473  };
5474
5475  const pull = () => {
5476    if (done || typeof stream?._read !== "function") {
5477      return;
5478    }
5479    try {
5480      stream._read();
5481    } catch (reason) {
5482      stream.errored = reason;
5483      onError(reason);
5484    }
5485  };
5486
5487  stream?.on?.("data", onData);
5488  stream?.on?.("end", onEnd);
5489  stream?.on?.("close", onEnd);
5490  stream?.on?.("error", onError);
5491
5492  return {
5493    next() {
5494      if (error) {
5495        return Promise.reject(error);
5496      }
5497      if (queuedChunks.length > 0) {
5498        return Promise.resolve({ done: false, value: queuedChunks.shift() });
5499      }
5500      if (done) {
5501        return Promise.resolve({ done: true, value: void 0 });
5502      }
5503      pull();
5504      if (queuedChunks.length > 0) {
5505        return Promise.resolve({ done: false, value: queuedChunks.shift() });
5506      }
5507      if (done) {
5508        return Promise.resolve({ done: true, value: void 0 });
5509      }
5510      return new Promise((resolve, reject) => {
5511        pendingResolve = resolve;
5512        pendingReject = reject;
5513      });
5514    },
5515    return() {
5516      done = true;
5517      cleanup();
5518      stream?.destroy?.();
5519      return Promise.resolve({ done: true, value: void 0 });
5520    },
5521    [Symbol.asyncIterator]() {
5522      return this;
5523    },
5524  };
5525}
5526
5527class Stream extends MiniEmitter {
5528  pipe(destination) {
5529    this.on("data", (chunk) => destination.write(chunk));
5530    this.once("end", () => destination.end());
5531    return destination;
5532  }
5533
5534  destroy(error) {
5535    if (this.destroyed) return this;
5536    this.destroyed = true;
5537    if (error) {
5538      this.errored = error;
5539      queueMicrotask(() => this.emit("error", error));
5540    }
5541    queueMicrotask(() => this.emit("close"));
5542    return this;
5543  }
5544}
5545
5546class Readable extends Stream {
5547  constructor() {
5548    super();
5549    this.readable = true;
5550    this.readableEnded = false;
5551    this.destroyed = false;
5552  }
5553
5554  push(chunk) {
5555    if (chunk === null) {
5556      if (!this.readableEnded) {
5557        this.readableEnded = true;
5558        queueMicrotask(() => {
5559          this.emit("end");
5560          this.emit("close");
5561        });
5562      }
5563      return false;
5564    }
5565    this.emit("data", Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk ?? []));
5566    return true;
5567  }
5568
5569  static fromWeb(stream) {
5570    if (!stream || typeof stream.getReader !== "function") {
5571      throw new TypeError("Readable.fromWeb expects a WHATWG ReadableStream");
5572    }
5573    return {
5574      async *[Symbol.asyncIterator]() {
5575        const reader = stream.getReader();
5576        try {
5577          while (true) {
5578            const { value, done } = await reader.read();
5579            if (done) break;
5580            yield Buffer.from(value ?? []);
5581          }
5582        } finally {
5583          reader.releaseLock?.();
5584        }
5585      },
5586    };
5587  }
5588
5589  [Symbol.asyncIterator]() {
5590    return createReadableAsyncIterator(this);
5591  }
5592}
5593
5594class Writable extends Stream {
5595  constructor(options = undefined) {
5596    super();
5597    this.writable = true;
5598    this.writableEnded = false;
5599    this.destroyed = false;
5600    this._writeOption =
5601      options && typeof options.write === "function" ? options.write : null;
5602    this._destroyOption =
5603      options && typeof options.destroy === "function" ? options.destroy : null;
5604  }
5605
5606  write(chunk, encodingOrCallback, callback) {
5607    if (this.writableEnded) {
5608      const error = new Error("write after end");
5609      queueResult(getCallback(encodingOrCallback, callback), error);
5610      this.emit("error", error);
5611      return false;
5612    }
5613    const done = getCallback(encodingOrCallback, callback);
5614    this._write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk ?? []), done);
5615    return true;
5616  }
5617
5618  _write(_chunk, callback) {
5619    if (!this._writeOption) {
5620      queueResult(callback);
5621      return;
5622    }
5623    try {
5624      this._writeOption.call(this, _chunk, "buffer", callback);
5625    } catch (error) {
5626      queueResult(callback, error);
5627    }
5628  }
5629
5630  _destroy(error, callback) {
5631    if (!this._destroyOption) {
5632      queueResult(callback, error);
5633      return;
5634    }
5635    try {
5636      this._destroyOption.call(this, error ?? null, callback);
5637    } catch (destroyError) {
5638      queueResult(callback, destroyError);
5639    }
5640  }
5641
5642  destroy(error) {
5643    if (this.destroyed) return this;
5644    this.destroyed = true;
5645    this._destroy(error ?? null, (destroyError) => {
5646      const finalError = destroyError ?? error;
5647      if (finalError) {
5648        this.errored = finalError;
5649        this.emit("error", finalError);
5650      }
5651      this.emit("close");
5652    });
5653    return this;
5654  }
5655
5656  end(chunk, encodingOrCallback, callback) {
5657    if (chunk !== undefined && chunk !== null) {
5658      this.write(chunk, encodingOrCallback);
5659    }
5660    if (this.writableEnded) {
5661      queueResult(getCallback(encodingOrCallback, callback));
5662      return this;
5663    }
5664    this.writableEnded = true;
5665    const done = getCallback(encodingOrCallback, callback);
5666    queueMicrotask(() => {
5667      queueResult(done);
5668      this.emit("finish");
5669      this.destroy();
5670    });
5671    return this;
5672  }
5673}
5674
5675class Duplex extends Readable {
5676  constructor() {
5677    super();
5678    this.writable = true;
5679    this.writableEnded = false;
5680  }
5681
5682  write(chunk, encodingOrCallback, callback) {
5683    return Writable.prototype.write.call(this, chunk, encodingOrCallback, callback);
5684  }
5685
5686  _write(chunk, callback) {
5687    queueResult(callback);
5688  }
5689
5690  end(chunk, encodingOrCallback, callback) {
5691    return Writable.prototype.end.call(this, chunk, encodingOrCallback, callback);
5692  }
5693}
5694
5695class Transform extends Duplex {
5696  _write(chunk, callback) {
5697    try {
5698      this._transform(chunk, "buffer", (error, output) => {
5699        if (!error && output !== undefined && output !== null) {
5700          this.push(output);
5701        }
5702        queueResult(callback, error ?? null);
5703      });
5704    } catch (error) {
5705      queueResult(callback, error);
5706      this.emit("error", error);
5707    }
5708  }
5709
5710  _transform(chunk, _encoding, callback) {
5711    callback(null, chunk);
5712  }
5713
5714  end(chunk, encodingOrCallback, callback) {
5715    Writable.prototype.end.call(this, chunk, encodingOrCallback, callback);
5716    this.push(null);
5717    return this;
5718  }
5719}
5720
5721class PassThrough extends Transform {}
5722
5723function finished(stream, callback) {
5724  const done = (error = null) => {
5725    cleanup();
5726    if (typeof callback === "function") callback(error);
5727  };
5728  const onFinish = () => done();
5729  const onEnd = () => done();
5730  const onClose = () => done();
5731  const onError = (error) => done(error);
5732  const cleanup = () => {
5733    stream?.off?.("finish", onFinish);
5734    stream?.off?.("end", onEnd);
5735    stream?.off?.("close", onClose);
5736    stream?.off?.("error", onError);
5737  };
5738  stream?.once?.("finish", onFinish);
5739  stream?.once?.("end", onEnd);
5740  stream?.once?.("close", onClose);
5741  stream?.once?.("error", onError);
5742  return cleanup;
5743}
5744
5745function pipeline(...streams) {
5746  const callback =
5747    streams.length > 0 && typeof streams[streams.length - 1] === "function"
5748      ? streams.pop()
5749      : null;
5750  if (streams.length < 2) {
5751    const error = new TypeError("pipeline requires at least two streams");
5752    callback?.(error);
5753    throw error;
5754  }
5755  for (let index = 0; index < streams.length - 1; index += 1) {
5756    streams[index].pipe(streams[index + 1]);
5757  }
5758  if (callback) {
5759    finished(streams[streams.length - 1], callback);
5760  }
5761  return streams[streams.length - 1];
5762}
5763
5764function compose(...streams) {
5765  return pipeline(...streams);
5766}
5767
5768function addAbortSignal(signal, stream) {
5769  if (signal?.aborted) {
5770    stream?.destroy?.(signal.reason);
5771    return stream;
5772  }
5773  signal?.addEventListener?.("abort", () => stream?.destroy?.(signal.reason), {
5774    once: true,
5775  });
5776  return stream;
5777}
5778
5779function isReadable(stream) {
5780  return Boolean(stream && stream.readable && !stream.destroyed);
5781}
5782
5783function isWritable(stream) {
5784  return Boolean(stream && stream.writable && !stream.destroyed);
5785}
5786
5787function isErrored(stream) {
5788  return Boolean(stream && stream.errored);
5789}
5790
5791function isDisturbed(stream) {
5792  return Boolean(
5793    stream && (stream.disturbed === true || stream.locked || stream.readableDidRead === true),
5794  );
5795}
5796
5797const streamModule = Stream;
5798Object.assign(streamModule, {
5799  Duplex,
5800  PassThrough,
5801  Readable,
5802  Stream,
5803  Transform,
5804  Writable,
5805  addAbortSignal,
5806  compose,
5807  finished,
5808  isDisturbed,
5809  isErrored,
5810  isReadable,
5811  isWritable,
5812  pipeline,
5813});
5814
5815export {
5816  Duplex,
5817  PassThrough,
5818  Readable,
5819  Stream,
5820  Transform,
5821  Writable,
5822  addAbortSignal,
5823  compose,
5824  finished,
5825  isDisturbed,
5826  isErrored,
5827  isReadable,
5828  isWritable,
5829  pipeline,
5830};
5831export default streamModule;
5832"#,
5833        );
5834    }
5835
5836    if module_name == "stream/promises" {
5837        return String::from(
5838            r#"const _m = globalThis._requireFrom("node:stream/promises", "/");
5839
5840export default _m;
5841export const finished = _m.finished;
5842export const pipeline = _m.pipeline;
5843"#,
5844        );
5845    }
5846
5847    if module_name == "zlib" {
5848        return String::from(
5849            r#"const _m = globalThis._requireFrom("node:zlib", "/");
5850const zlibConstants =
5851  typeof _m.constants === "object" && _m.constants !== null
5852    ? _m.constants
5853    : Object.fromEntries(
5854        Object.entries(_m).filter(
5855          ([key, value]) => /^[A-Z0-9_]+$/.test(key) && typeof value === "number",
5856        ),
5857      );
5858
5859if (typeof _m.constants === "undefined") {
5860  Object.defineProperty(_m, "constants", {
5861    configurable: true,
5862    enumerable: true,
5863    value: zlibConstants,
5864    writable: true,
5865  });
5866}
5867
5868export default _m;
5869export const constants = _m.constants;
5870export const BrotliCompress = _m.BrotliCompress;
5871export const BrotliDecompress = _m.BrotliDecompress;
5872export const Deflate = _m.Deflate;
5873export const DeflateRaw = _m.DeflateRaw;
5874export const Gunzip = _m.Gunzip;
5875export const Gzip = _m.Gzip;
5876export const Inflate = _m.Inflate;
5877export const InflateRaw = _m.InflateRaw;
5878export const Unzip = _m.Unzip;
5879export const brotliCompress = _m.brotliCompress;
5880export const brotliCompressSync = _m.brotliCompressSync;
5881export const brotliDecompress = _m.brotliDecompress;
5882export const brotliDecompressSync = _m.brotliDecompressSync;
5883export const createBrotliCompress = _m.createBrotliCompress;
5884export const createBrotliDecompress = _m.createBrotliDecompress;
5885export const createDeflate = _m.createDeflate;
5886export const createDeflateRaw = _m.createDeflateRaw;
5887export const createGunzip = _m.createGunzip;
5888export const createGzip = _m.createGzip;
5889export const createInflate = _m.createInflate;
5890export const createInflateRaw = _m.createInflateRaw;
5891export const createUnzip = _m.createUnzip;
5892export const deflate = _m.deflate;
5893export const deflateRaw = _m.deflateRaw;
5894export const deflateRawSync = _m.deflateRawSync;
5895export const deflateSync = _m.deflateSync;
5896export const gunzip = _m.gunzip;
5897export const gunzipSync = _m.gunzipSync;
5898export const gzip = _m.gzip;
5899export const gzipSync = _m.gzipSync;
5900export const inflate = _m.inflate;
5901export const inflateRaw = _m.inflateRaw;
5902export const inflateRawSync = _m.inflateRawSync;
5903export const inflateSync = _m.inflateSync;
5904export const unzip = _m.unzip;
5905export const unzipSync = _m.unzipSync;
5906"#,
5907        );
5908    }
5909
5910    if module_name == "stream/web" {
5911        return String::from(
5912            r#"export const ReadableStream = globalThis.ReadableStream;
5913export const WritableStream = globalThis.WritableStream;
5914export const TransformStream = globalThis.TransformStream;
5915export const TextEncoderStream = globalThis.TextEncoderStream;
5916export const TextDecoderStream = globalThis.TextDecoderStream;
5917export const CompressionStream = globalThis.CompressionStream;
5918export const DecompressionStream = globalThis.DecompressionStream;
5919export default {
5920  ReadableStream,
5921  WritableStream,
5922  TransformStream,
5923  TextEncoderStream,
5924  TextDecoderStream,
5925  CompressionStream,
5926  DecompressionStream,
5927};
5928"#,
5929        );
5930    }
5931
5932    if module_name == "fs/promises" {
5933        return String::from(
5934            r#"const fsModule = globalThis._requireFrom("node:fs", "/");
5935const _m = fsModule.promises;
5936
5937export default _m;
5938export const constants = fsModule.constants;
5939export const FileHandle = _m.FileHandle;
5940export const access = _m.access;
5941export const appendFile = _m.appendFile;
5942export const chmod = _m.chmod;
5943export const chown = _m.chown;
5944export const copyFile = _m.copyFile;
5945export const cp = _m.cp;
5946export const lchmod = _m.lchmod;
5947export const lchown = _m.lchown;
5948export const link = _m.link;
5949export const lstat = _m.lstat;
5950export const lutimes = _m.lutimes;
5951export const mkdir = _m.mkdir;
5952export const mkdtemp = _m.mkdtemp;
5953export const open = _m.open;
5954export const opendir = _m.opendir;
5955export const readFile = _m.readFile;
5956export const readdir = _m.readdir;
5957export const readlink = _m.readlink;
5958export const realpath = _m.realpath;
5959export const rename = _m.rename;
5960export const rm = _m.rm;
5961export const rmdir = _m.rmdir;
5962export const stat = _m.stat;
5963export const statfs = _m.statfs;
5964export const symlink = _m.symlink;
5965export const truncate = _m.truncate;
5966export const unlink = _m.unlink;
5967export const utimes = _m.utimes;
5968export const watch = _m.watch;
5969export const writeFile = _m.writeFile;
5970"#,
5971        );
5972    }
5973
5974    if module_name == "readline" {
5975        return String::from(
5976            r#"const _m = globalThis._requireFrom("node:readline", "/");
5977
5978function createInterface(...args) {
5979  const interfaceValue = _m.createInterface(...args);
5980  if (interfaceValue && typeof interfaceValue === "object") {
5981    if (interfaceValue.__agentOSReadlineWrapped === true) {
5982      return interfaceValue;
5983    }
5984    Object.defineProperty(interfaceValue, "__agentOSReadlineWrapped", {
5985      value: true,
5986      configurable: true,
5987      enumerable: false,
5988      writable: false,
5989    });
5990    const options = args[0] && typeof args[0] === "object" ? args[0] : {};
5991    const output = options.output ?? null;
5992    const originalOn = typeof interfaceValue.on === "function"
5993      ? interfaceValue.on.bind(interfaceValue)
5994      : null;
5995    const originalOff = typeof interfaceValue.off === "function"
5996      ? interfaceValue.off.bind(interfaceValue)
5997      : typeof interfaceValue.removeListener === "function"
5998        ? interfaceValue.removeListener.bind(interfaceValue)
5999        : null;
6000    const originalClose = typeof interfaceValue.close === "function"
6001      ? interfaceValue.close.bind(interfaceValue)
6002      : null;
6003    const queued = [];
6004    const pendingQuestionResolves = [];
6005    let pendingResolve = null;
6006    let done = false;
6007    const enqueue = (line) => {
6008      if (pendingQuestionResolves.length > 0) {
6009        const resolve = pendingQuestionResolves.shift();
6010        resolve(line);
6011        return;
6012      }
6013      if (pendingResolve) {
6014        const resolve = pendingResolve;
6015        pendingResolve = null;
6016        resolve({ done: false, value: line });
6017        return;
6018      }
6019      queued.push(line);
6020    };
6021    const finish = () => {
6022      if (done) {
6023        return;
6024      }
6025      done = true;
6026      while (pendingQuestionResolves.length > 0) {
6027        const resolve = pendingQuestionResolves.shift();
6028        resolve("");
6029      }
6030      if (pendingResolve) {
6031        const resolve = pendingResolve;
6032        pendingResolve = null;
6033        resolve({ done: true, value: void 0 });
6034      }
6035    };
6036    const readLine = () => {
6037      if (queued.length > 0) {
6038        return Promise.resolve(queued.shift());
6039      }
6040      if (done) {
6041        return Promise.resolve("");
6042      }
6043      return new Promise((resolve) => {
6044        pendingQuestionResolves.push(resolve);
6045      });
6046    };
6047    originalOn?.("line", enqueue);
6048    originalOn?.("close", finish);
6049    interfaceValue.question = (prompt, callback) => {
6050      if (output && typeof output.write === "function" && prompt) {
6051        output.write(String(prompt));
6052      }
6053      if (typeof callback === "function") {
6054        void readLine().then((line) => {
6055          callback(line);
6056        });
6057        return;
6058      }
6059      return readLine();
6060    };
6061    interfaceValue[Symbol.asyncIterator] = () => ({
6062      next() {
6063        if (queued.length > 0) {
6064          return Promise.resolve({ done: false, value: queued.shift() });
6065        }
6066        if (done) {
6067          return Promise.resolve({ done: true, value: void 0 });
6068        }
6069        return new Promise((resolve) => {
6070          pendingResolve = resolve;
6071        });
6072      },
6073      return() {
6074        originalOff?.("line", enqueue);
6075        originalOff?.("close", finish);
6076        originalClose?.();
6077        finish();
6078        return Promise.resolve({ done: true, value: void 0 });
6079      },
6080      [Symbol.asyncIterator]() {
6081        return this;
6082      },
6083    });
6084  }
6085  return interfaceValue;
6086}
6087
6088export default _m;
6089export { createInterface };
6090"#,
6091        );
6092    }
6093
6094    if module_name == "string_decoder" {
6095        return String::from(
6096            r#"class StringDecoder {
6097  constructor(encoding = "utf8") {
6098    this.encoding = encoding;
6099    this.decoder = new TextDecoder(encoding, { fatal: false });
6100  }
6101
6102  write(input) {
6103    const buffer =
6104      typeof input === "string"
6105        ? Buffer.from(input, this.encoding)
6106        : Buffer.isBuffer(input)
6107          ? input
6108          : Buffer.from(input ?? []);
6109    return this.decoder.decode(buffer, { stream: true });
6110  }
6111
6112  end(input) {
6113    let output = "";
6114    if (input !== undefined) {
6115      output += this.write(input);
6116    }
6117    output += this.decoder.decode();
6118    return output;
6119  }
6120}
6121
6122export { StringDecoder };
6123export default { StringDecoder };
6124"#,
6125        );
6126    }
6127
6128    if module_name == "v8" {
6129        return String::from(
6130            r#"function serialize(value) {
6131  return Buffer.from(JSON.stringify(value ?? null), "utf8");
6132}
6133
6134function deserialize(value) {
6135  const buffer = Buffer.isBuffer(value) ? value : Buffer.from(value ?? []);
6136  return JSON.parse(buffer.toString("utf8"));
6137}
6138
6139class Serializer {
6140  constructor() {
6141    this._value = null;
6142  }
6143
6144  writeHeader() {}
6145
6146  writeValue(value) {
6147    this._value = value;
6148  }
6149
6150  releaseBuffer() {
6151    return serialize(this._value);
6152  }
6153
6154  transferArrayBuffer() {}
6155}
6156
6157class Deserializer {
6158  constructor(buffer) {
6159    this._buffer = buffer;
6160  }
6161
6162  readHeader() {}
6163
6164  readValue() {
6165    return deserialize(this._buffer);
6166  }
6167
6168  transferArrayBuffer() {}
6169}
6170
6171function cachedDataVersionTag() {
6172  return 0;
6173}
6174
6175function getCppHeapStatistics() {
6176  return {
6177    committed_size_bytes: 0,
6178    resident_size_bytes: 0,
6179    used_size_bytes: 0,
6180    space_statistics: [],
6181  };
6182}
6183
6184function getHeapCodeStatistics() {
6185  return {
6186    code_and_metadata_size: 0,
6187    bytecode_and_metadata_size: 0,
6188    external_script_source_size: 0,
6189    cpu_profiler_metadata_size: 0,
6190  };
6191}
6192
6193function configuredHeapLimitBytes() {
6194  const configured = Number(globalThis.__agentOSV8HeapLimitBytes);
6195  if (!Number.isFinite(configured) || configured <= 0) {
6196    return 0;
6197  }
6198  return configured;
6199}
6200
6201function getHeapStatistics() {
6202  const heapLimit = configuredHeapLimitBytes();
6203  return {
6204    total_heap_size: 0,
6205    total_heap_size_executable: 0,
6206    total_physical_size: 0,
6207    total_available_size: 0,
6208    used_heap_size: 0,
6209    heap_size_limit: heapLimit,
6210    malloced_memory: 0,
6211    peak_malloced_memory: 0,
6212    does_zap_garbage: 0,
6213    number_of_native_contexts: 0,
6214    number_of_detached_contexts: 0,
6215    total_global_handles_size: 0,
6216    used_global_handles_size: 0,
6217    external_memory: 0,
6218  };
6219}
6220
6221function getHeapSpaceStatistics() {
6222  return [];
6223}
6224
6225function getHeapSnapshot() {
6226  return Readable.fromWeb(
6227    new ReadableStream({
6228      start(controller) {
6229        controller.enqueue(Buffer.from("{}"));
6230        controller.close();
6231      },
6232    }),
6233  );
6234}
6235
6236function isStringOneByteRepresentation(value) {
6237  return typeof value === "string" && !/[^\x00-\xff]/.test(value);
6238}
6239
6240function queryObjects() {
6241  return [];
6242}
6243
6244function setFlagsFromString() {}
6245
6246function setHeapSnapshotNearHeapLimit() {
6247  return [];
6248}
6249
6250function startCpuProfile() {
6251  return {
6252    stop() {
6253      return {};
6254    },
6255  };
6256}
6257
6258function stopCoverage() {
6259  return [];
6260}
6261
6262function takeCoverage() {
6263  return [];
6264}
6265
6266function writeHeapSnapshot() {
6267  return "";
6268}
6269
6270class GCProfiler {
6271  start() {}
6272
6273  stop() {
6274    return [];
6275  }
6276}
6277
6278const promiseHooks = {};
6279const startupSnapshot = {};
6280
6281export {
6282  GCProfiler,
6283  cachedDataVersionTag,
6284  Deserializer,
6285  deserialize,
6286  getCppHeapStatistics,
6287  getHeapCodeStatistics,
6288  getHeapSnapshot,
6289  getHeapSpaceStatistics,
6290  getHeapStatistics,
6291  isStringOneByteRepresentation,
6292  promiseHooks,
6293  queryObjects,
6294  serialize,
6295  Serializer,
6296  setFlagsFromString,
6297  setHeapSnapshotNearHeapLimit,
6298  startCpuProfile,
6299  startupSnapshot,
6300  stopCoverage,
6301  takeCoverage,
6302  writeHeapSnapshot,
6303};
6304export {
6305  Deserializer as DefaultDeserializer,
6306  Serializer as DefaultSerializer,
6307};
6308export default {
6309  GCProfiler,
6310  cachedDataVersionTag,
6311  DefaultDeserializer: Deserializer,
6312  DefaultSerializer: Serializer,
6313  Deserializer,
6314  deserialize,
6315  getCppHeapStatistics,
6316  getHeapCodeStatistics,
6317  getHeapSnapshot,
6318  getHeapSpaceStatistics,
6319  getHeapStatistics,
6320  isStringOneByteRepresentation,
6321  promiseHooks,
6322  queryObjects,
6323  serialize,
6324  Serializer,
6325  setFlagsFromString,
6326  setHeapSnapshotNearHeapLimit,
6327  startCpuProfile,
6328  startupSnapshot,
6329  stopCoverage,
6330  takeCoverage,
6331  writeHeapSnapshot,
6332};
6333"#,
6334        );
6335    }
6336
6337    if module_name == "vm" {
6338        return String::from(
6339            r#"const VM_CONTEXT_TAG = typeof Symbol === "function" ? Symbol.for("secure-exec.vm.context") : "__secure_exec_vm_context__";
6340const VM_CONTEXT_ID = typeof Symbol === "function" ? Symbol.for("secure-exec.vm.context.id") : "__secure_exec_vm_context_id__";
6341
6342function createVmNotImplementedError(feature) {
6343  const error = new Error(`node:vm ${feature} is not implemented in the secure-exec guest runtime`);
6344  error.code = "ERR_NOT_IMPLEMENTED";
6345  return error;
6346}
6347
6348function isVmContextCandidate(value) {
6349  return value !== null && (typeof value === "object" || typeof value === "function");
6350}
6351
6352function normalizeVmOptions(options = undefined) {
6353  if (typeof options === "string") {
6354    return { filename: options };
6355  }
6356  if (!options || typeof options !== "object") {
6357    return {};
6358  }
6359  const normalized = {};
6360  if (typeof options.filename === "string") {
6361    normalized.filename = options.filename;
6362  }
6363  if (Number.isInteger(options.lineOffset)) {
6364    normalized.lineOffset = options.lineOffset;
6365  }
6366  if (Number.isInteger(options.columnOffset)) {
6367    normalized.columnOffset = options.columnOffset;
6368  }
6369  if (Number.isInteger(options.timeout) && options.timeout > 0) {
6370    normalized.timeout = options.timeout;
6371  }
6372  if (options.cachedData !== undefined) {
6373    normalized.cachedData = options.cachedData;
6374  }
6375  if (options.produceCachedData === true) {
6376    normalized.produceCachedData = true;
6377  }
6378  return normalized;
6379}
6380
6381function mergeVmOptions(baseOptions, overrideOptions) {
6382  return { ...normalizeVmOptions(baseOptions), ...normalizeVmOptions(overrideOptions) };
6383}
6384
6385function createContext(context = {}) {
6386  if (!isVmContextCandidate(context)) {
6387    throw new TypeError('The "object" argument must be of type object.');
6388  }
6389  if (context[VM_CONTEXT_TAG] === true && Number.isInteger(context[VM_CONTEXT_ID])) {
6390    return context;
6391  }
6392  const contextId = globalThis._vmCreateContext(context);
6393  Object.defineProperty(context, VM_CONTEXT_TAG, {
6394    value: true,
6395    configurable: true,
6396    enumerable: false,
6397    writable: false,
6398  });
6399  Object.defineProperty(context, VM_CONTEXT_ID, {
6400    value: contextId,
6401    configurable: false,
6402    enumerable: false,
6403    writable: false,
6404  });
6405  return context;
6406}
6407
6408function isContext(context) {
6409  return isVmContextCandidate(context) && context[VM_CONTEXT_TAG] === true && Number.isInteger(context[VM_CONTEXT_ID]);
6410}
6411
6412function assertContext(context) {
6413  if (!isContext(context)) {
6414    throw new TypeError('The "contextifiedObject" argument must be a vm context.');
6415  }
6416  return context;
6417}
6418
6419function runInThisContext(code, options = undefined) {
6420  return globalThis._vmRunInThisContext(String(code), normalizeVmOptions(options));
6421}
6422
6423function runInContext(code, contextifiedObject, options = undefined) {
6424  const context = assertContext(contextifiedObject);
6425  return globalThis._vmRunInContext(context[VM_CONTEXT_ID], String(code), normalizeVmOptions(options), context);
6426}
6427
6428function runInNewContext(code, contextOrOptions = {}, maybeOptions = undefined) {
6429  const hasExplicitContext = isVmContextCandidate(contextOrOptions);
6430  const context = hasExplicitContext ? contextOrOptions : {};
6431  const options = hasExplicitContext ? maybeOptions : contextOrOptions;
6432  return runInContext(code, createContext(context), options);
6433}
6434
6435class Script {
6436  constructor(code, options = undefined) {
6437    this.code = String(code);
6438    this.options = normalizeVmOptions(options);
6439    this.filename = this.options.filename ?? "evalmachine.<anonymous>";
6440    this.lineOffset = this.options.lineOffset ?? 0;
6441    this.columnOffset = this.options.columnOffset ?? 0;
6442    this.cachedData = this.options.cachedData;
6443    this.cachedDataProduced = false;
6444    this.cachedDataRejected = false;
6445  }
6446
6447  createCachedData() {
6448    return typeof Buffer === "function" ? Buffer.alloc(0) : new Uint8Array(0);
6449  }
6450
6451  runInThisContext(options = undefined) {
6452    return runInThisContext(this.code, mergeVmOptions(this.options, options));
6453  }
6454
6455  runInContext(contextifiedObject, options = undefined) {
6456    return runInContext(this.code, contextifiedObject, mergeVmOptions(this.options, options));
6457  }
6458
6459  runInNewContext(context = {}, options = undefined) {
6460    return runInNewContext(this.code, context, mergeVmOptions(this.options, options));
6461  }
6462}
6463
6464function compileFunction() {
6465  throw createVmNotImplementedError("compileFunction");
6466}
6467
6468function measureMemory() {
6469  throw createVmNotImplementedError("measureMemory");
6470}
6471
6472export { Script, compileFunction, createContext, isContext, measureMemory, runInContext, runInNewContext, runInThisContext };
6473export default { Script, compileFunction, createContext, isContext, measureMemory, runInContext, runInNewContext, runInThisContext };
6474"#,
6475        );
6476    }
6477
6478    if module_name == "worker_threads" {
6479        return String::from(
6480            r#"function createNotImplementedError(feature) {
6481  const error = new Error(`node:worker_threads ${feature} is not available in the secure-exec guest runtime`);
6482  error.code = "ERR_NOT_IMPLEMENTED";
6483  return error;
6484}
6485
6486class MessagePort {
6487  postMessage() {}
6488  start() {}
6489  close() {}
6490  unref() {
6491    return this;
6492  }
6493  ref() {
6494    return this;
6495  }
6496}
6497
6498class MessageChannel {
6499  constructor() {
6500    this.port1 = new MessagePort();
6501    this.port2 = new MessagePort();
6502  }
6503}
6504
6505class Worker {
6506  constructor() {
6507    throw createNotImplementedError("Worker");
6508  }
6509}
6510
6511function getEnvironmentData() {
6512  return undefined;
6513}
6514
6515function markAsUncloneable() {}
6516
6517function markAsUntransferable() {}
6518
6519function moveMessagePortToContext() {
6520  throw createNotImplementedError("moveMessagePortToContext");
6521}
6522
6523function postMessageToThread() {
6524  throw createNotImplementedError("postMessageToThread");
6525}
6526
6527function receiveMessageOnPort() {
6528  return undefined;
6529}
6530
6531function setEnvironmentData() {}
6532
6533export const BroadcastChannel = globalThis.BroadcastChannel;
6534export { MessageChannel, MessagePort, Worker, getEnvironmentData, markAsUncloneable, markAsUntransferable, moveMessagePortToContext, postMessageToThread, receiveMessageOnPort, setEnvironmentData };
6535export const SHARE_ENV = Symbol.for("secure-exec.worker_threads.SHARE_ENV");
6536export const isMainThread = true;
6537export const parentPort = null;
6538export const resourceLimits = {};
6539export const threadId = 0;
6540export const workerData = null;
6541export default {
6542  BroadcastChannel: globalThis.BroadcastChannel,
6543  MessageChannel,
6544  MessagePort,
6545  SHARE_ENV,
6546  Worker,
6547  getEnvironmentData,
6548  isMainThread,
6549  markAsUncloneable,
6550  markAsUntransferable,
6551  moveMessagePortToContext,
6552  parentPort,
6553  postMessageToThread,
6554  receiveMessageOnPort,
6555  resourceLimits,
6556  setEnvironmentData,
6557  threadId,
6558  workerData,
6559};
6560"#,
6561        );
6562    }
6563
6564    let default_target = format!(
6565        "globalThis._requireFrom({}, \"/\")",
6566        serde_json::to_string(&format!("node:{module_name}"))
6567            .unwrap_or_else(|_| format!("\"node:{module_name}\""))
6568    );
6569    let mut exports = builtin_named_exports(module_name)
6570        .iter()
6571        .collect::<HashSet<_>>()
6572        .into_iter()
6573        .collect::<Vec<_>>();
6574    exports.sort_unstable();
6575
6576    let mut source = format!("const _m = {default_target};\nexport default _m;\n");
6577    for name in exports {
6578        source.push_str(&format!("export const {name} = _m[\"{name}\"];\n"));
6579    }
6580    source
6581}
6582
6583fn builtin_named_exports(module_name: &str) -> &'static [&'static str] {
6584    match module_name {
6585        "async_hooks" => &[
6586            "AsyncLocalStorage",
6587            "AsyncResource",
6588            "createHook",
6589            "executionAsyncId",
6590            "triggerAsyncId",
6591        ],
6592        "buffer" => &[
6593            "Blob",
6594            "Buffer",
6595            "File",
6596            "INSPECT_MAX_BYTES",
6597            "SlowBuffer",
6598            "isUtf8",
6599        ],
6600        "child_process" => &[
6601            "ChildProcess",
6602            "exec",
6603            "execFile",
6604            "execFileSync",
6605            "execSync",
6606            "fork",
6607            "spawn",
6608            "spawnSync",
6609        ],
6610        "console" => &[
6611            "Console",
6612            "assert",
6613            "clear",
6614            "context",
6615            "count",
6616            "countReset",
6617            "createTask",
6618            "debug",
6619            "dir",
6620            "dirxml",
6621            "error",
6622            "group",
6623            "groupCollapsed",
6624            "groupEnd",
6625            "info",
6626            "log",
6627            "profile",
6628            "profileEnd",
6629            "table",
6630            "time",
6631            "timeEnd",
6632            "timeLog",
6633            "timeStamp",
6634            "trace",
6635            "warn",
6636        ],
6637        "crypto" => &[
6638            "createHash",
6639            "createPrivateKey",
6640            "getHashes",
6641            "getRandomValues",
6642            "randomBytes",
6643            "randomFillSync",
6644            "randomUUID",
6645            "subtle",
6646        ],
6647        "diagnostics_channel" => &[
6648            "Channel",
6649            "channel",
6650            "hasSubscribers",
6651            "subscribe",
6652            "tracingChannel",
6653            "unsubscribe",
6654        ],
6655        "events" => &[
6656            "EventEmitter",
6657            "addAbortListener",
6658            "defaultMaxListeners",
6659            "errorMonitor",
6660            "getEventListeners",
6661            "getMaxListeners",
6662            "on",
6663            "once",
6664            "setMaxListeners",
6665        ],
6666        "dns" => &[
6667            "Resolver",
6668            "getServers",
6669            "lookup",
6670            "promises",
6671            "resolve",
6672            "resolve4",
6673            "resolve6",
6674            "setServers",
6675        ],
6676        "dns/promises" => &[
6677            "Resolver",
6678            "lookup",
6679            "resolve",
6680            "resolve4",
6681            "resolve6",
6682            "resolveAny",
6683            "resolveMx",
6684            "resolveTxt",
6685            "resolveSrv",
6686            "resolveCname",
6687            "resolvePtr",
6688            "resolveNs",
6689            "resolveSoa",
6690            "resolveNaptr",
6691            "resolveCaa",
6692        ],
6693        "fs" => &[
6694            "access",
6695            "accessSync",
6696            "appendFile",
6697            "appendFileSync",
6698            "chmod",
6699            "chmodSync",
6700            "closeSync",
6701            "constants",
6702            "createReadStream",
6703            "createWriteStream",
6704            "existsSync",
6705            "fstat",
6706            "fstatSync",
6707            "fsyncSync",
6708            "lstat",
6709            "lstatSync",
6710            "mkdir",
6711            "mkdirSync",
6712            "openSync",
6713            "readFile",
6714            "promises",
6715            "readFileSync",
6716            "readdir",
6717            "readSync",
6718            "readdirSync",
6719            "readlink",
6720            "realpathSync",
6721            "rename",
6722            "readlinkSync",
6723            "renameSync",
6724            "rm",
6725            "rmSync",
6726            "stat",
6727            "statSync",
6728            "unlink",
6729            "unlinkSync",
6730            "watch",
6731            "watchFile",
6732            "unwatchFile",
6733            "writeFile",
6734            "writeFileSync",
6735            "writeSync",
6736        ],
6737        "fs/promises" => &[
6738            "access",
6739            "appendFile",
6740            "chmod",
6741            "chown",
6742            "constants",
6743            "copyFile",
6744            "cp",
6745            "glob",
6746            "lchown",
6747            "link",
6748            "lstat",
6749            "mkdir",
6750            "mkdtemp",
6751            "open",
6752            "opendir",
6753            "readFile",
6754            "readdir",
6755            "readlink",
6756            "realpath",
6757            "rename",
6758            "rm",
6759            "rmdir",
6760            "stat",
6761            "statfs",
6762            "symlink",
6763            "truncate",
6764            "unlink",
6765            "utimes",
6766            "writeFile",
6767        ],
6768        "http" => &[
6769            "Agent",
6770            "ClientRequest",
6771            "IncomingMessage",
6772            "METHODS",
6773            "Server",
6774            "ServerResponse",
6775            "STATUS_CODES",
6776            "_checkInvalidHeaderChar",
6777            "_checkIsHttpToken",
6778            "createServer",
6779            "get",
6780            "globalAgent",
6781            "maxHeaderSize",
6782            "request",
6783            "validateHeaderName",
6784            "validateHeaderValue",
6785        ],
6786        "http2" => &["connect", "createServer", "createSecureServer"],
6787        "https" => &[
6788            "Agent",
6789            "ClientRequest",
6790            "IncomingMessage",
6791            "Server",
6792            "ServerResponse",
6793            "_checkInvalidHeaderChar",
6794            "_checkIsHttpToken",
6795            "createServer",
6796            "get",
6797            "globalAgent",
6798            "maxHeaderSize",
6799            "request",
6800            "validateHeaderName",
6801            "validateHeaderValue",
6802        ],
6803        "module" => &[
6804            "Module",
6805            "_cache",
6806            "_extensions",
6807            "_resolveFilename",
6808            "builtinModules",
6809            "createRequire",
6810            "findSourceMap",
6811            "isBuiltin",
6812            "syncBuiltinESMExports",
6813            "wrap",
6814        ],
6815        "net" => &[
6816            "BlockList",
6817            "Socket",
6818            "SocketAddress",
6819            "Server",
6820            "Stream",
6821            "connect",
6822            "createConnection",
6823            "createServer",
6824            "getDefaultAutoSelectFamily",
6825            "getDefaultAutoSelectFamilyAttemptTimeout",
6826            "isIP",
6827            "isIPv4",
6828            "isIPv6",
6829            "setDefaultAutoSelectFamily",
6830            "setDefaultAutoSelectFamilyAttemptTimeout",
6831        ],
6832        "os" => &[
6833            "EOL",
6834            "arch",
6835            "availableParallelism",
6836            "constants",
6837            "cpus",
6838            "endianness",
6839            "freemem",
6840            "homedir",
6841            "hostname",
6842            "networkInterfaces",
6843            "platform",
6844            "release",
6845            "totalmem",
6846            "tmpdir",
6847            "type",
6848            "userInfo",
6849            "version",
6850        ],
6851        "path" | "path/posix" | "path/win32" => &[
6852            "basename",
6853            "delimiter",
6854            "dirname",
6855            "extname",
6856            "format",
6857            "isAbsolute",
6858            "join",
6859            "normalize",
6860            "parse",
6861            "posix",
6862            "relative",
6863            "resolve",
6864            "sep",
6865            "win32",
6866        ],
6867        "process" => &[
6868            "arch", "argv", "argv0", "cwd", "env", "execPath", "exit", "pid", "platform", "ppid",
6869            "stderr", "stdin", "stdout", "umask", "version", "versions",
6870        ],
6871        "perf_hooks" => &[
6872            "PerformanceObserver",
6873            "constants",
6874            "createHistogram",
6875            "performance",
6876        ],
6877        "readline" => &["createInterface"],
6878        "sqlite" => &["DatabaseSync", "StatementSync", "constants"],
6879        "stream" => &[
6880            "Duplex",
6881            "PassThrough",
6882            "Readable",
6883            "Stream",
6884            "Transform",
6885            "Writable",
6886            "addAbortSignal",
6887            "compose",
6888            "finished",
6889            "isDisturbed",
6890            "isErrored",
6891            "isReadable",
6892            "pipeline",
6893        ],
6894        "stream/consumers" => &["arrayBuffer", "blob", "buffer", "json", "text"],
6895        "sys" => &[
6896            "MIMEType",
6897            "MIMEParams",
6898            "TextDecoder",
6899            "TextEncoder",
6900            "callbackify",
6901            "debug",
6902            "debuglog",
6903            "deprecate",
6904            "format",
6905            "formatWithOptions",
6906            "inherits",
6907            "inspect",
6908            "parseArgs",
6909            "promisify",
6910            "stripVTControlCharacters",
6911            "types",
6912        ],
6913        "timers" => &[
6914            "clearImmediate",
6915            "clearInterval",
6916            "clearTimeout",
6917            "setImmediate",
6918            "setInterval",
6919            "setTimeout",
6920        ],
6921        "tty" => &["ReadStream", "WriteStream", "isatty"],
6922        "tls" => &[
6923            "TLSSocket",
6924            "Server",
6925            "connect",
6926            "createSecureContext",
6927            "createServer",
6928            "getCiphers",
6929        ],
6930        "stream/promises" => &["finished", "pipeline"],
6931        "timers/promises" => &["scheduler", "setImmediate", "setInterval", "setTimeout"],
6932        "url" => &["URL", "fileURLToPath", "format", "parse", "pathToFileURL"],
6933        "util" => &[
6934            "MIMEType",
6935            "MIMEParams",
6936            "TextDecoder",
6937            "TextEncoder",
6938            "callbackify",
6939            "debug",
6940            "debuglog",
6941            "deprecate",
6942            "format",
6943            "formatWithOptions",
6944            "inherits",
6945            "inspect",
6946            "isDeepStrictEqual",
6947            "parseArgs",
6948            "promisify",
6949            "stripVTControlCharacters",
6950            "types",
6951        ],
6952        "util/types" => &[
6953            "isAnyArrayBuffer",
6954            "isArgumentsObject",
6955            "isArrayBuffer",
6956            "isArrayBufferView",
6957            "isAsyncFunction",
6958            "isBigInt64Array",
6959            "isBigIntObject",
6960            "isBigUint64Array",
6961            "isBooleanObject",
6962            "isBoxedPrimitive",
6963            "isCryptoKey",
6964            "isDataView",
6965            "isDate",
6966            "isExternal",
6967            "isFloat16Array",
6968            "isFloat32Array",
6969            "isFloat64Array",
6970            "isGeneratorFunction",
6971            "isGeneratorObject",
6972            "isInt16Array",
6973            "isInt32Array",
6974            "isInt8Array",
6975            "isKeyObject",
6976            "isMap",
6977            "isMapIterator",
6978            "isModuleNamespaceObject",
6979            "isNativeError",
6980            "isNumberObject",
6981            "isPromise",
6982            "isProxy",
6983            "isRegExp",
6984            "isSet",
6985            "isSetIterator",
6986            "isSharedArrayBuffer",
6987            "isStringObject",
6988            "isSymbolObject",
6989            "isTypedArray",
6990            "isUint16Array",
6991            "isUint32Array",
6992            "isUint8Array",
6993            "isUint8ClampedArray",
6994            "isWeakMap",
6995            "isWeakSet",
6996        ],
6997        "vm" => &[
6998            "Script",
6999            "compileFunction",
7000            "createContext",
7001            "isContext",
7002            "measureMemory",
7003            "runInContext",
7004            "runInNewContext",
7005            "runInThisContext",
7006        ],
7007        "v8" => &[
7008            "cachedDataVersionTag",
7009            "DefaultDeserializer",
7010            "DefaultSerializer",
7011            "Deserializer",
7012            "GCProfiler",
7013            "Serializer",
7014            "deserialize",
7015            "getCppHeapStatistics",
7016            "getHeapCodeStatistics",
7017            "getHeapSnapshot",
7018            "getHeapSpaceStatistics",
7019            "getHeapStatistics",
7020            "isStringOneByteRepresentation",
7021            "promiseHooks",
7022            "queryObjects",
7023            "serialize",
7024            "setFlagsFromString",
7025            "setHeapSnapshotNearHeapLimit",
7026            "startCpuProfile",
7027            "startupSnapshot",
7028            "stopCoverage",
7029            "takeCoverage",
7030            "writeHeapSnapshot",
7031        ],
7032        "worker_threads" => &[
7033            "MessageChannel",
7034            "MessagePort",
7035            "Worker",
7036            "isMainThread",
7037            "parentPort",
7038            "workerData",
7039        ],
7040        "zlib" => &[
7041            "BrotliCompress",
7042            "BrotliDecompress",
7043            "Deflate",
7044            "DeflateRaw",
7045            "Gunzip",
7046            "Gzip",
7047            "Inflate",
7048            "InflateRaw",
7049            "Unzip",
7050            "brotliCompress",
7051            "brotliCompressSync",
7052            "brotliDecompress",
7053            "brotliDecompressSync",
7054            "constants",
7055            "createBrotliCompress",
7056            "createBrotliDecompress",
7057            "createDeflate",
7058            "createDeflateRaw",
7059            "createGunzip",
7060            "createGzip",
7061            "createInflate",
7062            "createInflateRaw",
7063            "createUnzip",
7064            "deflate",
7065            "deflateRaw",
7066            "deflateRawSync",
7067            "deflateSync",
7068            "gunzip",
7069            "gunzipSync",
7070            "gzip",
7071            "gzipSync",
7072            "inflate",
7073            "inflateRaw",
7074            "inflateRawSync",
7075            "inflateSync",
7076            "unzip",
7077            "unzipSync",
7078        ],
7079        _ => &[],
7080    }
7081}
7082
7083fn split_package_request(request: &str) -> Option<(&str, &str)> {
7084    if request.starts_with('@') {
7085        let mut parts = request.splitn(3, '/');
7086        let scope = parts.next()?;
7087        let name = parts.next()?;
7088        let package_name = &request[..scope.len() + 1 + name.len()];
7089        let subpath = parts.next().unwrap_or("");
7090        Some((package_name, subpath))
7091    } else {
7092        request.split_once('/').or(Some((request, "")))
7093    }
7094}
7095
7096fn node_modules_direct_candidate_dirs(dir: &str, package_name: &str) -> Vec<String> {
7097    let mut candidates = HashSet::new();
7098    candidates.insert(join_guest_path(
7099        dir,
7100        &format!("node_modules/{package_name}"),
7101    ));
7102    if dir == "/node_modules" || dir.ends_with("/node_modules") {
7103        candidates.insert(join_guest_path(dir, package_name));
7104    }
7105    let mut candidates = candidates.into_iter().collect::<Vec<_>>();
7106    candidates.sort();
7107    candidates
7108}
7109
7110fn resolve_exports_target(
7111    exports_field: &Value,
7112    subpath: &str,
7113    mode: ModuleResolveMode,
7114) -> Option<String> {
7115    match exports_field {
7116        Value::String(value) => (subpath == ".").then(|| value.clone()),
7117        Value::Array(values) => values
7118            .iter()
7119            .find_map(|value| resolve_exports_target(value, subpath, mode)),
7120        Value::Object(record) => {
7121            if subpath == "."
7122                && !record.contains_key(".")
7123                && !record.keys().any(|key| key.starts_with("./"))
7124            {
7125                return resolve_conditional_target(record, mode);
7126            }
7127            if let Some(value) = record.get(subpath) {
7128                return resolve_exports_target(value, ".", mode);
7129            }
7130            for (key, value) in record {
7131                if let Some((prefix, suffix)) = key.split_once('*') {
7132                    if subpath.starts_with(prefix) && subpath.ends_with(suffix) {
7133                        let wildcard = &subpath[prefix.len()..subpath.len() - suffix.len()];
7134                        let resolved = resolve_exports_target(value, ".", mode)?;
7135                        return Some(resolved.replace('*', wildcard));
7136                    }
7137                }
7138            }
7139            if subpath == "." {
7140                record
7141                    .get(".")
7142                    .and_then(|value| resolve_exports_target(value, ".", mode))
7143            } else {
7144                None
7145            }
7146        }
7147        _ => None,
7148    }
7149}
7150
7151fn resolve_conditional_target(
7152    record: &serde_json::Map<String, Value>,
7153    mode: ModuleResolveMode,
7154) -> Option<String> {
7155    let order: &[&str] = match mode {
7156        ModuleResolveMode::Import => &["import", "node", "module", "default", "require"],
7157        ModuleResolveMode::Require => &["require", "node", "default", "import", "module"],
7158    };
7159    for key in order {
7160        if let Some(value) = record.get(*key) {
7161            if let Some(resolved) = resolve_exports_target(value, ".", mode) {
7162                return Some(resolved);
7163            }
7164        }
7165    }
7166    None
7167}
7168
7169fn resolve_imports_target(
7170    imports_field: &Value,
7171    specifier: &str,
7172    mode: ModuleResolveMode,
7173) -> Option<String> {
7174    match imports_field {
7175        Value::String(value) => Some(value.clone()),
7176        Value::Array(values) => values
7177            .iter()
7178            .find_map(|value| resolve_imports_target(value, specifier, mode)),
7179        Value::Object(record) => {
7180            if let Some(value) = record.get(specifier) {
7181                return resolve_exports_target(value, ".", mode);
7182            }
7183            for (key, value) in record {
7184                if let Some((prefix, suffix)) = key.split_once('*') {
7185                    if specifier.starts_with(prefix) && specifier.ends_with(suffix) {
7186                        let wildcard = &specifier[prefix.len()..specifier.len() - suffix.len()];
7187                        let resolved = resolve_exports_target(value, ".", mode)?;
7188                        return Some(resolved.replace('*', wildcard));
7189                    }
7190                }
7191            }
7192            None
7193        }
7194        _ => None,
7195    }
7196}
7197
7198#[cfg(test)]
7199mod tests {
7200    use super::*;
7201    use nix::fcntl::OFlag;
7202    use nix::unistd::pipe2;
7203    use serde_json::Value;
7204    use std::io::BufRead;
7205    use std::time::{SystemTime, UNIX_EPOCH};
7206    use tempfile::tempdir;
7207
7208    #[test]
7209    fn javascript_limits_are_read_from_typed_fields_and_env_is_inert() {
7210        // Misleading env values: a reader that still consulted `AGENTOS_*` would
7211        // observe these instead of the typed wire limits.
7212        let env = std::collections::BTreeMap::from([
7213            (
7214                String::from("AGENTOS_V8_HEAP_LIMIT_MB"),
7215                String::from("999999"),
7216            ),
7217            (
7218                String::from("AGENTOS_V8_CPU_TIME_LIMIT_MS"),
7219                String::from("999999"),
7220            ),
7221            (
7222                String::from("AGENTOS_V8_WALL_CLOCK_LIMIT_MS"),
7223                String::from("999999"),
7224            ),
7225            (
7226                String::from("AGENTOS_NODE_IMPORT_CACHE_MATERIALIZE_TIMEOUT_MS"),
7227                String::from("999999"),
7228            ),
7229            (
7230                String::from(NODE_SYNC_RPC_WAIT_TIMEOUT_MS_ENV),
7231                String::from("999999"),
7232            ),
7233        ]);
7234        let request = StartJavascriptExecutionRequest {
7235            guest_runtime: Default::default(),
7236            vm_id: String::from("vm-js"),
7237            context_id: String::from("ctx-js"),
7238            argv: vec![String::from("/entry.mjs")],
7239            env,
7240            cwd: std::path::PathBuf::from("/tmp"),
7241            limits: JavascriptExecutionLimits {
7242                v8_heap_limit_mb: Some(64),
7243                sync_rpc_wait_timeout_ms: Some(2_000),
7244                cpu_time_limit_ms: Some(750),
7245                wall_clock_limit_ms: Some(500),
7246                import_cache_materialize_timeout_ms: Some(125),
7247            },
7248            wasm_module_bytes: None,
7249            inline_code: None,
7250        };
7251
7252        assert_eq!(
7253            javascript_heap_limit_mb(&request),
7254            64,
7255            "heap must come from the typed wire limit, not AGENTOS_V8_HEAP_LIMIT_MB"
7256        );
7257        assert_eq!(
7258            javascript_sync_rpc_timeout(&request),
7259            std::time::Duration::from_millis(2_000),
7260            "sync-rpc wait must come from the typed wire limit, not env"
7261        );
7262        assert_eq!(
7263            javascript_cpu_time_limit_ms(&request),
7264            750,
7265            "CPU budget must come from the typed wire limit, not env"
7266        );
7267        assert_eq!(
7268            javascript_wall_clock_limit_ms(&request),
7269            500,
7270            "wall-clock budget must come from the typed wire limit, not env"
7271        );
7272        assert_eq!(
7273            javascript_import_cache_materialize_timeout(&request),
7274            std::time::Duration::from_millis(125),
7275            "import-cache timeout must come from the typed wire limit, not env"
7276        );
7277    }
7278
7279    #[test]
7280    fn javascript_limits_fall_back_to_defaults_when_unset() {
7281        let request = StartJavascriptExecutionRequest {
7282            guest_runtime: Default::default(),
7283            vm_id: String::from("vm-js"),
7284            context_id: String::from("ctx-js"),
7285            argv: vec![String::from("/entry.mjs")],
7286            env: std::collections::BTreeMap::new(),
7287            cwd: std::path::PathBuf::from("/tmp"),
7288            limits: JavascriptExecutionLimits::default(),
7289            wasm_module_bytes: None,
7290            inline_code: None,
7291        };
7292
7293        assert_eq!(
7294            javascript_heap_limit_mb(&request),
7295            0,
7296            "0 selects the engine default heap"
7297        );
7298        assert_eq!(
7299            javascript_sync_rpc_timeout(&request),
7300            std::time::Duration::from_millis(NODE_SYNC_RPC_DEFAULT_WAIT_TIMEOUT_MS),
7301        );
7302        assert_eq!(
7303            javascript_cpu_time_limit_ms(&request),
7304            DEFAULT_V8_CPU_TIME_LIMIT_MS
7305        );
7306        assert_eq!(
7307            javascript_wall_clock_limit_ms(&request),
7308            DEFAULT_V8_WALL_CLOCK_LIMIT_MS
7309        );
7310        assert_eq!(
7311            javascript_import_cache_materialize_timeout(&request),
7312            std::time::Duration::from_millis(DEFAULT_NODE_IMPORT_CACHE_MATERIALIZE_TIMEOUT_MS)
7313        );
7314    }
7315
7316    #[test]
7317    fn inline_code_module_detection_prefers_commonjs_when_import_only_appears_in_comment() {
7318        let source = "// import { x } from 'y';\nmodule.exports = { foo: 1 };";
7319        assert!(!inline_code_uses_module_mode(source));
7320    }
7321
7322    #[test]
7323    fn inline_code_module_detection_ignores_import_inside_string_literal() {
7324        let source = "const msg = \"run: import x from 'y'\";\nmodule.exports.msg = msg;";
7325        assert!(!inline_code_uses_module_mode(source));
7326    }
7327
7328    #[test]
7329    fn inline_code_module_detection_accepts_multiline_import_statements() {
7330        let source = "import\n  { default as foo }\nfrom 'bar';\nconsole.log(foo);";
7331        assert!(inline_code_uses_module_mode(source));
7332    }
7333
7334    #[test]
7335    fn inline_code_module_detection_accepts_real_esm_source() {
7336        let source = "import { foo } from 'bar';\nexport const baz = 1;\nconsole.log(foo, baz);";
7337        assert!(inline_code_uses_module_mode(source));
7338    }
7339
7340    #[test]
7341    fn inline_code_module_detection_is_deterministic_for_empty_comment_only_and_template_cases() {
7342        assert!(!inline_code_uses_module_mode(""));
7343        assert!(!inline_code_uses_module_mode(
7344            "// import x from 'y';\n/* export const z = 1; */"
7345        ));
7346        assert!(!inline_code_uses_module_mode(
7347            "const msg = `export const nope = 1;`;"
7348        ));
7349    }
7350
7351    #[test]
7352    fn javascript_sync_rpc_timeout_writes_clear_error_response() {
7353        let (reader_fd, writer_fd) = pipe2(OFlag::O_CLOEXEC).expect("create pipe");
7354        let reader = File::from(reader_fd);
7355        let writer = File::from(writer_fd);
7356        let response_writer =
7357            JavascriptSyncRpcResponseWriter::new(writer, Duration::from_millis(50));
7358        let pending = Arc::new(Mutex::new(Some(PendingSyncRpcState::Pending(7))));
7359
7360        spawn_javascript_sync_rpc_timeout(
7361            7,
7362            Duration::from_millis(20),
7363            pending.clone(),
7364            Some(response_writer),
7365        );
7366
7367        let mut line = String::new();
7368        let mut reader = BufReader::new(reader);
7369        reader.read_line(&mut line).expect("read timeout response");
7370
7371        let response: Value = serde_json::from_str(line.trim()).expect("parse timeout response");
7372        assert_eq!(response["id"], Value::from(7));
7373        assert_eq!(response["ok"], Value::from(false));
7374        assert_eq!(
7375            response["error"]["code"],
7376            Value::String(String::from("ERR_AGENTOS_NODE_SYNC_RPC_TIMEOUT"))
7377        );
7378        assert!(response["error"]["message"]
7379            .as_str()
7380            .expect("timeout message")
7381            .contains("timed out after 20ms"));
7382        assert_eq!(
7383            *pending.lock().expect("pending state lock"),
7384            Some(PendingSyncRpcState::TimedOut(7))
7385        );
7386    }
7387
7388    #[test]
7389    fn javascript_sync_rpc_response_writer_times_out_when_queue_is_full() {
7390        let (sender, _receiver) = mpsc::sync_channel(1);
7391        let writer = JavascriptSyncRpcResponseWriter {
7392            sender,
7393            timeout: Duration::from_millis(30),
7394        };
7395
7396        writer
7397            .send(b"first\n".to_vec())
7398            .expect("queue first response");
7399
7400        let started = Instant::now();
7401        let error = writer
7402            .send(b"second\n".to_vec())
7403            .expect_err("full queue should time out");
7404        assert!(
7405            started.elapsed() >= Duration::from_millis(30),
7406            "send should wait for the configured timeout"
7407        );
7408        assert!(error
7409            .to_string()
7410            .contains("timed out after 30ms while queueing JavaScript sync RPC response"));
7411    }
7412
7413    #[test]
7414    fn javascript_wait_capture_rejects_output_over_limit() {
7415        let mut stdout = vec![b'x'; JAVASCRIPT_CAPTURED_OUTPUT_LIMIT_BYTES - 1];
7416        append_captured_output(&mut stdout, vec![b'y'], "stdout").expect("fill to limit");
7417        assert_eq!(stdout.len(), JAVASCRIPT_CAPTURED_OUTPUT_LIMIT_BYTES);
7418
7419        let error = append_captured_output(&mut stdout, vec![b'z'], "stdout")
7420            .expect_err("captured output over limit should fail");
7421        assert!(matches!(
7422            error,
7423            JavascriptExecutionError::OutputBufferExceeded {
7424                stream: "stdout",
7425                limit: JAVASCRIPT_CAPTURED_OUTPUT_LIMIT_BYTES,
7426            }
7427        ));
7428    }
7429
7430    #[test]
7431    fn kernel_stdin_bridge_rejects_buffer_over_limit_and_closed_writes() {
7432        let bridge = LocalKernelStdinBridge::default();
7433        bridge
7434            .write(&vec![b'x'; KERNEL_STDIN_BUFFER_LIMIT_BYTES])
7435            .expect("fill stdin buffer to limit");
7436
7437        let error = bridge
7438            .write(b"y")
7439            .expect_err("stdin buffer over limit should fail");
7440        assert!(matches!(error, JavascriptExecutionError::Stdin(_)));
7441
7442        let bridge = LocalKernelStdinBridge::default();
7443        bridge.close();
7444        let error = bridge
7445            .write(b"x")
7446            .expect_err("write after stdin close should fail");
7447        assert!(matches!(error, JavascriptExecutionError::StdinClosed));
7448    }
7449
7450    #[test]
7451    fn javascript_event_sender_reports_closed_receiver() {
7452        let (sender, receiver) = channel(1);
7453        drop(receiver);
7454        let host = V8RuntimeHost::spawn().expect("spawn V8 runtime host");
7455        let session = host.session_handle(String::from("closed-event-sender-test"));
7456        let gauge = register_queue(TrackedLimit::JavascriptEventChannel, 1);
7457        assert!(!send_javascript_event(
7458            &sender,
7459            &session,
7460            &gauge,
7461            None,
7462            JavascriptExecutionEvent::Exited(1)
7463        ));
7464    }
7465
7466    // Regression: a full event channel must apply backpressure, not destroy the
7467    // session. The old code called `v8_session.destroy()` on the first `Full`,
7468    // truncating the stream and tearing the session down.
7469    #[test]
7470    fn javascript_event_sender_backpressures_instead_of_destroying_when_full() {
7471        let host = V8RuntimeHost::spawn().expect("spawn V8 runtime host");
7472        let session_id = format!(
7473            "event-overflow-{}",
7474            SystemTime::now()
7475                .duration_since(UNIX_EPOCH)
7476                .expect("system time")
7477                .as_nanos()
7478        );
7479        let _session_receiver = host
7480            .register_session(&session_id)
7481            .expect("register event overflow session");
7482        let session = host.session_handle(session_id.clone());
7483        let gauge = register_queue(TrackedLimit::JavascriptEventChannel, 1);
7484        let (sender, mut event_receiver) = channel(1);
7485
7486        // Drain slowly on another thread so the producer is forced onto the
7487        // blocking-backpressure path the old destroy-on-full code never reached.
7488        let drainer = std::thread::spawn(move || {
7489            let mut drained = 0usize;
7490            while event_receiver.blocking_recv().is_some() {
7491                drained += 1;
7492                std::thread::sleep(std::time::Duration::from_millis(1));
7493            }
7494            drained
7495        });
7496
7497        // Far more events than the 1-slot channel holds; every send must succeed.
7498        const SENDS: usize = 16;
7499        for _ in 0..SENDS {
7500            assert!(send_javascript_event(
7501                &sender,
7502                &session,
7503                &gauge,
7504                None,
7505                JavascriptExecutionEvent::Stdout(Vec::new())
7506            ));
7507        }
7508        drop(sender);
7509        let drained = drainer.join().expect("drainer thread panicked");
7510        assert_eq!(drained, SENDS, "every event must survive backpressure");
7511
7512        // The session must NOT have been destroyed: it is still registered, so a
7513        // re-registration attempt fails.
7514        host.register_session(&session_id)
7515            .expect_err("session must survive backpressure (not be destroyed)");
7516        host.unregister_session(&session_id);
7517    }
7518
7519    #[test]
7520    fn javascript_event_sender_destroys_session_when_event_is_oversized() {
7521        let host = V8RuntimeHost::spawn().expect("spawn V8 runtime host");
7522        let session_id = format!(
7523            "event-oversized-{}",
7524            SystemTime::now()
7525                .duration_since(UNIX_EPOCH)
7526                .expect("system time")
7527                .as_nanos()
7528        );
7529        let receiver = host
7530            .register_session(&session_id)
7531            .expect("register oversized event session");
7532        let session = host.session_handle(session_id.clone());
7533        let (sender, _event_receiver) = channel(JAVASCRIPT_EVENT_CHANNEL_CAPACITY);
7534        let gauge = register_queue(
7535            TrackedLimit::JavascriptEventChannel,
7536            JAVASCRIPT_EVENT_CHANNEL_CAPACITY,
7537        );
7538
7539        assert!(!send_javascript_event(
7540            &sender,
7541            &session,
7542            &gauge,
7543            None,
7544            JavascriptExecutionEvent::Stdout(vec![0; JAVASCRIPT_EVENT_PAYLOAD_LIMIT_BYTES + 1])
7545        ));
7546
7547        drop(receiver);
7548        let recovered = host
7549            .register_session(&session_id)
7550            .expect("oversized event should destroy and deregister the session");
7551        drop(recovered);
7552        host.unregister_session(&session_id);
7553    }
7554
7555    #[test]
7556    fn internal_bridge_host_context_resolves_relative_module_path() {
7557        let unique = SystemTime::now()
7558            .duration_since(UNIX_EPOCH)
7559            .expect("system time")
7560            .as_nanos();
7561        let root = std::env::temp_dir().join(format!(
7562            "secure-exec-module-bridge-{}-{unique}",
7563            std::process::id()
7564        ));
7565        let bin_dir = root.join("node_modules/next/dist/bin");
7566        let cli_dir = root.join("node_modules/next/dist/cli");
7567        fs::create_dir_all(&bin_dir).expect("create bin dir");
7568        fs::create_dir_all(&cli_dir).expect("create cli dir");
7569        fs::write(
7570            root.join("node_modules/next/package.json"),
7571            r#"{"name":"next"}"#,
7572        )
7573        .expect("write package.json");
7574        fs::write(bin_dir.join("next"), "#!/usr/bin/env node\n").expect("write next bin");
7575        fs::write(cli_dir.join("next-build.js"), "module.exports = 1;\n")
7576            .expect("write next-build.js");
7577
7578        let env = BTreeMap::new();
7579        let result = handle_internal_bridge_call_from_host_context(
7580            &root,
7581            "/",
7582            &env,
7583            "_resolveModule",
7584            &[
7585                Value::String(String::from("../cli/next-build.js")),
7586                Value::String(String::from("/node_modules/next/dist/bin/next")),
7587                Value::String(String::from("import")),
7588            ],
7589        );
7590
7591        assert_eq!(
7592            result,
7593            Some(Value::String(String::from(
7594                "/node_modules/next/dist/cli/next-build.js"
7595            )))
7596        );
7597
7598        fs::remove_dir_all(&root).expect("remove temp module tree");
7599    }
7600
7601    #[test]
7602    fn register_v8_session_deregisters_on_create_session_failure() {
7603        let host = V8RuntimeHost::spawn().expect("spawn V8 runtime host");
7604        let session_id = format!(
7605            "v8-register-failure-{}",
7606            SystemTime::now()
7607                .duration_since(UNIX_EPOCH)
7608                .expect("system time")
7609                .as_nanos()
7610        );
7611
7612        let error =
7613            match register_v8_session(&host, session_id.clone(), 0, 0, 0, None, |_command| {
7614                Err(std::io::Error::new(
7615                    std::io::ErrorKind::BrokenPipe,
7616                    "simulated CreateSession send failure",
7617                ))
7618            }) {
7619                Ok(_) => panic!("register_v8_session should surface create-session send failures"),
7620                Err(error) => error,
7621            };
7622
7623        match error {
7624            JavascriptExecutionError::Spawn(inner) => {
7625                assert_eq!(inner.kind(), std::io::ErrorKind::BrokenPipe);
7626            }
7627            other => panic!("unexpected error: {other:?}"),
7628        }
7629        let receiver = host
7630            .register_session(&session_id)
7631            .expect("failed registration should not leak the session output receiver");
7632        drop(receiver);
7633        host.unregister_session(&session_id);
7634    }
7635
7636    #[test]
7637    fn javascript_cpu_time_limit_defaults_to_bounded_value() {
7638        let request = StartJavascriptExecutionRequest {
7639            limits: Default::default(),
7640            guest_runtime: Default::default(),
7641            vm_id: String::from("vm-js-default-cpu"),
7642            context_id: String::from("ctx-js-default-cpu"),
7643            argv: vec![String::from("./entry.mjs")],
7644            env: BTreeMap::new(),
7645            cwd: std::path::PathBuf::from("/tmp"),
7646            wasm_module_bytes: None,
7647            inline_code: None,
7648        };
7649
7650        assert_eq!(
7651            javascript_cpu_time_limit_ms(&request),
7652            30_000,
7653            "unset JavaScript CPU budget must be bounded by default"
7654        );
7655    }
7656
7657    #[test]
7658    fn javascript_execution_drop_keeps_normal_v8_session_cleanup() {
7659        let temp = tempdir().expect("create temp dir");
7660        let mut engine = JavascriptExecutionEngine::default();
7661        let context = engine.create_context(CreateJavascriptContextRequest {
7662            vm_id: String::from("vm-drop-cleanup"),
7663            bootstrap_module: None,
7664            compile_cache_root: None,
7665        });
7666
7667        let execution = engine
7668            .start_execution(StartJavascriptExecutionRequest {
7669                limits: Default::default(),
7670                guest_runtime: Default::default(),
7671                vm_id: String::from("vm-drop-cleanup"),
7672                context_id: context.context_id,
7673                argv: vec![String::from("./entry.mjs")],
7674                env: BTreeMap::new(),
7675                cwd: temp.path().to_path_buf(),
7676                wasm_module_bytes: None,
7677                inline_code: Some(String::from("globalThis.__agentOSDropCleanup = true;")),
7678            })
7679            .expect("start JavaScript execution");
7680        let session_id = execution.v8_session.session_id().to_owned();
7681        let host = engine.v8_host.as_ref().expect("shared V8 runtime host");
7682
7683        drop(execution);
7684
7685        let receiver = host
7686            .register_session(&session_id)
7687            .expect("execution drop should still destroy and deregister the session");
7688        drop(receiver);
7689        host.unregister_session(&session_id);
7690    }
7691
7692    // --- Timer cancellation / cap regression tests (U4: H2 bridge timers, M3
7693    // kernel timers). These assert the *safeguards firing* (delay clamped, timer
7694    // entry reclaimed, callback suppressed) and never spawn unbounded threads. ---
7695
7696    #[test]
7697    fn timer_delay_is_clamped_to_the_cap() {
7698        // A guest can pass an arbitrarily large delay (up to u64::MAX ms); without
7699        // a cap the timer wheel could retain a session Arc behind a deadline that
7700        // is effectively forever away. The cap bounds that lifetime.
7701        assert_eq!(
7702            timer_delay_ms(Some(&json!(u64::MAX))),
7703            MAX_TIMER_DELAY_MS,
7704            "a u64::MAX delay must be clamped to MAX_TIMER_DELAY_MS"
7705        );
7706        assert_eq!(
7707            timer_delay_ms(Some(&json!(1.0e308_f64))),
7708            MAX_TIMER_DELAY_MS,
7709            "an enormous float delay must be clamped to the cap"
7710        );
7711        assert_eq!(
7712            timer_delay_ms(Some(&json!(MAX_TIMER_DELAY_MS + 1))),
7713            MAX_TIMER_DELAY_MS,
7714            "a delay one past the cap must clamp down to the cap"
7715        );
7716        // Below-cap values pass through unchanged so normal timers are unaffected.
7717        assert_eq!(timer_delay_ms(Some(&json!(250))), 250);
7718        assert_eq!(timer_delay_ms(Some(&json!(0))), 0);
7719    }
7720
7721    #[test]
7722    fn cleared_timer_is_suppressed_and_entry_reclaimed() {
7723        // Mirrors what a woken bridge/kernel timer action does after waiting: it
7724        // consults the shared map via `timer_should_fire`. When the entry has been
7725        // removed (clear or session teardown), the callback must be suppressed.
7726        let timers: Arc<Mutex<HashMap<u64, LocalTimerEntry>>> =
7727            Arc::new(Mutex::new(HashMap::new()));
7728        timers.lock().unwrap().insert(
7729            7,
7730            LocalTimerEntry {
7731                delay_ms: 1_000,
7732                generation: 0,
7733                repeat: false,
7734            },
7735        );
7736
7737        // Simulate `kernelTimerClear` / teardown removing the entry before the
7738        // action wakes.
7739        timers.lock().unwrap().remove(&7);
7740
7741        assert!(
7742            !timer_should_fire(&timers, 7, 0),
7743            "a cleared timer must not fire"
7744        );
7745        assert!(
7746            timers.lock().unwrap().is_empty(),
7747            "tracking map stays empty after a cleared timer is evaluated"
7748        );
7749    }
7750
7751    #[test]
7752    fn rearmed_timer_generation_mismatch_suppresses_stale_action() {
7753        // The bridge/kernel timer action captures the generation at schedule time.
7754        // If the timer is re-armed (generation bumped) before the stale action
7755        // wakes, the stale action must observe the mismatch and suppress, while the
7756        // entry survives for the live generation.
7757        let timers: Arc<Mutex<HashMap<u64, LocalTimerEntry>>> =
7758            Arc::new(Mutex::new(HashMap::new()));
7759        timers.lock().unwrap().insert(
7760            3,
7761            LocalTimerEntry {
7762                delay_ms: 10,
7763                generation: 1,
7764                repeat: false,
7765            },
7766        );
7767
7768        // Stale action captured generation 0; current entry is at generation 1.
7769        assert!(
7770            !timer_should_fire(&timers, 3, 0),
7771            "a stale generation must be suppressed"
7772        );
7773        assert!(
7774            timers.lock().unwrap().contains_key(&3),
7775            "the live entry must survive a stale-generation evaluation"
7776        );
7777
7778        // The matching (current) generation fires and reclaims the one-shot entry.
7779        assert!(
7780            timer_should_fire(&timers, 3, 1),
7781            "the current generation must fire"
7782        );
7783        assert!(
7784            timers.lock().unwrap().is_empty(),
7785            "a fired one-shot timer must reclaim its id from the map"
7786        );
7787    }
7788
7789    #[test]
7790    fn bridge_timer_registration_is_tracked_and_drop_clears_timers() {
7791        // H2: the bridge-timer path must register its timer (so it is cancellable)
7792        // before queuing a wheel action, and session teardown
7793        // (dropping LocalBridgeState) must wipe the tracking map so in-flight timer
7794        // actions are cancelled.
7795        let mut state = LocalBridgeState::default();
7796        // Observe the same map the queued actions would consult.
7797        let timers = state.timers.clone();
7798
7799        let id_a = state.register_oneshot_timer(MAX_TIMER_DELAY_MS);
7800        let id_b = state.register_oneshot_timer(500);
7801        assert_ne!(id_a, id_b, "each bridge timer gets a fresh id");
7802        assert_eq!(
7803            timers.lock().unwrap().len(),
7804            2,
7805            "registered bridge timers are tracked in the shared map"
7806        );
7807        // A registered timer would fire for its captured generation (proving the
7808        // entry is real and consultable) ...
7809        assert!(timer_should_fire(&timers, id_a, 0));
7810        // ... and seeding a still-pending one before teardown:
7811        let id_c = state.register_oneshot_timer(1_000);
7812
7813        // Session teardown: dropping the bridge state must clear every timer so any
7814        // queued action wakes to a missing entry and suppresses its callback.
7815        drop(state);
7816
7817        assert!(
7818            timers.lock().unwrap().is_empty(),
7819            "dropping LocalBridgeState must clear the timers map on teardown"
7820        );
7821        assert!(
7822            !timer_should_fire(&timers, id_c, 0),
7823            "a pending bridge timer is suppressed after teardown"
7824        );
7825    }
7826}