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