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