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