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