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