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