1use crate::common::{
2 encode_json_string, encode_json_string_array, encode_json_string_map, frozen_time_ms,
3};
4use crate::javascript::{
5 CreateJavascriptContextRequest, GuestRuntimeConfig, JavascriptExecution,
6 JavascriptExecutionEngine, JavascriptExecutionError, JavascriptExecutionEvent,
7 JavascriptExecutionLimits, JavascriptSyncRpcRequest, StartJavascriptExecutionRequest,
8};
9use crate::node_import_cache::NodeImportCache;
10use crate::runtime_support::{env_flag_enabled, file_fingerprint, warmup_marker_path};
11use crate::signal::{NodeSignalDispositionAction, NodeSignalHandlerRegistration};
12use crate::v8_host::{V8RuntimeHost, V8SessionHandle};
13use crate::v8_runtime;
14use agentos_bridge::queue_tracker::{
15 register_limit, warn_limit_exhausted, QueueGauge, TrackedLimit,
16};
17use agentos_runtime::RuntimeContext;
18use base64::Engine as _;
19use serde_json::{json, Value};
20use std::collections::{BTreeMap, HashMap, VecDeque};
21use std::fmt;
22use std::fs;
23use std::fs::OpenOptions;
24use std::io::{Read, Write};
25use std::os::unix::fs::{FileExt, MetadataExt, PermissionsExt};
26use std::path::{Path, PathBuf};
27use std::sync::{Arc, Mutex, OnceLock};
28use std::time::{Duration, Instant};
29use tokio::sync::Notify;
30
31const WASM_MODULE_PATH_ENV: &str = "AGENTOS_WASM_MODULE_PATH";
32const WASM_GUEST_ARGV_ENV: &str = "AGENTOS_GUEST_ARGV";
33const WASM_GUEST_ENV_ENV: &str = "AGENTOS_GUEST_ENV";
34const WASM_PERMISSION_TIER_ENV: &str = "AGENTOS_WASM_PERMISSION_TIER";
35const WASM_PREWARM_ONLY_ENV: &str = "AGENTOS_WASM_PREWARM_ONLY";
36const WASM_HOST_CWD_ENV: &str = "AGENTOS_WASM_HOST_CWD";
37const WASM_SANDBOX_ROOT_ENV: &str = "AGENTOS_SANDBOX_ROOT";
38const WASM_WARMUP_DEBUG_ENV: &str = "AGENTOS_WASM_WARMUP_DEBUG";
39pub const WASM_MAX_FUEL_ENV: &str = "AGENTOS_WASM_MAX_FUEL";
40pub const WASM_MAX_MEMORY_BYTES_ENV: &str = "AGENTOS_WASM_MAX_MEMORY_BYTES";
41pub const WASM_MAX_STACK_BYTES_ENV: &str = "AGENTOS_WASM_MAX_STACK_BYTES";
42pub const WASM_MAX_MODULE_FILE_BYTES_ENV: &str = "AGENTOS_WASM_MAX_MODULE_FILE_BYTES";
43const WASM_MAX_OPEN_FDS_ENV: &str = "AGENTOS_WASM_MAX_OPEN_FDS";
44const WASM_MAX_SPAWN_FILE_ACTIONS_ENV: &str = "AGENTOS_WASM_MAX_SPAWN_FILE_ACTIONS";
45const WASM_MAX_SPAWN_FILE_ACTION_BYTES_ENV: &str = "AGENTOS_WASM_MAX_SPAWN_FILE_ACTION_BYTES";
46const WASM_MAX_SOCKETS_ENV: &str = "AGENTOS_WASM_MAX_SOCKETS";
47const WASM_MAX_BLOCKING_READ_MS_ENV: &str = "AGENTOS_WASM_MAX_BLOCKING_READ_MS";
48const WASM_INTERNAL_MAX_STACK_BYTES_ENV: &str = "AGENTOS_INTERNAL_WASM_MAX_STACK_BYTES";
49const WASM_WARMUP_METRICS_PREFIX: &str = "__AGENTOS_WASM_WARMUP_METRICS__:";
50const WASM_SIGNAL_STATE_PREFIX: &str = "__AGENTOS_WASM_SIGNAL_STATE__:";
51const WASM_WARMUP_MARKER_VERSION: &str = "1";
52const WASM_PAGE_BYTES: u64 = 65_536;
53const WASM_TIMEOUT_EXIT_CODE: i32 = 124;
54const MAX_WASM_MODULE_FILE_BYTES: u64 = 256 * 1024 * 1024;
55const MAX_WASM_IMPORT_SECTION_ENTRIES: usize = 16_384;
56const MAX_WASM_MEMORY_SECTION_ENTRIES: usize = 1_024;
57const MAX_WASM_VARUINT_BYTES: usize = 10;
58const DEFAULT_WASM_GUEST_HOME: &str = "/root";
59const DEFAULT_WASM_GUEST_USER: &str = "root";
60const DEFAULT_WASM_GUEST_SHELL: &str = "/bin/sh";
61const DEFAULT_WASM_GUEST_PATH: &str =
62 "/usr/local/sbin:/usr/local/bin:/opt/agentos/bin:/usr/sbin:/usr/bin:/sbin:/bin";
63const DEFAULT_WASM_PREWARM_TIMEOUT_MS: u64 = 30_000;
66const DEFAULT_WASM_RUNNER_HEAP_LIMIT_MB: u32 = 2048;
86const _: () = assert!(DEFAULT_WASM_RUNNER_HEAP_LIMIT_MB > 128);
89const MAX_SYNC_WASM_PREWARM_MODULE_BYTES: u64 = 16 * 1024 * 1024;
90const WASM_CAPTURED_OUTPUT_LIMIT_BYTES: usize = 16 * 1024 * 1024;
91const WASM_SYNC_READ_LIMIT_BYTES: usize = 16 * 1024 * 1024;
92const WASM_PROCESS_SYNC_RPC_RESPONSE_BYTES: usize = 256 * 1024;
96const WASM_INLINE_RUNNER_ENTRYPOINT: &str = "./__agentos_wasm_runner__.mjs";
97const WASM_SNAPSHOT_RUNNER_ENV: &str = "AGENTOS_WASM_SNAPSHOT_RUNNER";
98const WASM_RUNNER_NO_CACHE_ENV: &str = "AGENTOS_WASM_RUNNER_NO_CACHE";
99const WASM_MODULE_BYTES_CACHE_CAPACITY: usize = 64;
100const NODE_WASI_MODULE_SOURCE: &str = include_str!("../assets/runners/wasi-module.js");
101const WASM_SIDECAR_ROUTED_FS_SYNC_METHODS: &[&str] = &[
102 "fs.accessSync",
103 "fs.blockingIoTimeoutMsSync",
104 "fs.chmodForProcessSync",
105 "fs.chmodSync",
106 "fs.chownSync",
107 "fs.closeSync",
108 "fs.collapseRangeSync",
109 "fs.existsSync",
110 "fs.fallocateSync",
111 "fs.fdatasyncSync",
112 "fs.fiemapSync",
113 "fs.fstatSync",
114 "fs.fsyncSync",
115 "fs.ftruncateSync",
116 "fs.getxattrSync",
117 "fs.insertRangeSync",
118 "fs.lchownSync",
119 "fs.linkFdSync",
120 "fs.linkSync",
121 "fs.listxattrSync",
122 "fs.lstatSync",
123 "fs.mkdirSync",
124 "fs.mknodSync",
125 "fs.namedFifoPeerReadySync",
126 "fs.openSync",
127 "fs.openTmpfileSync",
128 "fs.punchHoleSync",
129 "fs.readFileSync",
130 "fs.readSync",
131 "fs.readdirSync",
132 "fs.readlinkSync",
133 "fs.remountSync",
134 "fs.removexattrSync",
135 "fs.renameAt2Sync",
136 "fs.renameSync",
137 "fs.rmdirSync",
138 "fs.setxattrSync",
139 "fs.statfsSync",
140 "fs.statSync",
141 "fs.symlinkSync",
142 "fs.truncateForProcessSync",
143 "fs.unlinkSync",
144 "fs.zeroRangeSync",
145 "fs.writeFileSync",
146 "fs.writeSync",
147];
148const WASM_SIDECAR_ROUTED_KERNEL_SYNC_METHODS: &[&str] = &[
149 "__kernel_isatty",
150 "__kernel_poll",
151 "__kernel_stdin_read",
152 "__kernel_stdio_write",
153 "__kernel_tty_size",
154 "__pty_set_raw_mode",
155];
156
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158pub enum WasmSignalDispositionAction {
159 Default,
160 Ignore,
161 User,
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
165#[serde(rename_all = "kebab-case")]
166pub enum WasmPermissionTier {
167 Full,
168 ReadWrite,
169 ReadOnly,
170 Isolated,
171}
172
173impl WasmPermissionTier {
174 fn as_env_value(self) -> &'static str {
175 match self {
176 Self::Full => "full",
177 Self::ReadWrite => "read-write",
178 Self::ReadOnly => "read-only",
179 Self::Isolated => "isolated",
180 }
181 }
182}
183
184#[derive(Debug, Clone, PartialEq, Eq)]
185pub struct WasmSignalHandlerRegistration {
186 pub action: WasmSignalDispositionAction,
187 pub mask: Vec<u32>,
188 pub flags: u32,
189}
190
191#[derive(Debug, Clone, PartialEq, Eq)]
192pub struct CreateWasmContextRequest {
193 pub vm_id: String,
194 pub module_path: Option<String>,
195}
196
197#[derive(Debug, Clone, PartialEq, Eq)]
198pub struct WasmContext {
199 pub context_id: String,
200 pub vm_id: String,
201 pub module_path: Option<String>,
202}
203
204#[derive(Debug, Clone, Default, PartialEq, Eq)]
210pub struct WasmExecutionLimits {
211 pub max_fuel: Option<u64>,
213 pub max_memory_bytes: Option<u64>,
216 pub max_stack_bytes: Option<u64>,
220 pub max_module_file_bytes: Option<u64>,
223 pub max_spawn_file_actions: Option<u64>,
225 pub max_spawn_file_action_bytes: Option<u64>,
227 pub max_open_fds: Option<u64>,
229 pub max_sockets: Option<u64>,
231 pub max_blocking_read_ms: Option<u64>,
233 pub prewarm_timeout_ms: Option<u64>,
235 pub runner_heap_limit_mb: Option<u32>,
237 pub runner_cpu_time_limit_ms: Option<u32>,
239 pub reactor_work_quantum: Option<usize>,
241 pub bridge_call_timeout_ms: Option<u64>,
243}
244
245#[derive(Debug, Clone, PartialEq, Eq)]
246pub struct StartWasmExecutionRequest {
247 pub vm_id: String,
248 pub context_id: String,
249 pub argv: Vec<String>,
250 pub env: BTreeMap<String, String>,
251 pub cwd: PathBuf,
252 pub permission_tier: WasmPermissionTier,
253 pub limits: WasmExecutionLimits,
255 pub guest_runtime: GuestRuntimeConfig,
259}
260
261#[derive(Debug, Clone, PartialEq, Eq)]
262pub enum WasmExecutionEvent {
263 Stdout(Vec<u8>),
264 Stderr(Vec<u8>),
265 SyncRpcRequest(JavascriptSyncRpcRequest),
266 SignalState {
267 signal: u32,
268 registration: WasmSignalHandlerRegistration,
269 },
270 Exited(i32),
271}
272
273#[derive(Debug, Clone, PartialEq, Eq)]
274pub struct WasmExecutionResult {
275 pub execution_id: String,
276 pub exit_code: i32,
277 pub stdout: Vec<u8>,
278 pub stderr: Vec<u8>,
279}
280
281#[derive(Debug, Clone, PartialEq, Eq)]
282struct ResolvedWasmModule {
283 specifier: String,
284 resolved_path: PathBuf,
285}
286
287#[derive(Debug, Clone, Copy, PartialEq, Eq)]
288pub enum NativeBinaryFormat {
289 Elf,
290 MachO,
291 PeCoff,
292}
293
294impl NativeBinaryFormat {
295 fn display_name(self) -> &'static str {
296 match self {
297 Self::Elf => "ELF",
298 Self::MachO => "Mach-O",
299 Self::PeCoff => "PE/COFF",
300 }
301 }
302}
303
304#[derive(Debug)]
305pub enum WasmExecutionError {
306 MissingContext(String),
307 VmMismatch {
308 expected: String,
309 found: String,
310 },
311 MissingModulePath,
312 InvalidLimit(String),
313 InvalidModule(String),
314 NativeBinaryNotSupported {
315 path: PathBuf,
316 header: Vec<u8>,
317 format: NativeBinaryFormat,
318 },
319 NonWasmBinary {
320 path: PathBuf,
321 header: Vec<u8>,
322 shell_shim: bool,
323 },
324 PrepareWarmPath(std::io::Error),
325 WarmupSpawn(std::io::Error),
326 WarmupTimeout(Duration),
327 WarmupFailed {
328 exit_code: i32,
329 stderr: String,
330 },
331 Spawn(std::io::Error),
332 Control(std::io::Error),
333 RpcResponse(String),
334 StdinClosed,
335 Stdin(std::io::Error),
336 OutputBufferExceeded {
337 stream: &'static str,
338 limit: usize,
339 },
340 EventChannelClosed,
341}
342
343impl fmt::Display for WasmExecutionError {
344 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
345 match self {
346 Self::MissingContext(context_id) => {
347 write!(f, "unknown guest WebAssembly context: {context_id}")
348 }
349 Self::VmMismatch { expected, found } => {
350 write!(
351 f,
352 "guest WebAssembly context belongs to vm {expected}, not {found}"
353 )
354 }
355 Self::MissingModulePath => {
356 f.write_str("guest WebAssembly execution requires a module path")
357 }
358 Self::InvalidLimit(message) => write!(f, "invalid WebAssembly limit: {message}"),
359 Self::InvalidModule(message) => write!(f, "invalid WebAssembly module: {message}"),
360 Self::NativeBinaryNotSupported {
361 path,
362 header,
363 format,
364 } => {
365 let header_hex = header
366 .iter()
367 .map(|byte| format!("{byte:02x}"))
368 .collect::<Vec<_>>()
369 .join(" ");
370 write!(
371 f,
372 "ERR_NATIVE_BINARY_NOT_SUPPORTED: refused to execute native {} guest binary at {} inside the VM; only WebAssembly binaries are runnable there (header bytes: [{header_hex}])",
373 format.display_name(),
374 path.display()
375 )
376 }
377 Self::NonWasmBinary {
378 path,
379 header,
380 shell_shim,
381 } => {
382 let header_hex = header
383 .iter()
384 .map(|byte| format!("{byte:02x}"))
385 .collect::<Vec<_>>()
386 .join(" ");
387 if *shell_shim {
388 write!(
389 f,
390 "refused to compile guest WebAssembly module at {}: file is a shell-shim script (starts with \"#!\", header bytes: [{header_hex}]) instead of a \"\\0asm\" WebAssembly binary",
391 path.display()
392 )
393 } else {
394 write!(
395 f,
396 "refused to compile guest WebAssembly module at {}: first {} byte(s) [{header_hex}] do not match the \"\\0asm\" WebAssembly magic word",
397 path.display(),
398 header.len()
399 )
400 }
401 }
402 Self::PrepareWarmPath(err) => {
403 write!(f, "failed to prepare shared WebAssembly warm path: {err}")
404 }
405 Self::WarmupSpawn(err) => {
406 write!(f, "failed to start WebAssembly warmup runtime: {err}")
407 }
408 Self::WarmupTimeout(timeout) => {
409 write!(
410 f,
411 "WebAssembly warmup exceeded the configured timeout after {} ms",
412 timeout.as_millis()
413 )
414 }
415 Self::WarmupFailed { exit_code, stderr } => {
416 if stderr.trim().is_empty() {
417 write!(f, "WebAssembly warmup exited with status {exit_code}")
418 } else {
419 write!(
420 f,
421 "WebAssembly warmup exited with status {exit_code}: {}",
422 stderr.trim()
423 )
424 }
425 }
426 Self::Spawn(err) => write!(f, "failed to start guest WebAssembly runtime: {err}"),
427 Self::Control(err) => write!(f, "failed to control guest WebAssembly runtime: {err}"),
428 Self::RpcResponse(message) => {
429 write!(
430 f,
431 "failed to write guest WebAssembly sync RPC response: {message}"
432 )
433 }
434 Self::StdinClosed => f.write_str("guest WebAssembly stdin is already closed"),
435 Self::Stdin(err) => write!(f, "failed to write guest stdin: {err}"),
436 Self::OutputBufferExceeded { stream, limit } => {
437 write!(
438 f,
439 "guest WebAssembly {stream} exceeded the captured output limit of {limit} bytes"
440 )
441 }
442 Self::EventChannelClosed => {
443 f.write_str("guest WebAssembly event channel closed unexpectedly")
444 }
445 }
446 }
447}
448
449impl std::error::Error for WasmExecutionError {}
450
451#[derive(Debug)]
452pub struct WasmExecution {
453 execution_id: String,
454 child_pid: u32,
455 inner: JavascriptExecution,
456 execution_timeout: Option<Duration>,
457 execution_started_at: Instant,
458 timeout_reported: bool,
459 fuel_gauge: Option<Arc<QueueGauge>>,
460 internal_sync_rpc: WasmInternalSyncRpc,
461 pending_events: VecDeque<WasmExecutionEvent>,
462 stdout_stream_buffer: Vec<u8>,
463 stderr_stream_buffer: Vec<u8>,
464 max_stack_bytes: Option<u64>,
465 pending_v8_stack_overflow: Option<Vec<u8>>,
466}
467
468#[derive(Debug)]
469struct WasmInternalSyncRpc {
470 module_guest_paths: Vec<String>,
471 module_host_path: PathBuf,
472 guest_cwd: String,
473 host_cwd: PathBuf,
474 sandbox_root: Option<PathBuf>,
475 guest_path_mappings: Vec<WasmGuestPathMapping>,
476 route_fs_through_sidecar: bool,
477 next_fd: u32,
478 open_files: BTreeMap<u32, fs::File>,
479 pending_events: VecDeque<WasmExecutionEvent>,
480}
481
482#[derive(Debug, Clone)]
483struct WasmGuestPathMapping {
484 guest_path: String,
485 host_path: PathBuf,
486 read_only: bool,
487}
488
489impl WasmExecution {
490 pub fn execution_id(&self) -> &str {
491 &self.execution_id
492 }
493
494 pub fn child_pid(&self) -> u32 {
495 self.child_pid
496 }
497
498 pub fn v8_session_handle(&self) -> V8SessionHandle {
499 self.inner.v8_session_handle()
500 }
501
502 pub fn uses_shared_v8_runtime(&self) -> bool {
503 self.inner.uses_shared_v8_runtime()
504 }
505
506 pub fn start_prepared(&mut self) -> Result<(), WasmExecutionError> {
507 self.inner.start_prepared().map_err(map_javascript_error)?;
508 self.execution_started_at = Instant::now();
509 Ok(())
510 }
511
512 #[doc(hidden)]
513 pub fn is_prepared_for_start(&self) -> bool {
514 self.inner.is_prepared_for_start()
515 }
516
517 pub fn write_stdin(&mut self, chunk: &[u8]) -> Result<(), WasmExecutionError> {
518 self.inner.write_stdin(chunk).map_err(map_javascript_error)
519 }
520
521 pub fn write_stdin_kernel_only(&mut self, chunk: &[u8]) -> Result<(), WasmExecutionError> {
528 self.inner
529 .write_kernel_stdin_only(chunk)
530 .map_err(map_javascript_error)
531 }
532
533 pub fn close_stdin(&mut self) -> Result<(), WasmExecutionError> {
534 self.inner.close_stdin().map_err(map_javascript_error)
535 }
536
537 pub fn send_stream_event(
538 &self,
539 event_type: &str,
540 payload: Value,
541 ) -> Result<(), WasmExecutionError> {
542 self.inner
543 .send_stream_event(event_type, payload)
544 .map_err(map_javascript_error)
545 }
546
547 pub fn terminate(&self) -> Result<(), WasmExecutionError> {
548 self.inner.terminate().map_err(map_javascript_error)
549 }
550
551 pub fn pause(&self) -> Result<(), WasmExecutionError> {
552 self.inner.pause().map_err(map_javascript_error)
553 }
554
555 pub fn resume(&self) -> Result<(), WasmExecutionError> {
556 self.inner.resume().map_err(map_javascript_error)
557 }
558
559 pub fn respond_sync_rpc_success(
560 &mut self,
561 id: u64,
562 result: Value,
563 ) -> Result<(), WasmExecutionError> {
564 self.inner
565 .respond_sync_rpc_success(id, result)
566 .map_err(map_javascript_error)
567 }
568
569 pub fn claim_sync_rpc_response(&mut self, id: u64) -> Result<bool, WasmExecutionError> {
570 self.inner
571 .claim_sync_rpc_response(id)
572 .map_err(map_javascript_error)
573 }
574
575 pub fn respond_claimed_sync_rpc_success(
576 &mut self,
577 id: u64,
578 result: Value,
579 ) -> Result<(), WasmExecutionError> {
580 self.inner
581 .respond_claimed_sync_rpc_success(id, result)
582 .map_err(map_javascript_error)
583 }
584
585 pub fn respond_sync_rpc_raw_success(
586 &mut self,
587 id: u64,
588 payload: Vec<u8>,
589 ) -> Result<(), WasmExecutionError> {
590 self.inner
591 .respond_sync_rpc_raw_success(id, payload)
592 .map_err(map_javascript_error)
593 }
594
595 pub fn respond_sync_rpc_error(
596 &mut self,
597 id: u64,
598 code: impl Into<String>,
599 message: impl Into<String>,
600 ) -> Result<(), WasmExecutionError> {
601 self.inner
602 .respond_sync_rpc_error(id, code, message)
603 .map_err(map_javascript_error)
604 }
605
606 pub fn respond_claimed_sync_rpc_error(
607 &mut self,
608 id: u64,
609 code: impl Into<String>,
610 message: impl Into<String>,
611 ) -> Result<(), WasmExecutionError> {
612 self.inner
613 .respond_claimed_sync_rpc_error(id, code, message)
614 .map_err(map_javascript_error)
615 }
616
617 pub async fn poll_event(
618 &mut self,
619 timeout: Duration,
620 ) -> Result<Option<WasmExecutionEvent>, WasmExecutionError> {
621 loop {
622 if let Some(event) = self.pending_events.pop_front() {
623 return Ok(Some(event));
624 }
625 if let Some(event) = self.internal_sync_rpc.pending_events.pop_front() {
626 self.enqueue_wasm_event(event)?;
627 continue;
628 }
629 if let Some(event) = self.timeout_event_if_expired()? {
630 return Ok(Some(event));
631 }
632 let poll_timeout = self.deadline_capped_timeout(timeout);
633 match self
634 .inner
635 .poll_event(poll_timeout)
636 .await
637 .map_err(map_javascript_error)?
638 {
639 Some(event) => {
640 if let JavascriptExecutionEvent::SyncRpcRequest(request) = &event {
641 if self.handle_internal_sync_rpc(request)? {
642 continue;
643 }
644 if let Some(signal_state) = self.handle_signal_state_sync_rpc(request)? {
645 return Ok(Some(signal_state));
646 }
647 }
648 self.enqueue_javascript_event(event)?;
649 }
650 None if poll_timeout < timeout => continue,
651 None => return Ok(None),
652 }
653 }
654 }
655
656 pub fn try_poll_event(&mut self) -> Result<Option<WasmExecutionEvent>, WasmExecutionError> {
657 loop {
658 if let Some(event) = self.pending_events.pop_front() {
659 return Ok(Some(event));
660 }
661 if let Some(event) = self.internal_sync_rpc.pending_events.pop_front() {
662 self.enqueue_wasm_event(event)?;
663 continue;
664 }
665 if let Some(event) = self.timeout_event_if_expired()? {
666 return Ok(Some(event));
667 }
668 let Some(event) = self.inner.try_poll_event().map_err(map_javascript_error)? else {
669 return Ok(None);
670 };
671 if let JavascriptExecutionEvent::SyncRpcRequest(request) = &event {
672 if self.handle_internal_sync_rpc(request)? {
673 continue;
674 }
675 if let Some(signal_state) = self.handle_signal_state_sync_rpc(request)? {
676 return Ok(Some(signal_state));
677 }
678 }
679 self.enqueue_javascript_event(event)?;
680 }
681 }
682
683 pub fn poll_event_blocking(
684 &mut self,
685 timeout: Duration,
686 ) -> Result<Option<WasmExecutionEvent>, WasmExecutionError> {
687 loop {
688 if let Some(event) = self.pending_events.pop_front() {
689 return Ok(Some(event));
690 }
691 if let Some(event) = self.internal_sync_rpc.pending_events.pop_front() {
692 self.enqueue_wasm_event(event)?;
693 continue;
694 }
695 if let Some(event) = self.timeout_event_if_expired()? {
696 return Ok(Some(event));
697 }
698 let poll_timeout = self.deadline_capped_timeout(timeout);
699 match self
700 .inner
701 .poll_event_blocking(poll_timeout)
702 .map_err(map_javascript_error)?
703 {
704 Some(event) => {
705 if let JavascriptExecutionEvent::SyncRpcRequest(request) = &event {
706 if self.handle_internal_sync_rpc(request)? {
707 continue;
708 }
709 if let Some(signal_state) = self.handle_signal_state_sync_rpc(request)? {
710 return Ok(Some(signal_state));
711 }
712 }
713 self.enqueue_javascript_event(event)?;
714 }
715 None if poll_timeout < timeout => continue,
716 None => return Ok(None),
717 }
718 }
719 }
720
721 pub fn wait(mut self) -> Result<WasmExecutionResult, WasmExecutionError> {
722 self.close_stdin()?;
723 let mut stdout = Vec::new();
724 let mut stderr = Vec::new();
725
726 loop {
727 match self.wait_event_blocking()? {
728 WasmExecutionEvent::Stdout(chunk) => {
729 append_wasm_captured_output(&mut stdout, &chunk, "stdout")?;
730 }
731 WasmExecutionEvent::Stderr(chunk) => {
732 append_wasm_captured_output(&mut stderr, &chunk, "stderr")?;
733 }
734 WasmExecutionEvent::SyncRpcRequest(request) => {
735 if self.handle_wait_sync_rpc_request(&request, &mut stdout, &mut stderr)? {
736 continue;
737 }
738 return Err(WasmExecutionError::RpcResponse(format!(
739 "unexpected guest WebAssembly sync RPC request {} while waiting",
740 request.method
741 )));
742 }
743 WasmExecutionEvent::SignalState { .. } => {}
744 WasmExecutionEvent::Exited(exit_code) => {
745 return Ok(WasmExecutionResult {
746 execution_id: self.execution_id,
747 exit_code,
748 stdout,
749 stderr,
750 });
751 }
752 }
753 }
754 }
755
756 fn wait_event_blocking(&mut self) -> Result<WasmExecutionEvent, WasmExecutionError> {
760 loop {
761 if let Some(event) = self.pending_events.pop_front() {
762 return Ok(event);
763 }
764 if let Some(event) = self.internal_sync_rpc.pending_events.pop_front() {
765 self.enqueue_wasm_event(event)?;
766 continue;
767 }
768 if let Some(event) = self.timeout_event_if_expired()? {
769 return Ok(event);
770 }
771
772 let event = if let Some(limit) = self.execution_timeout {
773 let remaining = limit.saturating_sub(self.execution_started_at.elapsed());
774 if remaining.is_zero() {
775 continue;
776 }
777 let Some(event) = self
778 .inner
779 .poll_event_blocking(remaining)
780 .map_err(map_javascript_error)?
781 else {
782 continue;
785 };
786 event
787 } else {
788 self.inner
789 .next_event_blocking()
790 .map_err(map_javascript_error)?
791 };
792
793 if let JavascriptExecutionEvent::SyncRpcRequest(request) = &event {
794 if self.handle_internal_sync_rpc(request)? {
795 continue;
796 }
797 if let Some(signal_state) = self.handle_signal_state_sync_rpc(request)? {
798 return Ok(signal_state);
799 }
800 }
801 self.enqueue_javascript_event(event)?;
802 }
803 }
804
805 fn deadline_capped_timeout(&self, timeout: Duration) -> Duration {
806 self.execution_timeout
807 .map(|limit| {
808 let elapsed = self.execution_started_at.elapsed();
809 if elapsed >= limit {
810 Duration::ZERO
811 } else {
812 timeout.min(limit.saturating_sub(elapsed))
813 }
814 })
815 .unwrap_or(timeout)
816 }
817
818 fn timeout_event_if_expired(
819 &mut self,
820 ) -> Result<Option<WasmExecutionEvent>, WasmExecutionError> {
821 if self.timeout_reported {
822 return Ok(None);
823 }
824 let Some(limit) = self.execution_timeout else {
825 return Ok(None);
826 };
827 let elapsed = self.execution_started_at.elapsed();
828 if let Some(gauge) = &self.fuel_gauge {
832 gauge.observe_depth(duration_millis_saturating_usize(elapsed));
833 }
834 if elapsed < limit {
835 return Ok(None);
836 }
837
838 self.inner.terminate().map_err(map_javascript_error)?;
839 self.timeout_reported = true;
840 let capacity = duration_millis_saturating_usize(limit);
841 warn_limit_exhausted(TrackedLimit::WasmFuelMs, capacity, capacity);
842 self.enqueue_wasm_event(WasmExecutionEvent::Stderr(
843 b"WebAssembly fuel budget exhausted\n".to_vec(),
844 ))?;
845 self.enqueue_wasm_event(WasmExecutionEvent::Exited(WASM_TIMEOUT_EXIT_CODE))?;
846 Ok(self.pending_events.pop_front())
847 }
848
849 fn handle_internal_sync_rpc(
850 &mut self,
851 request: &JavascriptSyncRpcRequest,
852 ) -> Result<bool, WasmExecutionError> {
853 handle_internal_wasm_sync_rpc_request(&mut self.inner, &mut self.internal_sync_rpc, request)
854 }
855
856 fn handle_signal_state_sync_rpc(
857 &mut self,
858 request: &JavascriptSyncRpcRequest,
859 ) -> Result<Option<WasmExecutionEvent>, WasmExecutionError> {
860 translate_wasm_signal_state_sync_rpc_request(&mut self.inner, request)
861 }
862
863 fn enqueue_javascript_event(
864 &mut self,
865 event: JavascriptExecutionEvent,
866 ) -> Result<(), WasmExecutionError> {
867 match event {
868 JavascriptExecutionEvent::Stdout(chunk) => {
869 self.enqueue_stream_chunk(StreamChannel::Stdout, chunk)?
870 }
871 JavascriptExecutionEvent::Stderr(chunk) => {
872 if self.max_stack_bytes.is_some() && is_v8_stack_overflow_stderr(&chunk) {
873 let pending = self.pending_v8_stack_overflow.get_or_insert_with(Vec::new);
874 ensure_wasm_output_capacity(
875 pending.len(),
876 chunk.len(),
877 "pending stack-overflow stderr",
878 )?;
879 pending.extend_from_slice(&chunk);
880 } else {
881 self.enqueue_stream_chunk(StreamChannel::Stderr, chunk)?
882 }
883 }
884 JavascriptExecutionEvent::SyncRpcRequest(request) => {
885 self.pending_events
886 .push_back(WasmExecutionEvent::SyncRpcRequest(request));
887 }
888 JavascriptExecutionEvent::SignalState {
889 signal,
890 registration,
891 } => {
892 self.pending_events
893 .push_back(WasmExecutionEvent::SignalState {
894 signal,
895 registration: registration.into(),
896 });
897 }
898 JavascriptExecutionEvent::Exited(code) => {
899 if let Some(original) = self.pending_v8_stack_overflow.take() {
900 let chunk = if code != 0 {
901 configured_wasm_stack_limit_error(
902 self.max_stack_bytes
903 .expect("stack-overflow buffering requires a configured limit"),
904 )
905 .into_bytes()
906 } else {
907 original
908 };
909 self.enqueue_stream_chunk(StreamChannel::Stderr, chunk)?;
910 }
911 self.flush_stream_buffers();
912 self.pending_events
913 .push_back(WasmExecutionEvent::Exited(code));
914 }
915 }
916 Ok(())
917 }
918
919 fn enqueue_wasm_event(&mut self, event: WasmExecutionEvent) -> Result<(), WasmExecutionError> {
920 match event {
921 WasmExecutionEvent::Stdout(chunk) => {
922 self.enqueue_stream_chunk(StreamChannel::Stdout, chunk)?
923 }
924 WasmExecutionEvent::Stderr(chunk) => {
925 self.enqueue_stream_chunk(StreamChannel::Stderr, chunk)?
926 }
927 WasmExecutionEvent::Exited(code) => {
928 self.flush_stream_buffers();
929 self.pending_events
930 .push_back(WasmExecutionEvent::Exited(code));
931 }
932 other => self.pending_events.push_back(other),
933 }
934 Ok(())
935 }
936
937 fn enqueue_stream_chunk(
938 &mut self,
939 channel: StreamChannel,
940 chunk: Vec<u8>,
941 ) -> Result<(), WasmExecutionError> {
942 let buffer = match channel {
943 StreamChannel::Stdout => &mut self.stdout_stream_buffer,
944 StreamChannel::Stderr => &mut self.stderr_stream_buffer,
945 };
946 let stream = match channel {
947 StreamChannel::Stdout => "stdout",
948 StreamChannel::Stderr => "stderr",
949 };
950 ensure_wasm_output_capacity(buffer.len(), chunk.len(), stream)?;
951 buffer.extend_from_slice(&chunk);
952
953 let mut pending_stream_chunk = Vec::new();
954 while let Some(newline_index) = buffer.iter().position(|byte| *byte == b'\n') {
955 let line = buffer.drain(..=newline_index).collect::<Vec<_>>();
956 if let Some(signal_state) = parse_wasm_signal_state_line(&line)? {
957 if !pending_stream_chunk.is_empty() {
958 self.pending_events.push_back(match channel {
959 StreamChannel::Stdout => {
960 WasmExecutionEvent::Stdout(std::mem::take(&mut pending_stream_chunk))
961 }
962 StreamChannel::Stderr => {
963 WasmExecutionEvent::Stderr(std::mem::take(&mut pending_stream_chunk))
964 }
965 });
966 }
967 self.pending_events.push_back(signal_state);
968 continue;
969 }
970 pending_stream_chunk.extend_from_slice(&line);
971 }
972 if !pending_stream_chunk.is_empty() {
973 self.pending_events.push_back(match channel {
974 StreamChannel::Stdout => WasmExecutionEvent::Stdout(pending_stream_chunk),
975 StreamChannel::Stderr => WasmExecutionEvent::Stderr(pending_stream_chunk),
976 });
977 }
978
979 Ok(())
980 }
981
982 fn flush_stream_buffers(&mut self) {
983 if !self.stdout_stream_buffer.is_empty() {
984 self.pending_events
985 .push_back(WasmExecutionEvent::Stdout(std::mem::take(
986 &mut self.stdout_stream_buffer,
987 )));
988 }
989 if !self.stderr_stream_buffer.is_empty() {
990 self.pending_events
991 .push_back(WasmExecutionEvent::Stderr(std::mem::take(
992 &mut self.stderr_stream_buffer,
993 )));
994 }
995 }
996
997 fn handle_wait_sync_rpc_request(
998 &mut self,
999 request: &JavascriptSyncRpcRequest,
1000 stdout: &mut Vec<u8>,
1001 stderr: &mut Vec<u8>,
1002 ) -> Result<bool, WasmExecutionError> {
1003 if self
1004 .inner
1005 .handle_kernel_stdin_sync_rpc(request)
1006 .map_err(map_javascript_error)?
1007 {
1008 return Ok(true);
1009 }
1010
1011 if request.method != "__kernel_stdio_write" {
1012 return Ok(false);
1013 }
1014
1015 let Some(descriptor) = request.args.first().and_then(Value::as_u64) else {
1016 return Err(WasmExecutionError::RpcResponse(String::from(
1017 "missing __kernel_stdio_write descriptor",
1018 )));
1019 };
1020 let bytes = decode_wasm_bytes_arg(
1021 request.args.get(1),
1022 "__kernel_stdio_write payload bytes",
1023 WASM_CAPTURED_OUTPUT_LIMIT_BYTES,
1024 )?;
1025
1026 match descriptor {
1027 1 => append_wasm_captured_output(stdout, &bytes, "stdout")?,
1028 2 => append_wasm_captured_output(stderr, &bytes, "stderr")?,
1029 other => {
1030 return Err(WasmExecutionError::RpcResponse(format!(
1031 "unsupported __kernel_stdio_write descriptor {other}",
1032 )));
1033 }
1034 }
1035
1036 self.respond_sync_rpc_success(request.id, json!(bytes.len()))?;
1037 Ok(true)
1038 }
1039}
1040
1041#[derive(Clone, Copy)]
1042enum StreamChannel {
1043 Stdout,
1044 Stderr,
1045}
1046
1047#[derive(Debug)]
1048pub struct WasmExecutionEngine {
1049 runtime: Option<RuntimeContext>,
1050 next_context_id: usize,
1051 next_execution_id: usize,
1052 contexts: BTreeMap<String, WasmContext>,
1053 import_caches: BTreeMap<String, NodeImportCache>,
1054 javascript_context_ids: BTreeMap<String, String>,
1055 javascript_engine: JavascriptExecutionEngine,
1056}
1057
1058impl Default for WasmExecutionEngine {
1059 fn default() -> Self {
1060 let runtime = default_wasm_test_runtime_context();
1061 let javascript_engine = runtime
1062 .as_ref()
1063 .map_or_else(JavascriptExecutionEngine::default, |runtime| {
1064 JavascriptExecutionEngine::new(runtime.clone())
1065 });
1066 Self {
1067 runtime,
1068 next_context_id: 0,
1069 next_execution_id: 0,
1070 contexts: BTreeMap::new(),
1071 import_caches: BTreeMap::new(),
1072 javascript_context_ids: BTreeMap::new(),
1073 javascript_engine,
1074 }
1075 }
1076}
1077
1078#[cfg(test)]
1079fn default_wasm_test_runtime_context() -> Option<RuntimeContext> {
1080 agentos_runtime::SidecarRuntime::process(&agentos_runtime::RuntimeConfig::default())
1081 .ok()
1082 .map(agentos_runtime::SidecarRuntime::context)
1083}
1084
1085#[cfg(not(test))]
1086fn default_wasm_test_runtime_context() -> Option<RuntimeContext> {
1087 None
1088}
1089
1090impl WasmExecutionEngine {
1091 pub fn new(runtime: RuntimeContext) -> Self {
1092 Self {
1093 runtime: Some(runtime.clone()),
1094 next_context_id: 0,
1095 next_execution_id: 0,
1096 contexts: BTreeMap::new(),
1097 import_caches: BTreeMap::new(),
1098 javascript_context_ids: BTreeMap::new(),
1099 javascript_engine: JavascriptExecutionEngine::new(runtime),
1100 }
1101 }
1102
1103 pub fn set_runtime_context(&mut self, runtime: RuntimeContext) {
1104 self.javascript_engine.set_runtime_context(runtime.clone());
1105 self.runtime = Some(runtime);
1106 }
1107
1108 fn runtime_context(&self) -> Result<&RuntimeContext, WasmExecutionError> {
1109 self.runtime.as_ref().ok_or_else(|| {
1110 WasmExecutionError::Spawn(std::io::Error::other(
1111 "ERR_AGENTOS_RUNTIME_NOT_INJECTED: WasmExecutionEngine requires a process RuntimeContext; construct it with WasmExecutionEngine::new(runtime)",
1112 ))
1113 })
1114 }
1115
1116 pub fn set_event_notify(&mut self, notify: Option<Arc<Notify>>) {
1117 self.javascript_engine.set_event_notify(notify);
1118 }
1119
1120 pub fn create_context(&mut self, request: CreateWasmContextRequest) -> WasmContext {
1121 self.next_context_id += 1;
1122 self.import_caches.entry(request.vm_id.clone()).or_default();
1123 let javascript_context =
1124 self.javascript_engine
1125 .create_context(CreateJavascriptContextRequest {
1126 vm_id: request.vm_id.clone(),
1127 bootstrap_module: None,
1128 compile_cache_root: None,
1129 });
1130
1131 let context = WasmContext {
1132 context_id: format!("wasm-ctx-{}", self.next_context_id),
1133 vm_id: request.vm_id,
1134 module_path: request.module_path,
1135 };
1136 self.javascript_context_ids
1137 .insert(context.context_id.clone(), javascript_context.context_id);
1138 self.contexts
1139 .insert(context.context_id.clone(), context.clone());
1140 context
1141 }
1142
1143 pub fn dispose_context(&mut self, context_id: &str) -> bool {
1147 let removed = self.contexts.remove(context_id).is_some();
1148 if let Some(javascript_context_id) = self.javascript_context_ids.remove(context_id) {
1149 self.javascript_engine
1150 .dispose_context(&javascript_context_id);
1151 }
1152 removed
1153 }
1154
1155 #[doc(hidden)]
1156 pub fn context_count_for_test(&self) -> usize {
1157 self.contexts.len()
1158 }
1159
1160 #[doc(hidden)]
1161 pub fn javascript_context_count_for_test(&self) -> usize {
1162 self.javascript_engine.context_count_for_test()
1163 }
1164
1165 pub fn start_execution(
1166 &mut self,
1167 request: StartWasmExecutionRequest,
1168 ) -> Result<WasmExecution, WasmExecutionError> {
1169 let runtime = self.runtime_context()?.clone();
1170 self.create_execution_with_runtime(request, runtime, false)
1171 }
1172
1173 pub fn prepare_execution(
1174 &mut self,
1175 request: StartWasmExecutionRequest,
1176 ) -> Result<WasmExecution, WasmExecutionError> {
1177 let runtime = self.runtime_context()?.clone();
1178 self.create_execution_with_runtime(request, runtime, true)
1179 }
1180
1181 pub fn start_execution_with_runtime(
1182 &mut self,
1183 request: StartWasmExecutionRequest,
1184 runtime: RuntimeContext,
1185 ) -> Result<WasmExecution, WasmExecutionError> {
1186 self.create_execution_with_runtime(request, runtime, false)
1187 }
1188
1189 fn create_execution_with_runtime(
1190 &mut self,
1191 request: StartWasmExecutionRequest,
1192 runtime: RuntimeContext,
1193 defer_execute: bool,
1194 ) -> Result<WasmExecution, WasmExecutionError> {
1195 let context = self
1196 .contexts
1197 .get(&request.context_id)
1198 .cloned()
1199 .ok_or_else(|| WasmExecutionError::MissingContext(request.context_id.clone()))?;
1200
1201 if context.vm_id != request.vm_id {
1202 return Err(WasmExecutionError::VmMismatch {
1203 expected: context.vm_id,
1204 found: request.vm_id,
1205 });
1206 }
1207
1208 let resolved_module = resolve_wasm_module(&context, &request)?;
1209 verify_wasm_module_header(&resolved_module)?;
1210 let prewarm_timeout = resolve_wasm_prewarm_timeout(&request)?;
1211 let javascript_context_id = self
1212 .javascript_context_ids
1213 .get(&context.context_id)
1214 .cloned()
1215 .ok_or_else(|| WasmExecutionError::MissingContext(context.context_id.clone()))?;
1216 {
1217 let import_cache = self.import_caches.entry(context.vm_id.clone()).or_default();
1218 import_cache
1219 .ensure_materialized_with_timeout_and_runtime(&runtime, prewarm_timeout)
1220 .map_err(WasmExecutionError::PrepareWarmPath)?;
1221 }
1222 let frozen_time_ms = frozen_time_ms();
1223 validate_module_limits(&resolved_module, &request)?;
1224 wasm_stack_limit_bytes(&request)?;
1228 let execution_timeout = resolve_wasm_execution_timeout(&request)?;
1229 let import_cache = self
1230 .import_caches
1231 .get(&context.vm_id)
1232 .expect("vm import cache should exist after materialization");
1233 let warmup_metrics = match prewarm_wasm_path(
1234 import_cache,
1235 &mut self.javascript_engine,
1236 &javascript_context_id,
1237 &resolved_module,
1238 &request,
1239 WasmPrewarmOptions {
1240 frozen_time_ms,
1241 timeout: prewarm_timeout,
1242 runtime: &runtime,
1243 },
1244 ) {
1245 Ok(metrics) => metrics,
1246 Err(WasmExecutionError::WarmupTimeout(_)) => None,
1247 Err(error) => return Err(error),
1248 };
1249
1250 self.finish_start_execution(
1251 request,
1252 runtime,
1253 &context.vm_id,
1254 javascript_context_id,
1255 resolved_module,
1256 frozen_time_ms,
1257 execution_timeout,
1258 warmup_metrics,
1259 defer_execute,
1260 )
1261 }
1262
1263 pub async fn start_execution_with_runtime_async(
1267 &mut self,
1268 request: StartWasmExecutionRequest,
1269 runtime: RuntimeContext,
1270 ) -> Result<WasmExecution, WasmExecutionError> {
1271 let context = self
1272 .contexts
1273 .get(&request.context_id)
1274 .cloned()
1275 .ok_or_else(|| WasmExecutionError::MissingContext(request.context_id.clone()))?;
1276
1277 if context.vm_id != request.vm_id {
1278 return Err(WasmExecutionError::VmMismatch {
1279 expected: context.vm_id,
1280 found: request.vm_id,
1281 });
1282 }
1283
1284 let resolved_module = resolve_wasm_module(&context, &request)?;
1285 verify_wasm_module_header(&resolved_module)?;
1286 let prewarm_timeout = resolve_wasm_prewarm_timeout(&request)?;
1287 let javascript_context_id = self
1288 .javascript_context_ids
1289 .get(&context.context_id)
1290 .cloned()
1291 .ok_or_else(|| WasmExecutionError::MissingContext(context.context_id.clone()))?;
1292 {
1293 let import_cache = self.import_caches.entry(context.vm_id.clone()).or_default();
1294 import_cache
1295 .ensure_materialized_with_timeout_and_runtime_async(&runtime, prewarm_timeout)
1296 .await
1297 .map_err(WasmExecutionError::PrepareWarmPath)?;
1298 }
1299 let frozen_time_ms = frozen_time_ms();
1300 validate_module_limits(&resolved_module, &request)?;
1301 wasm_stack_limit_bytes(&request)?;
1302 let execution_timeout = resolve_wasm_execution_timeout(&request)?;
1303 let import_cache = self
1304 .import_caches
1305 .get(&context.vm_id)
1306 .expect("vm import cache should exist after materialization");
1307 let warmup_metrics = match prewarm_wasm_path_async(
1308 import_cache,
1309 &mut self.javascript_engine,
1310 &javascript_context_id,
1311 &resolved_module,
1312 &request,
1313 WasmPrewarmOptions {
1314 frozen_time_ms,
1315 timeout: prewarm_timeout,
1316 runtime: &runtime,
1317 },
1318 )
1319 .await
1320 {
1321 Ok(metrics) => metrics,
1322 Err(WasmExecutionError::WarmupTimeout(_)) => None,
1323 Err(error) => return Err(error),
1324 };
1325
1326 self.finish_start_execution(
1327 request,
1328 runtime,
1329 &context.vm_id,
1330 javascript_context_id,
1331 resolved_module,
1332 frozen_time_ms,
1333 execution_timeout,
1334 warmup_metrics,
1335 false,
1336 )
1337 }
1338
1339 #[allow(clippy::too_many_arguments)]
1340 fn finish_start_execution(
1341 &mut self,
1342 request: StartWasmExecutionRequest,
1343 runtime: RuntimeContext,
1344 vm_id: &str,
1345 javascript_context_id: String,
1346 resolved_module: ResolvedWasmModule,
1347 frozen_time_ms: u128,
1348 execution_timeout: Option<Duration>,
1349 warmup_metrics: Option<Vec<u8>>,
1350 defer_execute: bool,
1351 ) -> Result<WasmExecution, WasmExecutionError> {
1352 let import_cache = self
1353 .import_caches
1354 .get(vm_id)
1355 .expect("vm import cache should exist after materialization");
1356 self.next_execution_id += 1;
1357 let execution_id = format!("exec-{}", self.next_execution_id);
1358 let javascript_execution = start_wasm_javascript_execution(
1359 &mut self.javascript_engine,
1360 &runtime,
1361 import_cache,
1362 &javascript_context_id,
1363 &resolved_module,
1364 &request,
1365 WasmJavascriptExecutionOptions {
1366 frozen_time_ms,
1367 prewarm_only: false,
1368 warmup_metrics: warmup_metrics.as_deref(),
1369 defer_execute,
1370 },
1371 )?;
1372 let child_pid = javascript_execution.child_pid();
1373 let sandbox_root = wasm_sandbox_root(&request.env);
1374 let guest_path_mappings = wasm_guest_path_mappings(&request);
1375
1376 Ok(WasmExecution {
1377 execution_id,
1378 child_pid,
1379 inner: javascript_execution,
1380 execution_timeout,
1381 execution_started_at: Instant::now(),
1382 timeout_reported: false,
1383 fuel_gauge: execution_timeout.map(|limit| {
1386 register_limit(
1387 TrackedLimit::WasmFuelMs,
1388 duration_millis_saturating_usize(limit),
1389 )
1390 }),
1391 pending_events: VecDeque::new(),
1392 stdout_stream_buffer: Vec::new(),
1393 stderr_stream_buffer: Vec::new(),
1394 max_stack_bytes: request.limits.max_stack_bytes,
1395 pending_v8_stack_overflow: None,
1396 internal_sync_rpc: WasmInternalSyncRpc {
1397 module_guest_paths: wasm_guest_module_paths(
1398 &resolved_module.specifier,
1399 &request.env,
1400 ),
1401 module_host_path: resolved_module.resolved_path.clone(),
1402 guest_cwd: wasm_guest_cwd(&request.env),
1403 host_cwd: request.cwd.clone(),
1404 sandbox_root: sandbox_root.clone(),
1405 guest_path_mappings,
1406 route_fs_through_sidecar: sandbox_root.is_some(),
1407 next_fd: 64,
1408 open_files: BTreeMap::new(),
1409 pending_events: VecDeque::new(),
1410 },
1411 })
1412 }
1413
1414 pub fn dispose_vm(&mut self, vm_id: &str) {
1415 self.contexts.retain(|_, context| context.vm_id != vm_id);
1416 self.javascript_context_ids
1417 .retain(|wasm_context_id, _| self.contexts.contains_key(wasm_context_id));
1418 self.import_caches.remove(vm_id);
1419 self.javascript_engine.dispose_vm(vm_id);
1420 }
1421}
1422
1423fn map_javascript_error(error: JavascriptExecutionError) -> WasmExecutionError {
1424 match error {
1425 JavascriptExecutionError::EmptyArgv => WasmExecutionError::Spawn(std::io::Error::new(
1426 std::io::ErrorKind::InvalidInput,
1427 "guest WebAssembly bootstrap requires a JavaScript entrypoint",
1428 )),
1429 JavascriptExecutionError::InvalidLimit(message) => {
1430 WasmExecutionError::InvalidLimit(message)
1431 }
1432 JavascriptExecutionError::MissingContext(context_id) => {
1433 WasmExecutionError::MissingContext(context_id)
1434 }
1435 JavascriptExecutionError::VmMismatch { expected, found } => {
1436 WasmExecutionError::VmMismatch { expected, found }
1437 }
1438 JavascriptExecutionError::PrepareImportCache(error) => {
1439 WasmExecutionError::PrepareWarmPath(error)
1440 }
1441 JavascriptExecutionError::Spawn(error) => WasmExecutionError::Spawn(error),
1442 JavascriptExecutionError::PendingSyncRpcRequest(id) => WasmExecutionError::RpcResponse(
1443 format!("guest WebAssembly sync RPC request {id} is still pending"),
1444 ),
1445 JavascriptExecutionError::ExpiredSyncRpcRequest(id) => WasmExecutionError::RpcResponse(
1446 format!("guest WebAssembly sync RPC request {id} is no longer pending"),
1447 ),
1448 JavascriptExecutionError::RpcResponse(message) => WasmExecutionError::RpcResponse(message),
1449 JavascriptExecutionError::Terminate(error) => WasmExecutionError::Spawn(error),
1450 JavascriptExecutionError::Control(error) => WasmExecutionError::Control(error),
1451 JavascriptExecutionError::StdinClosed => WasmExecutionError::StdinClosed,
1452 JavascriptExecutionError::Stdin(error) => WasmExecutionError::Stdin(error),
1453 JavascriptExecutionError::OutputBufferExceeded { stream, limit } => {
1454 WasmExecutionError::OutputBufferExceeded { stream, limit }
1455 }
1456 JavascriptExecutionError::EventChannelClosed => WasmExecutionError::EventChannelClosed,
1457 }
1458}
1459
1460fn handle_internal_wasm_sync_rpc_request(
1461 execution: &mut JavascriptExecution,
1462 internal_sync_rpc: &mut WasmInternalSyncRpc,
1463 request: &JavascriptSyncRpcRequest,
1464) -> Result<bool, WasmExecutionError> {
1465 if execution
1469 .try_service_standalone_module_sync_rpc(request)
1470 .map_err(map_javascript_error)?
1471 {
1472 return Ok(true);
1473 }
1474
1475 if matches!(
1476 request.method.as_str(),
1477 "fs.promises.readFile" | "fs.readFileSync"
1478 ) && request
1479 .args
1480 .first()
1481 .and_then(Value::as_str)
1482 .is_some_and(|path| {
1483 internal_sync_rpc
1484 .module_guest_paths
1485 .iter()
1486 .any(|candidate| candidate == path)
1487 })
1488 {
1489 let module_bytes =
1490 fs::read(&internal_sync_rpc.module_host_path).map_err(WasmExecutionError::Spawn)?;
1491 execution
1492 .respond_sync_rpc_success(
1493 request.id,
1494 Value::String(v8_runtime::base64_encode_pub(&module_bytes)),
1495 )
1496 .map_err(map_javascript_error)?;
1497 return Ok(true);
1498 }
1499
1500 if wasm_sync_rpc_method_routes_through_sidecar_kernel(request, internal_sync_rpc) {
1501 return Ok(false);
1502 }
1503
1504 if request.method == "__kernel_isatty" {
1505 execution
1506 .respond_sync_rpc_success(request.id, Value::Bool(false))
1507 .map_err(map_javascript_error)?;
1508 return Ok(true);
1509 }
1510
1511 if request.method == "__kernel_tty_size" {
1512 execution
1513 .respond_sync_rpc_success(request.id, json!([80, 24]))
1514 .map_err(map_javascript_error)?;
1515 return Ok(true);
1516 }
1517
1518 if request.method == "fs.openSync" {
1519 let Some(path) = request.args.first().and_then(Value::as_str) else {
1520 return Err(WasmExecutionError::RpcResponse(String::from(
1521 "missing fs.openSync path",
1522 )));
1523 };
1524 let Some(host_path) = translate_wasm_guest_path(path, internal_sync_rpc) else {
1525 return Ok(false);
1526 };
1527 let flags = request.args.get(1).unwrap_or(&Value::Null);
1528 if wasm_open_flags_require_write(flags)
1529 && wasm_host_path_is_read_only(&host_path, internal_sync_rpc)
1530 {
1531 return respond_wasm_sync_rpc_value(
1532 execution,
1533 request,
1534 path,
1535 Err(wasm_read_only_filesystem_error(path)),
1536 )
1537 .map(|()| true);
1538 }
1539 let file = match open_wasm_guest_file(&host_path, flags) {
1540 Ok(file) => file,
1541 Err(error) => {
1542 return respond_wasm_sync_rpc_value(execution, request, path, Err(error))
1543 .map(|()| true);
1544 }
1545 };
1546 let fd = internal_sync_rpc.next_fd;
1547 internal_sync_rpc.next_fd += 1;
1548 internal_sync_rpc.open_files.insert(fd, file);
1549 execution
1550 .respond_sync_rpc_success(request.id, json!(fd))
1551 .map_err(map_javascript_error)?;
1552 return Ok(true);
1553 }
1554
1555 if matches!(request.method.as_str(), "fs.statSync" | "fs.lstatSync") {
1556 let Some(path) = request.args.first().and_then(Value::as_str) else {
1557 return Err(WasmExecutionError::RpcResponse(format!(
1558 "missing {} path",
1559 request.method
1560 )));
1561 };
1562 let Some(host_path) = translate_wasm_guest_path(path, internal_sync_rpc) else {
1563 return Ok(false);
1564 };
1565 let metadata = if request.method == "fs.lstatSync" {
1566 fs::symlink_metadata(&host_path)
1567 } else {
1568 fs::metadata(&host_path)
1569 };
1570 return respond_wasm_sync_rpc_metadata(execution, request, path, metadata).map(|()| true);
1571 }
1572
1573 if request.method == "fs.fstatSync" {
1574 let Some(fd) = request.args.first().and_then(Value::as_u64) else {
1575 return Err(WasmExecutionError::RpcResponse(String::from(
1576 "missing fs.fstatSync fd",
1577 )));
1578 };
1579 let Some(file) = internal_sync_rpc.open_files.get(&(fd as u32)) else {
1580 return Ok(false);
1581 };
1582 return respond_wasm_sync_rpc_metadata(
1583 execution,
1584 request,
1585 &fd.to_string(),
1586 file.metadata(),
1587 )
1588 .map(|()| true);
1589 }
1590
1591 if request.method == "fs.ftruncateSync" {
1592 let Some(fd) = request.args.first().and_then(Value::as_u64) else {
1593 return Err(WasmExecutionError::RpcResponse(String::from(
1594 "missing fs.ftruncateSync fd",
1595 )));
1596 };
1597 let length = request.args.get(1).and_then(Value::as_u64).unwrap_or(0);
1598 let Some(file) = internal_sync_rpc.open_files.get_mut(&(fd as u32)) else {
1599 return Ok(false);
1600 };
1601 let result = file.set_len(length);
1602 return respond_wasm_sync_rpc_unit(execution, request, &fd.to_string(), result)
1603 .map(|()| true);
1604 }
1605
1606 if request.method == "fs.closeSync" {
1607 let Some(fd) = request.args.first().and_then(Value::as_u64) else {
1608 return Err(WasmExecutionError::RpcResponse(String::from(
1609 "missing fs.closeSync fd",
1610 )));
1611 };
1612 if internal_sync_rpc.open_files.remove(&(fd as u32)).is_none() {
1613 return Ok(false);
1614 }
1615 execution
1616 .respond_sync_rpc_success(request.id, Value::Null)
1617 .map_err(map_javascript_error)?;
1618 return Ok(true);
1619 }
1620
1621 if request.method == "fs.chmodSync" {
1622 let Some(path) = request.args.first().and_then(Value::as_str) else {
1623 return Err(WasmExecutionError::RpcResponse(String::from(
1624 "missing fs.chmodSync path",
1625 )));
1626 };
1627 let Some(host_path) = translate_wasm_guest_path(path, internal_sync_rpc) else {
1628 return Ok(false);
1629 };
1630 if wasm_host_path_is_read_only(&host_path, internal_sync_rpc) {
1631 return respond_wasm_sync_rpc_unit(
1632 execution,
1633 request,
1634 path,
1635 Err(wasm_read_only_filesystem_error(path)),
1636 )
1637 .map(|()| true);
1638 }
1639 let mode = request.args.get(1).and_then(Value::as_u64).unwrap_or(0) as u32;
1640 let result = (|| -> Result<(), std::io::Error> {
1641 let mut permissions = fs::metadata(&host_path)?.permissions();
1642 permissions.set_mode(mode);
1643 fs::set_permissions(&host_path, permissions)
1644 })();
1645 return respond_wasm_sync_rpc_unit(execution, request, path, result).map(|()| true);
1646 }
1647
1648 if request.method == "fs.mkdirSync" {
1649 let Some(path) = request.args.first().and_then(Value::as_str) else {
1650 return Err(WasmExecutionError::RpcResponse(String::from(
1651 "missing fs.mkdirSync path",
1652 )));
1653 };
1654 let Some(host_path) = translate_wasm_guest_path(path, internal_sync_rpc) else {
1655 return Ok(false);
1656 };
1657 if wasm_host_path_is_read_only(&host_path, internal_sync_rpc) {
1658 return respond_wasm_sync_rpc_unit(
1659 execution,
1660 request,
1661 path,
1662 Err(wasm_read_only_filesystem_error(path)),
1663 )
1664 .map(|()| true);
1665 }
1666 let recursive = request
1667 .args
1668 .get(1)
1669 .map(|value| match value {
1670 Value::Bool(flag) => *flag,
1671 Value::Object(options) => options
1672 .get("recursive")
1673 .and_then(Value::as_bool)
1674 .unwrap_or(false),
1675 _ => false,
1676 })
1677 .unwrap_or(false);
1678 let result = if recursive {
1679 fs::create_dir_all(&host_path)
1680 } else {
1681 fs::create_dir(&host_path)
1682 };
1683 return respond_wasm_sync_rpc_unit(execution, request, path, result).map(|()| true);
1684 }
1685
1686 if request.method == "fs.rmdirSync" {
1687 let Some(path) = request.args.first().and_then(Value::as_str) else {
1688 return Err(WasmExecutionError::RpcResponse(String::from(
1689 "missing fs.rmdirSync path",
1690 )));
1691 };
1692 let Some(host_path) = translate_wasm_guest_path(path, internal_sync_rpc) else {
1693 return Ok(false);
1694 };
1695 if wasm_host_path_is_read_only(&host_path, internal_sync_rpc) {
1696 return respond_wasm_sync_rpc_unit(
1697 execution,
1698 request,
1699 path,
1700 Err(wasm_read_only_filesystem_error(path)),
1701 )
1702 .map(|()| true);
1703 }
1704 return respond_wasm_sync_rpc_unit(execution, request, path, fs::remove_dir(&host_path))
1705 .map(|()| true);
1706 }
1707
1708 if request.method == "fs.unlinkSync" {
1709 let Some(path) = request.args.first().and_then(Value::as_str) else {
1710 return Err(WasmExecutionError::RpcResponse(String::from(
1711 "missing fs.unlinkSync path",
1712 )));
1713 };
1714 let Some(host_path) = translate_wasm_guest_path(path, internal_sync_rpc) else {
1715 return Ok(false);
1716 };
1717 if wasm_host_path_is_read_only(&host_path, internal_sync_rpc) {
1718 return respond_wasm_sync_rpc_unit(
1719 execution,
1720 request,
1721 path,
1722 Err(wasm_read_only_filesystem_error(path)),
1723 )
1724 .map(|()| true);
1725 }
1726 return respond_wasm_sync_rpc_unit(execution, request, path, fs::remove_file(&host_path))
1727 .map(|()| true);
1728 }
1729
1730 if request.method == "fs.renameSync" {
1731 let Some(source) = request.args.first().and_then(Value::as_str) else {
1732 return Err(WasmExecutionError::RpcResponse(String::from(
1733 "missing fs.renameSync source",
1734 )));
1735 };
1736 let Some(destination) = request.args.get(1).and_then(Value::as_str) else {
1737 return Err(WasmExecutionError::RpcResponse(String::from(
1738 "missing fs.renameSync destination",
1739 )));
1740 };
1741 let Some(host_source) = translate_wasm_guest_path(source, internal_sync_rpc) else {
1742 return Ok(false);
1743 };
1744 let Some(host_destination) = translate_wasm_guest_path(destination, internal_sync_rpc)
1745 else {
1746 return Ok(false);
1747 };
1748 if wasm_mutation_touches_read_only_mapping(
1749 &host_source,
1750 &host_destination,
1751 internal_sync_rpc,
1752 ) {
1753 return respond_wasm_sync_rpc_unit(
1754 execution,
1755 request,
1756 source,
1757 Err(wasm_read_only_filesystem_error(source)),
1758 )
1759 .map(|()| true);
1760 }
1761 return respond_wasm_sync_rpc_unit(
1762 execution,
1763 request,
1764 source,
1765 fs::rename(&host_source, &host_destination),
1766 )
1767 .map(|()| true);
1768 }
1769
1770 if request.method == "fs.linkSync" {
1771 let Some(source) = request.args.first().and_then(Value::as_str) else {
1772 return Err(WasmExecutionError::RpcResponse(String::from(
1773 "missing fs.linkSync source",
1774 )));
1775 };
1776 let Some(destination) = request.args.get(1).and_then(Value::as_str) else {
1777 return Err(WasmExecutionError::RpcResponse(String::from(
1778 "missing fs.linkSync destination",
1779 )));
1780 };
1781 let Some(host_source) = translate_wasm_guest_path(source, internal_sync_rpc) else {
1782 return Ok(false);
1783 };
1784 let Some(host_destination) = translate_wasm_guest_path(destination, internal_sync_rpc)
1785 else {
1786 return Ok(false);
1787 };
1788 if wasm_host_path_is_read_only(&host_source, internal_sync_rpc)
1789 || wasm_host_path_is_read_only(&host_destination, internal_sync_rpc)
1790 {
1791 return respond_wasm_sync_rpc_unit(
1792 execution,
1793 request,
1794 source,
1795 Err(wasm_read_only_filesystem_error(source)),
1796 )
1797 .map(|()| true);
1798 }
1799 return respond_wasm_sync_rpc_unit(
1800 execution,
1801 request,
1802 source,
1803 fs::hard_link(&host_source, &host_destination),
1804 )
1805 .map(|()| true);
1806 }
1807
1808 if request.method == "fs.symlinkSync" {
1809 let Some(target) = request.args.first().and_then(Value::as_str) else {
1810 return Err(WasmExecutionError::RpcResponse(String::from(
1811 "missing fs.symlinkSync target",
1812 )));
1813 };
1814 let Some(link_path) = request.args.get(1).and_then(Value::as_str) else {
1815 return Err(WasmExecutionError::RpcResponse(String::from(
1816 "missing fs.symlinkSync path",
1817 )));
1818 };
1819 let target_path = if target.starts_with('/') {
1820 let Some(path) = translate_wasm_guest_path(target, internal_sync_rpc) else {
1821 return Ok(false);
1822 };
1823 path
1824 } else {
1825 PathBuf::from(target)
1826 };
1827 let Some(host_link_path) = translate_wasm_guest_path(link_path, internal_sync_rpc) else {
1828 return Ok(false);
1829 };
1830 if wasm_host_path_is_read_only(&host_link_path, internal_sync_rpc) {
1831 return respond_wasm_sync_rpc_unit(
1832 execution,
1833 request,
1834 link_path,
1835 Err(wasm_read_only_filesystem_error(link_path)),
1836 )
1837 .map(|()| true);
1838 }
1839 return respond_wasm_sync_rpc_unit(
1840 execution,
1841 request,
1842 link_path,
1843 std::os::unix::fs::symlink(&target_path, &host_link_path),
1844 )
1845 .map(|()| true);
1846 }
1847
1848 if request.method == "fs.readdirSync" {
1849 let Some(path) = request.args.first().and_then(Value::as_str) else {
1850 return Err(WasmExecutionError::RpcResponse(String::from(
1851 "missing fs.readdirSync path",
1852 )));
1853 };
1854 let Some(host_path) = translate_wasm_guest_path(path, internal_sync_rpc) else {
1855 return Ok(false);
1856 };
1857 let entries = fs::read_dir(&host_path)
1858 .and_then(|entries| {
1859 entries
1860 .map(|entry| {
1861 entry.map(|value| value.file_name().to_string_lossy().into_owned())
1862 })
1863 .collect::<Result<Vec<_>, _>>()
1864 })
1865 .map(|entries| json!(entries));
1866 return respond_wasm_sync_rpc_value(execution, request, path, entries).map(|()| true);
1867 }
1868
1869 if request.method == "fs.readlinkSync" {
1870 let Some(path) = request.args.first().and_then(Value::as_str) else {
1871 return Err(WasmExecutionError::RpcResponse(String::from(
1872 "missing fs.readlinkSync path",
1873 )));
1874 };
1875 let Some(host_path) = translate_wasm_guest_path(path, internal_sync_rpc) else {
1876 return Ok(false);
1877 };
1878 let target = fs::read_link(&host_path).map(|target| {
1879 Value::String(
1880 translate_wasm_host_symlink_target(&target, internal_sync_rpc)
1881 .unwrap_or_else(|| target.to_string_lossy().into_owned()),
1882 )
1883 });
1884 return respond_wasm_sync_rpc_value(execution, request, path, target).map(|()| true);
1885 }
1886
1887 if request.method == "fs.writeSync" {
1888 let Some(fd) = request.args.first().and_then(Value::as_u64) else {
1889 return Err(WasmExecutionError::RpcResponse(String::from(
1890 "missing fs.writeSync fd",
1891 )));
1892 };
1893 let bytes = decode_wasm_bytes_arg(
1894 request.args.get(1),
1895 "fs.writeSync bytes",
1896 WASM_CAPTURED_OUTPUT_LIMIT_BYTES,
1897 )?;
1898 if fd == 1 || fd == 2 {
1899 let bytes_len = bytes.len();
1900 internal_sync_rpc.pending_events.push_back(if fd == 1 {
1901 WasmExecutionEvent::Stdout(bytes)
1902 } else {
1903 WasmExecutionEvent::Stderr(bytes)
1904 });
1905 execution
1906 .respond_sync_rpc_success(request.id, json!(bytes_len))
1907 .map_err(map_javascript_error)?;
1908 return Ok(true);
1909 }
1910 let position = request.args.get(2).and_then(Value::as_u64);
1911 let Some(file) = internal_sync_rpc.open_files.get_mut(&(fd as u32)) else {
1912 return Ok(false);
1913 };
1914 let written = if let Some(position) = position {
1915 file.write_at(&bytes, position)
1916 .map_err(WasmExecutionError::Spawn)?
1917 } else {
1918 file.write(&bytes).map_err(WasmExecutionError::Spawn)?
1919 };
1920 execution
1921 .respond_sync_rpc_success(request.id, json!(written))
1922 .map_err(map_javascript_error)?;
1923 return Ok(true);
1924 }
1925
1926 if request.method == "fs.readSync" {
1927 let Some(fd) = request.args.first().and_then(Value::as_u64) else {
1928 return Err(WasmExecutionError::RpcResponse(String::from(
1929 "missing fs.readSync fd",
1930 )));
1931 };
1932 let length = wasm_sync_read_length(request.args.get(1).and_then(Value::as_u64))?;
1933 let position = request.args.get(2).and_then(Value::as_u64);
1934 let Some(file) = internal_sync_rpc.open_files.get_mut(&(fd as u32)) else {
1935 return Ok(false);
1936 };
1937 let mut buffer = vec![0u8; length];
1938 let bytes_read = if let Some(position) = position {
1939 file.read_at(&mut buffer, position)
1940 .map_err(WasmExecutionError::Spawn)?
1941 } else {
1942 file.read(&mut buffer).map_err(WasmExecutionError::Spawn)?
1943 };
1944 buffer.truncate(bytes_read);
1945 execution
1946 .respond_sync_rpc_success(
1947 request.id,
1948 json!({
1949 "__agentOSType": "bytes",
1950 "base64": v8_runtime::base64_encode_pub(&buffer),
1951 }),
1952 )
1953 .map_err(map_javascript_error)?;
1954 return Ok(true);
1955 }
1956
1957 Ok(false)
1958}
1959
1960fn wasm_sync_rpc_method_routes_through_sidecar_kernel(
1961 request: &JavascriptSyncRpcRequest,
1962 internal_sync_rpc: &WasmInternalSyncRpc,
1963) -> bool {
1964 internal_sync_rpc.route_fs_through_sidecar
1965 && (WASM_SIDECAR_ROUTED_FS_SYNC_METHODS.contains(&request.method.as_str())
1966 || WASM_SIDECAR_ROUTED_KERNEL_SYNC_METHODS.contains(&request.method.as_str()))
1967}
1968
1969fn translate_wasm_guest_path(
1970 path: &str,
1971 internal_sync_rpc: &WasmInternalSyncRpc,
1972) -> Option<PathBuf> {
1973 if let Some(host_path) = translate_wasm_host_runtime_path(path, internal_sync_rpc) {
1974 return confine_wasm_host_path(host_path, internal_sync_rpc);
1975 }
1976
1977 let normalized_path = if path.starts_with('/') {
1978 normalize_guest_path(path)
1979 } else {
1980 join_guest_path(&internal_sync_rpc.guest_cwd, path)
1981 };
1982
1983 if normalized_path == internal_sync_rpc.module_host_path.to_string_lossy() {
1984 return Some(internal_sync_rpc.module_host_path.clone());
1985 }
1986 if internal_sync_rpc
1987 .module_guest_paths
1988 .iter()
1989 .any(|candidate| candidate == &normalized_path)
1990 {
1991 return Some(internal_sync_rpc.module_host_path.clone());
1992 }
1993 for mapping in &internal_sync_rpc.guest_path_mappings {
1994 if let Some(suffix) = strip_guest_prefix(&normalized_path, &mapping.guest_path) {
1995 return confine_wasm_host_path(
1996 join_host_path(&mapping.host_path, &suffix),
1997 internal_sync_rpc,
1998 );
1999 }
2000 }
2001 if let Some(suffix) = strip_guest_prefix(&normalized_path, &internal_sync_rpc.guest_cwd) {
2002 return confine_wasm_host_path(
2003 join_host_path(&internal_sync_rpc.host_cwd, &suffix),
2004 internal_sync_rpc,
2005 );
2006 }
2007 if normalized_path.starts_with('/') {
2008 let root_candidate = internal_sync_rpc
2009 .sandbox_root
2010 .as_ref()
2011 .map(|root| join_host_path(root, normalized_path.trim_start_matches('/')));
2012 if let Some(candidate) = root_candidate.as_ref() {
2013 if candidate.exists() {
2014 return confine_wasm_host_path(candidate.clone(), internal_sync_rpc);
2015 }
2016 }
2017
2018 if internal_sync_rpc.guest_cwd != "/" {
2022 let cwd_relative_guest_path = join_guest_path(
2023 &internal_sync_rpc.guest_cwd,
2024 normalized_path.trim_start_matches('/'),
2025 );
2026 for mapping in &internal_sync_rpc.guest_path_mappings {
2027 if let Some(suffix) =
2028 strip_guest_prefix(&cwd_relative_guest_path, &mapping.guest_path)
2029 {
2030 let candidate = join_host_path(&mapping.host_path, &suffix);
2031 if candidate.exists() {
2032 return confine_wasm_host_path(candidate, internal_sync_rpc);
2033 }
2034 }
2035 }
2036 if let Some(suffix) =
2037 strip_guest_prefix(&cwd_relative_guest_path, &internal_sync_rpc.guest_cwd)
2038 {
2039 let candidate = join_host_path(&internal_sync_rpc.host_cwd, &suffix);
2040 if candidate.exists() {
2041 return confine_wasm_host_path(candidate, internal_sync_rpc);
2042 }
2043 }
2044 }
2045
2046 return root_candidate.and_then(|path| confine_wasm_host_path(path, internal_sync_rpc));
2047 }
2048 None
2049}
2050
2051fn confine_wasm_host_path(
2052 host_path: PathBuf,
2053 internal_sync_rpc: &WasmInternalSyncRpc,
2054) -> Option<PathBuf> {
2055 if host_path == internal_sync_rpc.module_host_path {
2056 return Some(host_path);
2057 }
2058
2059 let allowed_roots = wasm_allowed_host_roots(internal_sync_rpc);
2060 if allowed_roots.is_empty() {
2061 return None;
2062 }
2063
2064 if let Ok(canonical_path) = fs::canonicalize(&host_path) {
2065 return wasm_canonical_path_is_allowed(&canonical_path, &allowed_roots)
2066 .then_some(host_path);
2067 }
2068
2069 let existing_ancestor = nearest_existing_wasm_host_ancestor(&host_path)?;
2070 let canonical_ancestor = fs::canonicalize(existing_ancestor).ok()?;
2071 wasm_canonical_path_is_allowed(&canonical_ancestor, &allowed_roots).then_some(host_path)
2072}
2073
2074fn wasm_allowed_host_roots(internal_sync_rpc: &WasmInternalSyncRpc) -> Vec<PathBuf> {
2075 let mut roots = Vec::new();
2076 for root in internal_sync_rpc
2077 .guest_path_mappings
2078 .iter()
2079 .map(|mapping| mapping.host_path.as_path())
2080 .chain(std::iter::once(internal_sync_rpc.host_cwd.as_path()))
2081 .chain(internal_sync_rpc.sandbox_root.as_deref())
2082 {
2083 if let Ok(canonical_root) = fs::canonicalize(root) {
2084 if !roots.iter().any(|existing| existing == &canonical_root) {
2085 roots.push(canonical_root);
2086 }
2087 }
2088 }
2089 roots
2090}
2091
2092fn wasm_canonical_path_is_allowed(path: &Path, allowed_roots: &[PathBuf]) -> bool {
2093 allowed_roots
2094 .iter()
2095 .any(|root| path == root || path.starts_with(root))
2096}
2097
2098fn nearest_existing_wasm_host_ancestor(path: &Path) -> Option<&Path> {
2099 let mut candidate = Some(path);
2100 while let Some(current) = candidate {
2101 if fs::symlink_metadata(current).is_ok() {
2102 return Some(current);
2103 }
2104 candidate = current.parent();
2105 }
2106 None
2107}
2108
2109fn translate_wasm_host_runtime_path(
2110 path: &str,
2111 internal_sync_rpc: &WasmInternalSyncRpc,
2112) -> Option<PathBuf> {
2113 let candidate = Path::new(path);
2114 if !candidate.is_absolute() {
2115 return None;
2116 }
2117
2118 if candidate == internal_sync_rpc.module_host_path {
2119 return Some(candidate.to_path_buf());
2120 }
2121
2122 let mapped_host_root = internal_sync_rpc
2123 .guest_path_mappings
2124 .iter()
2125 .map(|mapping| mapping.host_path.as_path())
2126 .find(|root| candidate == *root || candidate.starts_with(root));
2127 if let Some(root) = mapped_host_root {
2128 let _ = root;
2129 return Some(candidate.to_path_buf());
2130 }
2131
2132 if candidate == internal_sync_rpc.host_cwd || candidate.starts_with(&internal_sync_rpc.host_cwd)
2133 {
2134 return Some(candidate.to_path_buf());
2135 }
2136
2137 if let Some(sandbox_root) = internal_sync_rpc.sandbox_root.as_ref() {
2138 if candidate == sandbox_root || candidate.starts_with(sandbox_root) {
2139 return Some(candidate.to_path_buf());
2140 }
2141 }
2142
2143 None
2144}
2145
2146fn translate_wasm_host_symlink_target(
2147 target: &Path,
2148 internal_sync_rpc: &WasmInternalSyncRpc,
2149) -> Option<String> {
2150 if !target.is_absolute() {
2151 return None;
2152 }
2153
2154 for mapping in &internal_sync_rpc.guest_path_mappings {
2155 if let Ok(suffix) = target.strip_prefix(&mapping.host_path) {
2156 return Some(join_guest_path(
2157 &mapping.guest_path,
2158 &suffix.to_string_lossy().replace('\\', "/"),
2159 ));
2160 }
2161 }
2162
2163 if let Some(suffix) = target
2164 .strip_prefix(&internal_sync_rpc.host_cwd)
2165 .ok()
2166 .filter(|_| internal_sync_rpc.guest_cwd.starts_with('/'))
2167 {
2168 return Some(join_guest_path(
2169 &internal_sync_rpc.guest_cwd,
2170 &suffix.to_string_lossy().replace('\\', "/"),
2171 ));
2172 }
2173
2174 if let Some(sandbox_root) = internal_sync_rpc.sandbox_root.as_ref() {
2175 if let Ok(suffix) = target.strip_prefix(sandbox_root) {
2176 return Some(join_guest_path(
2177 "/",
2178 &suffix.to_string_lossy().replace('\\', "/"),
2179 ));
2180 }
2181 }
2182
2183 None
2184}
2185
2186fn wasm_host_path_is_read_only(host_path: &Path, internal_sync_rpc: &WasmInternalSyncRpc) -> bool {
2187 let canonical_path = fs::canonicalize(host_path)
2188 .ok()
2189 .or_else(|| {
2190 nearest_existing_wasm_host_ancestor(host_path)
2191 .and_then(|ancestor| fs::canonicalize(ancestor).ok())
2192 })
2193 .unwrap_or_else(|| host_path.to_path_buf());
2194
2195 internal_sync_rpc
2196 .guest_path_mappings
2197 .iter()
2198 .filter_map(|mapping| {
2199 let root = fs::canonicalize(&mapping.host_path).ok()?;
2200 (canonical_path == root || canonical_path.starts_with(&root))
2201 .then_some((root.components().count(), mapping.read_only))
2202 })
2203 .max_by_key(|(depth, _)| *depth)
2204 .is_some_and(|(_, read_only)| read_only)
2205}
2206
2207fn wasm_mutation_touches_read_only_mapping(
2208 source: &Path,
2209 destination: &Path,
2210 internal_sync_rpc: &WasmInternalSyncRpc,
2211) -> bool {
2212 wasm_host_path_is_read_only(source, internal_sync_rpc)
2213 || wasm_host_path_is_read_only(destination, internal_sync_rpc)
2214}
2215
2216fn wasm_open_flags_require_write(flags: &Value) -> bool {
2217 match flags.as_str() {
2218 Some(value) => value.contains('w') || value.contains('a') || value.contains('+'),
2219 None if flags.as_u64().unwrap_or(0) == 0 => false,
2220 _ => {
2221 let numeric = flags.as_u64().unwrap_or(0);
2222 (numeric & 0o1) != 0
2223 || (numeric & 0o2) != 0
2224 || (numeric & 0o100) != 0
2225 || (numeric & 0o1000) != 0
2226 || (numeric & 0o2000) != 0
2227 }
2228 }
2229}
2230
2231fn wasm_read_only_filesystem_error(path: &str) -> std::io::Error {
2232 let _ = path;
2233 std::io::Error::from_raw_os_error(30)
2234}
2235
2236fn respond_wasm_sync_rpc_metadata(
2237 execution: &mut JavascriptExecution,
2238 request: &JavascriptSyncRpcRequest,
2239 label: &str,
2240 metadata: Result<fs::Metadata, std::io::Error>,
2241) -> Result<(), WasmExecutionError> {
2242 respond_wasm_sync_rpc_value(
2243 execution,
2244 request,
2245 label,
2246 metadata.map(|value| wasm_host_stat_value(&value)),
2247 )
2248}
2249
2250fn respond_wasm_sync_rpc_unit(
2251 execution: &mut JavascriptExecution,
2252 request: &JavascriptSyncRpcRequest,
2253 label: &str,
2254 result: Result<(), std::io::Error>,
2255) -> Result<(), WasmExecutionError> {
2256 respond_wasm_sync_rpc_value(execution, request, label, result.map(|()| Value::Null))
2257}
2258
2259fn respond_wasm_sync_rpc_value(
2260 execution: &mut JavascriptExecution,
2261 request: &JavascriptSyncRpcRequest,
2262 label: &str,
2263 result: Result<Value, std::io::Error>,
2264) -> Result<(), WasmExecutionError> {
2265 match result {
2266 Ok(value) => execution
2267 .respond_sync_rpc_success(request.id, value)
2268 .map_err(map_javascript_error),
2269 Err(error) => execution
2270 .respond_sync_rpc_error(
2271 request.id,
2272 wasm_sync_rpc_error_code(&error),
2273 format!("{} {} failed: {error}", request.method, label),
2274 )
2275 .map_err(map_javascript_error),
2276 }
2277}
2278
2279fn wasm_sync_rpc_error_code(error: &std::io::Error) -> &'static str {
2280 use std::io::ErrorKind;
2281
2282 if error.raw_os_error() == Some(30) {
2283 return "EROFS";
2284 }
2285
2286 match error.kind() {
2287 ErrorKind::NotFound => "ENOENT",
2288 ErrorKind::PermissionDenied => "EACCES",
2289 ErrorKind::AlreadyExists => "EEXIST",
2290 ErrorKind::InvalidInput => "EINVAL",
2291 ErrorKind::IsADirectory => "EISDIR",
2292 ErrorKind::NotADirectory => "ENOTDIR",
2293 _ => "EIO",
2294 }
2295}
2296
2297fn wasm_host_stat_value(metadata: &fs::Metadata) -> Value {
2298 json!({
2299 "mode": metadata.mode(),
2300 "size": metadata.size(),
2301 "blocks": metadata.blocks(),
2302 "dev": metadata.dev(),
2303 "rdev": metadata.rdev(),
2304 "isDirectory": metadata.is_dir(),
2305 "isSymbolicLink": metadata.file_type().is_symlink(),
2306 "atimeMs": metadata.atime() * 1000 + (metadata.atime_nsec() / 1_000_000),
2307 "mtimeMs": metadata.mtime() * 1000 + (metadata.mtime_nsec() / 1_000_000),
2308 "ctimeMs": metadata.ctime() * 1000 + (metadata.ctime_nsec() / 1_000_000),
2309 "birthtimeMs": metadata.ctime() * 1000 + (metadata.ctime_nsec() / 1_000_000),
2310 "ino": metadata.ino(),
2311 "nlink": metadata.nlink(),
2312 "uid": metadata.uid(),
2313 "gid": metadata.gid(),
2314 })
2315}
2316
2317fn strip_guest_prefix(path: &str, prefix: &str) -> Option<String> {
2318 let normalized_path = normalize_guest_path(path);
2319 let normalized_prefix = normalize_guest_path(prefix);
2320 if normalized_path == normalized_prefix {
2321 return Some(String::new());
2322 }
2323 normalized_path
2324 .strip_prefix(&(normalized_prefix + "/"))
2325 .map(str::to_owned)
2326}
2327
2328fn join_host_path(base: &Path, suffix: &str) -> PathBuf {
2329 if suffix.is_empty() {
2330 return base.to_path_buf();
2331 }
2332 suffix
2333 .split('/')
2334 .filter(|segment| !segment.is_empty())
2335 .fold(base.to_path_buf(), |path, segment| path.join(segment))
2336}
2337
2338fn decode_wasm_bytes_arg(
2339 value: Option<&Value>,
2340 label: &'static str,
2341 limit: usize,
2342) -> Result<Vec<u8>, WasmExecutionError> {
2343 let base64 = value
2344 .and_then(Value::as_object)
2345 .and_then(|value| value.get("base64"))
2346 .and_then(Value::as_str)
2347 .ok_or_else(|| WasmExecutionError::RpcResponse(format!("missing {label}")))?;
2348 let decoded_len = base64_decoded_len(base64)
2349 .ok_or_else(|| WasmExecutionError::RpcResponse(format!("invalid {label} base64")))?;
2350 if decoded_len > limit {
2351 return Err(WasmExecutionError::OutputBufferExceeded {
2352 stream: label,
2353 limit,
2354 });
2355 }
2356 base64::engine::general_purpose::STANDARD
2357 .decode(base64)
2358 .map_err(|_| WasmExecutionError::RpcResponse(format!("invalid {label} base64")))
2359}
2360
2361fn base64_decoded_len(base64: &str) -> Option<usize> {
2362 let len = base64.len();
2363 let padding = base64
2364 .as_bytes()
2365 .iter()
2366 .rev()
2367 .take_while(|byte| **byte == b'=')
2368 .take(2)
2369 .count();
2370 let full_quads = len / 4;
2371 let remainder = len % 4;
2372 let base_len = full_quads.checked_mul(3)?.checked_sub(padding)?;
2373 match remainder {
2374 0 => Some(base_len),
2375 1 => None,
2376 2 => base_len.checked_add(1),
2377 3 => base_len.checked_add(2),
2378 _ => None,
2379 }
2380}
2381
2382fn is_v8_stack_overflow_stderr(chunk: &[u8]) -> bool {
2383 std::str::from_utf8(chunk).is_ok_and(|message| {
2384 message.starts_with("RangeError: Maximum call stack size exceeded")
2385 && message.contains("wasm-function")
2386 })
2387}
2388
2389fn configured_wasm_stack_limit_error(limit: u64) -> String {
2390 format!(
2391 "WebAssembly guest exhausted its configured stack budget ({limit} bytes); \
2392raise limits.resources.maxWasmStackBytes to allow deeper guest call stacks.\n"
2393 )
2394}
2395
2396fn append_wasm_captured_output(
2397 buffer: &mut Vec<u8>,
2398 chunk: &[u8],
2399 stream: &'static str,
2400) -> Result<(), WasmExecutionError> {
2401 ensure_wasm_output_capacity(buffer.len(), chunk.len(), stream)?;
2402 buffer.extend_from_slice(chunk);
2403 Ok(())
2404}
2405
2406fn ensure_wasm_output_capacity(
2407 current_len: usize,
2408 chunk_len: usize,
2409 stream: &'static str,
2410) -> Result<(), WasmExecutionError> {
2411 let Some(next_len) = current_len.checked_add(chunk_len) else {
2412 return Err(WasmExecutionError::OutputBufferExceeded {
2413 stream,
2414 limit: WASM_CAPTURED_OUTPUT_LIMIT_BYTES,
2415 });
2416 };
2417 if next_len > WASM_CAPTURED_OUTPUT_LIMIT_BYTES {
2418 return Err(WasmExecutionError::OutputBufferExceeded {
2419 stream,
2420 limit: WASM_CAPTURED_OUTPUT_LIMIT_BYTES,
2421 });
2422 }
2423 Ok(())
2424}
2425
2426fn wasm_sync_read_length(length: Option<u64>) -> Result<usize, WasmExecutionError> {
2427 let length = length.unwrap_or(0);
2428 let length = usize::try_from(length).map_err(|_| {
2429 WasmExecutionError::InvalidLimit(format!("fs.readSync length {length} exceeds host usize"))
2430 })?;
2431 if length > WASM_SYNC_READ_LIMIT_BYTES {
2432 return Err(WasmExecutionError::InvalidLimit(format!(
2433 "fs.readSync length {length} exceeds maximum {WASM_SYNC_READ_LIMIT_BYTES}"
2434 )));
2435 }
2436 Ok(length)
2437}
2438
2439fn open_wasm_guest_file(path: &Path, flags: &Value) -> std::io::Result<fs::File> {
2440 let mut options = OpenOptions::new();
2441 let flags_label = flags.to_string();
2442
2443 match flags.as_str() {
2444 Some("r") | None if flags.as_u64().unwrap_or(0) == 0 => {
2445 options.read(true);
2446 }
2447 Some("r+") => {
2448 options.read(true).write(true);
2449 }
2450 Some("w") => {
2451 options.write(true).create(true).truncate(true);
2452 }
2453 Some("w+") => {
2454 options.read(true).write(true).create(true).truncate(true);
2455 }
2456 Some("a") => {
2457 options.append(true).create(true);
2458 }
2459 Some("a+") => {
2460 options.read(true).append(true).create(true);
2461 }
2462 _ => {
2463 let numeric = flags.as_u64().ok_or_else(|| {
2464 std::io::Error::new(
2465 std::io::ErrorKind::InvalidInput,
2466 format!("unsupported fs.openSync flags: {flags_label}"),
2467 )
2468 })?;
2469 let write_only = (numeric & 0o1) != 0;
2470 let read_write = (numeric & 0o2) != 0;
2471 let create = (numeric & 0o100) != 0;
2472 let truncate = (numeric & 0o1000) != 0;
2473 let append = (numeric & 0o2000) != 0;
2474
2475 if read_write {
2476 options.read(true).write(true);
2477 } else if write_only {
2478 options.write(true);
2479 } else {
2480 options.read(true);
2481 }
2482 if create {
2483 options.create(true);
2484 }
2485 if truncate {
2486 options.truncate(true);
2487 }
2488 if append {
2489 options.append(true);
2490 }
2491 }
2492 }
2493
2494 options.open(path).map_err(|error| {
2495 std::io::Error::new(
2496 error.kind(),
2497 format!(
2498 "failed to open guest file {} with flags {}: {error}",
2499 path.display(),
2500 flags_label
2501 ),
2502 )
2503 })
2504}
2505
2506fn translate_wasm_signal_state_sync_rpc_request(
2507 execution: &mut JavascriptExecution,
2508 request: &JavascriptSyncRpcRequest,
2509) -> Result<Option<WasmExecutionEvent>, WasmExecutionError> {
2510 if request.method != "process.signal_state" {
2511 return Ok(None);
2512 }
2513
2514 let signal = request
2515 .args
2516 .first()
2517 .and_then(Value::as_u64)
2518 .ok_or_else(|| WasmExecutionError::RpcResponse(String::from("missing signal number")))?;
2519 let action = match request
2520 .args
2521 .get(1)
2522 .and_then(Value::as_str)
2523 .unwrap_or("default")
2524 {
2525 "ignore" => WasmSignalDispositionAction::Ignore,
2526 "user" => WasmSignalDispositionAction::User,
2527 _ => WasmSignalDispositionAction::Default,
2528 };
2529 let mask = request
2530 .args
2531 .get(2)
2532 .and_then(Value::as_str)
2533 .map(serde_json::from_str::<Vec<u32>>)
2534 .transpose()
2535 .map_err(|error| WasmExecutionError::RpcResponse(error.to_string()))?
2536 .unwrap_or_default();
2537 let flags = request
2538 .args
2539 .get(3)
2540 .and_then(|value| {
2541 value
2542 .as_u64()
2543 .or_else(|| value.as_i64().map(|signed| signed as u64))
2544 })
2545 .unwrap_or_default() as u32;
2546
2547 execution
2548 .respond_sync_rpc_success(request.id, Value::Null)
2549 .map_err(map_javascript_error)?;
2550
2551 Ok(Some(WasmExecutionEvent::SignalState {
2552 signal: signal as u32,
2553 registration: WasmSignalHandlerRegistration {
2554 action,
2555 mask,
2556 flags,
2557 },
2558 }))
2559}
2560
2561fn parse_wasm_signal_state_line(
2562 line: &[u8],
2563) -> Result<Option<WasmExecutionEvent>, WasmExecutionError> {
2564 let line = line.strip_suffix(b"\n").unwrap_or(line);
2565 let line = line.strip_suffix(b"\r").unwrap_or(line);
2566 let payload = match line.strip_prefix(WASM_SIGNAL_STATE_PREFIX.as_bytes()) {
2567 Some(payload) => payload,
2568 None => return Ok(None),
2569 };
2570 let payload = std::str::from_utf8(payload)
2571 .map_err(|error| WasmExecutionError::RpcResponse(error.to_string()))?;
2572 let message: Value = serde_json::from_str(payload)
2573 .map_err(|error| WasmExecutionError::RpcResponse(error.to_string()))?;
2574 let signal = message
2575 .get("signal")
2576 .and_then(Value::as_u64)
2577 .ok_or_else(|| WasmExecutionError::RpcResponse(String::from("missing signal number")))?;
2578 let registration = message
2579 .get("registration")
2580 .and_then(Value::as_object)
2581 .ok_or_else(|| {
2582 WasmExecutionError::RpcResponse(String::from("missing signal registration"))
2583 })?;
2584 let action = match registration
2585 .get("action")
2586 .and_then(Value::as_str)
2587 .unwrap_or("default")
2588 {
2589 "ignore" => WasmSignalDispositionAction::Ignore,
2590 "user" => WasmSignalDispositionAction::User,
2591 _ => WasmSignalDispositionAction::Default,
2592 };
2593 let mask = registration
2594 .get("mask")
2595 .and_then(Value::as_array)
2596 .map(|entries| {
2597 entries
2598 .iter()
2599 .filter_map(Value::as_u64)
2600 .map(|value| value as u32)
2601 .collect::<Vec<_>>()
2602 })
2603 .unwrap_or_default();
2604 let flags = registration
2605 .get("flags")
2606 .and_then(Value::as_u64)
2607 .unwrap_or_default() as u32;
2608
2609 Ok(Some(WasmExecutionEvent::SignalState {
2610 signal: signal as u32,
2611 registration: WasmSignalHandlerRegistration {
2612 action,
2613 mask,
2614 flags,
2615 },
2616 }))
2617}
2618
2619struct WasmJavascriptExecutionOptions<'a> {
2620 frozen_time_ms: u128,
2621 prewarm_only: bool,
2622 warmup_metrics: Option<&'a [u8]>,
2623 defer_execute: bool,
2624}
2625
2626#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2627enum WasmSnapshotRunnerMode {
2628 Auto,
2629 Block,
2630 Off,
2631}
2632
2633fn wasm_snapshot_runner_mode() -> WasmSnapshotRunnerMode {
2634 match std::env::var(WASM_SNAPSHOT_RUNNER_ENV) {
2635 Ok(value) if value.eq_ignore_ascii_case("block") => WasmSnapshotRunnerMode::Block,
2636 Ok(value) if value.eq_ignore_ascii_case("off") => WasmSnapshotRunnerMode::Off,
2637 Ok(value) if value.eq_ignore_ascii_case("auto") => WasmSnapshotRunnerMode::Auto,
2638 Ok(value) => {
2639 tracing::warn!(
2640 value,
2641 "{WASM_SNAPSHOT_RUNNER_ENV} must be auto, block, or off; using auto"
2642 );
2643 WasmSnapshotRunnerMode::Auto
2644 }
2645 Err(_) => WasmSnapshotRunnerMode::Auto,
2646 }
2647}
2648
2649fn start_wasm_javascript_execution(
2650 javascript_engine: &mut JavascriptExecutionEngine,
2651 runtime: &RuntimeContext,
2652 import_cache: &NodeImportCache,
2653 javascript_context_id: &str,
2654 resolved_module: &ResolvedWasmModule,
2655 request: &StartWasmExecutionRequest,
2656 options: WasmJavascriptExecutionOptions<'_>,
2657) -> Result<JavascriptExecution, WasmExecutionError> {
2658 let wasm_module_bytes = cached_wasm_module_bytes(&resolved_module.resolved_path)?;
2659 let internal_env = build_wasm_internal_env(
2660 resolved_module,
2661 request,
2662 options.frozen_time_ms,
2663 options.prewarm_only,
2664 )?;
2665 let snapshot_mode = wasm_snapshot_runner_mode();
2666 let mut env = wasm_runner_base_env(request);
2667 let mut guest_runtime = request.guest_runtime.clone();
2668
2669 let inline_code = match snapshot_mode {
2670 WasmSnapshotRunnerMode::Off => {
2671 env.extend(
2672 internal_env
2673 .iter()
2674 .map(|(key, value)| (key.clone(), value.clone())),
2675 );
2676 build_wasm_runner_module_source(import_cache, &internal_env, options.warmup_metrics)?
2677 }
2678 WasmSnapshotRunnerMode::Auto | WasmSnapshotRunnerMode::Block => {
2679 let userland_bundle = build_wasm_runner_userland_bundle(import_cache)?;
2680 let runner_heap_limit_mb = wasm_runner_heap_limit_mb(request);
2681 let runtime = javascript_engine
2682 .runtime_context()
2683 .map_err(map_javascript_error)?;
2684 V8RuntimeHost::warm_snapshot_async(runtime, userland_bundle.clone());
2685 let use_snapshot = match snapshot_mode {
2686 WasmSnapshotRunnerMode::Block => {
2687 if !javascript_engine
2688 .snapshot_userland_ready(&userland_bundle)
2689 .map_err(map_javascript_error)?
2690 {
2691 javascript_engine
2692 .pre_warm_snapshot(&userland_bundle)
2693 .map_err(map_javascript_error)?;
2694 }
2695 javascript_engine
2696 .pre_warm_workers(
2697 &userland_bundle,
2698 runner_heap_limit_mb,
2699 v8_warm_worker_count(),
2700 )
2701 .map_err(map_javascript_error)?;
2702 javascript_engine
2703 .pre_warm_workers("", 0, v8_warm_worker_count())
2704 .map_err(map_javascript_error)?;
2705 true
2706 }
2707 WasmSnapshotRunnerMode::Auto => {
2708 let snapshot_ready = javascript_engine
2709 .snapshot_userland_ready(&userland_bundle)
2710 .unwrap_or(false);
2711 if snapshot_ready {
2712 javascript_engine
2713 .pre_warm_workers(
2714 &userland_bundle,
2715 runner_heap_limit_mb,
2716 v8_warm_worker_count(),
2717 )
2718 .map_err(map_javascript_error)?;
2719 }
2720 snapshot_ready
2721 }
2722 WasmSnapshotRunnerMode::Off => false,
2723 };
2724
2725 if use_snapshot {
2726 env = wasm_snapshot_runner_base_env(request);
2727 env.extend(
2728 internal_env
2729 .iter()
2730 .map(|(key, value)| (key.clone(), value.clone())),
2731 );
2732 guest_runtime.snapshot_userland_code = Some(userland_bundle);
2733 build_wasm_snapshot_runner_inline_code(options.warmup_metrics)
2734 } else {
2735 env.extend(
2736 internal_env
2737 .iter()
2738 .map(|(key, value)| (key.clone(), value.clone())),
2739 );
2740 build_wasm_runner_module_source(
2741 import_cache,
2742 &internal_env,
2743 options.warmup_metrics,
2744 )?
2745 }
2746 }
2747 };
2748
2749 let javascript_request = StartJavascriptExecutionRequest {
2750 vm_id: request.vm_id.clone(),
2751 context_id: javascript_context_id.to_owned(),
2752 argv: vec![String::from(WASM_INLINE_RUNNER_ENTRYPOINT)],
2753 argv0: None,
2754 env,
2755 cwd: request.cwd.clone(),
2756 limits: wasm_runner_javascript_limits(&request.limits, wasm_runner_heap_limit_mb(request)),
2763 guest_runtime,
2766 inline_code: Some(inline_code),
2767 wasm_module_bytes: Some(wasm_module_bytes),
2768 };
2769 if options.defer_execute {
2770 javascript_engine.prepare_execution_with_runtime(javascript_request, runtime.clone())
2771 } else {
2772 javascript_engine.start_execution_with_runtime(javascript_request, runtime.clone())
2773 }
2774 .map_err(map_javascript_error)
2775}
2776
2777fn wasm_runner_javascript_limits(
2778 limits: &WasmExecutionLimits,
2779 runner_heap_limit_mb: u32,
2780) -> JavascriptExecutionLimits {
2781 JavascriptExecutionLimits {
2782 v8_heap_limit_mb: Some(runner_heap_limit_mb),
2783 cpu_time_limit_ms: limits.runner_cpu_time_limit_ms,
2784 reactor_work_quantum: limits.reactor_work_quantum,
2785 bridge_call_timeout_ms: limits.bridge_call_timeout_ms,
2786 ..JavascriptExecutionLimits::default()
2787 }
2788}
2789
2790struct WasmModuleBytesCache {
2791 entries: HashMap<PathBuf, (String, Arc<Vec<u8>>)>,
2792}
2793
2794fn wasm_module_bytes_cache() -> &'static Mutex<WasmModuleBytesCache> {
2795 static CACHE: OnceLock<Mutex<WasmModuleBytesCache>> = OnceLock::new();
2796 CACHE.get_or_init(|| {
2797 Mutex::new(WasmModuleBytesCache {
2798 entries: HashMap::new(),
2799 })
2800 })
2801}
2802
2803fn cached_wasm_module_bytes(path: &Path) -> Result<Arc<Vec<u8>>, WasmExecutionError> {
2804 let current_fingerprint = file_fingerprint(path);
2805 {
2806 let cache = wasm_module_bytes_cache()
2807 .lock()
2808 .expect("wasm module bytes cache lock poisoned");
2809 if let Some((fingerprint, bytes)) = cache.entries.get(path) {
2810 if fingerprint == ¤t_fingerprint {
2811 return Ok(Arc::clone(bytes));
2812 }
2813 }
2814 }
2815
2816 let module_bytes = Arc::new(fs::read(path).map_err(WasmExecutionError::PrepareWarmPath)?);
2817 let fingerprint = file_fingerprint(path);
2818 let mut cache = wasm_module_bytes_cache()
2819 .lock()
2820 .expect("wasm module bytes cache lock poisoned");
2821 if !cache.entries.contains_key(path) && cache.entries.len() >= WASM_MODULE_BYTES_CACHE_CAPACITY
2822 {
2823 if let Some(evicted_path) = cache.entries.keys().next().cloned() {
2824 cache.entries.remove(&evicted_path);
2825 tracing::warn!(
2826 path = %evicted_path.display(),
2827 "evicting cached wasm module bytes entry"
2828 );
2829 }
2830 }
2831 cache
2832 .entries
2833 .insert(path.to_path_buf(), (fingerprint, Arc::clone(&module_bytes)));
2834 let cumulative_bytes: usize = cache.entries.values().map(|(_, bytes)| bytes.len()).sum();
2835 tracing::debug!(
2836 path = %path.display(),
2837 raw_bytes = module_bytes.len(),
2838 cumulative_bytes,
2839 "cached wasm module bytes entry"
2840 );
2841 Ok(module_bytes)
2842}
2843
2844fn build_wasm_internal_env(
2845 resolved_module: &ResolvedWasmModule,
2846 request: &StartWasmExecutionRequest,
2847 frozen_time_ms: u128,
2848 prewarm_only: bool,
2849) -> Result<BTreeMap<String, String>, WasmExecutionError> {
2850 let guest_path_mappings = wasm_guest_path_mappings(request);
2851 let mut internal_env = request
2852 .env
2853 .iter()
2854 .filter(|(key, _)| key.starts_with("AGENTOS_"))
2855 .map(|(key, value)| (key.clone(), value.clone()))
2856 .collect::<BTreeMap<_, _>>();
2857 if let Some(value) = request.env.get("AGENTOS_KEEP_STDIN_OPEN") {
2858 internal_env.insert(String::from("AGENTOS_KEEP_STDIN_OPEN"), value.clone());
2859 }
2860 scrub_migrated_wasm_limit_env(&mut internal_env);
2861 insert_optional_u64_env(
2862 &mut internal_env,
2863 WASM_MAX_MEMORY_BYTES_ENV,
2864 request.limits.max_memory_bytes,
2865 );
2866 insert_optional_u64_env(
2867 &mut internal_env,
2868 WASM_MAX_MODULE_FILE_BYTES_ENV,
2869 request.limits.max_module_file_bytes,
2870 );
2871 insert_optional_u64_env(
2872 &mut internal_env,
2873 WASM_MAX_OPEN_FDS_ENV,
2874 request.limits.max_open_fds,
2875 );
2876 insert_optional_u64_env(
2877 &mut internal_env,
2878 WASM_MAX_SPAWN_FILE_ACTIONS_ENV,
2879 request.limits.max_spawn_file_actions,
2880 );
2881 insert_optional_u64_env(
2882 &mut internal_env,
2883 WASM_MAX_SPAWN_FILE_ACTION_BYTES_ENV,
2884 request.limits.max_spawn_file_action_bytes,
2885 );
2886 insert_optional_u64_env(
2887 &mut internal_env,
2888 WASM_MAX_SOCKETS_ENV,
2889 request.limits.max_sockets,
2890 );
2891 insert_optional_u64_env(
2892 &mut internal_env,
2893 WASM_MAX_BLOCKING_READ_MS_ENV,
2894 request.limits.max_blocking_read_ms,
2895 );
2896 insert_optional_u64_env(
2897 &mut internal_env,
2898 WASM_INTERNAL_MAX_STACK_BYTES_ENV,
2899 request.limits.max_stack_bytes,
2900 );
2901 internal_env.insert(
2902 WASM_MODULE_PATH_ENV.to_string(),
2903 resolved_module.specifier.clone(),
2904 );
2905 internal_env.insert(
2906 String::from("AGENTOS_FORWARD_KERNEL_STDIN_RPC"),
2907 String::from("1"),
2908 );
2909 internal_env.insert(
2910 WASM_GUEST_ARGV_ENV.to_string(),
2911 encode_json_string_array(&warmup_guest_argv(resolved_module, request)),
2912 );
2913 internal_env.insert(
2914 WASM_GUEST_ENV_ENV.to_string(),
2915 encode_json_string_map(&guest_visible_wasm_env(&request.env)),
2916 );
2917 insert_wasm_runner_identity_env(&mut internal_env, &request.guest_runtime);
2918 internal_env.insert(
2919 WASM_HOST_CWD_ENV.to_string(),
2920 request.cwd.to_string_lossy().into_owned(),
2921 );
2922 internal_env.insert(
2923 String::from("AGENTOS_GUEST_PATH_MAPPINGS"),
2924 encode_wasm_guest_path_mappings(&guest_path_mappings),
2925 );
2926 internal_env.insert(
2927 WASM_PERMISSION_TIER_ENV.to_string(),
2928 request.permission_tier.as_env_value().to_string(),
2929 );
2930 internal_env.insert(
2931 String::from("AGENTOS_FROZEN_TIME_MS"),
2932 frozen_time_ms.to_string(),
2933 );
2934
2935 if prewarm_only {
2936 internal_env.insert(WASM_PREWARM_ONLY_ENV.to_string(), String::from("1"));
2937 } else {
2938 internal_env.remove(WASM_PREWARM_ONLY_ENV);
2939 }
2940 Ok(internal_env)
2941}
2942
2943fn wasm_runner_base_env(request: &StartWasmExecutionRequest) -> BTreeMap<String, String> {
2944 let mut env = request.env.clone();
2945 scrub_migrated_wasm_limit_env(&mut env);
2946 env
2947}
2948
2949fn wasm_snapshot_runner_base_env(request: &StartWasmExecutionRequest) -> BTreeMap<String, String> {
2950 let mut env = request
2951 .env
2952 .iter()
2953 .filter(|(key, _)| !is_internal_wasm_guest_env_key(key))
2954 .map(|(key, value)| (key.clone(), value.clone()))
2955 .collect::<BTreeMap<_, _>>();
2956 scrub_migrated_wasm_limit_env(&mut env);
2957 env
2958}
2959
2960fn scrub_migrated_wasm_limit_env(env: &mut BTreeMap<String, String>) {
2961 for key in [
2962 WASM_MAX_FUEL_ENV,
2963 WASM_MAX_MEMORY_BYTES_ENV,
2964 WASM_MAX_STACK_BYTES_ENV,
2965 WASM_MAX_MODULE_FILE_BYTES_ENV,
2966 WASM_MAX_OPEN_FDS_ENV,
2967 WASM_MAX_SPAWN_FILE_ACTIONS_ENV,
2968 WASM_MAX_SPAWN_FILE_ACTION_BYTES_ENV,
2969 WASM_MAX_SOCKETS_ENV,
2970 WASM_MAX_BLOCKING_READ_MS_ENV,
2971 "AGENTOS_WASM_PREWARM_TIMEOUT_MS",
2972 "AGENTOS_WASM_RUNNER_HEAP_LIMIT_MB",
2973 ] {
2974 env.remove(key);
2975 }
2976}
2977
2978fn insert_optional_u64_env(env: &mut BTreeMap<String, String>, key: &str, value: Option<u64>) {
2979 if let Some(value) = value {
2980 env.insert(key.to_string(), value.to_string());
2981 } else {
2982 env.remove(key);
2983 }
2984}
2985
2986fn insert_wasm_runner_identity_env(
2987 env: &mut BTreeMap<String, String>,
2988 guest_runtime: &GuestRuntimeConfig,
2989) {
2990 insert_optional_u64_env(
2991 env,
2992 "AGENTOS_VIRTUAL_PROCESS_UID",
2993 guest_runtime.virtual_uid,
2994 );
2995 insert_optional_u64_env(
2996 env,
2997 "AGENTOS_VIRTUAL_PROCESS_GID",
2998 guest_runtime.virtual_gid,
2999 );
3000 insert_optional_u64_env(
3001 env,
3002 "AGENTOS_VIRTUAL_PROCESS_PID",
3003 guest_runtime.virtual_pid,
3004 );
3005 insert_optional_u64_env(
3006 env,
3007 "AGENTOS_VIRTUAL_PROCESS_PPID",
3008 guest_runtime.virtual_ppid,
3009 );
3010}
3011
3012fn build_wasm_runner_module_source(
3013 import_cache: &NodeImportCache,
3014 internal_env: &BTreeMap<String, String>,
3015 warmup_metrics: Option<&[u8]>,
3016) -> Result<String, WasmExecutionError> {
3017 let runner_source = transformed_wasm_runner_source(import_cache)?;
3018 let bootstrap = build_wasm_runner_bootstrap(internal_env, warmup_metrics);
3019 Ok(insert_wasm_runner_bootstrap(&runner_source, &bootstrap))
3020}
3021
3022fn transformed_wasm_runner_source(
3023 import_cache: &NodeImportCache,
3024) -> Result<String, WasmExecutionError> {
3025 if std::env::var(WASM_RUNNER_NO_CACHE_ENV).as_deref() == Ok("1") {
3026 return read_transformed_wasm_runner_source(import_cache);
3027 }
3028
3029 static RUNNER_SOURCE: OnceLock<Result<Arc<str>, Arc<str>>> = OnceLock::new();
3030 RUNNER_SOURCE
3031 .get_or_init(|| {
3032 read_transformed_wasm_runner_source(import_cache)
3033 .map(Arc::<str>::from)
3034 .map_err(|error| Arc::<str>::from(error.to_string()))
3035 })
3036 .as_ref()
3037 .map(|source| source.to_string())
3038 .map_err(|message| {
3039 WasmExecutionError::PrepareWarmPath(std::io::Error::other(message.to_string()))
3040 })
3041}
3042
3043fn read_transformed_wasm_runner_source(
3044 import_cache: &NodeImportCache,
3045) -> Result<String, WasmExecutionError> {
3046 let runner_source = fs::read_to_string(import_cache.wasm_runner_path())
3047 .map_err(WasmExecutionError::PrepareWarmPath)?;
3048 Ok(runner_source.replace(
3049 "import { WASI } from 'node:wasi';\n",
3050 "const { WASI } = globalThis.__agentOSWasiModule;\n",
3051 ))
3052}
3053
3054fn build_wasm_runner_userland_bundle(
3055 import_cache: &NodeImportCache,
3056) -> Result<String, WasmExecutionError> {
3057 if std::env::var(WASM_RUNNER_NO_CACHE_ENV).as_deref() == Ok("1") {
3058 return build_wasm_runner_userland_bundle_uncached(import_cache);
3059 }
3060
3061 static USERLAND_BUNDLE: OnceLock<Result<Arc<str>, Arc<str>>> = OnceLock::new();
3062 USERLAND_BUNDLE
3063 .get_or_init(|| {
3064 build_wasm_runner_userland_bundle_uncached(import_cache)
3065 .map(Arc::<str>::from)
3066 .map_err(|error| Arc::<str>::from(error.to_string()))
3067 })
3068 .as_ref()
3069 .map(|bundle| bundle.to_string())
3070 .map_err(|message| {
3071 WasmExecutionError::PrepareWarmPath(std::io::Error::other(message.to_string()))
3072 })
3073}
3074
3075fn build_wasm_runner_userland_bundle_uncached(
3076 import_cache: &NodeImportCache,
3077) -> Result<String, WasmExecutionError> {
3078 let runner_source = transformed_wasm_runner_source(import_cache)?;
3079 if runner_source
3080 .lines()
3081 .any(|line| line.trim_start().starts_with("import "))
3082 {
3083 return Err(WasmExecutionError::PrepareWarmPath(std::io::Error::other(
3084 "transformed wasm runner still contains an ESM import statement",
3085 )));
3086 }
3087
3088 let mut bundle = build_wasm_runner_snapshot_prelude();
3089 bundle.push_str("\nglobalThis.__agentOSWasmRunnerRun = async function () {\n");
3090 bundle.push_str(&runner_source);
3091 bundle.push_str("\n};\n");
3092 Ok(bundle)
3093}
3094
3095fn build_wasm_runner_snapshot_prelude() -> String {
3096 let bootstrap = build_wasm_runner_bootstrap(&BTreeMap::new(), None);
3097 let bootstrap = bootstrap
3098 .strip_prefix("const __agentOSWasmInternalEnv = {};\n")
3099 .unwrap_or(&bootstrap);
3100 bootstrap.replace(wasm_internal_env_merge_source(), "")
3101}
3102
3103fn build_wasm_snapshot_runner_inline_code(warmup_metrics: Option<&[u8]>) -> String {
3104 let warmup_emit = wasm_warmup_metrics_emit_source(warmup_metrics);
3105 format!(
3106 r#"{warmup_emit}if (typeof process !== "undefined" && typeof globalThis.__agentOSProcessConfigEnv === "object") {{
3107 process.env = {{ ...(process.env || {{}}), ...globalThis.__agentOSProcessConfigEnv }};
3108}}
3109await globalThis.__agentOSWasmRunnerRun();"#
3110 )
3111}
3112
3113fn build_wasm_runner_bootstrap(
3114 internal_env: &BTreeMap<String, String>,
3115 warmup_metrics: Option<&[u8]>,
3116) -> String {
3117 let internal_env_json =
3118 serde_json::to_string(internal_env).unwrap_or_else(|_| String::from("{}"));
3119 let warmup_emit = wasm_warmup_metrics_emit_source(warmup_metrics);
3120 let wasi_module_source = render_native_wasi_module_source();
3121 let env_merge_source = wasm_internal_env_merge_source();
3122 let wasm_sync_rpc_read_payload_bytes =
3123 max_cbor_byte_string_payload_bytes(WASM_PROCESS_SYNC_RPC_RESPONSE_BYTES);
3124
3125 format!(
3126 r#"const __agentOSWasmInternalEnv = {internal_env_json};
3127const __agentOSWasmSyncRpcReadPayloadBytes = {wasm_sync_rpc_read_payload_bytes};
3128const __agentOSRequireBuiltin = (specifier) => {{
3129 if (typeof globalThis.require === "function") {{
3130 return globalThis.require(specifier);
3131 }}
3132 if (typeof process?.getBuiltinModule === "function") {{
3133 return process.getBuiltinModule(specifier);
3134 }}
3135 throw new Error(`agentos WASM bootstrap cannot load ${{specifier}}`);
3136}};
3137{wasi_module_source}
3138{env_merge_source}
3139if (typeof globalThis !== "undefined") {{
3140 const __agentOSNormalizeBytes = (value) => {{
3141 if (value == null) {{
3142 return value;
3143 }}
3144 if (typeof Buffer !== "undefined" && Buffer.isBuffer(value)) {{
3145 return value;
3146 }}
3147 if (value instanceof Uint8Array) {{
3148 return Buffer.from(value);
3149 }}
3150 if (ArrayBuffer.isView(value)) {{
3151 return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
3152 }}
3153 if (value instanceof ArrayBuffer) {{
3154 return Buffer.from(value);
3155 }}
3156 if (
3157 value &&
3158 typeof value === "object" &&
3159 value.__agentOSType === "bytes" &&
3160 typeof value.base64 === "string"
3161 ) {{
3162 return Buffer.from(value.base64, "base64");
3163 }}
3164 return value;
3165 }};
3166 const __agentOSWasmSyncRpc = {{
3167 callSync(method, args = []) {{
3168 switch (method) {{
3169 case "fs.fstatSync":
3170 return __agentOSRequireBuiltin("node:fs").fstatSync(...args);
3171 case "fs.lstatSync":
3172 return __agentOSRequireBuiltin("node:fs").lstatSync(...args);
3173 case "fs.statSync":
3174 return __agentOSRequireBuiltin("node:fs").statSync(...args);
3175 case "fs.chmodSync":
3176 return __agentOSRequireBuiltin("node:fs").chmodSync(...args);
3177 case "__kernel_stdio_write":
3178 if (typeof _kernelStdioWriteRaw === "undefined") {{
3179 throw new Error("agentos WASM kernel stdio bridge is unavailable");
3180 }}
3181 return _kernelStdioWriteRaw.applySync(void 0, args);
3182 case "__kernel_stdin_read":
3183 if (typeof _kernelStdinReadRaw === "undefined") {{
3184 throw new Error("agentos WASM kernel stdin bridge is unavailable");
3185 }}
3186 return _kernelStdinReadRaw.applySync(void 0, args);
3187 case "__kernel_poll":
3188 if (typeof _kernelPollRaw === "undefined") {{
3189 throw new Error("agentos WASM kernel poll bridge is unavailable");
3190 }}
3191 return _kernelPollRaw.applySync(void 0, args);
3192 case "__kernel_isatty":
3193 if (typeof _kernelIsattyRaw === "undefined") {{
3194 throw new Error("agentos WASM kernel isatty bridge is unavailable");
3195 }}
3196 return _kernelIsattyRaw.applySync(void 0, args);
3197 case "__kernel_flock_path":
3198 if (typeof _kernelFlockRaw === "undefined") {{
3199 throw new Error("agentos WASM kernel file-lock bridge is unavailable");
3200 }}
3201 return _kernelFlockRaw.applySync(void 0, args);
3202 case "__kernel_tty_size":
3203 if (typeof _kernelTtySizeRaw === "undefined") {{
3204 throw new Error("agentos WASM kernel tty size bridge is unavailable");
3205 }}
3206 return _kernelTtySizeRaw.applySync(void 0, args);
3207 case "__pty_set_raw_mode":
3208 if (typeof _ptySetRawMode === "undefined") {{
3209 throw new Error("agentos WASM PTY raw-mode bridge is unavailable");
3210 }}
3211 return _ptySetRawMode.applySync(void 0, args);
3212 case "child_process.spawn": {{
3213 if (typeof _childProcessSpawnStart === "undefined") {{
3214 throw new Error("agentos WASM child_process bridge is unavailable");
3215 }}
3216 const [request] = args;
3217 return _childProcessSpawnStart.applySync(void 0, [
3218 request?.command ?? "",
3219 JSON.stringify(request?.args ?? []),
3220 JSON.stringify(request?.options ?? {{}}),
3221 ]);
3222 }}
3223 case "child_process.poll":
3224 if (typeof _childProcessPoll === "undefined") {{
3225 throw new Error("agentos WASM child_process poll bridge is unavailable");
3226 }}
3227 return _childProcessPoll.applySync(void 0, args);
3228 case "child_process.kill":
3229 if (typeof _childProcessKill === "undefined") {{
3230 throw new Error("agentos WASM child_process kill bridge is unavailable");
3231 }}
3232 return _childProcessKill.applySync(void 0, args);
3233 case "process.kill":
3234 if (typeof _processKill === "undefined") {{
3235 throw new Error("agentos WASM process kill bridge is unavailable");
3236 }}
3237 return _processKill.applySync(void 0, args);
3238 case "process.exec":
3239 if (typeof _processExec === "undefined") {{
3240 throw new Error("agentos WASM process exec bridge is unavailable");
3241 }}
3242 return _processExec.applySync(void 0, args);
3243 case "process.exec_fd_image_commit":
3244 if (typeof _processExecFdImageCommit === "undefined") {{
3245 throw new Error("agentos WASM process fd image commit bridge is unavailable");
3246 }}
3247 return _processExecFdImageCommit.applySync(void 0, args);
3248 case "child_process.write_stdin": {{
3249 if (typeof _childProcessStdinWrite === "undefined") {{
3250 throw new Error("agentos WASM child_process stdin bridge is unavailable");
3251 }}
3252 const [childId, chunk] = args;
3253 return _childProcessStdinWrite.applySync(void 0, [
3254 childId,
3255 __agentOSNormalizeBytes(chunk),
3256 ]);
3257 }}
3258 case "child_process.close_stdin":
3259 if (typeof _childProcessStdinClose === "undefined") {{
3260 throw new Error("agentos WASM child_process stdin-close bridge is unavailable");
3261 }}
3262 return _childProcessStdinClose.applySync(void 0, args);
3263 case "net.connect":
3264 if (typeof _netSocketConnectRaw === "undefined") {{
3265 throw new Error("agentos WASM net.connect bridge is unavailable");
3266 }}
3267 return _netSocketConnectRaw.applySync(void 0, args);
3268 case "net.bind_unix":
3269 if (typeof _netBindUnixRaw === "undefined") {{
3270 throw new Error("agentos WASM net.bind_unix bridge is unavailable");
3271 }}
3272 return _netBindUnixRaw.applySync(void 0, args);
3273 case "net.bind_connected_unix":
3274 if (typeof _netBindConnectedUnixRaw === "undefined") {{
3275 throw new Error("agentos WASM net.bind_connected_unix bridge is unavailable");
3276 }}
3277 return _netBindConnectedUnixRaw.applySync(void 0, args);
3278 case "net.reserve_tcp_port":
3279 if (typeof _netReserveTcpPortRaw === "undefined") {{
3280 throw new Error("agentos WASM net.reserve_tcp_port bridge is unavailable");
3281 }}
3282 return _netReserveTcpPortRaw.applySync(void 0, args);
3283 case "net.release_tcp_port":
3284 if (typeof _netReleaseTcpPortRaw === "undefined") {{
3285 throw new Error("agentos WASM net.release_tcp_port bridge is unavailable");
3286 }}
3287 return _netReleaseTcpPortRaw.applySync(void 0, args);
3288 case "net.listen":
3289 if (typeof _netServerListenRaw === "undefined") {{
3290 throw new Error("agentos WASM net.listen bridge is unavailable");
3291 }}
3292 return _netServerListenRaw.applySync(void 0, args);
3293 case "net.server_accept":
3294 if (typeof _netServerAcceptRaw === "undefined") {{
3295 throw new Error("agentos WASM net.server_accept bridge is unavailable");
3296 }}
3297 return _netServerAcceptRaw.applySync(void 0, args);
3298 case "net.server_close":
3299 if (typeof _netServerCloseSyncRaw === "undefined") {{
3300 throw new Error("agentos WASM net.server_close bridge is unavailable");
3301 }}
3302 return _netServerCloseSyncRaw.applySync(void 0, args);
3303 case "net.poll":
3304 if (typeof _netSocketPollRaw === "undefined") {{
3305 throw new Error("agentos WASM net.poll bridge is unavailable");
3306 }}
3307 return _netSocketPollRaw.applySync(void 0, args);
3308 case "net.socket_read":
3309 if (typeof _netSocketReadRaw === "undefined") {{
3310 throw new Error("agentos WASM net.socket_read bridge is unavailable");
3311 }}
3312 return _netSocketReadRaw.applySync(void 0, args);
3313 case "net.socket_wait_connect":
3314 if (typeof _netSocketWaitConnectSyncRaw === "undefined") {{
3315 throw new Error("agentos WASM net.socket_wait_connect bridge is unavailable");
3316 }}
3317 return _netSocketWaitConnectSyncRaw.applySync(void 0, args);
3318 case "net.write":
3319 if (typeof _netSocketWriteSyncRaw === "undefined") {{
3320 throw new Error("agentos WASM net.write bridge is unavailable");
3321 }}
3322 return _netSocketWriteSyncRaw.applySync(void 0, [
3323 args[0],
3324 __agentOSNormalizeBytes(args[1]),
3325 args[2],
3326 ]);
3327 case "net.destroy":
3328 if (typeof _netSocketDestroyRaw === "undefined") {{
3329 throw new Error("agentos WASM net.destroy bridge is unavailable");
3330 }}
3331 return _netSocketDestroyRaw.applySync(void 0, args);
3332 case "net.socket_upgrade_tls":
3333 if (typeof _netSocketUpgradeTlsRaw === "undefined") {{
3334 throw new Error("agentos WASM TLS-upgrade bridge is unavailable");
3335 }}
3336 return _netSocketUpgradeTlsRaw.applySync(void 0, args);
3337 case "dgram.createSocket":
3338 if (typeof _dgramSocketCreateRaw === "undefined") {{
3339 throw new Error("agentos WASM dgram.createSocket bridge is unavailable");
3340 }}
3341 return _dgramSocketCreateRaw.applySync(void 0, args);
3342 case "dgram.bind":
3343 if (typeof _dgramSocketBindRaw === "undefined") {{
3344 throw new Error("agentos WASM dgram.bind bridge is unavailable");
3345 }}
3346 return _dgramSocketBindRaw.applySync(void 0, args);
3347 case "dgram.send": {{
3348 if (typeof _dgramSocketSendRaw === "undefined") {{
3349 throw new Error("agentos WASM dgram.send bridge is unavailable");
3350 }}
3351 const [socketId, chunk, options = {{}}] = args;
3352 return _dgramSocketSendRaw.applySync(void 0, [
3353 socketId,
3354 __agentOSNormalizeBytes(chunk),
3355 options,
3356 ]);
3357 }}
3358 case "dgram.poll":
3359 if (typeof _dgramSocketRecvRaw === "undefined") {{
3360 throw new Error("agentos WASM dgram.poll bridge is unavailable");
3361 }}
3362 const event = _dgramSocketRecvRaw.applySync(void 0, args);
3363 if (event && event.type === "message") {{
3364 const data = __agentOSNormalizeBytes(event.data);
3365 if (typeof Buffer !== "undefined" && Buffer.isBuffer(data)) {{
3366 return {{
3367 ...event,
3368 data: {{ base64: data.toString("base64") }},
3369 }};
3370 }}
3371 }}
3372 if (
3373 event &&
3374 event.type === "message" &&
3375 event.data &&
3376 typeof event.data === "object" &&
3377 typeof event.data.base64 === "string"
3378 ) {{
3379 return {{
3380 ...event,
3381 data: {{ base64: event.data.base64 }},
3382 }};
3383 }}
3384 return event;
3385 case "dgram.close":
3386 if (typeof _dgramSocketCloseRaw === "undefined") {{
3387 throw new Error("agentos WASM dgram.close bridge is unavailable");
3388 }}
3389 return _dgramSocketCloseRaw.applySync(void 0, args);
3390 case "dgram.address":
3391 if (typeof _dgramSocketAddressRaw === "undefined") {{
3392 throw new Error("agentos WASM dgram.address bridge is unavailable");
3393 }}
3394 return _dgramSocketAddressRaw.applySync(void 0, args);
3395 case "dgram.setBufferSize":
3396 if (typeof _dgramSocketSetBufferSizeRaw === "undefined") {{
3397 throw new Error("agentos WASM dgram.setBufferSize bridge is unavailable");
3398 }}
3399 return _dgramSocketSetBufferSizeRaw.applySync(void 0, args);
3400 case "dgram.getBufferSize":
3401 if (typeof _dgramSocketGetBufferSizeRaw === "undefined") {{
3402 throw new Error("agentos WASM dgram.getBufferSize bridge is unavailable");
3403 }}
3404 return _dgramSocketGetBufferSizeRaw.applySync(void 0, args);
3405 case "dns.lookup":
3406 if (typeof _networkDnsLookupSyncRaw === "undefined") {{
3407 throw new Error("agentos WASM dns.lookup bridge is unavailable");
3408 }}
3409 return _networkDnsLookupSyncRaw.applySync(void 0, args);
3410 case "process.signal_state": {{
3411 if (typeof _processSignalState === "undefined") {{
3412 throw new Error("agentos WASM signal-state bridge is unavailable");
3413 }}
3414 const [signal, action = "default", maskJson = "[]", flags = 0] = args;
3415 return _processSignalState.applySyncPromise(void 0, [
3416 signal,
3417 action,
3418 maskJson,
3419 flags,
3420 ]);
3421 }}
3422 case "process.take_signal":
3423 if (typeof _processTakeSignal === "undefined") {{
3424 throw new Error("agentos WASM signal-drain bridge is unavailable");
3425 }}
3426 return _processTakeSignal.applySync(void 0, args);
3427 case "process.getpgid":
3428 case "process.getuid":
3429 case "process.getgid":
3430 case "process.geteuid":
3431 case "process.getegid":
3432 case "process.getresuid":
3433 case "process.getresgid":
3434 case "process.getgroups":
3435 case "process.getpwuid":
3436 case "process.getpwnam":
3437 case "process.getpwent":
3438 case "process.getgrgid":
3439 case "process.getgrnam":
3440 case "process.getgrent":
3441 case "process.setuid":
3442 case "process.seteuid":
3443 case "process.setreuid":
3444 case "process.setresuid":
3445 case "process.setgid":
3446 case "process.setegid":
3447 case "process.setregid":
3448 case "process.setresgid":
3449 case "process.setgroups":
3450 case "process.umask":
3451 case "fs.accessSync":
3452 case "fs.blockingIoTimeoutMsSync":
3453 case "fs.chmodForProcessSync":
3454 case "fs.chownSync":
3455 case "fs.collapseRangeSync":
3456 case "fs.fallocateSync":
3457 case "fs.fiemapSync":
3458 case "fs.getxattrSync":
3459 case "fs.insertRangeSync":
3460 case "fs.lchownSync":
3461 case "fs.linkFdSync":
3462 case "fs.listxattrSync":
3463 case "fs.mknodSync":
3464 case "fs.namedFifoPeerReadySync":
3465 case "fs.openTmpfileSync":
3466 case "fs.punchHoleSync":
3467 case "fs.remountSync":
3468 case "fs.removexattrSync":
3469 case "fs.renameAt2Sync":
3470 case "fs.setxattrSync":
3471 case "fs.statfsSync":
3472 case "fs.truncateForProcessSync":
3473 case "fs.zeroRangeSync":
3474 case "process.setpgid":
3475 case "process.waitpid_transition":
3476 case "process.itimer_real":
3477 case "process.fd_pipe":
3478 case "process.fd_open":
3479 case "process.path_open_at":
3480 case "process.path_mkdir_at":
3481 case "process.path_stat_at":
3482 case "process.path_utimes_at":
3483 case "process.path_chown_at":
3484 case "process.path_link_at":
3485 case "process.path_readlink_at":
3486 case "process.path_remove_dir_at":
3487 case "process.path_rename_at":
3488 case "process.path_symlink_at":
3489 case "process.path_unlink_at":
3490 case "process.fd_snapshot":
3491 case "process.fd_read":
3492 case "process.fd_pread":
3493 case "process.fd_write":
3494 case "process.fd_pwrite":
3495 case "process.fd_sync":
3496 case "process.fd_datasync":
3497 case "process.fd_readdir":
3498 case "process.fd_close":
3499 case "process.fd_stat":
3500 case "process.fd_filestat":
3501 case "process.fd_chmod":
3502 case "process.fd_chown":
3503 case "process.fd_truncate":
3504 case "process.fd_set_flags":
3505 case "process.fd_getfd":
3506 case "process.fd_setfd":
3507 case "process.fd_flock":
3508 case "process.fd_record_lock":
3509 case "process.fd_record_lock_cancel":
3510 case "process.fd_dup":
3511 case "process.fd_dup2":
3512 case "process.fd_dup_min":
3513 case "process.fd_seek":
3514 case "process.fd_chdir_path":
3515 case "process.fd_socketpair":
3516 case "process.fd_sendmsg_rights":
3517 case "process.fd_recvmsg_rights":
3518 case "process.fd_socket_shutdown":
3519 if (typeof _processWasmSyncRpc === "undefined") {{
3520 throw new Error("agentos WASM process-syscall bridge is unavailable");
3521 }}
3522 return _processWasmSyncRpc.applySync(void 0, [method, ...args]);
3523 default:
3524 throw new Error(`agentos WASM sync RPC method not implemented in V8 runtime: ${{method}}`);
3525 }}
3526 }},
3527 async call(method, args = []) {{
3528 return this.callSync(method, args);
3529 }},
3530 }};
3531 Object.defineProperty(globalThis, "__agentOSSyncRpc", {{
3532 configurable: true,
3533 enumerable: false,
3534 value: __agentOSWasmSyncRpc,
3535 writable: true,
3536 }});
3537}}
3538{warmup_emit}"#
3539 )
3540}
3541
3542fn max_cbor_byte_string_payload_bytes(encoded_limit: usize) -> usize {
3543 for payload_bytes in (encoded_limit.saturating_sub(9)..=encoded_limit).rev() {
3547 let header_bytes = if payload_bytes <= 23 {
3548 1
3549 } else if u8::try_from(payload_bytes).is_ok() {
3550 2
3551 } else if u16::try_from(payload_bytes).is_ok() {
3552 3
3553 } else if u32::try_from(payload_bytes).is_ok() {
3554 5
3555 } else {
3556 9
3557 };
3558 if payload_bytes
3559 .checked_add(header_bytes)
3560 .is_some_and(|encoded_bytes| encoded_bytes <= encoded_limit)
3561 {
3562 return payload_bytes;
3563 }
3564 }
3565 0
3566}
3567
3568fn wasm_warmup_metrics_emit_source(warmup_metrics: Option<&[u8]>) -> String {
3569 let warmup_metrics_json = warmup_metrics.map(|bytes| {
3570 serde_json::to_string(&String::from_utf8_lossy(bytes).to_string())
3571 .unwrap_or_else(|_| String::from("\"\""))
3572 });
3573 warmup_metrics_json
3574 .map(|metrics| {
3575 format!(
3576 "if (typeof process?.stderr?.write === \"function\") {{\n process.stderr.write({metrics});\n}}\n"
3577 )
3578 })
3579 .unwrap_or_default()
3580}
3581
3582fn wasm_internal_env_merge_source() -> &'static str {
3583 r#"if (typeof process !== "undefined") {
3584 process.env = { ...(process.env || {}), ...__agentOSWasmInternalEnv };
3585}
3586"#
3587}
3588
3589fn render_native_wasi_module_source() -> &'static str {
3590 static SOURCE: OnceLock<String> = OnceLock::new();
3591 SOURCE.get_or_init(|| {
3592 NODE_WASI_MODULE_SOURCE.replace(
3593 "__AGENTOS_WASM_SYNC_READ_LIMIT_BYTES__",
3594 &WASM_SYNC_READ_LIMIT_BYTES.to_string(),
3595 )
3596 })
3597}
3598
3599fn insert_wasm_runner_bootstrap(source: &str, bootstrap: &str) -> String {
3600 let mut insert_at = 0usize;
3601 let mut saw_import = false;
3602 for line in source.split_inclusive('\n') {
3603 let trimmed = line.trim_start();
3604 if trimmed.starts_with("import ") || (saw_import && trimmed.is_empty()) {
3605 insert_at += line.len();
3606 saw_import = saw_import || trimmed.starts_with("import ");
3607 continue;
3608 }
3609 break;
3610 }
3611
3612 format!(
3613 "{}{}{}",
3614 &source[..insert_at],
3615 bootstrap,
3616 &source[insert_at..]
3617 )
3618}
3619
3620struct WasmPrewarmOptions<'a> {
3621 frozen_time_ms: u128,
3622 timeout: Duration,
3623 runtime: &'a RuntimeContext,
3624}
3625
3626fn prewarm_wasm_path(
3627 import_cache: &NodeImportCache,
3628 javascript_engine: &mut JavascriptExecutionEngine,
3629 javascript_context_id: &str,
3630 resolved_module: &ResolvedWasmModule,
3631 request: &StartWasmExecutionRequest,
3632 options: WasmPrewarmOptions<'_>,
3633) -> Result<Option<Vec<u8>>, WasmExecutionError> {
3634 let debug_enabled = env_flag_enabled(&request.env, WASM_WARMUP_DEBUG_ENV);
3635 let marker_contents = warmup_marker_contents(resolved_module);
3636 let marker_path = warmup_marker_path(
3637 import_cache.prewarm_marker_dir(),
3638 "wasm-runner-prewarm",
3639 WASM_WARMUP_MARKER_VERSION,
3640 &marker_contents,
3641 );
3642
3643 if let Ok(metadata) = fs::metadata(&resolved_module.resolved_path) {
3644 if metadata.len() > MAX_SYNC_WASM_PREWARM_MODULE_BYTES {
3645 return Ok(warmup_metrics_line(
3646 debug_enabled,
3647 false,
3648 "skipped-large-module",
3649 import_cache,
3650 &resolved_module.specifier,
3651 ));
3652 }
3653 }
3654
3655 if marker_path.exists() {
3656 return Ok(warmup_metrics_line(
3657 debug_enabled,
3658 false,
3659 "cached",
3660 import_cache,
3661 &resolved_module.specifier,
3662 ));
3663 }
3664
3665 let mut prewarm_execution = start_wasm_javascript_execution(
3666 javascript_engine,
3667 options.runtime,
3668 import_cache,
3669 javascript_context_id,
3670 resolved_module,
3671 request,
3672 WasmJavascriptExecutionOptions {
3673 frozen_time_ms: options.frozen_time_ms,
3674 prewarm_only: true,
3675 warmup_metrics: None,
3676 defer_execute: false,
3677 },
3678 )
3679 .map_err(|error| match error {
3680 WasmExecutionError::Spawn(err) => WasmExecutionError::WarmupSpawn(err),
3681 other => other,
3682 })?;
3683 let mut internal_sync_rpc = WasmInternalSyncRpc {
3684 module_guest_paths: wasm_guest_module_paths(&resolved_module.specifier, &request.env),
3685 module_host_path: resolved_module.resolved_path.clone(),
3686 guest_cwd: wasm_guest_cwd(&request.env),
3687 host_cwd: request.cwd.clone(),
3688 sandbox_root: wasm_sandbox_root(&request.env),
3689 guest_path_mappings: wasm_guest_path_mappings(request),
3690 route_fs_through_sidecar: false,
3691 next_fd: 64,
3692 open_files: BTreeMap::new(),
3693 pending_events: VecDeque::new(),
3694 };
3695 let mut stdout = Vec::new();
3696 let mut stderr = Vec::new();
3697 let started = Instant::now();
3698
3699 loop {
3700 let poll_timeout = options.timeout.saturating_sub(started.elapsed());
3701 if poll_timeout.is_zero() {
3702 if let Err(error) = prewarm_execution.terminate() {
3703 eprintln!(
3704 "ERR_AGENTOS_WASM_PREWARM_TERMINATE: timed-out prewarm did not terminate cleanly: {error}"
3705 );
3706 }
3707 return Err(WasmExecutionError::WarmupTimeout(options.timeout));
3708 }
3709
3710 match prewarm_execution
3711 .poll_event_blocking(poll_timeout)
3712 .map_err(map_javascript_error)?
3713 {
3714 Some(JavascriptExecutionEvent::Stdout(chunk)) => {
3715 append_wasm_captured_output(&mut stdout, &chunk, "stdout")?;
3716 }
3717 Some(JavascriptExecutionEvent::Stderr(chunk)) => {
3718 append_wasm_captured_output(&mut stderr, &chunk, "stderr")?;
3719 }
3720 Some(JavascriptExecutionEvent::Exited(exit_code)) => {
3721 if exit_code != 0 {
3722 return Err(WasmExecutionError::WarmupFailed {
3723 exit_code,
3724 stderr: String::from_utf8_lossy(&stderr).into_owned(),
3725 });
3726 }
3727 break;
3728 }
3729 Some(JavascriptExecutionEvent::SyncRpcRequest(sync_request)) => {
3730 let handled = handle_internal_wasm_sync_rpc_request(
3731 &mut prewarm_execution,
3732 &mut internal_sync_rpc,
3733 &sync_request,
3734 )?;
3735 if !handled {
3736 return Err(WasmExecutionError::WarmupFailed {
3737 exit_code: 1,
3738 stderr: format!(
3739 "unexpected WebAssembly prewarm sync RPC request {} {} {:?}",
3740 sync_request.id, sync_request.method, sync_request.args
3741 ),
3742 });
3743 }
3744 }
3745 Some(JavascriptExecutionEvent::SignalState { .. }) => {}
3746 None => {
3747 if let Err(error) = prewarm_execution.terminate() {
3748 eprintln!(
3749 "ERR_AGENTOS_WASM_PREWARM_TERMINATE: timed-out prewarm did not terminate cleanly: {error}"
3750 );
3751 }
3752 return Err(WasmExecutionError::WarmupTimeout(options.timeout));
3753 }
3754 }
3755 }
3756
3757 let _ = stdout;
3758 fs::write(&marker_path, marker_contents).map_err(WasmExecutionError::PrepareWarmPath)?;
3759 Ok(warmup_metrics_line(
3760 debug_enabled,
3761 true,
3762 "executed",
3763 import_cache,
3764 &resolved_module.specifier,
3765 ))
3766}
3767
3768async fn prewarm_wasm_path_async(
3769 import_cache: &NodeImportCache,
3770 javascript_engine: &mut JavascriptExecutionEngine,
3771 javascript_context_id: &str,
3772 resolved_module: &ResolvedWasmModule,
3773 request: &StartWasmExecutionRequest,
3774 options: WasmPrewarmOptions<'_>,
3775) -> Result<Option<Vec<u8>>, WasmExecutionError> {
3776 let debug_enabled = env_flag_enabled(&request.env, WASM_WARMUP_DEBUG_ENV);
3777 let marker_contents = warmup_marker_contents(resolved_module);
3778 let marker_path = warmup_marker_path(
3779 import_cache.prewarm_marker_dir(),
3780 "wasm-runner-prewarm",
3781 WASM_WARMUP_MARKER_VERSION,
3782 &marker_contents,
3783 );
3784
3785 if let Ok(metadata) = fs::metadata(&resolved_module.resolved_path) {
3786 if metadata.len() > MAX_SYNC_WASM_PREWARM_MODULE_BYTES {
3787 return Ok(warmup_metrics_line(
3788 debug_enabled,
3789 false,
3790 "skipped-large-module",
3791 import_cache,
3792 &resolved_module.specifier,
3793 ));
3794 }
3795 }
3796
3797 if marker_path.exists() {
3798 return Ok(warmup_metrics_line(
3799 debug_enabled,
3800 false,
3801 "cached",
3802 import_cache,
3803 &resolved_module.specifier,
3804 ));
3805 }
3806
3807 let mut prewarm_execution = start_wasm_javascript_execution(
3808 javascript_engine,
3809 options.runtime,
3810 import_cache,
3811 javascript_context_id,
3812 resolved_module,
3813 request,
3814 WasmJavascriptExecutionOptions {
3815 frozen_time_ms: options.frozen_time_ms,
3816 prewarm_only: true,
3817 warmup_metrics: None,
3818 defer_execute: false,
3819 },
3820 )
3821 .map_err(|error| match error {
3822 WasmExecutionError::Spawn(err) => WasmExecutionError::WarmupSpawn(err),
3823 other => other,
3824 })?;
3825 let mut internal_sync_rpc = WasmInternalSyncRpc {
3826 module_guest_paths: wasm_guest_module_paths(&resolved_module.specifier, &request.env),
3827 module_host_path: resolved_module.resolved_path.clone(),
3828 guest_cwd: wasm_guest_cwd(&request.env),
3829 host_cwd: request.cwd.clone(),
3830 sandbox_root: wasm_sandbox_root(&request.env),
3831 guest_path_mappings: wasm_guest_path_mappings(request),
3832 route_fs_through_sidecar: false,
3833 next_fd: 64,
3834 open_files: BTreeMap::new(),
3835 pending_events: VecDeque::new(),
3836 };
3837 let mut stdout = Vec::new();
3838 let mut stderr = Vec::new();
3839 let started = Instant::now();
3840
3841 loop {
3842 let poll_timeout = options.timeout.saturating_sub(started.elapsed());
3843 if poll_timeout.is_zero() {
3844 if let Err(error) = prewarm_execution.terminate() {
3845 eprintln!(
3846 "ERR_AGENTOS_WASM_PREWARM_TERMINATE: timed-out prewarm did not terminate cleanly: {error}"
3847 );
3848 }
3849 return Err(WasmExecutionError::WarmupTimeout(options.timeout));
3850 }
3851
3852 match prewarm_execution
3853 .poll_event(poll_timeout)
3854 .await
3855 .map_err(map_javascript_error)?
3856 {
3857 Some(JavascriptExecutionEvent::Stdout(chunk)) => {
3858 append_wasm_captured_output(&mut stdout, &chunk, "stdout")?;
3859 }
3860 Some(JavascriptExecutionEvent::Stderr(chunk)) => {
3861 append_wasm_captured_output(&mut stderr, &chunk, "stderr")?;
3862 }
3863 Some(JavascriptExecutionEvent::Exited(exit_code)) => {
3864 if exit_code != 0 {
3865 return Err(WasmExecutionError::WarmupFailed {
3866 exit_code,
3867 stderr: String::from_utf8_lossy(&stderr).into_owned(),
3868 });
3869 }
3870 break;
3871 }
3872 Some(JavascriptExecutionEvent::SyncRpcRequest(sync_request)) => {
3873 let handled = handle_internal_wasm_sync_rpc_request(
3874 &mut prewarm_execution,
3875 &mut internal_sync_rpc,
3876 &sync_request,
3877 )?;
3878 if !handled {
3879 return Err(WasmExecutionError::WarmupFailed {
3880 exit_code: 1,
3881 stderr: format!(
3882 "unexpected WebAssembly prewarm sync RPC request {} {} {:?}",
3883 sync_request.id, sync_request.method, sync_request.args
3884 ),
3885 });
3886 }
3887 }
3888 Some(JavascriptExecutionEvent::SignalState { .. }) => {}
3889 None => {
3890 if let Err(error) = prewarm_execution.terminate() {
3891 eprintln!(
3892 "ERR_AGENTOS_WASM_PREWARM_TERMINATE: timed-out prewarm did not terminate cleanly: {error}"
3893 );
3894 }
3895 return Err(WasmExecutionError::WarmupTimeout(options.timeout));
3896 }
3897 }
3898 }
3899
3900 let _ = stdout;
3901 fs::write(&marker_path, marker_contents).map_err(WasmExecutionError::PrepareWarmPath)?;
3902 Ok(warmup_metrics_line(
3903 debug_enabled,
3904 true,
3905 "executed",
3906 import_cache,
3907 &resolved_module.specifier,
3908 ))
3909}
3910
3911fn wasm_guest_module_paths(specifier: &str, env: &BTreeMap<String, String>) -> Vec<String> {
3912 let mut candidates = Vec::new();
3913 candidates.push(specifier.to_owned());
3914
3915 if specifier.starts_with('/') {
3916 candidates.push(normalize_guest_path(specifier));
3917 candidates.extend(mapped_guest_paths_for_host_path(Path::new(specifier), env));
3918 } else if !specifier.starts_with("file:") {
3919 let guest_cwd = wasm_guest_cwd(env);
3920 candidates.push(join_guest_path(&guest_cwd, specifier));
3921 }
3922
3923 candidates.sort();
3924 candidates.dedup();
3925 candidates
3926}
3927
3928fn wasm_guest_cwd(env: &BTreeMap<String, String>) -> String {
3929 env.get("PWD")
3930 .filter(|value| value.starts_with('/'))
3931 .cloned()
3932 .or_else(|| {
3933 env.get("HOME")
3934 .filter(|value| value.starts_with('/'))
3935 .cloned()
3936 })
3937 .unwrap_or_else(|| String::from(DEFAULT_WASM_GUEST_HOME))
3938}
3939
3940fn mapped_guest_paths_for_host_path(
3941 host_path: &Path,
3942 env: &BTreeMap<String, String>,
3943) -> Vec<String> {
3944 if !host_path.is_absolute() {
3945 return Vec::new();
3946 }
3947
3948 let mappings = env
3949 .get("AGENTOS_GUEST_PATH_MAPPINGS")
3950 .and_then(|value| serde_json::from_str::<Vec<Value>>(value).ok())
3951 .unwrap_or_default();
3952
3953 let mut candidates = Vec::new();
3954 for mapping in mappings {
3955 let Some(guest_root) = mapping.get("guestPath").and_then(Value::as_str) else {
3956 continue;
3957 };
3958 let Some(host_root) = mapping.get("hostPath").and_then(Value::as_str) else {
3959 continue;
3960 };
3961 let host_root = Path::new(host_root);
3962
3963 if let Ok(suffix) = host_path.strip_prefix(host_root) {
3964 candidates.push(join_guest_path(
3965 guest_root,
3966 &suffix.to_string_lossy().replace('\\', "/"),
3967 ));
3968 continue;
3969 }
3970
3971 let Ok(real_host_root) = host_root.canonicalize() else {
3972 continue;
3973 };
3974 if let Ok(suffix) = host_path.strip_prefix(&real_host_root) {
3975 candidates.push(join_guest_path(
3976 guest_root,
3977 &suffix.to_string_lossy().replace('\\', "/"),
3978 ));
3979 }
3980 }
3981
3982 candidates
3983}
3984
3985fn normalize_guest_path(path: &str) -> String {
3986 join_guest_path("/", path)
3987}
3988
3989fn join_guest_path(base: &str, suffix: &str) -> String {
3990 let mut segments = Vec::new();
3991 let mut absolute = false;
3992 for part in [base, suffix] {
3993 if part.starts_with('/') {
3994 absolute = true;
3995 }
3996 for segment in part.split('/') {
3997 match segment {
3998 "" | "." => {}
3999 ".." => {
4000 let _ = segments.pop();
4001 }
4002 value => segments.push(value),
4003 }
4004 }
4005 }
4006
4007 let joined = segments.join("/");
4008 if absolute {
4009 if joined.is_empty() {
4010 String::from("/")
4011 } else {
4012 format!("/{joined}")
4013 }
4014 } else if joined.is_empty() {
4015 String::from(".")
4016 } else {
4017 joined
4018 }
4019}
4020
4021fn module_path(
4022 context: &WasmContext,
4023 request: &StartWasmExecutionRequest,
4024) -> Result<String, WasmExecutionError> {
4025 match context.module_path.as_deref() {
4026 Some(module_path) => Ok(module_path.to_owned()),
4027 None => request
4028 .argv
4029 .first()
4030 .cloned()
4031 .ok_or(WasmExecutionError::MissingModulePath),
4032 }
4033}
4034
4035fn guest_visible_wasm_env(env: &BTreeMap<String, String>) -> BTreeMap<String, String> {
4036 let mut guest_env = env
4037 .iter()
4038 .filter(|(key, _)| !is_internal_wasm_guest_env_key(key))
4039 .map(|(key, value)| (key.clone(), value.clone()))
4040 .collect::<BTreeMap<_, _>>();
4041 let guest_cwd = wasm_guest_cwd(env);
4042 let guest_home = guest_env
4043 .get("HOME")
4044 .filter(|value| value.starts_with('/'))
4045 .cloned()
4046 .unwrap_or_else(|| guest_cwd.clone());
4047
4048 guest_env
4049 .entry(String::from("HOME"))
4050 .or_insert_with(|| guest_home.clone());
4051 guest_env
4052 .entry(String::from("PWD"))
4053 .or_insert_with(|| guest_cwd);
4054 guest_env
4055 .entry(String::from("USER"))
4056 .or_insert_with(|| String::from(DEFAULT_WASM_GUEST_USER));
4057 guest_env
4058 .entry(String::from("LOGNAME"))
4059 .or_insert_with(|| String::from(DEFAULT_WASM_GUEST_USER));
4060 guest_env
4061 .entry(String::from("SHELL"))
4062 .or_insert_with(|| String::from(DEFAULT_WASM_GUEST_SHELL));
4063 guest_env
4064 .entry(String::from("PATH"))
4065 .or_insert_with(|| String::from(DEFAULT_WASM_GUEST_PATH));
4066 guest_env
4067 .entry(String::from("TMPDIR"))
4068 .or_insert_with(|| String::from("/tmp"));
4069 guest_env
4070}
4071
4072fn wasm_guest_path_mappings(request: &StartWasmExecutionRequest) -> Vec<WasmGuestPathMapping> {
4073 let guest_cwd = wasm_guest_cwd(&request.env);
4074 let mut mappings = request
4075 .env
4076 .get("AGENTOS_GUEST_PATH_MAPPINGS")
4077 .and_then(|value| serde_json::from_str::<Vec<Value>>(value).ok())
4078 .unwrap_or_default()
4079 .into_iter()
4080 .filter_map(|mapping| {
4081 Some(WasmGuestPathMapping {
4082 guest_path: mapping.get("guestPath")?.as_str()?.to_owned(),
4083 host_path: PathBuf::from(mapping.get("hostPath")?.as_str()?),
4084 read_only: mapping
4085 .get("readOnly")
4086 .and_then(Value::as_bool)
4087 .unwrap_or(false),
4088 })
4089 })
4090 .collect::<Vec<_>>();
4091
4092 if let Some(sandbox_root) = wasm_sandbox_root(&request.env) {
4093 push_wasm_guest_path_mapping(&mut mappings, String::from("/"), sandbox_root);
4094 }
4095 push_wasm_guest_path_mapping(&mut mappings, guest_cwd, request.cwd.clone());
4096 push_wasm_guest_path_mapping(
4097 &mut mappings,
4098 String::from("/workspace"),
4099 request.cwd.clone(),
4100 );
4101 mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.guest_path.len()));
4102 mappings
4103}
4104
4105fn wasm_sandbox_root(env: &BTreeMap<String, String>) -> Option<PathBuf> {
4106 env.get(WASM_SANDBOX_ROOT_ENV)
4107 .filter(|value| Path::new(value.as_str()).is_absolute())
4108 .map(PathBuf::from)
4109}
4110
4111fn push_wasm_guest_path_mapping(
4112 mappings: &mut Vec<WasmGuestPathMapping>,
4113 guest_path: String,
4114 host_path: PathBuf,
4115) {
4116 if guest_path.is_empty() || !guest_path.starts_with('/') {
4117 return;
4118 }
4119 if mappings
4120 .iter()
4121 .any(|mapping| mapping.guest_path == guest_path)
4122 {
4123 return;
4124 }
4125 mappings.push(WasmGuestPathMapping {
4126 guest_path,
4127 host_path,
4128 read_only: false,
4129 });
4130}
4131
4132fn encode_wasm_guest_path_mappings(mappings: &[WasmGuestPathMapping]) -> String {
4133 serde_json::to_string(
4134 &mappings
4135 .iter()
4136 .map(|mapping| {
4137 json!({
4138 "guestPath": mapping.guest_path,
4139 "hostPath": mapping.host_path.to_string_lossy(),
4140 "readOnly": mapping.read_only,
4141 })
4142 })
4143 .collect::<Vec<_>>(),
4144 )
4145 .unwrap_or_else(|_| String::from("[]"))
4146}
4147
4148fn is_internal_wasm_guest_env_key(key: &str) -> bool {
4149 key.starts_with("AGENTOS_") || key.starts_with("NODE_SYNC_RPC_")
4150}
4151
4152fn warmup_marker_contents(resolved_module: &ResolvedWasmModule) -> String {
4153 let module_fingerprint = file_fingerprint(&resolved_module.resolved_path);
4154
4155 [
4156 env!("CARGO_PKG_NAME").to_string(),
4157 env!("CARGO_PKG_VERSION").to_string(),
4158 WASM_WARMUP_MARKER_VERSION.to_string(),
4159 resolved_module.specifier.clone(),
4160 resolved_module.resolved_path.display().to_string(),
4161 module_fingerprint,
4162 ]
4163 .join("\n")
4164}
4165
4166fn warmup_metrics_line(
4167 debug_enabled: bool,
4168 executed: bool,
4169 reason: &str,
4170 import_cache: &NodeImportCache,
4171 module_specifier: &str,
4172) -> Option<Vec<u8>> {
4173 if !debug_enabled {
4174 return None;
4175 }
4176
4177 Some(
4178 format!(
4179 "{WASM_WARMUP_METRICS_PREFIX}{{\"executed\":{},\"reason\":{},\"modulePath\":{},\"compileCacheDir\":{}}}\n",
4180 if executed { "true" } else { "false" },
4181 encode_json_string(reason),
4182 encode_json_string(module_specifier),
4183 encode_json_string(&import_cache.shared_compile_cache_dir().display().to_string()),
4184 )
4185 .into_bytes(),
4186 )
4187}
4188
4189fn resolve_wasm_execution_timeout(
4190 request: &StartWasmExecutionRequest,
4191) -> Result<Option<Duration>, WasmExecutionError> {
4192 Ok(request.limits.max_fuel.map(Duration::from_millis))
4205}
4206
4207fn resolve_wasm_stack_limit_bytes(
4211 request: &StartWasmExecutionRequest,
4212) -> Result<Option<u64>, WasmExecutionError> {
4213 match request.limits.max_stack_bytes {
4214 Some(0) => Err(WasmExecutionError::InvalidLimit(String::from(
4215 "wasm max stack bytes must be greater than zero",
4216 ))),
4217 Some(limit) => Err(WasmExecutionError::InvalidLimit(format!(
4218 "configured wasm max stack byte limit {limit} cannot be enforced by the V8 runner"
4219 ))),
4220 None => Ok(None),
4221 }
4222}
4223
4224fn resolve_wasm_prewarm_timeout(
4225 request: &StartWasmExecutionRequest,
4226) -> Result<Duration, WasmExecutionError> {
4227 Ok(Duration::from_millis(
4228 request
4229 .limits
4230 .prewarm_timeout_ms
4231 .filter(|value| *value > 0)
4232 .unwrap_or(DEFAULT_WASM_PREWARM_TIMEOUT_MS),
4233 ))
4234}
4235
4236fn resolve_wasm_module(
4237 context: &WasmContext,
4238 request: &StartWasmExecutionRequest,
4239) -> Result<ResolvedWasmModule, WasmExecutionError> {
4240 let specifier = module_path(context, request)?;
4241 let resolved_path = resolved_module_path(&specifier, &request.cwd);
4242 Ok(ResolvedWasmModule {
4243 specifier,
4244 resolved_path,
4245 })
4246}
4247
4248fn resolved_module_path(specifier: &str, cwd: &Path) -> PathBuf {
4249 resolve_path_like_specifier(cwd, specifier)
4250 .map(|path| path.canonicalize().unwrap_or(path))
4251 .unwrap_or_else(|| PathBuf::from(specifier))
4252}
4253
4254fn verify_wasm_module_header(
4267 resolved_module: &ResolvedWasmModule,
4268) -> Result<(), WasmExecutionError> {
4269 let resolved_path = &resolved_module.resolved_path;
4270 let metadata = fs::metadata(resolved_path).map_err(|error| {
4271 WasmExecutionError::InvalidModule(format!(
4272 "failed to stat {}: {error}",
4273 resolved_path.display()
4274 ))
4275 })?;
4276 if metadata.len() > MAX_WASM_MODULE_FILE_BYTES {
4277 return Err(WasmExecutionError::InvalidModule(format!(
4278 "module file size of {} bytes exceeds the configured parser cap of {} bytes",
4279 metadata.len(),
4280 MAX_WASM_MODULE_FILE_BYTES
4281 )));
4282 }
4283
4284 let mut file = fs::File::open(resolved_path).map_err(|error| {
4285 WasmExecutionError::InvalidModule(format!(
4286 "failed to open {}: {error}",
4287 resolved_path.display()
4288 ))
4289 })?;
4290 let mut header = [0u8; 4];
4291 let bytes_read = file.read(&mut header).map_err(|error| {
4292 WasmExecutionError::InvalidModule(format!(
4293 "failed to read header of {}: {error}",
4294 resolved_path.display()
4295 ))
4296 })?;
4297 let header = &header[..bytes_read];
4298 if header == b"\0asm" {
4299 return Ok(());
4300 }
4301
4302 let shell_shim = header.len() >= 2 && &header[..2] == b"#!";
4303 if let Some(format) = detect_native_binary_format(header) {
4304 return Err(WasmExecutionError::NativeBinaryNotSupported {
4305 path: resolved_path.clone(),
4306 header: header.to_vec(),
4307 format,
4308 });
4309 }
4310
4311 Err(WasmExecutionError::NonWasmBinary {
4312 path: resolved_path.clone(),
4313 header: header.to_vec(),
4314 shell_shim,
4315 })
4316}
4317
4318fn detect_native_binary_format(header: &[u8]) -> Option<NativeBinaryFormat> {
4319 if header.len() >= 4 && &header[..4] == b"\x7fELF" {
4320 return Some(NativeBinaryFormat::Elf);
4321 }
4322
4323 if header.starts_with(b"MZ") {
4324 return Some(NativeBinaryFormat::PeCoff);
4325 }
4326
4327 const MACH_O_MAGICS: [&[u8; 4]; 6] = [
4328 b"\xfe\xed\xfa\xce",
4329 b"\xce\xfa\xed\xfe",
4330 b"\xfe\xed\xfa\xcf",
4331 b"\xcf\xfa\xed\xfe",
4332 b"\xca\xfe\xba\xbe",
4333 b"\xbe\xba\xfe\xca",
4334 ];
4335 if header.len() >= 4 && MACH_O_MAGICS.iter().any(|magic| header[..4] == magic[..]) {
4336 return Some(NativeBinaryFormat::MachO);
4337 }
4338
4339 None
4340}
4341
4342fn warmup_guest_argv(
4343 resolved_module: &ResolvedWasmModule,
4344 request: &StartWasmExecutionRequest,
4345) -> Vec<String> {
4346 if !request.argv.is_empty() {
4347 return request.argv.clone();
4348 }
4349
4350 vec![resolved_module.specifier.clone()]
4351}
4352
4353fn wasm_memory_limit_bytes(
4354 request: &StartWasmExecutionRequest,
4355) -> Result<Option<u64>, WasmExecutionError> {
4356 Ok(request.limits.max_memory_bytes)
4357}
4358
4359fn wasm_stack_limit_bytes(
4360 request: &StartWasmExecutionRequest,
4361) -> Result<Option<u64>, WasmExecutionError> {
4362 resolve_wasm_stack_limit_bytes(request)
4363}
4364
4365#[cfg(test)]
4366fn wasm_memory_limit_pages(memory_limit_bytes: u64) -> Result<u32, WasmExecutionError> {
4367 let pages = memory_limit_bytes / WASM_PAGE_BYTES;
4368 u32::try_from(pages).map_err(|_| {
4369 WasmExecutionError::InvalidLimit(format!(
4370 "{WASM_MAX_MEMORY_BYTES_ENV}={memory_limit_bytes}: exceeds V8's wasm page limit range"
4371 ))
4372 })
4373}
4374
4375fn wasm_runner_heap_limit_mb(request: &StartWasmExecutionRequest) -> u32 {
4378 request
4379 .limits
4380 .runner_heap_limit_mb
4381 .filter(|value| *value > 0)
4382 .unwrap_or(DEFAULT_WASM_RUNNER_HEAP_LIMIT_MB)
4383}
4384
4385fn v8_warm_worker_count() -> usize {
4386 std::env::var("AGENTOS_V8_WARM_ISOLATES")
4387 .ok()
4388 .and_then(|value| value.parse::<usize>().ok())
4389 .unwrap_or(4)
4390}
4391
4392fn validate_module_limits(
4393 resolved_module: &ResolvedWasmModule,
4394 request: &StartWasmExecutionRequest,
4395) -> Result<(), WasmExecutionError> {
4396 let _stack_limit = resolve_wasm_stack_limit_bytes(request)?;
4399
4400 let Some(memory_limit) = wasm_memory_limit_bytes(request)? else {
4401 return Ok(());
4402 };
4403
4404 let resolved_path = &resolved_module.resolved_path;
4405 let metadata = fs::metadata(resolved_path).map_err(|error| {
4406 WasmExecutionError::InvalidModule(format!(
4407 "failed to stat {}: {error}",
4408 resolved_path.display()
4409 ))
4410 })?;
4411 if metadata.len() > MAX_WASM_MODULE_FILE_BYTES {
4412 return Err(WasmExecutionError::InvalidModule(format!(
4413 "module file size of {} bytes exceeds the configured parser cap of {} bytes",
4414 metadata.len(),
4415 MAX_WASM_MODULE_FILE_BYTES
4416 )));
4417 }
4418 let bytes = fs::read(resolved_path).map_err(|error| {
4419 WasmExecutionError::InvalidModule(format!(
4420 "failed to read {}: {error}",
4421 resolved_path.display()
4422 ))
4423 })?;
4424 let module_limits = extract_wasm_module_limits(&bytes)?;
4425
4426 if module_limits.imports_memory {
4427 return Err(WasmExecutionError::InvalidModule(String::from(
4428 "configured WebAssembly memory limit does not support imported memories yet",
4429 )));
4430 }
4431
4432 if let Some(initial_bytes) = module_limits.initial_memory_bytes {
4433 if initial_bytes > memory_limit {
4434 warn_limit_exhausted(
4435 TrackedLimit::WasmMemoryBytes,
4436 usize_saturating_from_u64(initial_bytes),
4437 usize_saturating_from_u64(memory_limit),
4438 );
4439 return Err(WasmExecutionError::InvalidModule(format!(
4440 "initial WebAssembly memory of {initial_bytes} bytes exceeds the configured limit of {memory_limit} bytes"
4441 )));
4442 }
4443 }
4444
4445 match module_limits.maximum_memory_bytes {
4446 Some(maximum_bytes) if maximum_bytes > memory_limit => {
4447 warn_limit_exhausted(
4448 TrackedLimit::WasmMemoryBytes,
4449 usize_saturating_from_u64(maximum_bytes),
4450 usize_saturating_from_u64(memory_limit),
4451 );
4452 Err(WasmExecutionError::InvalidModule(format!(
4453 "WebAssembly memory maximum of {maximum_bytes} bytes exceeds the configured limit of {memory_limit} bytes"
4454 )))
4455 }
4456 Some(_) => Ok(()),
4457 None => Ok(()),
4458 }
4459}
4460
4461fn duration_millis_saturating_usize(duration: Duration) -> usize {
4462 usize::try_from(duration.as_millis()).unwrap_or(usize::MAX)
4463}
4464
4465fn usize_saturating_from_u64(value: u64) -> usize {
4466 usize::try_from(value).unwrap_or(usize::MAX)
4467}
4468
4469#[derive(Debug, Default)]
4470struct WasmModuleLimits {
4471 imports_memory: bool,
4472 initial_memory_bytes: Option<u64>,
4473 maximum_memory_bytes: Option<u64>,
4474}
4475
4476fn extract_wasm_module_limits(bytes: &[u8]) -> Result<WasmModuleLimits, WasmExecutionError> {
4477 if bytes.len() < 8 || &bytes[..4] != b"\0asm" {
4478 return Err(WasmExecutionError::InvalidModule(String::from(
4479 "module is not a valid WebAssembly binary",
4480 )));
4481 }
4482
4483 let mut offset = 8;
4484 let mut limits = WasmModuleLimits::default();
4485
4486 while offset < bytes.len() {
4487 let section_id = bytes[offset];
4488 offset += 1;
4489 let section_size = read_varuint_usize(bytes, &mut offset, "section size")?;
4490 let section_end = offset.checked_add(section_size).ok_or_else(|| {
4491 WasmExecutionError::InvalidModule(String::from("section size overflow"))
4492 })?;
4493 if section_end > bytes.len() {
4494 return Err(WasmExecutionError::InvalidModule(String::from(
4495 "section extends past end of module",
4496 )));
4497 }
4498
4499 match section_id {
4500 2 => {
4501 let mut cursor = offset;
4502 let import_count = read_varuint_usize(bytes, &mut cursor, "import count")?;
4503 if import_count > MAX_WASM_IMPORT_SECTION_ENTRIES {
4504 return Err(WasmExecutionError::InvalidModule(format!(
4505 "import section contains {import_count} entries, which exceeds the parser cap of {MAX_WASM_IMPORT_SECTION_ENTRIES}"
4506 )));
4507 }
4508 for _ in 0..import_count {
4509 skip_name(bytes, &mut cursor)?;
4510 skip_name(bytes, &mut cursor)?;
4511 let kind = read_byte(bytes, &mut cursor)?;
4512 match kind {
4513 0x02 => {
4514 let _ = read_memory_limits(bytes, &mut cursor)?;
4515 limits.imports_memory = true;
4516 }
4517 0x00 => {
4518 let _ = read_varuint(bytes, &mut cursor)?;
4519 }
4520 0x01 => {
4521 skip_table_type(bytes, &mut cursor)?;
4522 }
4523 0x03 => {
4524 let _ = read_byte(bytes, &mut cursor)?;
4525 let _ = read_byte(bytes, &mut cursor)?;
4526 }
4527 other => {
4528 return Err(WasmExecutionError::InvalidModule(format!(
4529 "unsupported import kind {other}"
4530 )));
4531 }
4532 }
4533 }
4534 }
4535 5 => {
4536 let mut cursor = offset;
4537 let memory_count = read_varuint_usize(bytes, &mut cursor, "memory count")?;
4538 if memory_count > MAX_WASM_MEMORY_SECTION_ENTRIES {
4539 return Err(WasmExecutionError::InvalidModule(format!(
4540 "memory section contains {memory_count} entries, which exceeds the parser cap of {MAX_WASM_MEMORY_SECTION_ENTRIES}"
4541 )));
4542 }
4543 if memory_count > 0 {
4544 let (initial_pages, maximum_pages) = read_memory_limits(bytes, &mut cursor)?;
4545 limits.initial_memory_bytes =
4546 Some(initial_pages.saturating_mul(WASM_PAGE_BYTES));
4547 limits.maximum_memory_bytes =
4548 maximum_pages.map(|pages| pages.saturating_mul(WASM_PAGE_BYTES));
4549 }
4550 }
4551 _ => {}
4552 }
4553
4554 offset = section_end;
4555 }
4556
4557 Ok(limits)
4558}
4559
4560fn read_memory_limits(
4561 bytes: &[u8],
4562 offset: &mut usize,
4563) -> Result<(u64, Option<u64>), WasmExecutionError> {
4564 let flags = read_varuint(bytes, offset)?;
4565 let initial = read_varuint(bytes, offset)?;
4566 let maximum = if flags & 0x01 != 0 {
4567 Some(read_varuint(bytes, offset)?)
4568 } else {
4569 None
4570 };
4571 Ok((initial, maximum))
4572}
4573
4574fn skip_name(bytes: &[u8], offset: &mut usize) -> Result<(), WasmExecutionError> {
4575 let length = read_varuint_usize(bytes, offset, "name length")?;
4576 let end = offset
4577 .checked_add(length)
4578 .ok_or_else(|| WasmExecutionError::InvalidModule(String::from("name length overflow")))?;
4579 if end > bytes.len() {
4580 return Err(WasmExecutionError::InvalidModule(String::from(
4581 "name extends past end of module",
4582 )));
4583 }
4584 *offset = end;
4585 Ok(())
4586}
4587
4588fn skip_table_type(bytes: &[u8], offset: &mut usize) -> Result<(), WasmExecutionError> {
4589 let _ = read_byte(bytes, offset)?;
4590 let flags = read_varuint(bytes, offset)?;
4591 let _ = read_varuint(bytes, offset)?;
4592 if flags & 0x01 != 0 {
4593 let _ = read_varuint(bytes, offset)?;
4594 }
4595 Ok(())
4596}
4597
4598fn read_byte(bytes: &[u8], offset: &mut usize) -> Result<u8, WasmExecutionError> {
4599 let Some(byte) = bytes.get(*offset).copied() else {
4600 return Err(WasmExecutionError::InvalidModule(String::from(
4601 "unexpected end of module",
4602 )));
4603 };
4604 *offset += 1;
4605 Ok(byte)
4606}
4607
4608fn read_varuint(bytes: &[u8], offset: &mut usize) -> Result<u64, WasmExecutionError> {
4609 let mut shift = 0_u32;
4610 let mut value = 0_u64;
4611 let mut encoded_bytes = 0_usize;
4612
4613 loop {
4614 let byte = read_byte(bytes, offset)?;
4615 encoded_bytes += 1;
4616 if encoded_bytes > MAX_WASM_VARUINT_BYTES {
4617 return Err(WasmExecutionError::InvalidModule(format!(
4618 "varuint exceeds the parser cap of {MAX_WASM_VARUINT_BYTES} bytes"
4619 )));
4620 }
4621 value |= u64::from(byte & 0x7f) << shift;
4622 if byte & 0x80 == 0 {
4623 return Ok(value);
4624 }
4625 if encoded_bytes == MAX_WASM_VARUINT_BYTES {
4626 return Err(WasmExecutionError::InvalidModule(format!(
4627 "varuint exceeds the parser cap of {MAX_WASM_VARUINT_BYTES} bytes"
4628 )));
4629 }
4630 shift = shift.saturating_add(7);
4631 if shift >= 64 {
4632 return Err(WasmExecutionError::InvalidModule(String::from(
4633 "varuint is too large",
4634 )));
4635 }
4636 }
4637}
4638
4639fn read_varuint_usize(
4640 bytes: &[u8],
4641 offset: &mut usize,
4642 label: &str,
4643) -> Result<usize, WasmExecutionError> {
4644 let value = read_varuint(bytes, offset)?;
4645 usize::try_from(value).map_err(|_| {
4646 WasmExecutionError::InvalidModule(format!(
4647 "{label} of {value} exceeds platform usize range"
4648 ))
4649 })
4650}
4651
4652impl From<NodeSignalDispositionAction> for WasmSignalDispositionAction {
4653 fn from(value: NodeSignalDispositionAction) -> Self {
4654 match value {
4655 NodeSignalDispositionAction::Default => Self::Default,
4656 NodeSignalDispositionAction::Ignore => Self::Ignore,
4657 NodeSignalDispositionAction::User => Self::User,
4658 }
4659 }
4660}
4661
4662impl From<NodeSignalHandlerRegistration> for WasmSignalHandlerRegistration {
4663 fn from(value: NodeSignalHandlerRegistration) -> Self {
4664 Self {
4665 action: value.action.into(),
4666 mask: value.mask,
4667 flags: value.flags,
4668 }
4669 }
4670}
4671
4672fn resolve_path_like_specifier(cwd: &Path, specifier: &str) -> Option<PathBuf> {
4673 if specifier.starts_with("file://") {
4674 return Some(PathBuf::from(specifier.trim_start_matches("file://")));
4675 }
4676 if specifier.starts_with("file:") {
4677 return Some(PathBuf::from(specifier.trim_start_matches("file:")));
4678 }
4679 if specifier.starts_with('/') {
4680 return Some(PathBuf::from(specifier));
4681 }
4682 if specifier.starts_with("./") || specifier.starts_with("../") {
4683 return Some(cwd.join(specifier));
4684 }
4685
4686 None
4687}
4688
4689#[cfg(test)]
4690mod tests {
4691 use super::{
4692 build_wasm_internal_env, build_wasm_runner_bootstrap, max_cbor_byte_string_payload_bytes,
4693 open_wasm_guest_file, resolve_wasm_execution_timeout, resolve_wasm_prewarm_timeout,
4694 resolve_wasm_stack_limit_bytes, resolved_module_path, translate_wasm_guest_path,
4695 translate_wasm_host_symlink_target, wasm_guest_module_paths, wasm_host_path_is_read_only,
4696 wasm_memory_limit_bytes, wasm_memory_limit_pages, wasm_mutation_touches_read_only_mapping,
4697 wasm_read_only_filesystem_error, wasm_runner_base_env, wasm_runner_heap_limit_mb,
4698 wasm_runner_javascript_limits, wasm_sandbox_root, wasm_snapshot_runner_base_env,
4699 wasm_sync_read_length, wasm_sync_rpc_error_code,
4700 wasm_sync_rpc_method_routes_through_sidecar_kernel, CreateWasmContextRequest,
4701 GuestRuntimeConfig, JavascriptSyncRpcRequest, ResolvedWasmModule,
4702 StartWasmExecutionRequest, Value, WasmExecutionEngine, WasmExecutionError,
4703 WasmExecutionLimits, WasmInternalSyncRpc, WasmPermissionTier,
4704 DEFAULT_WASM_PREWARM_TIMEOUT_MS, DEFAULT_WASM_RUNNER_HEAP_LIMIT_MB,
4705 NODE_WASI_MODULE_SOURCE, WASM_CAPTURED_OUTPUT_LIMIT_BYTES,
4706 WASM_INTERNAL_MAX_STACK_BYTES_ENV, WASM_MAX_FUEL_ENV, WASM_MAX_MEMORY_BYTES_ENV,
4707 WASM_MAX_MODULE_FILE_BYTES_ENV, WASM_MAX_SPAWN_FILE_ACTIONS_ENV,
4708 WASM_MAX_SPAWN_FILE_ACTION_BYTES_ENV, WASM_MAX_STACK_BYTES_ENV, WASM_PAGE_BYTES,
4709 WASM_PROCESS_SYNC_RPC_RESPONSE_BYTES, WASM_SANDBOX_ROOT_ENV,
4710 WASM_SIDECAR_ROUTED_FS_SYNC_METHODS, WASM_SYNC_READ_LIMIT_BYTES,
4711 };
4712 use std::collections::{BTreeMap, BTreeSet, VecDeque};
4713 use std::fs;
4714 use std::os::unix::fs::symlink;
4715 use std::path::{Path, PathBuf};
4716 use std::time::Duration;
4717 use tempfile::tempdir;
4718
4719 #[test]
4720 fn wasm_runner_forwards_vm_reactor_limits_to_javascript() {
4721 let limits = WasmExecutionLimits {
4722 reactor_work_quantum: Some(17),
4723 bridge_call_timeout_ms: Some(12_345),
4724 ..WasmExecutionLimits::default()
4725 };
4726 let javascript = wasm_runner_javascript_limits(&limits, 192);
4727
4728 assert_eq!(javascript.v8_heap_limit_mb, Some(192));
4729 assert_eq!(javascript.reactor_work_quantum, Some(17));
4730 assert_eq!(javascript.bridge_call_timeout_ms, Some(12_345));
4731 }
4732
4733 #[test]
4734 fn wasm_process_reads_fit_the_encoded_bridge_response_budget() {
4735 let raw_limit = max_cbor_byte_string_payload_bytes(WASM_PROCESS_SYNC_RPC_RESPONSE_BYTES);
4736 assert_eq!(raw_limit, 256 * 1024 - 5);
4737 assert_eq!(
4738 agentos_bridge::bridge_contract()
4739 .response_max_bytes
4740 .get("_processWasmSyncRpc")
4741 .copied(),
4742 Some(WASM_PROCESS_SYNC_RPC_RESPONSE_BYTES)
4743 );
4744
4745 let bootstrap = build_wasm_runner_bootstrap(&BTreeMap::new(), None);
4746 assert!(bootstrap.contains(&format!(
4747 "const __agentOSWasmSyncRpcReadPayloadBytes = {raw_limit};"
4748 )));
4749 let runner = include_str!("../assets/runners/wasm-runner.mjs");
4750 assert!(runner.contains("boundedWasmSyncRpcReadLength("));
4751 assert!(runner.contains("callSyncRpc('process.fd_read'"));
4752 assert!(runner.contains("callSyncRpc('process.fd_pread'"));
4753 }
4754
4755 #[test]
4756 fn dispose_context_reclaims_wasm_and_nested_javascript_metadata() {
4757 let mut engine = WasmExecutionEngine::default();
4758 let baseline = (
4759 engine.context_count_for_test(),
4760 engine.javascript_context_count_for_test(),
4761 );
4762 let context = engine.create_context(CreateWasmContextRequest {
4763 vm_id: String::from("vm-wasm-context-dispose"),
4764 module_path: None,
4765 });
4766 assert_eq!(engine.context_count_for_test(), baseline.0 + 1);
4767 assert_eq!(engine.javascript_context_count_for_test(), baseline.1 + 1);
4768
4769 assert!(engine.dispose_context(&context.context_id));
4770 assert_eq!(
4771 (
4772 engine.context_count_for_test(),
4773 engine.javascript_context_count_for_test(),
4774 ),
4775 baseline
4776 );
4777 }
4778
4779 fn request_with_env(cwd: &Path, env: BTreeMap<String, String>) -> StartWasmExecutionRequest {
4780 let parse = |key: &str| env.get(key).and_then(|value| value.parse::<u64>().ok());
4784 let limits = WasmExecutionLimits {
4785 max_fuel: parse(WASM_MAX_FUEL_ENV),
4786 max_memory_bytes: parse(WASM_MAX_MEMORY_BYTES_ENV),
4787 max_stack_bytes: parse(WASM_MAX_STACK_BYTES_ENV),
4788 max_module_file_bytes: None,
4789 max_spawn_file_actions: None,
4790 max_spawn_file_action_bytes: None,
4791 prewarm_timeout_ms: None,
4792 max_open_fds: None,
4793 max_sockets: None,
4794 max_blocking_read_ms: None,
4795 runner_heap_limit_mb: None,
4796 runner_cpu_time_limit_ms: None,
4797 reactor_work_quantum: None,
4798 bridge_call_timeout_ms: None,
4799 };
4800 StartWasmExecutionRequest {
4801 limits,
4802 guest_runtime: GuestRuntimeConfig::default(),
4803 vm_id: String::from("vm-wasm"),
4804 context_id: String::from("ctx-wasm"),
4805 argv: Vec::new(),
4806 env,
4807 cwd: cwd.to_path_buf(),
4808 permission_tier: WasmPermissionTier::Full,
4809 }
4810 }
4811
4812 fn wasi_imports_from_source(source: &str) -> BTreeSet<String> {
4813 let table_start = source
4814 .find("this.wasiImport = {")
4815 .expect("WASI source should define a wasiImport table");
4816 let table_body = &source[table_start + "this.wasiImport = {".len()..];
4817 let table_end = table_body
4818 .find("\n };")
4819 .expect("WASI source should close the wasiImport table");
4820
4821 table_body[..table_end]
4822 .lines()
4823 .filter_map(|line| {
4824 let (name, _) = line.trim_start().split_once(':')?;
4825 name.chars()
4826 .all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '_')
4827 .then(|| name.to_string())
4828 })
4829 .collect()
4830 }
4831
4832 fn wasm_sync_rpc_request(method: &str) -> JavascriptSyncRpcRequest {
4833 JavascriptSyncRpcRequest {
4834 id: 1,
4835 method: method.to_string(),
4836 args: Vec::new(),
4837 raw_bytes_args: Default::default(),
4838 }
4839 }
4840
4841 fn request_with_typed_limits_and_misleading_env(
4844 limits: WasmExecutionLimits,
4845 ) -> StartWasmExecutionRequest {
4846 StartWasmExecutionRequest {
4847 limits,
4848 guest_runtime: GuestRuntimeConfig::default(),
4849 vm_id: String::from("vm-wasm"),
4850 context_id: String::from("ctx-wasm"),
4851 argv: Vec::new(),
4852 env: BTreeMap::from([
4855 (String::from(WASM_MAX_FUEL_ENV), String::from("999999")),
4856 (
4857 String::from(WASM_MAX_MEMORY_BYTES_ENV),
4858 String::from("999999"),
4859 ),
4860 (
4861 String::from(WASM_MAX_STACK_BYTES_ENV),
4862 String::from("999999"),
4863 ),
4864 (
4865 String::from("AGENTOS_WASM_PREWARM_TIMEOUT_MS"),
4866 String::from("999999"),
4867 ),
4868 (
4869 String::from("AGENTOS_WASM_RUNNER_HEAP_LIMIT_MB"),
4870 String::from("999999"),
4871 ),
4872 (
4873 String::from(WASM_MAX_SPAWN_FILE_ACTIONS_ENV),
4874 String::from("999999"),
4875 ),
4876 (
4877 String::from(WASM_MAX_SPAWN_FILE_ACTION_BYTES_ENV),
4878 String::from("999999"),
4879 ),
4880 ]),
4881 cwd: PathBuf::from("/tmp"),
4882 permission_tier: WasmPermissionTier::Full,
4883 }
4884 }
4885
4886 #[test]
4887 fn wasm_limits_are_read_from_typed_fields_and_env_is_inert() {
4888 let request = request_with_typed_limits_and_misleading_env(WasmExecutionLimits {
4889 max_fuel: Some(25),
4890 max_memory_bytes: Some(65_536),
4891 max_stack_bytes: Some(131_072),
4892 max_module_file_bytes: Some(262_144),
4893 max_spawn_file_actions: Some(7),
4894 max_spawn_file_action_bytes: Some(321),
4895 prewarm_timeout_ms: Some(750),
4896 max_open_fds: None,
4897 max_sockets: None,
4898 max_blocking_read_ms: None,
4899 runner_heap_limit_mb: Some(512),
4900 runner_cpu_time_limit_ms: Some(1_234),
4901 reactor_work_quantum: Some(64),
4902 bridge_call_timeout_ms: Some(30_000),
4903 });
4904
4905 assert_eq!(
4906 resolve_wasm_execution_timeout(&request).expect("fuel timeout"),
4907 Some(Duration::from_millis(25)),
4908 "fuel must come from the typed wire limit, not AGENTOS_WASM_MAX_FUEL"
4909 );
4910 assert_eq!(
4911 wasm_memory_limit_bytes(&request).expect("memory limit"),
4912 Some(65_536),
4913 "memory must come from the typed wire limit, not AGENTOS_WASM_MAX_MEMORY_BYTES"
4914 );
4915 let stack_error = resolve_wasm_stack_limit_bytes(&request)
4916 .expect_err("an unenforceable stack limit must fail closed");
4917 assert!(
4918 stack_error.to_string().contains("131072"),
4919 "the typed error must name the configured stack limit: {stack_error}"
4920 );
4921 assert_eq!(
4922 resolve_wasm_prewarm_timeout(&request).expect("prewarm timeout"),
4923 Duration::from_millis(750),
4924 "prewarm timeout must come from the typed wire limit, not AGENTOS_WASM_PREWARM_TIMEOUT_MS"
4925 );
4926 assert_eq!(
4927 wasm_runner_heap_limit_mb(&request),
4928 512,
4929 "runner heap must come from the typed wire limit, not AGENTOS_WASM_RUNNER_HEAP_LIMIT_MB"
4930 );
4931 assert_eq!(
4932 wasm_runner_javascript_limits(&request.limits, wasm_runner_heap_limit_mb(&request))
4933 .cpu_time_limit_ms,
4934 Some(1_234),
4935 );
4936 }
4937
4938 #[test]
4939 fn wasm_limits_default_to_bounded_timeout_when_unset_even_with_env_present() {
4940 let request = request_with_typed_limits_and_misleading_env(WasmExecutionLimits::default());
4944
4945 assert_eq!(
4946 resolve_wasm_execution_timeout(&request).expect("fuel"),
4947 None
4948 );
4949 assert_eq!(wasm_memory_limit_bytes(&request).expect("memory"), None);
4950 assert_eq!(
4951 resolve_wasm_stack_limit_bytes(&request).expect("stack"),
4952 None
4953 );
4954 assert_eq!(
4955 resolve_wasm_prewarm_timeout(&request).expect("prewarm"),
4956 Duration::from_millis(DEFAULT_WASM_PREWARM_TIMEOUT_MS)
4957 );
4958 assert_eq!(
4959 wasm_runner_heap_limit_mb(&request),
4960 DEFAULT_WASM_RUNNER_HEAP_LIMIT_MB
4961 );
4962 }
4963
4964 #[test]
4965 fn wasm_internal_env_scrubs_migrated_limit_env_keys() {
4966 let request = request_with_typed_limits_and_misleading_env(WasmExecutionLimits {
4967 max_fuel: Some(25),
4968 max_memory_bytes: Some(65_536),
4969 max_stack_bytes: Some(131_072),
4970 max_module_file_bytes: Some(262_144),
4971 max_spawn_file_actions: Some(7),
4972 max_spawn_file_action_bytes: Some(321),
4973 prewarm_timeout_ms: Some(750),
4974 max_open_fds: None,
4975 max_sockets: None,
4976 max_blocking_read_ms: None,
4977 runner_heap_limit_mb: Some(512),
4978 runner_cpu_time_limit_ms: Some(1_234),
4979 reactor_work_quantum: Some(64),
4980 bridge_call_timeout_ms: Some(30_000),
4981 });
4982 let resolved_module = ResolvedWasmModule {
4983 specifier: String::from("./guest.wasm"),
4984 resolved_path: PathBuf::from("/tmp/guest.wasm"),
4985 };
4986
4987 let internal_env =
4988 build_wasm_internal_env(&resolved_module, &request, 1_234, false).expect("env");
4989
4990 assert_eq!(
4991 internal_env.get(WASM_MAX_MEMORY_BYTES_ENV),
4992 Some(&String::from("65536"))
4993 );
4994 assert_eq!(
4995 internal_env.get(WASM_MAX_MODULE_FILE_BYTES_ENV),
4996 Some(&String::from("262144"))
4997 );
4998 assert_eq!(
4999 internal_env.get(WASM_MAX_SPAWN_FILE_ACTIONS_ENV),
5000 Some(&String::from("7"))
5001 );
5002 assert_eq!(
5003 internal_env.get(WASM_MAX_SPAWN_FILE_ACTION_BYTES_ENV),
5004 Some(&String::from("321"))
5005 );
5006 assert_eq!(
5007 internal_env.get(WASM_INTERNAL_MAX_STACK_BYTES_ENV),
5008 Some(&String::from("131072"))
5009 );
5010 assert!(!internal_env.contains_key(WASM_MAX_STACK_BYTES_ENV));
5011 assert!(!internal_env.contains_key(WASM_MAX_FUEL_ENV));
5012 assert!(!internal_env.contains_key("AGENTOS_WASM_PREWARM_TIMEOUT_MS"));
5013 assert!(!internal_env.contains_key("AGENTOS_WASM_RUNNER_HEAP_LIMIT_MB"));
5014 }
5015
5016 #[test]
5017 fn wasm_runner_base_env_scrubs_migrated_limit_env_keys() {
5018 let mut request = request_with_typed_limits_and_misleading_env(WasmExecutionLimits {
5019 max_fuel: Some(25),
5020 max_memory_bytes: Some(65_536),
5021 max_stack_bytes: Some(131_072),
5022 max_module_file_bytes: Some(262_144),
5023 max_spawn_file_actions: Some(7),
5024 max_spawn_file_action_bytes: Some(321),
5025 prewarm_timeout_ms: Some(750),
5026 max_open_fds: None,
5027 max_sockets: None,
5028 max_blocking_read_ms: None,
5029 runner_heap_limit_mb: Some(512),
5030 runner_cpu_time_limit_ms: Some(1_234),
5031 reactor_work_quantum: Some(64),
5032 bridge_call_timeout_ms: Some(30_000),
5033 });
5034 request
5035 .env
5036 .insert(String::from("USER_VISIBLE"), String::from("kept"));
5037 request
5038 .env
5039 .insert(String::from("AGENTOS_TRACE_ID"), String::from("kept"));
5040
5041 let env = wasm_runner_base_env(&request);
5042
5043 assert_eq!(env.get("USER_VISIBLE"), Some(&String::from("kept")));
5044 assert_eq!(env.get("AGENTOS_TRACE_ID"), Some(&String::from("kept")));
5045 assert!(!env.contains_key(WASM_MAX_FUEL_ENV));
5046 assert!(!env.contains_key(WASM_MAX_MEMORY_BYTES_ENV));
5047 assert!(!env.contains_key(WASM_MAX_MODULE_FILE_BYTES_ENV));
5048 assert!(!env.contains_key(WASM_MAX_STACK_BYTES_ENV));
5049 assert!(!env.contains_key("AGENTOS_WASM_PREWARM_TIMEOUT_MS"));
5050 assert!(!env.contains_key("AGENTOS_WASM_RUNNER_HEAP_LIMIT_MB"));
5051 }
5052
5053 #[test]
5054 fn wasm_snapshot_runner_base_env_scrubs_internal_and_migrated_limit_env_keys() {
5055 let mut request = request_with_typed_limits_and_misleading_env(WasmExecutionLimits {
5056 max_fuel: Some(25),
5057 max_memory_bytes: Some(65_536),
5058 max_stack_bytes: Some(131_072),
5059 max_module_file_bytes: Some(262_144),
5060 max_spawn_file_actions: Some(7),
5061 max_spawn_file_action_bytes: Some(321),
5062 prewarm_timeout_ms: Some(750),
5063 max_open_fds: None,
5064 max_sockets: None,
5065 max_blocking_read_ms: None,
5066 runner_heap_limit_mb: Some(512),
5067 runner_cpu_time_limit_ms: Some(1_234),
5068 reactor_work_quantum: Some(64),
5069 bridge_call_timeout_ms: Some(30_000),
5070 });
5071 request
5072 .env
5073 .insert(String::from("USER_VISIBLE"), String::from("kept"));
5074 request.env.insert(
5075 String::from("NODE_SYNC_RPC_WAIT_TIMEOUT_MS"),
5076 String::from("999"),
5077 );
5078
5079 let env = wasm_snapshot_runner_base_env(&request);
5080
5081 assert_eq!(env.get("USER_VISIBLE"), Some(&String::from("kept")));
5082 assert!(!env.contains_key("NODE_SYNC_RPC_WAIT_TIMEOUT_MS"));
5083 assert!(!env.contains_key(WASM_MAX_FUEL_ENV));
5084 assert!(!env.contains_key(WASM_MAX_MEMORY_BYTES_ENV));
5085 assert!(!env.contains_key(WASM_MAX_STACK_BYTES_ENV));
5086 assert!(!env.contains_key("AGENTOS_WASM_PREWARM_TIMEOUT_MS"));
5087 assert!(!env.contains_key("AGENTOS_WASM_RUNNER_HEAP_LIMIT_MB"));
5088 }
5089
5090 #[test]
5091 fn wasm_stack_limit_of_zero_is_rejected() {
5092 let request = request_with_typed_limits_and_misleading_env(WasmExecutionLimits {
5093 max_stack_bytes: Some(0),
5094 ..WasmExecutionLimits::default()
5095 });
5096
5097 assert!(
5098 resolve_wasm_stack_limit_bytes(&request).is_err(),
5099 "a zero stack cap must fail closed rather than be silently dropped"
5100 );
5101 }
5102
5103 #[test]
5104 fn resolved_module_path_canonicalizes_path_like_specifiers() {
5105 let temp = tempdir().expect("create temp dir");
5106 let real = temp.path().join("real.wasm");
5107 let alias = temp.path().join("alias.wasm");
5108 fs::write(&real, b"\0asm\x01\0\0\0").expect("write wasm file");
5109 symlink(&real, &alias).expect("create wasm symlink");
5110
5111 let resolved = resolved_module_path("./alias.wasm", temp.path());
5112
5113 assert_eq!(
5114 resolved,
5115 real.canonicalize().expect("canonicalize wasm target")
5116 );
5117 }
5118
5119 #[test]
5120 fn wasm_prewarm_timeout_is_separate_from_execution_timeout() {
5121 let temp = tempdir().expect("create temp dir");
5122 let mut request = request_with_env(
5123 temp.path(),
5124 BTreeMap::from([(String::from(WASM_MAX_FUEL_ENV), String::from("25"))]),
5125 );
5126 request.limits.prewarm_timeout_ms = Some(750);
5127
5128 assert_eq!(
5129 resolve_wasm_execution_timeout(&request).expect("execution timeout"),
5130 Some(Duration::from_millis(25))
5131 );
5132 assert_eq!(
5133 resolve_wasm_prewarm_timeout(&request).expect("prewarm timeout"),
5134 Duration::from_millis(750)
5135 );
5136 }
5137
5138 #[test]
5142 fn wasm_execution_timeout_is_unset_without_fuel_budget() {
5143 let temp = tempdir().expect("create temp dir");
5144 let request = request_with_env(temp.path(), BTreeMap::new());
5145
5146 let timeout = resolve_wasm_execution_timeout(&request)
5147 .expect("execution timeout resolves without fuel env");
5148
5149 assert_eq!(
5150 timeout, None,
5151 "no explicit fuel budget means no wall-clock timeout; the runner \
5152 isolate's TRUE-CPU budget (default 30s active CPU) is the bound \
5153 that terminates an infinite-loop module (F-004), so an idle \
5154 interactive guest is not killed on wall time"
5155 );
5156 }
5157
5158 #[test]
5159 fn wasm_captured_output_rejects_output_over_limit() {
5160 let mut stdout = vec![b'x'; WASM_CAPTURED_OUTPUT_LIMIT_BYTES - 1];
5161 super::append_wasm_captured_output(&mut stdout, b"y", "stdout").expect("fill to limit");
5162 assert_eq!(stdout.len(), WASM_CAPTURED_OUTPUT_LIMIT_BYTES);
5163
5164 let error = super::append_wasm_captured_output(&mut stdout, b"z", "stdout")
5165 .expect_err("captured output over limit should fail");
5166 assert!(matches!(
5167 error,
5168 WasmExecutionError::OutputBufferExceeded {
5169 stream: "stdout",
5170 limit: WASM_CAPTURED_OUTPUT_LIMIT_BYTES,
5171 }
5172 ));
5173 }
5174
5175 #[test]
5176 fn wasm_sync_read_length_rejects_oversized_guest_lengths() {
5177 assert_eq!(
5178 wasm_sync_read_length(Some(WASM_SYNC_READ_LIMIT_BYTES as u64))
5179 .expect("max read length should be accepted"),
5180 WASM_SYNC_READ_LIMIT_BYTES
5181 );
5182
5183 let error = wasm_sync_read_length(Some(WASM_SYNC_READ_LIMIT_BYTES as u64 + 1))
5184 .expect_err("oversized read length should fail before allocation");
5185 assert!(
5186 matches!(error, WasmExecutionError::InvalidLimit(message) if message.contains("fs.readSync length"))
5187 );
5188 }
5189
5190 #[test]
5191 fn wasm_bytes_arg_rejects_payloads_over_limit_before_decode() {
5192 let mut payload = serde_json::Map::new();
5193 payload.insert(
5194 String::from("base64"),
5195 Value::String(String::from("YWJjZA==")),
5196 );
5197
5198 let error =
5199 super::decode_wasm_bytes_arg(Some(&Value::Object(payload)), "fs.writeSync bytes", 3)
5200 .expect_err("decoded bytes over limit should fail before allocation");
5201
5202 assert!(matches!(
5203 error,
5204 WasmExecutionError::OutputBufferExceeded {
5205 stream: "fs.writeSync bytes",
5206 limit: 3,
5207 }
5208 ));
5209 }
5210
5211 #[test]
5212 fn wasm_runner_bootstrap_caps_wasi_iov_lengths_before_allocation() {
5213 let bootstrap = build_wasm_runner_bootstrap(&BTreeMap::new(), None);
5214
5215 assert!(bootstrap.contains("const __agentOSWasmSyncReadLimitBytes ="));
5219 assert!(bootstrap.contains(&format!(": {WASM_SYNC_READ_LIMIT_BYTES};")));
5220 assert!(!bootstrap.contains("__AGENTOS_WASM_SYNC_READ_LIMIT_BYTES__"));
5221 assert!(bootstrap.contains("_boundedIovLength(iovs, iovsLen)"));
5222 assert!(bootstrap.contains("const totalLength = this._boundedIovLength(iovs, iovsLen);\n const view = this._memoryView();"));
5223 assert!(bootstrap.contains("return Buffer.concat(chunks, totalLength);"));
5224 assert!(bootstrap.contains("const totalLength = this._boundedIovLength(iovs, iovsLen);"));
5225 assert!(!bootstrap.contains("const totalLength = (() => {"));
5226 }
5227
5228 #[test]
5229 fn wasi_preview1_import_manifest_matches_native_runner() {
5230 let expected: BTreeSet<String> = serde_json::from_str::<Vec<String>>(include_str!(
5231 "../assets/wasi-preview1-imports.json"
5232 ))
5233 .expect("parse WASI preview1 import manifest")
5234 .into_iter()
5235 .collect();
5236
5237 assert_eq!(expected, wasi_imports_from_source(NODE_WASI_MODULE_SOURCE));
5238 }
5239
5240 #[test]
5241 fn wasm_guest_module_paths_include_mapped_guest_paths_for_host_specifiers() {
5242 let temp = tempdir().expect("create temp dir");
5243 let command_root = temp.path().join("commands");
5244 let module = command_root.join("hello");
5245 fs::create_dir_all(&command_root).expect("create command root");
5246 fs::write(&module, b"\0asm\x01\0\0\0").expect("write wasm file");
5247
5248 let candidates = wasm_guest_module_paths(
5249 module.to_string_lossy().as_ref(),
5250 &BTreeMap::from([(
5251 String::from("AGENTOS_GUEST_PATH_MAPPINGS"),
5252 format!(
5253 "[{{\"guestPath\":\"/__agentos/commands/0\",\"hostPath\":\"{}\"}}]",
5254 command_root.display()
5255 ),
5256 )]),
5257 );
5258
5259 assert!(candidates.contains(&module.to_string_lossy().into_owned()));
5260 assert!(candidates.contains(&String::from("/__agentos/commands/0/hello")));
5261 }
5262
5263 #[test]
5264 fn translate_wasm_guest_path_uses_sandbox_root_for_absolute_paths() {
5265 let temp = tempdir().expect("create temp dir");
5266 let sandbox_root = temp.path().join("shadow-root");
5267 let cwd = sandbox_root.join("workspace");
5268 fs::create_dir_all(cwd.join("project")).expect("create host cwd");
5269
5270 let internal_sync_rpc = WasmInternalSyncRpc {
5271 module_guest_paths: Vec::new(),
5272 module_host_path: sandbox_root.join("module.wasm"),
5273 guest_cwd: String::from("/workspace"),
5274 host_cwd: cwd.clone(),
5275 sandbox_root: Some(sandbox_root.clone()),
5276 guest_path_mappings: Vec::new(),
5277 route_fs_through_sidecar: false,
5278 next_fd: 64,
5279 open_files: Default::default(),
5280 pending_events: VecDeque::new(),
5281 };
5282
5283 assert_eq!(
5284 translate_wasm_guest_path("/tmp/redir.txt", &internal_sync_rpc),
5285 Some(sandbox_root.join("tmp/redir.txt"))
5286 );
5287 assert_eq!(
5288 translate_wasm_guest_path("project/output.txt", &internal_sync_rpc),
5289 Some(cwd.join("project/output.txt"))
5290 );
5291 }
5292
5293 #[test]
5294 fn translate_wasm_host_symlink_target_returns_guest_path_for_mapped_targets() {
5295 let temp = tempdir().expect("create temp dir");
5296 let sandbox_root = temp.path().join("shadow-root");
5297 let cwd = sandbox_root.join("workspace");
5298 fs::create_dir_all(cwd.join("project")).expect("create host cwd");
5299
5300 let internal_sync_rpc = WasmInternalSyncRpc {
5301 module_guest_paths: Vec::new(),
5302 module_host_path: sandbox_root.join("module.wasm"),
5303 guest_cwd: String::from("/workspace"),
5304 host_cwd: cwd.clone(),
5305 sandbox_root: Some(sandbox_root.clone()),
5306 guest_path_mappings: vec![super::WasmGuestPathMapping {
5307 guest_path: String::from("/"),
5308 host_path: sandbox_root.clone(),
5309 read_only: false,
5310 }],
5311 route_fs_through_sidecar: false,
5312 next_fd: 64,
5313 open_files: Default::default(),
5314 pending_events: VecDeque::new(),
5315 };
5316
5317 assert_eq!(
5318 translate_wasm_host_symlink_target(
5319 &sandbox_root.join("tmp/sc/pdir/r.txt"),
5320 &internal_sync_rpc
5321 ),
5322 Some(String::from("/tmp/sc/pdir/r.txt"))
5323 );
5324 assert_eq!(
5325 translate_wasm_host_symlink_target(Path::new("relative-target"), &internal_sync_rpc),
5326 None
5327 );
5328 }
5329
5330 #[test]
5331 fn translate_wasm_guest_path_recovers_root_collapsed_relative_paths_from_guest_cwd() {
5332 let temp = tempdir().expect("create temp dir");
5333 let sandbox_root = temp.path().join("shadow-root");
5334 let cwd = temp.path().join("mounted-workspace");
5335 fs::create_dir_all(&sandbox_root).expect("create sandbox root");
5336 fs::create_dir_all(&cwd).expect("create mounted workspace");
5337 fs::write(cwd.join("note.txt"), b"hello").expect("write mounted file");
5338
5339 let internal_sync_rpc = WasmInternalSyncRpc {
5340 module_guest_paths: Vec::new(),
5341 module_host_path: sandbox_root.join("module.wasm"),
5342 guest_cwd: String::from("/workspace"),
5343 host_cwd: cwd.clone(),
5344 sandbox_root: Some(sandbox_root.clone()),
5345 guest_path_mappings: vec![super::WasmGuestPathMapping {
5346 guest_path: String::from("/workspace"),
5347 host_path: cwd.clone(),
5348 read_only: false,
5349 }],
5350 route_fs_through_sidecar: false,
5351 next_fd: 64,
5352 open_files: Default::default(),
5353 pending_events: VecDeque::new(),
5354 };
5355
5356 assert_eq!(
5357 translate_wasm_guest_path("/note.txt", &internal_sync_rpc),
5358 Some(cwd.join("note.txt"))
5359 );
5360 }
5361
5362 #[test]
5363 fn translate_wasm_guest_path_accepts_host_absolute_paths_within_known_roots() {
5364 let temp = tempdir().expect("create temp dir");
5365 let sandbox_root = temp.path().join("shadow-root");
5366 let cwd = temp.path().join("mounted-workspace");
5367 let mapped_root = temp.path().join("mounted-commands");
5368 fs::create_dir_all(&sandbox_root).expect("create sandbox root");
5369 fs::create_dir_all(cwd.join("subdir")).expect("create cwd");
5370 fs::create_dir_all(&mapped_root).expect("create mapped root");
5371
5372 let internal_sync_rpc = WasmInternalSyncRpc {
5373 module_guest_paths: vec![String::from("/workspace/guest.wasm")],
5374 module_host_path: cwd.join("guest.wasm"),
5375 guest_cwd: String::from("/workspace"),
5376 host_cwd: cwd.clone(),
5377 sandbox_root: Some(sandbox_root.clone()),
5378 guest_path_mappings: vec![
5379 super::WasmGuestPathMapping {
5380 guest_path: String::from("/workspace"),
5381 host_path: cwd.clone(),
5382 read_only: false,
5383 },
5384 super::WasmGuestPathMapping {
5385 guest_path: String::from("/__agentos/commands/0"),
5386 host_path: mapped_root.clone(),
5387 read_only: false,
5388 },
5389 ],
5390 route_fs_through_sidecar: false,
5391 next_fd: 64,
5392 open_files: Default::default(),
5393 pending_events: VecDeque::new(),
5394 };
5395
5396 assert_eq!(
5397 translate_wasm_guest_path(cwd.to_string_lossy().as_ref(), &internal_sync_rpc),
5398 Some(cwd.clone())
5399 );
5400 assert_eq!(
5401 translate_wasm_guest_path(
5402 cwd.join("subdir/output.txt").to_string_lossy().as_ref(),
5403 &internal_sync_rpc
5404 ),
5405 Some(cwd.join("subdir/output.txt"))
5406 );
5407 assert_eq!(
5408 translate_wasm_guest_path(
5409 mapped_root.join("tool.wasm").to_string_lossy().as_ref(),
5410 &internal_sync_rpc
5411 ),
5412 Some(mapped_root.join("tool.wasm"))
5413 );
5414 assert_eq!(
5415 translate_wasm_guest_path(
5416 sandbox_root
5417 .join("tmp/runtime.sock")
5418 .to_string_lossy()
5419 .as_ref(),
5420 &internal_sync_rpc
5421 ),
5422 Some(sandbox_root.join("tmp/runtime.sock"))
5423 );
5424 }
5425
5426 #[test]
5427 fn translate_wasm_guest_path_rejects_symlink_escape_from_sandbox_root() {
5428 let temp = tempdir().expect("create temp dir");
5429 let sandbox_root = temp.path().join("shadow-root");
5430 let outside = temp.path().join("outside");
5431 fs::create_dir_all(&sandbox_root).expect("create sandbox root");
5432 fs::create_dir_all(&outside).expect("create outside root");
5433 fs::write(outside.join("secret.txt"), b"host secret").expect("write outside file");
5434 symlink(&outside, sandbox_root.join("escape")).expect("create escape symlink");
5435
5436 let internal_sync_rpc = WasmInternalSyncRpc {
5437 module_guest_paths: Vec::new(),
5438 module_host_path: sandbox_root.join("module.wasm"),
5439 guest_cwd: String::from("/"),
5440 host_cwd: sandbox_root.clone(),
5441 sandbox_root: Some(sandbox_root.clone()),
5442 guest_path_mappings: vec![super::WasmGuestPathMapping {
5443 guest_path: String::from("/"),
5444 host_path: sandbox_root,
5445 read_only: false,
5446 }],
5447 route_fs_through_sidecar: false,
5448 next_fd: 64,
5449 open_files: Default::default(),
5450 pending_events: VecDeque::new(),
5451 };
5452
5453 assert_eq!(
5454 translate_wasm_guest_path("/escape/secret.txt", &internal_sync_rpc),
5455 None
5456 );
5457 assert_eq!(
5458 translate_wasm_guest_path("/escape/new.txt", &internal_sync_rpc),
5459 None
5460 );
5461 }
5462
5463 #[test]
5464 fn wasm_read_only_mapping_blocks_mutating_host_paths() {
5465 let temp = tempdir().expect("create temp dir");
5466 let sandbox_root = temp.path().join("shadow-root");
5467 let readonly_root = temp.path().join("readonly");
5468 fs::create_dir_all(&sandbox_root).expect("create sandbox root");
5469 fs::create_dir_all(&readonly_root).expect("create readonly root");
5470 fs::write(readonly_root.join("package.json"), b"{}").expect("write readonly file");
5471
5472 let internal_sync_rpc = WasmInternalSyncRpc {
5473 module_guest_paths: Vec::new(),
5474 module_host_path: sandbox_root.join("module.wasm"),
5475 guest_cwd: String::from("/workspace"),
5476 host_cwd: sandbox_root.clone(),
5477 sandbox_root: Some(sandbox_root),
5478 guest_path_mappings: vec![super::WasmGuestPathMapping {
5479 guest_path: String::from("/node_modules"),
5480 host_path: readonly_root.clone(),
5481 read_only: true,
5482 }],
5483 route_fs_through_sidecar: false,
5484 next_fd: 64,
5485 open_files: Default::default(),
5486 pending_events: VecDeque::new(),
5487 };
5488
5489 let host_path = translate_wasm_guest_path("/node_modules/package.json", &internal_sync_rpc)
5490 .expect("read path should resolve");
5491 assert_eq!(host_path, readonly_root.join("package.json"));
5492 assert!(wasm_host_path_is_read_only(&host_path, &internal_sync_rpc));
5493 assert!(wasm_host_path_is_read_only(
5494 &readonly_root.join("new-package.json"),
5495 &internal_sync_rpc
5496 ));
5497 assert_eq!(
5498 wasm_sync_rpc_error_code(&wasm_read_only_filesystem_error("/node_modules")),
5499 "EROFS"
5500 );
5501 }
5502
5503 #[test]
5504 fn wasm_open_guest_file_errors_remain_sync_rpc_errors() {
5505 let temp = tempdir().expect("create temp dir");
5506 let missing_path = temp.path().join("missing.txt");
5507
5508 let error = open_wasm_guest_file(&missing_path, &Value::from(0))
5509 .expect_err("missing file should return an open error");
5510
5511 assert_eq!(wasm_sync_rpc_error_code(&error), "ENOENT");
5512 }
5513
5514 #[test]
5515 fn wasm_hard_links_are_rejected_when_either_side_is_read_only() {
5516 let temp = tempdir().expect("create temp dir");
5517 let readonly_root = temp.path().join("readonly");
5518 let writable_root = temp.path().join("writable");
5519 fs::create_dir_all(&readonly_root).expect("create readonly root");
5520 fs::create_dir_all(&writable_root).expect("create writable root");
5521 let readonly_file = readonly_root.join("package.json");
5522 let writable_file = writable_root.join("source.txt");
5523 fs::write(&readonly_file, b"readonly").expect("write readonly source");
5524 fs::write(&writable_file, b"writable").expect("write writable source");
5525
5526 let internal_sync_rpc = WasmInternalSyncRpc {
5527 module_guest_paths: Vec::new(),
5528 module_host_path: writable_root.join("module.wasm"),
5529 guest_cwd: String::from("/workspace"),
5530 host_cwd: writable_root.clone(),
5531 sandbox_root: Some(writable_root.clone()),
5532 guest_path_mappings: vec![
5533 super::WasmGuestPathMapping {
5534 guest_path: String::from("/node_modules"),
5535 host_path: readonly_root.clone(),
5536 read_only: true,
5537 },
5538 super::WasmGuestPathMapping {
5539 guest_path: String::from("/workspace"),
5540 host_path: writable_root.clone(),
5541 read_only: false,
5542 },
5543 ],
5544 route_fs_through_sidecar: false,
5545 next_fd: 64,
5546 open_files: Default::default(),
5547 pending_events: VecDeque::new(),
5548 };
5549
5550 assert!(wasm_mutation_touches_read_only_mapping(
5551 &readonly_file,
5552 &writable_root.join("alias-from-readonly.json"),
5553 &internal_sync_rpc
5554 ));
5555 assert!(wasm_mutation_touches_read_only_mapping(
5556 &writable_file,
5557 &readonly_root.join("alias-into-readonly.txt"),
5558 &internal_sync_rpc
5559 ));
5560 assert!(!wasm_mutation_touches_read_only_mapping(
5561 &writable_file,
5562 &writable_root.join("alias.txt"),
5563 &internal_sync_rpc
5564 ));
5565
5566 let raw_alias = writable_root.join("raw-alias.json");
5567 fs::hard_link(&readonly_file, &raw_alias).expect("host hard link would otherwise succeed");
5568 fs::write(&raw_alias, b"mutated").expect("write through host hard link alias");
5569 assert_eq!(
5570 fs::read(&readonly_file).expect("read readonly source"),
5571 b"mutated"
5572 );
5573 }
5574
5575 #[test]
5576 fn translate_wasm_guest_path_preserves_real_root_paths_before_guest_cwd_fallback() {
5577 let temp = tempdir().expect("create temp dir");
5578 let sandbox_root = temp.path().join("shadow-root");
5579 let cwd = temp.path().join("mounted-workspace");
5580 fs::create_dir_all(&sandbox_root).expect("create sandbox root");
5581 fs::create_dir_all(&cwd).expect("create mounted workspace");
5582 fs::write(sandbox_root.join("note.txt"), b"root").expect("write root file");
5583 fs::write(cwd.join("note.txt"), b"cwd").expect("write cwd file");
5584
5585 let internal_sync_rpc = WasmInternalSyncRpc {
5586 module_guest_paths: Vec::new(),
5587 module_host_path: sandbox_root.join("module.wasm"),
5588 guest_cwd: String::from("/workspace"),
5589 host_cwd: cwd.clone(),
5590 sandbox_root: Some(sandbox_root.clone()),
5591 guest_path_mappings: vec![super::WasmGuestPathMapping {
5592 guest_path: String::from("/workspace"),
5593 host_path: cwd,
5594 read_only: false,
5595 }],
5596 route_fs_through_sidecar: false,
5597 next_fd: 64,
5598 open_files: Default::default(),
5599 pending_events: VecDeque::new(),
5600 };
5601
5602 assert_eq!(
5603 translate_wasm_guest_path("/note.txt", &internal_sync_rpc),
5604 Some(sandbox_root.join("note.txt"))
5605 );
5606 }
5607
5608 #[test]
5609 fn wasm_sandbox_root_reads_absolute_env_only() {
5610 let sandbox_root = wasm_sandbox_root(&BTreeMap::from([(
5611 String::from(WASM_SANDBOX_ROOT_ENV),
5612 String::from("/tmp/agentos-shadow"),
5613 )]));
5614 assert_eq!(sandbox_root, Some(PathBuf::from("/tmp/agentos-shadow")));
5615
5616 let relative = wasm_sandbox_root(&BTreeMap::from([(
5617 String::from(WASM_SANDBOX_ROOT_ENV),
5618 String::from("relative/shadow"),
5619 )]));
5620 assert_eq!(relative, None);
5621 }
5622
5623 #[test]
5624 fn wasm_sidecar_managed_fs_methods_route_to_kernel_sync_rpc() {
5625 let mut standalone = WasmInternalSyncRpc {
5626 module_guest_paths: Vec::new(),
5627 module_host_path: PathBuf::from("/tmp/module.wasm"),
5628 guest_cwd: String::from("/"),
5629 host_cwd: PathBuf::from("/tmp"),
5630 sandbox_root: None,
5631 guest_path_mappings: Vec::new(),
5632 route_fs_through_sidecar: false,
5633 next_fd: 64,
5634 open_files: Default::default(),
5635 pending_events: VecDeque::new(),
5636 };
5637 let sidecar_managed = WasmInternalSyncRpc {
5638 module_guest_paths: Vec::new(),
5639 module_host_path: PathBuf::from("/tmp/module.wasm"),
5640 guest_cwd: String::from("/"),
5641 host_cwd: PathBuf::from("/tmp"),
5642 sandbox_root: Some(PathBuf::from("/tmp/agentos-shadow")),
5643 guest_path_mappings: Vec::new(),
5644 route_fs_through_sidecar: true,
5645 next_fd: 64,
5646 open_files: Default::default(),
5647 pending_events: VecDeque::new(),
5648 };
5649
5650 for method in WASM_SIDECAR_ROUTED_FS_SYNC_METHODS {
5651 let request = wasm_sync_rpc_request(method);
5652 assert!(
5653 wasm_sync_rpc_method_routes_through_sidecar_kernel(&request, &sidecar_managed),
5654 "{method} should route through the sidecar kernel for managed WASI executions"
5655 );
5656 assert!(
5657 !wasm_sync_rpc_method_routes_through_sidecar_kernel(&request, &standalone),
5658 "{method} should stay host-direct for standalone/prewarm WASI execution"
5659 );
5660 }
5661
5662 standalone.route_fs_through_sidecar = true;
5663 let non_fs_request = wasm_sync_rpc_request("child_process.spawn");
5664 assert!(!wasm_sync_rpc_method_routes_through_sidecar_kernel(
5665 &non_fs_request,
5666 &standalone
5667 ));
5668 }
5669
5670 #[test]
5671 fn wasm_guest_path_mappings_mount_root_to_sandbox_root() {
5672 let temp = tempdir().expect("create temp dir");
5673 let sandbox_root = temp.path().join("shadow-root");
5674 let host_cwd = sandbox_root.join("workspace");
5675 fs::create_dir_all(&host_cwd).expect("create host cwd");
5676
5677 let mappings = super::wasm_guest_path_mappings(&request_with_env(
5678 &host_cwd,
5679 BTreeMap::from([
5680 (String::from("PWD"), String::from("/workspace")),
5681 (
5682 String::from(WASM_SANDBOX_ROOT_ENV),
5683 sandbox_root.to_string_lossy().into_owned(),
5684 ),
5685 ]),
5686 ));
5687
5688 assert!(mappings
5689 .iter()
5690 .any(|mapping| { mapping.guest_path == "/" && mapping.host_path == sandbox_root }));
5691 assert!(mappings.iter().any(|mapping| {
5692 mapping.guest_path == "/workspace" && mapping.host_path == host_cwd
5693 }));
5694 }
5695
5696 #[test]
5697 fn wasm_runner_bootstrap_keeps_root_preopens_rooted() {
5698 let bootstrap = build_wasm_runner_bootstrap(&BTreeMap::new(), None);
5699
5700 assert!(bootstrap.contains("if (guestPath === \".\") {"));
5701 assert!(!bootstrap.contains("if (guestPath === \".\" || guestPath === \"/\") {"));
5702 }
5703
5704 #[test]
5705 fn wasm_runner_bootstrap_exposes_unix_socket_sync_rpcs() {
5706 let bootstrap = build_wasm_runner_bootstrap(&BTreeMap::new(), None);
5707
5708 for (method, bridge) in [
5709 ("net.bind_unix", "_netBindUnixRaw.applySync"),
5710 (
5711 "net.bind_connected_unix",
5712 "_netBindConnectedUnixRaw.applySync",
5713 ),
5714 ("net.server_close", "_netServerCloseSyncRaw.applySync"),
5715 (
5716 "net.socket_wait_connect",
5717 "_netSocketWaitConnectSyncRaw.applySync",
5718 ),
5719 ("net.write", "_netSocketWriteSyncRaw.applySync"),
5720 ] {
5721 assert!(
5722 bootstrap.contains(&format!("case \"{method}\":")),
5723 "missing WASM sync RPC case for {method}"
5724 );
5725 assert!(
5726 bootstrap.contains(bridge),
5727 "missing synchronous V8 bridge call for {method}"
5728 );
5729 }
5730 }
5731
5732 #[test]
5733 fn wasm_runner_bootstrap_reports_dot_preopen_to_wasi() {
5734 let bootstrap = build_wasm_runner_bootstrap(&BTreeMap::new(), None);
5735
5736 assert!(bootstrap.contains("_currentGuestCwd()"));
5740 assert!(!bootstrap.contains("preopens['.'] = createPreopen(HOST_CWD, cwdReadOnly);"));
5741 assert!(bootstrap.contains("_descriptorPreopenName(entry)"));
5742 assert!(bootstrap.contains(
5743 "if (guestPath === \".\") {\n return this._descriptorGuestPath(entry);"
5744 ));
5745 assert!(bootstrap.contains("const guestPath = this._descriptorPreopenName(entry);"));
5746 }
5747
5748 #[test]
5749 fn wasm_runner_path_open_uses_guest_mapping_for_absolute_paths() {
5750 let bootstrap = build_wasm_runner_bootstrap(&BTreeMap::new(), None);
5751
5752 assert!(bootstrap
5753 .contains("const resolved = this._resolveDescriptorPath(fd, pathPtr, pathLen, {"));
5754 assert!(
5755 !bootstrap.contains("const hostPath = __agentOSPath().resolve(baseHostPath, target);")
5756 );
5757 }
5758
5759 #[test]
5760 fn wasm_runner_root_preopen_relative_paths_preserve_cwd_fallback() {
5761 let bootstrap = build_wasm_runner_bootstrap(&BTreeMap::new(), None);
5762
5763 assert!(bootstrap
5764 .contains("const rootGuestPath = __agentOSPath().posix.resolve(\"/\", target);"));
5765 assert!(bootstrap.contains(
5766 "const cwdGuestTarget = __agentOSPath().posix.resolve(cwdGuestPath, target);"
5767 ));
5768 assert!(bootstrap.contains("_rootRelativeTargetPrefersCwd(target)"));
5769 assert!(bootstrap.contains("_mappedPathExists(cwdGuestTarget, cwdHostTarget)"));
5770 assert!(bootstrap.contains("_mappedPathExists(rootGuestPath, rootHostPath)"));
5771 assert!(bootstrap
5772 .contains("__agentOSWasiSyncRpc().callSync(\"fs.statSync\", [sidecarGuestPath])"));
5773 assert!(bootstrap.contains("_rootRelativeTargetMatchesAbsoluteArg(target)"));
5774 assert!(bootstrap.contains("__agentOSPath().posix.normalize(arg) === rootGuestPath"));
5775 assert!(bootstrap.contains("_createParentExists(guestPath, hostPath)"));
5776 assert!(bootstrap.contains(
5777 "preferCreateParent &&\n !this._rootRelativeTargetIsWithinAbsoluteArg(target)"
5778 ));
5779 assert!(bootstrap.contains("this._createParentExists(cwdGuestTarget, cwdHostTarget)"));
5780 }
5781
5782 #[test]
5783 fn wasm_runner_readdir_uses_guest_preopen_path_in_sidecar() {
5784 let bootstrap = build_wasm_runner_bootstrap(&BTreeMap::new(), None);
5785
5786 assert!(bootstrap.contains("const fsPath = this._descriptorDirectoryFsPath(entry);"));
5787 assert!(
5788 bootstrap.contains("(entry?.kind === \"preopen\" || entry?.kind === \"directory\")")
5789 );
5790 }
5791
5792 #[test]
5793 fn wasm_runner_blocks_read_only_fd_write_paths() {
5794 let bootstrap = build_wasm_runner_bootstrap(&BTreeMap::new(), None);
5795
5796 assert!(bootstrap.contains("readOnly: entry.readOnly === true,"));
5797 assert!(bootstrap.contains(
5798 "if (handle.readOnly === true) {\n return __agentOSWasiErrnoRofs;\n }"
5799 ));
5800 assert!(bootstrap.contains(
5801 "if (entry.readOnly === true) {\n return __agentOSWasiErrnoRofs;\n }\n const written = __agentOSFs().writeSync("
5802 ));
5803 }
5804
5805 #[test]
5806 fn wasm_memory_limit_pages_floor_to_whole_wasm_pages() {
5807 assert_eq!(
5808 wasm_memory_limit_pages(WASM_PAGE_BYTES + 123).expect("page limit"),
5809 1
5810 );
5811 assert_eq!(
5812 wasm_memory_limit_pages(2 * WASM_PAGE_BYTES).expect("page limit"),
5813 2
5814 );
5815 }
5816
5817 #[test]
5818 fn wasm_memory_limit_no_longer_requires_declared_module_maximum() {
5819 let temp = tempdir().expect("create temp dir");
5820 let request = request_with_env(
5821 temp.path(),
5822 BTreeMap::from([(
5823 String::from(WASM_MAX_MEMORY_BYTES_ENV),
5824 (2 * WASM_PAGE_BYTES).to_string(),
5825 )]),
5826 );
5827
5828 assert!(
5829 super::validate_module_limits(
5830 &super::ResolvedWasmModule {
5831 specifier: String::from("./guest.wasm"),
5832 resolved_path: {
5833 let path = temp.path().join("guest.wasm");
5834 fs::write(
5835 &path,
5836 wat::parse_str(
5837 r#"
5838(module
5839 (memory (export "memory") 1)
5840 (func (export "_start"))
5841)
5842"#,
5843 )
5844 .expect("compile wasm fixture"),
5845 )
5846 .expect("write wasm fixture");
5847 path
5848 },
5849 },
5850 &request,
5851 )
5852 .is_ok(),
5853 "runtime memory cap should allow modules without a declared maximum"
5854 );
5855 }
5856}