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