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