Skip to main content

agentos_execution/
javascript.rs

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