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