Skip to main content

secure_exec_sidecar/
execution.rs

1//! Process execution, networking, and runtime event handling extracted from service.rs.
2
3use secure_exec_vm_config as vm_config;
4
5use crate::filesystem::{
6    handle_python_vfs_rpc_request as filesystem_handle_python_vfs_rpc_request,
7    service_javascript_fs_sync_rpc, service_javascript_module_sync_rpc,
8};
9use crate::protocol::{
10    BoundUdpSnapshotResponse, CloseStdinRequest, EventFrame, EventPayload, ExecuteRequest,
11    FindBoundUdpRequest, FindListenerRequest, GetProcessSnapshotRequest, GetSignalStateRequest,
12    GetZombieTimerCountRequest, GuestRuntimeKind, JavascriptChildProcessSpawnOptions,
13    JavascriptChildProcessSpawnRequest, JavascriptDgramBindRequest,
14    JavascriptDgramCreateSocketRequest, JavascriptDgramSendRequest, JavascriptDnsLookupRequest,
15    JavascriptDnsResolveRequest, JavascriptNetConnectRequest, JavascriptNetListenRequest,
16    JavascriptNetReserveTcpPortRequest, KillProcessRequest, ListenerSnapshotResponse,
17    OwnershipScope, ProcessExitedEvent, ProcessKilledResponse, ProcessOutputEvent,
18    ProcessSnapshotEntry, ProcessSnapshotResponse, ProcessSnapshotStatus, ProcessStartedResponse,
19    RequestFrame, ResponseFrame, ResponsePayload, SidecarRequestPayload, SignalDispositionAction,
20    SignalHandlerRegistration, SignalStateResponse, SocketStateEntry, StdinClosedResponse,
21    StdinWrittenResponse, StreamChannel, VmFetchRequest, VmFetchResponse, WasmPermissionTier,
22    WriteStdinRequest, ZombieTimerCountResponse,
23};
24use crate::service::{
25    audit_fields, dirname, emit_security_audit_event, emit_structured_event, javascript_error,
26    kernel_error, log_stale_process_event, normalize_host_path, normalize_path,
27    parse_javascript_child_process_spawn_request, path_is_within_root,
28    process_event_queue_overflow_error, python_error, wasm_error, MAX_PROCESS_EVENT_QUEUE,
29};
30use crate::state::{
31    ActiveCipherSession, ActiveDhSession, ActiveDiffieHellmanSession, ActiveEcdhSession,
32    ActiveExecution, ActiveExecutionEvent, ActiveHttp2Server, ActiveHttp2Session,
33    ActiveHttp2Stream, ActiveHttpServer, ActiveMappedHostFd, ActiveProcess, ActiveSqliteDatabase,
34    ActiveSqliteStatement, ActiveTcpListener, ActiveTcpSocket, ActiveTlsState, ActiveTlsStream,
35    ActiveUdpSocket, ActiveUnixListener, ActiveUnixSocket, BridgeError, ExitedProcessSnapshot,
36    Http2BridgeEvent, Http2RuntimeSnapshot, Http2SessionCommand, Http2SessionSnapshot,
37    Http2SocketSnapshot, JavascriptHttpLoopbackTarget, JavascriptSocketFamily,
38    JavascriptSocketPathContext, JavascriptTcpListenerEvent, JavascriptTcpSocketEvent,
39    JavascriptTlsBridgeOptions, JavascriptTlsClientHello, JavascriptTlsDataValue,
40    JavascriptTlsMaterial, JavascriptUdpFamily, JavascriptUdpSocketEvent,
41    JavascriptUnixListenerEvent, NetworkResourceCounts, PendingTcpSocket, PendingUnixSocket,
42    ProcNetEntry, ProcessEventEnvelope, ResolvedChildProcessExecution, ResolvedTcpConnectAddr,
43    SharedBridge, SharedSidecarRequestClient, SidecarKernel, SocketQueryKind, ToolExecution,
44    VmDnsConfig, VmListenPolicy, VmState, DEFAULT_JAVASCRIPT_NET_BACKLOG, EXECUTION_DRIVER_NAME,
45    EXECUTION_SANDBOX_ROOT_ENV, JAVASCRIPT_COMMAND, LOOPBACK_EXEMPT_PORTS_ENV,
46    MAPPED_HOST_FD_START, PYTHON_COMMAND, TOOL_DRIVER_NAME,
47    VM_LISTEN_ALLOW_PRIVILEGED_METADATA_KEY, WASM_COMMAND, WASM_STDIO_SYNC_RPC_ENV,
48};
49use crate::tools::{
50    format_tool_failure_output, is_tool_command, normalized_tool_command_name,
51    resolve_tool_command, ToolCommandResolution,
52};
53use crate::wire::{ProtocolFrame as WireProtocolFrame, WireFrameCodec, DEFAULT_MAX_FRAME_BYTES};
54use crate::{DispatchResult, NativeSidecar, NativeSidecarBridge, SidecarError};
55
56use base64::Engine;
57use bytes::Bytes;
58use h2::{client, server, Reason};
59use hickory_resolver::proto::rr::{RData, Record, RecordType};
60use hmac::{Hmac, Mac};
61use http::{HeaderMap, HeaderName, HeaderValue, Method, Request, Response, Uri};
62use md5::Md5;
63use nix::libc;
64use nix::sys::signal::{kill as send_signal, Signal};
65use nix::sys::wait::WaitStatus;
66#[cfg(not(target_os = "macos"))]
67use nix::sys::wait::{waitid as wait_on_child, Id as WaitId, WaitPidFlag};
68#[cfg(target_os = "macos")]
69use nix::sys::wait::{waitpid, WaitPidFlag};
70use nix::unistd::Pid;
71use openssl::bn::{BigNum, BigNumContext};
72use openssl::derive::Deriver;
73use openssl::dh::Dh;
74use openssl::ec::{EcGroup, EcKey, EcPoint, PointConversionForm};
75use openssl::hash::MessageDigest;
76use openssl::nid::Nid;
77use openssl::pkey::{Id as PKeyId, PKey, Params, Private, Public};
78use openssl::rand::rand_bytes;
79use openssl::rsa::{Padding, Rsa};
80use openssl::sign::{Signer, Verifier};
81use openssl::symm::{Cipher, Crypter, Mode};
82use pbkdf2::pbkdf2_hmac;
83use rusqlite::types::ValueRef as SqliteValueRef;
84use rusqlite::{
85    Connection as SqliteConnection, OpenFlags as SqliteOpenFlags, Statement as SqliteStatement,
86};
87use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
88use rustls::crypto::aws_lc_rs;
89use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName};
90use rustls::{
91    ClientConfig, ClientConnection, DigitallySignedStruct, RootCertStore, ServerConfig,
92    ServerConnection, SignatureScheme,
93};
94use scrypt::{scrypt, Params as ScryptParams};
95use secure_exec_bridge::LifecycleState;
96use secure_exec_execution::wasm::WasmExecutionError;
97use secure_exec_execution::{
98    javascript::handle_internal_bridge_call_from_host_context, v8_host::V8SessionHandle,
99    v8_runtime, CreateJavascriptContextRequest, CreatePythonContextRequest,
100    CreateWasmContextRequest, GuestModuleReader, GuestRuntimeConfig, JavascriptExecutionEvent,
101    JavascriptExecutionLimits, JavascriptSyncRpcRequest, ModuleFsReader,
102    NodeSignalDispositionAction, NodeSignalHandlerRegistration, PythonExecutionEvent,
103    PythonExecutionLimits, PythonVfsRpcMethod, PythonVfsRpcRequest, PythonVfsRpcResponsePayload,
104    StartJavascriptExecutionRequest, StartPythonExecutionRequest, StartWasmExecutionRequest,
105    WasmExecutionEvent, WasmExecutionLimits, WasmPermissionTier as ExecutionWasmPermissionTier,
106};
107use secure_exec_kernel::dns::{
108    DnsLookupPolicy, DnsRecordResolution, DnsResolutionSource as KernelDnsResolutionSource,
109};
110use secure_exec_kernel::kernel::{KernelProcessHandle, SpawnOptions, VirtualProcessOptions};
111use secure_exec_kernel::permissions::NetworkOperation;
112use secure_exec_kernel::poll::{PollEvents, PollFd, PollTargetEntry, POLLERR, POLLHUP, POLLIN};
113use secure_exec_kernel::process_table::{ProcessStatus, WaitPidFlags, SIGKILL, SIGTERM};
114use secure_exec_kernel::pty::LineDisciplineConfig;
115use secure_exec_kernel::resource_accounting::ResourceLimits;
116use secure_exec_kernel::root_fs::RootFilesystemMode;
117use secure_exec_kernel::socket_table::{
118    InetSocketAddress, SocketDomain, SocketId, SocketShutdown as KernelSocketShutdown, SocketSpec,
119    SocketState, SocketType,
120};
121use serde::{Deserialize, Serialize};
122use serde_json::{json, Map, Value};
123use sha1::Sha1;
124use sha2::{digest::Digest, Sha256, Sha512};
125use socket2::{SockRef, TcpKeepalive};
126use std::collections::VecDeque;
127use std::collections::{BTreeMap, BTreeSet};
128use std::fmt;
129use std::fs;
130use std::io::{Cursor, Read, Write};
131use std::net::{
132    IpAddr, Ipv4Addr, Ipv6Addr, Shutdown, SocketAddr, TcpListener, TcpStream, ToSocketAddrs,
133    UdpSocket,
134};
135use std::os::unix::fs::{MetadataExt, PermissionsExt};
136use std::os::unix::net::{SocketAddr as UnixSocketAddr, UnixListener, UnixStream};
137use std::path::{Path, PathBuf};
138use std::pin::Pin;
139use std::sync::atomic::{AtomicBool, Ordering};
140use std::sync::mpsc::{self, RecvTimeoutError, Sender};
141use std::sync::{Arc, Mutex, OnceLock, Weak};
142use std::thread;
143use std::time::{Duration, Instant};
144use tokio::io::{AsyncRead, AsyncWrite};
145use tokio::runtime::Builder as TokioRuntimeBuilder;
146use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver};
147use tokio_rustls::{TlsAcceptor, TlsConnector};
148use url::Url;
149
150const DEFAULT_KERNEL_STDIN_READ_MAX_BYTES: usize = 64 * 1024;
151const DEFAULT_KERNEL_STDIN_READ_TIMEOUT_MS: u64 = 100;
152const JAVASCRIPT_NET_TIMEOUT_SENTINEL: &str = "__secure_exec_net_timeout__";
153const PYTHON_PYODIDE_GUEST_ROOT: &str = "/__agentos_pyodide";
154const PYTHON_PYODIDE_CACHE_GUEST_ROOT: &str = "/__agentos_pyodide_cache";
155const TCP_SOCKET_POLL_TIMEOUT: Duration = Duration::from_millis(100);
156const TLS_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(5);
157const HTTP_LOOPBACK_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
158pub(crate) const MAX_PER_PROCESS_STATE_HANDLES: usize = 1024;
159const VM_FETCH_BUFFER_LIMIT_BYTES: usize = DEFAULT_MAX_FRAME_BYTES;
160const DEFAULT_SCRYPT_COST: u64 = 16_384;
161const DEFAULT_SCRYPT_BLOCK_SIZE: u32 = 8;
162const DEFAULT_SCRYPT_PARALLELIZATION: u32 = 1;
163const SQLITE_JS_SAFE_INTEGER_MAX: i64 = 9_007_199_254_740_991;
164const HTTP_LOOPBACK_REQUEST_TIMEOUT_MS_ENV: &str =
165    "SECURE_EXEC_TEST_HTTP_LOOPBACK_REQUEST_TIMEOUT_MS";
166
167trait Http2AsyncIo: AsyncRead + AsyncWrite + Unpin + Send {}
168
169impl<T> Http2AsyncIo for T where T: AsyncRead + AsyncWrite + Unpin + Send {}
170
171fn http_loopback_request_timeout() -> Duration {
172    static TIMEOUT: OnceLock<Duration> = OnceLock::new();
173    *TIMEOUT.get_or_init(|| {
174        std::env::var(HTTP_LOOPBACK_REQUEST_TIMEOUT_MS_ENV)
175            .ok()
176            .and_then(|value| value.parse::<u64>().ok())
177            .map(Duration::from_millis)
178            .unwrap_or(HTTP_LOOPBACK_REQUEST_TIMEOUT)
179    })
180}
181
182const DEFAULT_ALLOWED_NODE_BUILTINS: &[&str] = &[
183    "assert",
184    "buffer",
185    "console",
186    "child_process",
187    "crypto",
188    "dns",
189    "events",
190    "fs",
191    "http",
192    "http2",
193    "https",
194    "module",
195    "os",
196    "path",
197    "perf_hooks",
198    "querystring",
199    "sqlite",
200    "stream",
201    "string_decoder",
202    "timers",
203    "tls",
204    "tty",
205    "url",
206    "util",
207    "zlib",
208];
209
210#[derive(Debug, Clone, Copy, PartialEq, Eq)]
211enum JavascriptCryptoDigestAlgorithm {
212    Md5,
213    Sha1,
214    Sha256,
215    Sha512,
216}
217
218#[derive(Debug, Default, Deserialize)]
219#[serde(default, rename_all = "camelCase")]
220struct JavascriptScryptOptions {
221    #[serde(alias = "N")]
222    cost: Option<u64>,
223    #[serde(alias = "r")]
224    block_size: Option<u32>,
225    #[serde(alias = "p")]
226    parallelization: Option<u32>,
227}
228
229#[derive(Debug, Deserialize)]
230#[serde(rename_all = "camelCase")]
231struct JavascriptHttpListenRequest {
232    server_id: u64,
233    #[serde(default)]
234    port: Option<u16>,
235    #[serde(default)]
236    hostname: Option<String>,
237}
238
239#[derive(Debug, Default, Deserialize)]
240#[serde(default, rename_all = "camelCase")]
241struct JavascriptHttpRequestOptions {
242    method: Option<String>,
243    headers: BTreeMap<String, Value>,
244    body: Option<String>,
245    reject_unauthorized: Option<bool>,
246}
247
248#[derive(Debug, Default, Deserialize)]
249#[serde(default, rename_all = "camelCase")]
250struct JavascriptHttp2ServerListenRequest {
251    server_id: u64,
252    secure: bool,
253    port: Option<u16>,
254    host: Option<String>,
255    backlog: Option<u32>,
256    timeout: Option<u64>,
257    settings: BTreeMap<String, Value>,
258    tls: Option<JavascriptTlsBridgeOptions>,
259}
260
261#[derive(Debug, Default, Deserialize)]
262#[serde(default, rename_all = "camelCase")]
263struct JavascriptHttp2SessionConnectRequest {
264    authority: Option<String>,
265    protocol: Option<String>,
266    host: Option<String>,
267    port: Option<u16>,
268    settings: BTreeMap<String, Value>,
269    tls: Option<JavascriptTlsBridgeOptions>,
270}
271
272#[derive(Debug, Default, Deserialize)]
273#[serde(default, rename_all = "camelCase")]
274struct JavascriptHttp2RequestOptions {
275    end_stream: bool,
276}
277
278#[derive(Debug, Default, Deserialize)]
279#[serde(default, rename_all = "camelCase")]
280struct JavascriptHttp2FileResponseOptions {
281    offset: Option<u64>,
282    length: Option<i64>,
283}
284
285#[derive(Debug, Clone)]
286struct HttpHeaderCollection {
287    normalized: BTreeMap<String, Vec<String>>,
288    raw_pairs: Vec<(String, String)>,
289}
290
291#[derive(Debug)]
292struct InsecureTlsVerifier {
293    supported_schemes: Vec<SignatureScheme>,
294}
295
296impl ServerCertVerifier for InsecureTlsVerifier {
297    fn verify_server_cert(
298        &self,
299        _end_entity: &CertificateDer<'_>,
300        _intermediates: &[CertificateDer<'_>],
301        _server_name: &ServerName<'_>,
302        _ocsp_response: &[u8],
303        _now: rustls::pki_types::UnixTime,
304    ) -> Result<ServerCertVerified, rustls::Error> {
305        Ok(ServerCertVerified::assertion())
306    }
307
308    fn verify_tls12_signature(
309        &self,
310        _message: &[u8],
311        _cert: &CertificateDer<'_>,
312        _dss: &DigitallySignedStruct,
313    ) -> Result<HandshakeSignatureValid, rustls::Error> {
314        Ok(HandshakeSignatureValid::assertion())
315    }
316
317    fn verify_tls13_signature(
318        &self,
319        _message: &[u8],
320        _cert: &CertificateDer<'_>,
321        _dss: &DigitallySignedStruct,
322    ) -> Result<HandshakeSignatureValid, rustls::Error> {
323        Ok(HandshakeSignatureValid::assertion())
324    }
325
326    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
327        self.supported_schemes.clone()
328    }
329}
330
331impl ActiveProcess {
332    pub(crate) fn new(
333        kernel_pid: u32,
334        kernel_handle: KernelProcessHandle,
335        runtime: GuestRuntimeKind,
336        execution: ActiveExecution,
337    ) -> Self {
338        Self {
339            kernel_pid,
340            kernel_handle,
341            kernel_stdin_writer_fd: None,
342            runtime,
343            detached: false,
344            execution,
345            guest_cwd: String::from("/"),
346            env: BTreeMap::new(),
347            host_cwd: PathBuf::from("/"),
348            mapped_host_fds: BTreeMap::new(),
349            next_mapped_host_fd: MAPPED_HOST_FD_START,
350            pending_execution_events: VecDeque::new(),
351            pending_self_signal_exit: None,
352            child_processes: BTreeMap::new(),
353            next_child_process_id: 0,
354            http_servers: BTreeMap::new(),
355            pending_http_requests: BTreeMap::new(),
356            http2: Default::default(),
357            tcp_listeners: BTreeMap::new(),
358            next_tcp_listener_id: 0,
359            tcp_sockets: BTreeMap::new(),
360            next_tcp_socket_id: 0,
361            tcp_port_reservations: BTreeMap::new(),
362            next_tcp_port_reservation_id: 0,
363            unix_listeners: BTreeMap::new(),
364            next_unix_listener_id: 0,
365            unix_sockets: BTreeMap::new(),
366            next_unix_socket_id: 0,
367            udp_sockets: BTreeMap::new(),
368            next_udp_socket_id: 0,
369            cipher_sessions: BTreeMap::new(),
370            next_cipher_session_id: 0,
371            diffie_hellman_sessions: BTreeMap::new(),
372            next_diffie_hellman_session_id: 0,
373            sqlite_databases: BTreeMap::new(),
374            next_sqlite_database_id: 0,
375            sqlite_statements: BTreeMap::new(),
376            next_sqlite_statement_id: 0,
377            module_resolution_cache: secure_exec_execution::LocalModuleResolutionCache::default(),
378        }
379    }
380
381    pub(crate) fn queue_pending_execution_event(
382        &mut self,
383        event: ActiveExecutionEvent,
384    ) -> Result<(), SidecarError> {
385        if self.pending_execution_events.len() >= MAX_PROCESS_EVENT_QUEUE {
386            return Err(process_event_queue_overflow_error());
387        }
388        self.pending_execution_events.push_back(event);
389        Ok(())
390    }
391
392    pub(crate) fn with_host_cwd(mut self, host_cwd: PathBuf) -> Self {
393        self.host_cwd = host_cwd;
394        self
395    }
396
397    pub(crate) fn with_guest_cwd(mut self, guest_cwd: String) -> Self {
398        self.guest_cwd = guest_cwd;
399        self
400    }
401
402    pub(crate) fn with_env(mut self, env: BTreeMap<String, String>) -> Self {
403        self.env = env;
404        self
405    }
406
407    pub(crate) fn with_kernel_stdin_writer_fd(mut self, fd: u32) -> Self {
408        self.kernel_stdin_writer_fd = Some(fd);
409        self
410    }
411
412    pub(crate) fn with_detached(mut self, detached: bool) -> Self {
413        self.detached = detached;
414        self
415    }
416
417    pub(crate) fn allocate_mapped_host_fd(&mut self, fd: ActiveMappedHostFd) -> u32 {
418        let handle = self.next_mapped_host_fd;
419        self.next_mapped_host_fd = self
420            .next_mapped_host_fd
421            .checked_add(1)
422            .unwrap_or(MAPPED_HOST_FD_START);
423        self.mapped_host_fds.insert(handle, fd);
424        handle
425    }
426
427    pub(crate) fn mapped_host_fd(&self, fd: u32) -> Option<&ActiveMappedHostFd> {
428        self.mapped_host_fds.get(&fd)
429    }
430
431    pub(crate) fn mapped_host_fd_mut(&mut self, fd: u32) -> Option<&mut ActiveMappedHostFd> {
432        self.mapped_host_fds.get_mut(&fd)
433    }
434
435    pub(crate) fn close_mapped_host_fd(&mut self, fd: u32) -> bool {
436        self.mapped_host_fds.remove(&fd).is_some()
437    }
438
439    pub(crate) fn allocate_child_process_id(&mut self) -> String {
440        self.next_child_process_id += 1;
441        format!("child-{}", self.next_child_process_id)
442    }
443
444    fn allocate_tcp_listener_id(&mut self) -> String {
445        self.next_tcp_listener_id += 1;
446        format!("listener-{}", self.next_tcp_listener_id)
447    }
448
449    fn allocate_tcp_socket_id(&mut self) -> String {
450        self.next_tcp_socket_id += 1;
451        format!("socket-{}", self.next_tcp_socket_id)
452    }
453
454    fn allocate_tcp_port_reservation_id(&mut self) -> String {
455        self.next_tcp_port_reservation_id += 1;
456        format!("tcp-port-reservation-{}", self.next_tcp_port_reservation_id)
457    }
458
459    fn allocate_unix_listener_id(&mut self) -> String {
460        self.next_unix_listener_id += 1;
461        format!("unix-listener-{}", self.next_unix_listener_id)
462    }
463
464    fn allocate_unix_socket_id(&mut self) -> String {
465        self.next_unix_socket_id += 1;
466        format!("unix-socket-{}", self.next_unix_socket_id)
467    }
468
469    fn allocate_udp_socket_id(&mut self) -> String {
470        self.next_udp_socket_id += 1;
471        format!("udp-socket-{}", self.next_udp_socket_id)
472    }
473
474    pub(crate) fn network_resource_counts(&self) -> NetworkResourceCounts {
475        let mut counts = NetworkResourceCounts {
476            sockets: self.http_servers.len()
477                + self.tcp_listeners.len()
478                + self.tcp_sockets.len()
479                + self.unix_listeners.len()
480                + self.unix_sockets.len()
481                + self.udp_sockets.len(),
482            connections: self.tcp_sockets.len() + self.unix_sockets.len(),
483        };
484        if let Ok(http2) = self.http2.shared.lock() {
485            counts.sockets += http2.servers.len() + http2.sessions.len();
486            counts.connections += http2.sessions.len();
487        }
488
489        for child in self.child_processes.values() {
490            let child_counts = child.network_resource_counts();
491            counts.sockets += child_counts.sockets;
492            counts.connections += child_counts.connections;
493        }
494
495        counts
496    }
497
498    fn sidecar_only_network_resource_counts(&self) -> NetworkResourceCounts {
499        let mut counts = NetworkResourceCounts {
500            sockets: self.http_servers.len()
501                + self
502                    .tcp_listeners
503                    .values()
504                    .filter(|listener| listener.kernel_socket_id.is_none())
505                    .count()
506                + self
507                    .tcp_sockets
508                    .values()
509                    .filter(|socket| socket.kernel_socket_id.is_none())
510                    .count()
511                + self.unix_listeners.len()
512                + self.unix_sockets.len()
513                + self
514                    .udp_sockets
515                    .values()
516                    .filter(|socket| socket.kernel_socket_id.is_none())
517                    .count(),
518            connections: self
519                .tcp_sockets
520                .values()
521                .filter(|socket| socket.kernel_socket_id.is_none())
522                .count()
523                + self.unix_sockets.len(),
524        };
525        if let Ok(http2) = self.http2.shared.lock() {
526            counts.sockets += http2.servers.len() + http2.sessions.len();
527            counts.connections += http2.sessions.len();
528        }
529
530        for child in self.child_processes.values() {
531            let child_counts = child.sidecar_only_network_resource_counts();
532            counts.sockets += child_counts.sockets;
533            counts.connections += child_counts.connections;
534        }
535
536        counts
537    }
538}
539
540fn poll_tool_process_event(
541    execution: &ToolExecution,
542) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
543    let event = execution
544        .pending_events
545        .lock()
546        .unwrap_or_else(|poisoned| poisoned.into_inner())
547        .pop_front();
548    if event.is_some() {
549        return Ok(event);
550    }
551    if execution.events_overflowed.load(Ordering::Relaxed) {
552        return Err(process_event_queue_overflow_error());
553    }
554    Ok(None)
555}
556
557fn descendant_pending_execution_event_capacity(
558    root: &ActiveProcess,
559    child_path: &[&str],
560) -> Option<usize> {
561    let mut child = root;
562    for child_process_id in child_path {
563        child = child.child_processes.get(*child_process_id)?;
564    }
565    Some(MAX_PROCESS_EVENT_QUEUE.saturating_sub(child.pending_execution_events.len()))
566}
567
568fn poll_child_execution_after_exit(
569    child: &mut ActiveProcess,
570    wait: Duration,
571) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
572    match child.execution.poll_event_blocking(wait) {
573        Ok(event) => Ok(event),
574        Err(SidecarError::Execution(message))
575            if child.runtime == GuestRuntimeKind::WebAssembly
576                && message == WasmExecutionError::EventChannelClosed.to_string() =>
577        {
578            Ok(None)
579        }
580        Err(error) => Err(error),
581    }
582}
583
584fn closed_javascript_event_channel(message: &str) -> bool {
585    message == "guest JavaScript event channel closed unexpectedly"
586}
587
588fn closed_python_event_channel(message: &str) -> bool {
589    message == "guest Python event channel closed unexpectedly"
590}
591
592fn closed_wasm_event_channel(message: &str) -> bool {
593    message == WasmExecutionError::EventChannelClosed.to_string()
594}
595
596fn missing_vm_error(vm_id: &str) -> SidecarError {
597    SidecarError::InvalidState(format!("VM {vm_id} is no longer active"))
598}
599
600fn missing_process_error(vm_id: &str, process_id: &str) -> SidecarError {
601    SidecarError::InvalidState(format!(
602        "VM {vm_id} no longer has active process {process_id}"
603    ))
604}
605
606fn is_broken_pipe_error(error: &SidecarError) -> bool {
607    matches!(error, SidecarError::Execution(message) if message.contains("Broken pipe") || message.contains("os error 32") || message.contains("EPIPE"))
608}
609
610fn javascript_child_process_gone_error(process_id: &str, child_path: &[&str]) -> SidecarError {
611    let child_label = if child_path.is_empty() {
612        process_id.to_owned()
613    } else {
614        format!("{process_id}/{}", child_path.join("/"))
615    };
616    SidecarError::Execution(format!(
617        "ECHILD: child_process {child_label} is no longer available"
618    ))
619}
620
621fn is_javascript_child_process_gone_error(error: &SidecarError) -> bool {
622    matches!(
623        error,
624        SidecarError::Execution(message) if guest_errno_code(message) == Some("ECHILD")
625    )
626}
627
628fn loopback_tls_transport_registry(
629) -> &'static Mutex<BTreeMap<String, Weak<crate::state::LoopbackTlsTransportPair>>> {
630    static REGISTRY: OnceLock<
631        Mutex<BTreeMap<String, Weak<crate::state::LoopbackTlsTransportPair>>>,
632    > = OnceLock::new();
633    REGISTRY.get_or_init(|| Mutex::new(BTreeMap::new()))
634}
635
636fn loopback_tls_transport_key(
637    vm_id: &str,
638    socket_id: SocketId,
639    peer_socket_id: SocketId,
640) -> String {
641    let (lower, higher) = if socket_id <= peer_socket_id {
642        (socket_id, peer_socket_id)
643    } else {
644        (peer_socket_id, socket_id)
645    };
646    format!("{vm_id}:{lower}:{higher}")
647}
648
649fn loopback_tls_endpoint(
650    vm_id: &str,
651    socket_id: SocketId,
652    peer_socket_id: SocketId,
653) -> Result<crate::state::LoopbackTlsEndpoint, SidecarError> {
654    let key = loopback_tls_transport_key(vm_id, socket_id, peer_socket_id);
655    let registry = loopback_tls_transport_registry();
656    let mut transports = registry.lock().map_err(|_| {
657        SidecarError::InvalidState(String::from(
658            "loopback TLS transport registry lock poisoned",
659        ))
660    })?;
661    transports.retain(|_, pair| pair.strong_count() > 0);
662    let pair = transports
663        .get(&key)
664        .and_then(Weak::upgrade)
665        .unwrap_or_else(|| {
666            let pair = Arc::new(crate::state::LoopbackTlsTransportPair {
667                state: Mutex::new(crate::state::LoopbackTlsTransportPairState::default()),
668                ready: std::sync::Condvar::new(),
669            });
670            transports.insert(key, Arc::downgrade(&pair));
671            pair
672        });
673    Ok(crate::state::LoopbackTlsEndpoint {
674        pair,
675        is_lower_socket: socket_id <= peer_socket_id,
676    })
677}
678
679impl crate::state::LoopbackTlsEndpoint {
680    fn shutdown_write(&self) -> Result<(), SidecarError> {
681        let mut state = self.pair.state.lock().map_err(|_| {
682            SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
683        })?;
684        if self.is_lower_socket {
685            state.lower_write_closed = true;
686        } else {
687            state.higher_write_closed = true;
688        }
689        self.pair.ready.notify_all();
690        Ok(())
691    }
692
693    fn close_endpoint(&self) -> Result<(), SidecarError> {
694        let mut state = self.pair.state.lock().map_err(|_| {
695            SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
696        })?;
697        if self.is_lower_socket {
698            state.lower_write_closed = true;
699            state.lower_closed = true;
700        } else {
701            state.higher_write_closed = true;
702            state.higher_closed = true;
703        }
704        self.pair.ready.notify_all();
705        Ok(())
706    }
707}
708
709fn parse_tls_client_hello_from_bytes(
710    buffer: &[u8],
711) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
712    if buffer.is_empty() {
713        return Ok(None);
714    }
715
716    let mut acceptor = rustls::server::Acceptor::default();
717    let mut cursor = Cursor::new(buffer);
718    acceptor.read_tls(&mut cursor).map_err(sidecar_net_error)?;
719    let Some(accepted) = acceptor.accept().map_err(|(error, _)| {
720        SidecarError::Execution(format!("failed to parse TLS client hello: {error}"))
721    })?
722    else {
723        return Ok(None);
724    };
725    let client_hello = accepted.client_hello();
726    let alpn_protocols = client_hello.alpn().map(|protocols| {
727        protocols
728            .filter_map(|protocol| String::from_utf8(protocol.to_vec()).ok())
729            .collect::<Vec<_>>()
730    });
731    Ok(Some(JavascriptTlsClientHello {
732        servername: client_hello.server_name().map(str::to_owned),
733        alpn_protocols,
734    }))
735}
736
737fn peek_loopback_tls_client_hello(
738    vm_id: &str,
739    socket_id: SocketId,
740    peer_socket_id: SocketId,
741) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
742    let key = loopback_tls_transport_key(vm_id, socket_id, peer_socket_id);
743    let registry = loopback_tls_transport_registry();
744    let pair = registry
745        .lock()
746        .map_err(|_| {
747            SidecarError::InvalidState(String::from(
748                "loopback TLS transport registry lock poisoned",
749            ))
750        })?
751        .get(&key)
752        .and_then(Weak::upgrade);
753    let Some(pair) = pair else {
754        return Ok(None);
755    };
756    let is_lower_socket = socket_id <= peer_socket_id;
757    let state = pair.state.lock().map_err(|_| {
758        SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
759    })?;
760    let buffered = if is_lower_socket {
761        state.higher_to_lower.iter().copied().collect::<Vec<_>>()
762    } else {
763        state.lower_to_higher.iter().copied().collect::<Vec<_>>()
764    };
765    drop(state);
766    parse_tls_client_hello_from_bytes(&buffered)
767}
768
769fn wait_for_loopback_peer_socket_id(
770    kernel: &SidecarKernel,
771    socket_id: SocketId,
772) -> Option<SocketId> {
773    for _ in 0..50 {
774        if let Some(peer_socket_id) = kernel
775            .socket_get(socket_id)
776            .and_then(|record| record.peer_socket_id())
777        {
778            return Some(peer_socket_id);
779        }
780        std::thread::sleep(Duration::from_millis(10));
781    }
782    None
783}
784
785impl Drop for crate::state::LoopbackTlsEndpoint {
786    fn drop(&mut self) {
787        let _ = self.close_endpoint();
788    }
789}
790
791impl Read for crate::state::LoopbackTlsEndpoint {
792    fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
793        let mut state = self
794            .pair
795            .state
796            .lock()
797            .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
798
799        loop {
800            let (peer_write_closed, peer_closed) = if self.is_lower_socket {
801                (state.higher_write_closed, state.higher_closed)
802            } else {
803                (state.lower_write_closed, state.lower_closed)
804            };
805
806            let incoming = if self.is_lower_socket {
807                &mut state.higher_to_lower
808            } else {
809                &mut state.lower_to_higher
810            };
811
812            if !incoming.is_empty() {
813                let mut count = 0;
814                while count < buffer.len() {
815                    let Some(byte) = incoming.pop_front() else {
816                        break;
817                    };
818                    buffer[count] = byte;
819                    count += 1;
820                }
821                return Ok(count);
822            }
823
824            if peer_write_closed || peer_closed {
825                return Ok(0);
826            }
827
828            let (next_state, wait_result) = self
829                .pair
830                .ready
831                .wait_timeout(state, TCP_SOCKET_POLL_TIMEOUT)
832                .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
833            state = next_state;
834            if wait_result.timed_out() {
835                return Err(std::io::Error::new(
836                    std::io::ErrorKind::WouldBlock,
837                    "loopback TLS transport read timed out",
838                ));
839            }
840        }
841    }
842}
843
844impl Write for crate::state::LoopbackTlsEndpoint {
845    fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
846        let mut state = self
847            .pair
848            .state
849            .lock()
850            .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
851
852        let peer_closed = if self.is_lower_socket {
853            state.higher_closed
854        } else {
855            state.lower_closed
856        };
857        let outgoing = if self.is_lower_socket {
858            &mut state.lower_to_higher
859        } else {
860            &mut state.higher_to_lower
861        };
862        if peer_closed {
863            return Err(std::io::Error::new(
864                std::io::ErrorKind::BrokenPipe,
865                "loopback TLS peer is closed",
866            ));
867        }
868
869        outgoing.extend(buffer.iter().copied());
870        self.pair.ready.notify_all();
871        Ok(buffer.len())
872    }
873
874    fn flush(&mut self) -> std::io::Result<()> {
875        Ok(())
876    }
877}
878
879// TCP types moved to crate::state
880
881struct ActiveTcpConnectRequest<'a, B> {
882    bridge: &'a SharedBridge<B>,
883    kernel: &'a mut SidecarKernel,
884    kernel_pid: u32,
885    vm_id: &'a str,
886    dns: &'a VmDnsConfig,
887    host: &'a str,
888    port: u16,
889    local_address: Option<&'a str>,
890    local_port: Option<u16>,
891    local_reservation: Option<(JavascriptSocketFamily, u16)>,
892    context: &'a JavascriptSocketPathContext,
893}
894
895struct ActiveUdpSendToRequest<'a, B> {
896    bridge: &'a SharedBridge<B>,
897    kernel: &'a mut SidecarKernel,
898    kernel_pid: u32,
899    vm_id: &'a str,
900    dns: &'a VmDnsConfig,
901    host: &'a str,
902    port: u16,
903    context: &'a JavascriptSocketPathContext,
904    contents: &'a [u8],
905}
906
907struct UdpRemoteAddrRequest<'a, B> {
908    bridge: &'a SharedBridge<B>,
909    kernel: &'a SidecarKernel,
910    vm_id: &'a str,
911    dns: &'a VmDnsConfig,
912    host: &'a str,
913    port: u16,
914    family: JavascriptUdpFamily,
915    context: &'a JavascriptSocketPathContext,
916}
917
918pub(crate) struct JavascriptSyncRpcServiceRequest<'a, B> {
919    pub(crate) bridge: &'a SharedBridge<B>,
920    pub(crate) vm_id: &'a str,
921    pub(crate) dns: &'a VmDnsConfig,
922    pub(crate) socket_paths: &'a JavascriptSocketPathContext,
923    pub(crate) kernel: &'a mut SidecarKernel,
924    pub(crate) process: &'a mut ActiveProcess,
925    pub(crate) sync_request: &'a JavascriptSyncRpcRequest,
926    pub(crate) resource_limits: &'a ResourceLimits,
927    pub(crate) network_counts: NetworkResourceCounts,
928}
929
930pub(crate) struct JavascriptNetSyncRpcServiceRequest<'a, B> {
931    pub(crate) bridge: &'a SharedBridge<B>,
932    pub(crate) vm_id: &'a str,
933    pub(crate) dns: &'a VmDnsConfig,
934    pub(crate) socket_paths: &'a JavascriptSocketPathContext,
935    pub(crate) kernel: &'a mut SidecarKernel,
936    pub(crate) process: &'a mut ActiveProcess,
937    pub(crate) sync_request: &'a JavascriptSyncRpcRequest,
938    pub(crate) resource_limits: &'a ResourceLimits,
939    pub(crate) network_counts: NetworkResourceCounts,
940}
941
942struct LoopbackHttpResponseWaitRequest<'a, B> {
943    bridge: &'a SharedBridge<B>,
944    vm_id: &'a str,
945    dns: &'a VmDnsConfig,
946    socket_paths: &'a JavascriptSocketPathContext,
947    kernel: &'a mut SidecarKernel,
948    process: &'a mut ActiveProcess,
949    resource_limits: &'a ResourceLimits,
950    request_key: (u64, u64),
951}
952
953pub(crate) struct LoopbackHttpDispatchRequest<'a, B> {
954    pub(crate) bridge: &'a SharedBridge<B>,
955    pub(crate) vm_id: &'a str,
956    pub(crate) dns: &'a VmDnsConfig,
957    pub(crate) socket_paths: &'a JavascriptSocketPathContext,
958    pub(crate) kernel: &'a mut SidecarKernel,
959    pub(crate) process: &'a mut ActiveProcess,
960    pub(crate) resource_limits: &'a ResourceLimits,
961    pub(crate) server_id: u64,
962    pub(crate) request_json: &'a str,
963}
964
965struct JavascriptDgramSyncRpcServiceRequest<'a, B> {
966    bridge: &'a SharedBridge<B>,
967    kernel: &'a mut SidecarKernel,
968    vm_id: &'a str,
969    dns: &'a VmDnsConfig,
970    socket_paths: &'a JavascriptSocketPathContext,
971    process: &'a mut ActiveProcess,
972    sync_request: &'a JavascriptSyncRpcRequest,
973    resource_limits: &'a ResourceLimits,
974    network_counts: NetworkResourceCounts,
975}
976
977struct JavascriptHttp2SyncRpcServiceRequest<'a, B> {
978    bridge: &'a SharedBridge<B>,
979    kernel: &'a mut SidecarKernel,
980    vm_id: &'a str,
981    dns: &'a VmDnsConfig,
982    socket_paths: &'a JavascriptSocketPathContext,
983    process: &'a mut ActiveProcess,
984    sync_request: &'a JavascriptSyncRpcRequest,
985    resource_limits: &'a ResourceLimits,
986    network_counts: NetworkResourceCounts,
987}
988
989impl ActiveTcpSocket {
990    fn connect<B>(request: ActiveTcpConnectRequest<'_, B>) -> Result<Self, SidecarError>
991    where
992        B: NativeSidecarBridge + Send + 'static,
993        BridgeError<B>: fmt::Debug + Send + Sync + 'static,
994    {
995        let ActiveTcpConnectRequest {
996            bridge,
997            kernel,
998            kernel_pid,
999            vm_id,
1000            dns,
1001            host,
1002            port,
1003            local_address,
1004            local_port,
1005            local_reservation,
1006            context,
1007        } = request;
1008        let resolved = resolve_tcp_connect_addr(bridge, kernel, vm_id, dns, host, port, context)?;
1009        if resolved.use_kernel_loopback {
1010            let family = JavascriptSocketFamily::from_ip(resolved.guest_remote_addr.ip());
1011            let requested_local_port = local_port.unwrap_or(0);
1012            let local_port = if requested_local_port != 0
1013                && local_reservation == Some((family, requested_local_port))
1014            {
1015                requested_local_port
1016            } else {
1017                allocate_guest_listen_port(
1018                    requested_local_port,
1019                    family,
1020                    &context.used_tcp_guest_ports,
1021                    context.listen_policy,
1022                )?
1023            };
1024            let local_ip = match (family, local_address) {
1025                (JavascriptSocketFamily::Ipv4, Some("0.0.0.0")) => {
1026                    IpAddr::V4(Ipv4Addr::UNSPECIFIED)
1027                }
1028                (JavascriptSocketFamily::Ipv4, Some("127.0.0.1") | Some("localhost") | None) => {
1029                    IpAddr::V4(Ipv4Addr::LOCALHOST)
1030                }
1031                (JavascriptSocketFamily::Ipv6, Some("::")) => IpAddr::V6(Ipv6Addr::UNSPECIFIED),
1032                (JavascriptSocketFamily::Ipv6, Some("::1") | Some("localhost") | None) => {
1033                    IpAddr::V6(Ipv6Addr::LOCALHOST)
1034                }
1035                (JavascriptSocketFamily::Ipv4, Some(other)) => {
1036                    return Err(SidecarError::Execution(format!(
1037                        "EACCES: TCP sockets must bind to loopback or unspecified addresses, got {other}"
1038                    )));
1039                }
1040                (JavascriptSocketFamily::Ipv6, Some(other)) => {
1041                    return Err(SidecarError::Execution(format!(
1042                        "EACCES: TCP sockets must bind to loopback or unspecified addresses, got {other}"
1043                    )));
1044                }
1045            };
1046            let local_addr = SocketAddr::new(local_ip, local_port);
1047            let spec = match family {
1048                JavascriptSocketFamily::Ipv4 => SocketSpec::tcp(),
1049                JavascriptSocketFamily::Ipv6 => {
1050                    SocketSpec::new(SocketDomain::Inet6, SocketType::Stream)
1051                }
1052            };
1053            let socket_id = kernel
1054                .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
1055                .map_err(kernel_error)?;
1056            kernel
1057                .socket_bind_inet(
1058                    EXECUTION_DRIVER_NAME,
1059                    kernel_pid,
1060                    socket_id,
1061                    InetSocketAddress::new(local_ip.to_string(), local_port),
1062                )
1063                .map_err(kernel_error)?;
1064            kernel
1065                .socket_connect_inet_loopback(
1066                    EXECUTION_DRIVER_NAME,
1067                    kernel_pid,
1068                    socket_id,
1069                    InetSocketAddress::new(
1070                        resolved.guest_remote_addr.ip().to_string(),
1071                        resolved.guest_remote_addr.port(),
1072                    ),
1073                )
1074                .map_err(kernel_error)?;
1075            return Ok(Self::from_kernel(
1076                socket_id,
1077                None,
1078                local_addr,
1079                resolved.guest_remote_addr,
1080            ));
1081        }
1082
1083        let stream = TcpStream::connect_timeout(&resolved.actual_addr, Duration::from_secs(30))
1084            .map_err(sidecar_net_error)?;
1085        let guest_local_addr = stream.local_addr().map_err(sidecar_net_error)?;
1086        Self::from_stream(stream, None, guest_local_addr, resolved.guest_remote_addr)
1087    }
1088
1089    fn from_stream(
1090        stream: TcpStream,
1091        listener_id: Option<String>,
1092        guest_local_addr: SocketAddr,
1093        guest_remote_addr: SocketAddr,
1094    ) -> Result<Self, SidecarError> {
1095        let read_stream = stream.try_clone().map_err(sidecar_net_error)?;
1096        read_stream
1097            .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
1098            .map_err(sidecar_net_error)?;
1099        let stream = Arc::new(Mutex::new(stream));
1100        let pending_read_stream = Arc::new(Mutex::new(Some(read_stream)));
1101        let (sender, events) = mpsc::channel();
1102        let tls_mode = Arc::new(AtomicBool::new(false));
1103        let tls_stream = Arc::new(Mutex::new(None));
1104        let tls_state = Arc::new(Mutex::new(None));
1105        let saw_local_shutdown = Arc::new(AtomicBool::new(false));
1106        let saw_remote_end = Arc::new(AtomicBool::new(false));
1107        let close_notified = Arc::new(AtomicBool::new(false));
1108
1109        Ok(Self {
1110            stream: Some(stream),
1111            pending_read_stream: Some(pending_read_stream),
1112            events: Some(events),
1113            event_sender: Some(sender),
1114            kernel_socket_id: None,
1115            no_delay: false,
1116            keep_alive: false,
1117            keep_alive_initial_delay_secs: None,
1118            guest_local_addr,
1119            guest_remote_addr,
1120            listener_id,
1121            tls_mode,
1122            tls_stream,
1123            tls_state,
1124            saw_local_shutdown,
1125            saw_remote_end,
1126            close_notified,
1127        })
1128    }
1129
1130    fn from_kernel(
1131        socket_id: SocketId,
1132        listener_id: Option<String>,
1133        guest_local_addr: SocketAddr,
1134        guest_remote_addr: SocketAddr,
1135    ) -> Self {
1136        let (sender, events) = mpsc::channel();
1137        Self {
1138            stream: None,
1139            pending_read_stream: None,
1140            events: Some(events),
1141            event_sender: Some(sender),
1142            kernel_socket_id: Some(socket_id),
1143            no_delay: false,
1144            keep_alive: false,
1145            keep_alive_initial_delay_secs: None,
1146            guest_local_addr,
1147            guest_remote_addr,
1148            listener_id,
1149            tls_mode: Arc::new(AtomicBool::new(false)),
1150            tls_stream: Arc::new(Mutex::new(None)),
1151            tls_state: Arc::new(Mutex::new(None)),
1152            saw_local_shutdown: Arc::new(AtomicBool::new(false)),
1153            saw_remote_end: Arc::new(AtomicBool::new(false)),
1154            close_notified: Arc::new(AtomicBool::new(false)),
1155        }
1156    }
1157
1158    fn poll(
1159        &mut self,
1160        kernel: &mut SidecarKernel,
1161        kernel_pid: u32,
1162        wait: Duration,
1163    ) -> Result<Option<JavascriptTcpSocketEvent>, SidecarError> {
1164        if self.tls_mode.load(Ordering::SeqCst) {
1165            self.ensure_tcp_reader()?;
1166            return match self
1167                .events
1168                .as_ref()
1169                .ok_or_else(|| {
1170                    SidecarError::InvalidState(String::from("TCP socket event channel missing"))
1171                })?
1172                .recv_timeout(wait)
1173            {
1174                Ok(event) => Ok(Some(event)),
1175                Err(RecvTimeoutError::Timeout) => Ok(None),
1176                Err(RecvTimeoutError::Disconnected) => Ok(None),
1177            };
1178        }
1179
1180        if let Some(socket_id) = self.kernel_socket_id {
1181            let result = kernel
1182                .poll_targets(
1183                    EXECUTION_DRIVER_NAME,
1184                    kernel_pid,
1185                    vec![PollTargetEntry::socket(
1186                        socket_id,
1187                        POLLIN | POLLHUP | POLLERR,
1188                    )],
1189                    i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
1190                )
1191                .map_err(kernel_error)?;
1192            let revents = result
1193                .targets
1194                .first()
1195                .map(|entry| entry.revents)
1196                .unwrap_or_else(PollEvents::empty);
1197            if revents.is_empty() {
1198                return Ok(None);
1199            }
1200            if revents.intersects(POLLIN) {
1201                return match kernel.socket_read(
1202                    EXECUTION_DRIVER_NAME,
1203                    kernel_pid,
1204                    socket_id,
1205                    64 * 1024,
1206                ) {
1207                    Ok(Some(bytes)) if !bytes.is_empty() => {
1208                        Ok(Some(JavascriptTcpSocketEvent::Data(bytes)))
1209                    }
1210                    Ok(Some(_)) => Ok(Some(JavascriptTcpSocketEvent::Data(Vec::new()))),
1211                    Ok(None) => Ok(Some(JavascriptTcpSocketEvent::End)),
1212                    Err(error) if error.code() == "EAGAIN" => Ok(None),
1213                    Err(error) => Ok(Some(JavascriptTcpSocketEvent::Error {
1214                        code: Some(error.code().to_string()),
1215                        message: error.to_string(),
1216                    })),
1217                };
1218            }
1219            if revents.intersects(POLLHUP) {
1220                return Ok(Some(JavascriptTcpSocketEvent::End));
1221            }
1222            if revents.intersects(POLLERR) {
1223                return Ok(Some(JavascriptTcpSocketEvent::Error {
1224                    code: Some(String::from("EPIPE")),
1225                    message: String::from("kernel TCP socket reported POLLERR"),
1226                }));
1227            }
1228            return Ok(None);
1229        }
1230
1231        self.ensure_tcp_reader()?;
1232        match self
1233            .events
1234            .as_ref()
1235            .ok_or_else(|| {
1236                SidecarError::InvalidState(String::from("TCP socket event channel missing"))
1237            })?
1238            .recv_timeout(wait)
1239        {
1240            Ok(event) => Ok(Some(event)),
1241            Err(RecvTimeoutError::Timeout) => Ok(None),
1242            Err(RecvTimeoutError::Disconnected) => Ok(None),
1243        }
1244    }
1245
1246    fn ensure_tcp_reader(&self) -> Result<(), SidecarError> {
1247        if self.kernel_socket_id.is_some() {
1248            return Ok(());
1249        }
1250        if self.tls_mode.load(Ordering::SeqCst) {
1251            return Ok(());
1252        }
1253        let read_stream = self
1254            .pending_read_stream
1255            .as_ref()
1256            .ok_or_else(|| {
1257                SidecarError::InvalidState(String::from("TCP socket reader handle missing"))
1258            })?
1259            .lock()
1260            .map_err(|_| {
1261                SidecarError::InvalidState(String::from("TCP socket reader lock poisoned"))
1262            })?
1263            .take();
1264        if let Some(read_stream) = read_stream {
1265            spawn_tcp_socket_reader(
1266                read_stream,
1267                self.event_sender
1268                    .as_ref()
1269                    .ok_or_else(|| {
1270                        SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1271                    })?
1272                    .clone(),
1273                Arc::clone(&self.tls_mode),
1274                Arc::clone(&self.saw_local_shutdown),
1275                Arc::clone(&self.saw_remote_end),
1276                Arc::clone(&self.close_notified),
1277            );
1278        }
1279        Ok(())
1280    }
1281
1282    fn socket_info(&self) -> Value {
1283        json!({
1284            "localAddress": self.guest_local_addr.ip().to_string(),
1285            "localPort": self.guest_local_addr.port(),
1286            "localFamily": socket_addr_family(&self.guest_local_addr),
1287            "remoteAddress": self.guest_remote_addr.ip().to_string(),
1288            "remotePort": self.guest_remote_addr.port(),
1289            "remoteFamily": socket_addr_family(&self.guest_remote_addr),
1290        })
1291    }
1292
1293    fn set_no_delay(&mut self, enable: bool) -> Result<(), SidecarError> {
1294        self.no_delay = enable;
1295        if self.kernel_socket_id.is_some() {
1296            return Ok(());
1297        }
1298        let stream = self
1299            .stream
1300            .as_ref()
1301            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1302            .lock()
1303            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1304        stream.set_nodelay(enable).map_err(sidecar_net_error)
1305    }
1306
1307    fn set_keep_alive(
1308        &mut self,
1309        enable: bool,
1310        initial_delay_secs: Option<u64>,
1311    ) -> Result<(), SidecarError> {
1312        self.keep_alive = enable;
1313        self.keep_alive_initial_delay_secs = initial_delay_secs;
1314        if self.kernel_socket_id.is_some() {
1315            return Ok(());
1316        }
1317        let stream = self
1318            .stream
1319            .as_ref()
1320            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1321            .lock()
1322            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1323        let socket = SockRef::from(&*stream);
1324        socket.set_keepalive(enable).map_err(sidecar_net_error)?;
1325        if enable {
1326            if let Some(delay_secs) = initial_delay_secs.filter(|delay_secs| *delay_secs > 0) {
1327                socket
1328                    .set_tcp_keepalive(
1329                        &TcpKeepalive::new().with_time(Duration::from_secs(delay_secs)),
1330                    )
1331                    .map_err(sidecar_net_error)?;
1332            }
1333        }
1334        Ok(())
1335    }
1336
1337    fn upgrade_tls(
1338        &self,
1339        vm_id: &str,
1340        kernel: &SidecarKernel,
1341        options: JavascriptTlsBridgeOptions,
1342    ) -> Result<(), SidecarError> {
1343        if self.tls_mode.load(Ordering::SeqCst) {
1344            return Ok(());
1345        }
1346
1347        let client_hello = if options.is_server {
1348            self.peek_tls_client_hello(vm_id, kernel)?
1349        } else {
1350            None
1351        };
1352
1353        let tls_stream = if let Some(socket_id) = self.kernel_socket_id {
1354            let peer_socket_id = wait_for_loopback_peer_socket_id(kernel, socket_id)
1355                .ok_or_else(|| {
1356                    SidecarError::Execution(format!(
1357                        "ERR_NOT_IMPLEMENTED: kernel-backed loopback socket {socket_id} has no peer for TLS upgrade"
1358                    ))
1359                })?;
1360            let endpoint = loopback_tls_endpoint(vm_id, socket_id, peer_socket_id)?;
1361            if options.is_server {
1362                ActiveTlsStream::LoopbackServer(build_server_loopback_tls_stream(
1363                    endpoint, &options,
1364                )?)
1365            } else {
1366                ActiveTlsStream::LoopbackClient(build_client_loopback_tls_stream(
1367                    endpoint, &options,
1368                )?)
1369            }
1370        } else {
1371            self.pending_read_stream
1372                .as_ref()
1373                .ok_or_else(|| {
1374                    SidecarError::InvalidState(String::from("TCP socket reader handle missing"))
1375                })?
1376                .lock()
1377                .map_err(|_| {
1378                    SidecarError::InvalidState(String::from("TCP socket reader lock poisoned"))
1379                })?
1380                .take();
1381            let stream = self
1382                .stream
1383                .as_ref()
1384                .ok_or_else(|| {
1385                    SidecarError::InvalidState(String::from("TCP socket stream missing"))
1386                })?
1387                .lock()
1388                .map_err(|_| {
1389                    SidecarError::InvalidState(String::from("TCP socket lock poisoned"))
1390                })?;
1391            let cloned = stream.try_clone().map_err(sidecar_net_error)?;
1392            drop(stream);
1393
1394            if options.is_server {
1395                ActiveTlsStream::Server(build_server_tls_stream(cloned, &options)?)
1396            } else {
1397                ActiveTlsStream::Client(build_client_tls_stream(cloned, &options)?)
1398            }
1399        };
1400
1401        let tls_state = ActiveTlsState {
1402            client_hello,
1403            local_certificates: tls_local_certificates(&options)?,
1404            session_reused: false,
1405        };
1406
1407        self.tls_mode.store(true, Ordering::SeqCst);
1408        {
1409            let mut state = self
1410                .tls_state
1411                .lock()
1412                .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?;
1413            *state = Some(tls_state);
1414        }
1415        {
1416            let mut stream = self.tls_stream.lock().map_err(|_| {
1417                SidecarError::InvalidState(String::from("TLS stream lock poisoned"))
1418            })?;
1419            *stream = Some(tls_stream);
1420        }
1421
1422        spawn_tls_socket_reader(
1423            Arc::clone(&self.tls_stream),
1424            self.event_sender
1425                .as_ref()
1426                .ok_or_else(|| {
1427                    SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1428                })?
1429                .clone(),
1430            Arc::clone(&self.saw_local_shutdown),
1431            Arc::clone(&self.saw_remote_end),
1432            Arc::clone(&self.close_notified),
1433        );
1434        Ok(())
1435    }
1436
1437    fn peek_tls_client_hello(
1438        &self,
1439        vm_id: &str,
1440        kernel: &SidecarKernel,
1441    ) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
1442        if let Some(socket_id) = self.kernel_socket_id {
1443            let Some(peer_socket_id) = kernel
1444                .socket_get(socket_id)
1445                .and_then(|record| record.peer_socket_id())
1446            else {
1447                return Ok(None);
1448            };
1449            return peek_loopback_tls_client_hello(vm_id, socket_id, peer_socket_id);
1450        }
1451
1452        let stream = self
1453            .stream
1454            .as_ref()
1455            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1456            .lock()
1457            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1458        let mut buffer = vec![0_u8; 16 * 1024];
1459        let bytes = match stream.peek(&mut buffer) {
1460            Ok(0) => return Ok(None),
1461            Ok(bytes) => bytes,
1462            Err(error)
1463                if matches!(
1464                    error.kind(),
1465                    std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
1466                ) =>
1467            {
1468                return Ok(None);
1469            }
1470            Err(error) => return Err(sidecar_net_error(error)),
1471        };
1472        parse_tls_client_hello_from_bytes(&buffer[..bytes])
1473    }
1474
1475    fn tls_client_hello_json(
1476        &self,
1477        vm_id: &str,
1478        kernel: &SidecarKernel,
1479    ) -> Result<Value, SidecarError> {
1480        if let Some(client_hello) = self
1481            .tls_state
1482            .lock()
1483            .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?
1484            .as_ref()
1485            .and_then(|state| state.client_hello.clone())
1486        {
1487            return javascript_net_json_string(
1488                serde_json::to_value(client_hello).map_err(|error| {
1489                    SidecarError::InvalidState(format!(
1490                        "failed to serialize TLS client hello: {error}"
1491                    ))
1492                })?,
1493                "net.socket_get_tls_client_hello",
1494            );
1495        }
1496
1497        javascript_net_json_string(
1498            serde_json::to_value(
1499                self.peek_tls_client_hello(vm_id, kernel)?
1500                    .unwrap_or_default(),
1501            )
1502            .map_err(|error| {
1503                SidecarError::InvalidState(format!("failed to serialize TLS client hello: {error}"))
1504            })?,
1505            "net.socket_get_tls_client_hello",
1506        )
1507    }
1508
1509    fn tls_query(&self, query: &str, detailed: bool) -> Result<Value, SidecarError> {
1510        let state = self
1511            .tls_state
1512            .lock()
1513            .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?
1514            .clone();
1515        let mut tls_stream = self
1516            .tls_stream
1517            .lock()
1518            .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?;
1519        let Some(stream) = tls_stream.as_mut() else {
1520            return javascript_net_json_string(
1521                tls_bridge_undefined_value(),
1522                "net.socket_tls_query",
1523            );
1524        };
1525
1526        let payload = match query {
1527            "getSession" => tls_bridge_undefined_value(),
1528            "isSessionReused" => Value::Bool(
1529                state
1530                    .as_ref()
1531                    .is_some_and(|tls_state| tls_state.session_reused),
1532            ),
1533            "getPeerCertificate" => {
1534                let certificate = stream
1535                    .peer_certificates()
1536                    .and_then(|certificates| certificates.first())
1537                    .map(|certificate| {
1538                        tls_certificate_bridge_value(certificate.as_ref(), detailed)
1539                    });
1540                certificate.unwrap_or_else(tls_bridge_undefined_value)
1541            }
1542            "getCertificate" => state
1543                .as_ref()
1544                .and_then(|tls_state| tls_state.local_certificates.first())
1545                .map(|certificate| tls_certificate_bridge_value(certificate, detailed))
1546                .unwrap_or_else(tls_bridge_undefined_value),
1547            "getProtocol" => stream
1548                .protocol_version()
1549                .map(tls_protocol_name)
1550                .map(Value::String)
1551                .unwrap_or(Value::Null),
1552            "getCipher" => stream
1553                .negotiated_cipher_suite()
1554                .map(tls_cipher_bridge_value)
1555                .unwrap_or_else(tls_bridge_undefined_value),
1556            other => {
1557                return Err(SidecarError::InvalidState(format!(
1558                    "unsupported TLS query {other}"
1559                )));
1560            }
1561        };
1562        javascript_net_json_string(payload, "net.socket_tls_query")
1563    }
1564
1565    fn write_all(
1566        &self,
1567        kernel: &mut SidecarKernel,
1568        kernel_pid: u32,
1569        contents: &[u8],
1570    ) -> Result<usize, SidecarError> {
1571        if self.tls_mode.load(Ordering::SeqCst) {
1572            let mut tls_stream = self.tls_stream.lock().map_err(|_| {
1573                SidecarError::InvalidState(String::from("TLS stream lock poisoned"))
1574            })?;
1575            let stream = tls_stream.as_mut().ok_or_else(|| {
1576                SidecarError::InvalidState(String::from("TLS stream missing for upgraded socket"))
1577            })?;
1578            stream.write_all(contents)?;
1579            return Ok(contents.len());
1580        }
1581        if let Some(socket_id) = self.kernel_socket_id {
1582            return kernel
1583                .socket_write(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, contents)
1584                .map_err(kernel_error);
1585        }
1586
1587        let mut stream = self
1588            .stream
1589            .as_ref()
1590            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1591            .lock()
1592            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1593        stream.write_all(contents).map_err(sidecar_net_error)?;
1594        Ok(contents.len())
1595    }
1596
1597    fn shutdown_write(
1598        &self,
1599        kernel: &mut SidecarKernel,
1600        kernel_pid: u32,
1601    ) -> Result<(), SidecarError> {
1602        if self.tls_mode.load(Ordering::SeqCst) {
1603            if let Some(stream) = self
1604                .tls_stream
1605                .lock()
1606                .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?
1607                .as_mut()
1608            {
1609                let _ = stream.send_close_notify();
1610                let _ = stream.shutdown_write();
1611            }
1612            if self.kernel_socket_id.is_some() {
1613                self.saw_local_shutdown.store(true, Ordering::SeqCst);
1614                return Ok(());
1615            }
1616        }
1617        if let Some(socket_id) = self.kernel_socket_id {
1618            return kernel
1619                .socket_shutdown(
1620                    EXECUTION_DRIVER_NAME,
1621                    kernel_pid,
1622                    socket_id,
1623                    KernelSocketShutdown::Write,
1624                )
1625                .map_err(kernel_error);
1626        }
1627        let stream = self
1628            .stream
1629            .as_ref()
1630            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1631            .lock()
1632            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1633        self.saw_local_shutdown.store(true, Ordering::SeqCst);
1634        match stream.shutdown(Shutdown::Write) {
1635            Ok(()) => {}
1636            Err(error) if error.kind() == std::io::ErrorKind::NotConnected => {}
1637            Err(error) => return Err(sidecar_net_error(error)),
1638        }
1639        if self.saw_remote_end.load(Ordering::SeqCst)
1640            && !self.close_notified.swap(true, Ordering::SeqCst)
1641        {
1642            let _ = self
1643                .event_sender
1644                .as_ref()
1645                .ok_or_else(|| {
1646                    SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1647                })?
1648                .send(JavascriptTcpSocketEvent::Close { had_error: false });
1649        }
1650        Ok(())
1651    }
1652
1653    fn close(&self, kernel: &mut SidecarKernel, kernel_pid: u32) -> Result<(), SidecarError> {
1654        if self.tls_mode.load(Ordering::SeqCst) {
1655            if let Some(stream) = self
1656                .tls_stream
1657                .lock()
1658                .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?
1659                .as_mut()
1660            {
1661                let _ = stream.send_close_notify();
1662                let _ = stream.close();
1663            }
1664            if self.kernel_socket_id.is_some() {
1665                return Ok(());
1666            }
1667        }
1668        if let Some(socket_id) = self.kernel_socket_id {
1669            return kernel
1670                .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id)
1671                .map_err(kernel_error);
1672        }
1673        let stream = self
1674            .stream
1675            .as_ref()
1676            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1677            .lock()
1678            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1679        stream.shutdown(Shutdown::Both).map_err(sidecar_net_error)
1680    }
1681}
1682
1683impl ActiveTlsStream {
1684    fn write_all(&mut self, contents: &[u8]) -> Result<(), SidecarError> {
1685        match self {
1686            Self::Client(stream) => {
1687                stream.write_all(contents).map_err(sidecar_net_error)?;
1688                stream.flush().map_err(sidecar_net_error)
1689            }
1690            Self::Server(stream) => {
1691                stream.write_all(contents).map_err(sidecar_net_error)?;
1692                stream.flush().map_err(sidecar_net_error)
1693            }
1694            Self::LoopbackClient(stream) => {
1695                stream.write_all(contents).map_err(sidecar_net_error)?;
1696                stream.flush().map_err(sidecar_net_error)
1697            }
1698            Self::LoopbackServer(stream) => {
1699                stream.write_all(contents).map_err(sidecar_net_error)?;
1700                stream.flush().map_err(sidecar_net_error)
1701            }
1702        }
1703    }
1704
1705    fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
1706        match self {
1707            Self::Client(stream) => stream.read(buffer),
1708            Self::Server(stream) => stream.read(buffer),
1709            Self::LoopbackClient(stream) => stream.read(buffer),
1710            Self::LoopbackServer(stream) => stream.read(buffer),
1711        }
1712    }
1713
1714    fn send_close_notify(&mut self) -> Result<(), SidecarError> {
1715        match self {
1716            Self::Client(stream) => {
1717                stream.conn.send_close_notify();
1718                let _ = stream.conn.complete_io(&mut stream.sock);
1719            }
1720            Self::Server(stream) => {
1721                stream.conn.send_close_notify();
1722                let _ = stream.conn.complete_io(&mut stream.sock);
1723            }
1724            Self::LoopbackClient(stream) => {
1725                stream.conn.send_close_notify();
1726                let _ = stream.conn.complete_io(&mut stream.sock);
1727            }
1728            Self::LoopbackServer(stream) => {
1729                stream.conn.send_close_notify();
1730                let _ = stream.conn.complete_io(&mut stream.sock);
1731            }
1732        }
1733        Ok(())
1734    }
1735
1736    fn shutdown_write(&mut self) -> Result<(), SidecarError> {
1737        match self {
1738            Self::Client(stream) => stream
1739                .sock
1740                .shutdown(Shutdown::Write)
1741                .map_err(sidecar_net_error),
1742            Self::Server(stream) => stream
1743                .sock
1744                .shutdown(Shutdown::Write)
1745                .map_err(sidecar_net_error),
1746            Self::LoopbackClient(stream) => stream.sock.shutdown_write(),
1747            Self::LoopbackServer(stream) => stream.sock.shutdown_write(),
1748        }
1749    }
1750
1751    fn close(&mut self) -> Result<(), SidecarError> {
1752        match self {
1753            Self::Client(stream) => stream
1754                .sock
1755                .shutdown(Shutdown::Both)
1756                .map_err(sidecar_net_error),
1757            Self::Server(stream) => stream
1758                .sock
1759                .shutdown(Shutdown::Both)
1760                .map_err(sidecar_net_error),
1761            Self::LoopbackClient(stream) => stream.sock.close_endpoint(),
1762            Self::LoopbackServer(stream) => stream.sock.close_endpoint(),
1763        }
1764    }
1765
1766    fn peer_certificates(&self) -> Option<&[CertificateDer<'static>]> {
1767        match self {
1768            Self::Client(stream) => stream.conn.peer_certificates(),
1769            Self::Server(stream) => stream.conn.peer_certificates(),
1770            Self::LoopbackClient(stream) => stream.conn.peer_certificates(),
1771            Self::LoopbackServer(stream) => stream.conn.peer_certificates(),
1772        }
1773    }
1774
1775    fn negotiated_cipher_suite(&self) -> Option<rustls::SupportedCipherSuite> {
1776        match self {
1777            Self::Client(stream) => stream.conn.negotiated_cipher_suite(),
1778            Self::Server(stream) => stream.conn.negotiated_cipher_suite(),
1779            Self::LoopbackClient(stream) => stream.conn.negotiated_cipher_suite(),
1780            Self::LoopbackServer(stream) => stream.conn.negotiated_cipher_suite(),
1781        }
1782    }
1783
1784    fn protocol_version(&self) -> Option<rustls::ProtocolVersion> {
1785        match self {
1786            Self::Client(stream) => stream.conn.protocol_version(),
1787            Self::Server(stream) => stream.conn.protocol_version(),
1788            Self::LoopbackClient(stream) => stream.conn.protocol_version(),
1789            Self::LoopbackServer(stream) => stream.conn.protocol_version(),
1790        }
1791    }
1792}
1793
1794// ActiveTcpListener moved to crate::state
1795
1796// Unix socket types moved to crate::state
1797
1798impl ActiveUnixSocket {
1799    fn connect(host_path: &Path, guest_path: &str) -> Result<Self, SidecarError> {
1800        let stream = UnixStream::connect(host_path).map_err(sidecar_net_error)?;
1801        Self::from_stream(stream, None, None, Some(guest_path.to_owned()))
1802    }
1803
1804    fn from_stream(
1805        stream: UnixStream,
1806        listener_id: Option<String>,
1807        local_path: Option<String>,
1808        remote_path: Option<String>,
1809    ) -> Result<Self, SidecarError> {
1810        let read_stream = stream.try_clone().map_err(sidecar_net_error)?;
1811        let stream = Arc::new(Mutex::new(stream));
1812        let (sender, events) = mpsc::channel();
1813        let saw_local_shutdown = Arc::new(AtomicBool::new(false));
1814        let saw_remote_end = Arc::new(AtomicBool::new(false));
1815        let close_notified = Arc::new(AtomicBool::new(false));
1816        spawn_unix_socket_reader(
1817            read_stream,
1818            sender.clone(),
1819            Arc::clone(&saw_local_shutdown),
1820            Arc::clone(&saw_remote_end),
1821            Arc::clone(&close_notified),
1822        );
1823
1824        Ok(Self {
1825            stream,
1826            events,
1827            event_sender: sender,
1828            listener_id,
1829            local_path,
1830            remote_path,
1831            saw_local_shutdown,
1832            saw_remote_end,
1833            close_notified,
1834        })
1835    }
1836
1837    fn poll(&mut self, wait: Duration) -> Result<Option<JavascriptTcpSocketEvent>, SidecarError> {
1838        match self.events.recv_timeout(wait) {
1839            Ok(event) => Ok(Some(event)),
1840            Err(RecvTimeoutError::Timeout) => Ok(None),
1841            Err(RecvTimeoutError::Disconnected) => Ok(None),
1842        }
1843    }
1844
1845    fn socket_info(&self) -> Value {
1846        json!({
1847            "localPath": self.local_path.clone(),
1848            "remotePath": self.remote_path.clone(),
1849        })
1850    }
1851
1852    fn write_all(&self, contents: &[u8]) -> Result<usize, SidecarError> {
1853        let mut stream = self
1854            .stream
1855            .lock()
1856            .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
1857        stream.write_all(contents).map_err(sidecar_net_error)?;
1858        Ok(contents.len())
1859    }
1860
1861    fn shutdown_write(&self) -> Result<(), SidecarError> {
1862        let stream = self
1863            .stream
1864            .lock()
1865            .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
1866        self.saw_local_shutdown.store(true, Ordering::SeqCst);
1867        stream
1868            .shutdown(Shutdown::Write)
1869            .map_err(sidecar_net_error)?;
1870        if self.saw_remote_end.load(Ordering::SeqCst)
1871            && !self.close_notified.swap(true, Ordering::SeqCst)
1872        {
1873            let _ = self
1874                .event_sender
1875                .send(JavascriptTcpSocketEvent::Close { had_error: false });
1876        }
1877        Ok(())
1878    }
1879
1880    fn close(&self) -> Result<(), SidecarError> {
1881        let stream = self
1882            .stream
1883            .lock()
1884            .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
1885        stream.shutdown(Shutdown::Both).map_err(sidecar_net_error)
1886    }
1887}
1888
1889// ActiveUnixListener moved to crate::state
1890
1891impl ActiveUnixListener {
1892    fn bind(
1893        host_path: &Path,
1894        guest_path: &str,
1895        backlog: Option<u32>,
1896    ) -> Result<Self, SidecarError> {
1897        if let Some(parent) = host_path.parent() {
1898            fs::create_dir_all(parent).map_err(sidecar_net_error)?;
1899        }
1900        let listener = UnixListener::bind(host_path).map_err(sidecar_net_error)?;
1901        listener.set_nonblocking(true).map_err(sidecar_net_error)?;
1902        Ok(Self {
1903            listener,
1904            path: guest_path.to_owned(),
1905            backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
1906                .expect("default backlog fits within usize"),
1907            active_connection_ids: BTreeSet::new(),
1908        })
1909    }
1910
1911    fn path(&self) -> &str {
1912        &self.path
1913    }
1914
1915    fn poll(
1916        &mut self,
1917        wait: Duration,
1918    ) -> Result<Option<JavascriptUnixListenerEvent>, SidecarError> {
1919        let deadline = Instant::now() + wait;
1920        loop {
1921            match self.listener.accept() {
1922                Ok((stream, remote_addr)) => {
1923                    if self.active_connection_ids.len() >= self.backlog {
1924                        let _ = stream.shutdown(Shutdown::Both);
1925                        if wait.is_zero() || Instant::now() >= deadline {
1926                            return Ok(None);
1927                        }
1928                        continue;
1929                    }
1930
1931                    let local_path = Some(self.path.clone());
1932                    let remote_path = unix_socket_path(&remote_addr);
1933                    return Ok(Some(JavascriptUnixListenerEvent::Connection(
1934                        PendingUnixSocket {
1935                            stream,
1936                            local_path,
1937                            remote_path,
1938                        },
1939                    )));
1940                }
1941                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
1942                    if wait.is_zero() || Instant::now() >= deadline {
1943                        return Ok(None);
1944                    }
1945                    thread::sleep(Duration::from_millis(10));
1946                }
1947                Err(error) => {
1948                    return Ok(Some(JavascriptUnixListenerEvent::Error {
1949                        code: io_error_code(&error),
1950                        message: error.to_string(),
1951                    }));
1952                }
1953            }
1954        }
1955    }
1956
1957    fn close(&self) -> Result<(), SidecarError> {
1958        Ok(())
1959    }
1960
1961    fn active_connection_count(&self) -> usize {
1962        self.active_connection_ids.len()
1963    }
1964
1965    fn register_connection(&mut self, socket_id: &str) {
1966        self.active_connection_ids.insert(socket_id.to_string());
1967    }
1968
1969    fn release_connection(&mut self, socket_id: &str) {
1970        self.active_connection_ids.remove(socket_id);
1971    }
1972}
1973
1974impl ActiveTcpListener {
1975    fn bind(
1976        bind_host: &str,
1977        guest_host: &str,
1978        guest_port: u16,
1979        backlog: Option<u32>,
1980    ) -> Result<Self, SidecarError> {
1981        let bind_addr = resolve_tcp_bind_addr(bind_host, 0)?;
1982        let guest_addr = resolve_tcp_bind_addr(guest_host, guest_port)?;
1983        let listener = TcpListener::bind(bind_addr).map_err(sidecar_net_error)?;
1984        listener.set_nonblocking(true).map_err(sidecar_net_error)?;
1985        let local_addr = listener.local_addr().map_err(sidecar_net_error)?;
1986        Ok(Self {
1987            listener: Some(listener),
1988            kernel_socket_id: None,
1989            local_addr: Some(local_addr),
1990            guest_local_addr: guest_addr,
1991            backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
1992                .expect("default backlog fits within usize"),
1993            active_connection_ids: BTreeSet::new(),
1994        })
1995    }
1996
1997    fn bind_kernel(
1998        kernel: &mut SidecarKernel,
1999        kernel_pid: u32,
2000        guest_host: &str,
2001        guest_port: u16,
2002        backlog: Option<u32>,
2003    ) -> Result<Self, SidecarError> {
2004        let guest_addr = resolve_tcp_bind_addr(guest_host, guest_port)?;
2005        let spec = match guest_addr {
2006            SocketAddr::V4(_) => SocketSpec::tcp(),
2007            SocketAddr::V6(_) => SocketSpec::new(SocketDomain::Inet6, SocketType::Stream),
2008        };
2009        let socket_id = kernel
2010            .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
2011            .map_err(kernel_error)?;
2012        kernel
2013            .socket_bind_inet(
2014                EXECUTION_DRIVER_NAME,
2015                kernel_pid,
2016                socket_id,
2017                InetSocketAddress::new(guest_addr.ip().to_string(), guest_addr.port()),
2018            )
2019            .map_err(kernel_error)?;
2020        kernel
2021            .socket_listen(
2022                EXECUTION_DRIVER_NAME,
2023                kernel_pid,
2024                socket_id,
2025                usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
2026                    .expect("default backlog fits within usize"),
2027            )
2028            .map_err(kernel_error)?;
2029        Ok(Self {
2030            listener: None,
2031            kernel_socket_id: Some(socket_id),
2032            local_addr: Some(guest_addr),
2033            guest_local_addr: guest_addr,
2034            backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
2035                .expect("default backlog fits within usize"),
2036            active_connection_ids: BTreeSet::new(),
2037        })
2038    }
2039
2040    pub(crate) fn local_addr(&self) -> SocketAddr {
2041        self.local_addr.unwrap_or(self.guest_local_addr)
2042    }
2043
2044    fn guest_local_addr(&self) -> SocketAddr {
2045        self.guest_local_addr
2046    }
2047
2048    fn poll(
2049        &mut self,
2050        kernel: &mut SidecarKernel,
2051        kernel_pid: u32,
2052        wait: Duration,
2053    ) -> Result<Option<JavascriptTcpListenerEvent>, SidecarError> {
2054        if let Some(socket_id) = self.kernel_socket_id {
2055            let result = kernel
2056                .poll_targets(
2057                    EXECUTION_DRIVER_NAME,
2058                    kernel_pid,
2059                    vec![PollTargetEntry::socket(socket_id, POLLIN)],
2060                    i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
2061                )
2062                .map_err(kernel_error)?;
2063            let revents = result
2064                .targets
2065                .first()
2066                .map(|entry| entry.revents)
2067                .unwrap_or_else(PollEvents::empty);
2068            if revents.is_empty() {
2069                return Ok(None);
2070            }
2071            let accepted_socket_id =
2072                match kernel.socket_accept(EXECUTION_DRIVER_NAME, kernel_pid, socket_id) {
2073                    Ok(accepted_socket_id) => accepted_socket_id,
2074                    Err(error) if error.code() == "EAGAIN" => return Ok(None),
2075                    Err(error) => {
2076                        return Ok(Some(JavascriptTcpListenerEvent::Error {
2077                            code: Some(error.code().to_string()),
2078                            message: error.to_string(),
2079                        }));
2080                    }
2081                };
2082            let accepted = kernel.socket_get(accepted_socket_id).ok_or_else(|| {
2083                SidecarError::InvalidState(format!(
2084                    "accepted kernel TCP socket {accepted_socket_id} is missing"
2085                ))
2086            })?;
2087            let local_addr = accepted.local_address().ok_or_else(|| {
2088                SidecarError::InvalidState(format!(
2089                    "accepted kernel TCP socket {accepted_socket_id} missing local address"
2090                ))
2091            })?;
2092            let remote_addr = accepted.peer_address().ok_or_else(|| {
2093                SidecarError::InvalidState(format!(
2094                    "accepted kernel TCP socket {accepted_socket_id} missing peer address"
2095                ))
2096            })?;
2097            return Ok(Some(JavascriptTcpListenerEvent::Connection(
2098                PendingTcpSocket {
2099                    stream: None,
2100                    kernel_socket_id: Some(accepted_socket_id),
2101                    preallocated: true,
2102                    guest_local_addr: resolve_tcp_bind_addr(local_addr.host(), local_addr.port())?,
2103                    guest_remote_addr: resolve_tcp_bind_addr(
2104                        remote_addr.host(),
2105                        remote_addr.port(),
2106                    )?,
2107                },
2108            )));
2109        }
2110
2111        let deadline = Instant::now() + wait;
2112        loop {
2113            match self
2114                .listener
2115                .as_ref()
2116                .ok_or_else(|| {
2117                    SidecarError::InvalidState(String::from("TCP listener socket missing"))
2118                })?
2119                .accept()
2120            {
2121                Ok((stream, remote_addr)) => {
2122                    if self.active_connection_ids.len() >= self.backlog {
2123                        let _ = stream.shutdown(Shutdown::Both);
2124                        if wait.is_zero() || Instant::now() >= deadline {
2125                            return Ok(None);
2126                        }
2127                        continue;
2128                    }
2129                    return Ok(Some(JavascriptTcpListenerEvent::Connection(
2130                        PendingTcpSocket {
2131                            stream: Some(stream),
2132                            kernel_socket_id: None,
2133                            preallocated: false,
2134                            guest_local_addr: self.guest_local_addr,
2135                            guest_remote_addr: SocketAddr::new(
2136                                remote_addr.ip(),
2137                                remote_addr.port(),
2138                            ),
2139                        },
2140                    )));
2141                }
2142                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
2143                    if wait.is_zero() || Instant::now() >= deadline {
2144                        return Ok(None);
2145                    }
2146                    thread::sleep(Duration::from_millis(10));
2147                }
2148                Err(error) => {
2149                    return Ok(Some(JavascriptTcpListenerEvent::Error {
2150                        code: io_error_code(&error),
2151                        message: error.to_string(),
2152                    }));
2153                }
2154            }
2155        }
2156    }
2157
2158    fn close(&self, kernel: &mut SidecarKernel, kernel_pid: u32) -> Result<(), SidecarError> {
2159        if let Some(socket_id) = self.kernel_socket_id {
2160            kernel
2161                .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id)
2162                .map_err(kernel_error)?;
2163        }
2164        Ok(())
2165    }
2166
2167    fn active_connection_count(&self) -> usize {
2168        self.active_connection_ids.len()
2169    }
2170
2171    fn register_connection(&mut self, socket_id: &str) {
2172        self.active_connection_ids.insert(socket_id.to_string());
2173    }
2174
2175    fn release_connection(&mut self, socket_id: &str) {
2176        self.active_connection_ids.remove(socket_id);
2177    }
2178}
2179
2180// UDP types moved to crate::state
2181
2182impl ActiveUdpSocket {
2183    fn new(
2184        kernel: &mut SidecarKernel,
2185        kernel_pid: u32,
2186        family: JavascriptUdpFamily,
2187    ) -> Result<Self, SidecarError> {
2188        let spec = match family {
2189            JavascriptUdpFamily::Ipv4 => SocketSpec::udp(),
2190            JavascriptUdpFamily::Ipv6 => SocketSpec::new(SocketDomain::Inet6, SocketType::Datagram),
2191        };
2192        let socket_id = kernel
2193            .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
2194            .map_err(kernel_error)?;
2195        Ok(Self {
2196            family,
2197            socket: None,
2198            kernel_socket_id: Some(socket_id),
2199            guest_local_addr: None,
2200            recv_buffer_size: 0,
2201            send_buffer_size: 0,
2202        })
2203    }
2204
2205    fn local_addr(&self) -> Option<SocketAddr> {
2206        self.guest_local_addr
2207    }
2208
2209    fn socket(&self) -> Result<&UdpSocket, SidecarError> {
2210        self.socket
2211            .as_ref()
2212            .ok_or_else(|| SidecarError::Execution(String::from("EBADF: bad file descriptor")))
2213    }
2214
2215    fn bind(
2216        &mut self,
2217        kernel: &mut SidecarKernel,
2218        kernel_pid: u32,
2219        host: Option<&str>,
2220        port: u16,
2221        context: &JavascriptSocketPathContext,
2222    ) -> Result<SocketAddr, SidecarError> {
2223        if self.socket.is_some() || self.guest_local_addr.is_some() {
2224            return Err(SidecarError::Execution(String::from(
2225                "EINVAL: secure-exec dgram socket is already bound",
2226            )));
2227        }
2228
2229        let (bind_host, guest_host, guest_family) = normalize_udp_bind_host(host, self.family)?;
2230        let guest_port = allocate_guest_listen_port(
2231            port,
2232            guest_family,
2233            &context.used_udp_guest_ports,
2234            context.listen_policy,
2235        )?;
2236        let local_addr = resolve_udp_bind_addr(guest_host, guest_port, self.family)?;
2237        if let Some(socket_id) = self.kernel_socket_id {
2238            kernel
2239                .socket_bind_inet(
2240                    EXECUTION_DRIVER_NAME,
2241                    kernel_pid,
2242                    socket_id,
2243                    InetSocketAddress::new(local_addr.ip().to_string(), local_addr.port()),
2244                )
2245                .map_err(kernel_error)?;
2246        } else {
2247            let bind_addr = resolve_udp_bind_addr(bind_host, 0, self.family)?;
2248            let socket = UdpSocket::bind(bind_addr).map_err(sidecar_net_error)?;
2249            socket.set_nonblocking(true).map_err(sidecar_net_error)?;
2250            self.socket = Some(socket);
2251        }
2252        self.guest_local_addr = Some(local_addr);
2253        Ok(local_addr)
2254    }
2255
2256    fn ensure_bound_for_send(
2257        &mut self,
2258        kernel: &mut SidecarKernel,
2259        kernel_pid: u32,
2260        context: &JavascriptSocketPathContext,
2261    ) -> Result<SocketAddr, SidecarError> {
2262        if let Some(local_addr) = self.local_addr() {
2263            return Ok(local_addr);
2264        }
2265
2266        self.bind(kernel, kernel_pid, None, 0, context)
2267    }
2268
2269    fn send_to<B>(
2270        &mut self,
2271        request: ActiveUdpSendToRequest<'_, B>,
2272    ) -> Result<(usize, SocketAddr), SidecarError>
2273    where
2274        B: NativeSidecarBridge + Send + 'static,
2275        BridgeError<B>: fmt::Debug + Send + Sync + 'static,
2276    {
2277        let ActiveUdpSendToRequest {
2278            bridge,
2279            kernel,
2280            kernel_pid,
2281            vm_id,
2282            dns,
2283            host,
2284            port,
2285            context,
2286            contents,
2287        } = request;
2288        let remote_addr = resolve_udp_addr(UdpRemoteAddrRequest {
2289            bridge,
2290            kernel,
2291            vm_id,
2292            dns,
2293            host,
2294            port,
2295            family: self.family,
2296            context,
2297        })?;
2298        let local_addr = self.ensure_bound_for_send(kernel, kernel_pid, context)?;
2299        let written = if let Some(socket_id) = self.kernel_socket_id {
2300            if is_loopback_ip(remote_addr.ip()) && remote_addr.port() == port {
2301                kernel
2302                    .socket_send_to_inet_loopback(
2303                        EXECUTION_DRIVER_NAME,
2304                        kernel_pid,
2305                        socket_id,
2306                        InetSocketAddress::new(remote_addr.ip().to_string(), remote_addr.port()),
2307                        contents,
2308                    )
2309                    .map_err(kernel_error)?
2310            } else {
2311                return Err(SidecarError::Execution(String::from(
2312                    "ERR_NOT_IMPLEMENTED: external UDP datagrams are not yet supported by the kernel-backed V8 bridge",
2313                )));
2314            }
2315        } else {
2316            let socket = self.socket.as_ref().ok_or_else(|| {
2317                SidecarError::InvalidState(String::from("UDP socket is not initialized"))
2318            })?;
2319            socket
2320                .send_to(contents, remote_addr)
2321                .map_err(sidecar_net_error)?
2322        };
2323        Ok((written, local_addr))
2324    }
2325
2326    fn poll(
2327        &self,
2328        kernel: &mut SidecarKernel,
2329        kernel_pid: u32,
2330        wait: Duration,
2331    ) -> Result<Option<JavascriptUdpSocketEvent>, SidecarError> {
2332        if let Some(socket_id) = self.kernel_socket_id {
2333            let result = kernel
2334                .poll_targets(
2335                    EXECUTION_DRIVER_NAME,
2336                    kernel_pid,
2337                    vec![PollTargetEntry::socket(socket_id, POLLIN)],
2338                    i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
2339                )
2340                .map_err(kernel_error)?;
2341            let revents = result
2342                .targets
2343                .first()
2344                .map(|entry| entry.revents)
2345                .unwrap_or_else(PollEvents::empty);
2346            if revents.is_empty() {
2347                return Ok(None);
2348            }
2349            return match kernel.socket_recv_datagram(
2350                EXECUTION_DRIVER_NAME,
2351                kernel_pid,
2352                socket_id,
2353                64 * 1024,
2354            ) {
2355                Ok(Some(datagram)) => {
2356                    let (source_address, payload) = datagram.into_parts();
2357                    let remote_addr = source_address
2358                        .map(|source| {
2359                            resolve_udp_bind_addr(source.host(), source.port(), self.family)
2360                        })
2361                        .transpose()?
2362                        .unwrap_or_else(|| match self.family {
2363                            JavascriptUdpFamily::Ipv4 => {
2364                                SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)
2365                            }
2366                            JavascriptUdpFamily::Ipv6 => {
2367                                SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0)
2368                            }
2369                        });
2370                    Ok(Some(JavascriptUdpSocketEvent::Message {
2371                        data: payload,
2372                        remote_addr,
2373                    }))
2374                }
2375                Ok(None) => Ok(None),
2376                Err(error) if error.code() == "EAGAIN" => Ok(None),
2377                Err(error) => Ok(Some(JavascriptUdpSocketEvent::Error {
2378                    code: Some(error.code().to_string()),
2379                    message: error.to_string(),
2380                })),
2381            };
2382        }
2383        let socket = self.socket()?;
2384        let deadline = Instant::now() + wait;
2385        let mut buffer = vec![0_u8; 64 * 1024];
2386
2387        loop {
2388            match socket.recv_from(&mut buffer) {
2389                Ok((bytes_read, remote_addr)) => {
2390                    return Ok(Some(JavascriptUdpSocketEvent::Message {
2391                        data: buffer[..bytes_read].to_vec(),
2392                        remote_addr,
2393                    }));
2394                }
2395                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
2396                    if wait.is_zero() || Instant::now() >= deadline {
2397                        return Ok(None);
2398                    }
2399                    thread::sleep(Duration::from_millis(10));
2400                }
2401                Err(error) => {
2402                    return Ok(Some(JavascriptUdpSocketEvent::Error {
2403                        code: io_error_code(&error),
2404                        message: error.to_string(),
2405                    }));
2406                }
2407            }
2408        }
2409    }
2410
2411    fn close(&mut self, kernel: &mut SidecarKernel, kernel_pid: u32) {
2412        if let Some(socket_id) = self.kernel_socket_id {
2413            let _ = kernel.socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id);
2414        }
2415        self.socket.take();
2416        self.guest_local_addr = None;
2417    }
2418
2419    fn set_buffer_size(&mut self, which: &str, size: usize) -> Result<(), SidecarError> {
2420        match which {
2421            "recv" => self.recv_buffer_size = size,
2422            "send" => self.send_buffer_size = size,
2423            other => {
2424                return Err(SidecarError::InvalidState(format!(
2425                    "unsupported UDP buffer size kind {other}"
2426                )));
2427            }
2428        }
2429        if self.kernel_socket_id.is_some() {
2430            return Ok(());
2431        }
2432        let socket = self.socket()?;
2433        let socket = SockRef::from(socket);
2434        match which {
2435            "recv" => socket.set_recv_buffer_size(size).map_err(sidecar_net_error),
2436            "send" => socket.set_send_buffer_size(size).map_err(sidecar_net_error),
2437            other => Err(SidecarError::InvalidState(format!(
2438                "unsupported UDP buffer size kind {other}"
2439            ))),
2440        }
2441    }
2442
2443    fn get_buffer_size(&self, which: &str) -> Result<usize, SidecarError> {
2444        if self.kernel_socket_id.is_some() {
2445            return Ok(match which {
2446                "recv" => self.recv_buffer_size,
2447                "send" => self.send_buffer_size,
2448                other => {
2449                    return Err(SidecarError::InvalidState(format!(
2450                        "unsupported UDP buffer size kind {other}"
2451                    )));
2452                }
2453            });
2454        }
2455        let socket = self.socket()?;
2456        let socket = SockRef::from(socket);
2457        match which {
2458            "recv" => socket.recv_buffer_size().map_err(sidecar_net_error),
2459            "send" => socket.send_buffer_size().map_err(sidecar_net_error),
2460            other => Err(SidecarError::InvalidState(format!(
2461                "unsupported UDP buffer size kind {other}"
2462            ))),
2463        }
2464    }
2465}
2466
2467// ActiveExecution, ActiveExecutionEvent, SocketQueryKind moved to crate::state
2468
2469impl ActiveExecution {
2470    pub(crate) fn uses_shared_v8_runtime(&self) -> bool {
2471        match self {
2472            Self::Javascript(execution) => execution.uses_shared_v8_runtime(),
2473            Self::Python(execution) => execution.uses_shared_v8_runtime(),
2474            Self::Wasm(execution) => execution.uses_shared_v8_runtime(),
2475            Self::Tool(_) => false,
2476        }
2477    }
2478
2479    pub(crate) fn child_pid(&self) -> u32 {
2480        match self {
2481            Self::Javascript(execution) => execution.child_pid(),
2482            Self::Python(execution) => execution.child_pid(),
2483            Self::Wasm(execution) => execution.child_pid(),
2484            Self::Tool(_) => 0,
2485        }
2486    }
2487
2488    pub(crate) fn write_stdin(&mut self, chunk: &[u8]) -> Result<(), SidecarError> {
2489        match self {
2490            Self::Javascript(execution) => execution
2491                .write_stdin(chunk)
2492                .map_err(|error| SidecarError::Execution(error.to_string())),
2493            Self::Python(execution) => execution
2494                .write_stdin(chunk)
2495                .map_err(|error| SidecarError::Execution(error.to_string())),
2496            Self::Wasm(execution) => execution
2497                .write_stdin(chunk)
2498                .map_err(|error| SidecarError::Execution(error.to_string())),
2499            Self::Tool(_) => Ok(()),
2500        }
2501    }
2502
2503    pub(crate) fn close_stdin(&mut self) -> Result<(), SidecarError> {
2504        match self {
2505            Self::Javascript(execution) => execution
2506                .close_stdin()
2507                .map_err(|error| SidecarError::Execution(error.to_string())),
2508            Self::Python(execution) => execution
2509                .close_stdin()
2510                .map_err(|error| SidecarError::Execution(error.to_string())),
2511            Self::Wasm(execution) => execution
2512                .close_stdin()
2513                .map_err(|error| SidecarError::Execution(error.to_string())),
2514            Self::Tool(_) => Ok(()),
2515        }
2516    }
2517
2518    pub(crate) fn respond_python_vfs_rpc_success(
2519        &mut self,
2520        id: u64,
2521        payload: PythonVfsRpcResponsePayload,
2522    ) -> Result<(), SidecarError> {
2523        match self {
2524            Self::Python(execution) => execution
2525                .respond_vfs_rpc_success(id, payload)
2526                .map_err(|error| SidecarError::Execution(error.to_string())),
2527            _ => Err(SidecarError::InvalidState(String::from(
2528                "only Python executions can service Python VFS RPC responses",
2529            ))),
2530        }
2531    }
2532
2533    pub(crate) fn respond_python_vfs_rpc_error(
2534        &mut self,
2535        id: u64,
2536        code: impl Into<String>,
2537        message: impl Into<String>,
2538    ) -> Result<(), SidecarError> {
2539        match self {
2540            Self::Python(execution) => execution
2541                .respond_vfs_rpc_error(id, code, message)
2542                .map_err(|error| SidecarError::Execution(error.to_string())),
2543            _ => Err(SidecarError::InvalidState(String::from(
2544                "only Python executions can service Python VFS RPC responses",
2545            ))),
2546        }
2547    }
2548
2549    pub(crate) fn send_javascript_stream_event(
2550        &self,
2551        event_type: &str,
2552        payload: Value,
2553    ) -> Result<(), SidecarError> {
2554        match self {
2555            Self::Javascript(execution) => execution
2556                .send_stream_event(event_type, payload)
2557                .map_err(|error| SidecarError::Execution(error.to_string())),
2558            Self::Wasm(execution) => execution
2559                .send_stream_event(event_type, payload)
2560                .map_err(|error| SidecarError::Execution(error.to_string())),
2561            _ => Err(SidecarError::InvalidState(String::from(
2562                "only embedded V8 executions can receive JavaScript stream events",
2563            ))),
2564        }
2565    }
2566
2567    pub(crate) fn javascript_v8_session_handle(&self) -> Option<V8SessionHandle> {
2568        match self {
2569            Self::Javascript(execution) => Some(execution.v8_session_handle()),
2570            Self::Wasm(execution) => Some(execution.v8_session_handle()),
2571            _ => None,
2572        }
2573    }
2574
2575    pub(crate) fn terminate(&mut self) -> Result<(), SidecarError> {
2576        match self {
2577            Self::Javascript(execution) => execution
2578                .terminate()
2579                .map_err(|error| SidecarError::Execution(error.to_string())),
2580            Self::Python(execution) => execution
2581                .kill()
2582                .map_err(|error| SidecarError::Execution(error.to_string())),
2583            Self::Wasm(execution) => execution
2584                .terminate()
2585                .map_err(|error| SidecarError::Execution(error.to_string())),
2586            Self::Tool(_) => Ok(()),
2587        }
2588    }
2589
2590    pub(crate) fn respond_javascript_sync_rpc_success(
2591        &mut self,
2592        id: u64,
2593        result: Value,
2594    ) -> Result<(), SidecarError> {
2595        match self {
2596            Self::Javascript(execution) => execution
2597                .respond_sync_rpc_success(id, result)
2598                .map_err(|error| SidecarError::Execution(error.to_string())),
2599            Self::Python(execution) => execution
2600                .respond_javascript_sync_rpc_success(id, result)
2601                .map_err(|error| SidecarError::Execution(error.to_string())),
2602            Self::Wasm(execution) => execution
2603                .respond_sync_rpc_success(id, result)
2604                .map_err(|error| SidecarError::Execution(error.to_string())),
2605            _ => Err(SidecarError::InvalidState(String::from(
2606                "only JavaScript, Python, and WebAssembly executions can service JavaScript sync RPC responses",
2607            ))),
2608        }
2609    }
2610
2611    pub(crate) fn respond_javascript_sync_rpc_error(
2612        &mut self,
2613        id: u64,
2614        code: impl Into<String>,
2615        message: impl Into<String>,
2616    ) -> Result<(), SidecarError> {
2617        match self {
2618            Self::Javascript(execution) => execution
2619                .respond_sync_rpc_error(id, code, message)
2620                .map_err(|error| SidecarError::Execution(error.to_string())),
2621            Self::Python(execution) => execution
2622                .respond_javascript_sync_rpc_error(id, code, message)
2623                .map_err(|error| SidecarError::Execution(error.to_string())),
2624            Self::Wasm(execution) => execution
2625                .respond_sync_rpc_error(id, code, message)
2626                .map_err(|error| SidecarError::Execution(error.to_string())),
2627            _ => Err(SidecarError::InvalidState(String::from(
2628                "only JavaScript, Python, and WebAssembly executions can service JavaScript sync RPC responses",
2629            ))),
2630        }
2631    }
2632
2633    pub(crate) async fn poll_event(
2634        &mut self,
2635        timeout: Duration,
2636    ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
2637        match self {
2638            Self::Javascript(execution) => execution
2639                .poll_event(timeout)
2640                .await
2641                .map(|event| {
2642                    event.map(|event| match event {
2643                        JavascriptExecutionEvent::Stdout(chunk) => {
2644                            ActiveExecutionEvent::Stdout(chunk)
2645                        }
2646                        JavascriptExecutionEvent::Stderr(chunk) => {
2647                            ActiveExecutionEvent::Stderr(chunk)
2648                        }
2649                        JavascriptExecutionEvent::SyncRpcRequest(request) => {
2650                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2651                        }
2652                        JavascriptExecutionEvent::SignalState {
2653                            signal,
2654                            registration,
2655                        } => ActiveExecutionEvent::SignalState {
2656                            signal,
2657                            registration: map_node_signal_registration(registration),
2658                        },
2659                        JavascriptExecutionEvent::Exited(code) => {
2660                            ActiveExecutionEvent::Exited(code)
2661                        }
2662                    })
2663                })
2664                .map_err(|error| SidecarError::Execution(error.to_string())),
2665            Self::Python(execution) => execution
2666                .poll_event(timeout)
2667                .await
2668                .map(|event| {
2669                    event.map(|event| match event {
2670                        PythonExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2671                        PythonExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2672                        PythonExecutionEvent::JavascriptSyncRpcRequest(request) => {
2673                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2674                        }
2675                        PythonExecutionEvent::VfsRpcRequest(request) => {
2676                            ActiveExecutionEvent::PythonVfsRpcRequest(request)
2677                        }
2678                        PythonExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2679                    })
2680                })
2681                .map_err(|error| SidecarError::Execution(error.to_string())),
2682            Self::Wasm(execution) => execution
2683                .poll_event(timeout)
2684                .await
2685                .map(|event| {
2686                    event.map(|event| match event {
2687                        WasmExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2688                        WasmExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2689                        WasmExecutionEvent::SyncRpcRequest(request) => {
2690                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2691                        }
2692                        WasmExecutionEvent::SignalState {
2693                            signal,
2694                            registration,
2695                        } => ActiveExecutionEvent::SignalState {
2696                            signal,
2697                            registration: map_wasm_signal_registration(registration),
2698                        },
2699                        WasmExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2700                    })
2701                })
2702                .map_err(|error| SidecarError::Execution(error.to_string())),
2703            Self::Tool(execution) => {
2704                let _ = timeout;
2705                poll_tool_process_event(execution)
2706            }
2707        }
2708    }
2709
2710    pub(crate) fn poll_event_blocking(
2711        &mut self,
2712        timeout: Duration,
2713    ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
2714        match self {
2715            Self::Javascript(execution) => execution
2716                .poll_event_blocking(timeout)
2717                .map(|event| {
2718                    event.map(|event| match event {
2719                        JavascriptExecutionEvent::Stdout(chunk) => {
2720                            ActiveExecutionEvent::Stdout(chunk)
2721                        }
2722                        JavascriptExecutionEvent::Stderr(chunk) => {
2723                            ActiveExecutionEvent::Stderr(chunk)
2724                        }
2725                        JavascriptExecutionEvent::SyncRpcRequest(request) => {
2726                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2727                        }
2728                        JavascriptExecutionEvent::SignalState {
2729                            signal,
2730                            registration,
2731                        } => ActiveExecutionEvent::SignalState {
2732                            signal,
2733                            registration: map_node_signal_registration(registration),
2734                        },
2735                        JavascriptExecutionEvent::Exited(code) => {
2736                            ActiveExecutionEvent::Exited(code)
2737                        }
2738                    })
2739                })
2740                .map_err(|error| SidecarError::Execution(error.to_string())),
2741            Self::Python(execution) => execution
2742                .poll_event_blocking(timeout)
2743                .map(|event| {
2744                    event.map(|event| match event {
2745                        PythonExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2746                        PythonExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2747                        PythonExecutionEvent::JavascriptSyncRpcRequest(request) => {
2748                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2749                        }
2750                        PythonExecutionEvent::VfsRpcRequest(request) => {
2751                            ActiveExecutionEvent::PythonVfsRpcRequest(request)
2752                        }
2753                        PythonExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2754                    })
2755                })
2756                .map_err(|error| SidecarError::Execution(error.to_string())),
2757            Self::Wasm(execution) => execution
2758                .poll_event_blocking(timeout)
2759                .map(|event| {
2760                    event.map(|event| match event {
2761                        WasmExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2762                        WasmExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2763                        WasmExecutionEvent::SyncRpcRequest(request) => {
2764                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2765                        }
2766                        WasmExecutionEvent::SignalState {
2767                            signal,
2768                            registration,
2769                        } => ActiveExecutionEvent::SignalState {
2770                            signal,
2771                            registration: map_wasm_signal_registration(registration),
2772                        },
2773                        WasmExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2774                    })
2775                })
2776                .map_err(|error| SidecarError::Execution(error.to_string())),
2777            Self::Tool(execution) => {
2778                let _ = timeout;
2779                poll_tool_process_event(execution)
2780            }
2781        }
2782    }
2783}
2784
2785struct ToolProcessEventRequest {
2786    sidecar_requests: SharedSidecarRequestClient,
2787    connection_id: String,
2788    session_id: String,
2789    vm_id: String,
2790    tool_resolution: ToolCommandResolution,
2791    cancelled: Arc<AtomicBool>,
2792    pending_events: Arc<Mutex<VecDeque<ActiveExecutionEvent>>>,
2793    events_overflowed: Arc<AtomicBool>,
2794}
2795
2796pub(crate) fn send_tool_process_event(
2797    pending_events: &Arc<Mutex<VecDeque<ActiveExecutionEvent>>>,
2798    events_overflowed: &AtomicBool,
2799    event: ActiveExecutionEvent,
2800) -> bool {
2801    let mut pending_events = pending_events
2802        .lock()
2803        .unwrap_or_else(|poisoned| poisoned.into_inner());
2804    if pending_events.len() >= MAX_PROCESS_EVENT_QUEUE {
2805        events_overflowed.store(true, Ordering::Relaxed);
2806        return false;
2807    }
2808    pending_events.push_back(event);
2809    true
2810}
2811
2812fn spawn_tool_process_events(request: ToolProcessEventRequest) {
2813    let ToolProcessEventRequest {
2814        sidecar_requests,
2815        connection_id,
2816        session_id,
2817        vm_id,
2818        tool_resolution,
2819        cancelled,
2820        pending_events,
2821        events_overflowed,
2822    } = request;
2823    std::thread::spawn(move || match tool_resolution {
2824        ToolCommandResolution::Failure(message) => {
2825            if !send_tool_process_event(
2826                &pending_events,
2827                &events_overflowed,
2828                ActiveExecutionEvent::Stderr(format_tool_failure_output(&message)),
2829            ) {
2830                return;
2831            }
2832            let _ = send_tool_process_event(
2833                &pending_events,
2834                &events_overflowed,
2835                ActiveExecutionEvent::Exited(1),
2836            );
2837        }
2838        ToolCommandResolution::Invoke { request, timeout } => {
2839            let response = sidecar_requests.invoke(
2840                OwnershipScope::vm(connection_id.clone(), session_id.clone(), vm_id.clone()),
2841                SidecarRequestPayload::HostCallback(request.clone()),
2842                timeout,
2843            );
2844            if cancelled.load(Ordering::Relaxed) {
2845                return;
2846            }
2847
2848            match response {
2849                Ok(crate::protocol::SidecarResponsePayload::HostCallbackResult(result)) => {
2850                    if let Some(value) = result.result {
2851                        let value: serde_json::Value = serde_json::from_str(&value)
2852                            .unwrap_or(serde_json::Value::String(value));
2853                        let stdout = serde_json::to_vec(&json!({
2854                            "ok": true,
2855                            "result": value,
2856                        }))
2857                        .unwrap_or_else(|error| {
2858                            format_tool_failure_output(&format!(
2859                                "failed to serialize tool result: {error}"
2860                            ))
2861                        });
2862                        if !send_tool_process_event(
2863                            &pending_events,
2864                            &events_overflowed,
2865                            ActiveExecutionEvent::Stdout(stdout),
2866                        ) {
2867                            return;
2868                        }
2869                        let _ = send_tool_process_event(
2870                            &pending_events,
2871                            &events_overflowed,
2872                            ActiveExecutionEvent::Exited(0),
2873                        );
2874                    } else {
2875                        let message = result
2876                            .error
2877                            .unwrap_or_else(|| String::from("tool invocation returned no result"));
2878                        if !send_tool_process_event(
2879                            &pending_events,
2880                            &events_overflowed,
2881                            ActiveExecutionEvent::Stderr(format_tool_failure_output(&message)),
2882                        ) {
2883                            return;
2884                        }
2885                        let _ = send_tool_process_event(
2886                            &pending_events,
2887                            &events_overflowed,
2888                            ActiveExecutionEvent::Exited(1),
2889                        );
2890                    }
2891                }
2892                Ok(_) => {
2893                    if !send_tool_process_event(
2894                        &pending_events,
2895                        &events_overflowed,
2896                        ActiveExecutionEvent::Stderr(format_tool_failure_output(
2897                            "unexpected sidecar tool response",
2898                        )),
2899                    ) {
2900                        return;
2901                    }
2902                    let _ = send_tool_process_event(
2903                        &pending_events,
2904                        &events_overflowed,
2905                        ActiveExecutionEvent::Exited(1),
2906                    );
2907                }
2908                Err(error) => {
2909                    if !send_tool_process_event(
2910                        &pending_events,
2911                        &events_overflowed,
2912                        ActiveExecutionEvent::Stderr(format_tool_failure_output(
2913                            &error.to_string(),
2914                        )),
2915                    ) {
2916                        return;
2917                    }
2918                    let _ = send_tool_process_event(
2919                        &pending_events,
2920                        &events_overflowed,
2921                        ActiveExecutionEvent::Exited(1),
2922                    );
2923                }
2924            }
2925        }
2926    });
2927}
2928
2929impl<B> NativeSidecar<B>
2930where
2931    B: NativeSidecarBridge + Send + 'static,
2932    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
2933{
2934    pub(crate) async fn execute(
2935        &mut self,
2936        request: &RequestFrame,
2937        payload: ExecuteRequest,
2938    ) -> Result<DispatchResult, SidecarError> {
2939        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
2940        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
2941
2942        let vm = self
2943            .vms
2944            .get_mut(&vm_id)
2945            .ok_or_else(|| missing_vm_error(&vm_id))?;
2946        if vm.active_processes.contains_key(&payload.process_id) {
2947            return Err(SidecarError::InvalidState(format!(
2948                "VM {vm_id} already has an active process with id {}",
2949                payload.process_id
2950            )));
2951        }
2952
2953        if let Some(command) = payload.command.as_deref() {
2954            if let Some(tool_resolution) =
2955                resolve_tool_command(vm, command, &payload.args, payload.cwd.as_deref())?
2956            {
2957                let guest_cwd = payload
2958                    .cwd
2959                    .as_deref()
2960                    .map(normalize_path)
2961                    .unwrap_or_else(|| vm.guest_cwd.clone());
2962                let kernel_handle = vm
2963                    .kernel
2964                    .create_virtual_process(
2965                        EXECUTION_DRIVER_NAME,
2966                        TOOL_DRIVER_NAME,
2967                        command,
2968                        std::iter::once(command.to_owned())
2969                            .chain(payload.args.iter().cloned())
2970                            .collect(),
2971                        VirtualProcessOptions {
2972                            env: vm.guest_env.clone(),
2973                            cwd: Some(guest_cwd.clone()),
2974                            ..VirtualProcessOptions::default()
2975                        },
2976                    )
2977                    .map_err(kernel_error)?;
2978                let kernel_pid = kernel_handle.pid();
2979                let tool_execution = ToolExecution::default();
2980                let cancelled = tool_execution.cancelled.clone();
2981                let pending_events = tool_execution.pending_events.clone();
2982                let events_overflowed = tool_execution.events_overflowed.clone();
2983                vm.active_processes.insert(
2984                    payload.process_id.clone(),
2985                    ActiveProcess::new(
2986                        kernel_pid,
2987                        kernel_handle,
2988                        GuestRuntimeKind::JavaScript,
2989                        ActiveExecution::Tool(tool_execution),
2990                    )
2991                    .with_guest_cwd(guest_cwd.clone())
2992                    .with_host_cwd(resolve_vm_guest_path_to_host(vm, &guest_cwd)),
2993                );
2994                self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy)?;
2995                spawn_tool_process_events(ToolProcessEventRequest {
2996                    sidecar_requests: self.sidecar_requests.clone(),
2997                    connection_id: connection_id.clone(),
2998                    session_id: session_id.clone(),
2999                    vm_id: vm_id.clone(),
3000                    tool_resolution,
3001                    cancelled,
3002                    pending_events,
3003                    events_overflowed,
3004                });
3005
3006                return Ok(DispatchResult {
3007                    response: self.respond(
3008                        request,
3009                        ResponsePayload::ProcessStarted(ProcessStartedResponse {
3010                            process_id: payload.process_id,
3011                            pid: Some(kernel_pid),
3012                        }),
3013                    ),
3014                    events: Vec::new(),
3015                });
3016            }
3017        }
3018
3019        let resolved = resolve_execute_request(vm, &payload)?;
3020        let mut env = resolved.env.clone();
3021        let sandbox_root = normalize_host_path(&vm.cwd);
3022        env.insert(
3023            String::from(EXECUTION_SANDBOX_ROOT_ENV),
3024            sandbox_root.to_string_lossy().into_owned(),
3025        );
3026        if resolved.runtime == GuestRuntimeKind::JavaScript {
3027            env.insert(
3028                String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
3029                String::from("1"),
3030            );
3031        } else if resolved.runtime == GuestRuntimeKind::WebAssembly {
3032            env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
3033        }
3034        let argv = std::iter::once(resolved.entrypoint.clone())
3035            .chain(resolved.execution_args.iter().cloned())
3036            .collect::<Vec<_>>();
3037        let kernel_handle = vm
3038            .kernel
3039            .spawn_process(
3040                &resolved.command,
3041                argv,
3042                SpawnOptions {
3043                    requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
3044                    cwd: Some(resolved.guest_cwd.clone()),
3045                    ..SpawnOptions::default()
3046                },
3047            )
3048            .map_err(kernel_error)?;
3049        let kernel_pid = kernel_handle.pid();
3050
3051        let (execution, process_env) = match resolved.runtime {
3052            GuestRuntimeKind::JavaScript => {
3053                let inline_code = load_javascript_entrypoint_source(
3054                    vm,
3055                    &resolved.host_cwd,
3056                    &resolved.entrypoint,
3057                    &env,
3058                );
3059                prepare_javascript_shadow(vm, &resolved)?;
3060
3061                let context =
3062                    self.javascript_engine
3063                        .create_context(CreateJavascriptContextRequest {
3064                            vm_id: vm_id.clone(),
3065                            bootstrap_module: None,
3066                            compile_cache_root: Some(self.cache_root.join("node-compile-cache")),
3067                        });
3068                let built_reader = build_module_reader(vm, &resolved);
3069                let guest_reader = built_reader
3070                    .clone()
3071                    .map(|reader| {
3072                        Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader))
3073                            as Box<dyn GuestModuleReader>
3074                    });
3075                let module_reader = built_reader
3076                    .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
3077                let execution = self
3078                    .javascript_engine
3079                    .start_execution_with_module_reader(
3080                        StartJavascriptExecutionRequest {
3081                            guest_runtime: guest_runtime_identity(vm, None, None),
3082                            vm_id: vm_id.clone(),
3083                            context_id: context.context_id,
3084                            argv: std::iter::once(resolved.entrypoint.clone())
3085                                .chain(resolved.execution_args.iter().cloned())
3086                                .collect(),
3087                            env: env.clone(),
3088                            cwd: resolved.host_cwd.clone(),
3089                            limits: javascript_execution_limits(vm),
3090                            inline_code,
3091                        },
3092                        module_reader,
3093                        guest_reader,
3094                    )
3095                    .map_err(javascript_error)?;
3096                (ActiveExecution::Javascript(execution), env.clone())
3097            }
3098            GuestRuntimeKind::Python => {
3099                let python_file_path = python_file_entrypoint(&resolved.entrypoint);
3100                let pyodide_dist_path = self
3101                    .python_engine
3102                    .bundled_pyodide_dist_path_for_vm(&vm_id)
3103                    .map_err(python_error)?;
3104                let pyodide_cache_path = pyodide_dist_path
3105                    .parent()
3106                    .and_then(Path::parent)
3107                    .unwrap_or(pyodide_dist_path.as_path())
3108                    .join("pyodide-package-cache");
3109                add_runtime_guest_path_mapping(
3110                    &mut env,
3111                    PYTHON_PYODIDE_GUEST_ROOT,
3112                    &pyodide_dist_path,
3113                );
3114                add_runtime_guest_path_mapping(
3115                    &mut env,
3116                    PYTHON_PYODIDE_CACHE_GUEST_ROOT,
3117                    &pyodide_cache_path,
3118                );
3119                add_runtime_host_access_path(
3120                    &mut env,
3121                    "AGENTOS_EXTRA_FS_READ_PATHS",
3122                    &pyodide_dist_path,
3123                    true,
3124                );
3125                add_runtime_host_access_path(
3126                    &mut env,
3127                    "AGENTOS_EXTRA_FS_READ_PATHS",
3128                    &pyodide_cache_path,
3129                    true,
3130                );
3131                add_runtime_host_access_path(
3132                    &mut env,
3133                    "AGENTOS_EXTRA_FS_WRITE_PATHS",
3134                    &pyodide_cache_path,
3135                    false,
3136                );
3137                let context = self
3138                    .python_engine
3139                    .create_context(CreatePythonContextRequest {
3140                        vm_id: vm_id.clone(),
3141                        pyodide_dist_path,
3142                    });
3143                let execution = self
3144                    .python_engine
3145                    .start_execution(StartPythonExecutionRequest {
3146                        vm_id: vm_id.clone(),
3147                        context_id: context.context_id,
3148                        code: resolved.entrypoint.clone(),
3149                        file_path: python_file_path,
3150                        env: env.clone(),
3151                        cwd: resolved.host_cwd.clone(),
3152                        limits: python_execution_limits(vm),
3153                        guest_runtime: guest_runtime_identity(vm, None, None),
3154                    })
3155                    .map_err(python_error)?;
3156                (ActiveExecution::Python(execution), env.clone())
3157            }
3158            GuestRuntimeKind::WebAssembly => {
3159                let wasm_limits = wasm_execution_limits(vm);
3160                let wasm_guest_runtime =
3161                    guest_runtime_identity(vm, Some(u64::from(kernel_pid)), Some(0));
3162                let wasm_permission_tier = resolved.wasm_permission_tier.unwrap_or_else(|| {
3163                    resolve_wasm_permission_tier(
3164                        vm,
3165                        Some(&resolved.command),
3166                        None,
3167                        &resolved.entrypoint,
3168                    )
3169                });
3170                let context = self.wasm_engine.create_context(CreateWasmContextRequest {
3171                    vm_id: vm_id.clone(),
3172                    module_path: Some(resolved.entrypoint.clone()),
3173                });
3174                let execution = self
3175                    .wasm_engine
3176                    .start_execution(StartWasmExecutionRequest {
3177                        vm_id: vm_id.clone(),
3178                        context_id: context.context_id,
3179                        argv: resolved.process_args.clone(),
3180                        env: env.clone(),
3181                        cwd: resolved.host_cwd.clone(),
3182                        permission_tier: execution_wasm_permission_tier(wasm_permission_tier),
3183                        limits: wasm_limits,
3184                        guest_runtime: wasm_guest_runtime,
3185                    })
3186                    .map_err(wasm_error)?;
3187                (ActiveExecution::Wasm(Box::new(execution)), env)
3188            }
3189        };
3190        let child_pid = execution.child_pid();
3191        let kernel_stdin_writer_fd = install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?;
3192        vm.active_processes.insert(
3193            payload.process_id.clone(),
3194            ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
3195                .with_kernel_stdin_writer_fd(kernel_stdin_writer_fd)
3196                .with_guest_cwd(resolved.guest_cwd.clone())
3197                .with_env(process_env)
3198                .with_host_cwd(resolved.host_cwd.clone()),
3199        );
3200        self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy)?;
3201
3202        Ok(DispatchResult {
3203            response: self.respond(
3204                request,
3205                ResponsePayload::ProcessStarted(ProcessStartedResponse {
3206                    process_id: payload.process_id,
3207                    pid: Some(if child_pid == 0 {
3208                        kernel_pid
3209                    } else {
3210                        child_pid
3211                    }),
3212                }),
3213            ),
3214            events: Vec::new(),
3215        })
3216    }
3217
3218    pub(crate) async fn write_stdin(
3219        &mut self,
3220        request: &RequestFrame,
3221        payload: WriteStdinRequest,
3222    ) -> Result<DispatchResult, SidecarError> {
3223        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3224        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3225
3226        let vm = self
3227            .vms
3228            .get_mut(&vm_id)
3229            .ok_or_else(|| missing_vm_error(&vm_id))?;
3230        let process = vm
3231            .active_processes
3232            .get_mut(&payload.process_id)
3233            .ok_or_else(|| {
3234                SidecarError::InvalidState(format!(
3235                    "VM {vm_id} has no active process {}",
3236                    payload.process_id
3237                ))
3238            })?;
3239        process.execution.write_stdin(&payload.chunk)?;
3240        write_kernel_process_stdin(&mut vm.kernel, process, &payload.chunk)?;
3241
3242        Ok(DispatchResult {
3243            response: self.respond(
3244                request,
3245                ResponsePayload::StdinWritten(StdinWrittenResponse {
3246                    process_id: payload.process_id,
3247                    accepted_bytes: payload.chunk.len() as u64,
3248                }),
3249            ),
3250            events: Vec::new(),
3251        })
3252    }
3253
3254    pub(crate) async fn close_stdin(
3255        &mut self,
3256        request: &RequestFrame,
3257        payload: CloseStdinRequest,
3258    ) -> Result<DispatchResult, SidecarError> {
3259        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3260        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3261
3262        let vm = self
3263            .vms
3264            .get_mut(&vm_id)
3265            .ok_or_else(|| missing_vm_error(&vm_id))?;
3266        let process = vm
3267            .active_processes
3268            .get_mut(&payload.process_id)
3269            .ok_or_else(|| {
3270                SidecarError::InvalidState(format!(
3271                    "VM {vm_id} has no active process {}",
3272                    payload.process_id
3273                ))
3274            })?;
3275        process.execution.close_stdin()?;
3276        close_kernel_process_stdin(&mut vm.kernel, process)?;
3277
3278        Ok(DispatchResult {
3279            response: self.respond(
3280                request,
3281                ResponsePayload::StdinClosed(StdinClosedResponse {
3282                    process_id: payload.process_id,
3283                }),
3284            ),
3285            events: Vec::new(),
3286        })
3287    }
3288
3289    pub(crate) async fn kill_process(
3290        &mut self,
3291        request: &RequestFrame,
3292        payload: KillProcessRequest,
3293    ) -> Result<DispatchResult, SidecarError> {
3294        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3295        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3296        self.kill_process_internal(&vm_id, &payload.process_id, &payload.signal)?;
3297
3298        Ok(DispatchResult {
3299            response: self.respond(
3300                request,
3301                ResponsePayload::ProcessKilled(ProcessKilledResponse {
3302                    process_id: payload.process_id,
3303                }),
3304            ),
3305            events: Vec::new(),
3306        })
3307    }
3308
3309    pub(crate) async fn find_listener(
3310        &mut self,
3311        request: &RequestFrame,
3312        payload: FindListenerRequest,
3313    ) -> Result<DispatchResult, SidecarError> {
3314        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3315        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3316        require_vm_inspection_permission(
3317            &self.bridge,
3318            &vm_id,
3319            "network.inspect",
3320            "network",
3321            &socket_query_resource(SocketQueryKind::TcpListener, &payload),
3322        )?;
3323
3324        let listener =
3325            find_socket_state_entry(self.vms.get(&vm_id), SocketQueryKind::TcpListener, &payload)?;
3326
3327        Ok(DispatchResult {
3328            response: self.respond(
3329                request,
3330                ResponsePayload::ListenerSnapshot(ListenerSnapshotResponse { listener }),
3331            ),
3332            events: Vec::new(),
3333        })
3334    }
3335
3336    pub(crate) async fn get_process_snapshot(
3337        &mut self,
3338        request: &RequestFrame,
3339        _payload: GetProcessSnapshotRequest,
3340    ) -> Result<DispatchResult, SidecarError> {
3341        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3342        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3343        require_vm_inspection_permission(
3344            &self.bridge,
3345            &vm_id,
3346            "process.inspect",
3347            "process",
3348            "process://snapshot",
3349        )?;
3350
3351        let processes = self
3352            .vms
3353            .get_mut(&vm_id)
3354            .map(|vm| {
3355                prune_exited_process_snapshots(vm);
3356                snapshot_vm_processes(vm)
3357            })
3358            .unwrap_or_default();
3359
3360        Ok(DispatchResult {
3361            response: self.respond(
3362                request,
3363                ResponsePayload::ProcessSnapshot(ProcessSnapshotResponse { processes }),
3364            ),
3365            events: Vec::new(),
3366        })
3367    }
3368
3369    pub(crate) async fn find_bound_udp(
3370        &mut self,
3371        request: &RequestFrame,
3372        payload: FindBoundUdpRequest,
3373    ) -> Result<DispatchResult, SidecarError> {
3374        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3375        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3376
3377        let lookup_request = FindListenerRequest {
3378            host: payload.host,
3379            port: payload.port,
3380            path: None,
3381        };
3382        require_vm_inspection_permission(
3383            &self.bridge,
3384            &vm_id,
3385            "network.inspect",
3386            "network",
3387            &socket_query_resource(SocketQueryKind::UdpBound, &lookup_request),
3388        )?;
3389        let socket = find_socket_state_entry(
3390            self.vms.get(&vm_id),
3391            SocketQueryKind::UdpBound,
3392            &lookup_request,
3393        )?;
3394
3395        Ok(DispatchResult {
3396            response: self.respond(
3397                request,
3398                ResponsePayload::BoundUdpSnapshot(BoundUdpSnapshotResponse { socket }),
3399            ),
3400            events: Vec::new(),
3401        })
3402    }
3403
3404    pub(crate) async fn vm_fetch(
3405        &mut self,
3406        request: &RequestFrame,
3407        payload: VmFetchRequest,
3408    ) -> Result<DispatchResult, SidecarError> {
3409        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3410        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3411
3412        let vm = self
3413            .vms
3414            .get_mut(&vm_id)
3415            .ok_or_else(|| SidecarError::InvalidState(String::from("unknown sidecar VM")))?;
3416        let target_path = if payload.path.starts_with('/') {
3417            payload.path.clone()
3418        } else {
3419            format!("/{}", payload.path)
3420        };
3421        let request_url = Url::parse(&format!("http://127.0.0.1:{}{target_path}", payload.port))
3422            .map_err(|error| {
3423                SidecarError::InvalidState(format!(
3424                    "invalid vm.fetch target {target_path:?}: {error}"
3425                ))
3426            })?;
3427        let header_values: BTreeMap<String, Value> = serde_json::from_str(&payload.headers_json)
3428            .map_err(|error| {
3429                SidecarError::InvalidState(format!(
3430                    "vm.fetch headers_json must be valid JSON: {error}"
3431                ))
3432            })?;
3433        let options = JavascriptHttpRequestOptions {
3434            method: Some(payload.method),
3435            headers: header_values,
3436            body: payload.body,
3437            reject_unauthorized: None,
3438        };
3439        let headers = parse_http_header_collection(&options.headers, "vm.fetch headers")?;
3440        let target_process_id = find_kernel_http_listener_process(vm, payload.port);
3441        if let Some(target_process_id) = target_process_id {
3442            let max_fetch_response_bytes = vm.limits.http.max_fetch_response_bytes;
3443            let response_json = match dispatch_kernel_http_fetch(
3444                &self.bridge,
3445                &vm_id,
3446                vm,
3447                &target_process_id,
3448                payload.port,
3449                &target_path,
3450                &options,
3451                &headers,
3452                max_fetch_response_bytes,
3453            ) {
3454                Ok(response_json) => response_json,
3455                Err(error) => {
3456                    if let Some(exit_code) = kernel_http_fetch_target_exit_code(&error) {
3457                        let _ = vm;
3458                        self.finish_active_process_exit(&vm_id, &target_process_id, exit_code)?;
3459                    }
3460                    return Err(error);
3461                }
3462            };
3463            let response = self.respond(
3464                request,
3465                ResponsePayload::VmFetchResult(VmFetchResponse { response_json }),
3466            );
3467            ensure_vm_fetch_response_frame_within_limit(&response, self.config.max_frame_bytes)?;
3468
3469            return Ok(DispatchResult {
3470                response,
3471                events: Vec::new(),
3472            });
3473        }
3474
3475        let Some((target_process_id, server_id)) =
3476            vm.active_processes
3477                .iter()
3478                .find_map(|(process_id, process)| {
3479                    process
3480                        .http_servers
3481                        .iter()
3482                        .find(|(_, server)| server.guest_local_addr.port() == payload.port)
3483                        .map(|(server_id, _)| (process_id.clone(), *server_id))
3484                })
3485        else {
3486            return Err(SidecarError::Execution(format!(
3487                "vm.fetch could not find a guest HTTP listener on port {}",
3488                payload.port
3489            )));
3490        };
3491        let socket_paths = build_javascript_socket_path_context(vm)?;
3492        let resource_limits = vm.kernel.resource_limits().clone();
3493        let process = vm
3494            .active_processes
3495            .get_mut(&target_process_id)
3496            .ok_or_else(|| {
3497                SidecarError::InvalidState(format!(
3498                    "vm.fetch target process disappeared: {target_process_id}"
3499                ))
3500            })?;
3501        let request_json = serialize_http_loopback_request(&request_url, &options, &headers)?;
3502        let response_json = dispatch_loopback_http_request(LoopbackHttpDispatchRequest {
3503            bridge: &self.bridge,
3504            vm_id: &vm_id,
3505            dns: &vm.dns,
3506            socket_paths: &socket_paths,
3507            kernel: &mut vm.kernel,
3508            process,
3509            resource_limits: &resource_limits,
3510            server_id,
3511            request_json: &request_json,
3512        })?;
3513
3514        let response = self.respond(
3515            request,
3516            ResponsePayload::VmFetchResult(VmFetchResponse { response_json }),
3517        );
3518        ensure_vm_fetch_response_frame_within_limit(&response, self.config.max_frame_bytes)?;
3519
3520        Ok(DispatchResult {
3521            response,
3522            events: Vec::new(),
3523        })
3524    }
3525
3526    pub(crate) async fn get_signal_state(
3527        &mut self,
3528        request: &RequestFrame,
3529        payload: GetSignalStateRequest,
3530    ) -> Result<DispatchResult, SidecarError> {
3531        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3532        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3533
3534        let handlers = self
3535            .vms
3536            .get(&vm_id)
3537            .and_then(|vm| vm.signal_states.get(&payload.process_id))
3538            .cloned()
3539            .unwrap_or_default();
3540
3541        Ok(DispatchResult {
3542            response: self.respond(
3543                request,
3544                ResponsePayload::SignalState(SignalStateResponse {
3545                    process_id: payload.process_id,
3546                    handlers: handlers.into_iter().collect(),
3547                }),
3548            ),
3549            events: Vec::new(),
3550        })
3551    }
3552
3553    pub(crate) async fn get_zombie_timer_count(
3554        &mut self,
3555        request: &RequestFrame,
3556        _payload: GetZombieTimerCountRequest,
3557    ) -> Result<DispatchResult, SidecarError> {
3558        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3559        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3560
3561        let count = self
3562            .vms
3563            .get(&vm_id)
3564            .map(|vm| vm.kernel.zombie_timer_count() as u64)
3565            .unwrap_or_default();
3566
3567        Ok(DispatchResult {
3568            response: self.respond(
3569                request,
3570                ResponsePayload::ZombieTimerCount(ZombieTimerCountResponse { count }),
3571            ),
3572            events: Vec::new(),
3573        })
3574    }
3575
3576    pub(crate) fn kill_process_internal(
3577        &mut self,
3578        vm_id: &str,
3579        process_id: &str,
3580        signal: &str,
3581    ) -> Result<(), SidecarError> {
3582        let signal_name = signal.to_owned();
3583        let signal = parse_signal(signal)?;
3584        let vm = self
3585            .vms
3586            .get_mut(vm_id)
3587            .ok_or_else(|| SidecarError::InvalidState(format!("unknown sidecar VM {vm_id}")))?;
3588        let process = vm.active_processes.get_mut(process_id).ok_or_else(|| {
3589            SidecarError::InvalidState(format!("VM {vm_id} has no active process {process_id}"))
3590        })?;
3591        let kernel_pid = process.kernel_pid;
3592
3593        enum KillBehavior {
3594            Tool,
3595            SharedV8StateOnly,
3596            SharedV8Continue,
3597            SharedV8Terminate,
3598            SharedV8DispatchOrTerminate,
3599            Noop,
3600            HostPid(u32),
3601        }
3602
3603        let behavior = match &process.execution {
3604            ActiveExecution::Tool(_) => KillBehavior::Tool,
3605            ActiveExecution::Javascript(execution)
3606                if execution.uses_shared_v8_runtime() && matches!(signal, 0 | libc::SIGSTOP) =>
3607            {
3608                KillBehavior::SharedV8StateOnly
3609            }
3610            ActiveExecution::Javascript(execution)
3611                if execution.uses_shared_v8_runtime() && signal == libc::SIGCONT =>
3612            {
3613                KillBehavior::SharedV8Continue
3614            }
3615            ActiveExecution::Wasm(execution)
3616                if execution.uses_shared_v8_runtime()
3617                    && matches!(signal, 0 | libc::SIGSTOP | libc::SIGCONT) =>
3618            {
3619                KillBehavior::SharedV8StateOnly
3620            }
3621            ActiveExecution::Python(execution)
3622                if execution.uses_shared_v8_runtime()
3623                    && matches!(signal, 0 | libc::SIGSTOP | libc::SIGCONT) =>
3624            {
3625                KillBehavior::SharedV8StateOnly
3626            }
3627            ActiveExecution::Javascript(execution)
3628                if execution.uses_shared_v8_runtime() && signal == SIGKILL =>
3629            {
3630                KillBehavior::SharedV8Terminate
3631            }
3632            ActiveExecution::Wasm(execution)
3633                if execution.uses_shared_v8_runtime() && signal == SIGKILL =>
3634            {
3635                KillBehavior::SharedV8Terminate
3636            }
3637            ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime() => {
3638                KillBehavior::SharedV8DispatchOrTerminate
3639            }
3640            ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime() => {
3641                KillBehavior::SharedV8Terminate
3642            }
3643            ActiveExecution::Python(execution) if execution.uses_shared_v8_runtime() => {
3644                KillBehavior::SharedV8Terminate
3645            }
3646            ActiveExecution::Javascript(execution) if execution.child_pid() == 0 => {
3647                KillBehavior::Noop
3648            }
3649            _ => KillBehavior::HostPid(process.execution.child_pid()),
3650        };
3651
3652        match behavior {
3653            KillBehavior::Tool => {
3654                let ActiveExecution::Tool(execution) = &process.execution else {
3655                    unreachable!("kill behavior must match tool execution");
3656                };
3657                if signal != 0 {
3658                    execution.cancelled.store(true, Ordering::Relaxed);
3659                    process.queue_pending_execution_event(ActiveExecutionEvent::Exited(
3660                        128 + signal,
3661                    ))?;
3662                }
3663            }
3664            KillBehavior::SharedV8StateOnly => {
3665                if matches!(signal, libc::SIGSTOP | libc::SIGCONT) {
3666                    vm.kernel
3667                        .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal)
3668                        .map_err(kernel_error)?;
3669                }
3670            }
3671            KillBehavior::SharedV8Continue => {
3672                vm.kernel
3673                    .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal)
3674                    .map_err(kernel_error)?;
3675                if signal != 0 && !dispatch_v8_process_signal(process, signal)? {
3676                    process.execution.terminate()?;
3677                }
3678            }
3679            KillBehavior::SharedV8Terminate => {
3680                if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
3681                    close_kernel_process_stdin(&mut vm.kernel, process)?;
3682                }
3683                process.execution.terminate()?;
3684                let needs_synthetic_exit = matches!(process.execution, ActiveExecution::Wasm(_))
3685                    || (signal == SIGKILL
3686                        && matches!(process.execution, ActiveExecution::Javascript(_)));
3687                if signal != 0 && needs_synthetic_exit {
3688                    process.queue_pending_execution_event(ActiveExecutionEvent::Exited(
3689                        128 + signal,
3690                    ))?;
3691                }
3692            }
3693            KillBehavior::SharedV8DispatchOrTerminate => {
3694                if signal != 0 && !dispatch_v8_process_signal(process, signal)? {
3695                    process.execution.terminate()?;
3696                }
3697            }
3698            KillBehavior::Noop => {}
3699            KillBehavior::HostPid(pid) => {
3700                if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
3701                    close_kernel_process_stdin(&mut vm.kernel, process)?;
3702                }
3703                signal_runtime_process(pid, signal)?;
3704            }
3705        }
3706        emit_security_audit_event(
3707            &self.bridge,
3708            vm_id,
3709            "security.process.kill",
3710            audit_fields([
3711                (String::from("source"), String::from("control_plane")),
3712                (String::from("source_pid"), String::from("0")),
3713                (String::from("target_pid"), process.kernel_pid.to_string()),
3714                (String::from("process_id"), process_id.to_owned()),
3715                (String::from("signal"), signal_name),
3716                (
3717                    String::from("host_pid"),
3718                    process.execution.child_pid().to_string(),
3719                ),
3720            ]),
3721        );
3722        Ok(())
3723    }
3724
3725    pub async fn pump_process_events(
3726        &mut self,
3727        ownership: &OwnershipScope,
3728    ) -> Result<bool, SidecarError> {
3729        let mut emitted_any = false;
3730
3731        let mut queued_envelopes = Vec::new();
3732        {
3733            let pending_capacity = self.pending_process_event_capacity();
3734            let receiver = self.process_event_receiver.as_mut().ok_or_else(|| {
3735                SidecarError::InvalidState(String::from("process event receiver unavailable"))
3736            })?;
3737            loop {
3738                if queued_envelopes.len() >= pending_capacity {
3739                    if receiver.is_empty() {
3740                        break;
3741                    }
3742                    return Err(process_event_queue_overflow_error());
3743                }
3744                match receiver.try_recv() {
3745                    Ok(envelope) => {
3746                        queued_envelopes.push(envelope);
3747                        emitted_any = true;
3748                    }
3749                    Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
3750                    Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
3751                }
3752            }
3753        }
3754        for envelope in queued_envelopes {
3755            self.queue_pending_process_event(envelope)?;
3756        }
3757
3758        let vm_ids = self.vm_ids_for_scope(ownership)?;
3759        for vm_id in vm_ids {
3760            while let Some(vm) = self.vms.get(&vm_id) {
3761                let connection_id = vm.connection_id.clone();
3762                let session_id = vm.session_id.clone();
3763                let process_ids = self
3764                    .vms
3765                    .get(&vm_id)
3766                    .map(|vm| vm.active_processes.keys().cloned().collect::<Vec<_>>())
3767                    .unwrap_or_default();
3768                let mut emitted_this_pass = false;
3769
3770                for process_id in process_ids {
3771                    if self
3772                        .vms
3773                        .get(&vm_id)
3774                        .is_some_and(|vm| vm.detached_child_processes.contains(&process_id))
3775                    {
3776                        continue;
3777                    }
3778                    enum ProcessPollResult {
3779                        Event(Box<Option<ActiveExecutionEvent>>),
3780                        RecoverClosedChannel,
3781                    }
3782                    let poll_result = {
3783                        let Some(vm) = self.vms.get_mut(&vm_id) else {
3784                            continue;
3785                        };
3786                        let Some(process) = vm.active_processes.get_mut(&process_id) else {
3787                            continue;
3788                        };
3789                        if let Some(event) = process.pending_execution_events.pop_front() {
3790                            ProcessPollResult::Event(Box::new(Some(event)))
3791                        } else {
3792                            match process.execution.poll_event(Duration::ZERO).await {
3793                                Ok(event) => ProcessPollResult::Event(Box::new(event)),
3794                                Err(SidecarError::Execution(message))
3795                                    if (process.runtime == GuestRuntimeKind::JavaScript
3796                                        && closed_javascript_event_channel(&message))
3797                                        || (process.runtime == GuestRuntimeKind::Python
3798                                            && closed_python_event_channel(&message))
3799                                        || (process.runtime == GuestRuntimeKind::WebAssembly
3800                                            && closed_wasm_event_channel(&message)) =>
3801                                {
3802                                    ProcessPollResult::RecoverClosedChannel
3803                                }
3804                                Err(other) => return Err(other),
3805                            }
3806                        }
3807                    };
3808                    let event = match poll_result {
3809                        ProcessPollResult::Event(event) => *event,
3810                        ProcessPollResult::RecoverClosedChannel => {
3811                            self.recover_closed_root_runtime_process_event(&vm_id, &process_id)?
3812                        }
3813                    };
3814
3815                    let Some(event) = event else {
3816                        continue;
3817                    };
3818
3819                    if Self::internal_execution_event(&event) {
3820                        // These events are sidecar work items, not client-facing
3821                        // process events. Handle them immediately so a sibling
3822                        // process can service sync RPCs while another request
3823                        // waits on VM-local networking.
3824                        self.handle_execution_event(&vm_id, &process_id, event)?;
3825                    } else {
3826                        self.queue_pending_process_event(ProcessEventEnvelope {
3827                            connection_id: connection_id.clone(),
3828                            session_id: session_id.clone(),
3829                            vm_id: vm_id.clone(),
3830                            process_id: process_id.clone(),
3831                            event,
3832                        })?;
3833                    }
3834                    emitted_any = true;
3835                    emitted_this_pass = true;
3836                }
3837
3838                if !emitted_this_pass {
3839                    break;
3840                }
3841            }
3842
3843            if self.pump_detached_child_process_events(&vm_id)? {
3844                emitted_any = true;
3845            }
3846        }
3847
3848        Ok(emitted_any)
3849    }
3850
3851    fn internal_execution_event(event: &ActiveExecutionEvent) -> bool {
3852        matches!(
3853            event,
3854            ActiveExecutionEvent::JavascriptSyncRpcRequest(_)
3855                | ActiveExecutionEvent::PythonVfsRpcRequest(_)
3856                | ActiveExecutionEvent::SignalState { .. }
3857        )
3858    }
3859
3860    fn recover_closed_root_runtime_process_event(
3861        &mut self,
3862        vm_id: &str,
3863        process_id: &str,
3864    ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
3865        let Some(vm) = self.vms.get_mut(vm_id) else {
3866            return Ok(None);
3867        };
3868        let Some(process) = vm.active_processes.get(process_id) else {
3869            return Ok(None);
3870        };
3871        if process.execution.uses_shared_v8_runtime() {
3872            return Ok(None);
3873        }
3874        if process.runtime != GuestRuntimeKind::JavaScript
3875            && process.runtime != GuestRuntimeKind::Python
3876            && process.runtime != GuestRuntimeKind::WebAssembly
3877        {
3878            return Ok(None);
3879        }
3880        let runtime_child_pid = process.execution.child_pid();
3881        if runtime_child_pid == 0 {
3882            return Ok(None);
3883        }
3884        if let Some(status) = runtime_child_exit_status(runtime_child_pid)? {
3885            return Ok(Some(ActiveExecutionEvent::Exited(status)));
3886        }
3887        if runtime_child_is_alive(runtime_child_pid)? {
3888            return Ok(None);
3889        }
3890        Ok(Some(ActiveExecutionEvent::Exited(0)))
3891    }
3892
3893    fn active_process_by_path<'a>(
3894        process: &'a ActiveProcess,
3895        child_path: &[&str],
3896    ) -> Option<&'a ActiveProcess> {
3897        let mut current = process;
3898        for child_id in child_path {
3899            current = current.child_processes.get(*child_id)?;
3900        }
3901        Some(current)
3902    }
3903
3904    fn active_process_by_path_mut<'a>(
3905        process: &'a mut ActiveProcess,
3906        child_path: &[&str],
3907    ) -> Option<&'a mut ActiveProcess> {
3908        let mut current = process;
3909        for child_id in child_path {
3910            current = current.child_processes.get_mut(*child_id)?;
3911        }
3912        Some(current)
3913    }
3914
3915    fn active_process_by_owned_path_mut<'a>(
3916        process: &'a mut ActiveProcess,
3917        child_path: &[String],
3918    ) -> Option<&'a mut ActiveProcess> {
3919        let mut current = process;
3920        for child_id in child_path {
3921            current = current.child_processes.get_mut(child_id)?;
3922        }
3923        Some(current)
3924    }
3925
3926    fn active_process_path_by_kernel_pid(
3927        process: &ActiveProcess,
3928        kernel_pid: u32,
3929    ) -> Option<Vec<String>> {
3930        if process.kernel_pid == kernel_pid {
3931            return Some(Vec::new());
3932        }
3933
3934        for (child_id, child) in &process.child_processes {
3935            let Some(mut path) = Self::active_process_path_by_kernel_pid(child, kernel_pid) else {
3936                continue;
3937            };
3938            path.insert(0, child_id.clone());
3939            return Some(path);
3940        }
3941
3942        None
3943    }
3944
3945    fn descendant_parent_process<'a>(
3946        vm: &'a VmState,
3947        process_id: &str,
3948        child_path: &[&str],
3949    ) -> Option<&'a ActiveProcess> {
3950        let root = vm.active_processes.get(process_id)?;
3951        Self::active_process_by_path(root, child_path)
3952    }
3953
3954    fn descendant_parent_process_mut<'a>(
3955        vm: &'a mut VmState,
3956        process_id: &str,
3957        child_path: &[&str],
3958    ) -> Option<&'a mut ActiveProcess> {
3959        let root = vm.active_processes.get_mut(process_id)?;
3960        Self::active_process_by_path_mut(root, child_path)
3961    }
3962
3963    fn child_process_path_label(process_id: &str, child_path: &[&str]) -> String {
3964        if child_path.is_empty() {
3965            process_id.to_owned()
3966        } else {
3967            format!("{process_id}/{}", child_path.join("/"))
3968        }
3969    }
3970
3971    fn adopt_detached_child_processes(
3972        current_process_id: &str,
3973        process: &mut ActiveProcess,
3974    ) -> Vec<(String, ActiveProcess)> {
3975        let mut adopted = Vec::new();
3976        let child_ids = process.child_processes.keys().cloned().collect::<Vec<_>>();
3977        for child_id in child_ids {
3978            let child_process_id = format!("{current_process_id}/{child_id}");
3979            let Some(mut child) = process.child_processes.remove(&child_id) else {
3980                continue;
3981            };
3982            if child.detached {
3983                adopted.push((child_process_id, child));
3984                continue;
3985            }
3986
3987            adopted.extend(Self::adopt_detached_child_processes(
3988                &child_process_id,
3989                &mut child,
3990            ));
3991            process.child_processes.insert(child_id, child);
3992        }
3993        adopted
3994    }
3995
3996    fn child_process_signal_key<'a>(process_id: &'a str, child_path: &[&'a str]) -> &'a str {
3997        child_path.last().copied().unwrap_or(process_id)
3998    }
3999
4000    fn resolve_detached_child_process_path(
4001        vm: &VmState,
4002        detached_process_id: &str,
4003    ) -> Option<(String, Vec<String>)> {
4004        let root_process_id = vm
4005            .active_processes
4006            .keys()
4007            .filter(|candidate| {
4008                detached_process_id == candidate.as_str()
4009                    || detached_process_id
4010                        .strip_prefix(candidate.as_str())
4011                        .is_some_and(|remainder| remainder.starts_with('/'))
4012            })
4013            .max_by_key(|candidate| candidate.len())?
4014            .clone();
4015
4016        let remainder = detached_process_id
4017            .strip_prefix(root_process_id.as_str())
4018            .unwrap_or_default();
4019        if remainder.is_empty() {
4020            return Some((root_process_id, Vec::new()));
4021        }
4022
4023        Some((
4024            root_process_id,
4025            remainder
4026                .trim_start_matches('/')
4027                .split('/')
4028                .map(str::to_owned)
4029                .collect(),
4030        ))
4031    }
4032
4033    fn pump_detached_child_process_events(&mut self, vm_id: &str) -> Result<bool, SidecarError> {
4034        let detached_process_ids = self
4035            .vms
4036            .get(vm_id)
4037            .map(|vm| {
4038                vm.detached_child_processes
4039                    .iter()
4040                    .cloned()
4041                    .collect::<Vec<_>>()
4042            })
4043            .unwrap_or_default();
4044        let mut emitted_any = false;
4045        for detached_process_id in detached_process_ids {
4046            let Some((root_process_id, child_path)) = self
4047                .vms
4048                .get(vm_id)
4049                .and_then(|vm| Self::resolve_detached_child_process_path(vm, &detached_process_id))
4050            else {
4051                if let Some(vm) = self.vms.get_mut(vm_id) {
4052                    vm.detached_child_processes.remove(&detached_process_id);
4053                }
4054                continue;
4055            };
4056            if child_path.is_empty() {
4057                loop {
4058                    enum ProcessPollResult {
4059                        Event(Box<Option<ActiveExecutionEvent>>),
4060                        RecoverClosedChannel,
4061                    }
4062                    let poll_result = {
4063                        let Some(vm) = self.vms.get_mut(vm_id) else {
4064                            break;
4065                        };
4066                        let Some(process) = vm.active_processes.get_mut(&root_process_id) else {
4067                            break;
4068                        };
4069                        if let Some(event) = process.pending_execution_events.pop_front() {
4070                            ProcessPollResult::Event(Box::new(Some(event)))
4071                        } else {
4072                            match process.execution.poll_event_blocking(Duration::ZERO) {
4073                                Ok(event) => ProcessPollResult::Event(Box::new(event)),
4074                                Err(SidecarError::Execution(message))
4075                                    if (process.runtime == GuestRuntimeKind::JavaScript
4076                                        && closed_javascript_event_channel(&message))
4077                                        || (process.runtime == GuestRuntimeKind::Python
4078                                            && closed_python_event_channel(&message))
4079                                        || (process.runtime == GuestRuntimeKind::WebAssembly
4080                                            && closed_wasm_event_channel(&message)) =>
4081                                {
4082                                    ProcessPollResult::RecoverClosedChannel
4083                                }
4084                                Err(error) => return Err(error),
4085                            }
4086                        }
4087                    };
4088                    let event = match poll_result {
4089                        ProcessPollResult::Event(event) => *event,
4090                        ProcessPollResult::RecoverClosedChannel => {
4091                            self.recover_closed_root_runtime_process_event(vm_id, &root_process_id)?
4092                        }
4093                    };
4094                    let Some(event) = event else {
4095                        break;
4096                    };
4097                    let Some((connection_id, session_id)) = self
4098                        .vms
4099                        .get(vm_id)
4100                        .map(|vm| (vm.connection_id.clone(), vm.session_id.clone()))
4101                    else {
4102                        break;
4103                    };
4104                    match event {
4105                        ActiveExecutionEvent::Stdout(chunk) => {
4106                            self.queue_pending_process_event(ProcessEventEnvelope {
4107                                connection_id,
4108                                session_id,
4109                                vm_id: vm_id.to_owned(),
4110                                process_id: detached_process_id.clone(),
4111                                event: ActiveExecutionEvent::Stdout(chunk),
4112                            })?;
4113                            emitted_any = true;
4114                        }
4115                        ActiveExecutionEvent::Stderr(chunk) => {
4116                            self.queue_pending_process_event(ProcessEventEnvelope {
4117                                connection_id,
4118                                session_id,
4119                                vm_id: vm_id.to_owned(),
4120                                process_id: detached_process_id.clone(),
4121                                event: ActiveExecutionEvent::Stderr(chunk),
4122                            })?;
4123                            emitted_any = true;
4124                        }
4125                        ActiveExecutionEvent::Exited(exit_code) => {
4126                            if let Some(vm) = self.vms.get_mut(vm_id) {
4127                                vm.detached_child_processes.remove(&detached_process_id);
4128                            }
4129                            self.queue_pending_process_event(ProcessEventEnvelope {
4130                                connection_id,
4131                                session_id,
4132                                vm_id: vm_id.to_owned(),
4133                                process_id: detached_process_id.clone(),
4134                                event: ActiveExecutionEvent::Exited(exit_code),
4135                            })?;
4136                            emitted_any = true;
4137                            break;
4138                        }
4139                        ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
4140                            self.handle_javascript_sync_rpc_request(
4141                                vm_id,
4142                                &root_process_id,
4143                                request,
4144                            )?;
4145                        }
4146                        ActiveExecutionEvent::PythonVfsRpcRequest(request) => {
4147                            self.handle_python_vfs_rpc_request(vm_id, &root_process_id, *request)?;
4148                        }
4149                        ActiveExecutionEvent::SignalState {
4150                            signal,
4151                            registration,
4152                        } => {
4153                            if let Some(vm) = self.vms.get_mut(vm_id) {
4154                                vm.signal_states
4155                                    .entry(root_process_id.clone())
4156                                    .or_default()
4157                                    .insert(signal, registration);
4158                            }
4159                        }
4160                    }
4161                }
4162                continue;
4163            }
4164
4165            let parent_path = child_path[..child_path.len() - 1]
4166                .iter()
4167                .map(String::as_str)
4168                .collect::<Vec<_>>();
4169            let child_process_id = child_path.last().expect("child path cannot be empty");
4170
4171            loop {
4172                let event = match self.poll_descendant_javascript_child_process(
4173                    vm_id,
4174                    &root_process_id,
4175                    &parent_path,
4176                    child_process_id,
4177                    0,
4178                ) {
4179                    Ok(event) => event,
4180                    Err(SidecarError::InvalidState(message))
4181                        if message.contains("unknown child process")
4182                            || message.contains("unknown child process path") =>
4183                    {
4184                        if let Some(vm) = self.vms.get_mut(vm_id) {
4185                            vm.detached_child_processes.remove(&detached_process_id);
4186                        }
4187                        break;
4188                    }
4189                    Err(error) if is_javascript_child_process_gone_error(&error) => {
4190                        if let Some(vm) = self.vms.get_mut(vm_id) {
4191                            vm.detached_child_processes.remove(&detached_process_id);
4192                        }
4193                        break;
4194                    }
4195                    Err(error) => return Err(error),
4196                };
4197
4198                let Some(event_type) = event.get("type").and_then(Value::as_str) else {
4199                    break;
4200                };
4201                let Some((connection_id, session_id)) = self
4202                    .vms
4203                    .get(vm_id)
4204                    .map(|vm| (vm.connection_id.clone(), vm.session_id.clone()))
4205                else {
4206                    break;
4207                };
4208
4209                let envelope = match event_type {
4210                    "stdout" => Some(ProcessEventEnvelope {
4211                        connection_id: connection_id.clone(),
4212                        session_id: session_id.clone(),
4213                        vm_id: vm_id.to_owned(),
4214                        process_id: detached_process_id.clone(),
4215                        event: ActiveExecutionEvent::Stdout(javascript_sync_rpc_bytes_arg(
4216                            &[event.get("data").cloned().unwrap_or(Value::Null)],
4217                            0,
4218                            "detached child_process stdout",
4219                        )?),
4220                    }),
4221                    "stderr" => Some(ProcessEventEnvelope {
4222                        connection_id: connection_id.clone(),
4223                        session_id: session_id.clone(),
4224                        vm_id: vm_id.to_owned(),
4225                        process_id: detached_process_id.clone(),
4226                        event: ActiveExecutionEvent::Stderr(javascript_sync_rpc_bytes_arg(
4227                            &[event.get("data").cloned().unwrap_or(Value::Null)],
4228                            0,
4229                            "detached child_process stderr",
4230                        )?),
4231                    }),
4232                    "exit" => {
4233                        if let Some(vm) = self.vms.get_mut(vm_id) {
4234                            vm.detached_child_processes.remove(&detached_process_id);
4235                        }
4236                        Some(ProcessEventEnvelope {
4237                            connection_id,
4238                            session_id,
4239                            vm_id: vm_id.to_owned(),
4240                            process_id: detached_process_id.clone(),
4241                            event: ActiveExecutionEvent::Exited(
4242                                event
4243                                    .get("exitCode")
4244                                    .and_then(Value::as_i64)
4245                                    .map(|value| value as i32)
4246                                    .unwrap_or(1),
4247                            ),
4248                        })
4249                    }
4250                    _ => None,
4251                };
4252
4253                let Some(envelope) = envelope else {
4254                    break;
4255                };
4256                self.queue_pending_process_event(envelope)?;
4257                emitted_any = true;
4258
4259                if event_type == "exit" {
4260                    break;
4261                }
4262            }
4263        }
4264
4265        Ok(emitted_any)
4266    }
4267    pub(crate) fn drain_queued_descendant_javascript_child_process_events(
4268        &mut self,
4269        vm_id: &str,
4270        process_id: &str,
4271        child_path: &[&str],
4272    ) -> Result<(), SidecarError> {
4273        if child_path.is_empty() {
4274            return Ok(());
4275        }
4276        let target_process_id = Self::child_process_path_label(process_id, child_path);
4277        let mut child_capacity = self
4278            .vms
4279            .get(vm_id)
4280            .and_then(|vm| vm.active_processes.get(process_id))
4281            .and_then(|root| descendant_pending_execution_event_capacity(root, child_path));
4282
4283        let mut deferred = VecDeque::new();
4284        while let Some(envelope) = self.pending_process_events.pop_front() {
4285            if envelope.vm_id == vm_id && envelope.process_id == target_process_id {
4286                if matches!(child_capacity, Some(0)) {
4287                    self.pending_process_events.push_front(envelope);
4288                    while let Some(deferred_envelope) = deferred.pop_back() {
4289                        self.pending_process_events.push_front(deferred_envelope);
4290                    }
4291                    return Err(process_event_queue_overflow_error());
4292                }
4293                if let Some(vm) = self.vms.get_mut(vm_id) {
4294                    if let Some(root) = vm.active_processes.get_mut(process_id) {
4295                        if let Some(child) = Self::active_process_by_path_mut(root, child_path) {
4296                            child.queue_pending_execution_event(envelope.event)?;
4297                            child_capacity = child_capacity.map(|capacity| capacity - 1);
4298                            continue;
4299                        }
4300                    }
4301                }
4302            }
4303            deferred.push_back(envelope);
4304        }
4305        self.pending_process_events = deferred;
4306
4307        let mut queued = Vec::new();
4308        {
4309            let transfer_capacity = self
4310                .pending_process_event_capacity()
4311                .min(child_capacity.unwrap_or(usize::MAX));
4312            let receiver = self.process_event_receiver.as_mut().ok_or_else(|| {
4313                SidecarError::InvalidState(String::from("process event receiver unavailable"))
4314            })?;
4315            loop {
4316                if queued.len() >= transfer_capacity {
4317                    if receiver.is_empty() {
4318                        break;
4319                    }
4320                    return Err(process_event_queue_overflow_error());
4321                }
4322                match receiver.try_recv() {
4323                    Ok(envelope) => queued.push(envelope),
4324                    Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
4325                    Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
4326                }
4327            }
4328        }
4329        for envelope in queued {
4330            if envelope.vm_id == vm_id && envelope.process_id == target_process_id {
4331                if let Some(vm) = self.vms.get_mut(vm_id) {
4332                    if let Some(root) = vm.active_processes.get_mut(process_id) {
4333                        if let Some(child) = Self::active_process_by_path_mut(root, child_path) {
4334                            child.queue_pending_execution_event(envelope.event)?;
4335                            continue;
4336                        }
4337                    }
4338                }
4339            }
4340            self.queue_pending_process_event(envelope)?;
4341        }
4342
4343        Ok(())
4344    }
4345
4346    pub(crate) fn handle_execution_event(
4347        &mut self,
4348        vm_id: &str,
4349        process_id: &str,
4350        event: ActiveExecutionEvent,
4351    ) -> Result<Option<EventFrame>, SidecarError> {
4352        let Some(vm) = self.vms.get(vm_id) else {
4353            log_stale_process_event(&self.bridge, vm_id, process_id, "execution event dispatch");
4354            return Ok(None);
4355        };
4356        if !vm.active_processes.contains_key(process_id) {
4357            log_stale_process_event(&self.bridge, vm_id, process_id, "execution event dispatch");
4358            return Ok(None);
4359        }
4360        let (connection_id, session_id) = { (vm.connection_id.clone(), vm.session_id.clone()) };
4361        let ownership = OwnershipScope::vm(&connection_id, &session_id, vm_id);
4362
4363        if self.capture_extension_process_output_event(vm_id, process_id, &event) {
4364            return Ok(None);
4365        }
4366
4367        match event {
4368            ActiveExecutionEvent::Stdout(chunk) => Ok(Some(EventFrame::new(
4369                ownership,
4370                EventPayload::ProcessOutput(ProcessOutputEvent {
4371                    process_id: process_id.to_owned(),
4372                    channel: StreamChannel::Stdout,
4373                    chunk,
4374                }),
4375            ))),
4376            ActiveExecutionEvent::Stderr(chunk) => Ok(Some(EventFrame::new(
4377                ownership,
4378                EventPayload::ProcessOutput(ProcessOutputEvent {
4379                    process_id: process_id.to_owned(),
4380                    channel: StreamChannel::Stderr,
4381                    chunk,
4382                }),
4383            ))),
4384            ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
4385                self.handle_javascript_sync_rpc_request(vm_id, process_id, request)?;
4386                Ok(None)
4387            }
4388            ActiveExecutionEvent::PythonVfsRpcRequest(request) => {
4389                self.handle_python_vfs_rpc_request(vm_id, process_id, *request)?;
4390                Ok(None)
4391            }
4392            ActiveExecutionEvent::SignalState {
4393                signal,
4394                registration,
4395            } => {
4396                let Some(vm) = self.vms.get_mut(vm_id) else {
4397                    return Ok(None);
4398                };
4399                if !vm.active_processes.contains_key(process_id) {
4400                    return Ok(None);
4401                }
4402                vm.signal_states
4403                    .entry(process_id.to_owned())
4404                    .or_default()
4405                    .insert(signal, registration);
4406                Ok(None)
4407            }
4408            ActiveExecutionEvent::Exited(exit_code) => {
4409                let became_idle = self
4410                    .finish_active_process_exit(vm_id, process_id, exit_code)?
4411                    .unwrap_or(false);
4412
4413                if became_idle {
4414                    self.bridge.emit_lifecycle(vm_id, LifecycleState::Ready)?;
4415                }
4416
4417                Ok(Some(EventFrame::new(
4418                    ownership,
4419                    EventPayload::ProcessExited(ProcessExitedEvent {
4420                        process_id: process_id.to_owned(),
4421                        exit_code,
4422                    }),
4423                )))
4424            }
4425        }
4426    }
4427
4428    pub(crate) fn finish_active_process_exit(
4429        &mut self,
4430        vm_id: &str,
4431        process_id: &str,
4432        exit_code: i32,
4433    ) -> Result<Option<bool>, SidecarError> {
4434        let Some(vm) = self.vms.get_mut(vm_id) else {
4435            log_stale_process_event(&self.bridge, vm_id, process_id, "process exit cleanup");
4436            return Ok(None);
4437        };
4438        if !vm.active_processes.contains_key(process_id) {
4439            log_stale_process_event(&self.bridge, vm_id, process_id, "process exit cleanup");
4440            return Ok(None);
4441        }
4442
4443        prune_exited_process_snapshots(vm);
4444        let process_table = vm.kernel.list_processes();
4445        let Some(mut process) = vm.active_processes.remove(process_id) else {
4446            return Ok(None);
4447        };
4448        if let Some(info) = process_table.get(&process.kernel_pid) {
4449            vm.exited_process_snapshots
4450                .push_back(ExitedProcessSnapshot {
4451                    captured_at: Instant::now(),
4452                    process: build_process_snapshot_entry(
4453                        process_id,
4454                        &process,
4455                        info,
4456                        Some(exit_code),
4457                    ),
4458                });
4459        }
4460        let detached_children = Self::adopt_detached_child_processes(process_id, &mut process);
4461        sync_process_host_writes_to_kernel(vm, &process)?;
4462        terminate_child_process_tree(&mut vm.kernel, &mut process);
4463        process.kernel_handle.finish(exit_code);
4464        let _ = vm.kernel.wait_and_reap(process.kernel_pid);
4465        vm.signal_states.remove(process_id);
4466        for (detached_process_id, detached_child) in detached_children {
4467            vm.detached_child_processes
4468                .insert(detached_process_id.clone());
4469            vm.active_processes
4470                .insert(detached_process_id, detached_child);
4471        }
4472        let became_idle = vm.active_processes.is_empty();
4473        self.prune_extension_process_resource(process_id);
4474
4475        Ok(Some(became_idle))
4476    }
4477
4478    pub(crate) fn drain_process_events_blocking_with_limit(
4479        &mut self,
4480        vm_id: &str,
4481        process_id: &str,
4482        max_events: usize,
4483    ) -> Result<Vec<ActiveExecutionEvent>, SidecarError> {
4484        let mut events = Vec::new();
4485        if max_events == 0 {
4486            return Ok(events);
4487        }
4488        let mut deadline = Instant::now() + Duration::from_millis(150);
4489
4490        loop {
4491            if events.len() >= max_events {
4492                break;
4493            }
4494            let event = {
4495                let Some(vm) = self.vms.get_mut(vm_id) else {
4496                    break;
4497                };
4498                let Some(process) = vm.active_processes.get_mut(process_id) else {
4499                    break;
4500                };
4501                if let Some(event) = process.pending_execution_events.pop_front() {
4502                    Some(event)
4503                } else {
4504                    match process.execution.poll_event_blocking(Duration::ZERO) {
4505                        Ok(event) => event,
4506                        Err(SidecarError::Execution(_)) => None,
4507                        Err(other) => return Err(other),
4508                    }
4509                }
4510            };
4511
4512            let Some(event) = event else {
4513                if Instant::now() >= deadline {
4514                    break;
4515                }
4516                let blocking_wait = deadline.saturating_duration_since(Instant::now());
4517                if blocking_wait.is_zero() {
4518                    break;
4519                }
4520                if events.len() >= max_events {
4521                    break;
4522                }
4523                let delayed_event = {
4524                    let Some(vm) = self.vms.get_mut(vm_id) else {
4525                        break;
4526                    };
4527                    let Some(process) = vm.active_processes.get_mut(process_id) else {
4528                        break;
4529                    };
4530                    if let Some(event) = process.pending_execution_events.pop_front() {
4531                        Some(event)
4532                    } else {
4533                        match process.execution.poll_event_blocking(blocking_wait) {
4534                            Ok(event) => event,
4535                            Err(SidecarError::Execution(_)) => None,
4536                            Err(other) => return Err(other),
4537                        }
4538                    }
4539                };
4540                let Some(event) = delayed_event else {
4541                    break;
4542                };
4543                events.push(event);
4544                deadline = Instant::now() + Duration::from_millis(150);
4545                continue;
4546            };
4547            events.push(event);
4548            deadline = Instant::now() + Duration::from_millis(150);
4549        }
4550
4551        Ok(events)
4552    }
4553
4554    pub(crate) fn handle_python_vfs_rpc_request(
4555        &mut self,
4556        vm_id: &str,
4557        process_id: &str,
4558        request: PythonVfsRpcRequest,
4559    ) -> Result<(), SidecarError> {
4560        match request.method {
4561            PythonVfsRpcMethod::Read
4562            | PythonVfsRpcMethod::Write
4563            | PythonVfsRpcMethod::Stat
4564            | PythonVfsRpcMethod::ReadDir
4565            | PythonVfsRpcMethod::Mkdir => {
4566                filesystem_handle_python_vfs_rpc_request(self, vm_id, process_id, request)
4567            }
4568            PythonVfsRpcMethod::HttpRequest => {
4569                self.handle_python_http_rpc_request(vm_id, process_id, request)
4570            }
4571            PythonVfsRpcMethod::DnsLookup => {
4572                self.handle_python_dns_rpc_request(vm_id, process_id, request)
4573            }
4574            PythonVfsRpcMethod::SubprocessRun => {
4575                self.handle_python_subprocess_rpc_request(vm_id, process_id, request)
4576            }
4577        }
4578    }
4579
4580    fn handle_python_http_rpc_request(
4581        &mut self,
4582        vm_id: &str,
4583        process_id: &str,
4584        request: PythonVfsRpcRequest,
4585    ) -> Result<(), SidecarError> {
4586        let Some(vm) = self.vms.get(vm_id) else {
4587            return Ok(());
4588        };
4589        if !vm.active_processes.contains_key(process_id) {
4590            return Ok(());
4591        }
4592        let response = (|| {
4593            let url_text = request.url.as_deref().ok_or_else(|| {
4594                SidecarError::InvalidState(String::from("python httpRequest requires a url"))
4595            })?;
4596            let url = Url::parse(url_text)
4597                .map_err(|error| SidecarError::Execution(format!("ERR_INVALID_URL: {error}")))?;
4598            let host = url.host_str().ok_or_else(|| {
4599                SidecarError::Execution(String::from("ERR_INVALID_URL: missing host"))
4600            })?;
4601            let port = url.port_or_known_default().ok_or_else(|| {
4602                SidecarError::Execution(String::from("ERR_INVALID_URL: missing port"))
4603            })?;
4604            self.bridge.require_network_access(
4605                vm_id,
4606                NetworkOperation::Http,
4607                format_tcp_resource(host, port),
4608            )?;
4609            // Pin the outbound connection to the IP addresses that pass the
4610            // egress range guard at resolution time. A literal IP is validated
4611            // directly; a hostname is resolved once here and the resulting
4612            // address set is pinned into the HTTP client's resolver below so a
4613            // rebinding DNS server cannot make the second (TLS/TCP) lookup land
4614            // on a private/link-local/metadata IP that this check rejected.
4615            let pinned_addresses = if let Ok(literal_ip) = host.parse::<IpAddr>() {
4616                filter_dns_safe_ip_addrs(vec![literal_ip], host)?
4617            } else {
4618                filter_dns_safe_ip_addrs(
4619                    resolve_dns_ip_addrs(
4620                        &self.bridge,
4621                        &vm.kernel,
4622                        vm_id,
4623                        &vm.dns,
4624                        host,
4625                        DnsLookupPolicy::SkipPermissions,
4626                    )?,
4627                    host,
4628                )?
4629            };
4630            let mut headers = BTreeMap::new();
4631            for (name, value) in &request.headers {
4632                headers.insert(name.clone(), Value::String(value.clone()));
4633            }
4634            let options = JavascriptHttpRequestOptions {
4635                method: Some(
4636                    request
4637                        .http_method
4638                        .clone()
4639                        .unwrap_or_else(|| String::from("GET")),
4640                ),
4641                headers,
4642                body: request.body_base64.as_deref().map(|body| {
4643                    String::from_utf8(
4644                        base64::engine::general_purpose::STANDARD
4645                            .decode(body)
4646                            .unwrap_or_default(),
4647                    )
4648                    .unwrap_or_default()
4649                }),
4650                reject_unauthorized: None,
4651            };
4652            let headers =
4653                parse_http_header_collection(&options.headers, "python httpRequest headers")?;
4654            let response =
4655                issue_outbound_http_request(&url, &options, &headers, &pinned_addresses)?;
4656            let payload_json = response.as_str().ok_or_else(|| {
4657                SidecarError::Execution(String::from(
4658                    "python httpRequest returned a non-string response payload",
4659                ))
4660            })?;
4661            let payload: Value = serde_json::from_str(payload_json).map_err(|error| {
4662                SidecarError::Execution(format!(
4663                    "python httpRequest response must be valid JSON: {error}"
4664                ))
4665            })?;
4666            let header_map = payload
4667                .get("headers")
4668                .and_then(Value::as_array)
4669                .map(|entries| {
4670                    let mut normalized = BTreeMap::<String, Vec<String>>::new();
4671                    for entry in entries {
4672                        let Some(pair) = entry.as_array() else {
4673                            continue;
4674                        };
4675                        let Some(name) = pair.first().and_then(Value::as_str) else {
4676                            continue;
4677                        };
4678                        let Some(value) = pair.get(1).and_then(Value::as_str) else {
4679                            continue;
4680                        };
4681                        normalized
4682                            .entry(name.to_owned())
4683                            .or_default()
4684                            .push(value.to_owned());
4685                    }
4686                    normalized
4687                })
4688                .unwrap_or_default();
4689            Ok(PythonVfsRpcResponsePayload::Http {
4690                status: payload
4691                    .get("status")
4692                    .and_then(Value::as_u64)
4693                    .map(|value| value as u16)
4694                    .unwrap_or_default(),
4695                reason: payload
4696                    .get("statusText")
4697                    .and_then(Value::as_str)
4698                    .unwrap_or_default()
4699                    .to_owned(),
4700                url: payload
4701                    .get("url")
4702                    .and_then(Value::as_str)
4703                    .unwrap_or(url_text)
4704                    .to_owned(),
4705                headers: header_map,
4706                body_base64: payload
4707                    .get("body")
4708                    .and_then(Value::as_str)
4709                    .unwrap_or_default()
4710                    .to_owned(),
4711            })
4712        })();
4713
4714        self.respond_python_rpc(vm_id, process_id, request.id, response)
4715    }
4716
4717    fn handle_python_dns_rpc_request(
4718        &mut self,
4719        vm_id: &str,
4720        process_id: &str,
4721        request: PythonVfsRpcRequest,
4722    ) -> Result<(), SidecarError> {
4723        let Some(vm) = self.vms.get(vm_id) else {
4724            return Ok(());
4725        };
4726        if !vm.active_processes.contains_key(process_id) {
4727            return Ok(());
4728        }
4729        let response = (|| {
4730            let hostname = request.hostname.as_deref().ok_or_else(|| {
4731                SidecarError::InvalidState(String::from("python dnsLookup requires a hostname"))
4732            })?;
4733            let mut addresses = filter_dns_safe_ip_addrs(
4734                resolve_dns_ip_addrs(
4735                    &self.bridge,
4736                    &vm.kernel,
4737                    vm_id,
4738                    &vm.dns,
4739                    hostname,
4740                    DnsLookupPolicy::CheckPermissions,
4741                )?,
4742                hostname,
4743            )?;
4744            if let Some(family) = request.family {
4745                addresses.retain(|address| {
4746                    matches!((family, address), (4, IpAddr::V4(_)) | (6, IpAddr::V6(_)))
4747                });
4748            }
4749            Ok(PythonVfsRpcResponsePayload::DnsLookup {
4750                addresses: addresses
4751                    .into_iter()
4752                    .map(|address| address.to_string())
4753                    .collect(),
4754            })
4755        })();
4756
4757        self.respond_python_rpc(vm_id, process_id, request.id, response)
4758    }
4759
4760    fn handle_python_subprocess_rpc_request(
4761        &mut self,
4762        vm_id: &str,
4763        process_id: &str,
4764        request: PythonVfsRpcRequest,
4765    ) -> Result<(), SidecarError> {
4766        let command = request.command.clone().ok_or_else(|| {
4767            SidecarError::InvalidState(String::from("python subprocessRun requires a command"))
4768        })?;
4769        let (internal_bootstrap_env, cwd) = {
4770            let Some(vm) = self.vms.get(vm_id) else {
4771                return Ok(());
4772            };
4773            let Some(process) = vm.active_processes.get(process_id) else {
4774                return Ok(());
4775            };
4776            let virtual_home = guest_virtual_home(vm);
4777            let cwd = request.cwd.clone().or_else(|| {
4778                guest_runtime_path_for_host_path(
4779                    &vm.guest_env,
4780                    &virtual_home,
4781                    &vm.host_cwd,
4782                    &process.host_cwd.to_string_lossy(),
4783                )
4784            });
4785            (
4786                sanitize_javascript_child_process_internal_bootstrap_env(&vm.guest_env),
4787                cwd,
4788            )
4789        };
4790        let response = self
4791            .spawn_javascript_child_process_sync(
4792                vm_id,
4793                process_id,
4794                JavascriptChildProcessSpawnRequest {
4795                    command,
4796                    args: request.args.clone(),
4797                    options: JavascriptChildProcessSpawnOptions {
4798                        cwd,
4799                        env: request.env.clone(),
4800                        input: None,
4801                        internal_bootstrap_env,
4802                        shell: request.shell,
4803                        detached: false,
4804                        stdio: vec![
4805                            String::from("pipe"),
4806                            String::from("pipe"),
4807                            String::from("pipe"),
4808                        ],
4809                        timeout: None,
4810                        kill_signal: None,
4811                    },
4812                },
4813                request.max_buffer,
4814            )
4815            .map(|payload| PythonVfsRpcResponsePayload::SubprocessRun {
4816                exit_code: payload
4817                    .get("code")
4818                    .and_then(Value::as_i64)
4819                    .map(|value| value as i32)
4820                    .unwrap_or(1),
4821                stdout: payload
4822                    .get("stdout")
4823                    .and_then(Value::as_str)
4824                    .unwrap_or_default()
4825                    .to_owned(),
4826                stderr: payload
4827                    .get("stderr")
4828                    .and_then(Value::as_str)
4829                    .unwrap_or_default()
4830                    .to_owned(),
4831                max_buffer_exceeded: payload
4832                    .get("maxBufferExceeded")
4833                    .and_then(Value::as_bool)
4834                    .unwrap_or(false),
4835            });
4836
4837        self.respond_python_rpc(vm_id, process_id, request.id, response)
4838    }
4839
4840    fn respond_python_rpc(
4841        &mut self,
4842        vm_id: &str,
4843        process_id: &str,
4844        request_id: u64,
4845        response: Result<PythonVfsRpcResponsePayload, SidecarError>,
4846    ) -> Result<(), SidecarError> {
4847        let Some(vm) = self.vms.get_mut(vm_id) else {
4848            return Ok(());
4849        };
4850        let Some(process) = vm.active_processes.get_mut(process_id) else {
4851            return Ok(());
4852        };
4853        let result = match response {
4854            Ok(payload) => process
4855                .execution
4856                .respond_python_vfs_rpc_success(request_id, payload),
4857            Err(error) => process.execution.respond_python_vfs_rpc_error(
4858                request_id,
4859                "ERR_AGENTOS_PYTHON_VFS_RPC",
4860                error.to_string(),
4861            ),
4862        };
4863        match result {
4864            Ok(()) => Ok(()),
4865            Err(error) if is_broken_pipe_error(&error) => Ok(()),
4866            Err(error) => Err(error),
4867        }
4868    }
4869
4870    pub(crate) fn resolve_javascript_child_process_execution(
4871        &self,
4872        vm: &VmState,
4873        parent_env: &BTreeMap<String, String>,
4874        parent_guest_cwd: &str,
4875        parent_host_cwd: &Path,
4876        request: &JavascriptChildProcessSpawnRequest,
4877    ) -> Result<ResolvedChildProcessExecution, SidecarError> {
4878        let mut runtime_env = parent_env.clone();
4879        runtime_env.extend(request.options.internal_bootstrap_env.clone());
4880        let (guest_cwd, host_cwd_override) = request
4881            .options
4882            .cwd
4883            .as_deref()
4884            .map(|cwd| {
4885                let normalized_parent_host_cwd = normalize_host_path(parent_host_cwd);
4886                let requested_host_cwd = normalize_host_path(Path::new(cwd));
4887                if path_is_within_root(&requested_host_cwd, &normalized_parent_host_cwd) {
4888                    let relative = requested_host_cwd
4889                        .strip_prefix(&normalized_parent_host_cwd)
4890                        .unwrap_or_else(|_| Path::new(""));
4891                    let relative = relative.to_string_lossy().replace('\\', "/");
4892                    let guest_cwd = if relative.is_empty() {
4893                        parent_guest_cwd.to_owned()
4894                    } else {
4895                        normalize_path(&format!("{parent_guest_cwd}/{relative}"))
4896                    };
4897                    (guest_cwd, Some(requested_host_cwd))
4898                } else if Path::new(cwd).is_relative() {
4899                    (
4900                        normalize_path(&format!("{parent_guest_cwd}/{cwd}")),
4901                        Some(normalize_host_path(&parent_host_cwd.join(cwd))),
4902                    )
4903                } else {
4904                    (normalize_path(cwd), None)
4905                }
4906            })
4907            .unwrap_or_else(|| (parent_guest_cwd.to_owned(), None));
4908        let inherited_host_cwd = (host_cwd_override.is_none() && guest_cwd == parent_guest_cwd)
4909            .then(|| normalize_host_path(parent_host_cwd));
4910        let host_cwd = host_cwd_override
4911            .or(inherited_host_cwd)
4912            .or_else(|| {
4913                host_runtime_path_for_guest_path_with_env(
4914                    vm,
4915                    &runtime_env,
4916                    &guest_cwd,
4917                    parent_host_cwd,
4918                )
4919            })
4920            .unwrap_or_else(|| {
4921                let candidate = PathBuf::from(&guest_cwd);
4922                if guest_cwd == parent_guest_cwd {
4923                    normalize_host_path(parent_host_cwd)
4924                } else if candidate.is_absolute() {
4925                    shadow_path_for_guest(vm, &guest_cwd)
4926                } else {
4927                    vm.host_cwd.clone()
4928                }
4929            });
4930        let mut env = parent_env.clone();
4931        env.extend(request.options.env.clone());
4932        // Child JavaScript executions must resolve their own entrypoint/eval state.
4933        // Reusing the parent's values makes the sidecar load the wrong source file.
4934        env.remove("AGENTOS_GUEST_ENTRYPOINT");
4935        env.remove("AGENTOS_NODE_EVAL");
4936
4937        let (command, process_args) = if request.options.shell {
4938            let tokens = tokenize_shell_free_command(&request.command);
4939            let requires_shell = command_requires_shell(&request.command)
4940                || tokens.first().is_some_and(|command| {
4941                    is_posix_shell_builtin(command) || shell_first_token_requires_shell(command)
4942                });
4943            if requires_shell {
4944                if !vm.command_guest_paths.contains_key("sh") {
4945                    return Err(SidecarError::InvalidState(format!(
4946                        "shell-mode child_process command requires /bin/sh, which is not \
4947                         installed in this VM (install a software package that provides sh, \
4948                         for example @secure-exec/coreutils): {}",
4949                        request.command
4950                    )));
4951                }
4952                (
4953                    String::from("sh"),
4954                    vec![String::from("-c"), request.command.clone()],
4955                )
4956            } else {
4957                let Some((command, args)) = tokens.split_first() else {
4958                    return Err(SidecarError::InvalidState(String::from(
4959                        "child_process shell command must not be empty",
4960                    )));
4961                };
4962                (command.clone(), args.to_vec())
4963            }
4964        } else {
4965            (request.command.clone(), request.args.clone())
4966        };
4967        let process_args = apply_shell_cwd_prefix(&command, process_args, &guest_cwd);
4968        if is_tool_command(vm, &command) {
4969            let command = normalized_tool_command_name(&command).unwrap_or(command);
4970            return Ok(ResolvedChildProcessExecution {
4971                command: command.clone(),
4972                process_args: std::iter::once(command.clone())
4973                    .chain(process_args.iter().cloned())
4974                    .collect(),
4975                runtime: GuestRuntimeKind::JavaScript,
4976                entrypoint: command,
4977                execution_args: process_args,
4978                env,
4979                guest_cwd,
4980                host_cwd,
4981                wasm_permission_tier: None,
4982                tool_command: true,
4983            });
4984        }
4985
4986        if is_path_like_specifier(&command)
4987            && matches!(
4988                Path::new(&command).extension().and_then(|ext| ext.to_str()),
4989                Some("js" | "mjs" | "cjs" | "ts" | "mts" | "cts")
4990            )
4991        {
4992            let guest_entrypoint = if command.starts_with('/') {
4993                normalize_path(&command)
4994            } else if command.starts_with("file:") {
4995                normalize_path(command.trim_start_matches("file:"))
4996            } else {
4997                normalize_path(&format!("{guest_cwd}/{command}"))
4998            };
4999            let host_entrypoint = if command.starts_with("./") || command.starts_with("../") {
5000                normalize_host_path(&host_cwd.join(&command))
5001            } else {
5002                host_runtime_path_for_guest_path_with_env(
5003                    vm,
5004                    &runtime_env,
5005                    &guest_entrypoint,
5006                    parent_host_cwd,
5007                )
5008                .unwrap_or_else(|| {
5009                    let candidate = PathBuf::from(&guest_entrypoint);
5010                    if candidate.is_absolute() {
5011                        candidate
5012                    } else {
5013                        host_cwd.join(&guest_entrypoint)
5014                    }
5015                })
5016            };
5017            env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint);
5018            let guest_entrypoint = env.get("AGENTOS_GUEST_ENTRYPOINT").cloned();
5019            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
5020
5021            return Ok(ResolvedChildProcessExecution {
5022                command: command.clone(),
5023                process_args: std::iter::once(command)
5024                    .chain(process_args.iter().cloned())
5025                    .collect(),
5026                runtime: GuestRuntimeKind::JavaScript,
5027                entrypoint: host_entrypoint.to_string_lossy().into_owned(),
5028                execution_args: process_args,
5029                env,
5030                guest_cwd,
5031                host_cwd,
5032                wasm_permission_tier: None,
5033                tool_command: false,
5034            });
5035        }
5036
5037        if is_node_runtime_command(&command) {
5038            if let Some(cli) = resolve_host_node_cli_entrypoint(&command) {
5039                env.insert(
5040                    String::from("AGENTOS_NODE_EVAL"),
5041                    build_host_node_cli_eval(&cli),
5042                );
5043                prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
5044                add_runtime_guest_path_mapping(&mut env, &cli.guest_root, &cli.package_root);
5045                add_runtime_host_access_path(
5046                    &mut env,
5047                    "AGENTOS_EXTRA_FS_READ_PATHS",
5048                    &cli.package_root,
5049                    true,
5050                );
5051
5052                return Ok(ResolvedChildProcessExecution {
5053                    command: command.clone(),
5054                    process_args: std::iter::once(command.clone())
5055                        .chain(process_args.iter().cloned())
5056                        .collect(),
5057                    runtime: GuestRuntimeKind::JavaScript,
5058                    entrypoint: String::from("-e"),
5059                    execution_args: std::iter::once(cli.guest_entrypoint.clone())
5060                        .chain(process_args.iter().cloned())
5061                        .collect(),
5062                    env,
5063                    guest_cwd,
5064                    host_cwd,
5065                    wasm_permission_tier: None,
5066                    tool_command: false,
5067                });
5068            }
5069
5070            if process_args.is_empty() {
5071                env.insert(String::from("AGENTOS_NODE_EVAL"), String::new());
5072                prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
5073
5074                return Ok(ResolvedChildProcessExecution {
5075                    command: command.clone(),
5076                    process_args: vec![command.clone()],
5077                    runtime: GuestRuntimeKind::JavaScript,
5078                    entrypoint: String::from("-e"),
5079                    execution_args: Vec::new(),
5080                    env,
5081                    guest_cwd,
5082                    host_cwd,
5083                    wasm_permission_tier: None,
5084                    tool_command: false,
5085                });
5086            }
5087
5088            if let Some((entrypoint, execution_args)) =
5089                resolve_special_node_cli_invocation(&process_args, &mut env)
5090            {
5091                prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
5092
5093                return Ok(ResolvedChildProcessExecution {
5094                    command: command.clone(),
5095                    process_args: std::iter::once(command.clone())
5096                        .chain(process_args.iter().cloned())
5097                        .collect(),
5098                    runtime: GuestRuntimeKind::JavaScript,
5099                    entrypoint,
5100                    execution_args,
5101                    env,
5102                    guest_cwd,
5103                    host_cwd,
5104                    wasm_permission_tier: None,
5105                    tool_command: false,
5106                });
5107            }
5108
5109            let Some(entrypoint_specifier) = process_args.first() else {
5110                return Err(SidecarError::InvalidState(format!(
5111                    "{command} child_process spawn requires an entrypoint"
5112                )));
5113            };
5114
5115            let (entrypoint, execution_args) = if is_path_like_specifier(entrypoint_specifier) {
5116                let guest_entrypoint = if entrypoint_specifier.starts_with('/') {
5117                    normalize_path(entrypoint_specifier)
5118                } else if entrypoint_specifier.starts_with("file:") {
5119                    normalize_path(entrypoint_specifier.trim_start_matches("file:"))
5120                } else {
5121                    normalize_path(&format!("{guest_cwd}/{entrypoint_specifier}"))
5122                };
5123                let host_entrypoint = if entrypoint_specifier.starts_with("./")
5124                    || entrypoint_specifier.starts_with("../")
5125                {
5126                    normalize_host_path(&host_cwd.join(entrypoint_specifier))
5127                } else {
5128                    host_runtime_path_for_guest_path_with_env(
5129                        vm,
5130                        &runtime_env,
5131                        &guest_entrypoint,
5132                        parent_host_cwd,
5133                    )
5134                    .unwrap_or_else(|| {
5135                        let candidate = PathBuf::from(&guest_entrypoint);
5136                        if candidate.is_absolute() {
5137                            candidate
5138                        } else {
5139                            host_cwd.join(&guest_entrypoint)
5140                        }
5141                    })
5142                };
5143                env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint);
5144                (
5145                    host_entrypoint.to_string_lossy().into_owned(),
5146                    process_args.iter().skip(1).cloned().collect(),
5147                )
5148            } else {
5149                (
5150                    entrypoint_specifier.clone(),
5151                    process_args.iter().skip(1).cloned().collect(),
5152                )
5153            };
5154            let guest_entrypoint = env.get("AGENTOS_GUEST_ENTRYPOINT").cloned();
5155            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
5156
5157            return Ok(ResolvedChildProcessExecution {
5158                command: command.clone(),
5159                process_args: std::iter::once(command)
5160                    .chain(process_args.iter().cloned())
5161                    .collect(),
5162                runtime: GuestRuntimeKind::JavaScript,
5163                entrypoint,
5164                execution_args,
5165                env,
5166                guest_cwd,
5167                host_cwd,
5168                wasm_permission_tier: None,
5169                tool_command: false,
5170            });
5171        }
5172
5173        if command == PYTHON_COMMAND {
5174            return Err(SidecarError::InvalidState(String::from(
5175                "nested python child_process execution is not supported yet",
5176            )));
5177        }
5178
5179        let guest_entrypoint = resolve_guest_command_entrypoint(
5180            vm,
5181            &guest_cwd,
5182            &command,
5183            env.get("PATH").map(String::as_str),
5184        )
5185        .ok_or_else(|| SidecarError::InvalidState(format!("command not found: {command}")))?;
5186        let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint);
5187        let wasm_permission_tier = vm.command_permissions.get(&command).copied().or_else(|| {
5188            Path::new(&guest_entrypoint)
5189                .file_name()
5190                .and_then(|name| name.to_str())
5191                .and_then(|name| vm.command_permissions.get(name).copied())
5192        });
5193        if let Some((javascript_guest_entrypoint, javascript_host_entrypoint)) =
5194            resolve_javascript_command_entrypoint(vm, &guest_entrypoint, &host_entrypoint)
5195        {
5196            prepare_guest_runtime_env(
5197                vm,
5198                &mut env,
5199                &guest_cwd,
5200                &host_cwd,
5201                Some(javascript_guest_entrypoint),
5202            )?;
5203
5204            return Ok(ResolvedChildProcessExecution {
5205                command: command.clone(),
5206                process_args: std::iter::once(command)
5207                    .chain(process_args.iter().cloned())
5208                    .collect(),
5209                runtime: GuestRuntimeKind::JavaScript,
5210                entrypoint: javascript_host_entrypoint.to_string_lossy().into_owned(),
5211                execution_args: process_args,
5212                env,
5213                guest_cwd,
5214                host_cwd,
5215                wasm_permission_tier: None,
5216                tool_command: false,
5217            });
5218        }
5219        prepare_guest_runtime_env(
5220            vm,
5221            &mut env,
5222            &guest_cwd,
5223            &host_cwd,
5224            Some(guest_entrypoint.clone()),
5225        )?;
5226
5227        Ok(ResolvedChildProcessExecution {
5228            command: command.clone(),
5229            process_args: std::iter::once(command)
5230                .chain(process_args.iter().cloned())
5231                .collect(),
5232            runtime: GuestRuntimeKind::WebAssembly,
5233            entrypoint: host_entrypoint.to_string_lossy().into_owned(),
5234            execution_args: process_args,
5235            env,
5236            guest_cwd,
5237            host_cwd,
5238            wasm_permission_tier,
5239            tool_command: false,
5240        })
5241    }
5242
5243    pub(crate) fn spawn_javascript_child_process(
5244        &mut self,
5245        vm_id: &str,
5246        process_id: &str,
5247        request: JavascriptChildProcessSpawnRequest,
5248    ) -> Result<Value, SidecarError> {
5249        let resolved = {
5250            let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?;
5251            let parent = vm
5252                .active_processes
5253                .get(process_id)
5254                .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5255            self.resolve_javascript_child_process_execution(
5256                vm,
5257                &parent.env,
5258                &parent.guest_cwd,
5259                &parent.host_cwd,
5260                &request,
5261            )?
5262        };
5263        let (parent_kernel_pid, child_process_id) = {
5264            let vm = self
5265                .vms
5266                .get_mut(vm_id)
5267                .ok_or_else(|| missing_vm_error(vm_id))?;
5268            let process = vm
5269                .active_processes
5270                .get_mut(process_id)
5271                .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5272            (process.kernel_pid, process.allocate_child_process_id())
5273        };
5274        let sidecar_requests = self.sidecar_requests.clone();
5275        let vm = self
5276            .vms
5277            .get_mut(vm_id)
5278            .ok_or_else(|| missing_vm_error(vm_id))?;
5279        let (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd) = if resolved
5280            .tool_command
5281        {
5282            let tool_resolution = resolve_tool_command(
5283                vm,
5284                &resolved.command,
5285                &resolved.execution_args,
5286                Some(&resolved.guest_cwd),
5287            )?
5288            .ok_or_else(|| {
5289                SidecarError::InvalidState(format!(
5290                    "tool command no longer resolves: {}",
5291                    resolved.command
5292                ))
5293            })?;
5294            let kernel_handle = vm
5295                .kernel
5296                .create_virtual_process(
5297                    EXECUTION_DRIVER_NAME,
5298                    TOOL_DRIVER_NAME,
5299                    &resolved.command,
5300                    resolved.process_args.clone(),
5301                    VirtualProcessOptions {
5302                        parent_pid: Some(parent_kernel_pid),
5303                        env: resolved.env.clone(),
5304                        cwd: Some(resolved.guest_cwd.clone()),
5305                    },
5306                )
5307                .map_err(kernel_error)?;
5308            let kernel_pid = kernel_handle.pid();
5309            let tool_execution = ToolExecution::default();
5310            let cancelled = tool_execution.cancelled.clone();
5311            let pending_events = tool_execution.pending_events.clone();
5312            let events_overflowed = tool_execution.events_overflowed.clone();
5313            spawn_tool_process_events(ToolProcessEventRequest {
5314                sidecar_requests: sidecar_requests.clone(),
5315                connection_id: vm.connection_id.clone(),
5316                session_id: vm.session_id.clone(),
5317                vm_id: vm_id.to_owned(),
5318                tool_resolution,
5319                cancelled,
5320                pending_events,
5321                events_overflowed,
5322            });
5323            (
5324                kernel_pid,
5325                kernel_handle,
5326                ActiveExecution::Tool(tool_execution),
5327                None,
5328            )
5329        } else {
5330            let kernel_command = match resolved.runtime {
5331                GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND,
5332                GuestRuntimeKind::WebAssembly => WASM_COMMAND,
5333                GuestRuntimeKind::Python => {
5334                    unreachable!("python child_process execution is rejected")
5335                }
5336            };
5337            let kernel_handle = vm
5338                .kernel
5339                .spawn_process(
5340                    kernel_command,
5341                    resolved.process_args.clone(),
5342                    SpawnOptions {
5343                        requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
5344                        parent_pid: Some(parent_kernel_pid),
5345                        env: resolved.env.clone(),
5346                        cwd: Some(resolved.guest_cwd.clone()),
5347                    },
5348                )
5349                .map_err(kernel_error)?;
5350            let kernel_pid = kernel_handle.pid();
5351            if request.options.detached {
5352                vm.kernel
5353                    .setsid(EXECUTION_DRIVER_NAME, kernel_pid)
5354                    .map_err(kernel_error)?;
5355            }
5356            let mut execution_env = resolved.env.clone();
5357            execution_env.insert(
5358                String::from(EXECUTION_SANDBOX_ROOT_ENV),
5359                normalize_host_path(&vm.cwd).to_string_lossy().into_owned(),
5360            );
5361
5362            let execution = match resolved.runtime {
5363                GuestRuntimeKind::JavaScript => {
5364                    execution_env.extend(sanitize_javascript_child_process_internal_bootstrap_env(
5365                        &request.options.internal_bootstrap_env,
5366                    ));
5367                    execution_env.insert(
5368                        String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
5369                        String::from("1"),
5370                    );
5371                    let context =
5372                        self.javascript_engine
5373                            .create_context(CreateJavascriptContextRequest {
5374                                vm_id: vm_id.to_owned(),
5375                                bootstrap_module: None,
5376                                compile_cache_root: Some(
5377                                    self.cache_root.join("node-compile-cache"),
5378                                ),
5379                            });
5380                    let inline_code = load_javascript_entrypoint_source(
5381                        vm,
5382                        &resolved.host_cwd,
5383                        &resolved.entrypoint,
5384                        &execution_env,
5385                    );
5386                    prepare_javascript_shadow(vm, &resolved)?;
5387
5388                    let built_reader = build_module_reader(vm, &resolved);
5389                    let guest_reader = built_reader
5390                        .clone()
5391                        .map(|reader| {
5392                        Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader))
5393                            as Box<dyn GuestModuleReader>
5394                    });
5395                    let module_reader = built_reader
5396                        .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
5397                    let execution = self
5398                        .javascript_engine
5399                        .start_execution_with_module_reader(
5400                            StartJavascriptExecutionRequest {
5401                                guest_runtime: guest_runtime_identity(
5402                                    vm,
5403                                    Some(u64::from(kernel_pid)),
5404                                    Some(u64::from(parent_kernel_pid)),
5405                                ),
5406                                vm_id: vm_id.to_owned(),
5407                                context_id: context.context_id,
5408                                argv: std::iter::once(resolved.entrypoint.clone())
5409                                    .chain(resolved.execution_args.clone())
5410                                    .collect(),
5411                                env: execution_env,
5412                                cwd: resolved.host_cwd.clone(),
5413                                limits: javascript_execution_limits(vm),
5414                                inline_code,
5415                            },
5416                            module_reader,
5417                            guest_reader,
5418                        )
5419                        .map_err(javascript_error)?;
5420                    ActiveExecution::Javascript(execution)
5421                }
5422                GuestRuntimeKind::WebAssembly => {
5423                    execution_env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
5424                    let wasm_limits = wasm_execution_limits(vm);
5425                    let wasm_guest_runtime = guest_runtime_identity(
5426                        vm,
5427                        Some(u64::from(kernel_pid)),
5428                        Some(u64::from(parent_kernel_pid)),
5429                    );
5430                    let context = self.wasm_engine.create_context(CreateWasmContextRequest {
5431                        vm_id: vm_id.to_owned(),
5432                        module_path: Some(resolved.entrypoint.clone()),
5433                    });
5434                    let execution = self
5435                        .wasm_engine
5436                        .start_execution(StartWasmExecutionRequest {
5437                            vm_id: vm_id.to_owned(),
5438                            context_id: context.context_id,
5439                            argv: resolved.process_args.clone(),
5440                            env: execution_env,
5441                            cwd: resolved.host_cwd.clone(),
5442                            permission_tier: execution_wasm_permission_tier(
5443                                resolved
5444                                    .wasm_permission_tier
5445                                    .unwrap_or(WasmPermissionTier::Full),
5446                            ),
5447                            limits: wasm_limits,
5448                            guest_runtime: wasm_guest_runtime,
5449                        })
5450                        .map_err(wasm_error)?;
5451                    ActiveExecution::Wasm(Box::new(execution))
5452                }
5453                GuestRuntimeKind::Python => {
5454                    unreachable!("python child_process execution is rejected")
5455                }
5456            };
5457            let kernel_stdin_writer_fd = match javascript_child_process_stdin_mode(&request) {
5458                "pipe" => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5459                "ignore" => {
5460                    vm.kernel
5461                        .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, 0)
5462                        .map_err(kernel_error)?;
5463                    None
5464                }
5465                "inherit" => None,
5466                _ => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5467            };
5468            (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd)
5469        };
5470
5471        let process = vm
5472            .active_processes
5473            .get_mut(process_id)
5474            .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5475        process.child_processes.insert(
5476            child_process_id.clone(),
5477            ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
5478                .with_detached(request.options.detached)
5479                .with_guest_cwd(resolved.guest_cwd.clone())
5480                .with_env(resolved.env.clone())
5481                .with_host_cwd(resolved.host_cwd.clone()),
5482        );
5483        if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd {
5484            process
5485                .child_processes
5486                .get_mut(&child_process_id)
5487                .ok_or_else(|| {
5488                    SidecarError::InvalidState(format!(
5489                        "child process {child_process_id} disappeared during spawn"
5490                    ))
5491                })?
5492                .kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd);
5493        }
5494        Ok(json!({
5495            "childId": child_process_id,
5496            "pid": kernel_pid,
5497            "command": resolved.command,
5498            "args": resolved.process_args,
5499        }))
5500    }
5501
5502    pub(crate) fn spawn_javascript_child_process_sync(
5503        &mut self,
5504        vm_id: &str,
5505        process_id: &str,
5506        request: JavascriptChildProcessSpawnRequest,
5507        max_buffer: Option<usize>,
5508    ) -> Result<Value, SidecarError> {
5509        let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?;
5510        let timeout_deadline = request
5511            .options
5512            .timeout
5513            .map(|timeout_ms| Instant::now() + Duration::from_millis(timeout_ms));
5514        let timeout_signal = request
5515            .options
5516            .kill_signal
5517            .clone()
5518            .unwrap_or_else(|| String::from("SIGTERM"));
5519        let spawned = self.spawn_javascript_child_process(vm_id, process_id, request)?;
5520        let child_process_id = spawned
5521            .get("childId")
5522            .and_then(Value::as_str)
5523            .ok_or_else(|| {
5524                SidecarError::InvalidState(String::from(
5525                    "child_process.spawn_sync response is missing childId",
5526                ))
5527            })?
5528            .to_owned();
5529
5530        if let Some(input) = sync_input.as_deref() {
5531            self.write_javascript_child_process_stdin(vm_id, process_id, &child_process_id, input)?;
5532        }
5533        self.close_javascript_child_process_stdin(vm_id, process_id, &child_process_id)?;
5534
5535        let max_buffer = max_buffer.unwrap_or(1024 * 1024);
5536        let mut stdout = Vec::new();
5537        let mut stderr = Vec::new();
5538        let mut max_buffer_exceeded = false;
5539        let mut kill_sent = false;
5540        let mut timed_out = false;
5541
5542        let exit_code = loop {
5543            let wait_ms = if let Some(deadline) = timeout_deadline {
5544                let now = Instant::now();
5545                if now >= deadline {
5546                    if !kill_sent {
5547                        timed_out = true;
5548                        self.kill_javascript_child_process(
5549                            vm_id,
5550                            process_id,
5551                            &child_process_id,
5552                            &timeout_signal,
5553                        )?;
5554                        kill_sent = true;
5555                    }
5556                    0
5557                } else {
5558                    u64::try_from(deadline.saturating_duration_since(now).as_millis().min(50))
5559                        .unwrap_or(50)
5560                }
5561            } else {
5562                50
5563            };
5564            let event =
5565                self.poll_javascript_child_process(vm_id, process_id, &child_process_id, wait_ms)?;
5566            if event.is_null() {
5567                continue;
5568            }
5569
5570            match event.get("type").and_then(Value::as_str) {
5571                Some("stdout") => {
5572                    let chunk = javascript_sync_rpc_bytes_arg(
5573                        &[event.get("data").cloned().unwrap_or(Value::Null)],
5574                        0,
5575                        "child_process.spawn_sync stdout",
5576                    )?;
5577                    stdout.extend_from_slice(&chunk);
5578                    if stdout.len() > max_buffer && !kill_sent {
5579                        max_buffer_exceeded = true;
5580                        self.kill_javascript_child_process(
5581                            vm_id,
5582                            process_id,
5583                            &child_process_id,
5584                            "SIGTERM",
5585                        )?;
5586                        kill_sent = true;
5587                    }
5588                }
5589                Some("stderr") => {
5590                    let chunk = javascript_sync_rpc_bytes_arg(
5591                        &[event.get("data").cloned().unwrap_or(Value::Null)],
5592                        0,
5593                        "child_process.spawn_sync stderr",
5594                    )?;
5595                    stderr.extend_from_slice(&chunk);
5596                    if stderr.len() > max_buffer && !kill_sent {
5597                        max_buffer_exceeded = true;
5598                        self.kill_javascript_child_process(
5599                            vm_id,
5600                            process_id,
5601                            &child_process_id,
5602                            "SIGTERM",
5603                        )?;
5604                        kill_sent = true;
5605                    }
5606                }
5607                Some("exit") => {
5608                    break event
5609                        .get("exitCode")
5610                        .and_then(Value::as_i64)
5611                        .map(|value| value as i32)
5612                        .unwrap_or(1);
5613                }
5614                _ => {}
5615            }
5616        };
5617
5618        Ok(json!({
5619            "stdout": String::from_utf8_lossy(&stdout),
5620            "stderr": String::from_utf8_lossy(&stderr),
5621            "code": exit_code,
5622            "signal": if timed_out { Value::String(timeout_signal) } else { Value::Null },
5623            "timedOut": timed_out,
5624            "maxBufferExceeded": max_buffer_exceeded,
5625        }))
5626    }
5627
5628    fn spawn_descendant_javascript_child_process(
5629        &mut self,
5630        vm_id: &str,
5631        process_id: &str,
5632        current_process_path: &[&str],
5633        request: JavascriptChildProcessSpawnRequest,
5634    ) -> Result<Value, SidecarError> {
5635        let current_process_label =
5636            Self::child_process_path_label(process_id, current_process_path);
5637        let (resolved, parent_kernel_pid) = {
5638            let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?;
5639            let root = vm
5640                .active_processes
5641                .get(process_id)
5642                .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5643            let parent =
5644                Self::active_process_by_path(root, current_process_path).ok_or_else(|| {
5645                    SidecarError::InvalidState(format!(
5646                        "unknown child process path {current_process_label} during nested spawn"
5647                    ))
5648                })?;
5649            (
5650                self.resolve_javascript_child_process_execution(
5651                    vm,
5652                    &parent.env,
5653                    &parent.guest_cwd,
5654                    &parent.host_cwd,
5655                    &request,
5656                )?,
5657                parent.kernel_pid,
5658            )
5659        };
5660
5661        let sidecar_requests = self.sidecar_requests.clone();
5662        let vm = self
5663            .vms
5664            .get_mut(vm_id)
5665            .ok_or_else(|| missing_vm_error(vm_id))?;
5666        let child_process_id = {
5667            let root = vm
5668                .active_processes
5669                .get_mut(process_id)
5670                .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5671            let parent =
5672                Self::active_process_by_path_mut(root, current_process_path).ok_or_else(|| {
5673                    SidecarError::InvalidState(format!(
5674                        "unknown child process path {current_process_label} during nested spawn"
5675                    ))
5676                })?;
5677            parent.allocate_child_process_id()
5678        };
5679        let mut child_path = current_process_path.to_vec();
5680        child_path.push(child_process_id.as_str());
5681        let (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd) = if resolved
5682            .tool_command
5683        {
5684            let tool_resolution = resolve_tool_command(
5685                vm,
5686                &resolved.command,
5687                &resolved.execution_args,
5688                Some(&resolved.guest_cwd),
5689            )?
5690            .ok_or_else(|| {
5691                SidecarError::InvalidState(format!(
5692                    "tool command no longer resolves: {}",
5693                    resolved.command
5694                ))
5695            })?;
5696            let kernel_handle = vm
5697                .kernel
5698                .create_virtual_process(
5699                    EXECUTION_DRIVER_NAME,
5700                    TOOL_DRIVER_NAME,
5701                    &resolved.command,
5702                    resolved.process_args.clone(),
5703                    VirtualProcessOptions {
5704                        parent_pid: Some(parent_kernel_pid),
5705                        env: resolved.env.clone(),
5706                        cwd: Some(resolved.guest_cwd.clone()),
5707                    },
5708                )
5709                .map_err(kernel_error)?;
5710            let kernel_pid = kernel_handle.pid();
5711            let tool_execution = ToolExecution::default();
5712            let cancelled = tool_execution.cancelled.clone();
5713            let pending_events = tool_execution.pending_events.clone();
5714            let events_overflowed = tool_execution.events_overflowed.clone();
5715            spawn_tool_process_events(ToolProcessEventRequest {
5716                sidecar_requests: sidecar_requests.clone(),
5717                connection_id: vm.connection_id.clone(),
5718                session_id: vm.session_id.clone(),
5719                vm_id: vm_id.to_owned(),
5720                tool_resolution,
5721                cancelled,
5722                pending_events,
5723                events_overflowed,
5724            });
5725            (
5726                kernel_pid,
5727                kernel_handle,
5728                ActiveExecution::Tool(tool_execution),
5729                None,
5730            )
5731        } else {
5732            let kernel_command = match resolved.runtime {
5733                GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND,
5734                GuestRuntimeKind::WebAssembly => WASM_COMMAND,
5735                GuestRuntimeKind::Python => {
5736                    unreachable!("python child_process execution is rejected")
5737                }
5738            };
5739            let kernel_handle = vm
5740                .kernel
5741                .spawn_process(
5742                    kernel_command,
5743                    resolved.process_args.clone(),
5744                    SpawnOptions {
5745                        requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
5746                        parent_pid: Some(parent_kernel_pid),
5747                        env: resolved.env.clone(),
5748                        cwd: Some(resolved.guest_cwd.clone()),
5749                    },
5750                )
5751                .map_err(kernel_error)?;
5752            let kernel_pid = kernel_handle.pid();
5753            if request.options.detached {
5754                vm.kernel
5755                    .setsid(EXECUTION_DRIVER_NAME, kernel_pid)
5756                    .map_err(kernel_error)?;
5757            }
5758            let mut execution_env = resolved.env.clone();
5759            execution_env.insert(
5760                String::from(EXECUTION_SANDBOX_ROOT_ENV),
5761                normalize_host_path(&vm.cwd).to_string_lossy().into_owned(),
5762            );
5763            let execution = match resolved.runtime {
5764                GuestRuntimeKind::JavaScript => {
5765                    execution_env.extend(sanitize_javascript_child_process_internal_bootstrap_env(
5766                        &request.options.internal_bootstrap_env,
5767                    ));
5768                    execution_env.insert(
5769                        String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
5770                        String::from("1"),
5771                    );
5772                    let context =
5773                        self.javascript_engine
5774                            .create_context(CreateJavascriptContextRequest {
5775                                vm_id: vm_id.to_owned(),
5776                                bootstrap_module: None,
5777                                compile_cache_root: Some(
5778                                    self.cache_root.join("node-compile-cache"),
5779                                ),
5780                            });
5781                    let inline_code = load_javascript_entrypoint_source(
5782                        vm,
5783                        &resolved.host_cwd,
5784                        &resolved.entrypoint,
5785                        &execution_env,
5786                    );
5787                    prepare_javascript_shadow(vm, &resolved)?;
5788
5789                    let built_reader = build_module_reader(vm, &resolved);
5790                    let guest_reader = built_reader
5791                        .clone()
5792                        .map(|reader| {
5793                        Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader))
5794                            as Box<dyn GuestModuleReader>
5795                    });
5796                    let module_reader = built_reader
5797                        .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
5798                    let execution = self
5799                        .javascript_engine
5800                        .start_execution_with_module_reader(
5801                            StartJavascriptExecutionRequest {
5802                                guest_runtime: guest_runtime_identity(
5803                                    vm,
5804                                    Some(u64::from(kernel_pid)),
5805                                    Some(u64::from(parent_kernel_pid)),
5806                                ),
5807                                vm_id: vm_id.to_owned(),
5808                                context_id: context.context_id,
5809                                argv: std::iter::once(resolved.entrypoint.clone())
5810                                    .chain(resolved.execution_args.clone())
5811                                    .collect(),
5812                                env: execution_env,
5813                                cwd: resolved.host_cwd.clone(),
5814                                limits: javascript_execution_limits(vm),
5815                                inline_code,
5816                            },
5817                            module_reader,
5818                            guest_reader,
5819                        )
5820                        .map_err(javascript_error)?;
5821                    ActiveExecution::Javascript(execution)
5822                }
5823                GuestRuntimeKind::WebAssembly => {
5824                    execution_env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
5825                    let wasm_limits = wasm_execution_limits(vm);
5826                    let wasm_guest_runtime = guest_runtime_identity(
5827                        vm,
5828                        Some(u64::from(kernel_pid)),
5829                        Some(u64::from(parent_kernel_pid)),
5830                    );
5831                    let context = self.wasm_engine.create_context(CreateWasmContextRequest {
5832                        vm_id: vm_id.to_owned(),
5833                        module_path: Some(resolved.entrypoint.clone()),
5834                    });
5835                    let execution = self
5836                        .wasm_engine
5837                        .start_execution(StartWasmExecutionRequest {
5838                            vm_id: vm_id.to_owned(),
5839                            context_id: context.context_id,
5840                            argv: resolved.process_args.clone(),
5841                            env: execution_env,
5842                            cwd: resolved.host_cwd.clone(),
5843                            permission_tier: execution_wasm_permission_tier(
5844                                resolved
5845                                    .wasm_permission_tier
5846                                    .unwrap_or(WasmPermissionTier::Full),
5847                            ),
5848                            limits: wasm_limits,
5849                            guest_runtime: wasm_guest_runtime,
5850                        })
5851                        .map_err(wasm_error)?;
5852                    ActiveExecution::Wasm(Box::new(execution))
5853                }
5854                GuestRuntimeKind::Python => {
5855                    unreachable!("python child_process execution is rejected")
5856                }
5857            };
5858            let kernel_stdin_writer_fd = match javascript_child_process_stdin_mode(&request) {
5859                "pipe" => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5860                "ignore" => {
5861                    vm.kernel
5862                        .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, 0)
5863                        .map_err(kernel_error)?;
5864                    None
5865                }
5866                "inherit" => None,
5867                _ => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5868            };
5869            (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd)
5870        };
5871
5872        let root = vm
5873            .active_processes
5874            .get_mut(process_id)
5875            .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5876        let parent =
5877            Self::active_process_by_path_mut(root, current_process_path).ok_or_else(|| {
5878                SidecarError::InvalidState(format!(
5879                    "unknown child process path {current_process_label} during nested spawn"
5880                ))
5881            })?;
5882        parent.child_processes.insert(
5883            child_process_id.clone(),
5884            ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
5885                .with_detached(request.options.detached)
5886                .with_guest_cwd(resolved.guest_cwd.clone())
5887                .with_env(resolved.env.clone())
5888                .with_host_cwd(resolved.host_cwd.clone()),
5889        );
5890        if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd {
5891            parent
5892                .child_processes
5893                .get_mut(&child_process_id)
5894                .ok_or_else(|| {
5895                    SidecarError::InvalidState(format!(
5896                        "child process {child_process_id} disappeared during nested spawn"
5897                    ))
5898                })?
5899                .kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd);
5900        }
5901        Ok(json!({
5902            "childId": child_process_id,
5903            "pid": kernel_pid,
5904            "command": resolved.command,
5905            "args": resolved.process_args,
5906        }))
5907    }
5908
5909    fn spawn_descendant_javascript_child_process_sync(
5910        &mut self,
5911        vm_id: &str,
5912        process_id: &str,
5913        current_process_path: &[&str],
5914        request: JavascriptChildProcessSpawnRequest,
5915        max_buffer: Option<usize>,
5916    ) -> Result<Value, SidecarError> {
5917        let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?;
5918        let timeout_deadline = request
5919            .options
5920            .timeout
5921            .map(|timeout_ms| Instant::now() + Duration::from_millis(timeout_ms));
5922        let timeout_signal = request
5923            .options
5924            .kill_signal
5925            .clone()
5926            .unwrap_or_else(|| String::from("SIGTERM"));
5927        let spawned = self.spawn_descendant_javascript_child_process(
5928            vm_id,
5929            process_id,
5930            current_process_path,
5931            request,
5932        )?;
5933        let child_process_id = spawned
5934            .get("childId")
5935            .and_then(Value::as_str)
5936            .ok_or_else(|| {
5937                SidecarError::InvalidState(String::from(
5938                    "child_process.spawn_sync response is missing childId",
5939                ))
5940            })?
5941            .to_owned();
5942
5943        if let Some(input) = sync_input.as_deref() {
5944            self.write_descendant_javascript_child_process_stdin(
5945                vm_id,
5946                process_id,
5947                current_process_path,
5948                &child_process_id,
5949                input,
5950            )?;
5951        }
5952        self.close_descendant_javascript_child_process_stdin(
5953            vm_id,
5954            process_id,
5955            current_process_path,
5956            &child_process_id,
5957        )?;
5958
5959        let max_buffer = max_buffer.unwrap_or(1024 * 1024);
5960        let mut stdout = Vec::new();
5961        let mut stderr = Vec::new();
5962        let mut max_buffer_exceeded = false;
5963        let mut kill_sent = false;
5964        let mut timed_out = false;
5965
5966        let exit_code = loop {
5967            let wait_ms = if let Some(deadline) = timeout_deadline {
5968                let now = Instant::now();
5969                if now >= deadline {
5970                    if !kill_sent {
5971                        timed_out = true;
5972                        self.kill_descendant_javascript_child_process(
5973                            vm_id,
5974                            process_id,
5975                            current_process_path,
5976                            &child_process_id,
5977                            &timeout_signal,
5978                        )?;
5979                        kill_sent = true;
5980                    }
5981                    0
5982                } else {
5983                    u64::try_from(deadline.saturating_duration_since(now).as_millis().min(50))
5984                        .unwrap_or(50)
5985                }
5986            } else {
5987                50
5988            };
5989            let event = self.poll_descendant_javascript_child_process(
5990                vm_id,
5991                process_id,
5992                current_process_path,
5993                &child_process_id,
5994                wait_ms,
5995            )?;
5996            if event.is_null() {
5997                continue;
5998            }
5999
6000            match event.get("type").and_then(Value::as_str) {
6001                Some("stdout") => {
6002                    let chunk = javascript_sync_rpc_bytes_arg(
6003                        &[event.get("data").cloned().unwrap_or(Value::Null)],
6004                        0,
6005                        "child_process.spawn_sync stdout",
6006                    )?;
6007                    stdout.extend_from_slice(&chunk);
6008                    if stdout.len() > max_buffer && !kill_sent {
6009                        max_buffer_exceeded = true;
6010                        self.kill_descendant_javascript_child_process(
6011                            vm_id,
6012                            process_id,
6013                            current_process_path,
6014                            &child_process_id,
6015                            "SIGTERM",
6016                        )?;
6017                        kill_sent = true;
6018                    }
6019                }
6020                Some("stderr") => {
6021                    let chunk = javascript_sync_rpc_bytes_arg(
6022                        &[event.get("data").cloned().unwrap_or(Value::Null)],
6023                        0,
6024                        "child_process.spawn_sync stderr",
6025                    )?;
6026                    stderr.extend_from_slice(&chunk);
6027                    if stderr.len() > max_buffer && !kill_sent {
6028                        max_buffer_exceeded = true;
6029                        self.kill_descendant_javascript_child_process(
6030                            vm_id,
6031                            process_id,
6032                            current_process_path,
6033                            &child_process_id,
6034                            "SIGTERM",
6035                        )?;
6036                        kill_sent = true;
6037                    }
6038                }
6039                Some("exit") => {
6040                    break event
6041                        .get("exitCode")
6042                        .and_then(Value::as_i64)
6043                        .map(|value| value as i32)
6044                        .unwrap_or(1);
6045                }
6046                _ => {}
6047            }
6048        };
6049
6050        Ok(json!({
6051            "stdout": String::from_utf8_lossy(&stdout),
6052            "stderr": String::from_utf8_lossy(&stderr),
6053            "code": exit_code,
6054            "signal": if timed_out { Value::String(timeout_signal) } else { Value::Null },
6055            "timedOut": timed_out,
6056            "maxBufferExceeded": max_buffer_exceeded,
6057        }))
6058    }
6059
6060    fn handle_descendant_javascript_child_process_rpc(
6061        &mut self,
6062        vm_id: &str,
6063        process_id: &str,
6064        current_process_path: &[&str],
6065        request: &JavascriptSyncRpcRequest,
6066    ) -> Result<Value, SidecarError> {
6067        match request.method.as_str() {
6068            "child_process.spawn" => {
6069                let Some(vm) = self.vms.get(vm_id) else {
6070                    return Ok(Value::Null);
6071                };
6072                let (payload, _) = parse_javascript_child_process_spawn_request(vm, &request.args)?;
6073                self.spawn_descendant_javascript_child_process(
6074                    vm_id,
6075                    process_id,
6076                    current_process_path,
6077                    payload,
6078                )
6079            }
6080            "child_process.spawn_sync" => {
6081                let Some(vm) = self.vms.get(vm_id) else {
6082                    return Ok(Value::Null);
6083                };
6084                let (payload, max_buffer) =
6085                    parse_javascript_child_process_spawn_request(vm, &request.args)?;
6086                self.spawn_descendant_javascript_child_process_sync(
6087                    vm_id,
6088                    process_id,
6089                    current_process_path,
6090                    payload,
6091                    max_buffer,
6092                )
6093            }
6094            "child_process.poll" => {
6095                let child_process_id =
6096                    javascript_sync_rpc_arg_str(&request.args, 0, "child_process.poll child id")?;
6097                let wait_ms = javascript_sync_rpc_arg_u64_optional(
6098                    &request.args,
6099                    1,
6100                    "child_process.poll wait ms",
6101                )?
6102                .unwrap_or_default();
6103                self.poll_descendant_javascript_child_process(
6104                    vm_id,
6105                    process_id,
6106                    current_process_path,
6107                    child_process_id,
6108                    wait_ms,
6109                )
6110            }
6111            "child_process.write_stdin" => {
6112                let child_process_id = javascript_sync_rpc_arg_str(
6113                    &request.args,
6114                    0,
6115                    "child_process.write_stdin child id",
6116                )?;
6117                let chunk = javascript_sync_rpc_bytes_arg(
6118                    &request.args,
6119                    1,
6120                    "child_process.write_stdin chunk",
6121                )?;
6122                self.write_descendant_javascript_child_process_stdin(
6123                    vm_id,
6124                    process_id,
6125                    current_process_path,
6126                    child_process_id,
6127                    &chunk,
6128                )?;
6129                Ok(Value::Null)
6130            }
6131            "child_process.close_stdin" => {
6132                let child_process_id = javascript_sync_rpc_arg_str(
6133                    &request.args,
6134                    0,
6135                    "child_process.close_stdin child id",
6136                )?;
6137                self.close_descendant_javascript_child_process_stdin(
6138                    vm_id,
6139                    process_id,
6140                    current_process_path,
6141                    child_process_id,
6142                )?;
6143                Ok(Value::Null)
6144            }
6145            "child_process.kill" => {
6146                let child_process_id =
6147                    javascript_sync_rpc_arg_str(&request.args, 0, "child_process.kill child id")?;
6148                let signal =
6149                    javascript_sync_rpc_arg_str(&request.args, 1, "child_process.kill signal")?;
6150                self.kill_descendant_javascript_child_process(
6151                    vm_id,
6152                    process_id,
6153                    current_process_path,
6154                    child_process_id,
6155                    signal,
6156                )?;
6157                Ok(Value::Null)
6158            }
6159            _ => Err(SidecarError::InvalidState(format!(
6160                "unsupported nested child process RPC method {}",
6161                request.method
6162            ))),
6163        }
6164    }
6165
6166    fn poll_descendant_javascript_child_process(
6167        &mut self,
6168        vm_id: &str,
6169        process_id: &str,
6170        current_process_path: &[&str],
6171        child_process_id: &str,
6172        wait_ms: u64,
6173    ) -> Result<Value, SidecarError> {
6174        let mut child_path = current_process_path.to_vec();
6175        child_path.push(child_process_id);
6176        let child_gone_error = || javascript_child_process_gone_error(process_id, &child_path);
6177        let deadline = Instant::now() + Duration::from_millis(wait_ms);
6178        let mut polled_once = false;
6179
6180        loop {
6181            self.drain_queued_descendant_javascript_child_process_events(
6182                vm_id,
6183                process_id,
6184                &child_path,
6185            )?;
6186            enum ChildPollResult {
6187                Event(Box<Option<ActiveExecutionEvent>>),
6188                RecoverRuntimeExit,
6189                Timeout,
6190            }
6191            let wait = if wait_ms == 0 {
6192                Duration::ZERO
6193            } else {
6194                deadline.saturating_duration_since(Instant::now())
6195            };
6196            let poll_result = {
6197                let Some(vm) = self.vms.get_mut(vm_id) else {
6198                    return Ok(Value::Null);
6199                };
6200                let Some(parent) =
6201                    Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6202                else {
6203                    return Err(child_gone_error());
6204                };
6205                let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6206                    return Err(child_gone_error());
6207                };
6208                if let Some(event) = child.pending_execution_events.pop_front() {
6209                    ChildPollResult::Event(Box::new(Some(event)))
6210                } else if polled_once && wait.is_zero() {
6211                    ChildPollResult::Timeout
6212                } else {
6213                    polled_once = true;
6214                    match child.execution.poll_event_blocking(wait) {
6215                        Ok(Some(event)) => ChildPollResult::Event(Box::new(Some(event))),
6216                        Ok(None) => ChildPollResult::RecoverRuntimeExit,
6217                        Err(SidecarError::Execution(message))
6218                            if (child.runtime == GuestRuntimeKind::JavaScript
6219                                && closed_javascript_event_channel(&message))
6220                                || (child.runtime == GuestRuntimeKind::Python
6221                                    && closed_python_event_channel(&message))
6222                                || (child.runtime == GuestRuntimeKind::WebAssembly
6223                                    && closed_wasm_event_channel(&message)) =>
6224                        {
6225                            ChildPollResult::RecoverRuntimeExit
6226                        }
6227                        Err(error) => return Err(error),
6228                    }
6229                }
6230            };
6231            let event = match poll_result {
6232                ChildPollResult::Event(event) => *event,
6233                ChildPollResult::Timeout => return Ok(Value::Null),
6234                ChildPollResult::RecoverRuntimeExit => self
6235                    .recover_descendant_runtime_child_process_event(
6236                        vm_id,
6237                        process_id,
6238                        current_process_path,
6239                        child_process_id,
6240                        wait.as_millis().try_into().unwrap_or(u64::MAX),
6241                    )?,
6242            };
6243
6244            let Some(event) = event else {
6245                return Ok(Value::Null);
6246            };
6247
6248            match event {
6249                ActiveExecutionEvent::Stdout(chunk) => {
6250                    return Ok(json!({
6251                        "type": "stdout",
6252                        "data": javascript_sync_rpc_bytes_value(&chunk),
6253                    }));
6254                }
6255                ActiveExecutionEvent::Stderr(chunk) => {
6256                    return Ok(json!({
6257                        "type": "stderr",
6258                        "data": javascript_sync_rpc_bytes_value(&chunk),
6259                    }));
6260                }
6261                ActiveExecutionEvent::Exited(exit_code) => {
6262                    let had_trailing_events = {
6263                        let Some(vm) = self.vms.get_mut(vm_id) else {
6264                            return Ok(Value::Null);
6265                        };
6266                        let Some(parent) = Self::descendant_parent_process_mut(
6267                            vm,
6268                            process_id,
6269                            current_process_path,
6270                        ) else {
6271                            return Ok(Value::Null);
6272                        };
6273                        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6274                            return Ok(Value::Null);
6275                        };
6276                        let deadline = Instant::now() + Duration::from_millis(150);
6277                        loop {
6278                            let wait = deadline.saturating_duration_since(Instant::now());
6279                            let next = poll_child_execution_after_exit(child, wait)?;
6280                            let Some(next) = next else {
6281                                break;
6282                            };
6283                            if matches!(next, ActiveExecutionEvent::Exited(_)) {
6284                                continue;
6285                            }
6286                            child.queue_pending_execution_event(next)?;
6287                            if Instant::now() >= deadline {
6288                                break;
6289                            }
6290                        }
6291                        if !child.pending_execution_events.is_empty() {
6292                            child.queue_pending_execution_event(ActiveExecutionEvent::Exited(
6293                                exit_code,
6294                            ))?;
6295                            true
6296                        } else {
6297                            false
6298                        }
6299                    };
6300                    if had_trailing_events {
6301                        continue;
6302                    }
6303
6304                    let parent_signal_key =
6305                        Self::child_process_signal_key(process_id, current_process_path);
6306                    let Some(vm) = self.vms.get_mut(vm_id) else {
6307                        return Ok(Value::Null);
6308                    };
6309                    let signal_name = {
6310                        let Some(parent) = Self::descendant_parent_process_mut(
6311                            vm,
6312                            process_id,
6313                            current_process_path,
6314                        ) else {
6315                            return Ok(Value::Null);
6316                        };
6317                        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6318                            return Ok(Value::Null);
6319                        };
6320                        child.pending_self_signal_exit.take().and_then(|signal| {
6321                            if exit_code == 128 + signal {
6322                                canonical_signal_name(signal).map(str::to_owned)
6323                            } else {
6324                                None
6325                            }
6326                        })
6327                    };
6328                    let (parent_runtime_pid, parent_v8_signal_session, should_signal_parent) = {
6329                        let Some(parent) =
6330                            Self::descendant_parent_process(vm, process_id, current_process_path)
6331                        else {
6332                            return Ok(Value::Null);
6333                        };
6334                        (
6335                            parent.execution.child_pid(),
6336                            parent.execution.javascript_v8_session_handle().filter(|_| {
6337                                matches!(
6338                                    &parent.execution,
6339                                    ActiveExecution::Javascript(execution)
6340                                        if execution.uses_shared_v8_runtime()
6341                                )
6342                            }),
6343                            vm.signal_states
6344                                .get(parent_signal_key)
6345                                .and_then(|handlers| handlers.get(&(libc::SIGCHLD as u32)))
6346                                .is_some_and(|registration| {
6347                                    registration.action != SignalDispositionAction::Default
6348                                }),
6349                        )
6350                    };
6351                    let Some(parent) =
6352                        Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6353                    else {
6354                        return Ok(Value::Null);
6355                    };
6356                    let Some(mut child) = parent.child_processes.remove(child_process_id) else {
6357                        return Ok(Value::Null);
6358                    };
6359                    let child_process_label =
6360                        Self::child_process_path_label(process_id, &child_path);
6361                    let detached_children =
6362                        Self::adopt_detached_child_processes(&child_process_label, &mut child);
6363                    sync_process_host_writes_to_kernel(vm, &child)?;
6364                    terminate_child_process_tree(&mut vm.kernel, &mut child);
6365                    child.kernel_handle.finish(exit_code);
6366                    let _ = vm.kernel.wait_and_reap(child.kernel_pid);
6367                    vm.signal_states.remove(child_process_id);
6368                    for (detached_process_id, detached_child) in detached_children {
6369                        vm.detached_child_processes
6370                            .insert(detached_process_id.clone());
6371                        vm.active_processes
6372                            .insert(detached_process_id, detached_child);
6373                    }
6374                    if should_signal_parent {
6375                        if let Some(session) = parent_v8_signal_session {
6376                            dispatch_v8_session_signal_async(session, libc::SIGCHLD);
6377                        } else {
6378                            signal_runtime_process(parent_runtime_pid, libc::SIGCHLD)?;
6379                        }
6380                    }
6381                    let mut payload = Map::new();
6382                    payload.insert(String::from("type"), Value::String(String::from("exit")));
6383                    payload.insert(String::from("exitCode"), Value::from(exit_code));
6384                    if let Some(signal_name) = signal_name {
6385                        payload.insert(String::from("signal"), Value::String(signal_name));
6386                    }
6387                    return Ok(Value::Object(payload));
6388                }
6389                ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
6390                    let mut current_child_path = current_process_path.to_vec();
6391                    current_child_path.push(child_process_id);
6392                    let response = if request.method == "process.signal_state" {
6393                        let (signal, registration) =
6394                            parse_process_signal_state_request(&request.args)?;
6395                        let Some(vm) = self.vms.get_mut(vm_id) else {
6396                            return Ok(Value::Null);
6397                        };
6398                        let signal_key =
6399                            Self::child_process_signal_key(process_id, &current_child_path)
6400                                .to_owned();
6401                        apply_process_signal_state_update(
6402                            &mut vm.signal_states,
6403                            &signal_key,
6404                            signal,
6405                            registration,
6406                        );
6407                        Ok(Value::Null)
6408                    } else if request.method == "process.kill" {
6409                        self.handle_descendant_process_kill_rpc(
6410                            vm_id,
6411                            process_id,
6412                            current_process_path,
6413                            child_process_id,
6414                            &request,
6415                        )
6416                    } else if request.method.starts_with("child_process.") {
6417                        self.handle_descendant_javascript_child_process_rpc(
6418                            vm_id,
6419                            process_id,
6420                            &current_child_path,
6421                            &request,
6422                        )
6423                    } else {
6424                        let Some(vm) = self.vms.get_mut(vm_id) else {
6425                            return Ok(Value::Null);
6426                        };
6427                        let resource_limits = vm.kernel.resource_limits().clone();
6428                        let network_counts = vm_network_resource_counts(vm);
6429                        let socket_paths = build_javascript_socket_path_context(vm)?;
6430                        let Some(root) = vm.active_processes.get_mut(process_id) else {
6431                            return Ok(Value::Null);
6432                        };
6433                        let Some(parent) =
6434                            Self::active_process_by_path_mut(root, current_process_path)
6435                        else {
6436                            return Ok(Value::Null);
6437                        };
6438                        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6439                            return Ok(Value::Null);
6440                        };
6441                        service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
6442                            bridge: &self.bridge,
6443                            vm_id,
6444                            dns: &vm.dns,
6445                            socket_paths: &socket_paths,
6446                            kernel: &mut vm.kernel,
6447                            process: child,
6448                            sync_request: &request,
6449                            resource_limits: &resource_limits,
6450                            network_counts,
6451                        })
6452                    };
6453
6454                    let Some(vm) = self.vms.get_mut(vm_id) else {
6455                        return Ok(Value::Null);
6456                    };
6457                    let Some(parent) =
6458                        Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6459                    else {
6460                        return Ok(Value::Null);
6461                    };
6462                    let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6463                        return Ok(Value::Null);
6464                    };
6465                    let parent_signal_event = response.as_ref().ok().and_then(|result| {
6466                        let target_path_label =
6467                            Self::child_process_path_label(process_id, current_process_path);
6468                        if request.method != "process.kill"
6469                            || result.get("action").and_then(Value::as_str) != Some("user")
6470                            || result.get("targetProcessPath").and_then(Value::as_str)
6471                                != Some(target_path_label.as_str())
6472                        {
6473                            return None;
6474                        }
6475                        Some(json!({
6476                            "type": "signal",
6477                            "signal": result.get("signal").and_then(Value::as_str).unwrap_or_default(),
6478                            "number": result.get("number").and_then(Value::as_i64).unwrap_or_default(),
6479                        }))
6480                    });
6481                    match response {
6482                        Ok(result) => child
6483                            .execution
6484                            .respond_javascript_sync_rpc_success(request.id, result)
6485                            .or_else(ignore_stale_javascript_sync_rpc_response)?,
6486                        Err(error) => child
6487                            .execution
6488                            .respond_javascript_sync_rpc_error(
6489                                request.id,
6490                                javascript_sync_rpc_error_code(&error),
6491                                error.to_string(),
6492                            )
6493                            .or_else(ignore_stale_javascript_sync_rpc_response)?,
6494                    }
6495                    if let Some(event) = parent_signal_event {
6496                        return Ok(event);
6497                    }
6498                }
6499                ActiveExecutionEvent::PythonVfsRpcRequest(_) => {
6500                    return Err(SidecarError::InvalidState(String::from(
6501                        "nested Python child_process execution is not supported yet",
6502                    )));
6503                }
6504                ActiveExecutionEvent::SignalState {
6505                    signal,
6506                    registration,
6507                } => {
6508                    let Some(vm) = self.vms.get_mut(vm_id) else {
6509                        return Ok(Value::Null);
6510                    };
6511                    let signal_key =
6512                        Self::child_process_signal_key(process_id, &child_path).to_owned();
6513                    apply_process_signal_state_update(
6514                        &mut vm.signal_states,
6515                        &signal_key,
6516                        signal,
6517                        registration.clone(),
6518                    );
6519                    return Ok(json!({
6520                        "type": "signal_state",
6521                        "signal": signal,
6522                        "registration": registration,
6523                    }));
6524                }
6525            }
6526        }
6527    }
6528
6529    fn recover_descendant_runtime_child_process_event(
6530        &mut self,
6531        vm_id: &str,
6532        process_id: &str,
6533        current_process_path: &[&str],
6534        child_process_id: &str,
6535        wait_ms: u64,
6536    ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
6537        let (
6538            parent_kernel_pid,
6539            child_kernel_pid,
6540            child_runtime_pid,
6541            child_runtime,
6542            child_shared_runtime,
6543        ) = {
6544            let mut child_path = current_process_path.to_vec();
6545            child_path.push(child_process_id);
6546            let Some(vm) = self.vms.get_mut(vm_id) else {
6547                return Ok(None);
6548            };
6549            let Some(parent) =
6550                Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6551            else {
6552                return Err(javascript_child_process_gone_error(process_id, &child_path));
6553            };
6554            let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6555                return Err(javascript_child_process_gone_error(process_id, &child_path));
6556            };
6557            (
6558                parent.kernel_pid,
6559                child.kernel_pid,
6560                child.execution.child_pid(),
6561                child.runtime.clone(),
6562                child.execution.uses_shared_v8_runtime(),
6563            )
6564        };
6565        if child_runtime != GuestRuntimeKind::JavaScript
6566            && child_runtime != GuestRuntimeKind::Python
6567            && child_runtime != GuestRuntimeKind::WebAssembly
6568        {
6569            return Ok(None);
6570        }
6571        let wait_deadline = Instant::now() + Duration::from_millis(wait_ms.min(25));
6572        loop {
6573            let Some(vm) = self.vms.get_mut(vm_id) else {
6574                return Ok(None);
6575            };
6576            if let Some(process_info) = vm.kernel.list_processes().get(&child_kernel_pid) {
6577                if process_info.status == ProcessStatus::Exited {
6578                    return Ok(Some(ActiveExecutionEvent::Exited(
6579                        process_info.exit_code.unwrap_or(0),
6580                    )));
6581                }
6582            }
6583            if let Some(wait_result) = vm
6584                .kernel
6585                .waitpid_with_options(
6586                    EXECUTION_DRIVER_NAME,
6587                    parent_kernel_pid,
6588                    child_kernel_pid as i32,
6589                    WaitPidFlags::WNOHANG,
6590                )
6591                .map_err(kernel_error)?
6592            {
6593                return Ok(Some(ActiveExecutionEvent::Exited(wait_result.status)));
6594            }
6595
6596            if !child_shared_runtime && child_runtime_pid != 0 {
6597                if let Some(status) = runtime_child_exit_status(child_runtime_pid)? {
6598                    return Ok(Some(ActiveExecutionEvent::Exited(status)));
6599                }
6600                if !runtime_child_is_alive(child_runtime_pid)? {
6601                    return Ok(Some(ActiveExecutionEvent::Exited(0)));
6602                }
6603            }
6604            if Instant::now() >= wait_deadline {
6605                return Ok(None);
6606            }
6607            std::thread::sleep(Duration::from_millis(5));
6608        }
6609    }
6610
6611    fn write_descendant_javascript_child_process_stdin(
6612        &mut self,
6613        vm_id: &str,
6614        process_id: &str,
6615        current_process_path: &[&str],
6616        child_process_id: &str,
6617        chunk: &[u8],
6618    ) -> Result<(), SidecarError> {
6619        let mut child_path = current_process_path.to_vec();
6620        child_path.push(child_process_id);
6621        let Some(vm) = self.vms.get_mut(vm_id) else {
6622            return Err(javascript_child_process_gone_error(process_id, &child_path));
6623        };
6624        let Some(root) = vm.active_processes.get_mut(process_id) else {
6625            return Err(javascript_child_process_gone_error(process_id, &child_path));
6626        };
6627        let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
6628            return Err(javascript_child_process_gone_error(process_id, &child_path));
6629        };
6630        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6631            return Err(javascript_child_process_gone_error(process_id, &child_path));
6632        };
6633        if let Err(error) = child.execution.write_stdin(chunk) {
6634            if is_broken_pipe_error(&error) {
6635                return Ok(());
6636            }
6637            return Err(error);
6638        }
6639        write_kernel_process_stdin(&mut vm.kernel, child, chunk)
6640    }
6641
6642    fn close_descendant_javascript_child_process_stdin(
6643        &mut self,
6644        vm_id: &str,
6645        process_id: &str,
6646        current_process_path: &[&str],
6647        child_process_id: &str,
6648    ) -> Result<(), SidecarError> {
6649        let mut child_path = current_process_path.to_vec();
6650        child_path.push(child_process_id);
6651        let Some(vm) = self.vms.get_mut(vm_id) else {
6652            return Err(javascript_child_process_gone_error(process_id, &child_path));
6653        };
6654        let Some(root) = vm.active_processes.get_mut(process_id) else {
6655            return Err(javascript_child_process_gone_error(process_id, &child_path));
6656        };
6657        let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
6658            return Err(javascript_child_process_gone_error(process_id, &child_path));
6659        };
6660        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6661            return Err(javascript_child_process_gone_error(process_id, &child_path));
6662        };
6663        child.execution.close_stdin()?;
6664        close_kernel_process_stdin(&mut vm.kernel, child)
6665    }
6666
6667    fn kill_descendant_javascript_child_process(
6668        &mut self,
6669        vm_id: &str,
6670        process_id: &str,
6671        current_process_path: &[&str],
6672        child_process_id: &str,
6673        signal: &str,
6674    ) -> Result<(), SidecarError> {
6675        let signal_name = signal.to_owned();
6676        let signal = parse_signal(signal)?;
6677        let Some(vm) = self.vms.get_mut(vm_id) else {
6678            return Ok(());
6679        };
6680        let Some(root) = vm.active_processes.get_mut(process_id) else {
6681            return Ok(());
6682        };
6683        let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
6684            return Ok(());
6685        };
6686        let source_pid = parent.kernel_pid;
6687        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6688            return Ok(());
6689        };
6690        terminate_tracked_child_process_for_signal(&mut vm.kernel, child, signal)?;
6691        let child_process_label = if current_process_path.is_empty() {
6692            child_process_id.to_owned()
6693        } else {
6694            format!("{}/{}", current_process_path.join("/"), child_process_id)
6695        };
6696        emit_security_audit_event(
6697            &self.bridge,
6698            vm_id,
6699            "security.process.kill",
6700            audit_fields([
6701                (String::from("source"), String::from("guest_child_process")),
6702                (String::from("source_pid"), source_pid.to_string()),
6703                (String::from("target_pid"), child.kernel_pid.to_string()),
6704                (String::from("process_id"), process_id.to_owned()),
6705                (String::from("child_process_id"), child_process_label),
6706                (String::from("signal"), signal_name),
6707            ]),
6708        );
6709        Ok(())
6710    }
6711
6712    fn handle_descendant_process_kill_rpc(
6713        &mut self,
6714        vm_id: &str,
6715        process_id: &str,
6716        current_process_path: &[&str],
6717        child_process_id: &str,
6718        request: &JavascriptSyncRpcRequest,
6719    ) -> Result<Value, SidecarError> {
6720        let target_pid = javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?;
6721        let signal_name = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?;
6722        let signal = parse_signal(signal_name)?;
6723
6724        let mut source_path = current_process_path.to_vec();
6725        source_path.push(child_process_id);
6726
6727        if signal != 0 && target_pid < 0 {
6728            let pgid = target_pid.unsigned_abs();
6729            let caller_kernel_pid = {
6730                let Some(vm) = self.vms.get(vm_id) else {
6731                    return Err(SidecarError::InvalidState(String::from(
6732                        "ESRCH: unknown VM during process.kill",
6733                    )));
6734                };
6735                let Some(root) = vm.active_processes.get(process_id) else {
6736                    return Err(SidecarError::InvalidState(format!(
6737                        "ESRCH: unknown process {process_id} during process.kill",
6738                    )));
6739                };
6740                let Some(source) = Self::active_process_by_path(root, &source_path) else {
6741                    return Err(SidecarError::InvalidState(format!(
6742                        "ESRCH: unknown child process {child_process_id} during process.kill",
6743                    )));
6744                };
6745                source.kernel_pid
6746            };
6747            let caller_is_member =
6748                self.signal_vm_process_group(vm_id, caller_kernel_pid, pgid, signal_name)?;
6749            if !caller_is_member {
6750                return Ok(Value::Null);
6751            }
6752            let Some(vm) = self.vms.get_mut(vm_id) else {
6753                return Ok(Value::Null);
6754            };
6755            let Some(root) = vm.active_processes.get_mut(process_id) else {
6756                return Ok(Value::Null);
6757            };
6758            let Some(source) = Self::active_process_by_path_mut(root, &source_path) else {
6759                return Ok(Value::Null);
6760            };
6761            source.pending_self_signal_exit = None;
6762            if !matches!(
6763                canonical_signal_name(signal),
6764                Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
6765            ) {
6766                source.pending_self_signal_exit = Some(signal);
6767            }
6768            return Ok(json!({
6769                "self": true,
6770                "action": "default",
6771            }));
6772        }
6773
6774        let Some(vm) = self.vms.get_mut(vm_id) else {
6775            return Err(SidecarError::InvalidState(String::from(
6776                "ESRCH: unknown VM during process.kill",
6777            )));
6778        };
6779
6780        if signal == 0 {
6781            vm.kernel
6782                .signal_process(EXECUTION_DRIVER_NAME, target_pid, signal)
6783                .map_err(kernel_error)?;
6784            return Ok(Value::Null);
6785        }
6786
6787        let target_kernel_pid = u32::try_from(target_pid).map_err(|_| {
6788            SidecarError::InvalidState(format!("EINVAL: invalid process pid {target_pid}"))
6789        })?;
6790        let (source_pid, located_target_path) = {
6791            let Some(root) = vm.active_processes.get(process_id) else {
6792                return Err(SidecarError::InvalidState(format!(
6793                    "ESRCH: unknown process {process_id} during process.kill",
6794                )));
6795            };
6796            let Some(source) = Self::active_process_by_path(root, &source_path) else {
6797                return Err(SidecarError::InvalidState(format!(
6798                    "ESRCH: unknown child process {child_process_id} during process.kill",
6799                )));
6800            };
6801            vm.kernel
6802                .signal_process(EXECUTION_DRIVER_NAME, target_pid, 0)
6803                .map_err(kernel_error)?;
6804            (
6805                source.kernel_pid,
6806                Self::active_process_path_by_kernel_pid(root, target_kernel_pid),
6807            )
6808        };
6809        let Some(target_path) = located_target_path else {
6810            // The target is alive but not part of this root's process tree.
6811            // Resolve it VM-wide so cross-tree pids and untracked kernel
6812            // processes still receive the signal.
6813            self.signal_vm_kernel_pid(vm_id, target_kernel_pid, signal_name)?;
6814            return Ok(Value::Null);
6815        };
6816        let Some(vm) = self.vms.get_mut(vm_id) else {
6817            return Err(SidecarError::InvalidState(String::from(
6818                "ESRCH: unknown VM during process.kill",
6819            )));
6820        };
6821
6822        if source_pid == target_kernel_pid {
6823            let Some(root) = vm.active_processes.get_mut(process_id) else {
6824                return Ok(Value::Null);
6825            };
6826            let Some(source) = Self::active_process_by_path_mut(root, &source_path) else {
6827                return Ok(Value::Null);
6828            };
6829            source.pending_self_signal_exit = None;
6830            if !matches!(
6831                canonical_signal_name(signal),
6832                Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
6833            ) {
6834                source.pending_self_signal_exit = Some(signal);
6835            }
6836            return Ok(json!({
6837                "self": true,
6838                "action": "default",
6839            }));
6840        }
6841
6842        let signal_key = target_path.last().map(String::as_str).unwrap_or(process_id);
6843        let registration = vm
6844            .signal_states
6845            .get(signal_key)
6846            .and_then(|handlers| handlers.get(&(signal as u32)))
6847            .cloned();
6848
6849        let action = match registration
6850            .as_ref()
6851            .map(|registration| &registration.action)
6852        {
6853            Some(SignalDispositionAction::Ignore) => "ignore",
6854            Some(SignalDispositionAction::User) => {
6855                let Some(root) = vm.active_processes.get_mut(process_id) else {
6856                    return Ok(Value::Null);
6857                };
6858                let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path)
6859                else {
6860                    return Err(SidecarError::InvalidState(format!(
6861                        "ESRCH: unknown process pid {target_pid}"
6862                    )));
6863                };
6864                if let Some(session) = target.execution.javascript_v8_session_handle().filter(
6865                    |_| matches!(&target.execution, ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime())
6866                        || matches!(&target.execution, ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime()),
6867                ) {
6868                    dispatch_v8_session_signal_async(session, signal);
6869                } else if !dispatch_v8_process_signal(target, signal)? {
6870                    return Err(SidecarError::InvalidState(format!(
6871                        "unsupported guest signal delivery for pid {target_pid}"
6872                    )));
6873                }
6874                "user"
6875            }
6876            Some(SignalDispositionAction::Default) | None
6877                if matches!(
6878                    canonical_signal_name(signal),
6879                    Some("SIGWINCH" | "SIGCHLD" | "SIGURG")
6880                ) =>
6881            {
6882                "ignore"
6883            }
6884            Some(SignalDispositionAction::Default) | None => {
6885                let Some(root) = vm.active_processes.get_mut(process_id) else {
6886                    return Ok(Value::Null);
6887                };
6888                let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path)
6889                else {
6890                    return Err(SidecarError::InvalidState(format!(
6891                        "ESRCH: unknown process pid {target_pid}"
6892                    )));
6893                };
6894                apply_active_process_default_signal(&mut vm.kernel, target, signal)?;
6895                "default"
6896            }
6897        };
6898
6899        let target_path_label = Self::child_process_path_label(
6900            process_id,
6901            &target_path.iter().map(String::as_str).collect::<Vec<_>>(),
6902        );
6903        emit_security_audit_event(
6904            &self.bridge,
6905            vm_id,
6906            "security.process.kill",
6907            audit_fields([
6908                (String::from("source"), String::from("guest_process")),
6909                (String::from("source_pid"), source_pid.to_string()),
6910                (String::from("target_pid"), target_pid.to_string()),
6911                (String::from("process_id"), process_id.to_owned()),
6912                (
6913                    String::from("target_process_path"),
6914                    target_path_label.clone(),
6915                ),
6916                (String::from("signal"), signal_name.to_owned()),
6917            ]),
6918        );
6919
6920        Ok(json!({
6921            "self": false,
6922            "action": action,
6923            "signal": signal_name,
6924            "number": signal,
6925            "targetProcessPath": target_path_label,
6926        }))
6927    }
6928
6929    pub(crate) fn poll_javascript_child_process(
6930        &mut self,
6931        vm_id: &str,
6932        process_id: &str,
6933        child_process_id: &str,
6934        wait_ms: u64,
6935    ) -> Result<Value, SidecarError> {
6936        self.poll_descendant_javascript_child_process(
6937            vm_id,
6938            process_id,
6939            &[],
6940            child_process_id,
6941            wait_ms,
6942        )
6943    }
6944
6945    pub(crate) fn write_javascript_child_process_stdin(
6946        &mut self,
6947        vm_id: &str,
6948        process_id: &str,
6949        child_process_id: &str,
6950        chunk: &[u8],
6951    ) -> Result<(), SidecarError> {
6952        let Some(vm) = self.vms.get_mut(vm_id) else {
6953            return Err(javascript_child_process_gone_error(
6954                process_id,
6955                &[child_process_id],
6956            ));
6957        };
6958        let Some(child) = vm
6959            .active_processes
6960            .get_mut(process_id)
6961            .ok_or_else(|| missing_process_error(vm_id, process_id))?
6962            .child_processes
6963            .get_mut(child_process_id)
6964        else {
6965            return Err(javascript_child_process_gone_error(
6966                process_id,
6967                &[child_process_id],
6968            ));
6969        };
6970        if let Err(error) = child.execution.write_stdin(chunk) {
6971            if is_broken_pipe_error(&error) {
6972                return Ok(());
6973            }
6974            return Err(error);
6975        }
6976        write_kernel_process_stdin(&mut vm.kernel, child, chunk)
6977    }
6978
6979    pub(crate) fn close_javascript_child_process_stdin(
6980        &mut self,
6981        vm_id: &str,
6982        process_id: &str,
6983        child_process_id: &str,
6984    ) -> Result<(), SidecarError> {
6985        let Some(vm) = self.vms.get_mut(vm_id) else {
6986            return Err(javascript_child_process_gone_error(
6987                process_id,
6988                &[child_process_id],
6989            ));
6990        };
6991        let Some(child) = vm
6992            .active_processes
6993            .get_mut(process_id)
6994            .ok_or_else(|| missing_process_error(vm_id, process_id))?
6995            .child_processes
6996            .get_mut(child_process_id)
6997        else {
6998            return Err(javascript_child_process_gone_error(
6999                process_id,
7000                &[child_process_id],
7001            ));
7002        };
7003        child.execution.close_stdin()?;
7004        close_kernel_process_stdin(&mut vm.kernel, child)
7005    }
7006
7007    pub(crate) fn kill_javascript_child_process(
7008        &mut self,
7009        vm_id: &str,
7010        process_id: &str,
7011        child_process_id: &str,
7012        signal: &str,
7013    ) -> Result<(), SidecarError> {
7014        let signal_name = signal.to_owned();
7015        let signal = parse_signal(signal)?;
7016        let Some(vm) = self.vms.get_mut(vm_id) else {
7017            return Ok(());
7018        };
7019        let process = vm
7020            .active_processes
7021            .get_mut(process_id)
7022            .ok_or_else(|| missing_process_error(vm_id, process_id))?;
7023        let source_pid = process.kernel_pid;
7024        let child = process
7025            .child_processes
7026            .get_mut(child_process_id)
7027            .ok_or_else(|| {
7028                SidecarError::InvalidState(format!(
7029                    "unknown child process {child_process_id} during kill"
7030                ))
7031            })?;
7032        terminate_tracked_child_process_for_signal(&mut vm.kernel, child, signal)?;
7033        emit_security_audit_event(
7034            &self.bridge,
7035            vm_id,
7036            "security.process.kill",
7037            audit_fields([
7038                (String::from("source"), String::from("guest_child_process")),
7039                (String::from("source_pid"), source_pid.to_string()),
7040                (String::from("target_pid"), child.kernel_pid.to_string()),
7041                (String::from("process_id"), process_id.to_owned()),
7042                (
7043                    String::from("child_process_id"),
7044                    child_process_id.to_owned(),
7045                ),
7046                (String::from("signal"), signal_name),
7047            ]),
7048        );
7049        Ok(())
7050    }
7051
7052    /// Delivers a signal to one kernel pid inside a VM, resolving the target
7053    /// through the active-process tree first so tracked sidecar executions get
7054    /// the same termination handling as a direct `child_process.kill`.
7055    /// Untracked kernel processes (for example WASM subprocess trees) receive
7056    /// the signal through the kernel process table directly.
7057    pub(crate) fn signal_vm_kernel_pid(
7058        &mut self,
7059        vm_id: &str,
7060        target_kernel_pid: u32,
7061        signal_name: &str,
7062    ) -> Result<(), SidecarError> {
7063        let signal = parse_signal(signal_name)?;
7064        let located = {
7065            let Some(vm) = self.vms.get(vm_id) else {
7066                return Err(SidecarError::InvalidState(String::from(
7067                    "ESRCH: unknown VM during process.kill",
7068                )));
7069            };
7070            let alive = vm
7071                .kernel
7072                .list_processes()
7073                .get(&target_kernel_pid)
7074                .is_some_and(|info| info.status != ProcessStatus::Exited);
7075            if !alive {
7076                return Err(SidecarError::InvalidState(format!(
7077                    "ESRCH: no such process {target_kernel_pid}"
7078                )));
7079            }
7080            vm.active_processes.iter().find_map(|(process_id, root)| {
7081                Self::active_process_path_by_kernel_pid(root, target_kernel_pid)
7082                    .map(|path| (process_id.clone(), path))
7083            })
7084        };
7085
7086        match located {
7087            Some((process_id, path)) if path.is_empty() => {
7088                self.kill_process_internal(vm_id, &process_id, signal_name)
7089            }
7090            Some((process_id, path)) => {
7091                let Some(vm) = self.vms.get_mut(vm_id) else {
7092                    return Ok(());
7093                };
7094                let Some(root) = vm.active_processes.get_mut(&process_id) else {
7095                    return Ok(());
7096                };
7097                let Some(target) = Self::active_process_by_owned_path_mut(root, &path) else {
7098                    return Err(SidecarError::InvalidState(format!(
7099                        "ESRCH: no such process {target_kernel_pid}"
7100                    )));
7101                };
7102                terminate_tracked_child_process_for_signal(&mut vm.kernel, target, signal)?;
7103                emit_security_audit_event(
7104                    &self.bridge,
7105                    vm_id,
7106                    "security.process.kill",
7107                    audit_fields([
7108                        (String::from("source"), String::from("guest_process")),
7109                        (String::from("target_pid"), target_kernel_pid.to_string()),
7110                        (String::from("process_id"), process_id),
7111                        (String::from("signal"), signal_name.to_owned()),
7112                    ]),
7113                );
7114                Ok(())
7115            }
7116            None => {
7117                let Some(vm) = self.vms.get_mut(vm_id) else {
7118                    return Ok(());
7119                };
7120                let target_pid = i32::try_from(target_kernel_pid).map_err(|_| {
7121                    SidecarError::InvalidState(format!(
7122                        "EINVAL: invalid process pid {target_kernel_pid}"
7123                    ))
7124                })?;
7125                vm.kernel
7126                    .signal_process(EXECUTION_DRIVER_NAME, target_pid, signal)
7127                    .map_err(kernel_error)?;
7128                emit_security_audit_event(
7129                    &self.bridge,
7130                    vm_id,
7131                    "security.process.kill",
7132                    audit_fields([
7133                        (String::from("source"), String::from("guest_process")),
7134                        (String::from("target_pid"), target_kernel_pid.to_string()),
7135                        (String::from("signal"), signal_name.to_owned()),
7136                    ]),
7137                );
7138                Ok(())
7139            }
7140        }
7141    }
7142
7143    /// Delivers a signal to every live member of a VM process group, matching
7144    /// Linux `kill(-pgid, sig)` semantics. Returns whether the caller itself
7145    /// is a member of the group so entry points can apply self-signal
7146    /// delivery; the caller is intentionally skipped here.
7147    pub(crate) fn signal_vm_process_group(
7148        &mut self,
7149        vm_id: &str,
7150        caller_kernel_pid: u32,
7151        pgid: u32,
7152        signal_name: &str,
7153    ) -> Result<bool, SidecarError> {
7154        parse_signal(signal_name)?;
7155        let members = {
7156            let Some(vm) = self.vms.get(vm_id) else {
7157                return Err(SidecarError::InvalidState(String::from(
7158                    "ESRCH: unknown VM during process.kill",
7159                )));
7160            };
7161            vm.kernel
7162                .list_processes()
7163                .into_iter()
7164                .filter(|(_, info)| info.pgid == pgid && info.status != ProcessStatus::Exited)
7165                .map(|(pid, _)| pid)
7166                .collect::<Vec<_>>()
7167        };
7168        if members.is_empty() {
7169            return Err(SidecarError::InvalidState(format!(
7170                "ESRCH: no such process group {pgid}"
7171            )));
7172        }
7173
7174        let mut caller_is_member = false;
7175        for member_pid in members {
7176            if member_pid == caller_kernel_pid {
7177                caller_is_member = true;
7178                continue;
7179            }
7180            match self.signal_vm_kernel_pid(vm_id, member_pid, signal_name) {
7181                Ok(()) => {}
7182                // Group members can exit while the group is being signaled. A
7183                // vanished member is not an error for the group kill overall.
7184                Err(error) if sidecar_error_is_esrch(&error) => {}
7185                Err(error) => return Err(error),
7186            }
7187        }
7188        Ok(caller_is_member)
7189    }
7190}
7191
7192/// Applies a kill signal to a tracked child execution. Shared-runtime
7193/// executions for lethal signals are terminated directly with a synthetic
7194/// signal exit so child polls observe a prompt close; everything else routes
7195/// through the kernel process table.
7196fn terminate_tracked_child_process_for_signal(
7197    kernel: &mut SidecarKernel,
7198    child: &mut ActiveProcess,
7199    signal: i32,
7200) -> Result<(), SidecarError> {
7201    let should_terminate_shared_runtime = child.execution.uses_shared_v8_runtime()
7202        && signal != 0
7203        && !matches!(
7204            signal,
7205            libc::SIGHUP
7206                | libc::SIGINT
7207                | libc::SIGTERM
7208                | libc::SIGCHLD
7209                | libc::SIGWINCH
7210                | libc::SIGSTOP
7211                | libc::SIGCONT
7212        );
7213    if should_terminate_shared_runtime {
7214        child.execution.terminate()?;
7215        child.pending_self_signal_exit = Some(signal);
7216        child.queue_pending_execution_event(ActiveExecutionEvent::Exited(128 + signal))?;
7217    } else {
7218        kernel
7219            .kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, signal)
7220            .map_err(kernel_error)?;
7221    }
7222    Ok(())
7223}
7224
7225fn sidecar_error_is_esrch(error: &SidecarError) -> bool {
7226    error.to_string().contains("ESRCH")
7227}
7228
7229fn apply_active_process_default_signal(
7230    kernel: &mut SidecarKernel,
7231    process: &mut ActiveProcess,
7232    signal: i32,
7233) -> Result<(), SidecarError> {
7234    if matches!(signal, libc::SIGSTOP | libc::SIGCONT) {
7235        return kernel
7236            .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal)
7237            .map_err(kernel_error);
7238    }
7239
7240    if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
7241        close_kernel_process_stdin(kernel, process)?;
7242    }
7243
7244    if process.execution.uses_shared_v8_runtime() {
7245        process.execution.terminate()?;
7246        if signal != 0 && matches!(process.execution, ActiveExecution::Wasm(_)) {
7247            process.queue_pending_execution_event(ActiveExecutionEvent::Exited(128 + signal))?;
7248        }
7249        return Ok(());
7250    }
7251
7252    kernel
7253        .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal)
7254        .map_err(kernel_error)
7255}
7256
7257fn map_wasm_signal_registration(
7258    registration: secure_exec_execution::wasm::WasmSignalHandlerRegistration,
7259) -> SignalHandlerRegistration {
7260    SignalHandlerRegistration {
7261        action: match registration.action {
7262            secure_exec_execution::wasm::WasmSignalDispositionAction::Default => {
7263                crate::protocol::SignalDispositionAction::Default
7264            }
7265            secure_exec_execution::wasm::WasmSignalDispositionAction::Ignore => {
7266                crate::protocol::SignalDispositionAction::Ignore
7267            }
7268            secure_exec_execution::wasm::WasmSignalDispositionAction::User => {
7269                crate::protocol::SignalDispositionAction::User
7270            }
7271        },
7272        mask: registration.mask,
7273        flags: registration.flags,
7274    }
7275}
7276
7277fn parse_process_signal_state_request(
7278    args: &[Value],
7279) -> Result<(u32, SignalHandlerRegistration), SidecarError> {
7280    let signal = javascript_sync_rpc_arg_u32(args, 0, "process.signal_state signal")?;
7281    let action = javascript_sync_rpc_arg_str(args, 1, "process.signal_state action")?;
7282    let mask_json = javascript_sync_rpc_arg_str(args, 2, "process.signal_state mask")?;
7283    let flags = javascript_sync_rpc_arg_u32(args, 3, "process.signal_state flags")?;
7284    let mask: Vec<u32> = serde_json::from_str(mask_json).map_err(|error| {
7285        SidecarError::InvalidState(format!(
7286            "process.signal_state mask must be valid JSON: {error}"
7287        ))
7288    })?;
7289    let action = match action.trim().to_ascii_lowercase().as_str() {
7290        "default" => SignalDispositionAction::Default,
7291        "ignore" => SignalDispositionAction::Ignore,
7292        "user" => SignalDispositionAction::User,
7293        other => {
7294            return Err(SidecarError::InvalidState(format!(
7295                "unsupported process.signal_state action {other}"
7296            )));
7297        }
7298    };
7299
7300    Ok((
7301        signal,
7302        SignalHandlerRegistration {
7303            action,
7304            mask,
7305            flags,
7306        },
7307    ))
7308}
7309
7310fn apply_process_signal_state_update(
7311    signal_states: &mut BTreeMap<String, BTreeMap<u32, SignalHandlerRegistration>>,
7312    process_id: &str,
7313    signal: u32,
7314    registration: SignalHandlerRegistration,
7315) {
7316    if registration.action == SignalDispositionAction::Default
7317        && registration.mask.is_empty()
7318        && registration.flags == 0
7319    {
7320        let remove_process_entry = signal_states
7321            .get_mut(process_id)
7322            .map(|handlers| {
7323                handlers.remove(&signal);
7324                handlers.is_empty()
7325            })
7326            .unwrap_or(false);
7327        if remove_process_entry {
7328            signal_states.remove(process_id);
7329        }
7330        return;
7331    }
7332
7333    signal_states
7334        .entry(process_id.to_owned())
7335        .or_default()
7336        .insert(signal, registration);
7337}
7338
7339fn map_node_signal_registration(
7340    registration: NodeSignalHandlerRegistration,
7341) -> SignalHandlerRegistration {
7342    SignalHandlerRegistration {
7343        action: match registration.action {
7344            NodeSignalDispositionAction::Default => SignalDispositionAction::Default,
7345            NodeSignalDispositionAction::Ignore => SignalDispositionAction::Ignore,
7346            NodeSignalDispositionAction::User => SignalDispositionAction::User,
7347        },
7348        mask: registration.mask,
7349        flags: registration.flags,
7350    }
7351}
7352
7353fn javascript_child_process_sync_input_bytes(
7354    value: Option<&Value>,
7355) -> Result<Option<Vec<u8>>, SidecarError> {
7356    let Some(value) = value else {
7357        return Ok(None);
7358    };
7359
7360    match value {
7361        Value::Null => Ok(None),
7362        Value::String(text) => Ok(Some(text.as_bytes().to_vec())),
7363        other => javascript_sync_rpc_bytes_arg(
7364            std::slice::from_ref(other),
7365            0,
7366            "child_process.spawn_sync input",
7367        )
7368        .map(Some),
7369    }
7370}
7371
7372// bridge_permissions moved to crate::bridge
7373
7374// reconcile_mounts, resolve_cwd moved to crate::vm
7375
7376fn resolve_execute_request(
7377    vm: &VmState,
7378    payload: &ExecuteRequest,
7379) -> Result<ResolvedChildProcessExecution, SidecarError> {
7380    let payload_env: BTreeMap<String, String> = payload
7381        .env
7382        .iter()
7383        .map(|(k, v)| (k.clone(), v.clone()))
7384        .collect();
7385    if let Some(command) = payload.command.as_deref() {
7386        return resolve_command_execution(
7387            vm,
7388            command,
7389            &payload.args,
7390            &payload_env,
7391            payload.cwd.as_deref(),
7392            payload.wasm_permission_tier,
7393        );
7394    }
7395
7396    let runtime = payload.runtime.clone().ok_or_else(|| {
7397        SidecarError::InvalidState(String::from("execute requires either command or runtime"))
7398    })?;
7399    let entrypoint = payload.entrypoint.clone().ok_or_else(|| {
7400        SidecarError::InvalidState(String::from(
7401            "execute requires either command or entrypoint",
7402        ))
7403    })?;
7404    let (guest_cwd, host_cwd, allow_host_path_overrides) =
7405        resolve_execution_cwds(vm, payload.cwd.as_deref());
7406    let mut env = vm.guest_env.clone();
7407    env.extend(payload_env.clone());
7408
7409    let requested_host_entrypoint = resolve_host_entrypoint_within_vm_host_cwd(vm, &entrypoint);
7410    if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
7411        let requested_cwd = payload.cwd.as_deref().unwrap_or(guest_cwd.as_str());
7412        return Err(SidecarError::InvalidState(format!(
7413            "execution cwd {requested_cwd} is outside sandbox root {}",
7414            vm.host_cwd.to_string_lossy()
7415        )));
7416    }
7417    let host_entrypoint_override = allow_host_path_overrides
7418        .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, &entrypoint))
7419        .flatten();
7420
7421    let guest_entrypoint = host_entrypoint_override
7422        .as_ref()
7423        .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
7424        .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, &entrypoint));
7425    prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
7426
7427    Ok(ResolvedChildProcessExecution {
7428        command: match runtime {
7429            GuestRuntimeKind::JavaScript => String::from(JAVASCRIPT_COMMAND),
7430            GuestRuntimeKind::Python => String::from(PYTHON_COMMAND),
7431            GuestRuntimeKind::WebAssembly => String::from(WASM_COMMAND),
7432        },
7433        process_args: std::iter::once(entrypoint.clone())
7434            .chain(payload.args.iter().cloned())
7435            .collect(),
7436        runtime,
7437        entrypoint: host_entrypoint_override
7438            .map(|(_, host_entrypoint)| host_entrypoint)
7439            .unwrap_or(entrypoint),
7440        execution_args: payload.args.clone(),
7441        env,
7442        guest_cwd,
7443        host_cwd,
7444        wasm_permission_tier: payload.wasm_permission_tier,
7445        tool_command: false,
7446    })
7447}
7448
7449fn resolve_command_execution(
7450    vm: &VmState,
7451    command: &str,
7452    args: &[String],
7453    extra_env: &BTreeMap<String, String>,
7454    cwd: Option<&str>,
7455    explicit_wasm_permission_tier: Option<WasmPermissionTier>,
7456) -> Result<ResolvedChildProcessExecution, SidecarError> {
7457    let (guest_cwd, host_cwd, allow_host_path_overrides) = resolve_execution_cwds(vm, cwd);
7458    let mut env = vm.guest_env.clone();
7459    env.extend(extra_env.clone());
7460    let args = apply_shell_cwd_prefix(command, args.to_vec(), &guest_cwd);
7461
7462    if is_tool_command(vm, command) {
7463        let command = normalized_tool_command_name(command).unwrap_or_else(|| command.to_owned());
7464        return Ok(ResolvedChildProcessExecution {
7465            command: command.clone(),
7466            process_args: std::iter::once(command.clone())
7467                .chain(args.iter().cloned())
7468                .collect(),
7469            runtime: GuestRuntimeKind::JavaScript,
7470            entrypoint: command,
7471            execution_args: args,
7472            env,
7473            guest_cwd,
7474            host_cwd,
7475            wasm_permission_tier: None,
7476            tool_command: true,
7477        });
7478    }
7479
7480    if is_node_runtime_command(command) {
7481        if let Some(cli) = resolve_host_node_cli_entrypoint(command) {
7482            env.insert(
7483                String::from("AGENTOS_NODE_EVAL"),
7484                build_host_node_cli_eval(&cli),
7485            );
7486            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
7487            add_runtime_guest_path_mapping(&mut env, &cli.guest_root, &cli.package_root);
7488            add_runtime_host_access_path(
7489                &mut env,
7490                "AGENTOS_EXTRA_FS_READ_PATHS",
7491                &cli.package_root,
7492                true,
7493            );
7494
7495            return Ok(ResolvedChildProcessExecution {
7496                command: String::from(JAVASCRIPT_COMMAND),
7497                process_args: std::iter::once(command.to_owned())
7498                    .chain(args.iter().cloned())
7499                    .collect(),
7500                runtime: GuestRuntimeKind::JavaScript,
7501                entrypoint: String::from("-e"),
7502                execution_args: std::iter::once(cli.guest_entrypoint.clone())
7503                    .chain(args.iter().cloned())
7504                    .collect(),
7505                env,
7506                guest_cwd,
7507                host_cwd,
7508                wasm_permission_tier: None,
7509                tool_command: false,
7510            });
7511        }
7512
7513        if args.is_empty() {
7514            env.insert(String::from("AGENTOS_NODE_EVAL"), String::new());
7515            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
7516
7517            return Ok(ResolvedChildProcessExecution {
7518                command: String::from(JAVASCRIPT_COMMAND),
7519                process_args: vec![command.to_owned()],
7520                runtime: GuestRuntimeKind::JavaScript,
7521                entrypoint: String::from("-e"),
7522                execution_args: Vec::new(),
7523                env,
7524                guest_cwd,
7525                host_cwd,
7526                wasm_permission_tier: None,
7527                tool_command: false,
7528            });
7529        }
7530
7531        if let Some((entrypoint, execution_args)) =
7532            resolve_special_node_cli_invocation(&args, &mut env)
7533        {
7534            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
7535
7536            return Ok(ResolvedChildProcessExecution {
7537                command: String::from(JAVASCRIPT_COMMAND),
7538                process_args: std::iter::once(command.to_owned())
7539                    .chain(args.iter().cloned())
7540                    .collect(),
7541                runtime: GuestRuntimeKind::JavaScript,
7542                entrypoint,
7543                execution_args,
7544                env,
7545                guest_cwd,
7546                host_cwd,
7547                wasm_permission_tier: None,
7548                tool_command: false,
7549            });
7550        }
7551
7552        let Some(entrypoint_specifier) = args.first() else {
7553            return Err(SidecarError::InvalidState(format!(
7554                "{command} execution requires an entrypoint"
7555            )));
7556        };
7557
7558        let (entrypoint, execution_args, guest_entrypoint) = {
7559            let requested_host_entrypoint =
7560                resolve_host_entrypoint_within_vm_host_cwd(vm, entrypoint_specifier);
7561            if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
7562                let requested_cwd = cwd.unwrap_or(guest_cwd.as_str());
7563                return Err(SidecarError::InvalidState(format!(
7564                    "execution cwd {requested_cwd} is outside sandbox root {}",
7565                    vm.host_cwd.to_string_lossy()
7566                )));
7567            }
7568            let host_entrypoint_override = allow_host_path_overrides
7569                .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, entrypoint_specifier))
7570                .flatten();
7571            let guest_entrypoint = host_entrypoint_override
7572                .as_ref()
7573                .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
7574                .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, entrypoint_specifier));
7575            let entrypoint = host_entrypoint_override.map_or_else(
7576                || {
7577                    guest_entrypoint.as_ref().map_or_else(
7578                        || entrypoint_specifier.clone(),
7579                        |guest_entrypoint| {
7580                            resolve_vm_guest_path_to_host(vm, guest_entrypoint)
7581                                .to_string_lossy()
7582                                .into_owned()
7583                        },
7584                    )
7585                },
7586                |(_, host_entrypoint)| host_entrypoint,
7587            );
7588            (
7589                entrypoint,
7590                args.iter().skip(1).cloned().collect(),
7591                guest_entrypoint,
7592            )
7593        };
7594
7595        prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
7596
7597        return Ok(ResolvedChildProcessExecution {
7598            command: String::from(JAVASCRIPT_COMMAND),
7599            process_args: std::iter::once(command.to_owned())
7600                .chain(args.iter().cloned())
7601                .collect(),
7602            runtime: GuestRuntimeKind::JavaScript,
7603            entrypoint,
7604            execution_args,
7605            env,
7606            guest_cwd,
7607            host_cwd,
7608            wasm_permission_tier: None,
7609            tool_command: false,
7610        });
7611    }
7612
7613    if command.ends_with(".js") || command.ends_with(".mjs") || command.ends_with(".cjs") {
7614        let requested_host_entrypoint = resolve_host_entrypoint_within_vm_host_cwd(vm, command);
7615        if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
7616            let requested_cwd = cwd.unwrap_or(guest_cwd.as_str());
7617            return Err(SidecarError::InvalidState(format!(
7618                "execution cwd {requested_cwd} is outside sandbox root {}",
7619                vm.host_cwd.to_string_lossy()
7620            )));
7621        }
7622        let host_entrypoint_override = allow_host_path_overrides
7623            .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, command))
7624            .flatten();
7625        let guest_entrypoint = host_entrypoint_override
7626            .as_ref()
7627            .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
7628            .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, command));
7629        let entrypoint = host_entrypoint_override.map_or_else(
7630            || {
7631                guest_entrypoint.as_ref().map_or_else(
7632                    || command.to_owned(),
7633                    |guest_entrypoint| {
7634                        resolve_vm_guest_path_to_host(vm, guest_entrypoint)
7635                            .to_string_lossy()
7636                            .into_owned()
7637                    },
7638                )
7639            },
7640            |(_, host_entrypoint)| host_entrypoint,
7641        );
7642        prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
7643
7644        return Ok(ResolvedChildProcessExecution {
7645            command: String::from(JAVASCRIPT_COMMAND),
7646            process_args: std::iter::once(command.to_owned())
7647                .chain(args.iter().cloned())
7648                .collect(),
7649            runtime: GuestRuntimeKind::JavaScript,
7650            entrypoint,
7651            execution_args: args.to_vec(),
7652            env,
7653            guest_cwd,
7654            host_cwd,
7655            wasm_permission_tier: None,
7656            tool_command: false,
7657        });
7658    }
7659
7660    let guest_entrypoint = resolve_guest_command_entrypoint(
7661        vm,
7662        &guest_cwd,
7663        command,
7664        env.get("PATH").map(String::as_str),
7665    )
7666    .ok_or_else(|| {
7667        SidecarError::InvalidState(format!(
7668            "command not found on native sidecar path: {command}"
7669        ))
7670    })?;
7671    let wasm_permission_tier = explicit_wasm_permission_tier
7672        .or_else(|| vm.command_permissions.get(command).copied())
7673        .or_else(|| {
7674            Path::new(&guest_entrypoint)
7675                .file_name()
7676                .and_then(|name| name.to_str())
7677                .and_then(|name| vm.command_permissions.get(name).copied())
7678        });
7679
7680    let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint);
7681    if let Some((javascript_guest_entrypoint, javascript_host_entrypoint)) =
7682        resolve_javascript_command_entrypoint(vm, &guest_entrypoint, &host_entrypoint)
7683    {
7684        prepare_guest_runtime_env(
7685            vm,
7686            &mut env,
7687            &guest_cwd,
7688            &host_cwd,
7689            Some(javascript_guest_entrypoint),
7690        )?;
7691
7692        return Ok(ResolvedChildProcessExecution {
7693            command: command.to_owned(),
7694            process_args: std::iter::once(command.to_owned())
7695                .chain(args.iter().cloned())
7696                .collect(),
7697            runtime: GuestRuntimeKind::JavaScript,
7698            entrypoint: javascript_host_entrypoint.to_string_lossy().into_owned(),
7699            execution_args: args.to_vec(),
7700            env,
7701            guest_cwd,
7702            host_cwd,
7703            wasm_permission_tier: None,
7704            tool_command: false,
7705        });
7706    }
7707    prepare_guest_runtime_env(
7708        vm,
7709        &mut env,
7710        &guest_cwd,
7711        &host_cwd,
7712        Some(guest_entrypoint.clone()),
7713    )?;
7714
7715    Ok(ResolvedChildProcessExecution {
7716        command: command.to_owned(),
7717        process_args: std::iter::once(command.to_owned())
7718            .chain(args.iter().cloned())
7719            .collect(),
7720        runtime: GuestRuntimeKind::WebAssembly,
7721        entrypoint: host_entrypoint.to_string_lossy().into_owned(),
7722        execution_args: args.to_vec(),
7723        env,
7724        guest_cwd,
7725        host_cwd,
7726        wasm_permission_tier,
7727        tool_command: false,
7728    })
7729}
7730
7731const MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH: usize = 4;
7732
7733fn resolve_javascript_command_entrypoint(
7734    vm: &VmState,
7735    guest_entrypoint: &str,
7736    host_entrypoint: &Path,
7737) -> Option<(String, PathBuf)> {
7738    resolve_javascript_command_entrypoint_inner(
7739        vm,
7740        guest_entrypoint,
7741        host_entrypoint,
7742        MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH,
7743    )
7744}
7745
7746fn resolve_javascript_command_entrypoint_inner(
7747    vm: &VmState,
7748    guest_entrypoint: &str,
7749    host_entrypoint: &Path,
7750    redirects_remaining: usize,
7751) -> Option<(String, PathBuf)> {
7752    if redirects_remaining > 0 {
7753        let symlink_target = fs::symlink_metadata(host_entrypoint)
7754            .ok()
7755            .filter(|metadata| metadata.file_type().is_symlink())
7756            .and_then(|_| fs::read_link(host_entrypoint).ok());
7757        if let Some(symlink_target) = symlink_target {
7758            let guest_parent = Path::new(guest_entrypoint)
7759                .parent()
7760                .and_then(|path| path.to_str())
7761                .unwrap_or("/");
7762            let symlink_guest_entrypoint = if symlink_target.is_absolute() {
7763                normalize_path(&symlink_target.to_string_lossy())
7764            } else {
7765                normalize_path(&format!(
7766                    "{guest_parent}/{}",
7767                    symlink_target.to_string_lossy().replace('\\', "/")
7768                ))
7769            };
7770            let symlink_host_entrypoint =
7771                resolve_vm_guest_path_to_host(vm, &symlink_guest_entrypoint);
7772            return resolve_javascript_command_entrypoint_inner(
7773                vm,
7774                &symlink_guest_entrypoint,
7775                &symlink_host_entrypoint,
7776                redirects_remaining - 1,
7777            );
7778        }
7779    }
7780
7781    let script = load_executable_script_preview(host_entrypoint)?;
7782    let interpreter = parse_script_interpreter_name(&script);
7783
7784    if interpreter.is_none() && is_probable_javascript_entrypoint(host_entrypoint, &script) {
7785        return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf()));
7786    }
7787
7788    let interpreter = interpreter?;
7789    if interpreter == "node" {
7790        return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf()));
7791    }
7792
7793    if redirects_remaining == 0 || !matches!(interpreter.as_str(), "sh" | "bash" | "dash") {
7794        return None;
7795    }
7796
7797    let shim_target = parse_node_shell_shim_target(&script)?;
7798    let guest_parent = Path::new(guest_entrypoint)
7799        .parent()
7800        .and_then(|path| path.to_str())
7801        .unwrap_or("/");
7802    let shim_guest_entrypoint = normalize_path(&format!("{guest_parent}/{shim_target}"));
7803    let shim_host_entrypoint = resolve_vm_guest_path_to_host(vm, &shim_guest_entrypoint);
7804    resolve_javascript_command_entrypoint_inner(
7805        vm,
7806        &shim_guest_entrypoint,
7807        &shim_host_entrypoint,
7808        redirects_remaining - 1,
7809    )
7810}
7811
7812fn load_executable_script_preview(path: &Path) -> Option<String> {
7813    let bytes = fs::read(path).ok()?;
7814    let preview_len = bytes.len().min(16 * 1024);
7815    Some(String::from_utf8_lossy(&bytes[..preview_len]).into_owned())
7816}
7817
7818fn parse_script_interpreter_name(script: &str) -> Option<String> {
7819    let shebang = script.lines().next()?.strip_prefix("#!")?.trim();
7820    let mut tokens = shebang.split_whitespace();
7821    let command = tokens.next()?;
7822    let command_name = Path::new(command).file_name()?.to_str()?;
7823    if command_name == "env" {
7824        for token in tokens {
7825            if token.starts_with('-') {
7826                continue;
7827            }
7828            return Path::new(token)
7829                .file_name()
7830                .and_then(|name| name.to_str())
7831                .map(ToOwned::to_owned);
7832        }
7833        return None;
7834    }
7835
7836    Some(command_name.to_owned())
7837}
7838
7839fn parse_node_shell_shim_target(script: &str) -> Option<String> {
7840    for line in script.lines() {
7841        let trimmed = line.trim();
7842        if !trimmed.starts_with("exec ") {
7843            continue;
7844        }
7845
7846        let mut remaining = trimmed;
7847        while let Some(start) = remaining.find("\"$basedir/") {
7848            let after_prefix = &remaining[start + "\"$basedir/".len()..];
7849            let end = after_prefix.find('"')?;
7850            let candidate = &after_prefix[..end];
7851            remaining = &after_prefix[end + 1..];
7852
7853            if candidate.is_empty() || candidate == "node" || candidate.ends_with("/node") {
7854                continue;
7855            }
7856
7857            return Some(candidate.to_owned());
7858        }
7859    }
7860
7861    None
7862}
7863
7864fn is_probable_javascript_entrypoint(path: &Path, script: &str) -> bool {
7865    let extension = path
7866        .extension()
7867        .and_then(|value| value.to_str())
7868        .unwrap_or_default();
7869    if matches!(extension, "js" | "cjs" | "mjs") {
7870        return true;
7871    }
7872
7873    if !path
7874        .components()
7875        .any(|component| component.as_os_str() == "node_modules")
7876    {
7877        return false;
7878    }
7879
7880    let preview = script.trim_start_matches('\u{feff}').trim_start();
7881    !preview.is_empty()
7882        && !preview.starts_with("#!")
7883        && (preview.starts_with("\"use strict\"")
7884            || preview.starts_with("'use strict'")
7885            || preview.starts_with("import ")
7886            || preview.starts_with("export ")
7887            || preview.starts_with("const ")
7888            || preview.starts_with("let ")
7889            || preview.starts_with("var ")
7890            || preview.starts_with("Object.defineProperty(exports")
7891            || preview.starts_with("module.exports")
7892            || preview.starts_with("require("))
7893}
7894
7895fn resolve_guest_execution_cwd(vm: &VmState, value: Option<&str>) -> String {
7896    value
7897        .map(normalize_path)
7898        .unwrap_or_else(|| vm.guest_cwd.clone())
7899}
7900
7901fn resolve_execution_cwds(vm: &VmState, value: Option<&str>) -> (String, PathBuf, bool) {
7902    if let Some(raw_cwd) = value {
7903        let normalized_vm_host_cwd = normalize_host_path(&vm.host_cwd);
7904        let requested_host_cwd = normalize_host_path(Path::new(raw_cwd));
7905        if path_is_within_root(&requested_host_cwd, &normalized_vm_host_cwd) {
7906            let relative = requested_host_cwd
7907                .strip_prefix(&normalized_vm_host_cwd)
7908                .unwrap_or_else(|_| Path::new(""));
7909            let relative = relative.to_string_lossy().replace('\\', "/");
7910            let guest_cwd = if relative.is_empty() {
7911                String::from("/")
7912            } else {
7913                normalize_path(&format!("/{relative}"))
7914            };
7915            return (guest_cwd, requested_host_cwd, true);
7916        }
7917    }
7918
7919    let guest_cwd = resolve_guest_execution_cwd(vm, value);
7920    let host_cwd = if value.is_none() {
7921        vm.host_cwd.clone()
7922    } else {
7923        resolve_vm_guest_path_to_host(vm, &guest_cwd)
7924    };
7925    (guest_cwd, host_cwd, value.is_none())
7926}
7927
7928fn resolve_vm_guest_path_to_host(vm: &VmState, guest_path: &str) -> PathBuf {
7929    host_mount_path_for_guest_path(vm, guest_path)
7930        .unwrap_or_else(|| shadow_path_for_guest(vm, guest_path))
7931}
7932
7933fn shadow_path_for_guest(vm: &VmState, guest_path: &str) -> PathBuf {
7934    let normalized = normalize_path(guest_path);
7935    let relative = normalized.trim_start_matches('/');
7936    if relative.is_empty() {
7937        return vm.cwd.clone();
7938    }
7939    vm.cwd.join(relative)
7940}
7941
7942fn apply_shell_cwd_prefix(command: &str, mut args: Vec<String>, guest_cwd: &str) -> Vec<String> {
7943    if guest_cwd == "/" || !is_shell_command(command) {
7944        return args;
7945    }
7946
7947    let Some(flag) = args.first() else {
7948        return args;
7949    };
7950    if !matches!(flag.as_str(), "-c" | "-lc") || args.len() < 2 {
7951        return args;
7952    }
7953
7954    let command_text = args[1].clone();
7955    let quoted_cwd = shell_single_quote(guest_cwd);
7956    args[1] = format!("cd {quoted_cwd} && {command_text}");
7957    args
7958}
7959
7960fn is_shell_command(command: &str) -> bool {
7961    Path::new(command)
7962        .file_name()
7963        .and_then(|name| name.to_str())
7964        .unwrap_or(command)
7965        .trim_end_matches(".exe")
7966        .eq("sh")
7967        || Path::new(command)
7968            .file_name()
7969            .and_then(|name| name.to_str())
7970            .unwrap_or(command)
7971            .trim_end_matches(".exe")
7972            .eq("bash")
7973}
7974
7975fn shell_single_quote(value: &str) -> String {
7976    if value.is_empty() {
7977        return String::from("''");
7978    }
7979    format!("'{}'", value.replace('\'', "'\"'\"'"))
7980}
7981
7982pub(crate) fn sync_active_process_host_writes_to_kernel(
7983    vm: &mut VmState,
7984) -> Result<(), SidecarError> {
7985    if vm.root_filesystem_mode != RootFilesystemMode::ReadOnly {
7986        let shadow_root = vm.cwd.clone();
7987        sync_host_directory_tree_to_kernel(vm, &shadow_root, "/")?;
7988    }
7989
7990    let normalized_vm_root = normalize_host_path(&vm.cwd);
7991    let extra_roots = collect_active_process_host_sync_roots(vm, &normalized_vm_root);
7992    for (host_cwd, guest_cwd) in extra_roots {
7993        sync_host_directory_tree_to_kernel(vm, &host_cwd, &guest_cwd)?;
7994    }
7995
7996    Ok(())
7997}
7998
7999fn collect_active_process_host_sync_roots(
8000    vm: &VmState,
8001    normalized_vm_root: &Path,
8002) -> Vec<(PathBuf, String)> {
8003    let mut roots = Vec::new();
8004    let mut seen = BTreeSet::new();
8005
8006    for process in vm.active_processes.values() {
8007        collect_process_host_sync_roots(process, normalized_vm_root, &mut seen, &mut roots);
8008    }
8009
8010    roots
8011}
8012
8013fn collect_process_host_sync_roots(
8014    process: &ActiveProcess,
8015    normalized_vm_root: &Path,
8016    seen: &mut BTreeSet<(PathBuf, String)>,
8017    roots: &mut Vec<(PathBuf, String)>,
8018) {
8019    let normalized_host_cwd = normalize_host_path(&process.host_cwd);
8020    if !path_is_within_root(&normalized_host_cwd, normalized_vm_root) {
8021        let guest_cwd = normalize_path(&process.guest_cwd);
8022        if seen.insert((normalized_host_cwd.clone(), guest_cwd.clone())) {
8023            roots.push((normalized_host_cwd, guest_cwd));
8024        }
8025    }
8026
8027    for child in process.child_processes.values() {
8028        collect_process_host_sync_roots(child, normalized_vm_root, seen, roots);
8029    }
8030}
8031
8032fn sync_process_host_writes_to_kernel(
8033    vm: &mut VmState,
8034    process: &ActiveProcess,
8035) -> Result<(), SidecarError> {
8036    if vm.root_filesystem_mode != RootFilesystemMode::ReadOnly {
8037        let shadow_root = vm.cwd.clone();
8038        sync_host_directory_tree_to_kernel(vm, &shadow_root, "/")?;
8039    }
8040
8041    if !path_is_within_root(
8042        &normalize_host_path(&process.host_cwd),
8043        &normalize_host_path(&vm.cwd),
8044    ) {
8045        sync_host_directory_tree_to_kernel(vm, &process.host_cwd, &process.guest_cwd)?;
8046    }
8047
8048    Ok(())
8049}
8050
8051fn sync_host_directory_tree_to_kernel(
8052    vm: &mut VmState,
8053    host_root: &Path,
8054    guest_root: &str,
8055) -> Result<(), SidecarError> {
8056    let normalized_host_root = normalize_host_path(host_root);
8057    let normalized_guest_root = normalize_path(guest_root);
8058    let mut synced_file_times = BTreeMap::new();
8059    sync_host_directory_tree_to_kernel_inner(
8060        vm,
8061        &normalized_host_root,
8062        &normalized_host_root,
8063        &normalized_guest_root,
8064        &mut synced_file_times,
8065    )
8066}
8067
8068fn sync_host_directory_tree_to_kernel_inner(
8069    vm: &mut VmState,
8070    host_root: &Path,
8071    current_host_dir: &Path,
8072    guest_root: &str,
8073    synced_file_times: &mut BTreeMap<(u64, u64), (u64, u64)>,
8074) -> Result<(), SidecarError> {
8075    let entries = match fs::read_dir(current_host_dir) {
8076        Ok(entries) => entries,
8077        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
8078        Err(error) => {
8079            return Err(SidecarError::Io(format!(
8080                "failed to read host shadow directory {}: {error}",
8081                current_host_dir.display()
8082            )));
8083        }
8084    };
8085
8086    for entry in entries {
8087        let entry = entry.map_err(|error| {
8088            SidecarError::Io(format!(
8089                "failed to read host shadow entry in {}: {error}",
8090                current_host_dir.display()
8091            ))
8092        })?;
8093        let host_path = entry.path();
8094        let file_type = entry.file_type().map_err(|error| {
8095            SidecarError::Io(format!(
8096                "failed to stat host shadow entry {}: {error}",
8097                host_path.display()
8098            ))
8099        })?;
8100        let relative_path = host_path
8101            .strip_prefix(host_root)
8102            .map_err(|error| {
8103                SidecarError::InvalidState(format!(
8104                    "failed to relativize host shadow path {} against {}: {error}",
8105                    host_path.display(),
8106                    host_root.display()
8107                ))
8108            })?
8109            .to_string_lossy()
8110            .replace('\\', "/");
8111        let guest_path = if guest_root == "/" {
8112            normalize_path(&format!("/{relative_path}"))
8113        } else {
8114            normalize_path(&format!(
8115                "{}/{}",
8116                guest_root.trim_end_matches('/'),
8117                relative_path
8118            ))
8119        };
8120
8121        if should_skip_shadow_sync_path(vm, &guest_path) {
8122            continue;
8123        }
8124
8125        if file_type.is_dir() {
8126            let metadata = entry.metadata().map_err(|error| {
8127                SidecarError::Io(format!(
8128                    "failed to read host shadow metadata {}: {error}",
8129                    host_path.display()
8130                ))
8131            })?;
8132            if !is_shadow_bootstrap_dir(&guest_path)
8133                && !vm.kernel.exists(&guest_path).unwrap_or(false)
8134            {
8135                vm.kernel.mkdir(&guest_path, true).map_err(|error| {
8136                    SidecarError::InvalidState(format!(
8137                        "failed to sync host shadow directory {} to guest {}: {}",
8138                        host_path.display(),
8139                        guest_path,
8140                        kernel_error(error)
8141                    ))
8142                })?;
8143                vm.kernel
8144                    .chmod(&guest_path, host_shadow_mode(&metadata))
8145                    .map_err(|error| {
8146                        SidecarError::InvalidState(format!(
8147                            "failed to sync host shadow directory mode {} to guest {}: {}",
8148                            host_path.display(),
8149                            guest_path,
8150                            kernel_error(error)
8151                        ))
8152                    })?;
8153            }
8154            sync_host_directory_tree_to_kernel_inner(
8155                vm,
8156                host_root,
8157                &host_path,
8158                guest_root,
8159                synced_file_times,
8160            )?;
8161            continue;
8162        }
8163
8164        if file_type.is_file() {
8165            let metadata = entry.metadata().map_err(|error| {
8166                SidecarError::Io(format!(
8167                    "failed to read host shadow metadata {}: {error}",
8168                    host_path.display()
8169                ))
8170            })?;
8171            let timestamp_key = (metadata.dev(), metadata.ino());
8172            let (atime_ms, mtime_ms) =
8173                *synced_file_times.entry(timestamp_key).or_insert_with(|| {
8174                    (
8175                        metadata_time_ms(metadata.atime(), metadata.atime_nsec()),
8176                        metadata_time_ms(metadata.mtime(), metadata.mtime_nsec()),
8177                    )
8178                });
8179            let desired_mode = host_shadow_mode(&metadata);
8180            let bytes = read_host_shadow_file(&host_path, desired_mode).map_err(|error| {
8181                SidecarError::Io(format!(
8182                    "failed to read host shadow file {}: {error}",
8183                    host_path.display()
8184                ))
8185            })?;
8186            vm.kernel.write_file(&guest_path, bytes).map_err(|error| {
8187                SidecarError::InvalidState(format!(
8188                    "failed to sync host shadow file {} to guest {}: {}",
8189                    host_path.display(),
8190                    guest_path,
8191                    kernel_error(error)
8192                ))
8193            })?;
8194            vm.kernel
8195                .chmod(&guest_path, desired_mode)
8196                .map_err(|error| {
8197                    SidecarError::InvalidState(format!(
8198                        "failed to sync host shadow file mode {} to guest {}: {}",
8199                        host_path.display(),
8200                        guest_path,
8201                        kernel_error(error)
8202                    ))
8203                })?;
8204            vm.kernel
8205                .utimes(&guest_path, atime_ms, mtime_ms)
8206                .map_err(|error| {
8207                    SidecarError::InvalidState(format!(
8208                        "failed to sync host shadow file times {} to guest {}: {}",
8209                        host_path.display(),
8210                        guest_path,
8211                        kernel_error(error)
8212                    ))
8213                })?;
8214            continue;
8215        }
8216
8217        if file_type.is_symlink() {
8218            let target = match fs::read_link(&host_path) {
8219                Ok(target) => target,
8220                Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
8221                Err(error) => {
8222                    return Err(SidecarError::Io(format!(
8223                        "failed to read host shadow symlink {}: {error}",
8224                        host_path.display()
8225                    )));
8226                }
8227            };
8228            replace_kernel_symlink(vm, &guest_path, &target.to_string_lossy())?;
8229        }
8230    }
8231
8232    Ok(())
8233}
8234
8235fn replace_kernel_symlink(
8236    vm: &mut VmState,
8237    guest_path: &str,
8238    target: &str,
8239) -> Result<(), SidecarError> {
8240    if vm.kernel.symlink(target, guest_path).is_ok() {
8241        return Ok(());
8242    }
8243
8244    if let Ok(existing_target) = vm.kernel.read_link(guest_path) {
8245        if existing_target == target {
8246            return Ok(());
8247        }
8248    }
8249
8250    let _ = vm.kernel.remove_file(guest_path);
8251    let _ = vm.kernel.remove_dir(guest_path);
8252    vm.kernel
8253        .symlink(target, guest_path)
8254        .map_err(kernel_error)?;
8255    Ok(())
8256}
8257
8258fn host_shadow_mode(metadata: &fs::Metadata) -> u32 {
8259    metadata.permissions().mode() & 0o7777
8260}
8261
8262/// Reads a shadow-root file back into the kernel even when guest-visible mode
8263/// bits make it unreadable for the host user. The sidecar is the kernel for
8264/// this tree, so guest permission bits (for example a 0o200 write-only file
8265/// produced by `chmod` plus a shell append redirect) must not break the
8266/// exit-time shadow sync. The original mode is restored after the read.
8267fn read_host_shadow_file(host_path: &Path, mode: u32) -> std::io::Result<Vec<u8>> {
8268    match fs::read(host_path) {
8269        Ok(bytes) => Ok(bytes),
8270        Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => {
8271            fs::set_permissions(host_path, fs::Permissions::from_mode(mode | 0o400))?;
8272            let result = fs::read(host_path);
8273            fs::set_permissions(host_path, fs::Permissions::from_mode(mode))?;
8274            result
8275        }
8276        Err(error) => Err(error),
8277    }
8278}
8279
8280fn metadata_time_ms(seconds: i64, nanos: i64) -> u64 {
8281    let seconds = seconds.max(0) as u64;
8282    let nanos = nanos.max(0) as u64;
8283    seconds
8284        .saturating_mul(1_000)
8285        .saturating_add(nanos / 1_000_000)
8286}
8287
8288fn is_shadow_bootstrap_dir(path: &str) -> bool {
8289    matches!(
8290        path,
8291        "/dev"
8292            | "/proc"
8293            | "/tmp"
8294            | "/bin"
8295            | "/lib"
8296            | "/sbin"
8297            | "/boot"
8298            | "/etc"
8299            | "/root"
8300            | "/run"
8301            | "/srv"
8302            | "/sys"
8303            | "/opt"
8304            | "/mnt"
8305            | "/media"
8306            | "/home"
8307            | "/home/agentos"
8308            | "/usr"
8309            | "/usr/bin"
8310            | "/usr/games"
8311            | "/usr/include"
8312            | "/usr/lib"
8313            | "/usr/libexec"
8314            | "/usr/man"
8315            | "/usr/local"
8316            | "/usr/local/bin"
8317            | "/usr/sbin"
8318            | "/usr/share"
8319            | "/usr/share/man"
8320            | "/var"
8321            | "/var/cache"
8322            | "/var/empty"
8323            | "/var/lib"
8324            | "/var/lock"
8325            | "/var/log"
8326            | "/var/run"
8327            | "/var/spool"
8328            | "/var/tmp"
8329            | "/etc/agentos"
8330            | "/workspace"
8331    )
8332}
8333
8334#[cfg(test)]
8335mod shadow_sync_tests {
8336    use super::{is_protected_agentos_shadow_sync_path, is_shadow_bootstrap_dir};
8337
8338    #[test]
8339    fn shadow_bootstrap_sync_skips_virtual_home_tree() {
8340        assert!(is_shadow_bootstrap_dir("/home"));
8341        assert!(is_shadow_bootstrap_dir("/home/agentos"));
8342    }
8343
8344    #[test]
8345    fn protected_agentos_paths_are_not_shadow_synced() {
8346        assert!(is_protected_agentos_shadow_sync_path("/etc/agentos"));
8347        assert!(is_protected_agentos_shadow_sync_path(
8348            "/etc/agentos/instructions.md"
8349        ));
8350        assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos-copy"));
8351        assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos.md"));
8352    }
8353}
8354
8355fn is_kernel_owned_shadow_sync_path(path: &str) -> bool {
8356    matches!(path, "/dev" | "/proc" | "/sys")
8357        || path.starts_with("/dev/")
8358        || path.starts_with("/proc/")
8359        || path.starts_with("/sys/")
8360}
8361
8362pub(crate) fn is_protected_agentos_shadow_sync_path(path: &str) -> bool {
8363    path == "/etc/agentos" || path.starts_with("/etc/agentos/")
8364}
8365
8366fn should_skip_shadow_sync_path(vm: &VmState, guest_path: &str) -> bool {
8367    is_kernel_owned_shadow_sync_path(guest_path)
8368        || is_protected_agentos_shadow_sync_path(guest_path)
8369        || host_mount_path_for_guest_path_from_mounts(&vm.configuration.mounts, guest_path)
8370            .is_some()
8371}
8372
8373fn resolve_path_like_guest_specifier(cwd: &str, specifier: &str) -> String {
8374    if specifier.starts_with("file://") {
8375        normalize_path(specifier.trim_start_matches("file://"))
8376    } else if specifier.starts_with("file:") {
8377        normalize_path(specifier.trim_start_matches("file:"))
8378    } else if specifier.starts_with('/') {
8379        normalize_path(specifier)
8380    } else {
8381        normalize_path(&format!("{cwd}/{specifier}"))
8382    }
8383}
8384
8385fn guest_entrypoint_for_specifier(cwd: &str, specifier: &str) -> Option<String> {
8386    is_path_like_specifier(specifier).then(|| resolve_path_like_guest_specifier(cwd, specifier))
8387}
8388
8389fn is_node_runtime_command(command: &str) -> bool {
8390    matches!(command, "node" | "npm" | "npx")
8391        || Path::new(command)
8392            .file_name()
8393            .and_then(|name| name.to_str())
8394            .is_some_and(|name| matches!(name, "node" | "npm" | "npx"))
8395}
8396
8397fn resolve_special_node_cli_invocation(
8398    args: &[String],
8399    env: &mut BTreeMap<String, String>,
8400) -> Option<(String, Vec<String>)> {
8401    let first = args.first()?;
8402    match first.as_str() {
8403        "-e" | "--eval" => {
8404            env.insert(
8405                String::from("AGENTOS_NODE_EVAL"),
8406                args.get(1).cloned().unwrap_or_default(),
8407            );
8408            Some((first.clone(), args.iter().skip(2).cloned().collect()))
8409        }
8410        "-v" | "--version" => {
8411            env.insert(
8412                String::from("AGENTOS_NODE_EVAL"),
8413                String::from("console.log(process.version);"),
8414            );
8415            Some((String::from("-e"), args.to_vec()))
8416        }
8417        _ => None,
8418    }
8419}
8420
8421fn node_runtime_command_name(command: &str) -> Option<&str> {
8422    let name = Path::new(command)
8423        .file_name()
8424        .and_then(|name| name.to_str())?;
8425    matches!(name, "node" | "npm" | "npx").then_some(name)
8426}
8427
8428struct ResolvedHostNodeCliEntrypoint {
8429    command_name: String,
8430    guest_root: String,
8431    guest_entrypoint: String,
8432    package_root: PathBuf,
8433}
8434
8435fn resolve_host_node_cli_entrypoint(command: &str) -> Option<ResolvedHostNodeCliEntrypoint> {
8436    let command_name = node_runtime_command_name(command)?;
8437    if !matches!(command_name, "npm" | "npx") {
8438        return None;
8439    }
8440
8441    let path = std::env::var_os("PATH")?;
8442    for root in std::env::split_paths(&path) {
8443        let candidate = root.join(command_name);
8444        if !candidate.is_file() {
8445            continue;
8446        }
8447        let entrypoint = candidate.canonicalize().ok().unwrap_or(candidate);
8448        let package_root = entrypoint.parent()?.parent()?.to_path_buf();
8449        let guest_root = format!("/__secure_exec/node-runtime/{command_name}");
8450        let relative_entrypoint = entrypoint.strip_prefix(&package_root).ok()?;
8451        let guest_entrypoint = normalize_path(&format!(
8452            "{guest_root}/{}",
8453            relative_entrypoint.to_string_lossy().replace('\\', "/")
8454        ));
8455        return Some(ResolvedHostNodeCliEntrypoint {
8456            command_name: command_name.to_owned(),
8457            guest_root,
8458            guest_entrypoint,
8459            package_root,
8460        });
8461    }
8462
8463    None
8464}
8465
8466fn build_host_node_cli_eval(cli: &ResolvedHostNodeCliEntrypoint) -> String {
8467    let guest_npm_main = normalize_path(&format!("{}/lib/npm.js", cli.guest_root));
8468    let guest_npm_cli = normalize_path(&format!("{}/bin/npm-cli.js", cli.guest_root));
8469    let guest_package_json = normalize_path(&format!("{}/package.json", cli.guest_root));
8470    let guest_display_module = normalize_path(&format!("{}/lib/utils/display.js", cli.guest_root));
8471    let guest_log_file_module =
8472        normalize_path(&format!("{}/lib/utils/log-file.js", cli.guest_root));
8473    let debug_preamble = "const __agentOSDebugNpmCli = !!process.env.CODEX_DEBUG_NPM_CLI; const __agentOSDebugLog = (...args) => { if (__agentOSDebugNpmCli) { console.error('[secure-exec npm debug]', ...args); } }; const __agentOSIsProcessExitError = (error) => !!(error && typeof error === 'object' && (error._isProcessExit === true || error.name === 'ProcessExitError')); const __agentOSResolveExitCode = (code) => Number.isFinite(code) ? code : (Number.isFinite(process.exitCode) ? process.exitCode : 0); const __agentOSFinish = (code) => { process.exitCode = __agentOSResolveExitCode(code); }; if (__agentOSDebugNpmCli) { const __agentOSWrapAsyncFsMethod = (__agentOSTarget, __agentOSMethod) => { const __agentOSOriginal = __agentOSTarget[__agentOSMethod]; if (typeof __agentOSOriginal !== 'function' || __agentOSOriginal.__agentOSDebugWrapped) { return; } const __agentOSWrapped = async (...args) => { const target = args.length > 0 ? args[0] : '<none>'; __agentOSDebugLog(`fs.${__agentOSMethod}:start`, String(target)); try { const result = await __agentOSOriginal.apply(__agentOSTarget, args); __agentOSDebugLog(`fs.${__agentOSMethod}:done`, String(target)); return result; } catch (error) { __agentOSDebugLog(`fs.${__agentOSMethod}:error`, String(target), error && error.stack ? error.stack : String(error)); throw error; } }; __agentOSWrapped.__agentOSDebugWrapped = true; __agentOSTarget[__agentOSMethod] = __agentOSWrapped; }; const __agentOSWrapSyncFsMethod = (__agentOSTarget, __agentOSMethod) => { const __agentOSOriginal = __agentOSTarget[__agentOSMethod]; if (typeof __agentOSOriginal !== 'function' || __agentOSOriginal.__agentOSDebugWrapped) { return; } const __agentOSWrapped = (...args) => { const target = args.length > 0 ? args[0] : '<none>'; __agentOSDebugLog(`fs.${__agentOSMethod}:start`, String(target)); try { const result = __agentOSOriginal.apply(__agentOSTarget, args); __agentOSDebugLog(`fs.${__agentOSMethod}:done`, String(target)); return result; } catch (error) { __agentOSDebugLog(`fs.${__agentOSMethod}:error`, String(target), error && error.stack ? error.stack : String(error)); throw error; } }; __agentOSWrapped.__agentOSDebugWrapped = true; __agentOSTarget[__agentOSMethod] = __agentOSWrapped; }; const __agentOSFsPromiseModules = [require('fs/promises'), require('node:fs/promises')]; for (const __agentOSFsPromises of __agentOSFsPromiseModules) { for (const __agentOSMethod of ['access', 'lstat', 'mkdir', 'open', 'readFile', 'readdir', 'readlink', 'realpath', 'rename', 'rm', 'rmdir', 'stat', 'symlink', 'unlink', 'writeFile']) { __agentOSWrapAsyncFsMethod(__agentOSFsPromises, __agentOSMethod); } } const __agentOSFsModules = [require('fs'), require('node:fs')]; for (const __agentOSFs of __agentOSFsModules) { for (const __agentOSMethod of ['accessSync', 'existsSync', 'lstatSync', 'mkdirSync', 'openSync', 'readFileSync', 'readdirSync', 'readlinkSync', 'realpathSync', 'renameSync', 'rmSync', 'rmdirSync', 'statSync', 'symlinkSync', 'unlinkSync', 'writeFileSync']) { __agentOSWrapSyncFsMethod(__agentOSFs, __agentOSMethod); } } }";
8474    let display_stub = format!(
8475        "const __agentOSDisplayModulePath = require.resolve({display_module}); const __agentOSLogFileModulePath = require.resolve({log_file_module}); const __agentOSColorPassthrough = new Proxy((value) => value, {{ get: () => __agentOSColorPassthrough, apply: (_target, _thisArg, args) => args[0] }}); class __AgentOSNpmDisplayStub {{ constructor() {{ this.chalk = {{ noColor: __agentOSColorPassthrough, stdout: __agentOSColorPassthrough, stderr: __agentOSColorPassthrough }}; this._logPaused = true; this._logBuffer = []; this._outputBuffer = []; this._write = (stream, values) => {{ if (!Array.isArray(values) || values.length === 0) {{ return; }} const text = values.map((value) => typeof value === 'string' ? value : String(value)).join(' '); if (text.length === 0) {{ return; }} const normalized = text.replace(/\\r\\n/g, '\\n'); if (/^\\n?> npx\\n> /u.test(normalized)) {{ return; }} stream.write(text.endsWith('\\n') ? text : `${{text}}\\n`); }}; this._inputHandler = (level, ...args) => {{ if (level !== 'read') {{ return; }} const [resolve, reject, callback] = args; Promise.resolve().then(() => callback()).then(resolve, reject); }}; this._logHandler = (level, ...args) => {{ if (level === 'resume') {{ this._logPaused = false; for (const entry of this._logBuffer.splice(0)) {{ this._write(process.stderr, entry); }} return; }} if (level === 'pause') {{ this._logPaused = true; return; }} if (this._logPaused) {{ this._logBuffer.push(args); return; }} this._write(process.stderr, args); }}; this._outputHandler = (level, ...args) => {{ if (level === 'buffer') {{ this._outputBuffer.push(['standard', args]); return; }} if (level === 'flush') {{ for (const [bufferLevel, bufferArgs] of this._outputBuffer.splice(0)) {{ this._write(bufferLevel === 'error' ? process.stderr : process.stdout, bufferArgs); }} return; }} this._write(level === 'error' ? process.stderr : process.stdout, args); }}; process.on('input', this._inputHandler); process.on('log', this._logHandler); process.on('output', this._outputHandler); }} async load() {{ process.emit('log', 'resume'); process.emit('output', 'flush'); }} off() {{ if (this._inputHandler) {{ process.off('input', this._inputHandler); }} if (this._logHandler) {{ process.off('log', this._logHandler); }} if (this._outputHandler) {{ process.off('output', this._outputHandler); }} this._logBuffer.length = 0; this._outputBuffer.length = 0; }} }} class __AgentOSNpmLogFileStub {{ constructor() {{ this.files = []; }} async load() {{ return []; }} off() {{}} }} globalThis._moduleCache[__agentOSDisplayModulePath] = {{ exports: __AgentOSNpmDisplayStub }}; globalThis._moduleCache[__agentOSLogFileModulePath] = {{ exports: __AgentOSNpmLogFileStub }};",
8476        display_module = serde_json::to_string(&guest_display_module)
8477            .unwrap_or_else(|_| format!("\"{guest_display_module}\"")),
8478        log_file_module = serde_json::to_string(&guest_log_file_module)
8479            .unwrap_or_else(|_| format!("\"{guest_log_file_module}\"")),
8480    );
8481    let registry_fetch_stub = "const { createRequire: __agentOSCreateRequire } = require('module'); const __agentOSNpmRequire = __agentOSCreateRequire(require.resolve(__AGENTOS_NPM_MAIN__)); try { const __agentOSMinipassFetchPath = __agentOSNpmRequire.resolve('minipass-fetch'); const __agentOSMinipassFetch = __agentOSNpmRequire(__agentOSMinipassFetchPath); const { FetchError: __agentOSFetchError, Headers: __agentOSFetchHeaders, Request: __agentOSFetchRequest, Response: __agentOSFetchResponse, AbortError: __agentOSAbortError } = __agentOSMinipassFetch; const { Minipass: __agentOSMinipass } = __agentOSNpmRequire('minipass'); const __agentOSCreateBinaryMinipass = () => new __agentOSMinipass({ objectMode: false, encoding: null }); const __agentOSCloneBuffer = (buffer) => Buffer.isBuffer(buffer) ? Buffer.from(buffer) : Buffer.from(buffer ?? []); const __agentOSBufferToArrayBuffer = (buffer) => { const bytes = __agentOSCloneBuffer(buffer); return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); }; const __agentOSAttachBufferedBodyMethods = (response, responseBuffer) => { const __agentOSReadBuffer = async () => __agentOSCloneBuffer(responseBuffer); response.__agentOSBufferedBody = __agentOSCloneBuffer(responseBuffer); response.buffer = __agentOSReadBuffer; response.text = async () => (await __agentOSReadBuffer()).toString('utf8'); response.json = async () => JSON.parse(await response.text()); response.arrayBuffer = async () => __agentOSBufferToArrayBuffer(await __agentOSReadBuffer()); response.clone = () => { const clonedBody = __agentOSCreateBinaryMinipass(); const clonedBuffer = __agentOSCloneBuffer(responseBuffer); clonedBody.end(clonedBuffer); const clonedResponse = new __agentOSFetchResponse(clonedBody, { url: response.url, status: response.status, statusText: response.statusText, headers: response.headers, size: response.size, timeout: response.timeout, counter: response.counter, trailer: response.trailer }); return __agentOSAttachBufferedBodyMethods(clonedResponse, clonedBuffer); }; return response; }; const __agentOSNormalizeHeaders = (__agentOSHeaders) => { const normalized = {}; __agentOSHeaders.forEach((value, key) => { if (normalized[key] === undefined) { normalized[key] = value; return; } if (Array.isArray(normalized[key])) { normalized[key].push(value); return; } normalized[key] = [normalized[key], value]; }); return normalized; }; const __agentOSPatchedMinipassFetch = async (input, opts = {}) => { const request = input instanceof __agentOSFetchRequest ? input : new __agentOSFetchRequest(input, opts); const __agentOSController = !request.signal && typeof AbortController === 'function' ? new AbortController() : null; const __agentOSSignal = request.signal ?? __agentOSController?.signal; let __agentOSTimer = null; if (__agentOSController && Number.isFinite(request.timeout) && request.timeout > 0) { __agentOSTimer = setTimeout(() => __agentOSController.abort(new Error(`network timeout at: ${request.url}`)), request.timeout); __agentOSTimer.unref?.(); } try { const requestHeaders = {}; request.headers.forEach((value, key) => { requestHeaders[key] = value; }); const response = await fetch(request.url, { method: request.method, headers: requestHeaders, body: request.body ?? undefined, redirect: request.redirect ?? opts.redirect ?? 'follow', signal: __agentOSSignal, ...(request.body ? { duplex: 'half' } : {}) }); const responseBody = __agentOSCreateBinaryMinipass(); const contentType = String(response.headers.get('content-type') || '').toLowerCase(); const responseBuffer = contentType.includes('json') ? Buffer.from(JSON.stringify(await response.json())) : contentType.startsWith('text/') ? Buffer.from(await response.text()) : Buffer.from(await response.arrayBuffer()); responseBody.end(responseBuffer); return __agentOSAttachBufferedBodyMethods(new __agentOSFetchResponse(responseBody, { url: response.url, status: response.status, statusText: response.statusText, headers: __agentOSNormalizeHeaders(response.headers), size: request.size, timeout: request.timeout, counter: request.counter ?? opts.counter ?? 0, trailer: Promise.resolve(new __agentOSFetchHeaders()) }), responseBuffer); } catch (error) { if (error instanceof Error) { throw error; } throw new __agentOSFetchError(String(error), 'system', error); } finally { if (__agentOSTimer) { clearTimeout(__agentOSTimer); } } }; globalThis.__agentOSPatchedMinipassFetch = __agentOSPatchedMinipassFetch; __agentOSPatchedMinipassFetch.isRedirect = typeof __agentOSMinipassFetch.isRedirect === 'function' ? __agentOSMinipassFetch.isRedirect.bind(__agentOSMinipassFetch) : (code) => code === 301 || code === 302 || code === 303 || code === 307 || code === 308; __agentOSPatchedMinipassFetch.FetchError = __agentOSFetchError; __agentOSPatchedMinipassFetch.Headers = __agentOSFetchHeaders; __agentOSPatchedMinipassFetch.Request = __agentOSFetchRequest; __agentOSPatchedMinipassFetch.Response = __agentOSFetchResponse; __agentOSPatchedMinipassFetch.AbortError = __agentOSAbortError; globalThis._moduleCache[__agentOSMinipassFetchPath] = { exports: __agentOSPatchedMinipassFetch }; __agentOSDebugLog('patched-minipass-fetch', __agentOSMinipassFetchPath); const __agentOSCheckResponsePath = __agentOSNpmRequire.resolve('npm-registry-fetch/lib/check-response.js'); const __agentOSCheckResponse = __agentOSNpmRequire(__agentOSCheckResponsePath); const __agentOSEnsureResponseBodyStream = (response) => { if (!response || (response.body && typeof response.body.on === 'function')) { return response; } const body = __agentOSCreateBinaryMinipass(); const finishWithError = (error) => body.emit('error', error instanceof Error ? error : new Error(String(error))); try { if (typeof response.buffer === 'function') { Promise.resolve(response.buffer()).then((buffer) => body.end(buffer), finishWithError); } else if (Buffer.isBuffer(response.body) || typeof response.body === 'string') { body.end(response.body); } else if (response.body && typeof response.body[Symbol.asyncIterator] === 'function') { (async () => { try { for await (const chunk of response.body) { body.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); } body.end(); } catch (error) { finishWithError(error); body.end(); } })(); } else { body.end(); } } catch (error) { finishWithError(error); body.end(); } return new __agentOSFetchResponse(body, response); }; globalThis._moduleCache[__agentOSCheckResponsePath] = { exports: (payload) => { const normalized = { ...payload, res: __agentOSEnsureResponseBodyStream(payload.res) }; __agentOSDebugLog('check-response-body', normalized.res && normalized.res.status, typeof (normalized.res && normalized.res.body), normalized.res && normalized.res.body && typeof normalized.res.body.on, normalized.res && normalized.res.body && normalized.res.body.constructor && normalized.res.body.constructor.name, !!(normalized.res && normalized.res.__agentOSBufferedBody), normalized.res && typeof normalized.res.json); return __agentOSCheckResponse(normalized); } }; __agentOSDebugLog('patched-check-response', __agentOSCheckResponsePath); } catch (error) { __agentOSDebugLog('patch-minipass-fetch-failed', error && error.stack ? error.stack : String(error)); } try { const __agentOSRegistryFetchPath = __agentOSNpmRequire.resolve('npm-registry-fetch'); const __agentOSRegistryFetch = __agentOSNpmRequire(__agentOSRegistryFetchPath); const __agentOSWrapRegistryFetch = (fn) => { const wrapResult = (promise) => Promise.resolve(promise).then((res) => { __agentOSDebugLog('registry-fetch-result', res && res.status, typeof (res && res.body), res && res.body && typeof res.body.on, res && res.body && res.body.constructor && res.body.constructor.name, !!(res && res.__agentOSBufferedBody), res && typeof res.json); return res; }); const wrapped = (uri, opts = {}) => wrapResult(globalThis.__agentOSPatchedMinipassFetch(uri, { method: opts.method, headers: opts.headers, body: opts.body, redirect: opts.redirect, signal: opts.signal, timeout: opts.timeout, size: opts.size, counter: opts.counter })); if (typeof fn.json === 'function') { wrapped.json = (uri, opts = {}) => wrapped(uri, opts).then((res) => res.json()); } if (fn.json && typeof fn.json.stream === 'function') { wrapped.json = wrapped.json || {}; wrapped.json.stream = (uri, path, opts = {}) => fn.json.stream(uri, path, { ...opts, agent: false }); } if (typeof fn.pickRegistry === 'function') { wrapped.pickRegistry = fn.pickRegistry.bind(fn); } if (typeof fn.getAuth === 'function') { wrapped.getAuth = fn.getAuth.bind(fn); } return wrapped; }; globalThis._moduleCache[__agentOSRegistryFetchPath] = { exports: __agentOSWrapRegistryFetch(__agentOSRegistryFetch) }; __agentOSDebugLog('patched-npm-registry-fetch', __agentOSRegistryFetchPath); } catch (error) { __agentOSDebugLog('patch-npm-registry-fetch-failed', error && error.stack ? error.stack : String(error)); }";
8482    match cli.command_name.as_str() {
8483        "npx" => format!(
8484            "{debug_preamble} {display_stub} {registry_fetch_stub} process.argv[1] = require.resolve({npm_cli}); process.argv.splice(2, 0, 'exec'); __agentOSDebugLog('argv', JSON.stringify(process.argv), 'cwd', process.cwd()); (async () => {{ const pkg = require({package_json}); if (process.argv.includes('--version') || process.argv.includes('-v')) {{ __agentOSDebugLog('version-shortcut'); console.log(pkg.version); __agentOSFinish(0); return; }} const Npm = require({npm_main}); const npm = new Npm(); __agentOSDebugLog('before-load'); const loaded = await npm.load(); __agentOSDebugLog('after-load', loaded && loaded.command, JSON.stringify(loaded && loaded.args)); if (!loaded.exec) {{ __agentOSDebugLog('no-exec'); __agentOSFinish(); return; }} if (!loaded.command) {{ __agentOSDebugLog('no-command'); const {{ output }} = require('proc-log'); output.standard(npm.usage); __agentOSFinish(1); return; }} __agentOSDebugLog('before-exec', loaded.command, JSON.stringify(loaded.args)); await npm.exec(loaded.command, loaded.args); __agentOSDebugLog('after-exec', __agentOSResolveExitCode()); __agentOSFinish(); }})().catch((error) => {{ if (__agentOSIsProcessExitError(error)) {{ __agentOSDebugLog('process-exit-error', __agentOSResolveExitCode(error.code)); __agentOSFinish(error.code); return; }} console.error(error && error.stack ? error.stack : String(error)); __agentOSFinish(error && typeof error === 'object' && Number.isFinite(error.exitCode) ? error.exitCode : 1); }});",
8485            debug_preamble = debug_preamble,
8486            display_stub = display_stub,
8487            registry_fetch_stub = registry_fetch_stub.replace(
8488                "__AGENTOS_NPM_MAIN__",
8489                &serde_json::to_string(&guest_npm_main)
8490                    .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8491            ),
8492            npm_main = serde_json::to_string(&guest_npm_main)
8493                .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8494            npm_cli = serde_json::to_string(&guest_npm_cli)
8495                .unwrap_or_else(|_| format!("\"{guest_npm_cli}\"")),
8496            package_json = serde_json::to_string(&guest_package_json)
8497                .unwrap_or_else(|_| format!("\"{guest_package_json}\"")),
8498        ),
8499        _ => format!(
8500            "{debug_preamble} {display_stub} {registry_fetch_stub} __agentOSDebugLog('argv', JSON.stringify(process.argv), 'cwd', process.cwd()); (async () => {{ const pkg = require({package_json}); if (process.argv.includes('--version') || process.argv.includes('-v')) {{ __agentOSDebugLog('version-shortcut'); console.log(pkg.version); __agentOSFinish(0); return; }} const Npm = require({npm_main}); const npm = new Npm(); __agentOSDebugLog('before-load'); const loaded = await npm.load(); __agentOSDebugLog('after-load', loaded && loaded.command, JSON.stringify(loaded && loaded.args)); if (!loaded.exec) {{ __agentOSDebugLog('no-exec'); __agentOSFinish(); return; }} if (!loaded.command) {{ __agentOSDebugLog('no-command'); const {{ output }} = require('proc-log'); output.standard(npm.usage); __agentOSFinish(1); return; }} __agentOSDebugLog('before-exec', loaded.command, JSON.stringify(loaded.args)); await npm.exec(loaded.command, loaded.args); __agentOSDebugLog('after-exec', __agentOSResolveExitCode()); __agentOSFinish(); }})().catch((error) => {{ if (__agentOSIsProcessExitError(error)) {{ __agentOSDebugLog('process-exit-error', __agentOSResolveExitCode(error.code)); __agentOSFinish(error.code); return; }} console.error(error && error.stack ? error.stack : String(error)); __agentOSFinish(error && typeof error === 'object' && Number.isFinite(error.exitCode) ? error.exitCode : 1); }});",
8501            debug_preamble = debug_preamble,
8502            display_stub = display_stub,
8503            registry_fetch_stub = registry_fetch_stub.replace(
8504                "__AGENTOS_NPM_MAIN__",
8505                &serde_json::to_string(&guest_npm_main)
8506                    .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8507            ),
8508            npm_main = serde_json::to_string(&guest_npm_main)
8509                .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8510            package_json = serde_json::to_string(&guest_package_json)
8511                .unwrap_or_else(|_| format!("\"{guest_package_json}\"")),
8512        ),
8513    }
8514}
8515
8516fn resolve_guest_command_entrypoint(
8517    vm: &VmState,
8518    guest_cwd: &str,
8519    command: &str,
8520    path_env: Option<&str>,
8521) -> Option<String> {
8522    if !is_path_like_specifier(command) {
8523        if let Some(entrypoint) = vm.command_guest_paths.get(command) {
8524            return Some(entrypoint.clone());
8525        }
8526
8527        for search_dir in guest_command_search_dirs(vm, guest_cwd, path_env) {
8528            let candidate = normalize_path(&format!("{search_dir}/{command}"));
8529            if let Some(entrypoint) = resolve_guest_command_path_candidate(vm, &candidate) {
8530                return Some(entrypoint);
8531            }
8532        }
8533
8534        return None;
8535    }
8536
8537    let normalized = resolve_path_like_guest_specifier(guest_cwd, command);
8538    resolve_guest_command_path_candidate(vm, &normalized).or_else(|| {
8539        // Some guest shells materialize PATH lookups into absolute candidate paths.
8540        // If that path points into a searched directory but does not exist, fall
8541        // back to the command basename so the sidecar can remap VM command packages.
8542        let parent_dir = Path::new(&normalized).parent()?.to_str()?;
8543        if !guest_command_search_dirs(vm, guest_cwd, path_env)
8544            .iter()
8545            .any(|search_dir| normalize_path(search_dir) == normalize_path(parent_dir))
8546        {
8547            return None;
8548        }
8549
8550        let file_name = Path::new(&normalized).file_name()?.to_str()?;
8551        vm.command_guest_paths.get(file_name).cloned()
8552    })
8553}
8554
8555fn guest_command_search_dirs(vm: &VmState, guest_cwd: &str, path_env: Option<&str>) -> Vec<String> {
8556    let mut search_dirs = Vec::new();
8557    let mut seen = BTreeSet::new();
8558
8559    if let Some(path) = path_env.or_else(|| vm.guest_env.get("PATH").map(String::as_str)) {
8560        for segment in path.split(':') {
8561            let trimmed = segment.trim();
8562            if trimmed.is_empty() {
8563                continue;
8564            }
8565            let normalized = if trimmed.starts_with('/') {
8566                normalize_path(trimmed)
8567            } else {
8568                normalize_path(&format!("{guest_cwd}/{trimmed}"))
8569            };
8570            if seen.insert(normalized.clone()) {
8571                search_dirs.push(normalized);
8572            }
8573        }
8574    }
8575
8576    for fallback in ["/bin", "/usr/bin", "/usr/local/bin"] {
8577        let normalized = String::from(fallback);
8578        if seen.insert(normalized.clone()) {
8579            search_dirs.push(normalized);
8580        }
8581    }
8582
8583    search_dirs
8584}
8585
8586fn resolve_guest_command_path_candidate(vm: &VmState, candidate: &str) -> Option<String> {
8587    if candidate.starts_with("/bin/")
8588        || candidate.starts_with("/usr/bin/")
8589        || candidate.starts_with("/usr/local/bin/")
8590        || candidate.starts_with("/__secure_exec/commands/")
8591    {
8592        if let Some(file_name) = Path::new(candidate)
8593            .file_name()
8594            .and_then(|name| name.to_str())
8595        {
8596            if let Some(guest_entrypoint) = vm.command_guest_paths.get(file_name) {
8597                return Some(guest_entrypoint.clone());
8598            }
8599        }
8600    }
8601
8602    if vm
8603        .kernel
8604        .exists(candidate)
8605        .ok()
8606        .is_some_and(|exists| exists)
8607    {
8608        return Some(normalize_path(candidate));
8609    }
8610
8611    resolve_vm_guest_path_to_host(vm, candidate)
8612        .is_file()
8613        .then(|| normalize_path(candidate))
8614}
8615
8616fn resolve_host_entrypoint_within_vm_host_cwd(
8617    vm: &VmState,
8618    specifier: &str,
8619) -> Option<(String, String)> {
8620    let candidate = Path::new(specifier);
8621    if !candidate.is_absolute() {
8622        return None;
8623    }
8624
8625    let normalized_entrypoint = normalize_host_path(candidate);
8626    let normalized_host_cwd = normalize_host_path(&vm.host_cwd);
8627    if !path_is_within_root(&normalized_entrypoint, &normalized_host_cwd) {
8628        return None;
8629    }
8630
8631    let relative = normalized_entrypoint
8632        .strip_prefix(&normalized_host_cwd)
8633        .ok()?
8634        .to_string_lossy()
8635        .replace('\\', "/");
8636    let guest_entrypoint = if relative.is_empty() {
8637        String::from("/")
8638    } else {
8639        normalize_path(&format!("/{relative}"))
8640    };
8641    Some((
8642        guest_entrypoint,
8643        normalized_entrypoint.to_string_lossy().into_owned(),
8644    ))
8645}
8646
8647fn prepare_guest_runtime_env(
8648    vm: &VmState,
8649    env: &mut BTreeMap<String, String>,
8650    guest_cwd: &str,
8651    host_cwd: &Path,
8652    guest_entrypoint: Option<String>,
8653) -> Result<(), SidecarError> {
8654    let user = vm.kernel.user_profile();
8655    let path_mappings = runtime_guest_path_mappings(vm);
8656    let read_paths = expand_host_access_paths(
8657        std::iter::once(vm.cwd.clone())
8658            .chain(
8659                path_mappings
8660                    .iter()
8661                    .map(|mapping| PathBuf::from(&mapping.host_path)),
8662            )
8663            .chain(std::iter::once(host_cwd.to_path_buf()))
8664            .collect::<Vec<_>>()
8665            .as_slice(),
8666    );
8667    let write_paths = dedupe_host_paths(
8668        std::iter::once(vm.cwd.clone())
8669            .chain(std::iter::once(host_cwd.to_path_buf()))
8670            .chain(runtime_guest_writable_host_paths(vm))
8671            .collect::<Vec<_>>()
8672            .as_slice(),
8673    );
8674    let allowed_node_builtins = configured_allowed_node_builtins(vm);
8675    let loopback_exempt_ports = configured_loopback_exempt_ports(vm);
8676
8677    env.insert(
8678        String::from("AGENTOS_GUEST_PATH_MAPPINGS"),
8679        serde_json::to_string(&path_mappings).map_err(|error| {
8680            SidecarError::InvalidState(format!("failed to encode guest path mappings: {error}"))
8681        })?,
8682    );
8683    env.entry(String::from(EXECUTION_SANDBOX_ROOT_ENV))
8684        .or_insert_with(|| normalize_host_path(&vm.cwd).to_string_lossy().into_owned());
8685    env.insert(
8686        String::from("AGENTOS_EXTRA_FS_READ_PATHS"),
8687        serde_json::to_string(
8688            &read_paths
8689                .iter()
8690                .map(|path| path.to_string_lossy().into_owned())
8691                .collect::<Vec<_>>(),
8692        )
8693        .map_err(|error| {
8694            SidecarError::InvalidState(format!("failed to encode read paths: {error}"))
8695        })?,
8696    );
8697    env.insert(
8698        String::from("AGENTOS_EXTRA_FS_WRITE_PATHS"),
8699        serde_json::to_string(
8700            &write_paths
8701                .iter()
8702                .map(|path| path.to_string_lossy().into_owned())
8703                .collect::<Vec<_>>(),
8704        )
8705        .map_err(|error| {
8706            SidecarError::InvalidState(format!("failed to encode write paths: {error}"))
8707        })?,
8708    );
8709    env.insert(
8710        String::from("AGENTOS_ALLOWED_NODE_BUILTINS"),
8711        serde_json::to_string(&allowed_node_builtins).map_err(|error| {
8712            SidecarError::InvalidState(format!("failed to encode allowed builtins: {error}"))
8713        })?,
8714    );
8715    // The guest JS host platform drives subtractive global scrubbing in the
8716    // per-execution runtime shim (see prepend_v8_runtime_shim).
8717    env.insert(
8718        String::from("AGENTOS_JS_PLATFORM"),
8719        js_runtime_platform_env(vm).to_owned(),
8720    );
8721    // Module-resolution mode (omitted when full Node resolution / the default).
8722    if let Some(resolution) = js_runtime_module_resolution_env(vm) {
8723        env.insert(
8724            String::from("AGENTOS_JS_MODULE_RESOLUTION"),
8725            resolution.to_owned(),
8726        );
8727    }
8728    // Builtin allow-list gate for the live resolver. Present only when builtins
8729    // should be restricted (non-node platform => deny all; node + explicit
8730    // allow-list => exactly those). Absent => unrestricted (node default).
8731    if let Some(allowlist) = js_runtime_enforced_builtins(vm) {
8732        env.insert(
8733            String::from("AGENTOS_JS_BUILTIN_ALLOWLIST"),
8734            serde_json::to_string(&allowlist).map_err(|error| {
8735                SidecarError::InvalidState(format!(
8736                    "failed to encode jsRuntime builtin allow-list: {error}"
8737                ))
8738            })?,
8739        );
8740    }
8741    // Virtual OS identity (os.cpus/totalmem/freemem/homedir/userInfo/...) now
8742    // rides the typed `guest_runtime` (see `guest_runtime_identity`), exposed to
8743    // the guest as the `__agentOSVirtualOs` structured global by the runtime
8744    // shim — no longer the `AGENTOS_VIRTUAL_OS_*` env vars.
8745    // Virtual process uid/gid now ride the typed `guest_runtime` identity
8746    // (see `guest_runtime_identity`), not the `AGENTOS_VIRTUAL_PROCESS_*` env.
8747    env.entry(String::from("HOME"))
8748        .or_insert_with(|| user.homedir.clone());
8749    env.entry(String::from("USER"))
8750        .or_insert_with(|| user.username.clone());
8751    env.entry(String::from("LOGNAME"))
8752        .or_insert_with(|| user.username.clone());
8753    env.entry(String::from("SHELL"))
8754        .or_insert_with(|| user.shell.clone());
8755    env.entry(String::from("PATH")).or_insert_with(|| {
8756        vm.guest_env
8757            .get("PATH")
8758            .cloned()
8759            .unwrap_or_else(|| crate::vm::DEFAULT_GUEST_PATH_ENV.to_owned())
8760    });
8761    env.entry(String::from("TMPDIR"))
8762        .or_insert_with(|| String::from("/tmp"));
8763    env.insert(String::from("PWD"), guest_cwd.to_owned());
8764    if !loopback_exempt_ports.is_empty() {
8765        env.insert(
8766            String::from(LOOPBACK_EXEMPT_PORTS_ENV),
8767            serde_json::to_string(&loopback_exempt_ports).map_err(|error| {
8768                SidecarError::InvalidState(format!("failed to encode loopback exemptions: {error}"))
8769            })?,
8770        );
8771    }
8772    if let Some(guest_entrypoint) = guest_entrypoint {
8773        env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint);
8774    }
8775    Ok(())
8776}
8777
8778fn virtual_os_cpu_count(resource_limits: &ResourceLimits) -> usize {
8779    resource_limits.virtual_cpu_count.unwrap_or(1).max(1)
8780}
8781
8782fn virtual_os_totalmem_bytes(resource_limits: &ResourceLimits) -> u64 {
8783    resource_limits
8784        .max_wasm_memory_bytes
8785        .unwrap_or(1024 * 1024 * 1024)
8786}
8787
8788fn virtual_os_freemem_bytes(resource_limits: &ResourceLimits) -> u64 {
8789    resource_limits
8790        .max_wasm_memory_bytes
8791        .unwrap_or(512 * 1024 * 1024)
8792}
8793
8794/// Build the typed per-execution JavaScript limits from the per-VM `VmLimits`
8795/// (sourced from `CreateVmConfig` on the BARE wire). These ride the execution
8796/// request, not `AGENTOS_*` env vars — see the env-vs-wire rule in
8797/// `crates/sidecar/CLAUDE.md`.
8798fn javascript_execution_limits(vm: &VmState) -> JavascriptExecutionLimits {
8799    JavascriptExecutionLimits {
8800        v8_heap_limit_mb: vm.limits.js_runtime.v8_heap_limit_mb,
8801        sync_rpc_wait_timeout_ms: vm.limits.js_runtime.sync_rpc_wait_timeout_ms,
8802    }
8803}
8804
8805/// Build the typed per-execution guest-runtime identity (virtual `process.*`)
8806/// from kernel state. Replaces the `AGENTOS_VIRTUAL_PROCESS_{UID,GID,PID,PPID}`
8807/// env round-trip: the runtime shim reads these from `guest_runtime`, not env.
8808/// `uid`/`gid` come from the VM user profile (applied to every guest);
8809/// `pid`/`ppid` are per-process and only set for paths that assigned them.
8810fn guest_runtime_identity(
8811    vm: &VmState,
8812    virtual_pid: Option<u64>,
8813    virtual_ppid: Option<u64>,
8814) -> GuestRuntimeConfig {
8815    let user = vm.kernel.user_profile();
8816    let resource_limits = vm.kernel.resource_limits();
8817    GuestRuntimeConfig {
8818        virtual_uid: Some(u64::from(user.uid)),
8819        virtual_gid: Some(u64::from(user.gid)),
8820        virtual_pid,
8821        virtual_ppid,
8822        virtual_exec_path: None,
8823        os_cpu_count: Some(virtual_os_cpu_count(resource_limits) as u64),
8824        os_totalmem: Some(virtual_os_totalmem_bytes(resource_limits)),
8825        os_freemem: Some(virtual_os_freemem_bytes(resource_limits)),
8826        os_homedir: Some(user.homedir.clone()),
8827        os_hostname: None,
8828        os_shell: Some(user.shell.clone()),
8829        os_user: Some(user.username.clone()),
8830        // Userland bundle to bake into the per-sidecar snapshot, supplied by the
8831        // (trusted) client via jsRuntime.snapshotUserlandCode. The agent-os layer
8832        // sets this to the agent SDK bundle for snapshot-enabled agents; `None`
8833        // keeps the bridge-only snapshot.
8834        snapshot_userland_code: vm
8835            .configuration
8836            .js_runtime
8837            .as_ref()
8838            .and_then(|cfg| cfg.snapshot_userland_code.clone()),
8839    }
8840}
8841
8842/// The guest's virtual home directory, sourced from the VM user profile (the
8843/// same value carried to the guest as `os.homedir()` via `guest_runtime`). Used
8844/// by sidecar-internal `~`-path resolution; falls back to `/root` for a
8845/// non-absolute profile value.
8846fn guest_virtual_home(vm: &VmState) -> String {
8847    let homedir = vm.kernel.user_profile().homedir;
8848    if homedir.starts_with('/') {
8849        homedir
8850    } else {
8851        String::from("/root")
8852    }
8853}
8854
8855/// Build the typed per-execution Python limits from the per-VM `VmLimits`.
8856fn python_execution_limits(vm: &VmState) -> PythonExecutionLimits {
8857    PythonExecutionLimits {
8858        output_buffer_max_bytes: Some(vm.limits.python.output_buffer_max_bytes),
8859        execution_timeout_ms: Some(vm.limits.python.execution_timeout_ms),
8860        max_old_space_mb: Some(vm.limits.python.max_old_space_mb),
8861        vfs_rpc_timeout_ms: Some(vm.limits.python.vfs_rpc_timeout_ms),
8862    }
8863}
8864
8865/// Build the typed per-execution WebAssembly limits from the per-VM kernel
8866/// `ResourceLimits`. Replaces the old `apply_wasm_limit_env` env round-trip;
8867/// notably this is the path that finally enforces the stack cap that the
8868/// `AGENTOS_WASM_MAX_STACK_BYTES` env knob set but no reader consumed.
8869fn wasm_execution_limits(vm: &VmState) -> WasmExecutionLimits {
8870    let resource_limits = vm.kernel.resource_limits();
8871    WasmExecutionLimits {
8872        max_fuel: resource_limits.max_wasm_fuel,
8873        max_memory_bytes: resource_limits.max_wasm_memory_bytes,
8874        max_stack_bytes: resource_limits
8875            .max_wasm_stack_bytes
8876            .map(|value| value as u64),
8877    }
8878}
8879
8880/// The guest JavaScript host platform configured for this VM, defaulting to
8881/// full Node.js emulation when no `jsRuntime` config was supplied at create.
8882fn js_runtime_platform(vm: &VmState) -> vm_config::JsRuntimePlatform {
8883    vm.configuration
8884        .js_runtime
8885        .as_ref()
8886        .map(|cfg| cfg.platform)
8887        .unwrap_or(vm_config::JsRuntimePlatform::Node)
8888}
8889
8890/// Lowercase wire name for the configured platform, mirroring the serde
8891/// representation of `vm_config::JsRuntimePlatform`.
8892fn js_runtime_platform_env(vm: &VmState) -> &'static str {
8893    match js_runtime_platform(vm) {
8894        vm_config::JsRuntimePlatform::Node => "node",
8895        vm_config::JsRuntimePlatform::Browser => "browser",
8896        vm_config::JsRuntimePlatform::Neutral => "neutral",
8897        vm_config::JsRuntimePlatform::Bare => "bare",
8898    }
8899}
8900
8901/// Wire name for the configured module-resolution mode, or `None` when it is the
8902/// full-Node default (which the live resolver also assumes when the env is unset).
8903fn js_runtime_module_resolution_env(vm: &VmState) -> Option<&'static str> {
8904    let resolution = vm
8905        .configuration
8906        .js_runtime
8907        .as_ref()
8908        .map(|cfg| cfg.module_resolution)
8909        .unwrap_or(vm_config::JsModuleResolution::Node);
8910    match resolution {
8911        vm_config::JsModuleResolution::Node => None,
8912        vm_config::JsModuleResolution::Relative => Some("relative"),
8913        vm_config::JsModuleResolution::None => Some("none"),
8914    }
8915}
8916
8917/// The builtin allow-list the live resolver should enforce, or `None` to leave
8918/// builtins unrestricted (full Node default — preserving today's behavior).
8919/// Non-node platforms enforce an empty list (deny all builtins).
8920fn js_runtime_enforced_builtins(vm: &VmState) -> Option<Vec<String>> {
8921    if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node {
8922        return Some(Vec::new());
8923    }
8924    vm.configuration
8925        .js_runtime
8926        .as_ref()
8927        .and_then(|cfg| cfg.allowed_builtins.clone())
8928}
8929
8930fn configured_allowed_node_builtins(vm: &VmState) -> Vec<String> {
8931    // Non-node platforms expose no Node builtin modules at all.
8932    if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node {
8933        return Vec::new();
8934    }
8935    // Under the node platform an explicit allow-list wins — including an explicit
8936    // empty list, which means deny all. Absence falls back to the engine default.
8937    let configured = match vm
8938        .configuration
8939        .js_runtime
8940        .as_ref()
8941        .and_then(|cfg| cfg.allowed_builtins.as_ref())
8942    {
8943        Some(list) => list.clone(),
8944        None => DEFAULT_ALLOWED_NODE_BUILTINS
8945            .iter()
8946            .map(|value| (*value).to_owned())
8947            .collect::<Vec<_>>(),
8948    };
8949    dedupe_strings(&configured)
8950}
8951
8952fn configured_loopback_exempt_ports(vm: &VmState) -> Vec<String> {
8953    if !vm.configuration.loopback_exempt_ports.is_empty() {
8954        return vm
8955            .configuration
8956            .loopback_exempt_ports
8957            .iter()
8958            .map(ToString::to_string)
8959            .collect();
8960    }
8961
8962    vm.create_loopback_exempt_ports
8963        .iter()
8964        .map(ToString::to_string)
8965        .collect()
8966}
8967
8968/// Extract the `hostPath` string from a mount plugin's JSON-encoded config.
8969fn mount_config_host_path(config: &str) -> Option<String> {
8970    serde_json::from_str::<Value>(config)
8971        .ok()?
8972        .get("hostPath")
8973        .and_then(Value::as_str)
8974        .map(str::to_owned)
8975}
8976
8977fn runtime_guest_writable_host_paths(vm: &VmState) -> Vec<PathBuf> {
8978    vm.configuration
8979        .mounts
8980        .iter()
8981        .filter(|mount| !mount.read_only)
8982        .filter_map(|mount| {
8983            ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
8984                .then(|| mount_config_host_path(&mount.plugin.config))
8985                .flatten()
8986                .map(PathBuf::from)
8987        })
8988        .collect()
8989}
8990
8991fn runtime_guest_path_mappings(vm: &VmState) -> Vec<RuntimeGuestPathMapping> {
8992    let mut mappings = vm
8993        .configuration
8994        .mounts
8995        .iter()
8996        .filter_map(|mount| {
8997            ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
8998                .then(|| {
8999                    mount_config_host_path(&mount.plugin.config).map(|host_path| {
9000                        RuntimeGuestPathMapping {
9001                            guest_path: normalize_path(&mount.guest_path),
9002                            host_path,
9003                            read_only: mount.read_only,
9004                        }
9005                    })
9006                })
9007                .flatten()
9008        })
9009        .collect::<Vec<_>>();
9010    let mut command_root_mappings = vm
9011        .command_guest_paths
9012        .values()
9013        .filter_map(|guest_path| {
9014            Path::new(guest_path)
9015                .parent()
9016                .and_then(|parent| parent.to_str())
9017                .map(normalize_path)
9018        })
9019        .collect::<BTreeSet<_>>()
9020        .into_iter()
9021        .map(|guest_path| RuntimeGuestPathMapping {
9022            host_path: resolve_vm_guest_path_to_host(vm, &guest_path)
9023                .to_string_lossy()
9024                .into_owned(),
9025            guest_path,
9026            read_only: false,
9027        })
9028        .collect::<Vec<_>>();
9029    mappings.append(&mut command_root_mappings);
9030    let mut extra_node_modules_roots = mappings
9031        .iter()
9032        .filter(|mapping| mapping.guest_path.starts_with("/root/node_modules/"))
9033        .filter_map(|mapping| {
9034            host_node_modules_root(Path::new(&mapping.host_path)).map(|host_root| {
9035                RuntimeGuestPathMapping {
9036                    guest_path: String::from("/root/node_modules"),
9037                    host_path: host_root.to_string_lossy().into_owned(),
9038                    read_only: mapping.read_only,
9039                }
9040            })
9041        })
9042        .collect::<Vec<_>>();
9043    mappings.append(&mut extra_node_modules_roots);
9044    mappings.push(RuntimeGuestPathMapping {
9045        guest_path: String::from("/"),
9046        host_path: vm.cwd.to_string_lossy().into_owned(),
9047        read_only: false,
9048    });
9049    mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.guest_path.len()));
9050    mappings.dedup_by(|left, right| {
9051        left.guest_path == right.guest_path && left.host_path == right.host_path
9052    });
9053    mappings
9054}
9055
9056/// Build a `Send`-able, read-only VFS module reader over the VM's read-only
9057/// `host_dir`/`module_access` mounts (and the derived `/root/node_modules` root
9058/// for nested mounts). When present, the V8 bridge thread resolves modules
9059/// inline against this reader — concurrently with the service loop — so a large
9060/// cold-start module graph never serializes behind / starves an in-flight ACP
9061/// `session/new` bootstrap on the single service-loop thread. The reader reads
9062/// the same mounted tree the guest sees (anchored `openat2`, escaping-symlink
9063/// refusal), never the host-direct path translator. Returns `None` when the VM
9064/// has no usable read-only mount, so resolution falls back to the service-loop
9065/// kernel reader.
9066fn build_module_reader(
9067    vm: &VmState,
9068    resolved: &ResolvedChildProcessExecution,
9069) -> Option<crate::plugins::host_dir::HostDirModuleReader> {
9070    let mut pairs: Vec<(String, PathBuf)> = vm
9071        .configuration
9072        .mounts
9073        .iter()
9074        .filter(|mount| mount.read_only)
9075        .filter(|mount| (mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
9076        .filter_map(|mount| {
9077            mount_config_host_path(&mount.plugin.config)
9078                .map(|host_path| (normalize_path(&mount.guest_path), PathBuf::from(host_path)))
9079        })
9080        .collect();
9081
9082    let guest_entrypoint = resolved
9083        .env
9084        .get("AGENTOS_GUEST_ENTRYPOINT")
9085        .map(|path| normalize_path(path));
9086    if let Some(guest_entrypoint) = guest_entrypoint.as_deref() {
9087        let entrypoint_in_read_only_mount = pairs.iter().any(|(guest_path, _)| {
9088            guest_entrypoint == guest_path
9089                || guest_entrypoint.starts_with(&format!("{guest_path}/"))
9090        });
9091        if !entrypoint_in_read_only_mount {
9092            return None;
9093        }
9094    }
9095
9096    // Mirror runtime_guest_path_mappings: a mount nested under
9097    // `/root/node_modules/<pkg>` implies a `/root/node_modules` root the resolver
9098    // walks, so expose that root too (e.g. software-package mounts).
9099    let extra_roots: Vec<(String, PathBuf)> = pairs
9100        .iter()
9101        .filter(|(guest_path, _)| guest_path.starts_with("/root/node_modules/"))
9102        .filter_map(|(_, host_path)| {
9103            host_node_modules_root(host_path).map(|root| (String::from("/root/node_modules"), root))
9104        })
9105        .collect();
9106    pairs.extend(extra_roots);
9107
9108    crate::plugins::host_dir::HostDirModuleReader::from_mounts(pairs)
9109}
9110
9111fn host_node_modules_root(path: &Path) -> Option<PathBuf> {
9112    if let Some(root) = path
9113        .ancestors()
9114        .filter(|candidate| {
9115            candidate.file_name().and_then(|name| name.to_str()) == Some("node_modules")
9116        })
9117        .last()
9118        .map(Path::to_path_buf)
9119    {
9120        return Some(root);
9121    }
9122
9123    fs::canonicalize(path)
9124        .ok()?
9125        .ancestors()
9126        .filter(|candidate| {
9127            candidate.file_name().and_then(|name| name.to_str()) == Some("node_modules")
9128        })
9129        .last()
9130        .map(Path::to_path_buf)
9131}
9132
9133#[cfg(test)]
9134mod runtime_guest_path_mapping_tests {
9135    use super::{host_node_modules_root, javascript_sync_rpc_option_bool};
9136    use serde_json::json;
9137    use std::fs;
9138    use std::time::{SystemTime, UNIX_EPOCH};
9139
9140    #[test]
9141    fn host_node_modules_root_prefers_workspace_root_over_pnpm_package_node_modules() {
9142        let unique = SystemTime::now()
9143            .duration_since(UNIX_EPOCH)
9144            .expect("clock should be monotonic")
9145            .as_nanos();
9146        let temp = std::env::temp_dir().join(format!("secure-exec-sidecar-node-modules-{unique}"));
9147        let workspace_node_modules = temp.join("node_modules");
9148        let package_root = workspace_node_modules
9149            .join(".pnpm")
9150            .join("example@1.0.0")
9151            .join("node_modules")
9152            .join("@scope")
9153            .join("pkg");
9154        fs::create_dir_all(&package_root).expect("package root should be created");
9155
9156        let resolved =
9157            host_node_modules_root(&package_root).expect("node_modules root should resolve");
9158
9159        assert_eq!(resolved, workspace_node_modules);
9160
9161        fs::remove_dir_all(&temp).expect("temp tree should be removed");
9162    }
9163
9164    #[test]
9165    fn host_node_modules_root_preserves_symlinked_workspace_node_modules_path() {
9166        let unique = SystemTime::now()
9167            .duration_since(UNIX_EPOCH)
9168            .expect("clock should be monotonic")
9169            .as_nanos();
9170        let temp =
9171            std::env::temp_dir().join(format!("secure-exec-sidecar-node-modules-symlink-{unique}"));
9172        let workspace_node_modules = temp.join("node_modules");
9173        let package_link = workspace_node_modules.join("@scope").join("pkg");
9174        let real_package = temp.join("registry").join("agent").join("pkg");
9175        fs::create_dir_all(package_link.parent().expect("package parent should exist"))
9176            .expect("scoped parent should be created");
9177        fs::create_dir_all(&real_package).expect("real package root should be created");
9178        std::os::unix::fs::symlink(&real_package, &package_link)
9179            .expect("package symlink should be created");
9180
9181        let resolved =
9182            host_node_modules_root(&package_link).expect("node_modules root should resolve");
9183
9184        assert_eq!(resolved, workspace_node_modules);
9185
9186        fs::remove_dir_all(&temp).expect("temp tree should be removed");
9187    }
9188
9189    #[test]
9190    fn javascript_sync_rpc_option_bool_accepts_boolean_recursive_argument() {
9191        assert_eq!(
9192            javascript_sync_rpc_option_bool(&[json!("/workspace"), json!(true)], 1, "recursive"),
9193            Some(true)
9194        );
9195        assert_eq!(
9196            javascript_sync_rpc_option_bool(
9197                &[json!("/workspace"), json!({ "recursive": false })],
9198                1,
9199                "recursive"
9200            ),
9201            Some(false)
9202        );
9203    }
9204}
9205
9206#[cfg(test)]
9207mod kernel_poll_sync_rpc_tests {
9208    use super::{
9209        service_javascript_kernel_poll_sync_rpc, ActiveExecution, ActiveProcess,
9210        JavascriptSyncRpcRequest, KernelPollFdResponse, SidecarKernel, ToolExecution,
9211        EXECUTION_DRIVER_NAME, JAVASCRIPT_COMMAND,
9212    };
9213    use secure_exec_kernel::command_registry::CommandDriver;
9214    use secure_exec_kernel::kernel::{KernelVmConfig, SpawnOptions};
9215    use secure_exec_kernel::mount_table::MountTable;
9216    use secure_exec_kernel::permissions::Permissions;
9217    use secure_exec_kernel::poll::{POLLHUP, POLLIN};
9218    use secure_exec_kernel::vfs::MemoryFileSystem;
9219    use serde_json::{json, Value};
9220    #[test]
9221    fn javascript_kernel_poll_sync_rpc_reports_multiple_kernel_fds() {
9222        let mut config = KernelVmConfig::new("vm-js-kernel-poll");
9223        config.permissions = Permissions::allow_all();
9224        let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config);
9225        kernel
9226            .register_driver(CommandDriver::new(
9227                EXECUTION_DRIVER_NAME,
9228                [JAVASCRIPT_COMMAND],
9229            ))
9230            .expect("register execution driver");
9231
9232        let kernel_handle = kernel
9233            .spawn_process(
9234                JAVASCRIPT_COMMAND,
9235                Vec::new(),
9236                SpawnOptions {
9237                    requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
9238                    ..SpawnOptions::default()
9239                },
9240            )
9241            .expect("spawn javascript kernel process");
9242        let pid = kernel_handle.pid();
9243
9244        let (stdin_read_fd, stdin_write_fd) = kernel
9245            .open_pipe(EXECUTION_DRIVER_NAME, pid)
9246            .expect("open kernel stdin pipe");
9247        kernel
9248            .fd_dup2(EXECUTION_DRIVER_NAME, pid, stdin_read_fd, 0)
9249            .expect("dup stdin pipe onto fd 0");
9250        kernel
9251            .fd_close(EXECUTION_DRIVER_NAME, pid, stdin_read_fd)
9252            .expect("close original stdin read fd");
9253
9254        let process = ActiveProcess::new(
9255            pid,
9256            kernel_handle,
9257            super::GuestRuntimeKind::JavaScript,
9258            ActiveExecution::Tool(ToolExecution::default()),
9259        );
9260
9261        kernel
9262            .fd_write(EXECUTION_DRIVER_NAME, pid, stdin_write_fd, b"poll-ready")
9263            .expect("write kernel stdin payload");
9264        kernel
9265            .fd_close(EXECUTION_DRIVER_NAME, pid, stdin_write_fd)
9266            .expect("close kernel stdin writer");
9267
9268        let response = service_javascript_kernel_poll_sync_rpc(
9269            &mut kernel,
9270            &process,
9271            &JavascriptSyncRpcRequest {
9272                id: 1,
9273                method: String::from("__kernel_poll"),
9274                args: vec![
9275                    json!([
9276                        { "fd": 0, "events": POLLIN.bits() },
9277                        { "fd": 1, "events": POLLIN.bits() }
9278                    ]),
9279                    json!(250),
9280                ],
9281            },
9282        )
9283        .expect("poll kernel fds");
9284
9285        assert_eq!(response["readyCount"], Value::from(1));
9286        let fds: Vec<KernelPollFdResponse> =
9287            serde_json::from_value(response["fds"].clone()).expect("kernel poll fd response");
9288        assert_eq!(
9289            fds,
9290            vec![
9291                KernelPollFdResponse {
9292                    fd: 0,
9293                    events: POLLIN.bits(),
9294                    revents: (POLLIN | POLLHUP).bits(),
9295                },
9296                KernelPollFdResponse {
9297                    fd: 1,
9298                    events: POLLIN.bits(),
9299                    revents: 0,
9300                },
9301            ]
9302        );
9303
9304        process.kernel_handle.finish(0);
9305        kernel.waitpid(pid).expect("wait javascript kernel process");
9306    }
9307}
9308
9309fn dedupe_strings(values: &[String]) -> Vec<String> {
9310    let mut seen = BTreeSet::new();
9311    let mut deduped = Vec::new();
9312    for value in values {
9313        if seen.insert(value.clone()) {
9314            deduped.push(value.clone());
9315        }
9316    }
9317    deduped
9318}
9319
9320fn dedupe_host_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
9321    let mut seen = BTreeSet::new();
9322    let mut deduped = Vec::new();
9323    for path in paths {
9324        let normalized = normalize_host_path(path);
9325        let key = normalized.to_string_lossy().into_owned();
9326        if seen.insert(key) {
9327            deduped.push(normalized);
9328        }
9329    }
9330    deduped
9331}
9332
9333fn expand_host_access_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
9334    let mut expanded = Vec::new();
9335    let mut seen = BTreeSet::new();
9336
9337    let mut add_path = |candidate: PathBuf| {
9338        let normalized = normalize_host_path(&candidate);
9339        let key = normalized.to_string_lossy().into_owned();
9340        if seen.insert(key) {
9341            expanded.push(normalized);
9342        }
9343    };
9344
9345    for host_path in paths {
9346        add_path(host_path.clone());
9347        if let Ok(realpath) = fs::canonicalize(host_path) {
9348            add_path(realpath);
9349        }
9350
9351        if host_path.file_name().and_then(|name| name.to_str()) != Some("node_modules") {
9352            continue;
9353        }
9354
9355        let mut current = host_path.parent();
9356        while let Some(parent) = current {
9357            let candidate = parent.join("node_modules");
9358            if candidate.exists() {
9359                add_path(candidate.clone());
9360                if let Ok(realpath) = fs::canonicalize(&candidate) {
9361                    add_path(realpath);
9362                }
9363            }
9364            current = parent.parent();
9365        }
9366    }
9367
9368    expanded
9369}
9370
9371fn prepare_javascript_shadow(
9372    vm: &mut VmState,
9373    resolved: &ResolvedChildProcessExecution,
9374) -> Result<(), SidecarError> {
9375    let guest_entrypoint = resolved
9376        .env
9377        .get("AGENTOS_GUEST_ENTRYPOINT")
9378        .cloned()
9379        // An absolute `entrypoint` may be a host path that lives inside the VM's
9380        // host cwd (callers can pass a fully-qualified host path). The guest sees
9381        // it at its translated guest path (host_cwd -> guest_cwd), so the shadow
9382        // must be keyed by that guest path rather than the raw host path. Falling
9383        // back to the host path here would materialize the file at the wrong guest
9384        // location and the runtime's `require()` would fail with "Cannot find
9385        // module".
9386        .or_else(|| {
9387            resolve_host_entrypoint_within_vm_host_cwd(vm, &resolved.entrypoint)
9388                .map(|(guest_entrypoint, _)| guest_entrypoint)
9389        })
9390        .or_else(|| {
9391            resolved
9392                .entrypoint
9393                .starts_with('/')
9394                .then(|| normalize_path(&resolved.entrypoint))
9395        });
9396    let Some(guest_entrypoint) = guest_entrypoint else {
9397        return Ok(());
9398    };
9399    if host_mount_path_for_guest_path(vm, &guest_entrypoint).is_some() {
9400        return Ok(());
9401    }
9402    if vm.kernel.lstat(&guest_entrypoint).is_err() {
9403        let host_entrypoint = {
9404            let candidate = Path::new(&resolved.entrypoint);
9405            if candidate.is_absolute() {
9406                candidate.to_path_buf()
9407            } else {
9408                resolved.host_cwd.join(candidate)
9409            }
9410        };
9411        if host_entrypoint.exists() {
9412            materialize_host_path_to_shadow(vm, &guest_entrypoint, &host_entrypoint)?;
9413            // The shadow write only stages the file on the host side; the runtime
9414            // resolves modules against the kernel VFS, so the staged entrypoint
9415            // must be synced into the kernel before execution starts (otherwise
9416            // `require()` reports "Cannot find module").
9417            return sync_shadow_entrypoint_into_kernel(vm, &guest_entrypoint);
9418        }
9419    }
9420    materialize_guest_path_to_shadow(vm, &guest_entrypoint)
9421}
9422
9423/// Sync a freshly-staged shadow entrypoint into the kernel VFS so the runtime's
9424/// kernel-backed module resolver can read it. Mirrors the host->kernel file sync
9425/// used by the broader shadow reconciliation, but scoped to the single
9426/// entrypoint we just materialized.
9427fn sync_shadow_entrypoint_into_kernel(
9428    vm: &mut VmState,
9429    guest_entrypoint: &str,
9430) -> Result<(), SidecarError> {
9431    if vm.kernel.exists(guest_entrypoint).unwrap_or(false) {
9432        return Ok(());
9433    }
9434    let shadow_path = shadow_path_for_guest(vm, guest_entrypoint);
9435    let bytes = match fs::read(&shadow_path) {
9436        Ok(bytes) => bytes,
9437        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
9438        Err(error) => {
9439            return Err(SidecarError::Io(format!(
9440                "failed to read staged shadow entrypoint {}: {error}",
9441                shadow_path.display()
9442            )));
9443        }
9444    };
9445    if let Some(parent) = guest_parent_path(guest_entrypoint) {
9446        if !vm.kernel.exists(&parent).unwrap_or(false) {
9447            vm.kernel.mkdir(&parent, true).map_err(kernel_error)?;
9448        }
9449    }
9450    vm.kernel
9451        .write_file(guest_entrypoint, bytes)
9452        .map_err(kernel_error)?;
9453    Ok(())
9454}
9455
9456fn guest_parent_path(guest_path: &str) -> Option<String> {
9457    let parent = Path::new(guest_path).parent()?;
9458    let parent = parent.to_string_lossy();
9459    if parent.is_empty() || parent == "/" {
9460        None
9461    } else {
9462        Some(parent.into_owned())
9463    }
9464}
9465
9466fn materialize_host_path_to_shadow(
9467    vm: &VmState,
9468    guest_path: &str,
9469    host_path: &Path,
9470) -> Result<(), SidecarError> {
9471    let shadow_path = shadow_path_for_guest(vm, guest_path);
9472    let metadata = fs::symlink_metadata(host_path)
9473        .map_err(|error| SidecarError::Io(format!("failed to stat host entrypoint: {error}")))?;
9474
9475    if metadata.file_type().is_symlink() {
9476        if let Some(parent) = shadow_path.parent() {
9477            fs::create_dir_all(parent).map_err(|error| {
9478                SidecarError::Io(format!("failed to create shadow symlink parent: {error}"))
9479            })?;
9480        }
9481        let _ = fs::remove_file(&shadow_path);
9482        let _ = fs::remove_dir_all(&shadow_path);
9483        let target = fs::read_link(host_path)
9484            .map_err(|error| SidecarError::Io(format!("failed to read host symlink: {error}")))?;
9485        std::os::unix::fs::symlink(&target, &shadow_path)
9486            .map_err(|error| SidecarError::Io(format!("failed to mirror host symlink: {error}")))?;
9487        return Ok(());
9488    }
9489
9490    if metadata.is_dir() {
9491        fs::create_dir_all(&shadow_path).map_err(|error| {
9492            SidecarError::Io(format!("failed to create shadow directory: {error}"))
9493        })?;
9494        fs::set_permissions(
9495            &shadow_path,
9496            fs::Permissions::from_mode(metadata.permissions().mode() & 0o7777),
9497        )
9498        .map_err(|error| {
9499            SidecarError::Io(format!(
9500                "failed to set shadow directory mode on {}: {error}",
9501                shadow_path.display()
9502            ))
9503        })?;
9504        return Ok(());
9505    }
9506
9507    if let Some(parent) = shadow_path.parent() {
9508        fs::create_dir_all(parent).map_err(|error| {
9509            SidecarError::Io(format!("failed to create shadow parent: {error}"))
9510        })?;
9511    }
9512    let bytes = fs::read(host_path)
9513        .map_err(|error| SidecarError::Io(format!("failed to read host entrypoint: {error}")))?;
9514    fs::write(&shadow_path, bytes).map_err(|error| {
9515        SidecarError::Io(format!(
9516            "failed to mirror host file into shadow root: {error}"
9517        ))
9518    })?;
9519    fs::set_permissions(
9520        &shadow_path,
9521        fs::Permissions::from_mode(metadata.permissions().mode() & 0o7777),
9522    )
9523    .map_err(|error| {
9524        SidecarError::Io(format!(
9525            "failed to set shadow file mode on {}: {error}",
9526            shadow_path.display()
9527        ))
9528    })?;
9529    Ok(())
9530}
9531
9532fn materialize_guest_path_to_shadow(
9533    vm: &mut VmState,
9534    guest_path: &str,
9535) -> Result<(), SidecarError> {
9536    let stat = vm.kernel.lstat(guest_path).map_err(kernel_error)?;
9537    let shadow_path = shadow_path_for_guest(vm, guest_path);
9538
9539    if stat.is_symbolic_link {
9540        if let Some(parent) = shadow_path.parent() {
9541            fs::create_dir_all(parent).map_err(|error| {
9542                SidecarError::Io(format!("failed to create shadow symlink parent: {error}"))
9543            })?;
9544        }
9545        let _ = fs::remove_file(&shadow_path);
9546        let _ = fs::remove_dir_all(&shadow_path);
9547        let target = vm.kernel.read_link(guest_path).map_err(kernel_error)?;
9548        std::os::unix::fs::symlink(&target, &shadow_path)
9549            .map_err(|error| SidecarError::Io(format!("failed to mirror symlink: {error}")))?;
9550        return Ok(());
9551    }
9552
9553    if stat.is_directory {
9554        fs::create_dir_all(&shadow_path).map_err(|error| {
9555            SidecarError::Io(format!("failed to create shadow directory: {error}"))
9556        })?;
9557        fs::set_permissions(&shadow_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err(
9558            |error| {
9559                SidecarError::Io(format!(
9560                    "failed to set shadow directory mode on {}: {error}",
9561                    shadow_path.display()
9562                ))
9563            },
9564        )?;
9565        return Ok(());
9566    }
9567
9568    if let Some(parent) = shadow_path.parent() {
9569        fs::create_dir_all(parent).map_err(|error| {
9570            SidecarError::Io(format!("failed to create shadow parent: {error}"))
9571        })?;
9572    }
9573    let bytes = vm.kernel.read_file(guest_path).map_err(kernel_error)?;
9574    fs::write(&shadow_path, bytes).map_err(|error| {
9575        SidecarError::Io(format!(
9576            "failed to mirror guest file into shadow root: {error}"
9577        ))
9578    })?;
9579    fs::set_permissions(&shadow_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err(
9580        |error| {
9581            SidecarError::Io(format!(
9582                "failed to set shadow file mode on {}: {error}",
9583                shadow_path.display()
9584            ))
9585        },
9586    )?;
9587    Ok(())
9588}
9589
9590fn load_javascript_entrypoint_source(
9591    vm: &mut VmState,
9592    host_cwd: &Path,
9593    entrypoint: &str,
9594    env: &BTreeMap<String, String>,
9595) -> Option<String> {
9596    let mut read_guest_file = |path: &str| {
9597        vm.kernel
9598            .read_file(path)
9599            .ok()
9600            .and_then(|bytes| String::from_utf8(bytes).ok())
9601    };
9602
9603    if let Some(source) = env
9604        .get("AGENTOS_GUEST_ENTRYPOINT")
9605        .filter(|path| path.starts_with('/'))
9606        .and_then(|path| read_guest_file(path))
9607    {
9608        return Some(source);
9609    }
9610
9611    if entrypoint.starts_with('/') {
9612        if let Some(source) = read_guest_file(entrypoint) {
9613            return Some(source);
9614        }
9615    }
9616
9617    let host_entrypoint = if Path::new(entrypoint).is_absolute() {
9618        PathBuf::from(entrypoint)
9619    } else {
9620        host_cwd.join(entrypoint)
9621    };
9622    let normalized_entrypoint = normalize_host_path(&host_entrypoint);
9623    let sandbox_root = normalize_host_path(&vm.cwd);
9624    let host_cwd = normalize_host_path(&vm.host_cwd);
9625    if !path_is_within_root(&normalized_entrypoint, &sandbox_root)
9626        && !path_is_within_root(&normalized_entrypoint, &host_cwd)
9627    {
9628        return None;
9629    }
9630
9631    fs::read_to_string(&normalized_entrypoint).ok()
9632}
9633
9634fn emit_dns_resolution_event<B>(
9635    bridge: &SharedBridge<B>,
9636    vm_id: &str,
9637    hostname: &str,
9638    source: KernelDnsResolutionSource,
9639    addresses: &[IpAddr],
9640    dns: &VmDnsConfig,
9641) where
9642    B: NativeSidecarBridge + Send + 'static,
9643    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9644{
9645    let _ = emit_structured_event(
9646        bridge,
9647        vm_id,
9648        "network.dns.resolved",
9649        audit_fields([
9650            ("hostname", hostname.to_owned()),
9651            ("source", source.as_str().to_owned()),
9652            (
9653                "addresses",
9654                addresses
9655                    .iter()
9656                    .map(ToString::to_string)
9657                    .collect::<Vec<_>>()
9658                    .join(","),
9659            ),
9660            ("address_count", addresses.len().to_string()),
9661            ("resolver_count", dns.name_servers.len().to_string()),
9662            (
9663                "resolvers",
9664                dns.name_servers
9665                    .iter()
9666                    .map(ToString::to_string)
9667                    .collect::<Vec<_>>()
9668                    .join(","),
9669            ),
9670        ]),
9671    );
9672}
9673
9674fn emit_dns_record_resolution_event<B>(
9675    bridge: &SharedBridge<B>,
9676    vm_id: &str,
9677    hostname: &str,
9678    resolution: &DnsRecordResolution,
9679    dns: &VmDnsConfig,
9680) where
9681    B: NativeSidecarBridge + Send + 'static,
9682    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9683{
9684    if let Some(addresses) = dns_resolution_ip_addrs(resolution.records()) {
9685        emit_dns_resolution_event(
9686            bridge,
9687            vm_id,
9688            hostname,
9689            resolution.source(),
9690            &addresses,
9691            dns,
9692        );
9693        return;
9694    }
9695
9696    let _ = emit_structured_event(
9697        bridge,
9698        vm_id,
9699        "network.dns.resolved",
9700        audit_fields([
9701            ("hostname", hostname.to_owned()),
9702            ("source", resolution.source().as_str().to_owned()),
9703            (
9704                "addresses",
9705                resolution
9706                    .records()
9707                    .iter()
9708                    .map(summarize_dns_record)
9709                    .collect::<Vec<_>>()
9710                    .join(","),
9711            ),
9712            ("address_count", resolution.records().len().to_string()),
9713            ("resolver_count", dns.name_servers.len().to_string()),
9714            (
9715                "resolvers",
9716                dns.name_servers
9717                    .iter()
9718                    .map(ToString::to_string)
9719                    .collect::<Vec<_>>()
9720                    .join(","),
9721            ),
9722        ]),
9723    );
9724}
9725
9726fn emit_dns_resolution_failure_event<B>(
9727    bridge: &SharedBridge<B>,
9728    vm_id: &str,
9729    hostname: &str,
9730    dns: &VmDnsConfig,
9731    error: &SidecarError,
9732) where
9733    B: NativeSidecarBridge + Send + 'static,
9734    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9735{
9736    let _ = emit_structured_event(
9737        bridge,
9738        vm_id,
9739        "network.dns.resolve_failed",
9740        audit_fields([
9741            ("hostname", hostname.to_owned()),
9742            ("reason", error.to_string()),
9743            ("resolver_count", dns.name_servers.len().to_string()),
9744            (
9745                "resolvers",
9746                dns.name_servers
9747                    .iter()
9748                    .map(ToString::to_string)
9749                    .collect::<Vec<_>>()
9750                    .join(","),
9751            ),
9752        ]),
9753    );
9754}
9755
9756fn parse_dns_record_type(rrtype: &str) -> Result<RecordType, SidecarError> {
9757    match rrtype {
9758        "A" => Ok(RecordType::A),
9759        "AAAA" => Ok(RecordType::AAAA),
9760        "MX" => Ok(RecordType::MX),
9761        "TXT" => Ok(RecordType::TXT),
9762        "SRV" => Ok(RecordType::SRV),
9763        "CNAME" => Ok(RecordType::CNAME),
9764        "PTR" => Ok(RecordType::PTR),
9765        "NS" => Ok(RecordType::NS),
9766        "SOA" => Ok(RecordType::SOA),
9767        "NAPTR" => Ok(RecordType::NAPTR),
9768        "CAA" => Ok(RecordType::CAA),
9769        "ANY" => Ok(RecordType::ANY),
9770        other => Err(SidecarError::Execution(format!(
9771            "ERR_NOT_IMPLEMENTED: dns rrtype {other} is not supported by the secure-exec dns bridge"
9772        ))),
9773    }
9774}
9775
9776fn dns_resolution_to_node_value(
9777    resolution: &DnsRecordResolution,
9778    requested_type: &str,
9779) -> Result<Value, SidecarError> {
9780    let safe_ips = dns_resolution_safe_ip_set(resolution.records(), resolution.hostname())?;
9781    match requested_type {
9782        "A" | "AAAA" => Ok(Value::Array(
9783            resolution
9784                .records()
9785                .iter()
9786                .filter_map(|record| dns_record_ip_string(record, &safe_ips))
9787                .map(Value::String)
9788                .collect(),
9789        )),
9790        "MX" => Ok(Value::Array(
9791            resolution
9792                .records()
9793                .iter()
9794                .filter_map(|record| match record.data() {
9795                    RData::MX(mx) => Some(json!({
9796                        "priority": mx.preference,
9797                        "exchange": normalize_dns_name_for_node(&mx.exchange),
9798                        "type": "MX",
9799                    })),
9800                    _ => None,
9801                })
9802                .collect(),
9803        )),
9804        "TXT" => Ok(Value::Array(
9805            resolution
9806                .records()
9807                .iter()
9808                .filter_map(|record| match record.data() {
9809                    RData::TXT(txt) => Some(Value::Array(
9810                        txt.txt_data
9811                            .iter()
9812                            .map(|entry| Value::String(String::from_utf8_lossy(entry).into_owned()))
9813                            .collect(),
9814                    )),
9815                    _ => None,
9816                })
9817                .collect(),
9818        )),
9819        "SRV" => Ok(Value::Array(
9820            resolution
9821                .records()
9822                .iter()
9823                .filter_map(|record| match record.data() {
9824                    RData::SRV(srv) => Some(json!({
9825                        "priority": srv.priority,
9826                        "weight": srv.weight,
9827                        "port": srv.port,
9828                        "name": normalize_dns_name_for_node(&srv.target),
9829                        "type": "SRV",
9830                    })),
9831                    _ => None,
9832                })
9833                .collect(),
9834        )),
9835        "CNAME" => Ok(Value::Array(
9836            resolution
9837                .records()
9838                .iter()
9839                .filter_map(|record| match record.data() {
9840                    RData::CNAME(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9841                    _ => None,
9842                })
9843                .collect(),
9844        )),
9845        "PTR" => Ok(Value::Array(
9846            resolution
9847                .records()
9848                .iter()
9849                .filter_map(|record| match record.data() {
9850                    RData::PTR(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9851                    _ => None,
9852                })
9853                .collect(),
9854        )),
9855        "NS" => Ok(Value::Array(
9856            resolution
9857                .records()
9858                .iter()
9859                .filter_map(|record| match record.data() {
9860                    RData::NS(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9861                    _ => None,
9862                })
9863                .collect(),
9864        )),
9865        "SOA" => resolution
9866            .records()
9867            .iter()
9868            .find_map(|record| match record.data() {
9869                RData::SOA(soa) => Some(json!({
9870                    "nsname": normalize_dns_name_for_node(&soa.mname),
9871                    "hostmaster": normalize_dns_name_for_node(&soa.rname),
9872                    "serial": soa.serial,
9873                    "refresh": soa.refresh,
9874                    "retry": soa.retry,
9875                    "expire": soa.expire,
9876                    "minttl": soa.minimum,
9877                })),
9878                _ => None,
9879            })
9880            .ok_or_else(|| {
9881                SidecarError::Execution(String::from("failed to resolve DNS SOA record"))
9882            }),
9883        "NAPTR" => Ok(Value::Array(
9884            resolution
9885                .records()
9886                .iter()
9887                .filter_map(|record| match record.data() {
9888                    RData::NAPTR(naptr) => Some(json!({
9889                        "flags": String::from_utf8_lossy(&naptr.flags).into_owned(),
9890                        "service": String::from_utf8_lossy(&naptr.services).into_owned(),
9891                        "regexp": String::from_utf8_lossy(&naptr.regexp).into_owned(),
9892                        "replacement": normalize_dns_name_for_node(&naptr.replacement),
9893                        "order": naptr.order,
9894                        "preference": naptr.preference,
9895                    })),
9896                    _ => None,
9897                })
9898                .collect(),
9899        )),
9900        "CAA" => Ok(Value::Array(
9901            resolution
9902                .records()
9903                .iter()
9904                .filter_map(|record| match record.data() {
9905                    RData::CAA(caa) => {
9906                        let mut value = serde_json::Map::new();
9907                        value.insert(
9908                            "critical".to_owned(),
9909                            Value::from(u8::from(caa.issuer_critical)),
9910                        );
9911                        value.insert("type".to_owned(), Value::String(String::from("CAA")));
9912                        if caa.tag.eq_ignore_ascii_case("iodef") {
9913                            value.insert(
9914                                "iodef".to_owned(),
9915                                Value::String(
9916                                    caa.value_as_iodef()
9917                                        .map(|url| url.to_string())
9918                                        .unwrap_or_else(|_| {
9919                                            String::from_utf8_lossy(&caa.value).into_owned()
9920                                        }),
9921                                ),
9922                            );
9923                        } else if let Ok((issuer, _params)) = caa.value_as_issue() {
9924                            let field = if caa.tag.eq_ignore_ascii_case("issuewild") {
9925                                "issuewild"
9926                            } else {
9927                                "issue"
9928                            };
9929                            value.insert(
9930                                field.to_owned(),
9931                                Value::String(
9932                                    issuer.as_ref().map(ToString::to_string).unwrap_or_else(|| {
9933                                        String::from_utf8_lossy(&caa.value).into_owned()
9934                                    }),
9935                                ),
9936                            );
9937                        } else {
9938                            value.insert(
9939                                caa.tag.to_ascii_lowercase(),
9940                                Value::String(String::from_utf8_lossy(&caa.value).into_owned()),
9941                            );
9942                        }
9943                        Some(Value::Object(value))
9944                    }
9945                    _ => None,
9946                })
9947                .collect(),
9948        )),
9949        "ANY" => Ok(Value::Array(
9950            resolution
9951                .records()
9952                .iter()
9953                .filter_map(|record| dns_any_record_to_value(record, &safe_ips))
9954                .collect(),
9955        )),
9956        other => Err(SidecarError::Execution(format!(
9957            "ERR_NOT_IMPLEMENTED: dns rrtype {other} is not supported by the secure-exec dns bridge"
9958        ))),
9959    }
9960}
9961
9962fn dns_resolution_safe_ip_set(
9963    records: &[Record],
9964    hostname: &str,
9965) -> Result<BTreeSet<IpAddr>, SidecarError> {
9966    let ips = records
9967        .iter()
9968        .filter_map(dns_record_ip_addr)
9969        .collect::<Vec<_>>();
9970    if ips.is_empty() {
9971        return Ok(BTreeSet::new());
9972    }
9973    Ok(filter_dns_safe_ip_addrs(ips, hostname)?
9974        .into_iter()
9975        .collect())
9976}
9977
9978fn dns_resolution_ip_addrs(records: &[Record]) -> Option<Vec<IpAddr>> {
9979    let ips = records
9980        .iter()
9981        .filter_map(dns_record_ip_addr)
9982        .collect::<Vec<_>>();
9983    if ips.is_empty() {
9984        return None;
9985    }
9986    Some(ips)
9987}
9988
9989fn dns_record_ip_addr(record: &Record) -> Option<IpAddr> {
9990    match record.data() {
9991        RData::A(address) => Some(IpAddr::V4(**address)),
9992        RData::AAAA(address) => Some(IpAddr::V6(**address)),
9993        _ => None,
9994    }
9995}
9996
9997fn dns_record_ip_string(record: &Record, safe_ips: &BTreeSet<IpAddr>) -> Option<String> {
9998    let ip = dns_record_ip_addr(record)?;
9999    safe_ips.contains(&ip).then(|| ip.to_string())
10000}
10001
10002fn dns_any_record_to_value(record: &Record, safe_ips: &BTreeSet<IpAddr>) -> Option<Value> {
10003    let value = match record.data() {
10004        RData::A(_) | RData::AAAA(_) => json!({
10005            "address": dns_record_ip_string(record, safe_ips)?,
10006            "ttl": record.ttl(),
10007            "type": record.record_type().to_string(),
10008        }),
10009        RData::MX(mx) => json!({
10010            "exchange": normalize_dns_name_for_node(&mx.exchange),
10011            "priority": mx.preference,
10012            "type": "MX",
10013        }),
10014        RData::TXT(txt) => json!({
10015            "entries": txt
10016                .txt_data
10017                .iter()
10018                .map(|entry| String::from_utf8_lossy(entry).into_owned())
10019                .collect::<Vec<_>>(),
10020            "type": "TXT",
10021        }),
10022        RData::SRV(srv) => json!({
10023            "name": normalize_dns_name_for_node(&srv.target),
10024            "port": srv.port,
10025            "priority": srv.priority,
10026            "weight": srv.weight,
10027            "type": "SRV",
10028        }),
10029        RData::CNAME(name) => json!({
10030            "value": normalize_dns_name_for_node(&name.0),
10031            "type": "CNAME",
10032        }),
10033        RData::PTR(name) => json!({
10034            "value": normalize_dns_name_for_node(&name.0),
10035            "type": "PTR",
10036        }),
10037        RData::NS(name) => json!({
10038            "value": normalize_dns_name_for_node(&name.0),
10039            "type": "NS",
10040        }),
10041        RData::SOA(soa) => json!({
10042            "nsname": normalize_dns_name_for_node(&soa.mname),
10043            "hostmaster": normalize_dns_name_for_node(&soa.rname),
10044            "serial": soa.serial,
10045            "refresh": soa.refresh,
10046            "retry": soa.retry,
10047            "expire": soa.expire,
10048            "minttl": soa.minimum,
10049            "type": "SOA",
10050        }),
10051        RData::NAPTR(naptr) => json!({
10052            "flags": String::from_utf8_lossy(&naptr.flags).into_owned(),
10053            "service": String::from_utf8_lossy(&naptr.services).into_owned(),
10054            "regexp": String::from_utf8_lossy(&naptr.regexp).into_owned(),
10055            "replacement": normalize_dns_name_for_node(&naptr.replacement),
10056            "order": naptr.order,
10057            "preference": naptr.preference,
10058            "type": "NAPTR",
10059        }),
10060        RData::CAA(caa) => {
10061            let mut value = serde_json::Map::new();
10062            value.insert(
10063                "critical".to_owned(),
10064                Value::from(u8::from(caa.issuer_critical)),
10065            );
10066            value.insert("type".to_owned(), Value::String(String::from("CAA")));
10067            if caa.tag.eq_ignore_ascii_case("iodef") {
10068                value.insert(
10069                    "iodef".to_owned(),
10070                    Value::String(
10071                        caa.value_as_iodef()
10072                            .map(|url| url.to_string())
10073                            .unwrap_or_else(|_| String::from_utf8_lossy(&caa.value).into_owned()),
10074                    ),
10075                );
10076            } else if let Ok((issuer, _params)) = caa.value_as_issue() {
10077                let field = if caa.tag.eq_ignore_ascii_case("issuewild") {
10078                    "issuewild"
10079                } else {
10080                    "issue"
10081                };
10082                value.insert(
10083                    field.to_owned(),
10084                    Value::String(
10085                        issuer
10086                            .as_ref()
10087                            .map(ToString::to_string)
10088                            .unwrap_or_else(|| String::from_utf8_lossy(&caa.value).into_owned()),
10089                    ),
10090                );
10091            }
10092            Value::Object(value)
10093        }
10094        _ => return None,
10095    };
10096    Some(value)
10097}
10098
10099fn normalize_dns_name_for_node(name: &impl ToString) -> String {
10100    name.to_string().trim_end_matches('.').to_owned()
10101}
10102
10103fn summarize_dns_record(record: &Record) -> String {
10104    match record.data() {
10105        RData::A(_) | RData::AAAA(_) => record.data().to_string(),
10106        _ => format!("{} {}", record.record_type(), record.data()),
10107    }
10108}
10109
10110// build_root_filesystem, convert_root_lower_descriptor, convert_root_filesystem_entry,
10111// root_snapshot_entry moved to crate::bootstrap
10112
10113// apply_root_filesystem_entry, ensure_parent_directories moved to crate::bootstrap
10114
10115// ProcNetEntry moved to crate::state
10116
10117fn find_socket_state_entry(
10118    vm: Option<&VmState>,
10119    kind: SocketQueryKind,
10120    request: &FindListenerRequest,
10121) -> Result<Option<SocketStateEntry>, SidecarError> {
10122    let vm = vm.ok_or_else(|| SidecarError::InvalidState(String::from("unknown sidecar VM")))?;
10123
10124    for (process_id, process) in &vm.active_processes {
10125        if let Some(path) = request.path.as_deref() {
10126            if matches!(kind, SocketQueryKind::TcpListener) {
10127                for listener in process.unix_listeners.values() {
10128                    if listener.path() != path {
10129                        continue;
10130                    }
10131                    return Ok(Some(SocketStateEntry {
10132                        process_id: process_id.to_owned(),
10133                        host: None,
10134                        port: None,
10135                        path: Some(path.to_owned()),
10136                    }));
10137                }
10138            }
10139        }
10140
10141        if request.path.is_none() {
10142            if let Some(entry) =
10143                find_kernel_socket_state_entry(&vm.kernel, process_id, process, kind, request)?
10144            {
10145                return Ok(Some(entry));
10146            }
10147
10148            match kind {
10149                SocketQueryKind::TcpListener => {
10150                    for server in process.http_servers.values() {
10151                        let local_addr = server.guest_local_addr;
10152                        let local_host = local_addr.ip().to_string();
10153                        if !socket_host_matches(request.host.as_deref(), &local_host) {
10154                            continue;
10155                        }
10156                        if let Some(port) = request.port {
10157                            if local_addr.port() != port {
10158                                continue;
10159                            }
10160                        }
10161                        return Ok(Some(SocketStateEntry {
10162                            process_id: process_id.to_owned(),
10163                            host: Some(local_host),
10164                            port: Some(local_addr.port()),
10165                            path: None,
10166                        }));
10167                    }
10168
10169                    for listener in process.tcp_listeners.values() {
10170                        if listener.kernel_socket_id.is_some() {
10171                            continue;
10172                        }
10173                        let local_addr = listener.guest_local_addr();
10174                        let local_host = local_addr.ip().to_string();
10175                        if !socket_host_matches(request.host.as_deref(), &local_host) {
10176                            continue;
10177                        }
10178                        if let Some(port) = request.port {
10179                            if local_addr.port() != port {
10180                                continue;
10181                            }
10182                        }
10183                        return Ok(Some(SocketStateEntry {
10184                            process_id: process_id.to_owned(),
10185                            host: Some(local_host),
10186                            port: Some(local_addr.port()),
10187                            path: None,
10188                        }));
10189                    }
10190                }
10191                SocketQueryKind::UdpBound => {
10192                    for socket in process.udp_sockets.values() {
10193                        if socket.kernel_socket_id.is_some() {
10194                            continue;
10195                        }
10196                        let Some(local_addr) = socket.local_addr() else {
10197                            continue;
10198                        };
10199                        let local_host = local_addr.ip().to_string();
10200                        if !socket_host_matches(request.host.as_deref(), &local_host) {
10201                            continue;
10202                        }
10203                        if let Some(port) = request.port {
10204                            if local_addr.port() != port {
10205                                continue;
10206                            }
10207                        }
10208                        return Ok(Some(SocketStateEntry {
10209                            process_id: process_id.to_owned(),
10210                            host: Some(local_host),
10211                            port: Some(local_addr.port()),
10212                            path: None,
10213                        }));
10214                    }
10215                }
10216            }
10217        }
10218
10219        let child_pid = process.execution.child_pid();
10220        let inodes = socket_inodes_for_pid(child_pid)?;
10221        if inodes.is_empty() {
10222            continue;
10223        }
10224
10225        if let Some(path) = request.path.as_deref() {
10226            if let Some(listener) = find_unix_socket_for_pid(child_pid, &inodes, path, process_id)?
10227            {
10228                return Ok(Some(listener));
10229            }
10230            continue;
10231        }
10232
10233        let table_paths = match kind {
10234            SocketQueryKind::TcpListener => [
10235                format!("/proc/{child_pid}/net/tcp"),
10236                format!("/proc/{child_pid}/net/tcp6"),
10237            ],
10238            SocketQueryKind::UdpBound => [
10239                format!("/proc/{child_pid}/net/udp"),
10240                format!("/proc/{child_pid}/net/udp6"),
10241            ],
10242        };
10243        for table_path in table_paths {
10244            if let Some(entry) = find_inet_socket_for_pid(
10245                &table_path,
10246                &inodes,
10247                kind,
10248                request.host.as_deref(),
10249                request.port,
10250                process_id,
10251            )? {
10252                return Ok(Some(entry));
10253            }
10254        }
10255    }
10256
10257    Ok(None)
10258}
10259
10260fn require_vm_inspection_permission<B>(
10261    bridge: &SharedBridge<B>,
10262    vm_id: &str,
10263    capability: &str,
10264    domain: &str,
10265    resource: &str,
10266) -> Result<(), SidecarError>
10267where
10268    B: NativeSidecarBridge + Send + 'static,
10269    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
10270{
10271    let decision = bridge.static_permission_decision(vm_id, capability, domain, Some(resource));
10272    if decision.as_ref().is_some_and(|decision| decision.allow) {
10273        return Ok(());
10274    }
10275
10276    let reason = decision
10277        .and_then(|decision| decision.reason)
10278        .unwrap_or_else(|| format!("{capability} permission required"));
10279    Err(SidecarError::Execution(format!(
10280        "EACCES: permission denied, {resource}: {reason}"
10281    )))
10282}
10283
10284fn socket_query_resource(kind: SocketQueryKind, request: &FindListenerRequest) -> String {
10285    if let Some(path) = request.path.as_deref() {
10286        return format!("unix://{path}");
10287    }
10288
10289    let host = request.host.as_deref().unwrap_or("*");
10290    let port = request
10291        .port
10292        .map_or_else(|| String::from("*"), |port| port.to_string());
10293    match kind {
10294        SocketQueryKind::TcpListener => format!("tcp://{host}:{port}"),
10295        SocketQueryKind::UdpBound => format!("udp://{host}:{port}"),
10296    }
10297}
10298
10299fn snapshot_vm_processes(vm: &VmState) -> Vec<ProcessSnapshotEntry> {
10300    let process_table = vm.kernel.list_processes();
10301    snapshot_vm_processes_inner(vm, &process_table)
10302}
10303
10304fn snapshot_vm_processes_inner(
10305    vm: &VmState,
10306    process_table: &BTreeMap<u32, secure_exec_kernel::process_table::ProcessInfo>,
10307) -> Vec<ProcessSnapshotEntry> {
10308    let mut entries = Vec::new();
10309
10310    for (process_id, process) in &vm.active_processes {
10311        collect_process_snapshot_entries(process_id, process, process_table, &mut entries);
10312    }
10313
10314    for exited in &vm.exited_process_snapshots {
10315        entries.push(exited.process.clone());
10316    }
10317
10318    entries
10319}
10320
10321fn prune_exited_process_snapshots(vm: &mut VmState) {
10322    let cutoff = Instant::now() - EXITED_PROCESS_SNAPSHOT_RETENTION;
10323    while vm
10324        .exited_process_snapshots
10325        .front()
10326        .is_some_and(|snapshot| snapshot.captured_at < cutoff)
10327    {
10328        vm.exited_process_snapshots.pop_front();
10329    }
10330}
10331
10332fn build_process_snapshot_entry(
10333    process_id: &str,
10334    process: &ActiveProcess,
10335    info: &secure_exec_kernel::process_table::ProcessInfo,
10336    exit_code: Option<i32>,
10337) -> ProcessSnapshotEntry {
10338    ProcessSnapshotEntry {
10339        process_id: process_id.to_owned(),
10340        pid: info.pid,
10341        ppid: info.ppid,
10342        pgid: info.pgid,
10343        sid: info.sid,
10344        driver: info.driver.clone(),
10345        command: info.command.clone(),
10346        args: Vec::new(),
10347        cwd: process.guest_cwd.clone(),
10348        status: if exit_code.is_some() {
10349            ProcessSnapshotStatus::Exited
10350        } else {
10351            match info.status {
10352                ProcessStatus::Running => ProcessSnapshotStatus::Running,
10353                ProcessStatus::Stopped => ProcessSnapshotStatus::Stopped,
10354                ProcessStatus::Exited => ProcessSnapshotStatus::Exited,
10355            }
10356        },
10357        exit_code: exit_code.or(info.exit_code),
10358    }
10359}
10360
10361fn collect_process_snapshot_entries(
10362    process_id: &str,
10363    process: &ActiveProcess,
10364    process_table: &BTreeMap<u32, secure_exec_kernel::process_table::ProcessInfo>,
10365    entries: &mut Vec<ProcessSnapshotEntry>,
10366) {
10367    if let Some(info) = process_table.get(&process.kernel_pid) {
10368        entries.push(build_process_snapshot_entry(
10369            process_id, process, info, None,
10370        ));
10371    }
10372
10373    for (child_id, child) in &process.child_processes {
10374        let child_process_id = format!("{process_id}/{child_id}");
10375        collect_process_snapshot_entries(&child_process_id, child, process_table, entries);
10376    }
10377}
10378
10379fn find_kernel_socket_state_entry(
10380    kernel: &SidecarKernel,
10381    process_id: &str,
10382    process: &ActiveProcess,
10383    kind: SocketQueryKind,
10384    request: &FindListenerRequest,
10385) -> Result<Option<SocketStateEntry>, SidecarError> {
10386    let entry = match kind {
10387        SocketQueryKind::TcpListener => process
10388            .tcp_listeners
10389            .values()
10390            .filter_map(|listener| listener.kernel_socket_id)
10391            .find_map(|socket_id| {
10392                kernel_socket_state_entry(kernel, process_id, socket_id, kind, request)
10393            }),
10394        SocketQueryKind::UdpBound => process
10395            .udp_sockets
10396            .values()
10397            .filter_map(|socket| socket.kernel_socket_id)
10398            .find_map(|socket_id| {
10399                kernel_socket_state_entry(kernel, process_id, socket_id, kind, request)
10400            }),
10401    };
10402
10403    if entry.is_some() {
10404        return Ok(entry);
10405    }
10406
10407    for child in process.child_processes.values() {
10408        if let Some(entry) =
10409            find_kernel_socket_state_entry(kernel, process_id, child, kind, request)?
10410        {
10411            return Ok(Some(entry));
10412        }
10413    }
10414
10415    Ok(None)
10416}
10417
10418fn kernel_socket_state_entry(
10419    kernel: &SidecarKernel,
10420    process_id: &str,
10421    socket_id: SocketId,
10422    kind: SocketQueryKind,
10423    request: &FindListenerRequest,
10424) -> Option<SocketStateEntry> {
10425    let record = kernel.socket_get(socket_id)?;
10426    let local_address = record.local_address()?;
10427    match kind {
10428        SocketQueryKind::TcpListener if record.state() == SocketState::Listening => {}
10429        SocketQueryKind::TcpListener => return None,
10430        SocketQueryKind::UdpBound => {}
10431    }
10432
10433    if !socket_host_matches(request.host.as_deref(), local_address.host()) {
10434        return None;
10435    }
10436    if request
10437        .port
10438        .is_some_and(|port| local_address.port() != port)
10439    {
10440        return None;
10441    }
10442
10443    Some(SocketStateEntry {
10444        process_id: process_id.to_owned(),
10445        host: Some(local_address.host().to_owned()),
10446        port: Some(local_address.port()),
10447        path: None,
10448    })
10449}
10450
10451fn socket_inodes_for_pid(pid: u32) -> Result<BTreeSet<u64>, SidecarError> {
10452    let fd_dir = PathBuf::from(format!("/proc/{pid}/fd"));
10453    let entries = match fs::read_dir(&fd_dir) {
10454        Ok(entries) => entries,
10455        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(BTreeSet::new()),
10456        Err(error) => {
10457            return Err(SidecarError::Io(format!(
10458                "failed to read socket descriptors for process {pid}: {error}"
10459            )));
10460        }
10461    };
10462
10463    let mut inodes = BTreeSet::new();
10464    for entry in entries {
10465        let entry = entry.map_err(|error| {
10466            SidecarError::Io(format!(
10467                "failed to inspect fd entry for process {pid}: {error}"
10468            ))
10469        })?;
10470        let target = match fs::read_link(entry.path()) {
10471            Ok(target) => target,
10472            Err(_) => continue,
10473        };
10474        if let Some(inode) = parse_socket_inode(&target) {
10475            inodes.insert(inode);
10476        }
10477    }
10478
10479    Ok(inodes)
10480}
10481
10482fn parse_socket_inode(target: &Path) -> Option<u64> {
10483    let value = target.to_string_lossy();
10484    let trimmed = value.strip_prefix("socket:[")?.strip_suffix(']')?;
10485    trimmed.parse().ok()
10486}
10487
10488fn unix_socket_path(addr: &UnixSocketAddr) -> Option<String> {
10489    addr.as_pathname()
10490        .map(|path| path.to_string_lossy().into_owned())
10491}
10492
10493fn find_unix_socket_for_pid(
10494    pid: u32,
10495    inodes: &BTreeSet<u64>,
10496    path: &str,
10497    process_id: &str,
10498) -> Result<Option<SocketStateEntry>, SidecarError> {
10499    let table_path = format!("/proc/{pid}/net/unix");
10500    let contents = match fs::read_to_string(&table_path) {
10501        Ok(contents) => contents,
10502        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
10503        Err(error) => {
10504            return Err(SidecarError::Io(format!(
10505                "failed to inspect unix sockets for process {pid}: {error}"
10506            )));
10507        }
10508    };
10509
10510    for line in contents.lines().skip(1) {
10511        let columns = line.split_whitespace().collect::<Vec<_>>();
10512        if columns.len() < 8 {
10513            continue;
10514        }
10515        let Ok(inode) = columns[6].parse::<u64>() else {
10516            continue;
10517        };
10518        if !inodes.contains(&inode) || columns[7] != path {
10519            continue;
10520        }
10521        return Ok(Some(SocketStateEntry {
10522            process_id: process_id.to_owned(),
10523            host: None,
10524            port: None,
10525            path: Some(path.to_owned()),
10526        }));
10527    }
10528
10529    Ok(None)
10530}
10531
10532fn find_inet_socket_for_pid(
10533    table_path: &str,
10534    inodes: &BTreeSet<u64>,
10535    kind: SocketQueryKind,
10536    requested_host: Option<&str>,
10537    requested_port: Option<u16>,
10538    process_id: &str,
10539) -> Result<Option<SocketStateEntry>, SidecarError> {
10540    for entry in parse_proc_net_entries(table_path)? {
10541        if !inodes.contains(&entry.inode) {
10542            continue;
10543        }
10544        if matches!(kind, SocketQueryKind::TcpListener) && entry.state != "0A" {
10545            continue;
10546        }
10547        if !socket_host_matches(requested_host, &entry.local_host) {
10548            continue;
10549        }
10550        if let Some(port) = requested_port {
10551            if entry.local_port != port {
10552                continue;
10553            }
10554        }
10555        return Ok(Some(SocketStateEntry {
10556            process_id: process_id.to_owned(),
10557            host: Some(entry.local_host),
10558            port: Some(entry.local_port),
10559            path: None,
10560        }));
10561    }
10562
10563    Ok(None)
10564}
10565
10566fn is_unspecified_socket_host(host: &str) -> bool {
10567    host == "0.0.0.0" || host == "::"
10568}
10569
10570fn is_loopback_socket_host(host: &str) -> bool {
10571    host == "127.0.0.1" || host == "::1" || host.eq_ignore_ascii_case("localhost")
10572}
10573
10574pub(crate) fn vm_network_resource_counts(vm: &VmState) -> NetworkResourceCounts {
10575    let snapshot = vm.kernel.resource_snapshot();
10576    let mut counts = NetworkResourceCounts {
10577        sockets: snapshot.sockets,
10578        connections: snapshot.socket_connections,
10579    };
10580    for process in vm.active_processes.values() {
10581        let process_counts = process.sidecar_only_network_resource_counts();
10582        counts.sockets += process_counts.sockets;
10583        counts.connections += process_counts.connections;
10584    }
10585    counts
10586}
10587
10588#[allow(clippy::too_many_arguments)]
10589fn collect_javascript_socket_port_state(
10590    kernel: &SidecarKernel,
10591    process_id: &str,
10592    process: &ActiveProcess,
10593    tcp_guest_to_host: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10594    http_loopback_targets: &mut BTreeMap<
10595        (JavascriptSocketFamily, u16),
10596        JavascriptHttpLoopbackTarget,
10597    >,
10598    udp_guest_to_host: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10599    udp_host_to_guest: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10600    used_tcp_ports: &mut BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10601    used_udp_ports: &mut BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10602) {
10603    for (family, port) in process.tcp_port_reservations.values() {
10604        used_tcp_ports.entry(*family).or_default().insert(*port);
10605    }
10606
10607    let mut record_tcp_listener = |guest_addr: SocketAddr, host_port: u16| {
10608        let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10609        used_tcp_ports
10610            .entry(family)
10611            .or_default()
10612            .insert(guest_addr.port());
10613        // VM-local loopback connects should also resolve listeners bound to
10614        // unspecified guest addresses like 0.0.0.0/::.
10615        tcp_guest_to_host.insert((family, guest_addr.port()), host_port);
10616    };
10617
10618    for listener in process.tcp_listeners.values() {
10619        let local_addr = listener
10620            .kernel_socket_id
10621            .and_then(|socket_id| kernel.socket_get(socket_id))
10622            .and_then(|record| record.local_address().cloned())
10623            .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
10624            .unwrap_or_else(|| listener.guest_local_addr());
10625        record_tcp_listener(local_addr, local_addr.port());
10626    }
10627
10628    for (server_id, server) in &process.http_servers {
10629        let host_port = match server.listener.local_addr() {
10630            Ok(addr) => addr.port(),
10631            Err(_) => continue,
10632        };
10633        record_tcp_listener(server.guest_local_addr, host_port);
10634        let family = JavascriptSocketFamily::from_ip(server.guest_local_addr.ip());
10635        http_loopback_targets.insert(
10636            (family, server.guest_local_addr.port()),
10637            JavascriptHttpLoopbackTarget {
10638                process_id: process_id.to_owned(),
10639                server_id: *server_id,
10640            },
10641        );
10642    }
10643
10644    if let Ok(http2) = process.http2.shared.lock() {
10645        for server in http2.servers.values() {
10646            record_tcp_listener(server.guest_local_addr, server.actual_local_addr.port());
10647        }
10648    }
10649
10650    for socket in process.tcp_sockets.values() {
10651        let guest_addr = socket
10652            .kernel_socket_id
10653            .and_then(|socket_id| kernel.socket_get(socket_id))
10654            .and_then(|record| record.local_address().cloned())
10655            .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
10656            .unwrap_or(socket.guest_local_addr);
10657        let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10658        used_tcp_ports
10659            .entry(family)
10660            .or_default()
10661            .insert(guest_addr.port());
10662    }
10663
10664    for socket in process.udp_sockets.values() {
10665        let guest_addr = socket
10666            .kernel_socket_id
10667            .and_then(|socket_id| kernel.socket_get(socket_id))
10668            .and_then(|record| record.local_address().cloned())
10669            .and_then(|address| {
10670                resolve_udp_bind_addr(address.host(), address.port(), socket.family).ok()
10671            })
10672            .or_else(|| socket.local_addr());
10673        let Some(guest_addr) = guest_addr else {
10674            continue;
10675        };
10676        let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10677        used_udp_ports
10678            .entry(family)
10679            .or_default()
10680            .insert(guest_addr.port());
10681        if let Some(host_addr) = socket
10682            .socket
10683            .as_ref()
10684            .and_then(|socket| socket.local_addr().ok())
10685        {
10686            if is_loopback_ip(guest_addr.ip()) {
10687                udp_guest_to_host.insert((family, guest_addr.port()), host_addr.port());
10688                udp_host_to_guest.insert((family, host_addr.port()), guest_addr.port());
10689            }
10690        } else if socket.kernel_socket_id.is_some() && is_loopback_ip(guest_addr.ip()) {
10691            udp_guest_to_host.insert((family, guest_addr.port()), guest_addr.port());
10692            udp_host_to_guest.insert((family, guest_addr.port()), guest_addr.port());
10693        }
10694    }
10695
10696    for (child_process_id, child) in &process.child_processes {
10697        let child_id = format!("{process_id}/{child_process_id}");
10698        collect_javascript_socket_port_state(
10699            kernel,
10700            &child_id,
10701            child,
10702            tcp_guest_to_host,
10703            http_loopback_targets,
10704            udp_guest_to_host,
10705            udp_host_to_guest,
10706            used_tcp_ports,
10707            used_udp_ports,
10708        );
10709    }
10710}
10711
10712pub(crate) fn build_javascript_socket_path_context(
10713    vm: &VmState,
10714) -> Result<JavascriptSocketPathContext, SidecarError> {
10715    let mut loopback_exempt_ports = vm.create_loopback_exempt_ports.clone();
10716    loopback_exempt_ports.extend(vm.configuration.loopback_exempt_ports.iter().copied());
10717    let mut tcp_loopback_guest_to_host_ports = BTreeMap::new();
10718    let mut http_loopback_targets = BTreeMap::new();
10719    let mut udp_loopback_guest_to_host_ports = BTreeMap::new();
10720    let mut udp_loopback_host_to_guest_ports = BTreeMap::new();
10721    let mut used_tcp_guest_ports = BTreeMap::new();
10722    let mut used_udp_guest_ports = BTreeMap::new();
10723    for (process_id, process) in &vm.active_processes {
10724        collect_javascript_socket_port_state(
10725            &vm.kernel,
10726            process_id,
10727            process,
10728            &mut tcp_loopback_guest_to_host_ports,
10729            &mut http_loopback_targets,
10730            &mut udp_loopback_guest_to_host_ports,
10731            &mut udp_loopback_host_to_guest_ports,
10732            &mut used_tcp_guest_ports,
10733            &mut used_udp_guest_ports,
10734        );
10735    }
10736    Ok(JavascriptSocketPathContext {
10737        sandbox_root: vm.cwd.clone(),
10738        mounts: vm.configuration.mounts.clone(),
10739        listen_policy: vm.listen_policy,
10740        loopback_exempt_ports,
10741        tcp_loopback_guest_to_host_ports,
10742        http_loopback_targets,
10743        udp_loopback_guest_to_host_ports,
10744        udp_loopback_host_to_guest_ports,
10745        used_tcp_guest_ports,
10746        used_udp_guest_ports,
10747    })
10748}
10749
10750fn check_network_resource_limit(
10751    limit: Option<usize>,
10752    current: usize,
10753    additional: usize,
10754    label: &str,
10755) -> Result<(), SidecarError> {
10756    if let Some(limit) = limit {
10757        if current.saturating_add(additional) > limit {
10758            return Err(SidecarError::Execution(format!(
10759                "EAGAIN: maximum {label} count reached"
10760            )));
10761        }
10762    }
10763    Ok(())
10764}
10765
10766fn normalize_tcp_listen_host(
10767    host: Option<&str>,
10768) -> Result<(JavascriptSocketFamily, &'static str, &'static str), SidecarError> {
10769    match host.unwrap_or("127.0.0.1") {
10770        "127.0.0.1" | "localhost" => Ok((JavascriptSocketFamily::Ipv4, "127.0.0.1", "127.0.0.1")),
10771        "::1" => Ok((JavascriptSocketFamily::Ipv6, "::1", "::1")),
10772        "0.0.0.0" => Ok((JavascriptSocketFamily::Ipv4, "127.0.0.1", "0.0.0.0")),
10773        "::" => Ok((JavascriptSocketFamily::Ipv6, "::1", "::")),
10774        other => Err(SidecarError::Execution(format!(
10775            "EACCES: TCP listeners must bind to loopback or unspecified addresses, got {other}"
10776        ))),
10777    }
10778}
10779
10780fn normalize_udp_bind_host(
10781    host: Option<&str>,
10782    family: JavascriptUdpFamily,
10783) -> Result<(&'static str, &'static str, JavascriptSocketFamily), SidecarError> {
10784    match (family, host) {
10785        (JavascriptUdpFamily::Ipv4, None) | (JavascriptUdpFamily::Ipv4, Some("0.0.0.0")) => {
10786            Ok(("127.0.0.1", "0.0.0.0", JavascriptSocketFamily::Ipv4))
10787        }
10788        (JavascriptUdpFamily::Ipv4, Some("127.0.0.1"))
10789        | (JavascriptUdpFamily::Ipv4, Some("localhost")) => {
10790            Ok(("127.0.0.1", "127.0.0.1", JavascriptSocketFamily::Ipv4))
10791        }
10792        (JavascriptUdpFamily::Ipv6, None) | (JavascriptUdpFamily::Ipv6, Some("::")) => {
10793            Ok(("::1", "::", JavascriptSocketFamily::Ipv6))
10794        }
10795        (JavascriptUdpFamily::Ipv6, Some("::1"))
10796        | (JavascriptUdpFamily::Ipv6, Some("localhost")) => {
10797            Ok(("::1", "::1", JavascriptSocketFamily::Ipv6))
10798        }
10799        (JavascriptUdpFamily::Ipv4, Some(other)) => Err(SidecarError::Execution(format!(
10800            "EACCES: udp4 sockets must bind to 127.0.0.1 or 0.0.0.0, got {other}"
10801        ))),
10802        (JavascriptUdpFamily::Ipv6, Some(other)) => Err(SidecarError::Execution(format!(
10803            "EACCES: udp6 sockets must bind to ::1 or ::, got {other}"
10804        ))),
10805    }
10806}
10807
10808fn allocate_guest_listen_port(
10809    requested_port: u16,
10810    family: JavascriptSocketFamily,
10811    used_ports: &BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10812    policy: VmListenPolicy,
10813) -> Result<u16, SidecarError> {
10814    let is_allowed = |port: u16| {
10815        port >= policy.port_min
10816            && port <= policy.port_max
10817            && (policy.allow_privileged || port >= 1024)
10818    };
10819    let used = used_ports.get(&family);
10820
10821    if requested_port != 0 {
10822        if !is_allowed(requested_port) {
10823            let reason = if requested_port < 1024 && !policy.allow_privileged {
10824                format!(
10825                    "EACCES: privileged listen port {requested_port} requires {}=true",
10826                    VM_LISTEN_ALLOW_PRIVILEGED_METADATA_KEY
10827                )
10828            } else {
10829                format!(
10830                    "EACCES: listen port {requested_port} is outside the allowed range {}-{}",
10831                    policy.port_min, policy.port_max
10832                )
10833            };
10834            return Err(SidecarError::Execution(reason));
10835        }
10836        if used.is_some_and(|ports| ports.contains(&requested_port)) {
10837            return Err(sidecar_net_error(std::io::Error::from_raw_os_error(
10838                libc::EADDRINUSE,
10839            )));
10840        }
10841        return Ok(requested_port);
10842    }
10843
10844    let allocation_start = policy
10845        .port_min
10846        .max(if policy.allow_privileged { 1 } else { 1024 });
10847    for candidate in allocation_start..=policy.port_max {
10848        if used.is_some_and(|ports| ports.contains(&candidate)) {
10849            continue;
10850        }
10851        return Ok(candidate);
10852    }
10853
10854    Err(sidecar_net_error(std::io::Error::from_raw_os_error(
10855        libc::EADDRINUSE,
10856    )))
10857}
10858
10859fn socket_host_matches(requested: Option<&str>, actual: &str) -> bool {
10860    match requested {
10861        None => true,
10862        Some(requested) if requested == actual => true,
10863        Some(requested)
10864            if is_unspecified_socket_host(requested) && is_unspecified_socket_host(actual) =>
10865        {
10866            true
10867        }
10868        Some(requested) if is_unspecified_socket_host(requested) => is_loopback_socket_host(actual),
10869        Some(requested) if requested.eq_ignore_ascii_case("localhost") => {
10870            is_loopback_socket_host(actual)
10871        }
10872        _ => false,
10873    }
10874}
10875
10876fn parse_proc_net_entries(table_path: &str) -> Result<Vec<ProcNetEntry>, SidecarError> {
10877    let contents = match fs::read_to_string(table_path) {
10878        Ok(contents) => contents,
10879        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
10880        Err(error) => {
10881            return Err(SidecarError::Io(format!(
10882                "failed to inspect socket table {table_path}: {error}"
10883            )));
10884        }
10885    };
10886
10887    let mut entries = Vec::new();
10888    for line in contents.lines().skip(1) {
10889        let columns = line.split_whitespace().collect::<Vec<_>>();
10890        if columns.len() < 10 {
10891            continue;
10892        }
10893        let Some((host, port)) = parse_proc_ip_port(columns[1]) else {
10894            continue;
10895        };
10896        let Ok(inode) = columns[9].parse::<u64>() else {
10897            continue;
10898        };
10899        entries.push(ProcNetEntry {
10900            local_host: host,
10901            local_port: port,
10902            state: columns[3].to_owned(),
10903            inode,
10904        });
10905    }
10906
10907    Ok(entries)
10908}
10909
10910fn parse_proc_ip_port(value: &str) -> Option<(String, u16)> {
10911    let (raw_ip, raw_port) = value.split_once(':')?;
10912    let port = u16::from_str_radix(raw_port, 16).ok()?;
10913    let host = match raw_ip.len() {
10914        8 => {
10915            let raw = u32::from_str_radix(raw_ip, 16).ok()?;
10916            Ipv4Addr::from(raw.to_le_bytes()).to_string()
10917        }
10918        32 => {
10919            let mut bytes = [0_u8; 16];
10920            for (index, chunk) in raw_ip.as_bytes().chunks(8).enumerate() {
10921                let word = u32::from_str_radix(std::str::from_utf8(chunk).ok()?, 16).ok()?;
10922                bytes[index * 4..(index + 1) * 4].copy_from_slice(&word.to_le_bytes());
10923            }
10924            Ipv6Addr::from(bytes).to_string()
10925        }
10926        _ => return None,
10927    };
10928    Some((host, port))
10929}
10930
10931fn python_file_entrypoint(entrypoint: &str) -> Option<PathBuf> {
10932    let path = Path::new(entrypoint);
10933    (path.extension().and_then(|extension| extension.to_str()) == Some("py"))
10934        .then(|| path.to_path_buf())
10935}
10936
10937fn add_runtime_guest_path_mapping(
10938    env: &mut BTreeMap<String, String>,
10939    guest_path: &str,
10940    host_path: &Path,
10941) {
10942    let mut mappings = env
10943        .get("AGENTOS_GUEST_PATH_MAPPINGS")
10944        .and_then(|value| serde_json::from_str::<Vec<Value>>(value).ok())
10945        .unwrap_or_default();
10946    mappings.retain(|mapping| {
10947        mapping
10948            .get("guestPath")
10949            .and_then(Value::as_str)
10950            .map(|existing| normalize_path(existing) != normalize_path(guest_path))
10951            .unwrap_or(true)
10952    });
10953    mappings.push(json!({
10954        "guestPath": normalize_path(guest_path),
10955        "hostPath": host_path.display().to_string(),
10956    }));
10957    if let Ok(serialized) = serde_json::to_string(&mappings) {
10958        env.insert(String::from("AGENTOS_GUEST_PATH_MAPPINGS"), serialized);
10959    }
10960}
10961
10962fn add_runtime_host_access_path(
10963    env: &mut BTreeMap<String, String>,
10964    key: &str,
10965    host_path: &Path,
10966    expand: bool,
10967) {
10968    let existing = env
10969        .get(key)
10970        .and_then(|value| serde_json::from_str::<Vec<String>>(value).ok())
10971        .unwrap_or_default()
10972        .into_iter()
10973        .map(PathBuf::from)
10974        .collect::<Vec<_>>();
10975    let mut paths = existing;
10976    paths.push(host_path.to_path_buf());
10977    let normalized = if expand {
10978        expand_host_access_paths(&paths)
10979    } else {
10980        dedupe_host_paths(&paths)
10981    };
10982    let serialized = normalized
10983        .iter()
10984        .map(|path| path.to_string_lossy().into_owned())
10985        .collect::<Vec<_>>();
10986    if let Ok(serialized) = serde_json::to_string(&serialized) {
10987        env.insert(key.to_owned(), serialized);
10988    }
10989}
10990
10991// discover_command_guest_paths moved to crate::bootstrap
10992
10993fn is_path_like_specifier(specifier: &str) -> bool {
10994    specifier.starts_with('/')
10995        || specifier.starts_with("./")
10996        || specifier.starts_with("../")
10997        || specifier.starts_with("file:")
10998}
10999
11000fn execution_wasm_permission_tier(tier: WasmPermissionTier) -> ExecutionWasmPermissionTier {
11001    match tier {
11002        WasmPermissionTier::Full => ExecutionWasmPermissionTier::Full,
11003        WasmPermissionTier::ReadWrite => ExecutionWasmPermissionTier::ReadWrite,
11004        WasmPermissionTier::ReadOnly => ExecutionWasmPermissionTier::ReadOnly,
11005        WasmPermissionTier::Isolated => ExecutionWasmPermissionTier::Isolated,
11006    }
11007}
11008
11009fn resolve_wasm_permission_tier(
11010    vm: &VmState,
11011    command_name: Option<&str>,
11012    explicit_tier: Option<WasmPermissionTier>,
11013    entrypoint: &str,
11014) -> WasmPermissionTier {
11015    explicit_tier
11016        .or_else(|| command_name.and_then(|command| vm.command_permissions.get(command).copied()))
11017        .or_else(|| {
11018            Path::new(entrypoint)
11019                .file_name()
11020                .and_then(|name| name.to_str())
11021                .and_then(|command| vm.command_permissions.get(command).copied())
11022        })
11023        .unwrap_or(WasmPermissionTier::Full)
11024}
11025
11026fn tokenize_shell_free_command(command: &str) -> Vec<String> {
11027    command
11028        .split_whitespace()
11029        .filter(|segment| !segment.is_empty())
11030        .map(str::to_owned)
11031        .collect()
11032}
11033
11034fn is_posix_shell_builtin(command: &str) -> bool {
11035    matches!(
11036        command,
11037        "." | ":"
11038            | "break"
11039            | "cd"
11040            | "continue"
11041            | "eval"
11042            | "exec"
11043            | "exit"
11044            | "export"
11045            | "readonly"
11046            | "return"
11047            | "set"
11048            | "shift"
11049            | "times"
11050            | "trap"
11051            | "umask"
11052            | "unset"
11053    )
11054}
11055
11056/// Single-token checks for shell-mode commands whose first word forces a real
11057/// shell even when the command string has no shell metacharacters. This is not
11058/// a parser: env-assignment prefixes (`FOO=bar cmd`) and shell reserved words
11059/// have no meaning outside `sh`, so whitespace-tokenizing them would silently
11060/// run the wrong program.
11061fn shell_first_token_requires_shell(token: &str) -> bool {
11062    token.contains('=') || is_shell_reserved_word(token)
11063}
11064
11065fn is_shell_reserved_word(token: &str) -> bool {
11066    matches!(
11067        token,
11068        "if" | "then"
11069            | "elif"
11070            | "else"
11071            | "fi"
11072            | "for"
11073            | "in"
11074            | "do"
11075            | "done"
11076            | "while"
11077            | "until"
11078            | "case"
11079            | "esac"
11080            | "{"
11081            | "}"
11082            | "!"
11083    )
11084}
11085
11086fn command_requires_shell(command: &str) -> bool {
11087    command.chars().any(|ch| {
11088        matches!(
11089            ch,
11090            '|' | '&'
11091                | ';'
11092                | '<'
11093                | '>'
11094                | '('
11095                | ')'
11096                | '$'
11097                | '`'
11098                | '*'
11099                | '?'
11100                | '['
11101                | ']'
11102                | '{'
11103                | '}'
11104                | '~'
11105                | '\''
11106                | '"'
11107                | '\\'
11108                | '\n'
11109        )
11110    })
11111}
11112
11113fn host_mount_path_for_guest_path(vm: &VmState, guest_path: &str) -> Option<PathBuf> {
11114    let normalized = normalize_path(guest_path);
11115
11116    let mut mounts = vm
11117        .configuration
11118        .mounts
11119        .iter()
11120        .filter_map(|mount| {
11121            ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
11122                .then(|| {
11123                    mount_config_host_path(&mount.plugin.config)
11124                        .map(|host_path| (mount.guest_path.as_str(), host_path))
11125                })
11126                .flatten()
11127        })
11128        .collect::<Vec<_>>();
11129    mounts.sort_by_key(|mount| std::cmp::Reverse(mount.0.len()));
11130
11131    for (guest_root, host_root) in mounts {
11132        if normalized != guest_root && !normalized.starts_with(&format!("{guest_root}/")) {
11133            continue;
11134        }
11135
11136        let suffix = normalized
11137            .strip_prefix(guest_root)
11138            .unwrap_or_default()
11139            .trim_start_matches('/');
11140        let mut path = PathBuf::from(host_root);
11141        if !suffix.is_empty() {
11142            path.push(suffix);
11143        }
11144        return Some(path);
11145    }
11146
11147    None
11148}
11149
11150fn host_runtime_path_for_guest_path_with_env(
11151    vm: &VmState,
11152    runtime_env: &BTreeMap<String, String>,
11153    guest_path: &str,
11154    default_host_cwd: &Path,
11155) -> Option<PathBuf> {
11156    if let Some(path) = host_mount_path_for_guest_path(vm, guest_path) {
11157        return Some(path);
11158    }
11159    if let Some(path) = host_path_from_runtime_guest_mappings(runtime_env, guest_path) {
11160        return Some(path);
11161    }
11162
11163    let normalized = normalize_path(guest_path);
11164    let virtual_home = guest_virtual_home(vm);
11165
11166    if normalized == virtual_home || normalized.starts_with(&format!("{virtual_home}/")) {
11167        let suffix = normalized
11168            .strip_prefix(&virtual_home)
11169            .unwrap_or_default()
11170            .trim_start_matches('/');
11171        let mut host_path = default_host_cwd.to_path_buf();
11172        if !suffix.is_empty() {
11173            host_path.push(suffix);
11174        }
11175        return Some(host_path);
11176    }
11177
11178    None
11179}
11180
11181#[derive(Deserialize, Serialize)]
11182struct RuntimeGuestPathMapping {
11183    #[serde(rename = "guestPath")]
11184    guest_path: String,
11185    #[serde(rename = "hostPath")]
11186    host_path: String,
11187    #[serde(rename = "readOnly", default)]
11188    read_only: bool,
11189}
11190
11191pub(crate) fn host_path_from_runtime_guest_mappings(
11192    runtime_env: &BTreeMap<String, String>,
11193    guest_path: &str,
11194) -> Option<PathBuf> {
11195    let mappings = runtime_env
11196        .get("AGENTOS_GUEST_PATH_MAPPINGS")
11197        .and_then(|value| serde_json::from_str::<Vec<RuntimeGuestPathMapping>>(value).ok())?;
11198    let normalized = normalize_path(guest_path);
11199
11200    let mut sorted_mappings = mappings
11201        .into_iter()
11202        .filter_map(|mapping| {
11203            (!mapping.guest_path.is_empty() && !mapping.host_path.is_empty()).then_some((
11204                normalize_path(&mapping.guest_path),
11205                PathBuf::from(mapping.host_path),
11206            ))
11207        })
11208        .collect::<Vec<_>>();
11209    sorted_mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.0.len()));
11210
11211    for (guest_root, mut host_root) in sorted_mappings {
11212        if guest_root != "/"
11213            && normalized != guest_root
11214            && !normalized.starts_with(&format!("{guest_root}/"))
11215        {
11216            continue;
11217        }
11218        if guest_root == "/" && !normalized.starts_with('/') {
11219            continue;
11220        }
11221
11222        if host_root.is_relative() {
11223            host_root = std::env::current_dir().ok()?.join(host_root);
11224        }
11225
11226        let suffix = if guest_root == "/" {
11227            normalized.trim_start_matches('/')
11228        } else {
11229            normalized
11230                .strip_prefix(&guest_root)
11231                .unwrap_or_default()
11232                .trim_start_matches('/')
11233        };
11234        if !suffix.is_empty() {
11235            host_root.push(suffix);
11236        }
11237        return Some(host_root);
11238    }
11239
11240    None
11241}
11242
11243fn guest_runtime_path_for_host_path(
11244    runtime_env: &BTreeMap<String, String>,
11245    virtual_home: &str,
11246    cwd: &Path,
11247    host_path: &str,
11248) -> Option<String> {
11249    let resolved = if host_path.starts_with("file://") {
11250        PathBuf::from(host_path.trim_start_matches("file://"))
11251    } else if host_path.starts_with("file:") {
11252        PathBuf::from(host_path.trim_start_matches("file:"))
11253    } else {
11254        let candidate = PathBuf::from(host_path);
11255        if candidate.is_absolute() {
11256            candidate
11257        } else if host_path.starts_with("./") || host_path.starts_with("../") {
11258            cwd.join(candidate)
11259        } else {
11260            return None;
11261        }
11262    };
11263    let normalized = normalize_host_path(&resolved);
11264
11265    if let Some(path) = guest_path_from_runtime_host_mappings(runtime_env, &normalized) {
11266        return Some(path);
11267    }
11268
11269    let normalized_cwd = normalize_host_path(cwd);
11270    if !path_is_within_root(&normalized, &normalized_cwd) {
11271        return None;
11272    }
11273
11274    let virtual_home = if virtual_home.starts_with('/') {
11275        virtual_home.to_string()
11276    } else {
11277        String::from("/root")
11278    };
11279    let suffix = normalized
11280        .strip_prefix(&normalized_cwd)
11281        .ok()?
11282        .to_string_lossy()
11283        .replace('\\', "/")
11284        .trim_start_matches('/')
11285        .to_owned();
11286
11287    Some(if suffix.is_empty() {
11288        virtual_home
11289    } else {
11290        normalize_path(&format!("{virtual_home}/{suffix}"))
11291    })
11292}
11293
11294fn guest_path_from_runtime_host_mappings(
11295    runtime_env: &BTreeMap<String, String>,
11296    host_path: &Path,
11297) -> Option<String> {
11298    let mappings = runtime_env
11299        .get("AGENTOS_GUEST_PATH_MAPPINGS")
11300        .and_then(|value| serde_json::from_str::<Vec<RuntimeGuestPathMapping>>(value).ok())?;
11301    let normalized = normalize_host_path(host_path);
11302
11303    let mut sorted_mappings = mappings
11304        .into_iter()
11305        .filter_map(|mapping| {
11306            (!mapping.guest_path.is_empty() && !mapping.host_path.is_empty()).then_some((
11307                normalize_path(&mapping.guest_path),
11308                normalize_host_path(Path::new(&mapping.host_path)),
11309            ))
11310        })
11311        .collect::<Vec<_>>();
11312    sorted_mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.1.as_os_str().len()));
11313
11314    for (guest_root, host_root) in sorted_mappings {
11315        if !path_is_within_root(&normalized, &host_root) {
11316            continue;
11317        }
11318        let suffix = normalized
11319            .strip_prefix(&host_root)
11320            .ok()?
11321            .to_string_lossy()
11322            .replace('\\', "/")
11323            .trim_start_matches('/')
11324            .to_owned();
11325
11326        return Some(if suffix.is_empty() {
11327            guest_root
11328        } else if guest_root == "/" {
11329            normalize_path(&format!("/{suffix}"))
11330        } else {
11331            normalize_path(&format!("{guest_root}/{suffix}"))
11332        });
11333    }
11334
11335    None
11336}
11337
11338fn host_mount_path_for_guest_path_from_mounts(
11339    mounts: &[crate::protocol::MountDescriptor],
11340    guest_path: &str,
11341) -> Option<PathBuf> {
11342    let normalized = normalize_path(guest_path);
11343
11344    let mut host_mounts = mounts
11345        .iter()
11346        .filter_map(|mount| {
11347            ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
11348                .then(|| {
11349                    mount_config_host_path(&mount.plugin.config)
11350                        .map(|host_path| (mount.guest_path.as_str(), host_path))
11351                })
11352                .flatten()
11353        })
11354        .collect::<Vec<_>>();
11355    host_mounts.sort_by_key(|mount| std::cmp::Reverse(mount.0.len()));
11356
11357    for (guest_root, host_root) in host_mounts {
11358        if normalized != guest_root && !normalized.starts_with(&format!("{guest_root}/")) {
11359            continue;
11360        }
11361
11362        let suffix = normalized
11363            .strip_prefix(guest_root)
11364            .unwrap_or_default()
11365            .trim_start_matches('/');
11366        let mut path = PathBuf::from(host_root);
11367        if !suffix.is_empty() {
11368            path.push(suffix);
11369        }
11370        return Some(path);
11371    }
11372
11373    None
11374}
11375
11376#[cfg(test)]
11377mod host_mount_path_for_guest_path_from_mounts_tests {
11378    use super::host_mount_path_for_guest_path_from_mounts;
11379    use crate::protocol::{MountDescriptor, MountPluginDescriptor};
11380    use serde_json::json;
11381    use std::path::PathBuf;
11382
11383    #[test]
11384    fn resolves_module_access_mount_paths() {
11385        let mounts = vec![MountDescriptor {
11386            guest_path: String::from("/root/node_modules"),
11387            read_only: true,
11388            plugin: MountPluginDescriptor {
11389                id: String::from("module_access"),
11390                config: json!({
11391                    "hostPath": "/tmp/workspace/node_modules",
11392                })
11393                .to_string(),
11394            },
11395        }];
11396
11397        let resolved =
11398            host_mount_path_for_guest_path_from_mounts(&mounts, "/root/node_modules/pkg/index.js")
11399                .expect("module_access mount should resolve");
11400
11401        assert_eq!(
11402            resolved,
11403            PathBuf::from("/tmp/workspace/node_modules/pkg/index.js")
11404        );
11405    }
11406}
11407
11408fn resolve_guest_socket_host_path(
11409    context: &JavascriptSocketPathContext,
11410    guest_path: &str,
11411) -> PathBuf {
11412    if let Some(path) = host_mount_path_for_guest_path_from_mounts(&context.mounts, guest_path) {
11413        return path;
11414    }
11415
11416    let normalized = normalize_path(guest_path);
11417    let mut host_path = context.sandbox_root.clone();
11418    let suffix = normalized.trim_start_matches('/');
11419    if !suffix.is_empty() {
11420        host_path.push(suffix);
11421    }
11422    host_path
11423}
11424
11425fn ensure_kernel_parent_directories(
11426    kernel: &mut SidecarKernel,
11427    path: &str,
11428) -> Result<(), SidecarError> {
11429    let parent = dirname(path);
11430    if parent != "/" && !kernel.exists(&parent).map_err(kernel_error)? {
11431        kernel.mkdir(&parent, true).map_err(kernel_error)?;
11432    }
11433    Ok(())
11434}
11435
11436// JavascriptChildProcessSpawnOptions, JavascriptChildProcessSpawnRequest moved to crate::protocol
11437// ResolvedChildProcessExecution moved to crate::state
11438
11439pub(crate) fn sanitize_javascript_child_process_internal_bootstrap_env(
11440    env: &BTreeMap<String, String>,
11441) -> BTreeMap<String, String> {
11442    const ALLOWED_KEYS: &[&str] = &[
11443        "AGENTOS_ALLOWED_NODE_BUILTINS",
11444        "AGENTOS_GUEST_PATH_MAPPINGS",
11445        "AGENTOS_LOOPBACK_EXEMPT_PORTS",
11446        "AGENTOS_VIRTUAL_PROCESS_EXEC_PATH",
11447        "AGENTOS_VIRTUAL_PROCESS_UID",
11448        "AGENTOS_VIRTUAL_PROCESS_GID",
11449        "AGENTOS_VIRTUAL_PROCESS_VERSION",
11450    ];
11451
11452    env.iter()
11453        .filter(|(key, _)| {
11454            ALLOWED_KEYS.contains(&key.as_str()) || key.starts_with("AGENTOS_VIRTUAL_OS_")
11455        })
11456        .map(|(key, value)| (key.clone(), value.clone()))
11457        .collect()
11458}
11459
11460// Network request types moved to crate::protocol
11461
11462// VmDnsConfig, DnsResolutionSource moved to crate::state
11463
11464fn resolve_tcp_bind_addr(host: &str, port: u16) -> Result<SocketAddr, SidecarError> {
11465    (host, port)
11466        .to_socket_addrs()
11467        .map_err(sidecar_net_error)?
11468        .next()
11469        .ok_or_else(|| {
11470            SidecarError::Execution(format!("failed to resolve TCP bind address {host}:{port}"))
11471        })
11472}
11473
11474pub(crate) fn format_dns_resource(hostname: &str) -> String {
11475    format!("dns://{hostname}")
11476}
11477
11478pub(crate) fn format_tcp_resource(host: &str, port: u16) -> String {
11479    format!("tcp://{host}:{port}")
11480}
11481
11482fn is_loopback_ip(ip: IpAddr) -> bool {
11483    match ip {
11484        IpAddr::V4(ip) => ip.is_loopback(),
11485        IpAddr::V6(ip) => {
11486            ip.is_loopback()
11487                || ip
11488                    .to_ipv4_mapped()
11489                    .is_some_and(|mapped| mapped.is_loopback())
11490        }
11491    }
11492}
11493
11494fn loopback_cidr(ip: IpAddr) -> &'static str {
11495    match ip {
11496        IpAddr::V4(ip) if ip.is_loopback() => "127.0.0.0/8",
11497        IpAddr::V6(ip)
11498            if ip
11499                .to_ipv4_mapped()
11500                .is_some_and(|mapped| mapped.is_loopback()) =>
11501        {
11502            "127.0.0.0/8"
11503        }
11504        IpAddr::V6(_) => "::1/128",
11505        IpAddr::V4(_) => "127.0.0.0/8",
11506    }
11507}
11508
11509/// Returns the embedded IPv4 address of an IPv4-compatible IPv6 address
11510/// (`::a.b.c.d`): the first six 16-bit segments are zero and the final 32 bits
11511/// hold the IPv4 address. The all-zero (`::`) and loopback (`::1`) addresses are
11512/// deliberately excluded so they are handled by the unspecified/loopback paths
11513/// rather than treated as IPv4-compatible.
11514fn ipv4_compatible_embedded(ip: Ipv6Addr) -> Option<Ipv4Addr> {
11515    let segments = ip.segments();
11516    if segments[0..6].iter().any(|&s| s != 0) {
11517        return None;
11518    }
11519    let embedded = (u32::from(segments[6]) << 16) | u32::from(segments[7]);
11520    // Skip :: (0.0.0.0) and ::1 (0.0.0.1) — these are the IPv6 unspecified /
11521    // loopback addresses, not IPv4-compatible representations of an IPv4 host.
11522    if embedded == 0 || embedded == 1 {
11523        return None;
11524    }
11525    Some(Ipv4Addr::from(embedded))
11526}
11527
11528fn restricted_non_loopback_ip_range(ip: IpAddr) -> Option<(&'static str, &'static str)> {
11529    match ip {
11530        IpAddr::V4(ip) => {
11531            if ip.is_unspecified() {
11532                // 0.0.0.0 is unspecified; the host stack routes a connect() to
11533                // it back to 127.0.0.1, so it must not bypass the loopback gate.
11534                return Some(("0.0.0.0/32", "unspecified"));
11535            }
11536            let [first, second, ..] = ip.octets();
11537            match (first, second) {
11538                (10, _) => Some(("10.0.0.0/8", "private")),
11539                (100, 64..=127) => Some(("100.64.0.0/10", "carrier-grade-nat")),
11540                (172, 16..=31) => Some(("172.16.0.0/12", "private")),
11541                (192, 168) => Some(("192.168.0.0/16", "private")),
11542                (169, 254) => Some(("169.254.0.0/16", "link-local")),
11543                // 224.0.0.0/4 is the IPv4 multicast range and 240.0.0.0/4 is
11544                // reserved/future-use (255.255.255.255 broadcast falls in it).
11545                // Neither is a legitimate unicast egress target, so a guest
11546                // connect to them must be denied rather than attempted.
11547                (224..=239, _) => Some(("224.0.0.0/4", "multicast")),
11548                (240..=255, _) => Some(("240.0.0.0/4", "reserved")),
11549                _ => None,
11550            }
11551        }
11552        IpAddr::V6(ip) => {
11553            if let Some(mapped) = ip.to_ipv4_mapped() {
11554                return restricted_non_loopback_ip_range(IpAddr::V4(mapped));
11555            }
11556            // IPv4-compatible IPv6 (::a.b.c.d): the first six segments are zero
11557            // and the last two carry an embedded IPv4 address. `to_ipv4_mapped`
11558            // returns None for this form, so without canonicalizing it here a
11559            // guest could spell a restricted IPv4 target (e.g. cloud-metadata
11560            // ::169.254.169.254) and bypass the IPv4 classifier. `::`/`::1` are
11561            // excluded so they fall through to the unspecified/loopback paths.
11562            if let Some(compat) = ipv4_compatible_embedded(ip) {
11563                return restricted_non_loopback_ip_range(IpAddr::V4(compat));
11564            }
11565
11566            if ip.is_unspecified() {
11567                // :: is the IPv6 unspecified address; same routing hazard as
11568                // 0.0.0.0, so deny it rather than letting it reach the host.
11569                return Some(("::/128", "unspecified"));
11570            }
11571
11572            let segments = ip.segments();
11573            if (segments[0] & 0xfe00) == 0xfc00 {
11574                return Some(("fc00::/7", "unique-local"));
11575            }
11576            if (segments[0] & 0xffc0) == 0xfe80 {
11577                return Some(("fe80::/10", "link-local"));
11578            }
11579            None
11580        }
11581    }
11582}
11583
11584fn blocked_dns_resolution_error(
11585    resource: &str,
11586    ip: IpAddr,
11587    cidr: &str,
11588    label: &str,
11589) -> SidecarError {
11590    SidecarError::Execution(format!(
11591        "EACCES: blocked outbound network access to {resource}: {ip} is within restricted {label} range {cidr}"
11592    ))
11593}
11594
11595fn blocked_loopback_connect_error(resource: &str, ip: IpAddr, port: u16) -> SidecarError {
11596    SidecarError::Execution(format!(
11597        "EACCES: blocked outbound network access to {resource}: {ip} is loopback ({}) and port {port} is not owned by this VM and is not listed in {LOOPBACK_EXEMPT_PORTS_ENV}",
11598        loopback_cidr(ip)
11599    ))
11600}
11601
11602fn filter_dns_safe_ip_addrs(
11603    addresses: Vec<IpAddr>,
11604    hostname: &str,
11605) -> Result<Vec<IpAddr>, SidecarError> {
11606    let resource = format_dns_resource(hostname);
11607    let mut allowed = Vec::new();
11608    let mut blocked = None;
11609
11610    for ip in addresses {
11611        if let Some((cidr, label)) = restricted_non_loopback_ip_range(ip) {
11612            blocked.get_or_insert((ip, cidr, label));
11613            continue;
11614        }
11615        allowed.push(ip);
11616    }
11617
11618    if allowed.is_empty() {
11619        let (ip, cidr, label) = blocked.expect("blocked DNS results should capture a reason");
11620        return Err(blocked_dns_resolution_error(&resource, ip, cidr, label));
11621    }
11622
11623    Ok(allowed)
11624}
11625
11626fn loopback_connect_allowed(context: &JavascriptSocketPathContext, port: u16) -> bool {
11627    context.loopback_port_allowed(port)
11628}
11629
11630fn filter_tcp_connect_ip_addrs(
11631    addresses: Vec<IpAddr>,
11632    host: &str,
11633    port: u16,
11634    context: &JavascriptSocketPathContext,
11635) -> Result<Vec<IpAddr>, SidecarError> {
11636    let resource = format_tcp_resource(host, port);
11637    let mut allowed = Vec::new();
11638    let mut blocked = None;
11639
11640    for ip in addresses {
11641        if let Some((cidr, label)) = restricted_non_loopback_ip_range(ip) {
11642            blocked.get_or_insert_with(|| blocked_dns_resolution_error(&resource, ip, cidr, label));
11643            continue;
11644        }
11645        if is_loopback_ip(ip) && !loopback_connect_allowed(context, port) {
11646            blocked.get_or_insert_with(|| blocked_loopback_connect_error(&resource, ip, port));
11647            continue;
11648        }
11649        allowed.push(ip);
11650    }
11651
11652    if allowed.is_empty() {
11653        return Err(blocked.expect("blocked TCP connect results should capture a reason"));
11654    }
11655
11656    Ok(allowed)
11657}
11658
11659fn resolve_tcp_connect_addr<B>(
11660    bridge: &SharedBridge<B>,
11661    kernel: &SidecarKernel,
11662    vm_id: &str,
11663    dns: &VmDnsConfig,
11664    host: &str,
11665    port: u16,
11666    context: &JavascriptSocketPathContext,
11667) -> Result<ResolvedTcpConnectAddr, SidecarError>
11668where
11669    B: NativeSidecarBridge + Send + 'static,
11670    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11671{
11672    let allowed = filter_tcp_connect_ip_addrs(
11673        resolve_dns_ip_addrs(
11674            bridge,
11675            kernel,
11676            vm_id,
11677            dns,
11678            host,
11679            DnsLookupPolicy::SkipPermissions,
11680        )?,
11681        host,
11682        port,
11683        context,
11684    )?;
11685    let ip = allowed
11686        .iter()
11687        .copied()
11688        .find(|candidate| {
11689            let family = JavascriptSocketFamily::from_ip(*candidate);
11690            context.translate_tcp_loopback_port(family, port).is_some()
11691        })
11692        // We do not implement Happy Eyeballs yet, so prefer IPv4 over a
11693        // verbatim IPv6-first DNS answer for general outbound TCP connects.
11694        .or_else(|| allowed.iter().copied().find(IpAddr::is_ipv4))
11695        .or_else(|| allowed.first().copied())
11696        .ok_or_else(|| {
11697            SidecarError::Execution(format!("failed to resolve TCP address {host}:{port}"))
11698        })?;
11699    let family = JavascriptSocketFamily::from_ip(ip);
11700    let translated_loopback_port = context.translate_tcp_loopback_port(family, port);
11701    let use_kernel_loopback = is_loopback_ip(ip) && translated_loopback_port == Some(port);
11702    let actual_port = if is_loopback_ip(ip) {
11703        translated_loopback_port.unwrap_or(port)
11704    } else {
11705        port
11706    };
11707    Ok(ResolvedTcpConnectAddr {
11708        actual_addr: SocketAddr::new(ip, actual_port),
11709        guest_remote_addr: SocketAddr::new(ip, port),
11710        use_kernel_loopback,
11711    })
11712}
11713
11714fn resolve_dns_ip_addrs<B>(
11715    bridge: &SharedBridge<B>,
11716    kernel: &SidecarKernel,
11717    vm_id: &str,
11718    dns: &VmDnsConfig,
11719    hostname: &str,
11720    policy: DnsLookupPolicy,
11721) -> Result<Vec<IpAddr>, SidecarError>
11722where
11723    B: NativeSidecarBridge + Send + 'static,
11724    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11725{
11726    let resolution = match kernel.resolve_dns(hostname, policy) {
11727        Ok(resolution) => resolution,
11728        Err(error) => {
11729            let sidecar_error = kernel_error(error.clone());
11730            if error.code() != "EACCES" {
11731                emit_dns_resolution_failure_event(bridge, vm_id, hostname, dns, &sidecar_error);
11732            }
11733            return Err(sidecar_error);
11734        }
11735    };
11736    emit_dns_resolution_event(
11737        bridge,
11738        vm_id,
11739        hostname,
11740        resolution.source(),
11741        resolution.addresses(),
11742        dns,
11743    );
11744    Ok(resolution.addresses().to_vec())
11745}
11746
11747fn resolve_dns_records<B>(
11748    bridge: &SharedBridge<B>,
11749    kernel: &SidecarKernel,
11750    vm_id: &str,
11751    dns: &VmDnsConfig,
11752    hostname: &str,
11753    record_type: RecordType,
11754    policy: DnsLookupPolicy,
11755) -> Result<DnsRecordResolution, SidecarError>
11756where
11757    B: NativeSidecarBridge + Send + 'static,
11758    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11759{
11760    let resolution = match kernel.resolve_dns_records(hostname, record_type, policy) {
11761        Ok(resolution) => resolution,
11762        Err(error) => {
11763            let sidecar_error = kernel_error(error.clone());
11764            if error.code() != "EACCES" {
11765                emit_dns_resolution_failure_event(bridge, vm_id, hostname, dns, &sidecar_error);
11766            }
11767            return Err(sidecar_error);
11768        }
11769    };
11770    emit_dns_record_resolution_event(bridge, vm_id, hostname, &resolution, dns);
11771    Ok(resolution)
11772}
11773
11774fn filter_dns_ip_addrs(
11775    addresses: Vec<IpAddr>,
11776    family: Option<u8>,
11777) -> Result<Vec<IpAddr>, SidecarError> {
11778    let filtered: Vec<_> = match family.unwrap_or(0) {
11779        0 => addresses,
11780        4 => addresses
11781            .into_iter()
11782            .filter(|ip| matches!(ip, IpAddr::V4(_)))
11783            .collect(),
11784        6 => addresses
11785            .into_iter()
11786            .filter(|ip| matches!(ip, IpAddr::V6(_)))
11787            .collect(),
11788        other => {
11789            return Err(SidecarError::InvalidState(format!(
11790                "unsupported dns family {other}"
11791            )));
11792        }
11793    };
11794
11795    if filtered.is_empty() {
11796        return Err(SidecarError::Execution(String::from(
11797            "failed to resolve DNS address for requested family",
11798        )));
11799    }
11800
11801    Ok(filtered)
11802}
11803
11804fn resolve_udp_bind_addr(
11805    host: &str,
11806    port: u16,
11807    family: JavascriptUdpFamily,
11808) -> Result<SocketAddr, SidecarError> {
11809    (host, port)
11810        .to_socket_addrs()
11811        .map_err(sidecar_net_error)?
11812        .find(|addr| family.matches_addr(addr))
11813        .ok_or_else(|| {
11814            SidecarError::Execution(format!(
11815                "failed to resolve {} UDP bind address {host}:{port}",
11816                family.socket_type()
11817            ))
11818        })
11819}
11820
11821fn resolve_udp_addr<B>(request: UdpRemoteAddrRequest<'_, B>) -> Result<SocketAddr, SidecarError>
11822where
11823    B: NativeSidecarBridge + Send + 'static,
11824    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11825{
11826    let UdpRemoteAddrRequest {
11827        bridge,
11828        kernel,
11829        vm_id,
11830        dns,
11831        host,
11832        port,
11833        family,
11834        context,
11835    } = request;
11836    resolve_dns_ip_addrs(
11837        bridge,
11838        kernel,
11839        vm_id,
11840        dns,
11841        host,
11842        DnsLookupPolicy::SkipPermissions,
11843    )?
11844    .into_iter()
11845    .map(|ip| {
11846        let family_key = JavascriptSocketFamily::from_ip(ip);
11847        let actual_port = if is_loopback_ip(ip) {
11848            context
11849                .translate_udp_loopback_port(family_key, port)
11850                .unwrap_or(port)
11851        } else {
11852            port
11853        };
11854        SocketAddr::new(ip, actual_port)
11855    })
11856    .find(|addr| family.matches_addr(addr))
11857    .ok_or_else(|| {
11858        SidecarError::Execution(format!(
11859            "failed to resolve {} UDP address {host}:{port}",
11860            family.socket_type()
11861        ))
11862    })
11863}
11864
11865fn socket_addr_family(addr: &SocketAddr) -> &'static str {
11866    match addr {
11867        SocketAddr::V4(_) => "IPv4",
11868        SocketAddr::V6(_) => "IPv6",
11869    }
11870}
11871
11872fn javascript_net_timeout_value() -> Value {
11873    Value::String(String::from(JAVASCRIPT_NET_TIMEOUT_SENTINEL))
11874}
11875
11876fn javascript_net_json_string(value: Value, label: &str) -> Result<Value, SidecarError> {
11877    serde_json::to_string(&value)
11878        .map(Value::String)
11879        .map_err(|error| {
11880            SidecarError::InvalidState(format!("failed to serialize {label} payload: {error}"))
11881        })
11882}
11883
11884fn javascript_net_read_value(
11885    event: Option<JavascriptTcpSocketEvent>,
11886) -> Result<Value, SidecarError> {
11887    match event {
11888        Some(JavascriptTcpSocketEvent::Data(chunk)) => Ok(Value::String(
11889            base64::engine::general_purpose::STANDARD.encode(chunk),
11890        )),
11891        Some(JavascriptTcpSocketEvent::End | JavascriptTcpSocketEvent::Close { .. }) => {
11892            Ok(Value::Null)
11893        }
11894        Some(JavascriptTcpSocketEvent::Error { code, message }) => {
11895            let detail = code.unwrap_or_else(|| String::from("socket read"));
11896            Err(SidecarError::Execution(format!("{detail}: {message}")))
11897        }
11898        None => Ok(javascript_net_timeout_value()),
11899    }
11900}
11901
11902fn io_error_code(error: &std::io::Error) -> Option<String> {
11903    match error.raw_os_error() {
11904        Some(libc::EADDRINUSE) => Some(String::from("EADDRINUSE")),
11905        Some(libc::EADDRNOTAVAIL) => Some(String::from("EADDRNOTAVAIL")),
11906        Some(libc::ECONNREFUSED) => Some(String::from("ECONNREFUSED")),
11907        Some(libc::ECONNRESET) => Some(String::from("ECONNRESET")),
11908        Some(libc::EINVAL) => Some(String::from("EINVAL")),
11909        Some(libc::EPIPE) => Some(String::from("EPIPE")),
11910        Some(libc::ETIMEDOUT) => Some(String::from("ETIMEDOUT")),
11911        Some(libc::EHOSTUNREACH) => Some(String::from("EHOSTUNREACH")),
11912        Some(libc::ENETUNREACH) => Some(String::from("ENETUNREACH")),
11913        _ => None,
11914    }
11915}
11916
11917fn sidecar_net_error(error: std::io::Error) -> SidecarError {
11918    let message = match io_error_code(&error) {
11919        Some(code) => format!("{code}: {error}"),
11920        None => error.to_string(),
11921    };
11922    SidecarError::Execution(message)
11923}
11924
11925fn tls_provider() -> Arc<rustls::crypto::CryptoProvider> {
11926    Arc::new(aws_lc_rs::default_provider())
11927}
11928
11929fn tls_local_certificates(
11930    options: &JavascriptTlsBridgeOptions,
11931) -> Result<Vec<Vec<u8>>, SidecarError> {
11932    let Some(certificates) = options.cert.as_ref() else {
11933        return Ok(Vec::new());
11934    };
11935    tls_material_entries(certificates)
11936}
11937
11938fn tls_material_entries(material: &JavascriptTlsMaterial) -> Result<Vec<Vec<u8>>, SidecarError> {
11939    match material {
11940        JavascriptTlsMaterial::Single(entry) => tls_data_value(entry).map(|value| vec![value]),
11941        JavascriptTlsMaterial::Many(entries) => entries.iter().map(tls_data_value).collect(),
11942    }
11943}
11944
11945fn tls_data_value(value: &JavascriptTlsDataValue) -> Result<Vec<u8>, SidecarError> {
11946    match value {
11947        JavascriptTlsDataValue::Buffer { data } => base64::engine::general_purpose::STANDARD
11948            .decode(data)
11949            .map_err(|error| {
11950                SidecarError::InvalidState(format!("TLS material contains invalid base64: {error}"))
11951            }),
11952        JavascriptTlsDataValue::String { data } => Ok(data.as_bytes().to_vec()),
11953    }
11954}
11955
11956fn tls_certificates_from_material(
11957    material: &JavascriptTlsMaterial,
11958) -> Result<Vec<CertificateDer<'static>>, SidecarError> {
11959    let mut certificates = Vec::new();
11960    for entry in tls_material_entries(material)? {
11961        let mut reader = std::io::BufReader::new(Cursor::new(entry.clone()));
11962        let parsed = rustls_pemfile::certs(&mut reader)
11963            .collect::<Result<Vec<_>, _>>()
11964            .map_err(sidecar_net_error)?;
11965        if parsed.is_empty() {
11966            certificates.push(CertificateDer::from(entry));
11967        } else {
11968            certificates.extend(parsed);
11969        }
11970    }
11971    if certificates.is_empty() {
11972        return Err(SidecarError::InvalidState(String::from(
11973            "TLS certificate material did not contain any certificates",
11974        )));
11975    }
11976    Ok(certificates)
11977}
11978
11979fn tls_private_key_from_material(
11980    material: &JavascriptTlsMaterial,
11981) -> Result<PrivateKeyDer<'static>, SidecarError> {
11982    for entry in tls_material_entries(material)? {
11983        let mut reader = std::io::BufReader::new(Cursor::new(entry));
11984        if let Some(key) = rustls_pemfile::private_key(&mut reader).map_err(sidecar_net_error)? {
11985            return Ok(key);
11986        }
11987    }
11988    Err(SidecarError::InvalidState(String::from(
11989        "TLS private key material did not contain a supported key",
11990    )))
11991}
11992
11993fn tls_root_store(options: &JavascriptTlsBridgeOptions) -> Result<RootCertStore, SidecarError> {
11994    let mut roots = RootCertStore::empty();
11995    if let Some(ca) = options.ca.as_ref() {
11996        for certificate in tls_certificates_from_material(ca)? {
11997            roots.add(certificate).map_err(|error| {
11998                SidecarError::InvalidState(format!("failed to add TLS CA certificate: {error}"))
11999            })?;
12000        }
12001        return Ok(roots);
12002    }
12003
12004    for certificate in rustls_native_certs::load_native_certs().certs {
12005        roots.add(certificate).map_err(|error| {
12006            SidecarError::InvalidState(format!(
12007                "failed to add native TLS certificate to root store: {error}"
12008            ))
12009        })?;
12010    }
12011    Ok(roots)
12012}
12013
12014fn build_client_tls_stream(
12015    stream: TcpStream,
12016    options: &JavascriptTlsBridgeOptions,
12017) -> Result<rustls::StreamOwned<ClientConnection, TcpStream>, SidecarError> {
12018    let config = build_client_tls_config(options)?;
12019    let server_name = options
12020        .servername
12021        .clone()
12022        .unwrap_or_else(|| String::from("localhost"));
12023    let server_name = ServerName::try_from(server_name)
12024        .map_err(|_| SidecarError::InvalidState(String::from("invalid TLS servername")))?;
12025    stream
12026        .set_read_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
12027        .map_err(sidecar_net_error)?;
12028    stream
12029        .set_write_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
12030        .map_err(sidecar_net_error)?;
12031    let mut tls_stream = rustls::StreamOwned::new(
12032        ClientConnection::new(Arc::new(config), server_name).map_err(|error| {
12033            SidecarError::Execution(format!("failed to start TLS client: {error}"))
12034        })?,
12035        stream,
12036    );
12037    while tls_stream.conn.is_handshaking() {
12038        tls_stream
12039            .conn
12040            .complete_io(&mut tls_stream.sock)
12041            .map_err(sidecar_net_error)?;
12042    }
12043    tls_stream
12044        .sock
12045        .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
12046        .map_err(sidecar_net_error)?;
12047    tls_stream
12048        .sock
12049        .set_write_timeout(None)
12050        .map_err(sidecar_net_error)?;
12051    Ok(tls_stream)
12052}
12053
12054fn build_client_loopback_tls_stream(
12055    transport: crate::state::LoopbackTlsEndpoint,
12056    options: &JavascriptTlsBridgeOptions,
12057) -> Result<rustls::StreamOwned<ClientConnection, crate::state::LoopbackTlsEndpoint>, SidecarError>
12058{
12059    let config = build_client_tls_config(options)?;
12060    let server_name = options
12061        .servername
12062        .clone()
12063        .unwrap_or_else(|| String::from("localhost"));
12064    let server_name = ServerName::try_from(server_name)
12065        .map_err(|_| SidecarError::InvalidState(String::from("invalid TLS servername")))?;
12066    let mut tls_stream = rustls::StreamOwned::new(
12067        ClientConnection::new(Arc::new(config), server_name).map_err(|error| {
12068            SidecarError::Execution(format!("failed to start TLS client: {error}"))
12069        })?,
12070        transport,
12071    );
12072    match tls_stream.conn.complete_io(&mut tls_stream.sock) {
12073        Ok(_) => {}
12074        Err(error)
12075            if matches!(
12076                error.kind(),
12077                std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
12078            ) => {}
12079        Err(error) => return Err(sidecar_net_error(error)),
12080    }
12081    Ok(tls_stream)
12082}
12083
12084fn build_client_tls_config(
12085    options: &JavascriptTlsBridgeOptions,
12086) -> Result<ClientConfig, SidecarError> {
12087    let provider = tls_provider();
12088    let builder = ClientConfig::builder_with_provider(provider.clone())
12089        .with_safe_default_protocol_versions()
12090        .map_err(|error| {
12091            SidecarError::InvalidState(format!("invalid TLS protocol config: {error}"))
12092        })?;
12093
12094    let mut config = if options.reject_unauthorized == Some(false) {
12095        let verifier = Arc::new(InsecureTlsVerifier {
12096            supported_schemes: provider
12097                .signature_verification_algorithms
12098                .supported_schemes(),
12099        });
12100        builder
12101            .dangerous()
12102            .with_custom_certificate_verifier(verifier)
12103            .with_no_client_auth()
12104    } else {
12105        builder
12106            .with_root_certificates(tls_root_store(options)?)
12107            .with_no_client_auth()
12108    };
12109
12110    if let Some(protocols) = options.alpn_protocols.as_ref() {
12111        config.alpn_protocols = protocols
12112            .iter()
12113            .map(|protocol| protocol.as_bytes().to_vec())
12114            .collect();
12115    }
12116    Ok(config)
12117}
12118
12119fn build_server_tls_stream(
12120    stream: TcpStream,
12121    options: &JavascriptTlsBridgeOptions,
12122) -> Result<rustls::StreamOwned<ServerConnection, TcpStream>, SidecarError> {
12123    let config = build_server_tls_config(options)?;
12124    stream
12125        .set_read_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
12126        .map_err(sidecar_net_error)?;
12127    stream
12128        .set_write_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
12129        .map_err(sidecar_net_error)?;
12130    let mut tls_stream = rustls::StreamOwned::new(
12131        ServerConnection::new(Arc::new(config)).map_err(|error| {
12132            SidecarError::Execution(format!("failed to start TLS server: {error}"))
12133        })?,
12134        stream,
12135    );
12136    while tls_stream.conn.is_handshaking() {
12137        tls_stream
12138            .conn
12139            .complete_io(&mut tls_stream.sock)
12140            .map_err(sidecar_net_error)?;
12141    }
12142    tls_stream
12143        .sock
12144        .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
12145        .map_err(sidecar_net_error)?;
12146    tls_stream
12147        .sock
12148        .set_write_timeout(None)
12149        .map_err(sidecar_net_error)?;
12150    Ok(tls_stream)
12151}
12152
12153fn build_server_loopback_tls_stream(
12154    transport: crate::state::LoopbackTlsEndpoint,
12155    options: &JavascriptTlsBridgeOptions,
12156) -> Result<rustls::StreamOwned<ServerConnection, crate::state::LoopbackTlsEndpoint>, SidecarError>
12157{
12158    let config = build_server_tls_config(options)?;
12159    Ok(rustls::StreamOwned::new(
12160        ServerConnection::new(Arc::new(config)).map_err(|error| {
12161            SidecarError::Execution(format!("failed to start TLS server: {error}"))
12162        })?,
12163        transport,
12164    ))
12165}
12166
12167fn build_server_tls_config(
12168    options: &JavascriptTlsBridgeOptions,
12169) -> Result<ServerConfig, SidecarError> {
12170    let certificates = tls_certificates_from_material(options.cert.as_ref().ok_or_else(|| {
12171        SidecarError::InvalidState(String::from("TLS server upgrade requires a certificate"))
12172    })?)?;
12173    let key = tls_private_key_from_material(options.key.as_ref().ok_or_else(|| {
12174        SidecarError::InvalidState(String::from("TLS server upgrade requires a private key"))
12175    })?)?;
12176
12177    let mut config = ServerConfig::builder_with_provider(tls_provider())
12178        .with_safe_default_protocol_versions()
12179        .map_err(|error| {
12180            SidecarError::InvalidState(format!("invalid TLS protocol config: {error}"))
12181        })?
12182        .with_no_client_auth()
12183        .with_single_cert(certificates, key)
12184        .map_err(|error| {
12185            SidecarError::InvalidState(format!("invalid TLS server config: {error}"))
12186        })?;
12187
12188    if let Some(protocols) = options.alpn_protocols.as_ref() {
12189        config.alpn_protocols = protocols
12190            .iter()
12191            .map(|protocol| protocol.as_bytes().to_vec())
12192            .collect();
12193    }
12194    Ok(config)
12195}
12196
12197fn tls_protocol_name(version: rustls::ProtocolVersion) -> String {
12198    match version {
12199        rustls::ProtocolVersion::TLSv1_2 => String::from("TLSv1.2"),
12200        rustls::ProtocolVersion::TLSv1_3 => String::from("TLSv1.3"),
12201        other => other
12202            .as_str()
12203            .map(str::to_owned)
12204            .unwrap_or_else(|| format!("{other:?}")),
12205    }
12206}
12207
12208fn tls_cipher_bridge_value(suite: rustls::SupportedCipherSuite) -> Value {
12209    tls_bridge_object(vec![
12210        (
12211            "name",
12212            suite
12213                .suite()
12214                .as_str()
12215                .map(|value| Value::String(value.to_owned()))
12216                .unwrap_or(Value::Null),
12217        ),
12218        (
12219            "standardName",
12220            suite
12221                .suite()
12222                .as_str()
12223                .map(|value| Value::String(value.to_owned()))
12224                .unwrap_or(Value::Null),
12225        ),
12226        (
12227            "version",
12228            Value::String(if suite.tls13().is_some() {
12229                String::from("TLSv1.3")
12230            } else {
12231                String::from("TLSv1.2")
12232            }),
12233        ),
12234    ])
12235}
12236
12237fn tls_certificate_bridge_value(certificate: &[u8], detailed: bool) -> Value {
12238    let mut fields = vec![("raw", tls_bridge_buffer_value(certificate))];
12239    if detailed {
12240        fields.push(("issuerCertificate", tls_bridge_undefined_value()));
12241    }
12242    tls_bridge_object(fields)
12243}
12244
12245fn tls_bridge_buffer_value(bytes: &[u8]) -> Value {
12246    json!({
12247        "type": "buffer",
12248        "data": base64::engine::general_purpose::STANDARD.encode(bytes),
12249    })
12250}
12251
12252fn tls_bridge_object(entries: Vec<(&str, Value)>) -> Value {
12253    let value = entries
12254        .into_iter()
12255        .map(|(key, value)| (key.to_owned(), value))
12256        .collect::<serde_json::Map<String, Value>>();
12257    json!({
12258        "type": "object",
12259        "id": 1,
12260        "value": value,
12261    })
12262}
12263
12264fn tls_bridge_undefined_value() -> Value {
12265    json!({
12266        "type": "undefined",
12267    })
12268}
12269
12270fn spawn_tcp_socket_reader(
12271    stream: TcpStream,
12272    sender: Sender<JavascriptTcpSocketEvent>,
12273    tls_mode: Arc<AtomicBool>,
12274    saw_local_shutdown: Arc<AtomicBool>,
12275    saw_remote_end: Arc<AtomicBool>,
12276    close_notified: Arc<AtomicBool>,
12277) {
12278    thread::spawn(move || {
12279        let mut stream = stream;
12280        let mut buffer = vec![0_u8; 64 * 1024];
12281        loop {
12282            if tls_mode.load(Ordering::SeqCst) {
12283                break;
12284            }
12285            match stream.read(&mut buffer) {
12286                Ok(0) => {
12287                    saw_remote_end.store(true, Ordering::SeqCst);
12288                    let _ = sender.send(JavascriptTcpSocketEvent::End);
12289                    if saw_local_shutdown.load(Ordering::SeqCst)
12290                        && !close_notified.swap(true, Ordering::SeqCst)
12291                    {
12292                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12293                    }
12294                    break;
12295                }
12296                Ok(bytes_read) => {
12297                    if sender
12298                        .send(JavascriptTcpSocketEvent::Data(
12299                            buffer[..bytes_read].to_vec(),
12300                        ))
12301                        .is_err()
12302                    {
12303                        break;
12304                    }
12305                }
12306                Err(error)
12307                    if matches!(
12308                        error.kind(),
12309                        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
12310                    ) =>
12311                {
12312                    continue;
12313                }
12314                Err(error) => {
12315                    let code = io_error_code(&error);
12316                    let _ = sender.send(JavascriptTcpSocketEvent::Error {
12317                        code,
12318                        message: error.to_string(),
12319                    });
12320                    if !close_notified.swap(true, Ordering::SeqCst) {
12321                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12322                    }
12323                    break;
12324                }
12325            }
12326        }
12327    });
12328}
12329
12330fn spawn_tls_socket_reader(
12331    tls_stream: Arc<Mutex<Option<ActiveTlsStream>>>,
12332    sender: Sender<JavascriptTcpSocketEvent>,
12333    saw_local_shutdown: Arc<AtomicBool>,
12334    saw_remote_end: Arc<AtomicBool>,
12335    close_notified: Arc<AtomicBool>,
12336) {
12337    thread::spawn(move || {
12338        let mut buffer = vec![0_u8; 64 * 1024];
12339        loop {
12340            let read_result = {
12341                let mut guard = match tls_stream.lock() {
12342                    Ok(guard) => guard,
12343                    Err(_) => return,
12344                };
12345                let Some(stream) = guard.as_mut() else {
12346                    return;
12347                };
12348                stream.read(&mut buffer)
12349            };
12350
12351            match read_result {
12352                Ok(0) => {
12353                    saw_remote_end.store(true, Ordering::SeqCst);
12354                    let _ = sender.send(JavascriptTcpSocketEvent::End);
12355                    if saw_local_shutdown.load(Ordering::SeqCst)
12356                        && !close_notified.swap(true, Ordering::SeqCst)
12357                    {
12358                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12359                    }
12360                    break;
12361                }
12362                Ok(bytes_read) => {
12363                    if sender
12364                        .send(JavascriptTcpSocketEvent::Data(
12365                            buffer[..bytes_read].to_vec(),
12366                        ))
12367                        .is_err()
12368                    {
12369                        break;
12370                    }
12371                }
12372                Err(error)
12373                    if matches!(
12374                        error.kind(),
12375                        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
12376                    ) =>
12377                {
12378                    // The TLS reader and writer share one rustls stream mutex. Yield after
12379                    // timed-out reads so request writes can acquire the lock promptly.
12380                    std::thread::sleep(Duration::from_millis(1));
12381                    continue;
12382                }
12383                Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => {
12384                    saw_remote_end.store(true, Ordering::SeqCst);
12385                    let _ = sender.send(JavascriptTcpSocketEvent::End);
12386                    if saw_local_shutdown.load(Ordering::SeqCst)
12387                        && !close_notified.swap(true, Ordering::SeqCst)
12388                    {
12389                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12390                    }
12391                    break;
12392                }
12393                Err(error) => {
12394                    let code = io_error_code(&error);
12395                    let _ = sender.send(JavascriptTcpSocketEvent::Error {
12396                        code,
12397                        message: error.to_string(),
12398                    });
12399                    if !close_notified.swap(true, Ordering::SeqCst) {
12400                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12401                    }
12402                    break;
12403                }
12404            }
12405        }
12406    });
12407}
12408
12409fn spawn_unix_socket_reader(
12410    stream: UnixStream,
12411    sender: Sender<JavascriptTcpSocketEvent>,
12412    saw_local_shutdown: Arc<AtomicBool>,
12413    saw_remote_end: Arc<AtomicBool>,
12414    close_notified: Arc<AtomicBool>,
12415) {
12416    thread::spawn(move || {
12417        let mut stream = stream;
12418        let mut buffer = vec![0_u8; 64 * 1024];
12419        loop {
12420            match stream.read(&mut buffer) {
12421                Ok(0) => {
12422                    saw_remote_end.store(true, Ordering::SeqCst);
12423                    let _ = sender.send(JavascriptTcpSocketEvent::End);
12424                    if saw_local_shutdown.load(Ordering::SeqCst)
12425                        && !close_notified.swap(true, Ordering::SeqCst)
12426                    {
12427                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12428                    }
12429                    break;
12430                }
12431                Ok(bytes_read) => {
12432                    if sender
12433                        .send(JavascriptTcpSocketEvent::Data(
12434                            buffer[..bytes_read].to_vec(),
12435                        ))
12436                        .is_err()
12437                    {
12438                        break;
12439                    }
12440                }
12441                Err(error) => {
12442                    let code = io_error_code(&error);
12443                    let _ = sender.send(JavascriptTcpSocketEvent::Error {
12444                        code,
12445                        message: error.to_string(),
12446                    });
12447                    if !close_notified.swap(true, Ordering::SeqCst) {
12448                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12449                    }
12450                    break;
12451                }
12452            }
12453        }
12454    });
12455}
12456
12457fn terminate_child_process_tree(kernel: &mut SidecarKernel, process: &mut ActiveProcess) {
12458    let sqlite_database_ids = process.sqlite_databases.keys().copied().collect::<Vec<_>>();
12459    for database_id in sqlite_database_ids {
12460        let _ = close_sqlite_database(kernel, process, database_id);
12461    }
12462    process.sqlite_statements.clear();
12463    process.http_servers.clear();
12464    process.pending_http_requests.clear();
12465    if let Ok(mut http2) = process.http2.shared.lock() {
12466        let sessions = http2.sessions.values().cloned().collect::<Vec<_>>();
12467        http2.server_events.clear();
12468        http2.session_events.clear();
12469        http2.streams.clear();
12470        http2.servers.clear();
12471        http2.sessions.clear();
12472        drop(http2);
12473        for session in sessions {
12474            let (respond_to, _rx) = mpsc::channel();
12475            let _ = session.command_tx.send(Http2SessionCommand::Close {
12476                abrupt: true,
12477                respond_to,
12478            });
12479        }
12480    }
12481
12482    let listener_ids = process.tcp_listeners.keys().cloned().collect::<Vec<_>>();
12483    for listener_id in listener_ids {
12484        if let Some(listener) = process.tcp_listeners.remove(&listener_id) {
12485            let _ = listener.close(kernel, process.kernel_pid);
12486        }
12487    }
12488
12489    let sockets = process.tcp_sockets.keys().cloned().collect::<Vec<_>>();
12490    for socket_id in sockets {
12491        if let Some(socket) = process.tcp_sockets.remove(&socket_id) {
12492            let _ = socket.close(kernel, process.kernel_pid);
12493        }
12494    }
12495
12496    let unix_listener_ids = process.unix_listeners.keys().cloned().collect::<Vec<_>>();
12497    for listener_id in unix_listener_ids {
12498        if let Some(listener) = process.unix_listeners.remove(&listener_id) {
12499            let _ = listener.close();
12500        }
12501    }
12502
12503    let unix_sockets = process.unix_sockets.keys().cloned().collect::<Vec<_>>();
12504    for socket_id in unix_sockets {
12505        if let Some(socket) = process.unix_sockets.remove(&socket_id) {
12506            let _ = socket.close();
12507        }
12508    }
12509
12510    let udp_socket_ids = process.udp_sockets.keys().cloned().collect::<Vec<_>>();
12511    for socket_id in udp_socket_ids {
12512        if let Some(mut socket) = process.udp_sockets.remove(&socket_id) {
12513            socket.close(kernel, process.kernel_pid);
12514        }
12515    }
12516
12517    let child_ids = process.child_processes.keys().cloned().collect::<Vec<_>>();
12518    for child_id in child_ids {
12519        let Some(mut child) = process.child_processes.remove(&child_id) else {
12520            continue;
12521        };
12522        terminate_child_process_tree(kernel, &mut child);
12523        let _ = kernel.kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, SIGTERM);
12524        let _ = signal_runtime_process(child.execution.child_pid(), SIGTERM);
12525        child.kernel_handle.finish(0);
12526        let _ = kernel.wait_and_reap(child.kernel_pid);
12527    }
12528}
12529
12530fn service_javascript_sqlite_sync_rpc(
12531    kernel: &mut SidecarKernel,
12532    process: &mut ActiveProcess,
12533    request: &JavascriptSyncRpcRequest,
12534) -> Result<Value, SidecarError> {
12535    match request.method.as_str() {
12536        "sqlite.constants" => Ok(json!({})),
12537        "sqlite.open" => sqlite_open_database(kernel, process, request),
12538        "sqlite.close" => {
12539            let database_id =
12540                javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.close database id")?;
12541            close_sqlite_database(kernel, process, database_id)?;
12542            Ok(Value::Null)
12543        }
12544        "sqlite.exec" => sqlite_exec_database(kernel, process, request),
12545        "sqlite.query" => sqlite_query_database(process, request),
12546        "sqlite.prepare" => sqlite_prepare_statement(process, request),
12547        "sqlite.location" => {
12548            let database_id =
12549                javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.location database id")?;
12550            let database = sqlite_database(process, database_id)?;
12551            Ok(database
12552                .vm_path
12553                .as_ref()
12554                .map(|path| Value::String(path.clone()))
12555                .unwrap_or(Value::Null))
12556        }
12557        "sqlite.checkpoint" => {
12558            let database_id =
12559                javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.checkpoint database id")?;
12560            let kernel_pid = process.kernel_pid;
12561            let database = sqlite_database_mut(process, database_id)?;
12562            sqlite_sync_database(kernel, kernel_pid, database)?;
12563            Ok(Value::Null)
12564        }
12565        "sqlite.statement.run" => sqlite_run_statement(kernel, process, request),
12566        "sqlite.statement.get" => sqlite_get_statement(process, request),
12567        "sqlite.statement.all" | "sqlite.statement.iterate" => {
12568            sqlite_all_statement(process, request)
12569        }
12570        "sqlite.statement.columns" => sqlite_statement_columns(process, request),
12571        "sqlite.statement.setReturnArrays" => {
12572            let statement_id = javascript_sync_rpc_arg_u64(
12573                &request.args,
12574                0,
12575                "sqlite.statement.setReturnArrays statement id",
12576            )?;
12577            let enabled = javascript_sync_rpc_arg_bool(
12578                &request.args,
12579                1,
12580                "sqlite.statement.setReturnArrays enabled",
12581            )?;
12582            sqlite_statement_mut(process, statement_id)?.return_arrays = enabled;
12583            Ok(Value::Null)
12584        }
12585        "sqlite.statement.setReadBigInts" => {
12586            let statement_id = javascript_sync_rpc_arg_u64(
12587                &request.args,
12588                0,
12589                "sqlite.statement.setReadBigInts statement id",
12590            )?;
12591            let enabled = javascript_sync_rpc_arg_bool(
12592                &request.args,
12593                1,
12594                "sqlite.statement.setReadBigInts enabled",
12595            )?;
12596            sqlite_statement_mut(process, statement_id)?.read_bigints = enabled;
12597            Ok(Value::Null)
12598        }
12599        "sqlite.statement.setAllowBareNamedParameters" => {
12600            let statement_id = javascript_sync_rpc_arg_u64(
12601                &request.args,
12602                0,
12603                "sqlite.statement.setAllowBareNamedParameters statement id",
12604            )?;
12605            let enabled = javascript_sync_rpc_arg_bool(
12606                &request.args,
12607                1,
12608                "sqlite.statement.setAllowBareNamedParameters enabled",
12609            )?;
12610            sqlite_statement_mut(process, statement_id)?.allow_bare_named_parameters = enabled;
12611            Ok(Value::Null)
12612        }
12613        "sqlite.statement.setAllowUnknownNamedParameters" => {
12614            let statement_id = javascript_sync_rpc_arg_u64(
12615                &request.args,
12616                0,
12617                "sqlite.statement.setAllowUnknownNamedParameters statement id",
12618            )?;
12619            let enabled = javascript_sync_rpc_arg_bool(
12620                &request.args,
12621                1,
12622                "sqlite.statement.setAllowUnknownNamedParameters enabled",
12623            )?;
12624            sqlite_statement_mut(process, statement_id)?.allow_unknown_named_parameters = enabled;
12625            Ok(Value::Null)
12626        }
12627        "sqlite.statement.finalize" => {
12628            let statement_id = javascript_sync_rpc_arg_u64(
12629                &request.args,
12630                0,
12631                "sqlite.statement.finalize statement id",
12632            )?;
12633            process
12634                .sqlite_statements
12635                .remove(&statement_id)
12636                .ok_or_else(|| {
12637                    SidecarError::InvalidState(format!(
12638                        "sqlite statement handle not found: {statement_id}"
12639                    ))
12640                })?;
12641            Ok(Value::Null)
12642        }
12643        other => Err(SidecarError::InvalidState(format!(
12644            "unsupported JavaScript sqlite sync RPC method {other}"
12645        ))),
12646    }
12647}
12648
12649fn sqlite_open_database(
12650    kernel: &mut SidecarKernel,
12651    process: &mut ActiveProcess,
12652    request: &JavascriptSyncRpcRequest,
12653) -> Result<Value, SidecarError> {
12654    ensure_per_process_state_handle_capacity(process.sqlite_databases.len(), "sqlite database")?;
12655    let path = request.args.first().and_then(Value::as_str);
12656    let vm_path = path.filter(|value| !value.is_empty() && *value != ":memory:");
12657    let options = request.args.get(1);
12658    let read_only = sqlite_option_bool(options, "readOnly").unwrap_or(false);
12659    let create = sqlite_option_bool(options, "create").unwrap_or(!read_only);
12660    let timeout_ms = sqlite_option_u64(options, "timeout");
12661
12662    process.next_sqlite_database_id += 1;
12663    let database_id = process.next_sqlite_database_id;
12664
12665    let host_path = if vm_path.is_some() {
12666        Some(
12667            std::env::temp_dir()
12668                .join(format!(
12669                    "secure-exec-sidecar-sqlite-{}-{database_id}",
12670                    process.kernel_pid
12671                ))
12672                .join("database.sqlite"),
12673        )
12674    } else {
12675        None
12676    };
12677
12678    if let Some(host_path) = host_path.as_ref() {
12679        if let Some(parent) = host_path.parent() {
12680            fs::create_dir_all(parent).map_err(|error| {
12681                SidecarError::Io(format!(
12682                    "failed to prepare sqlite temp directory {}: {error}",
12683                    parent.display()
12684                ))
12685            })?;
12686        }
12687    }
12688
12689    if let (Some(vm_path), Some(host_path)) = (vm_path, host_path.as_ref()) {
12690        if kernel
12691            .exists_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, vm_path)
12692            .map_err(kernel_error)?
12693        {
12694            let contents = kernel
12695                .read_file_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, vm_path)
12696                .map_err(kernel_error)?;
12697            fs::write(host_path, contents).map_err(|error| {
12698                SidecarError::Io(format!(
12699                    "failed to materialize sqlite database {}: {error}",
12700                    host_path.display()
12701                ))
12702            })?;
12703        } else if read_only && !create {
12704            return Err(SidecarError::InvalidState(format!(
12705                "sqlite database does not exist: {vm_path}"
12706            )));
12707        }
12708    }
12709
12710    let target = host_path
12711        .as_ref()
12712        .map(|path| path.to_string_lossy().into_owned())
12713        .unwrap_or_else(|| String::from(":memory:"));
12714    let mut flags = if read_only {
12715        SqliteOpenFlags::SQLITE_OPEN_READ_ONLY
12716    } else {
12717        SqliteOpenFlags::SQLITE_OPEN_READ_WRITE
12718    };
12719    if create && !read_only {
12720        flags |= SqliteOpenFlags::SQLITE_OPEN_CREATE;
12721    }
12722
12723    let connection = SqliteConnection::open_with_flags(&target, flags).map_err(|error| {
12724        SidecarError::InvalidState(format!(
12725            "sqlite database open failed for {}: {error}",
12726            vm_path.unwrap_or(":memory:")
12727        ))
12728    })?;
12729    if let Some(timeout_ms) = timeout_ms {
12730        connection
12731            .busy_timeout(Duration::from_millis(timeout_ms))
12732            .map_err(sqlite_error)?;
12733    }
12734    if host_path.is_some() && !read_only {
12735        let _ = connection.pragma_update(None, "journal_mode", "WAL");
12736    }
12737
12738    process.sqlite_databases.insert(
12739        database_id,
12740        ActiveSqliteDatabase {
12741            connection,
12742            host_path,
12743            vm_path: vm_path.map(String::from),
12744            dirty: false,
12745            transaction_depth: 0,
12746            read_only,
12747        },
12748    );
12749
12750    Ok(json!(database_id))
12751}
12752
12753fn sqlite_exec_database(
12754    kernel: &mut SidecarKernel,
12755    process: &mut ActiveProcess,
12756    request: &JavascriptSyncRpcRequest,
12757) -> Result<Value, SidecarError> {
12758    let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.exec database id")?;
12759    let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.exec sql")?;
12760    let kernel_pid = process.kernel_pid;
12761    let database = sqlite_database_mut(process, database_id)?;
12762    let before = database.connection.total_changes();
12763    database
12764        .connection
12765        .execute_batch(sql)
12766        .map_err(sqlite_error)?;
12767    mark_sqlite_mutation(database, sql);
12768    sqlite_sync_database(kernel, kernel_pid, database)?;
12769    Ok(json!(database
12770        .connection
12771        .total_changes()
12772        .saturating_sub(before)))
12773}
12774
12775fn sqlite_query_database(
12776    process: &mut ActiveProcess,
12777    request: &JavascriptSyncRpcRequest,
12778) -> Result<Value, SidecarError> {
12779    let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.query database id")?;
12780    let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.query sql")?;
12781    let params = request.args.get(2);
12782    let options = request.args.get(3);
12783    let return_arrays = sqlite_option_bool(options, "returnArrays").unwrap_or(false);
12784    let read_bigints = sqlite_option_bool(options, "readBigInts").unwrap_or(false);
12785    let database = sqlite_database_mut(process, database_id)?;
12786    sqlite_query_rows(
12787        &mut database.connection,
12788        sql,
12789        params,
12790        return_arrays,
12791        read_bigints,
12792        true,
12793        false,
12794    )
12795}
12796
12797fn sqlite_prepare_statement(
12798    process: &mut ActiveProcess,
12799    request: &JavascriptSyncRpcRequest,
12800) -> Result<Value, SidecarError> {
12801    ensure_per_process_state_handle_capacity(process.sqlite_statements.len(), "sqlite statement")?;
12802    let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.prepare database id")?;
12803    let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.prepare sql")?;
12804    let _ = sqlite_database(process, database_id)?;
12805    process.next_sqlite_statement_id += 1;
12806    let statement_id = process.next_sqlite_statement_id;
12807    process.sqlite_statements.insert(
12808        statement_id,
12809        ActiveSqliteStatement {
12810            database_id,
12811            sql: sql.to_owned(),
12812            return_arrays: false,
12813            read_bigints: false,
12814            allow_bare_named_parameters: false,
12815            allow_unknown_named_parameters: false,
12816        },
12817    );
12818    Ok(json!(statement_id))
12819}
12820
12821fn sqlite_run_statement(
12822    kernel: &mut SidecarKernel,
12823    process: &mut ActiveProcess,
12824    request: &JavascriptSyncRpcRequest,
12825) -> Result<Value, SidecarError> {
12826    let statement_id =
12827        javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.run statement id")?;
12828    let params = request.args.get(1);
12829    let statement_state = sqlite_statement(process, statement_id)?.clone();
12830    let kernel_pid = process.kernel_pid;
12831    let database = sqlite_database_mut(process, statement_state.database_id)?;
12832    let before = database.connection.total_changes();
12833    {
12834        let mut statement = database
12835            .connection
12836            .prepare(&statement_state.sql)
12837            .map_err(sqlite_error)?;
12838        bind_sqlite_parameters(
12839            &mut statement,
12840            params,
12841            statement_state.allow_bare_named_parameters,
12842            statement_state.allow_unknown_named_parameters,
12843        )?;
12844        statement.raw_execute().map_err(sqlite_error)?;
12845    }
12846    let changes = database.connection.total_changes().saturating_sub(before);
12847    let last_insert_rowid = database.connection.last_insert_rowid();
12848    mark_sqlite_mutation(database, &statement_state.sql);
12849    sqlite_sync_database(kernel, kernel_pid, database)?;
12850    let result = json!({
12851        "changes": changes,
12852        "lastInsertRowid": encode_sqlite_integer(last_insert_rowid, true),
12853    });
12854    Ok(result)
12855}
12856
12857fn sqlite_get_statement(
12858    process: &mut ActiveProcess,
12859    request: &JavascriptSyncRpcRequest,
12860) -> Result<Value, SidecarError> {
12861    let statement_id =
12862        javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.get statement id")?;
12863    let params = request.args.get(1);
12864    let statement_state = sqlite_statement(process, statement_id)?.clone();
12865    let database = sqlite_database_mut(process, statement_state.database_id)?;
12866    let rows = sqlite_query_rows(
12867        &mut database.connection,
12868        &statement_state.sql,
12869        params,
12870        statement_state.return_arrays,
12871        statement_state.read_bigints,
12872        statement_state.allow_bare_named_parameters,
12873        statement_state.allow_unknown_named_parameters,
12874    )?;
12875    Ok(rows
12876        .as_array()
12877        .and_then(|rows| rows.first().cloned())
12878        .unwrap_or(Value::Null))
12879}
12880
12881fn sqlite_all_statement(
12882    process: &mut ActiveProcess,
12883    request: &JavascriptSyncRpcRequest,
12884) -> Result<Value, SidecarError> {
12885    let statement_id =
12886        javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.all statement id")?;
12887    let params = request.args.get(1);
12888    let statement_state = sqlite_statement(process, statement_id)?.clone();
12889    let database = sqlite_database_mut(process, statement_state.database_id)?;
12890    sqlite_query_rows(
12891        &mut database.connection,
12892        &statement_state.sql,
12893        params,
12894        statement_state.return_arrays,
12895        statement_state.read_bigints,
12896        statement_state.allow_bare_named_parameters,
12897        statement_state.allow_unknown_named_parameters,
12898    )
12899}
12900
12901fn sqlite_statement_columns(
12902    process: &mut ActiveProcess,
12903    request: &JavascriptSyncRpcRequest,
12904) -> Result<Value, SidecarError> {
12905    let statement_id =
12906        javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.columns statement id")?;
12907    let statement_state = sqlite_statement(process, statement_id)?.clone();
12908    let database = sqlite_database_mut(process, statement_state.database_id)?;
12909    let statement = database
12910        .connection
12911        .prepare(&statement_state.sql)
12912        .map_err(sqlite_error)?;
12913    Ok(Value::Array(
12914        statement
12915            .column_names()
12916            .iter()
12917            .map(|name| json!({ "name": name }))
12918            .collect(),
12919    ))
12920}
12921
12922fn sqlite_query_rows(
12923    connection: &mut SqliteConnection,
12924    sql: &str,
12925    params: Option<&Value>,
12926    return_arrays: bool,
12927    read_bigints: bool,
12928    allow_bare_named_parameters: bool,
12929    allow_unknown_named_parameters: bool,
12930) -> Result<Value, SidecarError> {
12931    let mut statement = connection.prepare(sql).map_err(sqlite_error)?;
12932    let column_names = statement
12933        .column_names()
12934        .iter()
12935        .map(|name| (*name).to_owned())
12936        .collect::<Vec<_>>();
12937    let column_count = statement.column_count();
12938    bind_sqlite_parameters(
12939        &mut statement,
12940        params,
12941        allow_bare_named_parameters,
12942        allow_unknown_named_parameters,
12943    )?;
12944    let mut rows = statement.raw_query();
12945    let mut encoded_rows = Vec::new();
12946    while let Some(row) = rows.next().map_err(sqlite_error)? {
12947        encoded_rows.push(encode_sqlite_row(
12948            row,
12949            &column_names,
12950            column_count,
12951            return_arrays,
12952            read_bigints,
12953        )?);
12954    }
12955    Ok(Value::Array(encoded_rows))
12956}
12957
12958fn encode_sqlite_row(
12959    row: &rusqlite::Row<'_>,
12960    column_names: &[String],
12961    column_count: usize,
12962    return_arrays: bool,
12963    read_bigints: bool,
12964) -> Result<Value, SidecarError> {
12965    if return_arrays {
12966        let mut values = Vec::with_capacity(column_count);
12967        for index in 0..column_count {
12968            values.push(encode_sqlite_value_ref(
12969                row.get_ref(index).map_err(sqlite_error)?,
12970                read_bigints,
12971            )?);
12972        }
12973        return Ok(Value::Array(values));
12974    }
12975
12976    let mut object = Map::with_capacity(column_count);
12977    for (index, name) in column_names.iter().enumerate() {
12978        object.insert(
12979            name.clone(),
12980            encode_sqlite_value_ref(row.get_ref(index).map_err(sqlite_error)?, read_bigints)?,
12981        );
12982    }
12983    Ok(Value::Object(object))
12984}
12985
12986fn encode_sqlite_value_ref(
12987    value: SqliteValueRef<'_>,
12988    read_bigints: bool,
12989) -> Result<Value, SidecarError> {
12990    Ok(match value {
12991        SqliteValueRef::Null => Value::Null,
12992        SqliteValueRef::Integer(number) => encode_sqlite_integer(number, read_bigints),
12993        SqliteValueRef::Real(number) => json!(number),
12994        SqliteValueRef::Text(text) => Value::String(String::from_utf8_lossy(text).into_owned()),
12995        SqliteValueRef::Blob(bytes) => json!({
12996            "__agentosSqliteType": "uint8array",
12997            "value": base64::engine::general_purpose::STANDARD.encode(bytes),
12998        }),
12999    })
13000}
13001
13002fn encode_sqlite_integer(number: i64, read_bigints: bool) -> Value {
13003    if read_bigints || number.abs() > SQLITE_JS_SAFE_INTEGER_MAX {
13004        json!({
13005            "__agentosSqliteType": "bigint",
13006            "value": number.to_string(),
13007        })
13008    } else {
13009        json!(number)
13010    }
13011}
13012
13013fn bind_sqlite_parameters(
13014    statement: &mut SqliteStatement<'_>,
13015    params: Option<&Value>,
13016    allow_bare_named_parameters: bool,
13017    allow_unknown_named_parameters: bool,
13018) -> Result<(), SidecarError> {
13019    let Some(params) = params else {
13020        return Ok(());
13021    };
13022    match params {
13023        Value::Null => Ok(()),
13024        Value::Array(values) => {
13025            for (index, value) in values.iter().enumerate() {
13026                statement
13027                    .raw_bind_parameter(index + 1, decode_sqlite_parameter(value)?)
13028                    .map_err(sqlite_error)?;
13029            }
13030            Ok(())
13031        }
13032        Value::Object(map)
13033            if map
13034                .get("__agentosSqliteType")
13035                .and_then(Value::as_str)
13036                .is_none() =>
13037        {
13038            for (key, value) in map {
13039                let index =
13040                    resolve_sqlite_parameter_index(statement, key, allow_bare_named_parameters)?;
13041                let Some(index) = index else {
13042                    if allow_unknown_named_parameters {
13043                        continue;
13044                    }
13045                    return Err(SidecarError::InvalidState(format!(
13046                        "sqlite named parameter not found: {key}"
13047                    )));
13048                };
13049                statement
13050                    .raw_bind_parameter(index, decode_sqlite_parameter(value)?)
13051                    .map_err(sqlite_error)?;
13052            }
13053            Ok(())
13054        }
13055        other => statement
13056            .raw_bind_parameter(1, decode_sqlite_parameter(other)?)
13057            .map_err(sqlite_error),
13058    }
13059}
13060
13061fn resolve_sqlite_parameter_index(
13062    statement: &mut SqliteStatement<'_>,
13063    key: &str,
13064    allow_bare_named_parameters: bool,
13065) -> Result<Option<usize>, SidecarError> {
13066    let mut candidates = vec![key.to_owned()];
13067    if allow_bare_named_parameters
13068        && !key.starts_with(':')
13069        && !key.starts_with('@')
13070        && !key.starts_with('$')
13071    {
13072        candidates.push(format!(":{key}"));
13073        candidates.push(format!("@{key}"));
13074        candidates.push(format!("${key}"));
13075    }
13076    for candidate in candidates {
13077        if let Some(index) = statement
13078            .parameter_index(&candidate)
13079            .map_err(sqlite_error)?
13080        {
13081            return Ok(Some(index));
13082        }
13083    }
13084    Ok(None)
13085}
13086
13087fn decode_sqlite_parameter(value: &Value) -> Result<rusqlite::types::Value, SidecarError> {
13088    Ok(match value {
13089        Value::Null => rusqlite::types::Value::Null,
13090        Value::Bool(value) => rusqlite::types::Value::Integer(i64::from(*value)),
13091        Value::Number(value) => match (value.as_i64(), value.as_f64()) {
13092            (Some(integer), _) => rusqlite::types::Value::Integer(integer),
13093            (_, Some(real)) => rusqlite::types::Value::Real(real),
13094            _ => {
13095                return Err(SidecarError::InvalidState(String::from(
13096                    "sqlite parameter number is not representable",
13097                )));
13098            }
13099        },
13100        Value::String(value) => rusqlite::types::Value::Text(value.clone()),
13101        Value::Array(_) => {
13102            return Err(SidecarError::InvalidState(String::from(
13103                "sqlite parameters do not support nested arrays",
13104            )));
13105        }
13106        Value::Object(map) => match map.get("__agentosSqliteType").and_then(Value::as_str) {
13107            Some("bigint") => rusqlite::types::Value::Integer(
13108                map.get("value")
13109                    .and_then(Value::as_str)
13110                    .ok_or_else(|| {
13111                        SidecarError::InvalidState(String::from(
13112                            "sqlite bigint parameter missing string value",
13113                        ))
13114                    })?
13115                    .parse::<i64>()
13116                    .map_err(|error| {
13117                        SidecarError::InvalidState(format!(
13118                            "sqlite bigint parameter is not a signed 64-bit integer: {error}"
13119                        ))
13120                    })?,
13121            ),
13122            Some("uint8array") => rusqlite::types::Value::Blob(
13123                base64::engine::general_purpose::STANDARD
13124                    .decode(map.get("value").and_then(Value::as_str).ok_or_else(|| {
13125                        SidecarError::InvalidState(String::from(
13126                            "sqlite blob parameter missing base64 value",
13127                        ))
13128                    })?)
13129                    .map_err(|error| {
13130                        SidecarError::InvalidState(format!(
13131                            "sqlite blob parameter contains invalid base64: {error}"
13132                        ))
13133                    })?,
13134            ),
13135            Some(other) => {
13136                return Err(SidecarError::InvalidState(format!(
13137                    "unsupported sqlite tagged parameter type {other}"
13138                )));
13139            }
13140            None => {
13141                return Err(SidecarError::InvalidState(String::from(
13142                    "sqlite named parameter objects must be passed as the top-level params object",
13143                )));
13144            }
13145        },
13146    })
13147}
13148
13149fn close_sqlite_database(
13150    kernel: &mut SidecarKernel,
13151    process: &mut ActiveProcess,
13152    database_id: u64,
13153) -> Result<(), SidecarError> {
13154    let mut database = process
13155        .sqlite_databases
13156        .remove(&database_id)
13157        .ok_or_else(|| {
13158            SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
13159        })?;
13160    process
13161        .sqlite_statements
13162        .retain(|_, statement| statement.database_id != database_id);
13163    sqlite_sync_database(kernel, process.kernel_pid, &mut database)?;
13164    let host_path = database.host_path.clone();
13165    drop(database);
13166    cleanup_sqlite_host_artifacts(host_path.as_deref())?;
13167    Ok(())
13168}
13169
13170fn ensure_per_process_state_handle_capacity(len: usize, label: &str) -> Result<(), SidecarError> {
13171    if len >= MAX_PER_PROCESS_STATE_HANDLES {
13172        return Err(SidecarError::InvalidState(format!(
13173            "{label} handle limit exceeded: limit is {MAX_PER_PROCESS_STATE_HANDLES}"
13174        )));
13175    }
13176    Ok(())
13177}
13178
13179fn sqlite_sync_database(
13180    kernel: &mut SidecarKernel,
13181    kernel_pid: u32,
13182    database: &mut ActiveSqliteDatabase,
13183) -> Result<(), SidecarError> {
13184    if !database.dirty
13185        || database.transaction_depth > 0
13186        || database.read_only
13187        || database.host_path.is_none()
13188        || database.vm_path.is_none()
13189    {
13190        return Ok(());
13191    }
13192
13193    let _ = database
13194        .connection
13195        .execute_batch("PRAGMA wal_checkpoint(TRUNCATE)");
13196    let host_path = database.host_path.as_ref().expect("sqlite host path");
13197    if !host_path.exists() {
13198        return Ok(());
13199    }
13200    ensure_vm_parent_dir(
13201        kernel,
13202        kernel_pid,
13203        database.vm_path.as_deref().expect("sqlite vm path"),
13204    )?;
13205    let contents = fs::read(host_path).map_err(|error| {
13206        SidecarError::Io(format!(
13207            "failed to read sqlite temp database {}: {error}",
13208            host_path.display()
13209        ))
13210    })?;
13211    kernel
13212        .write_file_for_process(
13213            EXECUTION_DRIVER_NAME,
13214            kernel_pid,
13215            database.vm_path.as_deref().expect("sqlite vm path"),
13216            contents,
13217            None,
13218        )
13219        .map_err(kernel_error)?;
13220    database.dirty = false;
13221    Ok(())
13222}
13223
13224fn cleanup_sqlite_host_artifacts(host_path: Option<&Path>) -> Result<(), SidecarError> {
13225    let Some(host_path) = host_path else {
13226        return Ok(());
13227    };
13228    let parent = host_path.parent().map(PathBuf::from);
13229    for suffix in ["", "-wal", "-shm"] {
13230        let path = PathBuf::from(format!("{}{}", host_path.display(), suffix));
13231        if path.exists() {
13232            fs::remove_file(&path).map_err(|error| {
13233                SidecarError::Io(format!(
13234                    "failed to remove sqlite temp artifact {}: {error}",
13235                    path.display()
13236                ))
13237            })?;
13238        }
13239    }
13240    if let Some(parent) = parent {
13241        let _ = fs::remove_dir_all(parent);
13242    }
13243    Ok(())
13244}
13245
13246fn ensure_vm_parent_dir(
13247    kernel: &mut SidecarKernel,
13248    kernel_pid: u32,
13249    path: &str,
13250) -> Result<(), SidecarError> {
13251    let parent = dirname(path);
13252    if parent == "/" || parent == "." {
13253        return Ok(());
13254    }
13255    let mut current = String::new();
13256    for segment in parent.split('/').filter(|segment| !segment.is_empty()) {
13257        current.push('/');
13258        current.push_str(segment);
13259        if !kernel
13260            .exists_for_process(EXECUTION_DRIVER_NAME, kernel_pid, &current)
13261            .map_err(kernel_error)?
13262        {
13263            kernel
13264                .mkdir_for_process(EXECUTION_DRIVER_NAME, kernel_pid, &current, false, None)
13265                .map_err(kernel_error)?;
13266        }
13267    }
13268    Ok(())
13269}
13270
13271fn sqlite_database(
13272    process: &ActiveProcess,
13273    database_id: u64,
13274) -> Result<&ActiveSqliteDatabase, SidecarError> {
13275    process.sqlite_databases.get(&database_id).ok_or_else(|| {
13276        SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
13277    })
13278}
13279
13280fn sqlite_database_mut(
13281    process: &mut ActiveProcess,
13282    database_id: u64,
13283) -> Result<&mut ActiveSqliteDatabase, SidecarError> {
13284    process
13285        .sqlite_databases
13286        .get_mut(&database_id)
13287        .ok_or_else(|| {
13288            SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
13289        })
13290}
13291
13292fn sqlite_statement(
13293    process: &ActiveProcess,
13294    statement_id: u64,
13295) -> Result<&ActiveSqliteStatement, SidecarError> {
13296    process.sqlite_statements.get(&statement_id).ok_or_else(|| {
13297        SidecarError::InvalidState(format!("sqlite statement handle not found: {statement_id}"))
13298    })
13299}
13300
13301fn sqlite_statement_mut(
13302    process: &mut ActiveProcess,
13303    statement_id: u64,
13304) -> Result<&mut ActiveSqliteStatement, SidecarError> {
13305    process
13306        .sqlite_statements
13307        .get_mut(&statement_id)
13308        .ok_or_else(|| {
13309            SidecarError::InvalidState(format!("sqlite statement handle not found: {statement_id}"))
13310        })
13311}
13312
13313fn mark_sqlite_mutation(database: &mut ActiveSqliteDatabase, sql: &str) {
13314    let normalized = sql.trim_start().to_ascii_lowercase();
13315    if normalized.starts_with("begin") || normalized.starts_with("savepoint") {
13316        database.dirty = true;
13317        database.transaction_depth += 1;
13318        return;
13319    }
13320    if normalized.starts_with("commit") || normalized.starts_with("release savepoint") {
13321        database.dirty = true;
13322        database.transaction_depth = database.transaction_depth.saturating_sub(1);
13323        return;
13324    }
13325    if normalized.starts_with("rollback") && !normalized.starts_with("rollback to") {
13326        database.dirty = true;
13327        database.transaction_depth = database.transaction_depth.saturating_sub(1);
13328        return;
13329    }
13330    if normalized.starts_with("insert")
13331        || normalized.starts_with("update")
13332        || normalized.starts_with("delete")
13333        || normalized.starts_with("replace")
13334        || normalized.starts_with("create")
13335        || normalized.starts_with("alter")
13336        || normalized.starts_with("drop")
13337        || normalized.starts_with("vacuum")
13338        || normalized.starts_with("reindex")
13339        || normalized.starts_with("analyze")
13340        || normalized.starts_with("attach")
13341        || normalized.starts_with("detach")
13342        || normalized.starts_with("pragma")
13343    {
13344        database.dirty = true;
13345    }
13346}
13347
13348fn sqlite_option_bool(options: Option<&Value>, key: &str) -> Option<bool> {
13349    options
13350        .and_then(|value| value.get(key))
13351        .and_then(Value::as_bool)
13352}
13353
13354fn sqlite_option_u64(options: Option<&Value>, key: &str) -> Option<u64> {
13355    options
13356        .and_then(|value| value.get(key))
13357        .and_then(Value::as_u64)
13358}
13359
13360fn sqlite_error(error: rusqlite::Error) -> SidecarError {
13361    SidecarError::InvalidState(format!("sqlite error: {error}"))
13362}
13363
13364pub(crate) fn javascript_sync_rpc_arg_str<'a>(
13365    args: &'a [Value],
13366    index: usize,
13367    label: &str,
13368) -> Result<&'a str, SidecarError> {
13369    args.get(index)
13370        .and_then(Value::as_str)
13371        .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a string argument")))
13372}
13373
13374pub(crate) fn javascript_sync_rpc_arg_bool(
13375    args: &[Value],
13376    index: usize,
13377    label: &str,
13378) -> Result<bool, SidecarError> {
13379    args.get(index)
13380        .and_then(Value::as_bool)
13381        .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a boolean argument")))
13382}
13383
13384pub(crate) fn javascript_sync_rpc_encoding(args: &[Value]) -> Option<String> {
13385    args.get(1).and_then(|value| {
13386        value.as_str().map(str::to_owned).or_else(|| {
13387            value
13388                .get("encoding")
13389                .and_then(Value::as_str)
13390                .map(str::to_owned)
13391        })
13392    })
13393}
13394
13395pub(crate) fn javascript_sync_rpc_option_bool(
13396    args: &[Value],
13397    index: usize,
13398    key: &str,
13399) -> Option<bool> {
13400    let value = args.get(index)?;
13401    if key == "recursive" {
13402        if let Some(boolean) = value.as_bool() {
13403            return Some(boolean);
13404        }
13405    }
13406    value.get(key).and_then(Value::as_bool)
13407}
13408
13409pub(crate) fn javascript_sync_rpc_option_u32(
13410    args: &[Value],
13411    index: usize,
13412    key: &str,
13413) -> Result<Option<u32>, SidecarError> {
13414    let Some(value) = args.get(index).and_then(|value| {
13415        if value.is_object() {
13416            value.get(key)
13417        } else if key == "mode" && value.is_number() {
13418            Some(value)
13419        } else {
13420            None
13421        }
13422    }) else {
13423        return Ok(None);
13424    };
13425    if value.is_null() {
13426        return Ok(None);
13427    }
13428
13429    let numeric = value
13430        .as_u64()
13431        .or_else(|| {
13432            value
13433                .as_f64()
13434                .filter(|number| number.is_finite() && *number >= 0.0)
13435                .map(|number| number as u64)
13436        })
13437        .ok_or_else(|| SidecarError::InvalidState(format!("{key} must be numeric")))?;
13438
13439    u32::try_from(numeric)
13440        .map(Some)
13441        .map_err(|_| SidecarError::InvalidState(format!("{key} must fit within u32")))
13442}
13443
13444pub(crate) fn javascript_sync_rpc_arg_u32(
13445    args: &[Value],
13446    index: usize,
13447    label: &str,
13448) -> Result<u32, SidecarError> {
13449    let value = javascript_sync_rpc_arg_u64(args, index, label)?;
13450    u32::try_from(value)
13451        .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")))
13452}
13453
13454pub(crate) fn javascript_sync_rpc_arg_i32(
13455    args: &[Value],
13456    index: usize,
13457    label: &str,
13458) -> Result<i32, SidecarError> {
13459    let Some(value) = args.get(index) else {
13460        return Err(SidecarError::InvalidState(format!("{label} is required")));
13461    };
13462
13463    let numeric = value
13464        .as_i64()
13465        .or_else(|| {
13466            value
13467                .as_f64()
13468                .filter(|number| number.is_finite())
13469                .map(|number| number as i64)
13470        })
13471        .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a numeric argument")))?;
13472
13473    i32::try_from(numeric)
13474        .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within i32")))
13475}
13476
13477pub(crate) fn javascript_sync_rpc_arg_u32_optional(
13478    args: &[Value],
13479    index: usize,
13480    label: &str,
13481) -> Result<Option<u32>, SidecarError> {
13482    javascript_sync_rpc_arg_u64_optional(args, index, label)?
13483        .map(|value| {
13484            u32::try_from(value)
13485                .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")))
13486        })
13487        .transpose()
13488}
13489
13490pub(crate) fn javascript_sync_rpc_arg_u64(
13491    args: &[Value],
13492    index: usize,
13493    label: &str,
13494) -> Result<u64, SidecarError> {
13495    let Some(value) = args.get(index) else {
13496        return Err(SidecarError::InvalidState(format!("{label} is required")));
13497    };
13498
13499    value
13500        .as_u64()
13501        .or_else(|| {
13502            value
13503                .as_f64()
13504                .filter(|number| number.is_finite() && *number >= 0.0)
13505                .map(|number| number as u64)
13506        })
13507        .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a numeric argument")))
13508}
13509
13510pub(crate) fn javascript_sync_rpc_arg_u64_optional(
13511    args: &[Value],
13512    index: usize,
13513    label: &str,
13514) -> Result<Option<u64>, SidecarError> {
13515    let Some(value) = args.get(index) else {
13516        return Ok(None);
13517    };
13518    if value.is_null() {
13519        return Ok(None);
13520    }
13521    javascript_sync_rpc_arg_u64(args, index, label).map(Some)
13522}
13523
13524pub(crate) fn javascript_sync_rpc_bytes_arg(
13525    args: &[Value],
13526    index: usize,
13527    label: &str,
13528) -> Result<Vec<u8>, SidecarError> {
13529    let Some(value) = args.get(index) else {
13530        return Err(SidecarError::InvalidState(format!("{label} is required")));
13531    };
13532
13533    if let Some(text) = value.as_str() {
13534        return Ok(text.as_bytes().to_vec());
13535    }
13536
13537    let Some(base64_value) = value
13538        .get("__agentOSType")
13539        .and_then(Value::as_str)
13540        .filter(|kind| *kind == "bytes")
13541        .and_then(|_| value.get("base64"))
13542        .and_then(Value::as_str)
13543    else {
13544        return Err(SidecarError::InvalidState(format!(
13545            "{label} must be a string or encoded bytes payload"
13546        )));
13547    };
13548
13549    base64::engine::general_purpose::STANDARD
13550        .decode(base64_value)
13551        .map_err(|error| {
13552            SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
13553        })
13554}
13555
13556pub(crate) fn javascript_sync_rpc_bytes_value(bytes: &[u8]) -> Value {
13557    json!({
13558        "__agentOSType": "bytes",
13559        "base64": base64::engine::general_purpose::STANDARD.encode(bytes),
13560    })
13561}
13562
13563#[derive(Debug, Deserialize)]
13564struct KernelPollFdRequest {
13565    fd: u32,
13566    events: u16,
13567}
13568
13569#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
13570struct KernelPollFdResponse {
13571    fd: u32,
13572    events: u16,
13573    revents: u16,
13574}
13575
13576fn javascript_sync_rpc_base64_arg(
13577    args: &[Value],
13578    index: usize,
13579    label: &str,
13580) -> Result<Vec<u8>, SidecarError> {
13581    let value = javascript_sync_rpc_arg_str(args, index, label)?;
13582    base64::engine::general_purpose::STANDARD
13583        .decode(value)
13584        .map_err(|error| {
13585            SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
13586        })
13587}
13588
13589// ── Sync-RPC round-trip counting (opt-in via AGENTOS_SYNC_RPC_TRACE=1) ──
13590// Each guest fs/module/net sync RPC funnels through service_javascript_sync_rpc,
13591// so this is the one place to measure the kernel-VFS "syscall storm" that makes
13592// metadata-heavy phases (resourceLoader.reload, createAgentSession) 40-90x slower
13593// in the VM than on bare node. Emits a perf log line every 200 calls with the
13594// running per-method breakdown.
13595static SYNC_RPC_STATS: std::sync::OnceLock<
13596    std::sync::Mutex<std::collections::BTreeMap<String, u64>>,
13597> = std::sync::OnceLock::new();
13598
13599fn sync_rpc_trace_enabled() -> bool {
13600    std::env::var("AGENTOS_SYNC_RPC_TRACE").as_deref() == Ok("1")
13601}
13602
13603fn record_sync_rpc(method: &str) {
13604    let stats =
13605        SYNC_RPC_STATS.get_or_init(|| std::sync::Mutex::new(std::collections::BTreeMap::new()));
13606    let Ok(mut map) = stats.lock() else {
13607        return;
13608    };
13609    *map.entry(method.to_string()).or_insert(0) += 1;
13610    let total: u64 = map.values().sum();
13611    if total == 1 || total % 50 == 0 {
13612        let mut top: Vec<(&String, &u64)> = map.iter().collect();
13613        top.sort_by(|a, b| b.1.cmp(a.1));
13614        let breakdown = top
13615            .iter()
13616            .take(8)
13617            .map(|(m, c)| format!("{m}={c}"))
13618            .collect::<Vec<_>>()
13619            .join(" ");
13620        tracing::info!(target: "secure_exec_sidecar::perf", total, %breakdown, "sync_rpc count");
13621    }
13622}
13623
13624pub(crate) fn service_javascript_sync_rpc<B>(
13625    request: JavascriptSyncRpcServiceRequest<'_, B>,
13626) -> Result<Value, SidecarError>
13627where
13628    B: NativeSidecarBridge + Send + 'static,
13629    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
13630{
13631    if sync_rpc_trace_enabled() {
13632        record_sync_rpc(request.sync_request.method.as_str());
13633    }
13634    let JavascriptSyncRpcServiceRequest {
13635        bridge,
13636        vm_id,
13637        dns,
13638        socket_paths,
13639        kernel,
13640        process,
13641        sync_request: request,
13642        resource_limits,
13643        network_counts,
13644    } = request;
13645    match request.method.as_str() {
13646        // Module resolution / loading / format detection read the kernel VFS so
13647        // the resolver sees exactly what the guest and `kernel.readFile()` see.
13648        "_resolveModule"
13649        | "_resolveModuleSync"
13650        | "__resolve_module"
13651        | "_batchResolveModules"
13652        | "__batch_resolve_modules"
13653        | "_loadFile"
13654        | "_loadFileSync"
13655        | "__load_file"
13656        | "_moduleFormat"
13657        | "__module_format" => service_javascript_module_sync_rpc(kernel, process, request),
13658        // Polyfills are static guest expressions, not VFS reads.
13659        "_loadPolyfill" | "__load_polyfill" => {
13660            service_javascript_internal_bridge_sync_rpc(process, request)
13661        }
13662        "__kernel_stdin_read" => match &process.execution {
13663            ActiveExecution::Javascript(execution) => execution
13664                .read_kernel_stdin_sync_rpc(request)
13665                .map_err(|error| SidecarError::Execution(error.to_string())),
13666            ActiveExecution::Python(_) | ActiveExecution::Wasm(_) | ActiveExecution::Tool(_) => {
13667                service_javascript_kernel_stdin_sync_rpc(kernel, process, request)
13668            }
13669        },
13670        "__kernel_stdio_write" => {
13671            service_javascript_kernel_stdio_write_sync_rpc(kernel, process, request)
13672        }
13673        "__kernel_poll" => service_javascript_kernel_poll_sync_rpc(kernel, process, request),
13674        "__pty_set_raw_mode" => {
13675            service_javascript_pty_set_raw_mode_sync_rpc(kernel, process, request)
13676        }
13677        "crypto.hashDigest"
13678        | "crypto.hmacDigest"
13679        | "crypto.pbkdf2"
13680        | "crypto.scrypt"
13681        | "crypto.cipheriv"
13682        | "crypto.decipheriv"
13683        | "crypto.cipherivCreate"
13684        | "crypto.cipherivUpdate"
13685        | "crypto.cipherivFinal"
13686        | "crypto.sign"
13687        | "crypto.verify"
13688        | "crypto.asymmetricOp"
13689        | "crypto.createKeyObject"
13690        | "crypto.generateKeyPairSync"
13691        | "crypto.generateKeySync"
13692        | "crypto.generatePrimeSync"
13693        | "crypto.diffieHellman"
13694        | "crypto.diffieHellmanGroup"
13695        | "crypto.diffieHellmanSessionCreate"
13696        | "crypto.diffieHellmanSessionCall"
13697        | "crypto.diffieHellmanSessionDestroy"
13698        | "crypto.subtle" => service_javascript_crypto_sync_rpc(process, request),
13699        "dns.lookup" | "dns.resolve" | "dns.resolve4" | "dns.resolve6" => {
13700            service_javascript_dns_sync_rpc(bridge, kernel, vm_id, dns, request)
13701        }
13702        "net.http_listen" | "net.http_close" | "net.http_wait" | "net.http_respond" => {
13703            service_javascript_net_sync_rpc(JavascriptNetSyncRpcServiceRequest {
13704                bridge,
13705                vm_id,
13706                dns,
13707                socket_paths,
13708                kernel,
13709                process,
13710                sync_request: request,
13711                resource_limits,
13712                network_counts,
13713            })
13714        }
13715        "net.http2_server_listen"
13716        | "net.http2_server_poll"
13717        | "net.http2_server_close"
13718        | "net.http2_server_respond"
13719        | "net.http2_server_wait"
13720        | "net.http2_session_connect"
13721        | "net.http2_session_request"
13722        | "net.http2_session_settings"
13723        | "net.http2_session_set_local_window_size"
13724        | "net.http2_session_goaway"
13725        | "net.http2_session_close"
13726        | "net.http2_session_destroy"
13727        | "net.http2_session_poll"
13728        | "net.http2_session_wait"
13729        | "net.http2_stream_respond"
13730        | "net.http2_stream_push_stream"
13731        | "net.http2_stream_write"
13732        | "net.http2_stream_end"
13733        | "net.http2_stream_close"
13734        | "net.http2_stream_pause"
13735        | "net.http2_stream_resume"
13736        | "net.http2_stream_respond_with_file" => {
13737            service_javascript_http2_sync_rpc(JavascriptHttp2SyncRpcServiceRequest {
13738                bridge,
13739                kernel,
13740                vm_id,
13741                dns,
13742                socket_paths,
13743                process,
13744                sync_request: request,
13745                resource_limits,
13746                network_counts,
13747            })
13748        }
13749        "net.connect"
13750        | "net.reserve_tcp_port"
13751        | "net.release_tcp_port"
13752        | "net.listen"
13753        | "net.poll"
13754        | "net.socket_wait_connect"
13755        | "net.socket_read"
13756        | "net.socket_set_no_delay"
13757        | "net.socket_set_keep_alive"
13758        | "net.socket_upgrade_tls"
13759        | "net.socket_get_tls_client_hello"
13760        | "net.socket_tls_query"
13761        | "net.server_poll"
13762        | "net.server_accept"
13763        | "net.server_connections"
13764        | "net.upgrade_socket_write"
13765        | "net.upgrade_socket_end"
13766        | "net.upgrade_socket_destroy"
13767        | "net.write"
13768        | "net.shutdown"
13769        | "net.destroy"
13770        | "net.server_close"
13771        | "tls.get_ciphers" => {
13772            service_javascript_net_sync_rpc(JavascriptNetSyncRpcServiceRequest {
13773                bridge,
13774                vm_id,
13775                dns,
13776                socket_paths,
13777                kernel,
13778                process,
13779                sync_request: request,
13780                resource_limits,
13781                network_counts,
13782            })
13783        }
13784        "dgram.createSocket"
13785        | "dgram.bind"
13786        | "dgram.send"
13787        | "dgram.poll"
13788        | "dgram.close"
13789        | "dgram.address"
13790        | "dgram.setBufferSize"
13791        | "dgram.getBufferSize" => {
13792            service_javascript_dgram_sync_rpc(JavascriptDgramSyncRpcServiceRequest {
13793                bridge,
13794                kernel,
13795                vm_id,
13796                dns,
13797                socket_paths,
13798                process,
13799                sync_request: request,
13800                resource_limits,
13801                network_counts,
13802            })
13803        }
13804        "sqlite.constants"
13805        | "sqlite.open"
13806        | "sqlite.close"
13807        | "sqlite.exec"
13808        | "sqlite.query"
13809        | "sqlite.prepare"
13810        | "sqlite.location"
13811        | "sqlite.checkpoint"
13812        | "sqlite.statement.run"
13813        | "sqlite.statement.get"
13814        | "sqlite.statement.all"
13815        | "sqlite.statement.iterate"
13816        | "sqlite.statement.columns"
13817        | "sqlite.statement.setReturnArrays"
13818        | "sqlite.statement.setReadBigInts"
13819        | "sqlite.statement.setAllowBareNamedParameters"
13820        | "sqlite.statement.setAllowUnknownNamedParameters"
13821        | "sqlite.statement.finalize" => {
13822            service_javascript_sqlite_sync_rpc(kernel, process, request)
13823        }
13824        "process.kill" => {
13825            let target_pid =
13826                javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?;
13827            let signal = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?;
13828            let parsed_signal = parse_signal(signal)?;
13829            if parsed_signal == 0 {
13830                kernel
13831                    .signal_process(EXECUTION_DRIVER_NAME, target_pid, parsed_signal)
13832                    .map_err(kernel_error)?;
13833                return Ok(Value::Null);
13834            }
13835            let process_pid = i32::try_from(process.kernel_pid)
13836                .map_err(|_| SidecarError::InvalidState("process pid exceeds i32".into()))?;
13837            if target_pid != process_pid {
13838                return Err(SidecarError::InvalidState(format!(
13839                    "unknown process pid {target_pid}"
13840                )));
13841            }
13842            process.pending_self_signal_exit = None;
13843            if parsed_signal != 0
13844                && !matches!(
13845                    canonical_signal_name(parsed_signal),
13846                    Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
13847                )
13848            {
13849                process.pending_self_signal_exit = Some(parsed_signal);
13850            }
13851            Ok(json!({
13852                "self": true,
13853                "action": "default",
13854            }))
13855        }
13856        "process.umask" => {
13857            let new_mask = javascript_sync_rpc_arg_u32_optional(&request.args, 0, "process umask")?;
13858            kernel
13859                .umask(EXECUTION_DRIVER_NAME, process.kernel_pid, new_mask)
13860                .map(|mask| json!(mask))
13861                .map_err(kernel_error)
13862        }
13863        "fs.chmodSync" | "fs.promises.chmod" => {
13864            let response =
13865                service_javascript_fs_sync_rpc(kernel, process, process.kernel_pid, request)?;
13866            mirror_process_chmod_to_host(process, request)?;
13867            Ok(response)
13868        }
13869        _ => service_javascript_fs_sync_rpc(kernel, process, process.kernel_pid, request),
13870    }
13871}
13872
13873fn service_javascript_internal_bridge_sync_rpc(
13874    process: &ActiveProcess,
13875    request: &JavascriptSyncRpcRequest,
13876) -> Result<Value, SidecarError> {
13877    // Module resolution / loading / format now reads the kernel VFS via
13878    // `service_javascript_module_sync_rpc`. This host-context path only handles
13879    // polyfills, which are static guest expressions independent of the FS.
13880    let method = match request.method.as_str() {
13881        "_loadPolyfill" | "__load_polyfill" => "_loadPolyfill",
13882        other => {
13883            return Err(SidecarError::InvalidState(format!(
13884                "unsupported JavaScript internal bridge method {other}"
13885            )));
13886        }
13887    };
13888
13889    handle_internal_bridge_call_from_host_context(
13890        &process.host_cwd,
13891        &process.guest_cwd,
13892        &process.env,
13893        method,
13894        &request.args,
13895    )
13896    .ok_or_else(|| {
13897        SidecarError::InvalidState(format!(
13898            "JavaScript internal bridge method {method} returned no value"
13899        ))
13900    })
13901}
13902
13903fn mirror_process_chmod_to_host(
13904    process: &ActiveProcess,
13905    request: &JavascriptSyncRpcRequest,
13906) -> Result<(), SidecarError> {
13907    let guest_path = javascript_sync_rpc_arg_str(&request.args, 0, "filesystem chmod path")?;
13908    let mode = javascript_sync_rpc_arg_u32(&request.args, 1, "filesystem chmod mode")? & 0o7777;
13909    let Some(host_path) = resolve_process_guest_path_to_host(process, guest_path) else {
13910        return Ok(());
13911    };
13912    if !host_path.exists() {
13913        return Ok(());
13914    }
13915    fs::set_permissions(&host_path, fs::Permissions::from_mode(mode)).map_err(|error| {
13916        SidecarError::Io(format!(
13917            "failed to mirror chmod to host path {}: {error}",
13918            host_path.display()
13919        ))
13920    })
13921}
13922
13923fn resolve_process_guest_path_to_host(
13924    process: &ActiveProcess,
13925    guest_path: &str,
13926) -> Option<PathBuf> {
13927    let normalized_guest_path = if guest_path.starts_with('/') {
13928        normalize_path(guest_path)
13929    } else {
13930        normalize_path(&format!(
13931            "{}/{}",
13932            process.guest_cwd.trim_end_matches('/'),
13933            guest_path
13934        ))
13935    };
13936    if let Some(host_path) =
13937        host_path_from_runtime_guest_mappings(&process.env, &normalized_guest_path)
13938    {
13939        return Some(host_path);
13940    }
13941    let normalized_guest_cwd = normalize_path(&process.guest_cwd);
13942    let mut host_root = normalize_host_path(&process.host_cwd);
13943    for _ in normalized_guest_cwd
13944        .trim_start_matches('/')
13945        .split('/')
13946        .filter(|segment| !segment.is_empty())
13947    {
13948        host_root = host_root.parent()?.to_path_buf();
13949    }
13950    if normalized_guest_path == "/" {
13951        Some(host_root)
13952    } else {
13953        Some(host_root.join(normalized_guest_path.trim_start_matches('/')))
13954    }
13955}
13956
13957pub(crate) fn service_javascript_crypto_sync_rpc(
13958    process: &mut ActiveProcess,
13959    request: &JavascriptSyncRpcRequest,
13960) -> Result<Value, SidecarError> {
13961    match request.method.as_str() {
13962        "crypto.hashDigest" => {
13963            let algorithm = javascript_crypto_digest_algorithm(
13964                &request.args,
13965                0,
13966                "crypto.hashDigest algorithm",
13967            )?;
13968            let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.hashDigest data")?;
13969            Ok(Value::String(
13970                base64::engine::general_purpose::STANDARD.encode(algorithm.digest(&data)),
13971            ))
13972        }
13973        "crypto.hmacDigest" => {
13974            let algorithm = javascript_crypto_digest_algorithm(
13975                &request.args,
13976                0,
13977                "crypto.hmacDigest algorithm",
13978            )?;
13979            let key = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.hmacDigest key")?;
13980            let data = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.hmacDigest data")?;
13981            Ok(Value::String(
13982                base64::engine::general_purpose::STANDARD.encode(algorithm.hmac(&key, &data)?),
13983            ))
13984        }
13985        "crypto.pbkdf2" => {
13986            let password =
13987                javascript_sync_rpc_base64_arg(&request.args, 0, "crypto.pbkdf2 password")?;
13988            let salt = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.pbkdf2 salt")?;
13989            let iterations =
13990                javascript_sync_rpc_arg_u32(&request.args, 2, "crypto.pbkdf2 iterations")?;
13991            if iterations == 0 {
13992                return Err(SidecarError::InvalidState(String::from(
13993                    "crypto.pbkdf2 iterations must be greater than zero",
13994                )));
13995            }
13996            let key_len = usize::try_from(javascript_sync_rpc_arg_u64(
13997                &request.args,
13998                3,
13999                "crypto.pbkdf2 key length",
14000            )?)
14001            .map_err(|_| {
14002                SidecarError::InvalidState(String::from(
14003                    "crypto.pbkdf2 key length must fit within usize",
14004                ))
14005            })?;
14006            let algorithm =
14007                javascript_crypto_digest_algorithm(&request.args, 4, "crypto.pbkdf2 digest")?;
14008            let mut output = vec![0u8; key_len];
14009            algorithm.pbkdf2(&password, &salt, iterations, &mut output);
14010            Ok(Value::String(
14011                base64::engine::general_purpose::STANDARD.encode(output),
14012            ))
14013        }
14014        "crypto.scrypt" => {
14015            let password =
14016                javascript_sync_rpc_base64_arg(&request.args, 0, "crypto.scrypt password")?;
14017            let salt = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.scrypt salt")?;
14018            let key_len = usize::try_from(javascript_sync_rpc_arg_u64(
14019                &request.args,
14020                2,
14021                "crypto.scrypt key length",
14022            )?)
14023            .map_err(|_| {
14024                SidecarError::InvalidState(String::from(
14025                    "crypto.scrypt key length must fit within usize",
14026                ))
14027            })?;
14028            let options_json =
14029                javascript_sync_rpc_arg_str(&request.args, 3, "crypto.scrypt options")?;
14030            let options: JavascriptScryptOptions =
14031                serde_json::from_str(options_json).map_err(|error| {
14032                    SidecarError::InvalidState(format!(
14033                        "crypto.scrypt options must be valid JSON: {error}"
14034                    ))
14035                })?;
14036            let cost = options.cost.unwrap_or(DEFAULT_SCRYPT_COST);
14037            if cost == 0 || !cost.is_power_of_two() {
14038                return Err(SidecarError::InvalidState(String::from(
14039                    "crypto.scrypt cost must be a positive power of two",
14040                )));
14041            }
14042            let log_n = u8::try_from(cost.ilog2()).map_err(|_| {
14043                SidecarError::InvalidState(String::from(
14044                    "crypto.scrypt cost exceeds supported parameter range",
14045                ))
14046            })?;
14047            let params = ScryptParams::new(
14048                log_n,
14049                options.block_size.unwrap_or(DEFAULT_SCRYPT_BLOCK_SIZE),
14050                options
14051                    .parallelization
14052                    .unwrap_or(DEFAULT_SCRYPT_PARALLELIZATION),
14053                key_len,
14054            )
14055            .map_err(|error| {
14056                SidecarError::InvalidState(format!("crypto.scrypt options are invalid: {error}"))
14057            })?;
14058            let mut output = vec![0u8; key_len];
14059            scrypt(&password, &salt, &params, &mut output).map_err(|error| {
14060                SidecarError::Execution(format!("crypto.scrypt failed: {error}"))
14061            })?;
14062            Ok(Value::String(
14063                base64::engine::general_purpose::STANDARD.encode(output),
14064            ))
14065        }
14066        "crypto.cipheriv" => service_javascript_crypto_cipheriv_sync_rpc(request),
14067        "crypto.decipheriv" => service_javascript_crypto_decipheriv_sync_rpc(request),
14068        "crypto.cipherivCreate" => {
14069            service_javascript_crypto_cipheriv_create_sync_rpc(process, request)
14070        }
14071        "crypto.cipherivUpdate" => {
14072            service_javascript_crypto_cipheriv_update_sync_rpc(process, request)
14073        }
14074        "crypto.cipherivFinal" => {
14075            service_javascript_crypto_cipheriv_final_sync_rpc(process, request)
14076        }
14077        "crypto.sign" => service_javascript_crypto_sign_sync_rpc(request),
14078        "crypto.verify" => service_javascript_crypto_verify_sync_rpc(request),
14079        "crypto.asymmetricOp" => service_javascript_crypto_asymmetric_op_sync_rpc(request),
14080        "crypto.createKeyObject" => service_javascript_crypto_create_key_object_sync_rpc(request),
14081        "crypto.generateKeyPairSync" => {
14082            service_javascript_crypto_generate_key_pair_sync_rpc(request)
14083        }
14084        "crypto.generateKeySync" => service_javascript_crypto_generate_key_sync_rpc(request),
14085        "crypto.generatePrimeSync" => service_javascript_crypto_generate_prime_sync_rpc(request),
14086        "crypto.diffieHellman" => service_javascript_crypto_diffie_hellman_sync_rpc(request),
14087        "crypto.diffieHellmanGroup" => {
14088            service_javascript_crypto_diffie_hellman_group_sync_rpc(request)
14089        }
14090        "crypto.diffieHellmanSessionCreate" => {
14091            service_javascript_crypto_diffie_hellman_session_create_sync_rpc(process, request)
14092        }
14093        "crypto.diffieHellmanSessionCall" => {
14094            service_javascript_crypto_diffie_hellman_session_call_sync_rpc(process, request)
14095        }
14096        "crypto.diffieHellmanSessionDestroy" => {
14097            service_javascript_crypto_diffie_hellman_session_destroy_sync_rpc(process, request)
14098        }
14099        "crypto.subtle" => service_javascript_crypto_subtle_sync_rpc(request),
14100        _ => Err(SidecarError::InvalidState(format!(
14101            "unsupported JavaScript crypto sync RPC method {}",
14102            request.method
14103        ))),
14104    }
14105}
14106
14107fn javascript_crypto_digest_algorithm(
14108    args: &[Value],
14109    index: usize,
14110    label: &str,
14111) -> Result<JavascriptCryptoDigestAlgorithm, SidecarError> {
14112    JavascriptCryptoDigestAlgorithm::parse(javascript_sync_rpc_arg_str(args, index, label)?)
14113}
14114
14115impl JavascriptCryptoDigestAlgorithm {
14116    fn parse(value: &str) -> Result<Self, SidecarError> {
14117        match value.trim().to_ascii_lowercase().replace('-', "").as_str() {
14118            "md5" => Ok(Self::Md5),
14119            "sha1" => Ok(Self::Sha1),
14120            "sha256" => Ok(Self::Sha256),
14121            "sha512" => Ok(Self::Sha512),
14122            _ => Err(SidecarError::InvalidState(format!(
14123                "unsupported crypto digest algorithm {value}"
14124            ))),
14125        }
14126    }
14127
14128    fn digest(self, data: &[u8]) -> Vec<u8> {
14129        match self {
14130            Self::Md5 => Md5::digest(data).to_vec(),
14131            Self::Sha1 => Sha1::digest(data).to_vec(),
14132            Self::Sha256 => Sha256::digest(data).to_vec(),
14133            Self::Sha512 => Sha512::digest(data).to_vec(),
14134        }
14135    }
14136
14137    fn hmac(self, key: &[u8], data: &[u8]) -> Result<Vec<u8>, SidecarError> {
14138        match self {
14139            Self::Md5 => {
14140                let mut mac = Hmac::<Md5>::new_from_slice(key).map_err(|error| {
14141                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14142                })?;
14143                mac.update(data);
14144                Ok(mac.finalize().into_bytes().to_vec())
14145            }
14146            Self::Sha1 => {
14147                let mut mac = Hmac::<Sha1>::new_from_slice(key).map_err(|error| {
14148                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14149                })?;
14150                mac.update(data);
14151                Ok(mac.finalize().into_bytes().to_vec())
14152            }
14153            Self::Sha256 => {
14154                let mut mac = Hmac::<Sha256>::new_from_slice(key).map_err(|error| {
14155                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14156                })?;
14157                mac.update(data);
14158                Ok(mac.finalize().into_bytes().to_vec())
14159            }
14160            Self::Sha512 => {
14161                let mut mac = Hmac::<Sha512>::new_from_slice(key).map_err(|error| {
14162                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14163                })?;
14164                mac.update(data);
14165                Ok(mac.finalize().into_bytes().to_vec())
14166            }
14167        }
14168    }
14169
14170    fn pbkdf2(self, password: &[u8], salt: &[u8], iterations: u32, output: &mut [u8]) {
14171        match self {
14172            Self::Md5 => pbkdf2_hmac::<Md5>(password, salt, iterations, output),
14173            Self::Sha1 => pbkdf2_hmac::<Sha1>(password, salt, iterations, output),
14174            Self::Sha256 => pbkdf2_hmac::<Sha256>(password, salt, iterations, output),
14175            Self::Sha512 => pbkdf2_hmac::<Sha512>(password, salt, iterations, output),
14176        }
14177    }
14178}
14179
14180#[derive(Debug, Clone)]
14181enum JavascriptCryptoKeyMaterial {
14182    Private(PKey<Private>),
14183    Public(PKey<Public>),
14184    Secret(Vec<u8>),
14185}
14186
14187#[derive(Debug, Clone, Deserialize, Serialize)]
14188struct JavascriptSerializedSandboxKeyObject {
14189    #[serde(rename = "type")]
14190    kind: String,
14191    #[serde(skip_serializing_if = "Option::is_none")]
14192    pem: Option<String>,
14193    #[serde(skip_serializing_if = "Option::is_none")]
14194    raw: Option<String>,
14195    #[serde(skip_serializing_if = "Option::is_none", rename = "asymmetricKeyType")]
14196    asymmetric_key_type: Option<String>,
14197    #[serde(
14198        skip_serializing_if = "Option::is_none",
14199        rename = "asymmetricKeyDetails"
14200    )]
14201    asymmetric_key_details: Option<Map<String, Value>>,
14202    #[serde(skip_serializing_if = "Option::is_none")]
14203    jwk: Option<Value>,
14204}
14205
14206#[derive(Debug, Clone)]
14207struct JavascriptDirectKeyInput {
14208    key: JavascriptCryptoKeyMaterial,
14209    padding: Option<Padding>,
14210}
14211
14212fn service_javascript_crypto_cipheriv_sync_rpc(
14213    request: &JavascriptSyncRpcRequest,
14214) -> Result<Value, SidecarError> {
14215    service_javascript_crypto_cipheriv_inner(request, false)
14216}
14217
14218fn service_javascript_crypto_decipheriv_sync_rpc(
14219    request: &JavascriptSyncRpcRequest,
14220) -> Result<Value, SidecarError> {
14221    service_javascript_crypto_cipheriv_inner(request, true)
14222}
14223
14224fn service_javascript_crypto_cipheriv_create_sync_rpc(
14225    process: &mut ActiveProcess,
14226    request: &JavascriptSyncRpcRequest,
14227) -> Result<Value, SidecarError> {
14228    ensure_per_process_state_handle_capacity(process.cipher_sessions.len(), "cipher session")?;
14229    let mode = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.cipherivCreate mode")?;
14230    let decrypt = mode == "decipher";
14231    let algorithm =
14232        javascript_sync_rpc_arg_str(&request.args, 1, "crypto.cipherivCreate algorithm")?;
14233    let key = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.cipherivCreate key")?;
14234    let iv = javascript_sync_rpc_base64_arg_optional(&request.args, 3, "crypto.cipherivCreate iv")?;
14235    let options =
14236        javascript_sync_rpc_json_arg_optional(&request.args, 4, "crypto.cipherivCreate options")?;
14237    let auth_tag_len = javascript_crypto_requested_aead_tag_len(algorithm, options.as_ref())?;
14238    let context = javascript_crypto_build_cipher_context(
14239        algorithm,
14240        &key,
14241        iv.as_deref(),
14242        decrypt,
14243        options.as_ref(),
14244    )?;
14245    process.next_cipher_session_id += 1;
14246    let session_id = process.next_cipher_session_id;
14247    process.cipher_sessions.insert(
14248        session_id,
14249        ActiveCipherSession {
14250            algorithm: algorithm.to_string(),
14251            auth_tag_len,
14252            context,
14253        },
14254    );
14255    Ok(json!(session_id))
14256}
14257
14258fn service_javascript_crypto_cipheriv_update_sync_rpc(
14259    process: &mut ActiveProcess,
14260    request: &JavascriptSyncRpcRequest,
14261) -> Result<Value, SidecarError> {
14262    let session_id =
14263        javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.cipherivUpdate session id")?;
14264    let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.cipherivUpdate data")?;
14265    let session = process
14266        .cipher_sessions
14267        .get_mut(&session_id)
14268        .ok_or_else(|| {
14269            SidecarError::InvalidState(format!("Cipher session {session_id} not found"))
14270        })?;
14271    let result = javascript_crypto_cipher_update(&mut session.context, &data)?;
14272    Ok(Value::String(
14273        base64::engine::general_purpose::STANDARD.encode(result),
14274    ))
14275}
14276
14277fn service_javascript_crypto_cipheriv_final_sync_rpc(
14278    process: &mut ActiveProcess,
14279    request: &JavascriptSyncRpcRequest,
14280) -> Result<Value, SidecarError> {
14281    let session_id =
14282        javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.cipherivFinal session id")?;
14283    let mut session = process.cipher_sessions.remove(&session_id).ok_or_else(|| {
14284        SidecarError::InvalidState(format!("Cipher session {session_id} not found"))
14285    })?;
14286    let data = javascript_crypto_cipher_finalize(&mut session.context)?;
14287    let mut response = Map::new();
14288    response.insert(
14289        String::from("data"),
14290        Value::String(base64::engine::general_purpose::STANDARD.encode(data)),
14291    );
14292    if javascript_crypto_is_aead(&session.algorithm) {
14293        let mut auth_tag = vec![0_u8; session.auth_tag_len];
14294        session
14295            .context
14296            .get_tag(&mut auth_tag)
14297            .map_err(javascript_crypto_openssl_error)?;
14298        response.insert(
14299            String::from("authTag"),
14300            Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
14301        );
14302    }
14303    Ok(Value::String(serde_json::to_string(&response).map_err(
14304        |error| SidecarError::InvalidState(format!("serialize cipher final response: {error}")),
14305    )?))
14306}
14307
14308fn service_javascript_crypto_sign_sync_rpc(
14309    request: &JavascriptSyncRpcRequest,
14310) -> Result<Value, SidecarError> {
14311    let algorithm = request.args.first().and_then(Value::as_str);
14312    let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.sign data")?;
14313    let key_json = javascript_sync_rpc_arg_str(&request.args, 2, "crypto.sign key")?;
14314    let key_input =
14315        javascript_crypto_parse_direct_key_input(key_json, Some("private"), "crypto.sign key")?;
14316    let private_key = javascript_crypto_expect_private_key(key_input.key, "crypto.sign key")?;
14317    let mut signer = javascript_crypto_new_signer(algorithm, &private_key)?;
14318    if let Some(padding) = key_input.padding {
14319        signer
14320            .set_rsa_padding(padding)
14321            .map_err(javascript_crypto_openssl_error)?;
14322    }
14323    signer
14324        .update(&data)
14325        .map_err(javascript_crypto_openssl_error)?;
14326    Ok(Value::String(
14327        base64::engine::general_purpose::STANDARD.encode(
14328            signer
14329                .sign_to_vec()
14330                .map_err(javascript_crypto_openssl_error)?,
14331        ),
14332    ))
14333}
14334
14335fn service_javascript_crypto_verify_sync_rpc(
14336    request: &JavascriptSyncRpcRequest,
14337) -> Result<Value, SidecarError> {
14338    let algorithm = request.args.first().and_then(Value::as_str);
14339    let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.verify data")?;
14340    let key_json = javascript_sync_rpc_arg_str(&request.args, 2, "crypto.verify key")?;
14341    let signature = javascript_sync_rpc_base64_arg(&request.args, 3, "crypto.verify signature")?;
14342    let key_input =
14343        javascript_crypto_parse_direct_key_input(key_json, Some("public"), "crypto.verify key")?;
14344    let public_key = javascript_crypto_expect_public_key(key_input.key, "crypto.verify key")?;
14345    let mut verifier = javascript_crypto_new_verifier(algorithm, &public_key)?;
14346    if let Some(padding) = key_input.padding {
14347        verifier
14348            .set_rsa_padding(padding)
14349            .map_err(javascript_crypto_openssl_error)?;
14350    }
14351    verifier
14352        .update(&data)
14353        .map_err(javascript_crypto_openssl_error)?;
14354    Ok(json!(verifier
14355        .verify(&signature)
14356        .map_err(javascript_crypto_openssl_error)?))
14357}
14358
14359fn service_javascript_crypto_asymmetric_op_sync_rpc(
14360    request: &JavascriptSyncRpcRequest,
14361) -> Result<Value, SidecarError> {
14362    let operation = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.asymmetricOp operation")?;
14363    let key_json = javascript_sync_rpc_arg_str(&request.args, 1, "crypto.asymmetricOp key")?;
14364    let data = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.asymmetricOp data")?;
14365    let expect_kind = match operation {
14366        "publicEncrypt" | "publicDecrypt" => Some("public"),
14367        "privateEncrypt" | "privateDecrypt" => Some("private"),
14368        other => {
14369            return Err(SidecarError::InvalidState(format!(
14370                "Unsupported asymmetric crypto operation: {other}"
14371            )));
14372        }
14373    };
14374    let key_input =
14375        javascript_crypto_parse_direct_key_input(key_json, expect_kind, "crypto.asymmetricOp key")?;
14376    let padding = key_input.padding.unwrap_or(Padding::PKCS1);
14377    let mut output = vec![0_u8; javascript_crypto_rsa_output_size(&key_input.key)?];
14378    let written = match (operation, key_input.key) {
14379        ("publicEncrypt", JavascriptCryptoKeyMaterial::Public(key))
14380        | ("publicDecrypt", JavascriptCryptoKeyMaterial::Public(key)) => {
14381            let rsa = key.rsa().map_err(javascript_crypto_openssl_error)?;
14382            if operation == "publicEncrypt" {
14383                rsa.public_encrypt(&data, &mut output, padding)
14384                    .map_err(javascript_crypto_openssl_error)?
14385            } else {
14386                rsa.public_decrypt(&data, &mut output, padding)
14387                    .map_err(javascript_crypto_openssl_error)?
14388            }
14389        }
14390        ("privateEncrypt", JavascriptCryptoKeyMaterial::Private(key))
14391        | ("privateDecrypt", JavascriptCryptoKeyMaterial::Private(key)) => {
14392            let rsa = key.rsa().map_err(javascript_crypto_openssl_error)?;
14393            if operation == "privateEncrypt" {
14394                rsa.private_encrypt(&data, &mut output, padding)
14395                    .map_err(javascript_crypto_openssl_error)?
14396            } else {
14397                rsa.private_decrypt(&data, &mut output, padding)
14398                    .map_err(javascript_crypto_openssl_error)?
14399            }
14400        }
14401        _ => {
14402            return Err(SidecarError::InvalidState(format!(
14403                "{operation} requires an RSA {} key",
14404                expect_kind.unwrap_or("asymmetric")
14405            )));
14406        }
14407    };
14408    output.truncate(written);
14409    Ok(Value::String(
14410        base64::engine::general_purpose::STANDARD.encode(output),
14411    ))
14412}
14413
14414fn service_javascript_crypto_create_key_object_sync_rpc(
14415    request: &JavascriptSyncRpcRequest,
14416) -> Result<Value, SidecarError> {
14417    let operation =
14418        javascript_sync_rpc_arg_str(&request.args, 0, "crypto.createKeyObject operation")?;
14419    let key_json = javascript_sync_rpc_arg_str(&request.args, 1, "crypto.createKeyObject key")?;
14420    let expected = match operation {
14421        "createPrivateKey" => Some("private"),
14422        "createPublicKey" => Some("public"),
14423        other => {
14424            return Err(SidecarError::InvalidState(format!(
14425                "Unsupported key creation operation: {other}"
14426            )));
14427        }
14428    };
14429    let key_input =
14430        javascript_crypto_parse_direct_key_input(key_json, expected, "crypto.createKeyObject key")?;
14431    Ok(Value::String(
14432        serde_json::to_string(&javascript_crypto_serialize_sandbox_key_object(
14433            &key_input.key,
14434        )?)
14435        .map_err(|error| {
14436            SidecarError::InvalidState(format!("serialize crypto key object: {error}"))
14437        })?,
14438    ))
14439}
14440
14441fn service_javascript_crypto_generate_key_pair_sync_rpc(
14442    request: &JavascriptSyncRpcRequest,
14443) -> Result<Value, SidecarError> {
14444    let key_type =
14445        javascript_sync_rpc_arg_str(&request.args, 0, "crypto.generateKeyPairSync type")?;
14446    let options = javascript_crypto_parse_serialized_options_arg(
14447        &request.args,
14448        1,
14449        "crypto.generateKeyPairSync options",
14450    )?
14451    .unwrap_or(Value::Object(Map::new()));
14452    let public_encoding = options.get("publicKeyEncoding").cloned();
14453    let private_encoding = options.get("privateKeyEncoding").cloned();
14454
14455    let private_key = match key_type {
14456        "rsa" => {
14457            let bits = options
14458                .get("modulusLength")
14459                .and_then(Value::as_u64)
14460                .unwrap_or(2048) as u32;
14461            let exponent = options
14462                .get("publicExponent")
14463                .map(|value| javascript_crypto_u32_from_bridge_value(value, "rsa publicExponent"))
14464                .transpose()?
14465                .unwrap_or(65_537);
14466            let exponent = BigNum::from_u32(exponent).map_err(javascript_crypto_openssl_error)?;
14467            let rsa =
14468                Rsa::generate_with_e(bits, &exponent).map_err(javascript_crypto_openssl_error)?;
14469            PKey::from_rsa(rsa).map_err(javascript_crypto_openssl_error)?
14470        }
14471        "ec" => {
14472            let named_curve = options
14473                .get("namedCurve")
14474                .and_then(Value::as_str)
14475                .ok_or_else(|| {
14476                    SidecarError::InvalidState(String::from(
14477                        "crypto.generateKeyPairSync ec requires namedCurve",
14478                    ))
14479                })?;
14480            let group = EcGroup::from_curve_name(javascript_crypto_curve_nid(named_curve)?)
14481                .map_err(javascript_crypto_openssl_error)?;
14482            let key = EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?;
14483            PKey::from_ec_key(key).map_err(javascript_crypto_openssl_error)?
14484        }
14485        "ed25519" => PKey::generate_ed25519().map_err(javascript_crypto_openssl_error)?,
14486        "x25519" => PKey::generate_x25519().map_err(javascript_crypto_openssl_error)?,
14487        other => {
14488            return Err(SidecarError::InvalidState(format!(
14489                "unsupported crypto key pair type {other}"
14490            )));
14491        }
14492    };
14493    let public_key = PKey::public_key_from_pem(
14494        &private_key
14495            .public_key_to_pem()
14496            .map_err(javascript_crypto_openssl_error)?,
14497    )
14498    .map_err(javascript_crypto_openssl_error)?;
14499    let response = if public_encoding.is_some() || private_encoding.is_some() {
14500        json!({
14501            "publicKey": javascript_crypto_serialize_encoded_key_value_public(&public_key, public_encoding.as_ref())?,
14502            "privateKey": javascript_crypto_serialize_encoded_key_value_private(&private_key, private_encoding.as_ref())?,
14503        })
14504    } else {
14505        json!({
14506            "publicKey": javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Public(public_key))?,
14507            "privateKey": javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Private(private_key))?,
14508        })
14509    };
14510    Ok(Value::String(serde_json::to_string(&response).map_err(
14511        |error| SidecarError::InvalidState(format!("serialize generated key pair: {error}")),
14512    )?))
14513}
14514
14515fn service_javascript_crypto_generate_key_sync_rpc(
14516    request: &JavascriptSyncRpcRequest,
14517) -> Result<Value, SidecarError> {
14518    let key_type = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.generateKeySync type")?;
14519    let options = javascript_crypto_parse_serialized_options_arg(
14520        &request.args,
14521        1,
14522        "crypto.generateKeySync options",
14523    )?
14524    .unwrap_or(Value::Object(Map::new()));
14525    let bit_length = options
14526        .get("length")
14527        .and_then(Value::as_u64)
14528        .ok_or_else(|| {
14529            SidecarError::InvalidState(String::from(
14530                "crypto.generateKeySync options.length is required",
14531            ))
14532        })? as usize;
14533    let mut raw = vec![0_u8; bit_length.div_ceil(8)];
14534    rand_bytes(&mut raw).map_err(javascript_crypto_openssl_error)?;
14535    let serialized = match key_type {
14536        "hmac" => javascript_crypto_serialize_sandbox_key_object(
14537            &JavascriptCryptoKeyMaterial::Secret(raw),
14538        )?,
14539        "aes" => javascript_crypto_serialize_sandbox_key_object(
14540            &JavascriptCryptoKeyMaterial::Secret(raw),
14541        )?,
14542        other => {
14543            return Err(SidecarError::InvalidState(format!(
14544                "unsupported crypto.generateKeySync type {other}"
14545            )));
14546        }
14547    };
14548    Ok(Value::String(serde_json::to_string(&serialized).map_err(
14549        |error| SidecarError::InvalidState(format!("serialize generated key: {error}")),
14550    )?))
14551}
14552
14553fn service_javascript_crypto_generate_prime_sync_rpc(
14554    request: &JavascriptSyncRpcRequest,
14555) -> Result<Value, SidecarError> {
14556    let bits =
14557        javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.generatePrimeSync size")? as i32;
14558    let options = javascript_crypto_parse_serialized_options_arg(
14559        &request.args,
14560        1,
14561        "crypto.generatePrimeSync options",
14562    )?
14563    .unwrap_or(Value::Object(Map::new()));
14564    let safe = options
14565        .get("safe")
14566        .and_then(Value::as_bool)
14567        .unwrap_or(false);
14568    let add = options
14569        .get("add")
14570        .map(|value| javascript_crypto_bignum_from_bridge_value(value, "prime add"))
14571        .transpose()?;
14572    let rem = options
14573        .get("rem")
14574        .map(|value| javascript_crypto_bignum_from_bridge_value(value, "prime rem"))
14575        .transpose()?;
14576    let mut prime = BigNum::new().map_err(javascript_crypto_openssl_error)?;
14577    prime
14578        .generate_prime(bits, safe, add.as_deref(), rem.as_deref())
14579        .map_err(javascript_crypto_openssl_error)?;
14580    let payload = if options
14581        .get("bigint")
14582        .and_then(Value::as_bool)
14583        .unwrap_or(false)
14584    {
14585        json!({
14586            "__type": "bigint",
14587            "value": prime.to_dec_str().map_err(javascript_crypto_openssl_error)?.to_string(),
14588        })
14589    } else {
14590        json!({
14591            "__type": "buffer",
14592            "value": base64::engine::general_purpose::STANDARD.encode(prime.to_vec()),
14593        })
14594    };
14595    Ok(Value::String(serde_json::to_string(&payload).map_err(
14596        |error| SidecarError::InvalidState(format!("serialize generated prime: {error}")),
14597    )?))
14598}
14599
14600fn service_javascript_crypto_diffie_hellman_sync_rpc(
14601    request: &JavascriptSyncRpcRequest,
14602) -> Result<Value, SidecarError> {
14603    let options = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.diffieHellman options")?;
14604    let parsed: Value = serde_json::from_str(options).map_err(|error| {
14605        SidecarError::InvalidState(format!(
14606            "crypto.diffieHellman options must be valid JSON: {error}"
14607        ))
14608    })?;
14609    let private_key = javascript_crypto_parse_key_material_value(
14610        parsed.get("privateKey").ok_or_else(|| {
14611            SidecarError::InvalidState(String::from("crypto.diffieHellman missing privateKey"))
14612        })?,
14613        Some("private"),
14614        "crypto.diffieHellman privateKey",
14615    )?;
14616    let public_key = javascript_crypto_parse_key_material_value(
14617        parsed.get("publicKey").ok_or_else(|| {
14618            SidecarError::InvalidState(String::from("crypto.diffieHellman missing publicKey"))
14619        })?,
14620        Some("public"),
14621        "crypto.diffieHellman publicKey",
14622    )?;
14623    let private_key =
14624        javascript_crypto_expect_private_key(private_key, "crypto.diffieHellman privateKey")?;
14625    let public_key =
14626        javascript_crypto_expect_public_key(public_key, "crypto.diffieHellman publicKey")?;
14627    let mut deriver = Deriver::new(&private_key).map_err(javascript_crypto_openssl_error)?;
14628    deriver
14629        .set_peer(&public_key)
14630        .map_err(javascript_crypto_openssl_error)?;
14631    let secret = deriver
14632        .derive_to_vec()
14633        .map_err(javascript_crypto_openssl_error)?;
14634    Ok(Value::String(
14635        serde_json::to_string(&json!({
14636            "__type": "buffer",
14637            "value": base64::engine::general_purpose::STANDARD.encode(secret),
14638        }))
14639        .map_err(|error| {
14640            SidecarError::InvalidState(format!("serialize derived secret: {error}"))
14641        })?,
14642    ))
14643}
14644
14645fn service_javascript_crypto_diffie_hellman_group_sync_rpc(
14646    request: &JavascriptSyncRpcRequest,
14647) -> Result<Value, SidecarError> {
14648    let name = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.diffieHellmanGroup name")?;
14649    let params = javascript_crypto_named_dh_group(name)?;
14650    let response = json!({
14651        "prime": {
14652            "__type": "buffer",
14653            "value": base64::engine::general_purpose::STANDARD.encode(params.prime_p().to_vec()),
14654        },
14655        "generator": {
14656            "__type": "buffer",
14657            "value": base64::engine::general_purpose::STANDARD.encode(params.generator().to_vec()),
14658        },
14659    });
14660    Ok(Value::String(serde_json::to_string(&response).map_err(
14661        |error| {
14662            SidecarError::InvalidState(format!("serialize diffieHellmanGroup response: {error}"))
14663        },
14664    )?))
14665}
14666
14667fn service_javascript_crypto_diffie_hellman_session_create_sync_rpc(
14668    process: &mut ActiveProcess,
14669    request: &JavascriptSyncRpcRequest,
14670) -> Result<Value, SidecarError> {
14671    ensure_per_process_state_handle_capacity(
14672        process.diffie_hellman_sessions.len(),
14673        "diffie-hellman session",
14674    )?;
14675    let raw = javascript_sync_rpc_arg_str(
14676        &request.args,
14677        0,
14678        "crypto.diffieHellmanSessionCreate request",
14679    )?;
14680    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14681        SidecarError::InvalidState(format!(
14682            "crypto.diffieHellmanSessionCreate request must be valid JSON: {error}"
14683        ))
14684    })?;
14685    let session = match parsed.get("type").and_then(Value::as_str) {
14686        Some("group") => {
14687            let name = parsed.get("name").and_then(Value::as_str).ok_or_else(|| {
14688                SidecarError::InvalidState(String::from(
14689                    "crypto.diffieHellmanSessionCreate group requires name",
14690                ))
14691            })?;
14692            ActiveDiffieHellmanSession::Dh(ActiveDhSession {
14693                params: javascript_crypto_named_dh_group(name)?,
14694                key_pair: None,
14695            })
14696        }
14697        Some("dh") => {
14698            let args = parsed
14699                .get("args")
14700                .and_then(Value::as_array)
14701                .ok_or_else(|| {
14702                    SidecarError::InvalidState(String::from(
14703                        "crypto.diffieHellmanSessionCreate dh requires args",
14704                    ))
14705                })?;
14706            let params = javascript_crypto_build_dh_params(args)?;
14707            ActiveDiffieHellmanSession::Dh(ActiveDhSession {
14708                params,
14709                key_pair: None,
14710            })
14711        }
14712        Some("ecdh") => {
14713            let curve = parsed.get("name").and_then(Value::as_str).ok_or_else(|| {
14714                SidecarError::InvalidState(String::from(
14715                    "crypto.diffieHellmanSessionCreate ecdh requires name",
14716                ))
14717            })?;
14718            ActiveDiffieHellmanSession::Ecdh(ActiveEcdhSession {
14719                curve: curve.to_string(),
14720                key_pair: None,
14721            })
14722        }
14723        other => {
14724            return Err(SidecarError::InvalidState(format!(
14725                "Unsupported Diffie-Hellman session type: {}",
14726                other.unwrap_or("<missing>")
14727            )));
14728        }
14729    };
14730    process.next_diffie_hellman_session_id += 1;
14731    let session_id = process.next_diffie_hellman_session_id;
14732    process.diffie_hellman_sessions.insert(session_id, session);
14733    Ok(json!(session_id))
14734}
14735
14736fn service_javascript_crypto_diffie_hellman_session_call_sync_rpc(
14737    process: &mut ActiveProcess,
14738    request: &JavascriptSyncRpcRequest,
14739) -> Result<Value, SidecarError> {
14740    let session_id = javascript_sync_rpc_arg_u64(
14741        &request.args,
14742        0,
14743        "crypto.diffieHellmanSessionCall session id",
14744    )?;
14745    let raw =
14746        javascript_sync_rpc_arg_str(&request.args, 1, "crypto.diffieHellmanSessionCall request")?;
14747    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14748        SidecarError::InvalidState(format!(
14749            "crypto.diffieHellmanSessionCall request must be valid JSON: {error}"
14750        ))
14751    })?;
14752    let method = parsed
14753        .get("method")
14754        .and_then(Value::as_str)
14755        .ok_or_else(|| {
14756            SidecarError::InvalidState(String::from(
14757                "crypto.diffieHellmanSessionCall request missing method",
14758            ))
14759        })?;
14760    let args = parsed
14761        .get("args")
14762        .and_then(Value::as_array)
14763        .cloned()
14764        .unwrap_or_default();
14765    let session = process
14766        .diffie_hellman_sessions
14767        .get_mut(&session_id)
14768        .ok_or_else(|| {
14769            SidecarError::InvalidState(format!("Diffie-Hellman session {session_id} not found"))
14770        })?;
14771    let (result, has_result) = match session {
14772        ActiveDiffieHellmanSession::Dh(session) => {
14773            javascript_crypto_call_dh_session(session, method, &args)?
14774        }
14775        ActiveDiffieHellmanSession::Ecdh(session) => {
14776            javascript_crypto_call_ecdh_session(session, method, &args)?
14777        }
14778    };
14779    Ok(Value::String(
14780        serde_json::to_string(&json!({
14781            "result": result,
14782            "hasResult": has_result,
14783        }))
14784        .map_err(|error| {
14785            SidecarError::InvalidState(format!("serialize diffie session result: {error}"))
14786        })?,
14787    ))
14788}
14789
14790fn service_javascript_crypto_diffie_hellman_session_destroy_sync_rpc(
14791    process: &mut ActiveProcess,
14792    request: &JavascriptSyncRpcRequest,
14793) -> Result<Value, SidecarError> {
14794    let session_id = javascript_sync_rpc_arg_u64(
14795        &request.args,
14796        0,
14797        "crypto.diffieHellmanSessionDestroy session id",
14798    )?;
14799    process
14800        .diffie_hellman_sessions
14801        .remove(&session_id)
14802        .ok_or_else(|| {
14803            SidecarError::InvalidState(format!("Diffie-Hellman session {session_id} not found"))
14804        })?;
14805    Ok(Value::Null)
14806}
14807
14808fn service_javascript_crypto_subtle_sync_rpc(
14809    request: &JavascriptSyncRpcRequest,
14810) -> Result<Value, SidecarError> {
14811    let raw = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.subtle request")?;
14812    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14813        SidecarError::InvalidState(format!("crypto.subtle request must be valid JSON: {error}"))
14814    })?;
14815    let op = parsed.get("op").and_then(Value::as_str).ok_or_else(|| {
14816        SidecarError::InvalidState(String::from("crypto.subtle request missing op"))
14817    })?;
14818    match op {
14819        "digest" => {
14820            let algorithm = parsed
14821                .get("algorithm")
14822                .and_then(Value::as_str)
14823                .ok_or_else(|| {
14824                    SidecarError::InvalidState(String::from(
14825                        "crypto.subtle.digest missing algorithm",
14826                    ))
14827                })?;
14828            let data = parsed.get("data").and_then(Value::as_str).ok_or_else(|| {
14829                SidecarError::InvalidState(String::from("crypto.subtle.digest missing data"))
14830            })?;
14831            let bytes = base64::engine::general_purpose::STANDARD
14832                .decode(data)
14833                .map_err(|error| {
14834                    SidecarError::InvalidState(format!("crypto.subtle.digest data base64: {error}"))
14835                })?;
14836            let digest = JavascriptCryptoDigestAlgorithm::parse(algorithm)?.digest(&bytes);
14837            Ok(Value::String(
14838                serde_json::to_string(&json!({
14839                    "data": base64::engine::general_purpose::STANDARD.encode(digest),
14840                }))
14841                .map_err(|error| {
14842                    SidecarError::InvalidState(format!("serialize crypto.subtle digest: {error}"))
14843                })?,
14844            ))
14845        }
14846        "generateKey" => {
14847            let algorithm = parsed.get("algorithm").ok_or_else(|| {
14848                SidecarError::InvalidState(String::from(
14849                    "crypto.subtle.generateKey missing algorithm",
14850                ))
14851            })?;
14852            let name =
14853                javascript_crypto_subtle_algorithm_name(algorithm, "crypto.subtle.generateKey")?;
14854            if !matches!(name, "AES-GCM" | "AES-CBC" | "AES-CTR" | "AES-KW") {
14855                return Err(SidecarError::InvalidState(format!(
14856                    "Unsupported key algorithm: {name}"
14857                )));
14858            }
14859            let length_bits = algorithm
14860                .get("length")
14861                .and_then(Value::as_u64)
14862                .ok_or_else(|| {
14863                    SidecarError::InvalidState(String::from(
14864                        "crypto.subtle.generateKey AES algorithm requires length",
14865                    ))
14866                })?;
14867            if length_bits % 8 != 0 {
14868                return Err(SidecarError::InvalidState(String::from(
14869                    "crypto.subtle.generateKey length must be byte-aligned",
14870                )));
14871            }
14872            let length_bytes = usize::try_from(length_bits / 8).map_err(|_| {
14873                SidecarError::InvalidState(String::from(
14874                    "crypto.subtle.generateKey length is too large",
14875                ))
14876            })?;
14877            let mut raw = vec![0_u8; length_bytes];
14878            rand_bytes(&mut raw).map_err(javascript_crypto_openssl_error)?;
14879            let key = javascript_crypto_serialize_subtle_secret_key(
14880                &raw,
14881                javascript_crypto_normalize_subtle_secret_algorithm(algorithm.clone(), &raw)?,
14882                parsed
14883                    .get("extractable")
14884                    .and_then(Value::as_bool)
14885                    .unwrap_or(false),
14886                parsed.get("usages").cloned().unwrap_or_else(|| json!([])),
14887            )?;
14888            Ok(Value::String(
14889                serde_json::to_string(&json!({ "key": key })).map_err(|error| {
14890                    SidecarError::InvalidState(format!(
14891                        "serialize crypto.subtle generated key: {error}"
14892                    ))
14893                })?,
14894            ))
14895        }
14896        "importKey" => {
14897            let format = parsed
14898                .get("format")
14899                .and_then(Value::as_str)
14900                .ok_or_else(|| {
14901                    SidecarError::InvalidState(String::from(
14902                        "crypto.subtle.importKey missing format",
14903                    ))
14904                })?;
14905            if format != "raw" {
14906                return Err(SidecarError::InvalidState(format!(
14907                    "Unsupported import format: {format}"
14908                )));
14909            }
14910            let key_data = parsed
14911                .get("keyData")
14912                .and_then(Value::as_str)
14913                .ok_or_else(|| {
14914                    SidecarError::InvalidState(String::from(
14915                        "crypto.subtle.importKey missing keyData",
14916                    ))
14917                })?;
14918            let raw = base64::engine::general_purpose::STANDARD
14919                .decode(key_data)
14920                .map_err(|error| {
14921                    SidecarError::InvalidState(format!(
14922                        "crypto.subtle.importKey keyData base64: {error}"
14923                    ))
14924                })?;
14925            let algorithm = parsed.get("algorithm").ok_or_else(|| {
14926                SidecarError::InvalidState(String::from(
14927                    "crypto.subtle.importKey missing algorithm",
14928                ))
14929            })?;
14930            let key = javascript_crypto_serialize_subtle_secret_key(
14931                &raw,
14932                javascript_crypto_normalize_subtle_secret_algorithm(algorithm.clone(), &raw)?,
14933                parsed
14934                    .get("extractable")
14935                    .and_then(Value::as_bool)
14936                    .unwrap_or(false),
14937                parsed.get("usages").cloned().unwrap_or_else(|| json!([])),
14938            )?;
14939            Ok(Value::String(
14940                serde_json::to_string(&json!({ "key": key })).map_err(|error| {
14941                    SidecarError::InvalidState(format!(
14942                        "serialize crypto.subtle imported key: {error}"
14943                    ))
14944                })?,
14945            ))
14946        }
14947        "exportKey" => {
14948            let format = parsed
14949                .get("format")
14950                .and_then(Value::as_str)
14951                .ok_or_else(|| {
14952                    SidecarError::InvalidState(String::from(
14953                        "crypto.subtle.exportKey missing format",
14954                    ))
14955                })?;
14956            if format != "raw" {
14957                return Err(SidecarError::InvalidState(format!(
14958                    "Unsupported export format: {format}"
14959                )));
14960            }
14961            let raw = javascript_crypto_subtle_key_raw(
14962                parsed.get("key").ok_or_else(|| {
14963                    SidecarError::InvalidState(String::from("crypto.subtle.exportKey missing key"))
14964                })?,
14965                "crypto.subtle.exportKey key",
14966            )?;
14967            Ok(Value::String(
14968                serde_json::to_string(&json!({
14969                    "data": base64::engine::general_purpose::STANDARD.encode(raw),
14970                }))
14971                .map_err(|error| {
14972                    SidecarError::InvalidState(format!("serialize crypto.subtle export: {error}"))
14973                })?,
14974            ))
14975        }
14976        "encrypt" | "decrypt" => service_javascript_crypto_subtle_aes_crypt_sync_rpc(op, &parsed),
14977        _ => Err(SidecarError::InvalidState(format!(
14978            "Unsupported subtle operation: {op}"
14979        ))),
14980    }
14981}
14982
14983fn javascript_crypto_subtle_algorithm_name<'a>(
14984    algorithm: &'a Value,
14985    label: &str,
14986) -> Result<&'a str, SidecarError> {
14987    if let Some(name) = algorithm.as_str() {
14988        return Ok(name);
14989    }
14990    algorithm
14991        .get("name")
14992        .and_then(Value::as_str)
14993        .ok_or_else(|| SidecarError::InvalidState(format!("{label} algorithm missing name")))
14994}
14995
14996fn javascript_crypto_normalize_subtle_secret_algorithm(
14997    algorithm: Value,
14998    raw: &[u8],
14999) -> Result<Value, SidecarError> {
15000    let mut object = match algorithm {
15001        Value::String(name) => {
15002            let mut object = Map::new();
15003            object.insert(String::from("name"), Value::String(name));
15004            object
15005        }
15006        Value::Object(object) => object,
15007        _ => {
15008            return Err(SidecarError::InvalidState(String::from(
15009                "crypto.subtle secret algorithm must be a string or object",
15010            )));
15011        }
15012    };
15013    let name = object
15014        .get("name")
15015        .and_then(Value::as_str)
15016        .ok_or_else(|| {
15017            SidecarError::InvalidState(String::from("crypto.subtle secret algorithm missing name"))
15018        })?
15019        .to_string();
15020    if matches!(name.as_str(), "AES-GCM" | "AES-CBC" | "AES-CTR" | "AES-KW")
15021        && !object.contains_key("length")
15022    {
15023        object.insert(String::from("length"), json!(raw.len() * 8));
15024    }
15025    Ok(Value::Object(object))
15026}
15027
15028fn javascript_crypto_serialize_subtle_secret_key(
15029    raw: &[u8],
15030    algorithm: Value,
15031    extractable: bool,
15032    usages: Value,
15033) -> Result<Value, SidecarError> {
15034    let raw_base64 = base64::engine::general_purpose::STANDARD.encode(raw);
15035    let source_key_object_data = javascript_crypto_serialize_sandbox_key_object(
15036        &JavascriptCryptoKeyMaterial::Secret(raw.to_vec()),
15037    )?;
15038    Ok(json!({
15039        "type": "secret",
15040        "algorithm": algorithm,
15041        "extractable": extractable,
15042        "usages": usages,
15043        "_raw": raw_base64,
15044        "_sourceKeyObjectData": source_key_object_data,
15045    }))
15046}
15047
15048fn javascript_crypto_subtle_key_raw(key: &Value, label: &str) -> Result<Vec<u8>, SidecarError> {
15049    let raw = key.get("_raw").and_then(Value::as_str).ok_or_else(|| {
15050        SidecarError::InvalidState(format!("{label} must be a raw secret CryptoKey"))
15051    })?;
15052    base64::engine::general_purpose::STANDARD
15053        .decode(raw)
15054        .map_err(|error| SidecarError::InvalidState(format!("{label} raw base64: {error}")))
15055}
15056
15057fn service_javascript_crypto_subtle_aes_crypt_sync_rpc(
15058    op: &str,
15059    parsed: &Value,
15060) -> Result<Value, SidecarError> {
15061    let algorithm = parsed.get("algorithm").ok_or_else(|| {
15062        SidecarError::InvalidState(format!("crypto.subtle.{op} missing algorithm"))
15063    })?;
15064    let name = javascript_crypto_subtle_algorithm_name(algorithm, &format!("crypto.subtle.{op}"))?;
15065    if name != "AES-GCM" {
15066        return Err(SidecarError::InvalidState(format!(
15067            "Unsupported subtle AES operation algorithm: {name}"
15068        )));
15069    }
15070    let key = javascript_crypto_subtle_key_raw(
15071        parsed
15072            .get("key")
15073            .ok_or_else(|| SidecarError::InvalidState(format!("crypto.subtle.{op} missing key")))?,
15074        &format!("crypto.subtle.{op} key"),
15075    )?;
15076    let iv = algorithm.get("iv").and_then(Value::as_str).ok_or_else(|| {
15077        SidecarError::InvalidState(format!("crypto.subtle.{op} AES-GCM missing iv"))
15078    })?;
15079    let iv = base64::engine::general_purpose::STANDARD
15080        .decode(iv)
15081        .map_err(|error| {
15082            SidecarError::InvalidState(format!("crypto.subtle.{op} iv base64: {error}"))
15083        })?;
15084    let data = parsed
15085        .get("data")
15086        .and_then(Value::as_str)
15087        .ok_or_else(|| SidecarError::InvalidState(format!("crypto.subtle.{op} missing data")))?;
15088    let mut data = base64::engine::general_purpose::STANDARD
15089        .decode(data)
15090        .map_err(|error| {
15091            SidecarError::InvalidState(format!("crypto.subtle.{op} data base64: {error}"))
15092        })?;
15093    let tag_len = javascript_crypto_subtle_aes_gcm_tag_len(algorithm)?;
15094    let mut options = Map::new();
15095    options.insert(String::from("authTagLength"), json!(tag_len));
15096    if let Some(additional_data) = algorithm.get("additionalData").and_then(Value::as_str) {
15097        options.insert(
15098            String::from("aad"),
15099            Value::String(additional_data.to_string()),
15100        );
15101    }
15102    let decrypt = op == "decrypt";
15103    if decrypt {
15104        if data.len() < tag_len {
15105            return Err(SidecarError::InvalidState(String::from(
15106                "crypto.subtle.decrypt AES-GCM data shorter than auth tag",
15107            )));
15108        }
15109        let auth_tag = data.split_off(data.len() - tag_len);
15110        options.insert(
15111            String::from("authTag"),
15112            Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
15113        );
15114    }
15115    let cipher_name = format!("aes-{}-gcm", key.len() * 8);
15116    let mut context = javascript_crypto_build_cipher_context(
15117        &cipher_name,
15118        &key,
15119        Some(&iv),
15120        decrypt,
15121        Some(&Value::Object(options)),
15122    )?;
15123    let mut output = javascript_crypto_cipher_update(&mut context, &data)?;
15124    output.extend(javascript_crypto_cipher_finalize(&mut context)?);
15125    if !decrypt {
15126        let mut auth_tag = vec![0_u8; tag_len];
15127        context
15128            .get_tag(&mut auth_tag)
15129            .map_err(javascript_crypto_openssl_error)?;
15130        output.extend(auth_tag);
15131    }
15132    Ok(Value::String(
15133        serde_json::to_string(&json!({
15134            "data": base64::engine::general_purpose::STANDARD.encode(output),
15135        }))
15136        .map_err(|error| {
15137            SidecarError::InvalidState(format!("serialize crypto.subtle {op}: {error}"))
15138        })?,
15139    ))
15140}
15141
15142fn javascript_crypto_subtle_aes_gcm_tag_len(algorithm: &Value) -> Result<usize, SidecarError> {
15143    let tag_bits = algorithm
15144        .get("tagLength")
15145        .and_then(Value::as_u64)
15146        .unwrap_or(128);
15147    if !tag_bits.is_multiple_of(8) {
15148        return Err(SidecarError::InvalidState(String::from(
15149            "crypto.subtle AES-GCM tagLength must be byte-aligned",
15150        )));
15151    }
15152    usize::try_from(tag_bits / 8).map_err(|_| {
15153        SidecarError::InvalidState(String::from("crypto.subtle AES-GCM tagLength too large"))
15154    })
15155}
15156
15157fn service_javascript_crypto_cipheriv_inner(
15158    request: &JavascriptSyncRpcRequest,
15159    decrypt: bool,
15160) -> Result<Value, SidecarError> {
15161    let label = if decrypt {
15162        "crypto.decipheriv"
15163    } else {
15164        "crypto.cipheriv"
15165    };
15166    let algorithm = javascript_sync_rpc_arg_str(&request.args, 0, &format!("{label} algorithm"))?;
15167    let key = javascript_sync_rpc_base64_arg(&request.args, 1, &format!("{label} key"))?;
15168    let iv = javascript_sync_rpc_base64_arg_optional(&request.args, 2, &format!("{label} iv"))?;
15169    let data = javascript_sync_rpc_base64_arg(&request.args, 3, &format!("{label} data"))?;
15170    let options =
15171        javascript_sync_rpc_json_arg_optional(&request.args, 4, &format!("{label} options"))?;
15172    let auth_tag_len = javascript_crypto_requested_aead_tag_len(algorithm, options.as_ref())?;
15173    let mut context = javascript_crypto_build_cipher_context(
15174        algorithm,
15175        &key,
15176        iv.as_deref(),
15177        decrypt,
15178        options.as_ref(),
15179    )?;
15180    let payload = javascript_crypto_cipher_update(&mut context, &data)?;
15181    let final_bytes = javascript_crypto_cipher_finalize(&mut context)?;
15182    if decrypt {
15183        let mut output = payload;
15184        output.extend(final_bytes);
15185        return Ok(Value::String(
15186            base64::engine::general_purpose::STANDARD.encode(output),
15187        ));
15188    }
15189
15190    let mut response = Map::new();
15191    let mut encrypted = payload;
15192    encrypted.extend(final_bytes);
15193    response.insert(
15194        String::from("data"),
15195        Value::String(base64::engine::general_purpose::STANDARD.encode(encrypted)),
15196    );
15197    if javascript_crypto_is_aead(algorithm) {
15198        let mut auth_tag = vec![0_u8; auth_tag_len];
15199        context
15200            .get_tag(&mut auth_tag)
15201            .map_err(javascript_crypto_openssl_error)?;
15202        response.insert(
15203            String::from("authTag"),
15204            Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
15205        );
15206    }
15207    Ok(Value::String(serde_json::to_string(&response).map_err(
15208        |error| SidecarError::InvalidState(format!("serialize {label} response: {error}")),
15209    )?))
15210}
15211
15212fn javascript_sync_rpc_base64_arg_optional(
15213    args: &[Value],
15214    index: usize,
15215    label: &str,
15216) -> Result<Option<Vec<u8>>, SidecarError> {
15217    if args.get(index).is_none() || args[index].is_null() {
15218        return Ok(None);
15219    }
15220    javascript_sync_rpc_base64_arg(args, index, label).map(Some)
15221}
15222
15223fn javascript_sync_rpc_json_arg_optional(
15224    args: &[Value],
15225    index: usize,
15226    label: &str,
15227) -> Result<Option<Value>, SidecarError> {
15228    if args.get(index).is_none() || args[index].is_null() {
15229        return Ok(None);
15230    }
15231    let raw = javascript_sync_rpc_arg_str(args, index, label)?;
15232    serde_json::from_str(raw)
15233        .map(Some)
15234        .map_err(|error| SidecarError::InvalidState(format!("{label} must be valid JSON: {error}")))
15235}
15236
15237fn javascript_crypto_parse_direct_key_input(
15238    raw: &str,
15239    expected: Option<&str>,
15240    label: &str,
15241) -> Result<JavascriptDirectKeyInput, SidecarError> {
15242    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
15243        SidecarError::InvalidState(format!("{label} must be valid JSON: {error}"))
15244    })?;
15245    let padding = match parsed.as_object().and_then(|value| value.get("padding")) {
15246        Some(value) => javascript_crypto_padding_from_value(value)?,
15247        None => None,
15248    };
15249    Ok(JavascriptDirectKeyInput {
15250        key: javascript_crypto_parse_key_material_value(&parsed, expected, label)?,
15251        padding,
15252    })
15253}
15254
15255fn javascript_crypto_parse_key_material_value(
15256    value: &Value,
15257    expected: Option<&str>,
15258    label: &str,
15259) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15260    if let Some(object) = value.as_object() {
15261        if object.get("__type").and_then(Value::as_str) == Some("keyObject") {
15262            let serialized = object.get("value").ok_or_else(|| {
15263                SidecarError::InvalidState(format!("{label} keyObject is missing a value"))
15264            })?;
15265            return javascript_crypto_parse_serialized_key_object(serialized, expected, label);
15266        }
15267        if object.contains_key("type") && (object.contains_key("pem") || object.contains_key("raw"))
15268        {
15269            return javascript_crypto_parse_serialized_key_object(value, expected, label);
15270        }
15271        if let Some(source) = object.get("key") {
15272            return javascript_crypto_parse_key_source(
15273                source,
15274                object.get("format").and_then(Value::as_str),
15275                object.get("type").and_then(Value::as_str),
15276                expected,
15277                label,
15278            );
15279        }
15280    }
15281    javascript_crypto_parse_key_source(value, None, None, expected, label)
15282}
15283
15284fn javascript_crypto_parse_key_source(
15285    source: &Value,
15286    format: Option<&str>,
15287    kind: Option<&str>,
15288    expected: Option<&str>,
15289    label: &str,
15290) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15291    match source {
15292        Value::String(pem) => javascript_crypto_parse_key_from_pem(pem.as_bytes(), expected, label),
15293        Value::Object(object) if object.get("__type").and_then(Value::as_str) == Some("buffer") => {
15294            let data = javascript_crypto_decode_bridge_buffer(source, label)?;
15295            javascript_crypto_parse_key_from_bytes(&data, format, kind, expected, label)
15296        }
15297        Value::Object(_) => {
15298            if format == Some("jwk") {
15299                return Err(SidecarError::InvalidState(format!(
15300                    "{label} jwk inputs are not supported yet"
15301                )));
15302            }
15303            Err(SidecarError::InvalidState(format!(
15304                "{label} has an unsupported key shape"
15305            )))
15306        }
15307        _ => Err(SidecarError::InvalidState(format!(
15308            "{label} has an unsupported key value"
15309        ))),
15310    }
15311}
15312
15313fn javascript_crypto_parse_key_from_pem(
15314    pem: &[u8],
15315    expected: Option<&str>,
15316    label: &str,
15317) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15318    match expected {
15319        Some("private") => PKey::private_key_from_pem(pem)
15320            .map(JavascriptCryptoKeyMaterial::Private)
15321            .map_err(|error| {
15322                SidecarError::InvalidState(format!("{label} private key is invalid: {error}"))
15323            }),
15324        Some("public") => PKey::public_key_from_pem(pem)
15325            .map(JavascriptCryptoKeyMaterial::Public)
15326            .map_err(|error| {
15327                SidecarError::InvalidState(format!("{label} public key is invalid: {error}"))
15328            }),
15329        _ => PKey::private_key_from_pem(pem)
15330            .map(JavascriptCryptoKeyMaterial::Private)
15331            .or_else(|_| PKey::public_key_from_pem(pem).map(JavascriptCryptoKeyMaterial::Public))
15332            .map_err(|error| {
15333                SidecarError::InvalidState(format!("{label} PEM key is invalid: {error}"))
15334            }),
15335    }
15336}
15337
15338fn javascript_crypto_parse_key_from_bytes(
15339    der: &[u8],
15340    format: Option<&str>,
15341    kind: Option<&str>,
15342    expected: Option<&str>,
15343    label: &str,
15344) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15345    match (format.unwrap_or("der"), kind.or(expected)) {
15346        ("der", Some("pkcs8")) | ("der", Some("private")) => PKey::private_key_from_der(der)
15347            .map(JavascriptCryptoKeyMaterial::Private)
15348            .map_err(|error| {
15349                SidecarError::InvalidState(format!("{label} private key DER is invalid: {error}"))
15350            }),
15351        ("der", Some("spki")) | ("der", Some("public")) => PKey::public_key_from_der(der)
15352            .map(JavascriptCryptoKeyMaterial::Public)
15353            .map_err(|error| {
15354                SidecarError::InvalidState(format!("{label} public key DER is invalid: {error}"))
15355            }),
15356        _ => Err(SidecarError::InvalidState(format!(
15357            "{label} unsupported key bytes format"
15358        ))),
15359    }
15360}
15361
15362fn javascript_crypto_parse_serialized_key_object(
15363    value: &Value,
15364    expected: Option<&str>,
15365    label: &str,
15366) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15367    let serialized: JavascriptSerializedSandboxKeyObject = serde_json::from_value(value.clone())
15368        .map_err(|error| {
15369            SidecarError::InvalidState(format!("{label} keyObject is invalid: {error}"))
15370        })?;
15371    match serialized.kind.as_str() {
15372        "secret" => {
15373            if expected == Some("public") || expected == Some("private") {
15374                return Err(SidecarError::InvalidState(format!(
15375                    "{label} expected an asymmetric key"
15376                )));
15377            }
15378            Ok(JavascriptCryptoKeyMaterial::Secret(
15379                base64::engine::general_purpose::STANDARD
15380                    .decode(serialized.raw.unwrap_or_default())
15381                    .map_err(|error| {
15382                        SidecarError::InvalidState(format!(
15383                            "{label} secret key contains invalid base64: {error}"
15384                        ))
15385                    })?,
15386            ))
15387        }
15388        "private" => {
15389            let pem = serialized.pem.ok_or_else(|| {
15390                SidecarError::InvalidState(format!("{label} private keyObject is missing pem"))
15391            })?;
15392            javascript_crypto_parse_key_from_pem(pem.as_bytes(), Some("private"), label)
15393        }
15394        "public" => {
15395            let pem = serialized.pem.ok_or_else(|| {
15396                SidecarError::InvalidState(format!("{label} public keyObject is missing pem"))
15397            })?;
15398            javascript_crypto_parse_key_from_pem(pem.as_bytes(), Some("public"), label)
15399        }
15400        other => Err(SidecarError::InvalidState(format!(
15401            "{label} has unsupported keyObject type {other}"
15402        ))),
15403    }
15404}
15405
15406fn javascript_crypto_expect_private_key(
15407    key: JavascriptCryptoKeyMaterial,
15408    label: &str,
15409) -> Result<PKey<Private>, SidecarError> {
15410    match key {
15411        JavascriptCryptoKeyMaterial::Private(key) => Ok(key),
15412        _ => Err(SidecarError::InvalidState(format!(
15413            "{label} requires a private key"
15414        ))),
15415    }
15416}
15417
15418fn javascript_crypto_expect_public_key(
15419    key: JavascriptCryptoKeyMaterial,
15420    label: &str,
15421) -> Result<PKey<Public>, SidecarError> {
15422    match key {
15423        JavascriptCryptoKeyMaterial::Public(key) => Ok(key),
15424        JavascriptCryptoKeyMaterial::Private(key) => {
15425            let pem = key
15426                .public_key_to_pem()
15427                .map_err(javascript_crypto_openssl_error)?;
15428            PKey::public_key_from_pem(&pem).map_err(javascript_crypto_openssl_error)
15429        }
15430        _ => Err(SidecarError::InvalidState(format!(
15431            "{label} requires a public key"
15432        ))),
15433    }
15434}
15435
15436fn javascript_crypto_new_signer<'a>(
15437    algorithm: Option<&'a str>,
15438    key: &'a PKey<Private>,
15439) -> Result<Signer<'a>, SidecarError> {
15440    if matches!(key.id(), PKeyId::ED25519 | PKeyId::ED448) || algorithm.is_none() {
15441        return Signer::new_without_digest(key).map_err(javascript_crypto_openssl_error);
15442    }
15443    Signer::new(
15444        javascript_crypto_message_digest_from_name(algorithm.ok_or_else(|| {
15445            SidecarError::InvalidState(String::from("crypto.sign requires a digest algorithm"))
15446        })?)?,
15447        key,
15448    )
15449    .map_err(javascript_crypto_openssl_error)
15450}
15451
15452fn javascript_crypto_new_verifier<'a>(
15453    algorithm: Option<&'a str>,
15454    key: &'a PKey<Public>,
15455) -> Result<Verifier<'a>, SidecarError> {
15456    if matches!(key.id(), PKeyId::ED25519 | PKeyId::ED448) || algorithm.is_none() {
15457        return Verifier::new_without_digest(key).map_err(javascript_crypto_openssl_error);
15458    }
15459    Verifier::new(
15460        javascript_crypto_message_digest_from_name(algorithm.ok_or_else(|| {
15461            SidecarError::InvalidState(String::from("crypto.verify requires a digest algorithm"))
15462        })?)?,
15463        key,
15464    )
15465    .map_err(javascript_crypto_openssl_error)
15466}
15467
15468fn javascript_crypto_message_digest_from_name(name: &str) -> Result<MessageDigest, SidecarError> {
15469    match name.trim().to_ascii_lowercase().replace('-', "").as_str() {
15470        "md5" => Ok(MessageDigest::md5()),
15471        "sha1" => Ok(MessageDigest::sha1()),
15472        "sha256" => Ok(MessageDigest::sha256()),
15473        "sha384" => Ok(MessageDigest::sha384()),
15474        "sha512" => Ok(MessageDigest::sha512()),
15475        other => Err(SidecarError::InvalidState(format!(
15476            "unsupported crypto digest algorithm {other}"
15477        ))),
15478    }
15479}
15480
15481fn javascript_crypto_padding_from_value(value: &Value) -> Result<Option<Padding>, SidecarError> {
15482    let Some(number) = value.as_i64() else {
15483        return Ok(None);
15484    };
15485    let padding = match number {
15486        1 => Padding::PKCS1,
15487        3 => Padding::NONE,
15488        4 => Padding::PKCS1_OAEP,
15489        6 => Padding::PKCS1_PSS,
15490        other => {
15491            return Err(SidecarError::InvalidState(format!(
15492                "unsupported RSA padding constant {other}"
15493            )));
15494        }
15495    };
15496    Ok(Some(padding))
15497}
15498
15499fn javascript_crypto_decode_bridge_buffer(
15500    value: &Value,
15501    label: &str,
15502) -> Result<Vec<u8>, SidecarError> {
15503    let base64_value = value
15504        .as_object()
15505        .filter(|object| object.get("__type").and_then(Value::as_str) == Some("buffer"))
15506        .and_then(|object| object.get("value"))
15507        .and_then(Value::as_str)
15508        .ok_or_else(|| {
15509            SidecarError::InvalidState(format!("{label} must be a serialized bridge buffer"))
15510        })?;
15511    base64::engine::general_purpose::STANDARD
15512        .decode(base64_value)
15513        .map_err(|error| {
15514            SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
15515        })
15516}
15517
15518fn javascript_crypto_serialize_sandbox_key_object(
15519    key: &JavascriptCryptoKeyMaterial,
15520) -> Result<Value, SidecarError> {
15521    let serialized = match key {
15522        JavascriptCryptoKeyMaterial::Private(key) => JavascriptSerializedSandboxKeyObject {
15523            kind: String::from("private"),
15524            pem: Some(
15525                String::from_utf8(
15526                    key.private_key_to_pem_pkcs8()
15527                        .map_err(javascript_crypto_openssl_error)?,
15528                )
15529                .map_err(|error| {
15530                    SidecarError::InvalidState(format!("private key PEM is not utf8: {error}"))
15531                })?,
15532            ),
15533            raw: None,
15534            asymmetric_key_type: javascript_crypto_pkey_type_name(key.id()),
15535            asymmetric_key_details: None,
15536            jwk: None,
15537        },
15538        JavascriptCryptoKeyMaterial::Public(key) => JavascriptSerializedSandboxKeyObject {
15539            kind: String::from("public"),
15540            pem: Some(
15541                String::from_utf8(
15542                    key.public_key_to_pem()
15543                        .map_err(javascript_crypto_openssl_error)?,
15544                )
15545                .map_err(|error| {
15546                    SidecarError::InvalidState(format!("public key PEM is not utf8: {error}"))
15547                })?,
15548            ),
15549            raw: None,
15550            asymmetric_key_type: javascript_crypto_pkey_type_name(key.id()),
15551            asymmetric_key_details: None,
15552            jwk: None,
15553        },
15554        JavascriptCryptoKeyMaterial::Secret(raw) => JavascriptSerializedSandboxKeyObject {
15555            kind: String::from("secret"),
15556            pem: None,
15557            raw: Some(base64::engine::general_purpose::STANDARD.encode(raw)),
15558            asymmetric_key_type: None,
15559            asymmetric_key_details: None,
15560            jwk: None,
15561        },
15562    };
15563    serde_json::to_value(serialized)
15564        .map_err(|error| SidecarError::InvalidState(format!("serialize key object: {error}")))
15565}
15566
15567fn javascript_crypto_pkey_type_name(id: PKeyId) -> Option<String> {
15568    match id {
15569        PKeyId::RSA => Some(String::from("rsa")),
15570        PKeyId::EC => Some(String::from("ec")),
15571        PKeyId::ED25519 => Some(String::from("ed25519")),
15572        PKeyId::ED448 => Some(String::from("ed448")),
15573        PKeyId::X25519 => Some(String::from("x25519")),
15574        PKeyId::X448 => Some(String::from("x448")),
15575        PKeyId::DH => Some(String::from("dh")),
15576        _ => None,
15577    }
15578}
15579
15580fn javascript_crypto_rsa_output_size(
15581    key: &JavascriptCryptoKeyMaterial,
15582) -> Result<usize, SidecarError> {
15583    match key {
15584        JavascriptCryptoKeyMaterial::Private(key) => key
15585            .rsa()
15586            .map(|rsa| rsa.size() as usize)
15587            .map_err(javascript_crypto_openssl_error),
15588        JavascriptCryptoKeyMaterial::Public(key) => key
15589            .rsa()
15590            .map(|rsa| rsa.size() as usize)
15591            .map_err(javascript_crypto_openssl_error),
15592        JavascriptCryptoKeyMaterial::Secret(_) => Err(SidecarError::InvalidState(String::from(
15593            "RSA operations require an asymmetric key",
15594        ))),
15595    }
15596}
15597
15598fn javascript_crypto_parse_serialized_options_arg(
15599    args: &[Value],
15600    index: usize,
15601    label: &str,
15602) -> Result<Option<Value>, SidecarError> {
15603    let Some(raw) = args.get(index).and_then(Value::as_str) else {
15604        return Ok(None);
15605    };
15606    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
15607        SidecarError::InvalidState(format!("{label} must be valid JSON: {error}"))
15608    })?;
15609    if parsed.get("hasOptions").and_then(Value::as_bool) == Some(true) {
15610        Ok(parsed.get("options").cloned())
15611    } else {
15612        Ok(None)
15613    }
15614}
15615
15616fn javascript_crypto_u32_from_bridge_value(
15617    value: &Value,
15618    label: &str,
15619) -> Result<u32, SidecarError> {
15620    if let Some(number) = value.as_u64() {
15621        return u32::try_from(number)
15622            .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")));
15623    }
15624    let bytes = javascript_crypto_decode_bridge_buffer(value, label)?;
15625    if bytes.len() > 4 {
15626        return Err(SidecarError::InvalidState(format!(
15627            "{label} buffer is too large for u32"
15628        )));
15629    }
15630    Ok(bytes
15631        .into_iter()
15632        .fold(0_u32, |acc, byte| (acc << 8) | u32::from(byte)))
15633}
15634
15635fn javascript_crypto_bignum_from_bridge_value(
15636    value: &Value,
15637    label: &str,
15638) -> Result<BigNum, SidecarError> {
15639    if let Some(object) = value.as_object() {
15640        if object.get("__type").and_then(Value::as_str) == Some("bigint") {
15641            let decimal = object.get("value").and_then(Value::as_str).ok_or_else(|| {
15642                SidecarError::InvalidState(format!("{label} bigint is missing a value"))
15643            })?;
15644            return BigNum::from_dec_str(decimal).map_err(javascript_crypto_openssl_error);
15645        }
15646    }
15647    let bytes = javascript_crypto_decode_bridge_buffer(value, label)?;
15648    BigNum::from_slice(&bytes).map_err(javascript_crypto_openssl_error)
15649}
15650
15651fn javascript_crypto_curve_nid(name: &str) -> Result<Nid, SidecarError> {
15652    match name {
15653        "prime256v1" | "P-256" => Ok(Nid::X9_62_PRIME256V1),
15654        "secp384r1" | "P-384" => Ok(Nid::SECP384R1),
15655        "secp521r1" | "P-521" => Ok(Nid::SECP521R1),
15656        "secp256k1" => Ok(Nid::SECP256K1),
15657        other => Err(SidecarError::InvalidState(format!(
15658            "unsupported EC curve {other}"
15659        ))),
15660    }
15661}
15662
15663fn javascript_crypto_named_dh_group(name: &str) -> Result<Dh<Params>, SidecarError> {
15664    match name {
15665        "modp2" => Dh::get_1024_160().map_err(javascript_crypto_openssl_error),
15666        "modp14" | "modp15" | "modp16" | "modp17" | "modp18" => {
15667            Dh::get_2048_256().map_err(javascript_crypto_openssl_error)
15668        }
15669        other => Err(SidecarError::InvalidState(format!(
15670            "unsupported Diffie-Hellman group {other}"
15671        ))),
15672    }
15673}
15674
15675fn javascript_crypto_clone_dh_params(params: &Dh<Params>) -> Result<Dh<Params>, SidecarError> {
15676    Dh::from_pqg(
15677        params
15678            .prime_p()
15679            .to_owned()
15680            .map_err(javascript_crypto_openssl_error)?,
15681        params
15682            .prime_q()
15683            .map(|value| value.to_owned().map_err(javascript_crypto_openssl_error))
15684            .transpose()?,
15685        params
15686            .generator()
15687            .to_owned()
15688            .map_err(javascript_crypto_openssl_error)?,
15689    )
15690    .map_err(javascript_crypto_openssl_error)
15691}
15692
15693fn javascript_crypto_build_dh_params(args: &[Value]) -> Result<Dh<Params>, SidecarError> {
15694    let Some(first) = args.first() else {
15695        return Err(SidecarError::InvalidState(String::from(
15696            "Diffie-Hellman session args are required",
15697        )));
15698    };
15699    if let Some(bits) = first.as_u64() {
15700        let generator = args
15701            .get(1)
15702            .map(|value| javascript_crypto_u32_from_bridge_value(value, "Diffie-Hellman generator"))
15703            .transpose()?
15704            .unwrap_or(2);
15705        return Dh::generate_params(bits as u32, generator)
15706            .map_err(javascript_crypto_openssl_error);
15707    }
15708    let prime = javascript_crypto_bignum_from_bridge_value(first, "Diffie-Hellman prime")?;
15709    let generator = args
15710        .get(1)
15711        .map(|value| javascript_crypto_bignum_from_bridge_value(value, "Diffie-Hellman generator"))
15712        .transpose()?
15713        .unwrap_or(BigNum::from_u32(2).map_err(javascript_crypto_openssl_error)?);
15714    Dh::from_pqg(prime, None, generator).map_err(javascript_crypto_openssl_error)
15715}
15716
15717fn javascript_crypto_call_dh_session(
15718    session: &mut ActiveDhSession,
15719    method: &str,
15720    args: &[Value],
15721) -> Result<(Value, bool), SidecarError> {
15722    match method {
15723        "verifyError" => Ok((Value::Null, false)),
15724        "generateKeys" => {
15725            if session.key_pair.is_none() {
15726                session.key_pair = Some(
15727                    javascript_crypto_clone_dh_params(&session.params)?
15728                        .generate_key()
15729                        .map_err(javascript_crypto_openssl_error)?,
15730                );
15731            }
15732            let public = session
15733                .key_pair
15734                .as_ref()
15735                .expect("dh key pair")
15736                .public_key()
15737                .to_vec();
15738            Ok((javascript_crypto_bridge_buffer_value(&public), true))
15739        }
15740        "computeSecret" => {
15741            if session.key_pair.is_none() {
15742                session.key_pair = Some(
15743                    javascript_crypto_clone_dh_params(&session.params)?
15744                        .generate_key()
15745                        .map_err(javascript_crypto_openssl_error)?,
15746                );
15747            }
15748            let peer = javascript_crypto_bignum_from_bridge_value(
15749                args.first().ok_or_else(|| {
15750                    SidecarError::InvalidState(String::from(
15751                        "computeSecret requires peer public key",
15752                    ))
15753                })?,
15754                "Diffie-Hellman peer public key",
15755            )?;
15756            let secret = session
15757                .key_pair
15758                .as_ref()
15759                .expect("dh key pair")
15760                .compute_key(&peer)
15761                .map_err(javascript_crypto_openssl_error)?;
15762            Ok((javascript_crypto_bridge_buffer_value(&secret), true))
15763        }
15764        "getPrime" => Ok((
15765            javascript_crypto_bridge_buffer_value(&session.params.prime_p().to_vec()),
15766            true,
15767        )),
15768        "getGenerator" => Ok((
15769            javascript_crypto_bridge_buffer_value(&session.params.generator().to_vec()),
15770            true,
15771        )),
15772        "getPublicKey" => {
15773            if session.key_pair.is_none() {
15774                session.key_pair = Some(
15775                    javascript_crypto_clone_dh_params(&session.params)?
15776                        .generate_key()
15777                        .map_err(javascript_crypto_openssl_error)?,
15778                );
15779            }
15780            Ok((
15781                javascript_crypto_bridge_buffer_value(
15782                    &session
15783                        .key_pair
15784                        .as_ref()
15785                        .expect("dh key pair")
15786                        .public_key()
15787                        .to_vec(),
15788                ),
15789                true,
15790            ))
15791        }
15792        "getPrivateKey" => {
15793            if session.key_pair.is_none() {
15794                session.key_pair = Some(
15795                    javascript_crypto_clone_dh_params(&session.params)?
15796                        .generate_key()
15797                        .map_err(javascript_crypto_openssl_error)?,
15798                );
15799            }
15800            Ok((
15801                javascript_crypto_bridge_buffer_value(
15802                    &session
15803                        .key_pair
15804                        .as_ref()
15805                        .expect("dh key pair")
15806                        .private_key()
15807                        .to_vec(),
15808                ),
15809                true,
15810            ))
15811        }
15812        other => Err(SidecarError::InvalidState(format!(
15813            "Unsupported Diffie-Hellman method: {other}"
15814        ))),
15815    }
15816}
15817
15818fn javascript_crypto_call_ecdh_session(
15819    session: &mut ActiveEcdhSession,
15820    method: &str,
15821    args: &[Value],
15822) -> Result<(Value, bool), SidecarError> {
15823    let nid = javascript_crypto_curve_nid(&session.curve)?;
15824    let group = EcGroup::from_curve_name(nid).map_err(javascript_crypto_openssl_error)?;
15825    match method {
15826        "verifyError" => Ok((Value::Null, false)),
15827        "generateKeys" => {
15828            if session.key_pair.is_none() {
15829                session.key_pair =
15830                    Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15831            }
15832            let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
15833            let bytes = session
15834                .key_pair
15835                .as_ref()
15836                .expect("ecdh key pair")
15837                .public_key()
15838                .to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx)
15839                .map_err(javascript_crypto_openssl_error)?;
15840            Ok((javascript_crypto_bridge_buffer_value(&bytes), true))
15841        }
15842        "computeSecret" => {
15843            if session.key_pair.is_none() {
15844                session.key_pair =
15845                    Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15846            }
15847            let peer_bytes = javascript_crypto_decode_bridge_buffer(
15848                args.first().ok_or_else(|| {
15849                    SidecarError::InvalidState(String::from(
15850                        "computeSecret requires peer public key",
15851                    ))
15852                })?,
15853                "ECDH peer public key",
15854            )?;
15855            let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
15856            let peer_point = EcPoint::from_bytes(&group, &peer_bytes, &mut ctx)
15857                .map_err(javascript_crypto_openssl_error)?;
15858            let peer_key = EcKey::from_public_key(&group, &peer_point)
15859                .map_err(javascript_crypto_openssl_error)?;
15860            let private =
15861                PKey::from_ec_key(session.key_pair.as_ref().expect("ecdh key pair").to_owned())
15862                    .map_err(javascript_crypto_openssl_error)?;
15863            let peer = PKey::from_ec_key(peer_key).map_err(javascript_crypto_openssl_error)?;
15864            let mut deriver = Deriver::new(&private).map_err(javascript_crypto_openssl_error)?;
15865            deriver
15866                .set_peer(&peer)
15867                .map_err(javascript_crypto_openssl_error)?;
15868            let secret = deriver
15869                .derive_to_vec()
15870                .map_err(javascript_crypto_openssl_error)?;
15871            Ok((javascript_crypto_bridge_buffer_value(&secret), true))
15872        }
15873        "getPublicKey" => {
15874            if session.key_pair.is_none() {
15875                session.key_pair =
15876                    Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15877            }
15878            let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
15879            let bytes = session
15880                .key_pair
15881                .as_ref()
15882                .expect("ecdh key pair")
15883                .public_key()
15884                .to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx)
15885                .map_err(javascript_crypto_openssl_error)?;
15886            Ok((javascript_crypto_bridge_buffer_value(&bytes), true))
15887        }
15888        "getPrivateKey" => {
15889            if session.key_pair.is_none() {
15890                session.key_pair =
15891                    Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15892            }
15893            Ok((
15894                javascript_crypto_bridge_buffer_value(
15895                    &session
15896                        .key_pair
15897                        .as_ref()
15898                        .expect("ecdh key pair")
15899                        .private_key()
15900                        .to_vec(),
15901                ),
15902                true,
15903            ))
15904        }
15905        other => Err(SidecarError::InvalidState(format!(
15906            "Unsupported Diffie-Hellman method: {other}"
15907        ))),
15908    }
15909}
15910
15911fn javascript_crypto_serialize_encoded_key_value_public(
15912    key: &PKey<Public>,
15913    encoding: Option<&Value>,
15914) -> Result<Value, SidecarError> {
15915    if let Some(encoding) = encoding {
15916        let format = encoding
15917            .get("format")
15918            .and_then(Value::as_str)
15919            .unwrap_or("pem");
15920        return Ok(match format {
15921            "der" => json!({
15922                "kind": "buffer",
15923                "value": base64::engine::general_purpose::STANDARD
15924                    .encode(key.public_key_to_der().map_err(javascript_crypto_openssl_error)?),
15925            }),
15926            _ => json!({
15927                "kind": "string",
15928                "value": String::from_utf8(
15929                    key.public_key_to_pem().map_err(javascript_crypto_openssl_error)?,
15930                )
15931                .map_err(|error| SidecarError::InvalidState(format!("public key PEM utf8: {error}")))?,
15932            }),
15933        });
15934    }
15935    javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Public(
15936        key.to_owned(),
15937    ))
15938}
15939
15940fn javascript_crypto_serialize_encoded_key_value_private(
15941    key: &PKey<Private>,
15942    encoding: Option<&Value>,
15943) -> Result<Value, SidecarError> {
15944    if let Some(encoding) = encoding {
15945        let format = encoding
15946            .get("format")
15947            .and_then(Value::as_str)
15948            .unwrap_or("pem");
15949        return Ok(match format {
15950            "der" => json!({
15951                "kind": "buffer",
15952                "value": base64::engine::general_purpose::STANDARD
15953                    .encode(key.private_key_to_der().map_err(javascript_crypto_openssl_error)?),
15954            }),
15955            _ => json!({
15956                "kind": "string",
15957                "value": String::from_utf8(
15958                    key.private_key_to_pem_pkcs8().map_err(javascript_crypto_openssl_error)?,
15959                )
15960                .map_err(|error| SidecarError::InvalidState(format!("private key PEM utf8: {error}")))?,
15961            }),
15962        });
15963    }
15964    javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Private(
15965        key.to_owned(),
15966    ))
15967}
15968
15969fn javascript_crypto_bridge_buffer_value(bytes: &[u8]) -> Value {
15970    json!({
15971        "__type": "buffer",
15972        "value": base64::engine::general_purpose::STANDARD.encode(bytes),
15973    })
15974}
15975
15976fn javascript_crypto_build_cipher_context(
15977    algorithm: &str,
15978    key: &[u8],
15979    iv: Option<&[u8]>,
15980    decrypt: bool,
15981    options: Option<&Value>,
15982) -> Result<Crypter, SidecarError> {
15983    let cipher = javascript_crypto_cipher_from_name(algorithm)?;
15984    let mode = if decrypt {
15985        Mode::Decrypt
15986    } else {
15987        Mode::Encrypt
15988    };
15989    let mut context =
15990        Crypter::new(cipher, mode, key, iv).map_err(javascript_crypto_openssl_error)?;
15991    if let Some(auto_padding) = options
15992        .and_then(|value| value.get("autoPadding"))
15993        .and_then(Value::as_bool)
15994    {
15995        context.pad(auto_padding);
15996    }
15997    if javascript_crypto_is_aead(algorithm) {
15998        if let Some(aad) = options
15999            .and_then(|value| value.get("aad"))
16000            .and_then(Value::as_str)
16001        {
16002            context
16003                .aad_update(
16004                    &base64::engine::general_purpose::STANDARD
16005                        .decode(aad)
16006                        .map_err(|error| {
16007                            SidecarError::InvalidState(format!(
16008                                "cipher aad contains invalid base64: {error}"
16009                            ))
16010                        })?,
16011                )
16012                .map_err(javascript_crypto_openssl_error)?;
16013        }
16014        if decrypt {
16015            if let Some(auth_tag) = options
16016                .and_then(|value| value.get("authTag"))
16017                .and_then(Value::as_str)
16018            {
16019                let decoded = base64::engine::general_purpose::STANDARD
16020                    .decode(auth_tag)
16021                    .map_err(|error| {
16022                        SidecarError::InvalidState(format!(
16023                            "cipher authTag contains invalid base64: {error}"
16024                        ))
16025                    })?;
16026                context
16027                    .set_tag(&decoded)
16028                    .map_err(javascript_crypto_openssl_error)?;
16029            }
16030        }
16031    }
16032    Ok(context)
16033}
16034
16035fn javascript_crypto_requested_aead_tag_len(
16036    algorithm: &str,
16037    options: Option<&Value>,
16038) -> Result<usize, SidecarError> {
16039    if !javascript_crypto_is_aead(algorithm) {
16040        return Ok(0);
16041    }
16042    let requested = options
16043        .and_then(|value| value.get("authTagLength"))
16044        .and_then(Value::as_u64)
16045        .unwrap_or(javascript_crypto_aead_tag_len(algorithm) as u64);
16046    usize::try_from(requested).map_err(|_| {
16047        SidecarError::InvalidState(String::from("cipher authTagLength must fit within usize"))
16048    })
16049}
16050
16051fn javascript_crypto_cipher_update(
16052    context: &mut Crypter,
16053    data: &[u8],
16054) -> Result<Vec<u8>, SidecarError> {
16055    let mut output = vec![0_u8; data.len() + 32];
16056    let written = context
16057        .update(data, &mut output)
16058        .map_err(javascript_crypto_openssl_error)?;
16059    output.truncate(written);
16060    Ok(output)
16061}
16062
16063fn javascript_crypto_cipher_finalize(context: &mut Crypter) -> Result<Vec<u8>, SidecarError> {
16064    let mut output = vec![0_u8; 32];
16065    let written = context
16066        .finalize(&mut output)
16067        .map_err(javascript_crypto_openssl_error)?;
16068    output.truncate(written);
16069    Ok(output)
16070}
16071
16072fn javascript_crypto_cipher_from_name(name: &str) -> Result<Cipher, SidecarError> {
16073    match name.to_ascii_lowercase().as_str() {
16074        "aes-128-cbc" => Ok(Cipher::aes_128_cbc()),
16075        "aes-192-cbc" => Ok(Cipher::aes_192_cbc()),
16076        "aes-256-cbc" => Ok(Cipher::aes_256_cbc()),
16077        "aes-128-ctr" => Ok(Cipher::aes_128_ctr()),
16078        "aes-192-ctr" => Ok(Cipher::aes_192_ctr()),
16079        "aes-256-ctr" => Ok(Cipher::aes_256_ctr()),
16080        "aes-128-gcm" => Ok(Cipher::aes_128_gcm()),
16081        "aes-192-gcm" => Ok(Cipher::aes_192_gcm()),
16082        "aes-256-gcm" => Ok(Cipher::aes_256_gcm()),
16083        other => Err(SidecarError::InvalidState(format!(
16084            "unsupported crypto cipher algorithm {other}"
16085        ))),
16086    }
16087}
16088
16089fn javascript_crypto_is_aead(algorithm: &str) -> bool {
16090    algorithm.to_ascii_lowercase().ends_with("-gcm")
16091}
16092
16093fn javascript_crypto_aead_tag_len(_algorithm: &str) -> usize {
16094    16
16095}
16096
16097fn javascript_crypto_openssl_error(error: openssl::error::ErrorStack) -> SidecarError {
16098    SidecarError::Execution(format!("crypto operation failed: {error}"))
16099}
16100
16101fn service_javascript_kernel_stdin_sync_rpc(
16102    kernel: &mut SidecarKernel,
16103    process: &mut ActiveProcess,
16104    request: &JavascriptSyncRpcRequest,
16105) -> Result<Value, SidecarError> {
16106    let max_bytes =
16107        javascript_sync_rpc_arg_u64_optional(&request.args, 0, "__kernel_stdin_read max bytes")?
16108            .map(|value| value.clamp(1, DEFAULT_KERNEL_STDIN_READ_MAX_BYTES as u64) as usize)
16109            .unwrap_or(DEFAULT_KERNEL_STDIN_READ_MAX_BYTES);
16110    let timeout_ms =
16111        javascript_sync_rpc_arg_u64_optional(&request.args, 1, "__kernel_stdin_read timeout ms")?
16112            .unwrap_or(DEFAULT_KERNEL_STDIN_READ_TIMEOUT_MS);
16113
16114    match kernel
16115        .fd_read_with_timeout_result(
16116            EXECUTION_DRIVER_NAME,
16117            process.kernel_pid,
16118            0,
16119            max_bytes,
16120            Some(Duration::from_millis(timeout_ms)),
16121        )
16122        .map_err(kernel_error)
16123    {
16124        Ok(Some(chunk)) if !chunk.is_empty() => Ok(json!({
16125            "dataBase64": base64::engine::general_purpose::STANDARD.encode(chunk),
16126        })),
16127        Ok(Some(_)) => Ok(Value::Null),
16128        Ok(None) => Ok(json!({
16129            "done": true,
16130        })),
16131        Err(SidecarError::Kernel(error)) if error.starts_with("EAGAIN:") => Ok(Value::Null),
16132        Err(error) => Err(error),
16133    }
16134}
16135
16136fn service_javascript_pty_set_raw_mode_sync_rpc(
16137    kernel: &mut SidecarKernel,
16138    process: &mut ActiveProcess,
16139    request: &JavascriptSyncRpcRequest,
16140) -> Result<Value, SidecarError> {
16141    let enabled = javascript_sync_rpc_arg_bool(&request.args, 0, "__pty_set_raw_mode enabled")?;
16142    kernel
16143        .pty_set_discipline(
16144            EXECUTION_DRIVER_NAME,
16145            process.kernel_pid,
16146            0,
16147            LineDisciplineConfig {
16148                canonical: Some(!enabled),
16149                echo: Some(!enabled),
16150                isig: Some(!enabled),
16151            },
16152        )
16153        .map_err(kernel_error)?;
16154    Ok(Value::Null)
16155}
16156
16157fn service_javascript_kernel_stdio_write_sync_rpc(
16158    kernel: &mut SidecarKernel,
16159    process: &mut ActiveProcess,
16160    request: &JavascriptSyncRpcRequest,
16161) -> Result<Value, SidecarError> {
16162    let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_stdio_write fd")?;
16163    let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "__kernel_stdio_write chunk")?;
16164
16165    let written = match fd {
16166        1 => kernel
16167            .write_process_stdout(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
16168            .map_err(kernel_error)?,
16169        2 => kernel
16170            .write_process_stderr(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
16171            .map_err(kernel_error)?,
16172        other => {
16173            return Err(SidecarError::InvalidState(format!(
16174                "__kernel_stdio_write only supports fd 1/2, got {other}"
16175            )));
16176        }
16177    };
16178
16179    let event = if fd == 1 {
16180        ActiveExecutionEvent::Stdout(chunk)
16181    } else {
16182        ActiveExecutionEvent::Stderr(chunk)
16183    };
16184    process.queue_pending_execution_event(event)?;
16185
16186    Ok(json!(written))
16187}
16188
16189fn service_javascript_kernel_poll_sync_rpc(
16190    kernel: &mut SidecarKernel,
16191    process: &ActiveProcess,
16192    request: &JavascriptSyncRpcRequest,
16193) -> Result<Value, SidecarError> {
16194    let fd_requests: Vec<KernelPollFdRequest> = serde_json::from_value(
16195        request
16196            .args
16197            .first()
16198            .cloned()
16199            .unwrap_or_else(|| Value::Array(Vec::new())),
16200    )
16201    .map_err(|error| {
16202        SidecarError::InvalidState(format!(
16203            "__kernel_poll fd list must be a JSON array of {{ fd, events }} objects: {error}"
16204        ))
16205    })?;
16206    let timeout_ms =
16207        javascript_sync_rpc_arg_u64_optional(&request.args, 1, "__kernel_poll timeout ms")?
16208            .unwrap_or_default();
16209    let timeout_ms = i32::try_from(timeout_ms).map_err(|_| {
16210        SidecarError::InvalidState(String::from("__kernel_poll timeout ms must fit within i32"))
16211    })?;
16212
16213    let poll_fds = fd_requests
16214        .iter()
16215        .map(|entry| PollFd {
16216            fd: entry.fd,
16217            events: PollEvents::from_bits(entry.events),
16218            revents: PollEvents::empty(),
16219        })
16220        .collect::<Vec<_>>();
16221    let result = kernel
16222        .poll_fds(
16223            EXECUTION_DRIVER_NAME,
16224            process.kernel_pid,
16225            poll_fds,
16226            timeout_ms,
16227        )
16228        .map_err(kernel_error)?;
16229
16230    Ok(json!({
16231        "readyCount": result.ready_count,
16232        "fds": result
16233            .fds
16234            .into_iter()
16235            .map(|entry| KernelPollFdResponse {
16236                fd: entry.fd,
16237                events: entry.events.bits(),
16238                revents: entry.revents.bits(),
16239            })
16240            .collect::<Vec<_>>(),
16241    }))
16242}
16243
16244fn install_kernel_stdin_pipe(kernel: &mut SidecarKernel, pid: u32) -> Result<u32, SidecarError> {
16245    let (read_fd, write_fd) = kernel
16246        .open_pipe(EXECUTION_DRIVER_NAME, pid)
16247        .map_err(kernel_error)?;
16248    kernel
16249        .fd_dup2(EXECUTION_DRIVER_NAME, pid, read_fd, 0)
16250        .map_err(kernel_error)?;
16251    kernel
16252        .fd_close(EXECUTION_DRIVER_NAME, pid, read_fd)
16253        .map_err(kernel_error)?;
16254    Ok(write_fd)
16255}
16256
16257fn javascript_child_process_stdin_mode(request: &JavascriptChildProcessSpawnRequest) -> &str {
16258    request
16259        .options
16260        .stdio
16261        .first()
16262        .map(String::as_str)
16263        .unwrap_or("pipe")
16264}
16265
16266pub(crate) fn write_kernel_process_stdin(
16267    kernel: &mut SidecarKernel,
16268    process: &mut ActiveProcess,
16269    chunk: &[u8],
16270) -> Result<(), SidecarError> {
16271    if process.runtime == GuestRuntimeKind::JavaScript {
16272        return Ok(());
16273    }
16274    let Some(writer_fd) = process.kernel_stdin_writer_fd else {
16275        return Ok(());
16276    };
16277    kernel
16278        .fd_write(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd, chunk)
16279        .map(|_| ())
16280        .map_err(kernel_error)
16281}
16282
16283pub(crate) fn close_kernel_process_stdin(
16284    kernel: &mut SidecarKernel,
16285    process: &mut ActiveProcess,
16286) -> Result<(), SidecarError> {
16287    let Some(writer_fd) = process.kernel_stdin_writer_fd.take() else {
16288        return Ok(());
16289    };
16290    kernel
16291        .fd_close(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd)
16292        .map_err(kernel_error)
16293}
16294
16295fn parse_http_header_collection(
16296    headers: &BTreeMap<String, Value>,
16297    label: &str,
16298) -> Result<HttpHeaderCollection, SidecarError> {
16299    let mut normalized = BTreeMap::<String, Vec<String>>::new();
16300    let mut raw_pairs = Vec::new();
16301
16302    for (raw_name, value) in headers {
16303        let normalized_name = raw_name.to_ascii_lowercase();
16304        let values = match value {
16305            Value::String(text) => vec![text.clone()],
16306            Value::Array(values) => values
16307                .iter()
16308                .map(|entry| {
16309                    entry.as_str().map(str::to_owned).ok_or_else(|| {
16310                        SidecarError::InvalidState(format!(
16311                            "{label} header {raw_name} must contain only strings"
16312                        ))
16313                    })
16314                })
16315                .collect::<Result<Vec<_>, _>>()?,
16316            other => {
16317                return Err(SidecarError::InvalidState(format!(
16318                    "{label} header {raw_name} must be a string or string array, received {other}"
16319                )));
16320            }
16321        };
16322        raw_pairs.extend(
16323            values
16324                .iter()
16325                .cloned()
16326                .map(|entry| (raw_name.clone(), entry)),
16327        );
16328        normalized
16329            .entry(normalized_name)
16330            .or_default()
16331            .extend(values);
16332    }
16333
16334    Ok(HttpHeaderCollection {
16335        normalized,
16336        raw_pairs,
16337    })
16338}
16339
16340fn http_headers_json(headers: &HttpHeaderCollection) -> Value {
16341    let map = headers
16342        .normalized
16343        .iter()
16344        .map(|(name, values)| {
16345            let value = if values.len() == 1 {
16346                Value::String(values[0].clone())
16347            } else {
16348                Value::Array(values.iter().cloned().map(Value::String).collect())
16349            };
16350            (name.clone(), value)
16351        })
16352        .collect::<Map<String, Value>>();
16353    Value::Object(map)
16354}
16355
16356fn http_raw_headers_json(headers: &HttpHeaderCollection) -> Value {
16357    Value::Array(
16358        headers
16359            .raw_pairs
16360            .iter()
16361            .flat_map(|(name, value)| [Value::String(name.clone()), Value::String(value.clone())])
16362            .collect(),
16363    )
16364}
16365
16366fn is_loopback_request_host(host: &str) -> bool {
16367    let bare = host
16368        .strip_prefix('[')
16369        .and_then(|value| value.strip_suffix(']'))
16370        .unwrap_or(host);
16371    matches!(bare, "localhost" | "127.0.0.1" | "::1")
16372}
16373
16374fn serialize_http_loopback_request(
16375    url: &Url,
16376    options: &JavascriptHttpRequestOptions,
16377    headers: &HttpHeaderCollection,
16378) -> Result<String, SidecarError> {
16379    let body_base64 = options
16380        .body
16381        .as_ref()
16382        .map(|body| base64::engine::general_purpose::STANDARD.encode(body.as_bytes()));
16383    serde_json::to_string(&json!({
16384        "method": options.method.clone().unwrap_or_else(|| String::from("GET")),
16385        "url": http_request_target(url),
16386        "headers": http_headers_json(headers),
16387        "rawHeaders": http_raw_headers_json(headers),
16388        "bodyBase64": body_base64,
16389    }))
16390    .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
16391}
16392
16393fn http_request_target(url: &Url) -> String {
16394    let path = if url.path().is_empty() {
16395        "/"
16396    } else {
16397        url.path()
16398    };
16399    format!(
16400        "{path}{}",
16401        url.query()
16402            .map(|query| format!("?{query}"))
16403            .unwrap_or_default()
16404    )
16405}
16406
16407fn find_kernel_http_listener_process(vm: &VmState, port: u16) -> Option<String> {
16408    vm.active_processes
16409        .iter()
16410        .find_map(|(process_id, process)| {
16411            process.tcp_listeners.values().find_map(|listener| {
16412                let socket_id = listener.kernel_socket_id?;
16413                let record = vm.kernel.socket_get(socket_id)?;
16414                let local_addr = record
16415                    .local_address()
16416                    .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
16417                    .unwrap_or_else(|| listener.guest_local_addr());
16418                if local_addr.port() == port && is_vm_local_http_listener_addr(local_addr.ip()) {
16419                    Some(process_id.to_owned())
16420                } else {
16421                    None
16422                }
16423            })
16424        })
16425}
16426
16427fn is_vm_local_http_listener_addr(ip: IpAddr) -> bool {
16428    ip.is_loopback() || ip.is_unspecified()
16429}
16430
16431fn serialize_kernel_http_fetch_request(
16432    port: u16,
16433    path: &str,
16434    options: &JavascriptHttpRequestOptions,
16435    headers: &HttpHeaderCollection,
16436) -> Vec<u8> {
16437    let method = options.method.as_deref().unwrap_or("GET");
16438    let mut lines = vec![format!("{method} {path} HTTP/1.1")];
16439    let mut has_host = false;
16440    let mut has_connection = false;
16441    let mut has_content_length = false;
16442    for (name, values) in &headers.normalized {
16443        match name.as_str() {
16444            "host" => has_host = true,
16445            "connection" => has_connection = true,
16446            "content-length" => has_content_length = true,
16447            _ => {}
16448        }
16449        lines.push(format!("{name}: {}", values.join(", ")));
16450    }
16451    if !has_host {
16452        lines.push(format!("Host: 127.0.0.1:{port}"));
16453    }
16454    if !has_connection {
16455        lines.push(String::from("Connection: close"));
16456    }
16457    let body = options.body.as_deref().unwrap_or("").as_bytes();
16458    if !has_content_length && !body.is_empty() {
16459        lines.push(format!("Content-Length: {}", body.len()));
16460    }
16461    lines.push(String::new());
16462    lines.push(String::new());
16463
16464    let mut request = lines.join("\r\n").into_bytes();
16465    request.extend_from_slice(body);
16466    request
16467}
16468
16469fn parse_kernel_http_fetch_response(
16470    buffer: &[u8],
16471    peer_closed: bool,
16472    url: &str,
16473) -> Result<Option<String>, SidecarError> {
16474    let Some(header_end) = find_http_header_end(buffer) else {
16475        return Ok(None);
16476    };
16477    let header_bytes = &buffer[..header_end];
16478    let head = String::from_utf8_lossy(header_bytes);
16479    let mut lines = head.split("\r\n");
16480    let status_line = lines.next().unwrap_or_default();
16481    let mut status_parts = status_line.splitn(3, ' ');
16482    let version = status_parts.next().unwrap_or_default();
16483    if !version.starts_with("HTTP/") {
16484        return Err(SidecarError::Execution(format!(
16485            "invalid vm.fetch HTTP response status line: {status_line}"
16486        )));
16487    }
16488    let status = status_parts
16489        .next()
16490        .ok_or_else(|| {
16491            SidecarError::Execution(format!(
16492                "invalid vm.fetch HTTP response status line: {status_line}"
16493            ))
16494        })?
16495        .parse::<u16>()
16496        .map_err(|error| {
16497            SidecarError::Execution(format!(
16498                "invalid vm.fetch HTTP response status code in {status_line:?}: {error}"
16499            ))
16500        })?;
16501    let status_text = status_parts.next().unwrap_or_default();
16502    let mut headers = Vec::new();
16503    let mut raw_headers = Vec::new();
16504    let mut content_length = None;
16505    let mut transfer_encoding_values = Vec::new();
16506    for line in lines {
16507        if line.is_empty() {
16508            continue;
16509        }
16510        let Some((name, value)) = line.split_once(':') else {
16511            return Err(SidecarError::Execution(format!(
16512                "invalid vm.fetch HTTP response header line: {line}"
16513            )));
16514        };
16515        let value = value.trim().to_owned();
16516        let normalized = name.to_ascii_lowercase();
16517        if normalized == "content-length" {
16518            content_length = Some(value.parse::<usize>().map_err(|error| {
16519                SidecarError::Execution(format!(
16520                    "invalid vm.fetch Content-Length header {value:?}: {error}"
16521                ))
16522            })?);
16523        } else if normalized == "transfer-encoding" {
16524            transfer_encoding_values.push(value.clone());
16525        }
16526        headers.push(json!([normalized, value.clone()]));
16527        raw_headers.push(Value::String(name.to_owned()));
16528        raw_headers.push(Value::String(value));
16529    }
16530
16531    let body_start = header_end + 4;
16532    let transfer_encoding = transfer_encoding_tokens(&transfer_encoding_values);
16533    let is_chunked = transfer_encoding.iter().any(|token| token == "chunked");
16534    let body = if is_chunked {
16535        if content_length.is_some() {
16536            return Err(SidecarError::Execution(String::from(
16537                "vm.fetch HTTP response cannot include both Transfer-Encoding: chunked and Content-Length",
16538            )));
16539        }
16540        if transfer_encoding.len() != 1 {
16541            return Err(SidecarError::Execution(format!(
16542                "unsupported vm.fetch Transfer-Encoding: {}",
16543                transfer_encoding.join(", ")
16544            )));
16545        }
16546        let Some(decoded) = decode_kernel_http_chunked_body(&buffer[body_start..])? else {
16547            return Ok(None);
16548        };
16549        decoded
16550    } else if !transfer_encoding.is_empty() {
16551        return Err(SidecarError::Execution(format!(
16552            "unsupported vm.fetch Transfer-Encoding: {}",
16553            transfer_encoding.join(", ")
16554        )));
16555    } else if let Some(content_length) = content_length {
16556        let body_end = body_start.saturating_add(content_length);
16557        if buffer.len() < body_end {
16558            return Ok(None);
16559        }
16560        buffer[body_start..body_end].to_vec()
16561    } else if peer_closed {
16562        buffer[body_start..].to_vec()
16563    } else {
16564        return Ok(None);
16565    };
16566
16567    serde_json::to_string(&json!({
16568        "status": status,
16569        "statusText": status_text,
16570        "headers": headers,
16571        "rawHeaders": raw_headers,
16572        "body": base64::engine::general_purpose::STANDARD.encode(&body),
16573        "bodyEncoding": "base64",
16574        "url": url,
16575    }))
16576    .map(Some)
16577    .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
16578}
16579
16580fn find_http_header_end(buffer: &[u8]) -> Option<usize> {
16581    buffer.windows(4).position(|window| window == b"\r\n\r\n")
16582}
16583
16584fn find_crlf(buffer: &[u8], start: usize) -> Option<usize> {
16585    buffer
16586        .get(start..)?
16587        .windows(2)
16588        .position(|window| window == b"\r\n")
16589        .map(|offset| start + offset)
16590}
16591
16592fn transfer_encoding_tokens(values: &[String]) -> Vec<String> {
16593    values
16594        .iter()
16595        .flat_map(|value| value.split(','))
16596        .map(|token| token.trim().to_ascii_lowercase())
16597        .filter(|token| !token.is_empty())
16598        .collect()
16599}
16600
16601fn decode_kernel_http_chunked_body(buffer: &[u8]) -> Result<Option<Vec<u8>>, SidecarError> {
16602    let mut offset = 0;
16603    let mut body = Vec::new();
16604    loop {
16605        let Some(line_end) = find_crlf(buffer, offset) else {
16606            return Ok(None);
16607        };
16608        let size_line = std::str::from_utf8(&buffer[offset..line_end]).map_err(|error| {
16609            SidecarError::Execution(format!(
16610                "invalid vm.fetch chunk size line encoding: {error}"
16611            ))
16612        })?;
16613        let size_part = size_line.split(';').next().unwrap_or_default();
16614        if size_part.is_empty() || !size_part.bytes().all(|byte| byte.is_ascii_hexdigit()) {
16615            return Err(SidecarError::Execution(format!(
16616                "invalid vm.fetch chunk size line: {size_line:?}"
16617            )));
16618        }
16619        let chunk_size = usize::from_str_radix(size_part, 16).map_err(|error| {
16620            SidecarError::Execution(format!(
16621                "invalid vm.fetch chunk size {size_part:?}: {error}"
16622            ))
16623        })?;
16624        let chunk_start = line_end + 2;
16625        let chunk_end = chunk_start
16626            .checked_add(chunk_size)
16627            .ok_or_else(|| SidecarError::Execution(String::from("vm.fetch chunk size overflow")))?;
16628        if chunk_size > 0 {
16629            let chunk_terminator_end = chunk_end.checked_add(2).ok_or_else(|| {
16630                SidecarError::Execution(String::from("vm.fetch chunk terminator overflow"))
16631            })?;
16632            if chunk_terminator_end > buffer.len() {
16633                return Ok(None);
16634            }
16635            if buffer.get(chunk_end..chunk_terminator_end) != Some(b"\r\n") {
16636                return Err(SidecarError::Execution(String::from(
16637                    "invalid vm.fetch chunk terminator",
16638                )));
16639            }
16640            body.extend_from_slice(&buffer[chunk_start..chunk_end]);
16641            offset = chunk_terminator_end;
16642            continue;
16643        }
16644
16645        if buffer.get(chunk_start..chunk_start + 2) == Some(b"\r\n") {
16646            return Ok(Some(body));
16647        }
16648        let Some(trailer_end) = find_http_header_end(&buffer[chunk_start..]) else {
16649            return Ok(None);
16650        };
16651        let trailer_bytes = &buffer[chunk_start..chunk_start + trailer_end];
16652        let trailers = String::from_utf8_lossy(trailer_bytes);
16653        for line in trailers.split("\r\n") {
16654            if line.is_empty() {
16655                continue;
16656            }
16657            if line.starts_with(' ') || line.starts_with('\t') || !line.contains(':') {
16658                return Err(SidecarError::Execution(format!(
16659                    "invalid vm.fetch chunk trailer line: {line}"
16660                )));
16661            }
16662        }
16663        return Ok(Some(body));
16664    }
16665}
16666
16667fn kernel_http_fetch_target_exit_code(error: &SidecarError) -> Option<i32> {
16668    let SidecarError::Execution(message) = error else {
16669        return None;
16670    };
16671    message
16672        .strip_prefix("vm.fetch target exited before responding (exit code ")?
16673        .strip_suffix(')')?
16674        .parse()
16675        .ok()
16676}
16677
16678#[allow(clippy::too_many_arguments)]
16679fn service_host_fetch_target_event<B>(
16680    bridge: &SharedBridge<B>,
16681    vm_id: &str,
16682    dns: &VmDnsConfig,
16683    socket_paths: &JavascriptSocketPathContext,
16684    kernel: &mut SidecarKernel,
16685    process: &mut ActiveProcess,
16686    resource_limits: &ResourceLimits,
16687    wait: Duration,
16688) -> Result<bool, SidecarError>
16689where
16690    B: NativeSidecarBridge + Send + 'static,
16691    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16692{
16693    let Some(event) = process
16694        .execution
16695        .poll_event_blocking(wait)
16696        .map_err(|error| SidecarError::Execution(error.to_string()))?
16697    else {
16698        return Ok(false);
16699    };
16700
16701    match event {
16702        ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
16703            let network_counts = process.network_resource_counts();
16704            let response = service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
16705                bridge,
16706                vm_id,
16707                dns,
16708                socket_paths,
16709                kernel,
16710                process,
16711                sync_request: &request,
16712                resource_limits,
16713                network_counts,
16714            });
16715            match response {
16716                Ok(result) => process
16717                    .execution
16718                    .respond_javascript_sync_rpc_success(request.id, result)
16719                    .or_else(ignore_stale_javascript_sync_rpc_response)?,
16720                Err(error) => process
16721                    .execution
16722                    .respond_javascript_sync_rpc_error(
16723                        request.id,
16724                        javascript_sync_rpc_error_code(&error),
16725                        error.to_string(),
16726                    )
16727                    .or_else(ignore_stale_javascript_sync_rpc_response)?,
16728            }
16729        }
16730        ActiveExecutionEvent::Exited(code) => {
16731            return Err(SidecarError::Execution(format!(
16732                "vm.fetch target exited before responding (exit code {code})"
16733            )));
16734        }
16735        other => {
16736            process.queue_pending_execution_event(other)?;
16737        }
16738    }
16739    Ok(true)
16740}
16741
16742fn drain_host_fetch_target_events<B>(
16743    bridge: &SharedBridge<B>,
16744    vm_id: &str,
16745    vm: &mut VmState,
16746    target_process_id: &str,
16747    socket_paths: &JavascriptSocketPathContext,
16748    resource_limits: &ResourceLimits,
16749) -> Result<(), SidecarError>
16750where
16751    B: NativeSidecarBridge + Send + 'static,
16752    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16753{
16754    for _ in 0..32 {
16755        let dns = vm.dns.clone();
16756        let Some(process) = vm.active_processes.get_mut(target_process_id) else {
16757            break;
16758        };
16759        let serviced = service_host_fetch_target_event(
16760            bridge,
16761            vm_id,
16762            &dns,
16763            socket_paths,
16764            &mut vm.kernel,
16765            process,
16766            resource_limits,
16767            Duration::from_millis(1),
16768        )?;
16769        if !serviced {
16770            break;
16771        }
16772    }
16773    Ok(())
16774}
16775
16776#[allow(clippy::too_many_arguments)]
16777fn dispatch_kernel_http_fetch<B>(
16778    bridge: &SharedBridge<B>,
16779    vm_id: &str,
16780    vm: &mut VmState,
16781    target_process_id: &str,
16782    port: u16,
16783    path: &str,
16784    options: &JavascriptHttpRequestOptions,
16785    headers: &HttpHeaderCollection,
16786    max_fetch_response_bytes: usize,
16787) -> Result<String, SidecarError>
16788where
16789    B: NativeSidecarBridge + Send + 'static,
16790    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16791{
16792    let socket_paths = build_javascript_socket_path_context(vm)?;
16793    let family = JavascriptSocketFamily::Ipv4;
16794    let local_port = allocate_guest_listen_port(
16795        0,
16796        family,
16797        &socket_paths.used_tcp_guest_ports,
16798        socket_paths.listen_policy,
16799    )?;
16800    let resource_limits = vm.kernel.resource_limits().clone();
16801    let network_counts = vm_network_resource_counts(vm);
16802    check_network_resource_limit(
16803        resource_limits.max_sockets,
16804        network_counts.sockets,
16805        2,
16806        "socket",
16807    )?;
16808    check_network_resource_limit(
16809        resource_limits.max_connections,
16810        network_counts.connections,
16811        2,
16812        "connection",
16813    )?;
16814
16815    let kernel_pid = vm
16816        .active_processes
16817        .get(target_process_id)
16818        .ok_or_else(|| {
16819            SidecarError::InvalidState(format!(
16820                "vm.fetch target process disappeared: {target_process_id}"
16821            ))
16822        })?
16823        .kernel_pid;
16824    let socket_id = vm
16825        .kernel
16826        .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, SocketSpec::tcp())
16827        .map_err(kernel_error)?;
16828
16829    let result = dispatch_kernel_http_fetch_with_socket(
16830        bridge,
16831        vm_id,
16832        vm,
16833        target_process_id,
16834        kernel_pid,
16835        socket_id,
16836        local_port,
16837        port,
16838        path,
16839        options,
16840        headers,
16841        &socket_paths,
16842        &resource_limits,
16843        max_fetch_response_bytes,
16844    );
16845    let close_result = vm
16846        .kernel
16847        .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id)
16848        .map_err(kernel_error);
16849    let cleanup_result = if result.is_err() {
16850        drain_host_fetch_target_events(
16851            bridge,
16852            vm_id,
16853            vm,
16854            target_process_id,
16855            &socket_paths,
16856            &resource_limits,
16857        )
16858    } else {
16859        Ok(())
16860    };
16861    match (result, close_result) {
16862        (Ok(response), Ok(())) => cleanup_result.map(|()| response),
16863        (Err(error), _) => Err(error),
16864        (Ok(_), Err(error)) => Err(error),
16865    }
16866}
16867
16868#[allow(clippy::too_many_arguments)]
16869fn dispatch_kernel_http_fetch_with_socket<B>(
16870    bridge: &SharedBridge<B>,
16871    vm_id: &str,
16872    vm: &mut VmState,
16873    target_process_id: &str,
16874    kernel_pid: u32,
16875    socket_id: SocketId,
16876    local_port: u16,
16877    port: u16,
16878    path: &str,
16879    options: &JavascriptHttpRequestOptions,
16880    headers: &HttpHeaderCollection,
16881    socket_paths: &JavascriptSocketPathContext,
16882    resource_limits: &ResourceLimits,
16883    max_fetch_response_bytes: usize,
16884) -> Result<String, SidecarError>
16885where
16886    B: NativeSidecarBridge + Send + 'static,
16887    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16888{
16889    vm.kernel
16890        .socket_bind_inet(
16891            EXECUTION_DRIVER_NAME,
16892            kernel_pid,
16893            socket_id,
16894            InetSocketAddress::new("127.0.0.1", local_port),
16895        )
16896        .map_err(kernel_error)?;
16897    vm.kernel
16898        .socket_connect_inet_loopback(
16899            EXECUTION_DRIVER_NAME,
16900            kernel_pid,
16901            socket_id,
16902            InetSocketAddress::new("127.0.0.1", port),
16903        )
16904        .map_err(kernel_error)?;
16905
16906    let request_bytes = serialize_kernel_http_fetch_request(port, path, options, headers);
16907    vm.kernel
16908        .socket_write(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, &request_bytes)
16909        .map_err(kernel_error)?;
16910
16911    let mut response_buffer = Vec::new();
16912    let mut peer_closed = false;
16913    let url = format!("http://127.0.0.1:{port}{path}");
16914    let deadline = Instant::now() + http_loopback_request_timeout();
16915    loop {
16916        if let Some(response) =
16917            parse_kernel_http_fetch_response(&response_buffer, peer_closed, &url)?
16918        {
16919            ensure_vm_fetch_response_within_limit(&response, "vm.fetch", max_fetch_response_bytes)?;
16920            return Ok(response);
16921        }
16922        if Instant::now() >= deadline {
16923            let preview = String::from_utf8_lossy(&response_buffer);
16924            return Err(SidecarError::Execution(format!(
16925                "vm.fetch timed out waiting for kernel TCP HTTP response ({} buffered bytes: {:?})",
16926                response_buffer.len(),
16927                preview.chars().take(200).collect::<String>()
16928            )));
16929        }
16930
16931        {
16932            let dns = vm.dns.clone();
16933            let process = vm
16934                .active_processes
16935                .get_mut(target_process_id)
16936                .ok_or_else(|| {
16937                    SidecarError::InvalidState(format!(
16938                        "vm.fetch target process disappeared: {target_process_id}"
16939                    ))
16940                })?;
16941            service_host_fetch_target_event(
16942                bridge,
16943                vm_id,
16944                &dns,
16945                socket_paths,
16946                &mut vm.kernel,
16947                process,
16948                resource_limits,
16949                Duration::from_millis(5),
16950            )?;
16951        }
16952
16953        let poll = vm
16954            .kernel
16955            .poll_targets(
16956                EXECUTION_DRIVER_NAME,
16957                kernel_pid,
16958                vec![PollTargetEntry::socket(
16959                    socket_id,
16960                    POLLIN | POLLHUP | POLLERR,
16961                )],
16962                5,
16963            )
16964            .map_err(kernel_error)?;
16965        let revents = poll
16966            .targets
16967            .first()
16968            .map(|entry| entry.revents)
16969            .unwrap_or_else(PollEvents::empty);
16970        if revents.intersects(POLLERR) {
16971            return Err(SidecarError::Execution(String::from(
16972                "vm.fetch kernel TCP socket reported POLLERR",
16973            )));
16974        }
16975        if revents.intersects(POLLIN) {
16976            match vm
16977                .kernel
16978                .socket_read(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, 64 * 1024)
16979            {
16980                Ok(Some(bytes)) if !bytes.is_empty() => {
16981                    response_buffer.extend(bytes);
16982                    ensure_vm_fetch_raw_response_buffer_within_limit(
16983                        response_buffer.len(),
16984                        "vm.fetch",
16985                    )?;
16986                }
16987                Ok(Some(_)) => {}
16988                Ok(None) => peer_closed = true,
16989                Err(error) if error.code() == "EAGAIN" => {}
16990                Err(error) => return Err(kernel_error(error)),
16991            }
16992        }
16993        if revents.intersects(POLLHUP) {
16994            peer_closed = true;
16995        }
16996    }
16997}
16998
16999fn outbound_http_response_json(url: &Url, response: ureq::Response) -> Result<Value, SidecarError> {
17000    let status = response.status();
17001    let status_text = response.status_text().to_owned();
17002    let mut header_pairs = Vec::new();
17003    let mut raw_headers = Vec::new();
17004    for raw_name in response.headers_names() {
17005        for value in response.all(&raw_name) {
17006            header_pairs.push(json!([raw_name.to_ascii_lowercase(), value]));
17007            raw_headers.push(Value::String(raw_name.clone()));
17008            raw_headers.push(Value::String(value.to_owned()));
17009        }
17010    }
17011    let mut reader = response.into_reader();
17012    let mut body = Vec::new();
17013    reader.read_to_end(&mut body).map_err(|error| {
17014        SidecarError::Execution(format!("failed to read HTTP response: {error}"))
17015    })?;
17016    serde_json::to_string(&json!({
17017        "status": status,
17018        "statusText": status_text,
17019        "headers": header_pairs,
17020        "rawHeaders": raw_headers,
17021        "body": base64::engine::general_purpose::STANDARD.encode(body),
17022        "bodyEncoding": "base64",
17023        "url": url.as_str(),
17024    }))
17025    .map(Value::String)
17026    .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
17027}
17028
17029/// Split a ureq resolver `netloc` (`host:port`, with optional `[..]` IPv6
17030/// brackets) into its host and port components. Returns `None` if the port is
17031/// missing or unparseable.
17032fn split_netloc(netloc: &str) -> Option<(&str, u16)> {
17033    let (host, port) = netloc.rsplit_once(':')?;
17034    let port: u16 = port.parse().ok()?;
17035    let host = host
17036        .strip_prefix('[')
17037        .and_then(|rest| rest.strip_suffix(']'))
17038        .unwrap_or(host);
17039    Some((host, port))
17040}
17041
17042fn issue_outbound_http_request(
17043    url: &Url,
17044    options: &JavascriptHttpRequestOptions,
17045    headers: &HttpHeaderCollection,
17046    pinned_addresses: &[IpAddr],
17047) -> Result<Value, SidecarError> {
17048    let method = options.method.as_deref().unwrap_or("GET");
17049    // Pin the underlying resolver to the egress-vetted addresses. ureq performs
17050    // its own DNS resolution for the TCP/TLS connect; without this override an
17051    // https:// request would re-resolve the hostname through the host resolver
17052    // (a rebinding DNS server could then return a private/metadata IP that the
17053    // earlier range check would have rejected). The pinned resolver returns only
17054    // the vetted addresses and refuses any host it was not vetted for, while the
17055    // request URL keeps the original hostname so TLS SNI and the Host header stay
17056    // correct.
17057    let pinned_host = url.host_str().map(str::to_owned);
17058    let pinned: Vec<IpAddr> = pinned_addresses.to_vec();
17059    let resolver = move |netloc: &str| -> std::io::Result<Vec<SocketAddr>> {
17060        let (host, port) = split_netloc(netloc).ok_or_else(|| {
17061            std::io::Error::new(
17062                std::io::ErrorKind::InvalidInput,
17063                format!("invalid network location: {netloc}"),
17064            )
17065        })?;
17066        let expected_host = pinned_host.as_deref();
17067        if expected_host != Some(host) {
17068            return Err(std::io::Error::new(
17069                std::io::ErrorKind::PermissionDenied,
17070                format!(
17071                    "EACCES: outbound HTTP resolver pinned to {expected_host:?}, refusing {host}"
17072                ),
17073            ));
17074        }
17075        if pinned.is_empty() {
17076            return Err(std::io::Error::new(
17077                std::io::ErrorKind::PermissionDenied,
17078                "EACCES: no egress-vetted address available for outbound HTTP request",
17079            ));
17080        }
17081        Ok(pinned.iter().map(|ip| SocketAddr::new(*ip, port)).collect())
17082    };
17083    let mut agent_builder = ureq::AgentBuilder::new()
17084        .resolver(resolver)
17085        .timeout_connect(Duration::from_secs(5))
17086        .timeout_read(Duration::from_secs(15))
17087        .timeout_write(Duration::from_secs(15));
17088    if url.scheme() == "https" {
17089        let tls_options = JavascriptTlsBridgeOptions {
17090            is_server: false,
17091            servername: url.host_str().map(str::to_owned),
17092            alpn_protocols: Some(vec![String::from("http/1.1")]),
17093            reject_unauthorized: options.reject_unauthorized,
17094            ..JavascriptTlsBridgeOptions::default()
17095        };
17096        agent_builder = agent_builder.tls_config(Arc::new(build_client_tls_config(&tls_options)?));
17097    }
17098    let agent = agent_builder.build();
17099    let mut request = agent.request_url(method, url);
17100    for (name, values) in &headers.normalized {
17101        if name == "host" {
17102            continue;
17103        }
17104        let header_value = values.join(", ");
17105        request = request.set(name, &header_value);
17106    }
17107    let response = match options.body.as_deref() {
17108        Some(body) => request.send_string(body),
17109        None => request.call(),
17110    };
17111
17112    match response {
17113        Ok(response) => outbound_http_response_json(url, response),
17114        Err(ureq::Error::Status(_, response)) => outbound_http_response_json(url, response),
17115        Err(ureq::Error::Transport(error)) => Err(SidecarError::Execution(format!(
17116            "ERR_HTTP_REQUEST_FAILED: {error}"
17117        ))),
17118    }
17119}
17120
17121fn wait_for_loopback_http_response<B>(
17122    request: LoopbackHttpResponseWaitRequest<'_, B>,
17123) -> Result<String, SidecarError>
17124where
17125    B: NativeSidecarBridge + Send + 'static,
17126    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17127{
17128    let LoopbackHttpResponseWaitRequest {
17129        bridge,
17130        vm_id,
17131        dns,
17132        socket_paths,
17133        kernel,
17134        process,
17135        resource_limits,
17136        request_key,
17137    } = request;
17138    let deadline = Instant::now() + http_loopback_request_timeout();
17139    loop {
17140        if let Some(response) = process
17141            .pending_http_requests
17142            .get(&request_key)
17143            .and_then(|response| response.clone())
17144        {
17145            process.pending_http_requests.remove(&request_key);
17146            return Ok(response);
17147        }
17148
17149        if Instant::now() >= deadline {
17150            process.pending_http_requests.remove(&request_key);
17151            return Err(SidecarError::Execution(String::from(
17152                "HTTP loopback request timed out waiting for net.http_respond",
17153            )));
17154        }
17155
17156        let Some(event) = process
17157            .execution
17158            .poll_event_blocking(Duration::from_millis(10))
17159            .map_err(|error| SidecarError::Execution(error.to_string()))?
17160        else {
17161            continue;
17162        };
17163
17164        match event {
17165            ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
17166                let network_counts = process.network_resource_counts();
17167                let response = service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
17168                    bridge,
17169                    vm_id,
17170                    dns,
17171                    socket_paths,
17172                    kernel,
17173                    process,
17174                    sync_request: &request,
17175                    resource_limits,
17176                    network_counts,
17177                });
17178                match response {
17179                    Ok(result) => process
17180                        .execution
17181                        .respond_javascript_sync_rpc_success(request.id, result)
17182                        .or_else(ignore_stale_javascript_sync_rpc_response)?,
17183                    Err(error) => process
17184                        .execution
17185                        .respond_javascript_sync_rpc_error(
17186                            request.id,
17187                            javascript_sync_rpc_error_code(&error),
17188                            error.to_string(),
17189                        )
17190                        .or_else(ignore_stale_javascript_sync_rpc_response)?,
17191                }
17192            }
17193            ActiveExecutionEvent::Exited(code) => {
17194                process.pending_http_requests.remove(&request_key);
17195                return Err(SidecarError::Execution(format!(
17196                    "HTTP loopback server exited before responding (exit code {code})"
17197                )));
17198            }
17199            ActiveExecutionEvent::Stdout(_)
17200            | ActiveExecutionEvent::Stderr(_)
17201            | ActiveExecutionEvent::PythonVfsRpcRequest(_)
17202            | ActiveExecutionEvent::SignalState { .. } => {}
17203        }
17204    }
17205}
17206
17207pub(crate) fn dispatch_loopback_http_request<B>(
17208    request: LoopbackHttpDispatchRequest<'_, B>,
17209) -> Result<String, SidecarError>
17210where
17211    B: NativeSidecarBridge + Send + 'static,
17212    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17213{
17214    let LoopbackHttpDispatchRequest {
17215        bridge,
17216        vm_id,
17217        dns,
17218        socket_paths,
17219        kernel,
17220        process,
17221        resource_limits,
17222        server_id,
17223        request_json,
17224    } = request;
17225    let request_id = {
17226        let server = process.http_servers.get_mut(&server_id).ok_or_else(|| {
17227            SidecarError::InvalidState(format!("HTTP target server disappeared: {server_id}"))
17228        })?;
17229        server.next_request_id += 1;
17230        server.next_request_id
17231    };
17232    process
17233        .pending_http_requests
17234        .insert((server_id, request_id), None);
17235    process.execution.send_javascript_stream_event(
17236        "http_request",
17237        json!({
17238            "serverId": server_id,
17239            "requestId": request_id,
17240            "request": request_json,
17241        }),
17242    )?;
17243    wait_for_loopback_http_response(LoopbackHttpResponseWaitRequest {
17244        bridge,
17245        vm_id,
17246        dns,
17247        socket_paths,
17248        kernel,
17249        process,
17250        resource_limits,
17251        request_key: (server_id, request_id),
17252    })
17253}
17254
17255fn ensure_vm_fetch_response_within_limit(
17256    response_json: &str,
17257    operation: &str,
17258    limit: usize,
17259) -> Result<(), SidecarError> {
17260    let size = response_json.len();
17261    if size > limit {
17262        return Err(SidecarError::Execution(format!(
17263            "{operation} payload is {size} bytes, limit is {limit}"
17264        )));
17265    }
17266    Ok(())
17267}
17268
17269fn ensure_vm_fetch_raw_response_buffer_within_limit(
17270    size: usize,
17271    operation: &str,
17272) -> Result<(), SidecarError> {
17273    if size > VM_FETCH_BUFFER_LIMIT_BYTES {
17274        return Err(SidecarError::Execution(format!(
17275            "{operation} raw response buffer is {size} bytes, limit is {VM_FETCH_BUFFER_LIMIT_BYTES}"
17276        )));
17277    }
17278    Ok(())
17279}
17280
17281pub(crate) fn ensure_vm_fetch_response_frame_within_limit(
17282    response: &ResponseFrame,
17283    max_frame_bytes: usize,
17284) -> Result<(), SidecarError> {
17285    let max_frame_bytes = max_frame_bytes.min(VM_FETCH_BUFFER_LIMIT_BYTES);
17286    let frame = crate::protocol::to_generated_protocol_frame(
17287        &crate::protocol::ProtocolFrame::Response(response.clone()),
17288    )
17289    .map_err(|error| SidecarError::FrameTooLarge(error.to_string()))?;
17290    let WireProtocolFrame::ResponseFrame(_) = &frame else {
17291        return Err(SidecarError::FrameTooLarge(String::from(
17292            "vm fetch response converted to non-response wire frame",
17293        )));
17294    };
17295    WireFrameCodec::new(max_frame_bytes)
17296        .encode(&frame)
17297        .map(|_| ())
17298        .map_err(|error| SidecarError::FrameTooLarge(error.to_string()))
17299}
17300
17301fn service_javascript_dns_sync_rpc<B>(
17302    bridge: &SharedBridge<B>,
17303    kernel: &SidecarKernel,
17304    vm_id: &str,
17305    dns: &VmDnsConfig,
17306    request: &JavascriptSyncRpcRequest,
17307) -> Result<Value, SidecarError>
17308where
17309    B: NativeSidecarBridge + Send + 'static,
17310    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17311{
17312    match request.method.as_str() {
17313        "dns.lookup" => {
17314            let payload = request
17315                .args
17316                .first()
17317                .cloned()
17318                .ok_or_else(|| {
17319                    SidecarError::InvalidState(String::from(
17320                        "dns.lookup requires a request payload",
17321                    ))
17322                })
17323                .and_then(|value| {
17324                    serde_json::from_value::<JavascriptDnsLookupRequest>(value).map_err(|error| {
17325                        SidecarError::InvalidState(format!("invalid dns.lookup payload: {error}"))
17326                    })
17327                })?;
17328            let addresses = filter_dns_ip_addrs(
17329                resolve_dns_ip_addrs(
17330                    bridge,
17331                    kernel,
17332                    vm_id,
17333                    dns,
17334                    &payload.hostname,
17335                    DnsLookupPolicy::CheckPermissions,
17336                )?,
17337                payload.family,
17338            )?;
17339            let addresses = filter_dns_safe_ip_addrs(addresses, &payload.hostname)?;
17340            Ok(Value::Array(
17341                addresses
17342                    .into_iter()
17343                    .map(|ip| {
17344                        json!({
17345                            "address": ip.to_string(),
17346                            "family": if ip.is_ipv6() { 6 } else { 4 },
17347                        })
17348                    })
17349                    .collect(),
17350            ))
17351        }
17352        "dns.resolve" | "dns.resolve4" | "dns.resolve6" => {
17353            let payload = request
17354                .args
17355                .first()
17356                .cloned()
17357                .ok_or_else(|| {
17358                    SidecarError::InvalidState(String::from(
17359                        "dns.resolve requires a request payload",
17360                    ))
17361                })
17362                .and_then(|value| {
17363                    serde_json::from_value::<JavascriptDnsResolveRequest>(value).map_err(|error| {
17364                        SidecarError::InvalidState(format!("invalid dns.resolve payload: {error}"))
17365                    })
17366                })?;
17367            let requested_type = match request.method.as_str() {
17368                "dns.resolve4" => String::from("A"),
17369                "dns.resolve6" => String::from("AAAA"),
17370                _ => payload
17371                    .rrtype
17372                    .as_deref()
17373                    .unwrap_or("A")
17374                    .to_ascii_uppercase(),
17375            };
17376            let record_type = parse_dns_record_type(&requested_type)?;
17377            let resolution = resolve_dns_records(
17378                bridge,
17379                kernel,
17380                vm_id,
17381                dns,
17382                &payload.hostname,
17383                record_type,
17384                DnsLookupPolicy::CheckPermissions,
17385            )?;
17386            dns_resolution_to_node_value(&resolution, &requested_type)
17387        }
17388        other => Err(SidecarError::InvalidState(format!(
17389            "unsupported JavaScript dns sync RPC method {other}"
17390        ))),
17391    }
17392}
17393
17394fn service_javascript_dgram_sync_rpc<B>(
17395    request: JavascriptDgramSyncRpcServiceRequest<'_, B>,
17396) -> Result<Value, SidecarError>
17397where
17398    B: NativeSidecarBridge + Send + 'static,
17399    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17400{
17401    let JavascriptDgramSyncRpcServiceRequest {
17402        bridge,
17403        kernel,
17404        vm_id,
17405        dns,
17406        socket_paths,
17407        process,
17408        sync_request: request,
17409        resource_limits,
17410        network_counts,
17411    } = request;
17412    match request.method.as_str() {
17413        "dgram.createSocket" => {
17414            check_network_resource_limit(
17415                resource_limits.max_sockets,
17416                network_counts.sockets,
17417                1,
17418                "socket",
17419            )?;
17420            let payload = request
17421                .args
17422                .first()
17423                .cloned()
17424                .ok_or_else(|| {
17425                    SidecarError::InvalidState(String::from(
17426                        "dgram.createSocket requires a request payload",
17427                    ))
17428                })
17429                .and_then(|value| {
17430                    serde_json::from_value::<JavascriptDgramCreateSocketRequest>(value).map_err(
17431                        |error| {
17432                            SidecarError::InvalidState(format!(
17433                                "invalid dgram.createSocket payload: {error}"
17434                            ))
17435                        },
17436                    )
17437                })?;
17438            let family = JavascriptUdpFamily::from_socket_type(&payload.socket_type)?;
17439            let socket_id = process.allocate_udp_socket_id();
17440            process.udp_sockets.insert(
17441                socket_id.clone(),
17442                ActiveUdpSocket::new(kernel, process.kernel_pid, family)?,
17443            );
17444            Ok(json!({
17445                "socketId": socket_id,
17446                "type": family.socket_type(),
17447            }))
17448        }
17449        "dgram.bind" => {
17450            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.bind socket id")?;
17451            let payload = request
17452                .args
17453                .get(1)
17454                .cloned()
17455                .ok_or_else(|| {
17456                    SidecarError::InvalidState(String::from(
17457                        "dgram.bind requires a request payload",
17458                    ))
17459                })
17460                .and_then(|value| {
17461                    serde_json::from_value::<JavascriptDgramBindRequest>(value).map_err(|error| {
17462                        SidecarError::InvalidState(format!("invalid dgram.bind payload: {error}"))
17463                    })
17464                })?;
17465            let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
17466                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17467            })?;
17468            let local_addr = socket.bind(
17469                kernel,
17470                process.kernel_pid,
17471                payload.address.as_deref(),
17472                payload.port,
17473                socket_paths,
17474            )?;
17475            Ok(json!({
17476                "localAddress": local_addr.ip().to_string(),
17477                "localPort": local_addr.port(),
17478                "family": socket_addr_family(&local_addr),
17479            }))
17480        }
17481        "dgram.send" => {
17482            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.send socket id")?;
17483            let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "dgram.send payload")?;
17484            let payload = request
17485                .args
17486                .get(2)
17487                .cloned()
17488                .ok_or_else(|| {
17489                    SidecarError::InvalidState(String::from(
17490                        "dgram.send requires a request payload",
17491                    ))
17492                })
17493                .and_then(|value| {
17494                    serde_json::from_value::<JavascriptDgramSendRequest>(value).map_err(|error| {
17495                        SidecarError::InvalidState(format!("invalid dgram.send payload: {error}"))
17496                    })
17497                })?;
17498            let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
17499                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17500            })?;
17501            let (written, local_addr) = socket.send_to(ActiveUdpSendToRequest {
17502                bridge,
17503                kernel,
17504                kernel_pid: process.kernel_pid,
17505                vm_id,
17506                dns,
17507                host: payload.address.as_deref().unwrap_or("localhost"),
17508                port: payload.port,
17509                context: socket_paths,
17510                contents: &chunk,
17511            })?;
17512            Ok(json!({
17513                "bytes": written,
17514                "localAddress": local_addr.ip().to_string(),
17515                "localPort": local_addr.port(),
17516                "family": socket_addr_family(&local_addr),
17517            }))
17518        }
17519        "dgram.poll" => {
17520            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.poll socket id")?;
17521            let wait_ms =
17522                javascript_sync_rpc_arg_u64_optional(&request.args, 1, "dgram.poll wait ms")?
17523                    .unwrap_or_default();
17524            let event = {
17525                let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
17526                    SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17527                })?;
17528                socket.poll(kernel, process.kernel_pid, Duration::from_millis(wait_ms))?
17529            };
17530
17531            match event {
17532                Some(JavascriptUdpSocketEvent::Message { data, remote_addr }) => {
17533                    let family = JavascriptSocketFamily::from_ip(remote_addr.ip());
17534                    let guest_remote_port = if is_loopback_ip(remote_addr.ip()) {
17535                        socket_paths
17536                            .guest_udp_port_for_host_port(family, remote_addr.port())
17537                            .unwrap_or(remote_addr.port())
17538                    } else {
17539                        remote_addr.port()
17540                    };
17541                    Ok(json!({
17542                    "type": "message",
17543                    "data": javascript_sync_rpc_bytes_value(&data),
17544                    "remoteAddress": remote_addr.ip().to_string(),
17545                    "remotePort": guest_remote_port,
17546                    "remoteFamily": socket_addr_family(&remote_addr),
17547                    }))
17548                }
17549                Some(JavascriptUdpSocketEvent::Error { code, message }) => Ok(json!({
17550                    "type": "error",
17551                    "code": code,
17552                    "message": message,
17553                })),
17554                None => Ok(Value::Null),
17555            }
17556        }
17557        "dgram.close" => {
17558            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.close socket id")?;
17559            let mut socket = process.udp_sockets.remove(socket_id).ok_or_else(|| {
17560                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17561            })?;
17562            socket.close(kernel, process.kernel_pid);
17563            Ok(Value::Null)
17564        }
17565        "dgram.address" => {
17566            let socket_id =
17567                javascript_sync_rpc_arg_str(&request.args, 0, "dgram.address socket id")?;
17568            let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
17569                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17570            })?;
17571            let local_addr = socket.local_addr().ok_or_else(|| {
17572                SidecarError::Execution(String::from("EBADF: bad file descriptor"))
17573            })?;
17574            javascript_net_json_string(
17575                json!({
17576                    "address": local_addr.ip().to_string(),
17577                    "port": local_addr.port(),
17578                    "family": socket_addr_family(&local_addr),
17579                }),
17580                "dgram.address",
17581            )
17582        }
17583        "dgram.setBufferSize" => {
17584            let socket_id =
17585                javascript_sync_rpc_arg_str(&request.args, 0, "dgram.setBufferSize socket id")?;
17586            let which =
17587                javascript_sync_rpc_arg_str(&request.args, 1, "dgram.setBufferSize buffer kind")?;
17588            let size = javascript_sync_rpc_arg_u64(&request.args, 2, "dgram.setBufferSize size")?;
17589            let size = usize::try_from(size).map_err(|_| {
17590                SidecarError::InvalidState(String::from(
17591                    "dgram.setBufferSize size must fit within usize",
17592                ))
17593            })?;
17594            let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
17595                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17596            })?;
17597            socket.set_buffer_size(which, size)?;
17598            Ok(Value::Null)
17599        }
17600        "dgram.getBufferSize" => {
17601            let socket_id =
17602                javascript_sync_rpc_arg_str(&request.args, 0, "dgram.getBufferSize socket id")?;
17603            let which =
17604                javascript_sync_rpc_arg_str(&request.args, 1, "dgram.getBufferSize buffer kind")?;
17605            let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
17606                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17607            })?;
17608            let size = socket.get_buffer_size(which)?;
17609            Ok(json!(size))
17610        }
17611        other => Err(SidecarError::InvalidState(format!(
17612            "unsupported JavaScript dgram sync RPC method {other}"
17613        ))),
17614    }
17615}
17616
17617#[derive(Debug)]
17618struct ClientHttp2StreamState {
17619    send_stream: Option<h2::SendStream<Bytes>>,
17620}
17621
17622#[derive(Debug)]
17623struct ServerHttp2StreamState {
17624    send_response: Option<ServerHttp2Responder>,
17625    send_stream: Option<h2::SendStream<Bytes>>,
17626}
17627
17628#[derive(Debug)]
17629enum ServerHttp2Responder {
17630    Regular(server::SendResponse<Bytes>),
17631    Pushed(server::SendPushedResponse<Bytes>),
17632}
17633
17634const HTTP2_DEFAULT_WINDOW_SIZE: u32 = 65_535;
17635const HTTP2_POLL_DELAY: Duration = Duration::from_millis(10);
17636
17637fn http2_runtime_snapshot() -> Http2RuntimeSnapshot {
17638    Http2RuntimeSnapshot {
17639        effective_local_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
17640        local_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
17641        remote_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
17642        next_stream_id: 1,
17643        outbound_queue_size: 1,
17644        deflate_dynamic_table_size: 0,
17645        inflate_dynamic_table_size: 0,
17646    }
17647}
17648
17649fn http2_snapshot_json(snapshot: &Http2SessionSnapshot) -> Result<String, SidecarError> {
17650    serde_json::to_string(snapshot)
17651        .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
17652}
17653
17654fn http2_event_value(event: &Http2BridgeEvent) -> Result<Value, SidecarError> {
17655    serde_json::to_string(event)
17656        .map(Value::String)
17657        .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
17658}
17659
17660fn push_http2_server_event(
17661    shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17662    server_id: u64,
17663    event: Http2BridgeEvent,
17664) {
17665    if let Ok(mut state) = shared.lock() {
17666        state
17667            .server_events
17668            .entry(server_id)
17669            .or_default()
17670            .push_back(event);
17671    }
17672}
17673
17674fn push_http2_session_event(
17675    shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17676    session_id: u64,
17677    event: Http2BridgeEvent,
17678) {
17679    if let Ok(mut state) = shared.lock() {
17680        state
17681            .session_events
17682            .entry(session_id)
17683            .or_default()
17684            .push_back(event);
17685    }
17686}
17687
17688fn pop_http2_event(
17689    queue: &mut BTreeMap<u64, VecDeque<Http2BridgeEvent>>,
17690    id: u64,
17691) -> Option<Http2BridgeEvent> {
17692    queue.get_mut(&id).and_then(VecDeque::pop_front)
17693}
17694
17695fn wait_for_http2_event(
17696    shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17697    id: u64,
17698    is_server: bool,
17699    wait_ms: u64,
17700) -> Option<Http2BridgeEvent> {
17701    let deadline = Instant::now() + Duration::from_millis(wait_ms);
17702    loop {
17703        if let Ok(mut state) = shared.lock() {
17704            let queue = if is_server {
17705                &mut state.server_events
17706            } else {
17707                &mut state.session_events
17708            };
17709            if let Some(event) = pop_http2_event(queue, id) {
17710                return Some(event);
17711            }
17712        }
17713        if wait_ms == 0 || Instant::now() >= deadline {
17714            return None;
17715        }
17716        thread::sleep(HTTP2_POLL_DELAY);
17717    }
17718}
17719
17720fn next_http2_session_id(shared: &mut crate::state::Http2SharedState) -> u64 {
17721    shared.next_session_id += 1;
17722    shared.next_session_id
17723}
17724
17725fn next_http2_stream_id(shared: &mut crate::state::Http2SharedState) -> u64 {
17726    shared.next_stream_id += 1;
17727    shared.next_stream_id
17728}
17729
17730fn http2_reason(code: Option<u32>) -> Reason {
17731    code.unwrap_or(Reason::NO_ERROR.into()).into()
17732}
17733
17734fn http2_error_payload(message: impl Into<String>) -> String {
17735    serde_json::to_string(&json!({
17736        "name": "Error",
17737        "code": "ERR_HTTP2_ERROR",
17738        "message": message.into(),
17739    }))
17740    .unwrap_or_else(|_| {
17741        String::from(
17742            "{\"name\":\"Error\",\"code\":\"ERR_HTTP2_ERROR\",\"message\":\"HTTP/2 bridge error\"}",
17743        )
17744    })
17745}
17746
17747fn http2_socket_snapshot(local_addr: SocketAddr, remote_addr: SocketAddr) -> Http2SocketSnapshot {
17748    Http2SocketSnapshot {
17749        encrypted: false,
17750        allow_half_open: false,
17751        local_address: Some(local_addr.ip().to_string()),
17752        local_port: Some(local_addr.port()),
17753        local_family: Some(socket_addr_family(&local_addr).to_string()),
17754        remote_address: Some(remote_addr.ip().to_string()),
17755        remote_port: Some(remote_addr.port()),
17756        remote_family: Some(socket_addr_family(&remote_addr).to_string()),
17757        servername: None,
17758        alpn_protocol: Some(String::from("h2c")),
17759    }
17760}
17761
17762fn http2_wait_result(kind: &str, id: u64) -> Value {
17763    json!({
17764        "kind": kind,
17765        "id": id,
17766    })
17767}
17768
17769fn is_http2_terminal_event(event: &Http2BridgeEvent, is_server: bool, id: u64) -> bool {
17770    if is_server {
17771        event.kind == "serverClose" && event.id == id
17772    } else {
17773        event.kind == "sessionClose" && event.id == id
17774    }
17775}
17776
17777fn dispatch_http2_wait_loop(
17778    process: &ActiveProcess,
17779    id: u64,
17780    is_server: bool,
17781) -> Result<Value, SidecarError> {
17782    loop {
17783        if let Some(event) = wait_for_http2_event(&process.http2.shared, id, is_server, 50) {
17784            let payload = serde_json::to_value(&event).map_err(|error| {
17785                SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}"))
17786            })?;
17787            process
17788                .execution
17789                .send_javascript_stream_event("http2", payload.clone())?;
17790            if is_http2_terminal_event(&event, is_server, id) {
17791                return Ok(payload);
17792            }
17793            continue;
17794        }
17795
17796        let exists = process
17797            .http2
17798            .shared
17799            .lock()
17800            .map(|state| {
17801                if is_server {
17802                    state.servers.contains_key(&id)
17803                } else {
17804                    state.sessions.contains_key(&id)
17805                }
17806            })
17807            .unwrap_or(false);
17808        if !exists {
17809            return Ok(if is_server {
17810                http2_wait_result("serverClose", id)
17811            } else {
17812                http2_wait_result("sessionClose", id)
17813            });
17814        }
17815    }
17816}
17817
17818fn dispatch_http_wait_loop(process: &ActiveProcess, server_id: u64) -> Result<Value, SidecarError> {
17819    loop {
17820        if !process.http_servers.contains_key(&server_id) {
17821            return Ok(json!({
17822                "kind": "serverClose",
17823                "id": server_id,
17824            }));
17825        }
17826        thread::sleep(Duration::from_millis(25));
17827    }
17828}
17829
17830fn http2_settings_from_value(settings: &BTreeMap<String, Value>) -> BTreeMap<String, Value> {
17831    settings.clone()
17832}
17833
17834fn parse_http2_headers_json(
17835    headers_json: &str,
17836    label: &str,
17837) -> Result<BTreeMap<String, Value>, SidecarError> {
17838    serde_json::from_str::<BTreeMap<String, Value>>(headers_json)
17839        .map_err(|error| SidecarError::InvalidState(format!("{label} must be valid JSON: {error}")))
17840}
17841
17842fn apply_http2_header_values(
17843    header_map: &mut HeaderMap,
17844    name: &str,
17845    value: &Value,
17846) -> Result<(), SidecarError> {
17847    let header_name = HeaderName::from_bytes(name.as_bytes()).map_err(|error| {
17848        SidecarError::InvalidState(format!("invalid HTTP/2 header name {name:?}: {error}"))
17849    })?;
17850    match value {
17851        Value::Array(values) => {
17852            for value in values {
17853                apply_http2_header_values(header_map, name, value)?;
17854            }
17855        }
17856        Value::String(text) => {
17857            let value = HeaderValue::from_str(text).map_err(|error| {
17858                SidecarError::InvalidState(format!(
17859                    "invalid HTTP/2 header value for {name}: {error}"
17860                ))
17861            })?;
17862            header_map.append(header_name.clone(), value);
17863        }
17864        Value::Number(number) => {
17865            let value = HeaderValue::from_str(&number.to_string()).map_err(|error| {
17866                SidecarError::InvalidState(format!(
17867                    "invalid HTTP/2 numeric header value for {name}: {error}"
17868                ))
17869            })?;
17870            header_map.append(header_name.clone(), value);
17871        }
17872        Value::Bool(boolean) => {
17873            let value = HeaderValue::from_str(if *boolean { "true" } else { "false" }).map_err(
17874                |error| {
17875                    SidecarError::InvalidState(format!(
17876                        "invalid HTTP/2 boolean header value for {name}: {error}"
17877                    ))
17878                },
17879            )?;
17880            header_map.append(header_name.clone(), value);
17881        }
17882        Value::Null => {}
17883        Value::Object(_) => {
17884            return Err(SidecarError::InvalidState(format!(
17885                "unsupported HTTP/2 header object value for {name}"
17886            )));
17887        }
17888    }
17889    Ok(())
17890}
17891
17892fn build_http2_request(headers_json: &str) -> Result<Request<()>, SidecarError> {
17893    let headers = parse_http2_headers_json(headers_json, "HTTP/2 request headers")?;
17894    let method = headers
17895        .get(":method")
17896        .and_then(Value::as_str)
17897        .unwrap_or("GET");
17898    let path = headers.get(":path").and_then(Value::as_str).unwrap_or("/");
17899    let mut builder = Request::builder()
17900        .method(Method::from_bytes(method.as_bytes()).map_err(|error| {
17901            SidecarError::InvalidState(format!("invalid HTTP/2 method {method:?}: {error}"))
17902        })?)
17903        .uri(path.parse::<Uri>().map_err(|error| {
17904            SidecarError::InvalidState(format!("invalid HTTP/2 path {path:?}: {error}"))
17905        })?);
17906    {
17907        let header_map = builder.headers_mut().expect("request header map");
17908        for (name, value) in &headers {
17909            if name.starts_with(':') {
17910                continue;
17911            }
17912            apply_http2_header_values(header_map, name, value)?;
17913        }
17914    }
17915    builder
17916        .body(())
17917        .map_err(|error| SidecarError::InvalidState(format!("invalid HTTP/2 request: {error}")))
17918}
17919
17920fn build_http2_response(headers_json: &str) -> Result<Response<()>, SidecarError> {
17921    let headers = parse_http2_headers_json(headers_json, "HTTP/2 response headers")?;
17922    let status = headers
17923        .get(":status")
17924        .and_then(Value::as_u64)
17925        .or_else(|| {
17926            headers
17927                .get(":status")
17928                .and_then(Value::as_str)
17929                .and_then(|value| value.parse::<u16>().ok().map(u64::from))
17930        })
17931        .unwrap_or(200);
17932    let mut builder = Response::builder().status(status as u16);
17933    {
17934        let header_map = builder.headers_mut().expect("response header map");
17935        for (name, value) in &headers {
17936            if name.starts_with(':') {
17937                continue;
17938            }
17939            apply_http2_header_values(header_map, name, value)?;
17940        }
17941    }
17942    builder.body(()).map_err(|error| {
17943        SidecarError::InvalidState(format!("invalid HTTP/2 response headers: {error}"))
17944    })
17945}
17946
17947fn serialize_http2_headers_map(
17948    pseudo: BTreeMap<String, Value>,
17949    headers: &HeaderMap,
17950) -> Result<String, SidecarError> {
17951    let mut serialized = pseudo;
17952    for (name, value) in headers {
17953        let name = name.as_str().to_string();
17954        let value = Value::String(
17955            value
17956                .to_str()
17957                .map_err(|error| {
17958                    SidecarError::Execution(format!("invalid HTTP/2 header value: {error}"))
17959                })?
17960                .to_owned(),
17961        );
17962        match serialized.get_mut(&name) {
17963            Some(Value::Array(values)) => values.push(value),
17964            Some(existing) => {
17965                let first = existing.clone();
17966                *existing = Value::Array(vec![first, value]);
17967            }
17968            None => {
17969                serialized.insert(name, value);
17970            }
17971        }
17972    }
17973    serde_json::to_string(&serialized)
17974        .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
17975}
17976
17977fn serialize_http2_request_headers(
17978    request: &Request<h2::RecvStream>,
17979) -> Result<String, SidecarError> {
17980    let mut pseudo = BTreeMap::new();
17981    pseudo.insert(
17982        String::from(":method"),
17983        Value::String(request.method().as_str().to_string()),
17984    );
17985    pseudo.insert(
17986        String::from(":path"),
17987        Value::String(
17988            request
17989                .uri()
17990                .path_and_query()
17991                .map(|value| value.as_str().to_string())
17992                .unwrap_or_else(|| String::from("/")),
17993        ),
17994    );
17995    serialize_http2_headers_map(pseudo, request.headers())
17996}
17997
17998fn serialize_http2_response_headers(
17999    response: &Response<h2::RecvStream>,
18000) -> Result<String, SidecarError> {
18001    let mut pseudo = BTreeMap::new();
18002    pseudo.insert(
18003        String::from(":status"),
18004        Value::Number(serde_json::Number::from(response.status().as_u16())),
18005    );
18006    serialize_http2_headers_map(pseudo, response.headers())
18007}
18008
18009fn remove_http2_session_resources(
18010    shared: &Arc<Mutex<crate::state::Http2SharedState>>,
18011    session_id: u64,
18012) {
18013    if let Ok(mut state) = shared.lock() {
18014        state.sessions.remove(&session_id);
18015        state.session_events.remove(&session_id);
18016        let stream_ids = state
18017            .streams
18018            .iter()
18019            .filter_map(|(stream_id, stream)| {
18020                (stream.session_id == session_id).then_some(*stream_id)
18021            })
18022            .collect::<Vec<_>>();
18023        for stream_id in stream_ids {
18024            state.streams.remove(&stream_id);
18025        }
18026    }
18027}
18028
18029fn spawn_http2_client_session(
18030    shared: Arc<Mutex<crate::state::Http2SharedState>>,
18031    session_id: u64,
18032    remote_addr: SocketAddr,
18033    tls: Option<JavascriptTlsBridgeOptions>,
18034    snapshot: Arc<Mutex<Http2SessionSnapshot>>,
18035    mut command_rx: UnboundedReceiver<Http2SessionCommand>,
18036) {
18037    thread::spawn(move || {
18038        let runtime = match TokioRuntimeBuilder::new_current_thread()
18039            .enable_all()
18040            .build()
18041        {
18042            Ok(runtime) => runtime,
18043            Err(error) => {
18044                push_http2_session_event(
18045                    &shared,
18046                    session_id,
18047                    Http2BridgeEvent {
18048                        kind: String::from("sessionError"),
18049                        id: session_id,
18050                        data: Some(http2_error_payload(error.to_string())),
18051                        ..Http2BridgeEvent::default()
18052                    },
18053                );
18054                remove_http2_session_resources(&shared, session_id);
18055                return;
18056            }
18057        };
18058
18059        runtime.block_on(async move {
18060            let stream = match tokio::net::TcpStream::connect(remote_addr).await {
18061                Ok(stream) => stream,
18062                Err(error) => {
18063                    push_http2_session_event(
18064                        &shared,
18065                        session_id,
18066                        Http2BridgeEvent {
18067                            kind: String::from("sessionError"),
18068                            id: session_id,
18069                            data: Some(http2_error_payload(error.to_string())),
18070                            ..Http2BridgeEvent::default()
18071                        },
18072                    );
18073                    remove_http2_session_resources(&shared, session_id);
18074                    return;
18075                }
18076            };
18077
18078            let local_addr = match stream.local_addr() {
18079                Ok(addr) => addr,
18080                Err(error) => {
18081                    push_http2_session_event(
18082                        &shared,
18083                        session_id,
18084                        Http2BridgeEvent {
18085                            kind: String::from("sessionError"),
18086                            id: session_id,
18087                            data: Some(http2_error_payload(error.to_string())),
18088                            ..Http2BridgeEvent::default()
18089                        },
18090                    );
18091                    remove_http2_session_resources(&shared, session_id);
18092                    return;
18093                }
18094            };
18095
18096            {
18097                let mut snapshot_guard = snapshot.lock().expect("http2 snapshot lock");
18098                snapshot_guard.socket = http2_socket_snapshot(local_addr, remote_addr);
18099                if let Some(options) = tls.as_ref() {
18100                    snapshot_guard.encrypted = true;
18101                    snapshot_guard.alpn_protocol = Some(String::from("h2"));
18102                    snapshot_guard.socket.encrypted = true;
18103                    snapshot_guard.socket.servername = options.servername.clone();
18104                    snapshot_guard.socket.alpn_protocol = Some(String::from("h2"));
18105                }
18106                snapshot_guard.state = http2_runtime_snapshot();
18107            }
18108            if let Ok(snapshot_json) =
18109                http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())
18110            {
18111                push_http2_session_event(
18112                    &shared,
18113                    session_id,
18114                    Http2BridgeEvent {
18115                        kind: String::from("sessionConnect"),
18116                        id: session_id,
18117                        data: Some(snapshot_json),
18118                        ..Http2BridgeEvent::default()
18119                    },
18120                );
18121            }
18122
18123            let io: Pin<Box<dyn Http2AsyncIo>> = if let Some(options) = tls.as_ref() {
18124                let server_name = match ServerName::try_from(
18125                    options
18126                        .servername
18127                        .clone()
18128                        .unwrap_or_else(|| String::from("localhost")),
18129                ) {
18130                    Ok(server_name) => server_name,
18131                    Err(_) => {
18132                        push_http2_session_event(
18133                            &shared,
18134                            session_id,
18135                            Http2BridgeEvent {
18136                                kind: String::from("sessionError"),
18137                                id: session_id,
18138                                data: Some(http2_error_payload("invalid TLS servername")),
18139                                ..Http2BridgeEvent::default()
18140                            },
18141                        );
18142                        remove_http2_session_resources(&shared, session_id);
18143                        return;
18144                    }
18145                };
18146                let connector = match build_client_tls_config(options) {
18147                    Ok(config) => TlsConnector::from(Arc::new(config)),
18148                    Err(error) => {
18149                        push_http2_session_event(
18150                            &shared,
18151                            session_id,
18152                            Http2BridgeEvent {
18153                                kind: String::from("sessionError"),
18154                                id: session_id,
18155                                data: Some(http2_error_payload(error.to_string())),
18156                                ..Http2BridgeEvent::default()
18157                            },
18158                        );
18159                        remove_http2_session_resources(&shared, session_id);
18160                        return;
18161                    }
18162                };
18163                match connector.connect(server_name, stream).await {
18164                    Ok(tls_stream) => Box::pin(tls_stream),
18165                    Err(error) => {
18166                        push_http2_session_event(
18167                            &shared,
18168                            session_id,
18169                            Http2BridgeEvent {
18170                                kind: String::from("sessionError"),
18171                                id: session_id,
18172                                data: Some(http2_error_payload(error.to_string())),
18173                                ..Http2BridgeEvent::default()
18174                            },
18175                        );
18176                        remove_http2_session_resources(&shared, session_id);
18177                        return;
18178                    }
18179                }
18180            } else {
18181                Box::pin(stream)
18182            };
18183
18184            let (mut sender, connection) = match client::handshake(io).await {
18185                Ok(parts) => parts,
18186                Err(error) => {
18187                    push_http2_session_event(
18188                        &shared,
18189                        session_id,
18190                        Http2BridgeEvent {
18191                            kind: String::from("sessionError"),
18192                            id: session_id,
18193                            data: Some(http2_error_payload(error.to_string())),
18194                            ..Http2BridgeEvent::default()
18195                        },
18196                    );
18197                    remove_http2_session_resources(&shared, session_id);
18198                    return;
18199                }
18200            };
18201
18202            let (status_tx, mut status_rx) = unbounded_channel::<Result<(), String>>();
18203            tokio::spawn(async move {
18204                let _ = status_tx.send(connection.await.map_err(|error| error.to_string()));
18205            });
18206
18207            let streams: Arc<Mutex<BTreeMap<u64, ClientHttp2StreamState>>> =
18208                Arc::new(Mutex::new(BTreeMap::new()));
18209
18210            loop {
18211                tokio::select! {
18212                    Some(result) = status_rx.recv() => {
18213                        if let Err(message) = result {
18214                            push_http2_session_event(
18215                                &shared,
18216                                session_id,
18217                                Http2BridgeEvent {
18218                                    kind: String::from("sessionError"),
18219                                    id: session_id,
18220                                    data: Some(http2_error_payload(message)),
18221                                    ..Http2BridgeEvent::default()
18222                                },
18223                            );
18224                        }
18225                        push_http2_session_event(
18226                            &shared,
18227                            session_id,
18228                            Http2BridgeEvent {
18229                                kind: String::from("sessionClose"),
18230                                id: session_id,
18231                                ..Http2BridgeEvent::default()
18232                            },
18233                        );
18234                        remove_http2_session_resources(&shared, session_id);
18235                        break;
18236                    }
18237                    Some(command) = command_rx.recv() => {
18238                        match command {
18239                            Http2SessionCommand::Request { headers_json, options_json, respond_to } => {
18240                                let request = match build_http2_request(&headers_json) {
18241                                    Ok(request) => request,
18242                                    Err(error) => {
18243                                        let _ = respond_to.send(Err(error.to_string()));
18244                                        continue;
18245                                    }
18246                                };
18247                                let options: JavascriptHttp2RequestOptions =
18248                                    serde_json::from_str(&options_json).unwrap_or_default();
18249                                let stream_id = {
18250                                    let mut state = shared.lock().expect("http2 shared state");
18251                                    let stream_id = next_http2_stream_id(&mut state);
18252                                    state.streams.insert(
18253                                        stream_id,
18254                                        ActiveHttp2Stream {
18255                                            session_id,
18256                                            paused: Arc::new(AtomicBool::new(false)),
18257                                        },
18258                                    );
18259                                    stream_id
18260                                };
18261                                match sender.send_request(request, options.end_stream) {
18262                                    Ok((response_future, send_stream)) => {
18263                                        if !options.end_stream {
18264                                            streams
18265                                                .lock()
18266                                                .expect("http2 client streams")
18267                                                .insert(stream_id, ClientHttp2StreamState { send_stream: Some(send_stream) });
18268                                        }
18269                                        let shared_clone = Arc::clone(&shared);
18270                                        let snapshot_clone = Arc::clone(&snapshot);
18271                                        tokio::spawn(async move {
18272                                            match response_future.await {
18273                                                Ok(response) => {
18274                                                    if let Ok(headers_json) = serialize_http2_response_headers(&response) {
18275                                                        push_http2_session_event(
18276                                                            &shared_clone,
18277                                                            session_id,
18278                                                            Http2BridgeEvent {
18279                                                                kind: String::from("clientResponseHeaders"),
18280                                                                id: stream_id,
18281                                                                data: Some(headers_json),
18282                                                                ..Http2BridgeEvent::default()
18283                                                            },
18284                                                        );
18285                                                    }
18286                                                    let mut body = response.into_body();
18287                                                    while let Some(chunk) = body.data().await {
18288                                                        match chunk {
18289                                                            Ok(bytes) => {
18290                                                                let paused = {
18291                                                                    let state = shared_clone.lock().expect("http2 shared state");
18292                                                                    state.streams.get(&stream_id).map(|stream| Arc::clone(&stream.paused))
18293                                                                };
18294                                                                if let Some(paused) = paused {
18295                                                                    while paused.load(Ordering::SeqCst) {
18296                                                                        tokio::time::sleep(HTTP2_POLL_DELAY).await;
18297                                                                    }
18298                                                                }
18299                                                                let _ = body.flow_control().release_capacity(bytes.len());
18300                                                                push_http2_session_event(
18301                                                                    &shared_clone,
18302                                                                    session_id,
18303                                                                    Http2BridgeEvent {
18304                                                                        kind: String::from("clientData"),
18305                                                                        id: stream_id,
18306                                                                        data: Some(base64::engine::general_purpose::STANDARD.encode(bytes)),
18307                                                                        ..Http2BridgeEvent::default()
18308                                                                    },
18309                                                                );
18310                                                            }
18311                                                            Err(error) => {
18312                                                                push_http2_session_event(
18313                                                                    &shared_clone,
18314                                                                    session_id,
18315                                                                    Http2BridgeEvent {
18316                                                                        kind: String::from("clientError"),
18317                                                                        id: stream_id,
18318                                                                        data: Some(http2_error_payload(error.to_string())),
18319                                                                        ..Http2BridgeEvent::default()
18320                                                                    },
18321                                                                );
18322                                                                break;
18323                                                            }
18324                                                        }
18325                                                    }
18326                                                    {
18327                                                        let mut snapshot = snapshot_clone.lock().expect("http2 snapshot lock");
18328                                                        snapshot.state.next_stream_id =
18329                                                            snapshot.state.next_stream_id.saturating_add(2);
18330                                                    }
18331                                                    push_http2_session_event(
18332                                                        &shared_clone,
18333                                                        session_id,
18334                                                        Http2BridgeEvent {
18335                                                            kind: String::from("clientEnd"),
18336                                                            id: stream_id,
18337                                                            ..Http2BridgeEvent::default()
18338                                                        },
18339                                                    );
18340                                                    push_http2_session_event(
18341                                                        &shared_clone,
18342                                                        session_id,
18343                                                        Http2BridgeEvent {
18344                                                            kind: String::from("clientClose"),
18345                                                            id: stream_id,
18346                                                            extra_number: Some(0),
18347                                                            ..Http2BridgeEvent::default()
18348                                                        },
18349                                                    );
18350                                                    if let Ok(mut state) = shared_clone.lock() {
18351                                                        state.streams.remove(&stream_id);
18352                                                    }
18353                                                }
18354                                                Err(error) => {
18355                                                    push_http2_session_event(
18356                                                        &shared_clone,
18357                                                        session_id,
18358                                                        Http2BridgeEvent {
18359                                                            kind: String::from("clientError"),
18360                                                            id: stream_id,
18361                                                            data: Some(http2_error_payload(error.to_string())),
18362                                                            ..Http2BridgeEvent::default()
18363                                                        },
18364                                                    );
18365                                                    push_http2_session_event(
18366                                                        &shared_clone,
18367                                                        session_id,
18368                                                        Http2BridgeEvent {
18369                                                            kind: String::from("clientClose"),
18370                                                            id: stream_id,
18371                                                            extra_number: Some(u32::from(Reason::INTERNAL_ERROR) as u64),
18372                                                            ..Http2BridgeEvent::default()
18373                                                        },
18374                                                    );
18375                                                    if let Ok(mut state) = shared_clone.lock() {
18376                                                        state.streams.remove(&stream_id);
18377                                                    }
18378                                                }
18379                                            }
18380                                        });
18381                                        let _ = respond_to.send(Ok(json!(stream_id)));
18382                                    }
18383                                    Err(error) => {
18384                                        if let Ok(mut state) = shared.lock() {
18385                                            state.streams.remove(&stream_id);
18386                                        }
18387                                        let _ = respond_to.send(Err(error.to_string()));
18388                                    }
18389                                }
18390                            }
18391                            Http2SessionCommand::Settings { settings_json, respond_to } => {
18392                                let settings = serde_json::from_str::<BTreeMap<String, Value>>(&settings_json)
18393                                    .unwrap_or_default();
18394                                {
18395                                    let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18396                                    snapshot.local_settings = http2_settings_from_value(&settings);
18397                                }
18398                                if let Ok(headers_json) = serde_json::to_string(&settings) {
18399                                    push_http2_session_event(
18400                                        &shared,
18401                                        session_id,
18402                                        Http2BridgeEvent {
18403                                            kind: String::from("sessionLocalSettings"),
18404                                            id: session_id,
18405                                            data: Some(headers_json.clone()),
18406                                            ..Http2BridgeEvent::default()
18407                                        },
18408                                    );
18409                                    push_http2_session_event(
18410                                        &shared,
18411                                        session_id,
18412                                        Http2BridgeEvent {
18413                                            kind: String::from("sessionSettingsAck"),
18414                                            id: session_id,
18415                                            ..Http2BridgeEvent::default()
18416                                        },
18417                                    );
18418                                }
18419                                let _ = respond_to.send(Ok(Value::Null));
18420                            }
18421                            Http2SessionCommand::SetLocalWindowSize { size, respond_to } => {
18422                                {
18423                                    let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18424                                    snapshot.state.local_window_size = size;
18425                                    snapshot.state.effective_local_window_size = size;
18426                                }
18427                                let value = snapshot
18428                                    .lock()
18429                                    .ok()
18430                                    .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok())
18431                                    .map(Value::String)
18432                                    .unwrap_or(Value::Null);
18433                                let _ = respond_to.send(Ok(value));
18434                            }
18435                            Http2SessionCommand::Goaway { error_code, last_stream_id, opaque_data, respond_to } => {
18436                                push_http2_session_event(
18437                                    &shared,
18438                                    session_id,
18439                                    Http2BridgeEvent {
18440                                        kind: String::from("sessionGoaway"),
18441                                        id: session_id,
18442                                        data: opaque_data.map(|value| {
18443                                            base64::engine::general_purpose::STANDARD.encode(value)
18444                                        }),
18445                                        extra_number: Some(error_code as u64),
18446                                        flags: Some(last_stream_id as u64),
18447                                        ..Http2BridgeEvent::default()
18448                                    },
18449                                );
18450                                let _ = respond_to.send(Ok(Value::Null));
18451                            }
18452                            Http2SessionCommand::Close { respond_to, .. } => {
18453                                let _ = respond_to.send(Ok(Value::Null));
18454                                push_http2_session_event(
18455                                    &shared,
18456                                    session_id,
18457                                    Http2BridgeEvent {
18458                                        kind: String::from("sessionClose"),
18459                                        id: session_id,
18460                                        ..Http2BridgeEvent::default()
18461                                    },
18462                                );
18463                                remove_http2_session_resources(&shared, session_id);
18464                                break;
18465                            }
18466                            Http2SessionCommand::StreamWrite { stream_id, chunk, end_stream, respond_to } => {
18467                                let result = streams
18468                                    .lock()
18469                                    .expect("http2 client streams")
18470                                    .get_mut(&stream_id)
18471                                    .and_then(|stream| stream.send_stream.as_mut())
18472                                    .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 client stream {stream_id}")))
18473                                    .and_then(|stream| stream.send_data(Bytes::from(chunk), end_stream).map_err(|error| SidecarError::Execution(error.to_string())));
18474                                match result {
18475                                    Ok(()) => {
18476                                        if end_stream {
18477                                            streams.lock().expect("http2 client streams").remove(&stream_id);
18478                                        }
18479                                        let _ = respond_to.send(Ok(Value::Bool(true)));
18480                                    }
18481                                    Err(error) => {
18482                                        let _ = respond_to.send(Err(error.to_string()));
18483                                    }
18484                                }
18485                            }
18486                            Http2SessionCommand::StreamClose { stream_id, error_code, respond_to } => {
18487                                let mut streams = streams.lock().expect("http2 client streams");
18488                                let Some(mut state) = streams.remove(&stream_id) else {
18489                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 client stream {stream_id}")));
18490                                    continue;
18491                                };
18492                                if let Some(stream) = state.send_stream.as_mut() {
18493                                    stream.send_reset(http2_reason(error_code));
18494                                }
18495                                if let Ok(mut state) = shared.lock() {
18496                                    state.streams.remove(&stream_id);
18497                                }
18498                                push_http2_session_event(
18499                                    &shared,
18500                                    session_id,
18501                                    Http2BridgeEvent {
18502                                        kind: String::from("clientClose"),
18503                                        id: stream_id,
18504                                        extra_number: Some(u32::from(http2_reason(error_code)) as u64),
18505                                        ..Http2BridgeEvent::default()
18506                                    },
18507                                );
18508                                let _ = respond_to.send(Ok(Value::Null));
18509                            }
18510                            Http2SessionCommand::StreamRespond { respond_to, .. }
18511                            | Http2SessionCommand::StreamPush { respond_to, .. }
18512                            | Http2SessionCommand::StreamRespondWithFile { respond_to, .. } => {
18513                                let _ = respond_to.send(Err(String::from("HTTP/2 client streams cannot send server responses")));
18514                            }
18515                        }
18516                    }
18517                    else => break,
18518                }
18519            }
18520        });
18521    });
18522}
18523
18524fn spawn_http2_server_session(
18525    shared: Arc<Mutex<crate::state::Http2SharedState>>,
18526    server_id: u64,
18527    session_id: u64,
18528    stream: TcpStream,
18529    tls: Option<JavascriptTlsBridgeOptions>,
18530    snapshot: Arc<Mutex<Http2SessionSnapshot>>,
18531    mut command_rx: UnboundedReceiver<Http2SessionCommand>,
18532) {
18533    thread::spawn(move || {
18534        let runtime = match TokioRuntimeBuilder::new_current_thread()
18535            .enable_all()
18536            .build()
18537        {
18538            Ok(runtime) => runtime,
18539            Err(error) => {
18540                push_http2_server_event(
18541                    &shared,
18542                    server_id,
18543                    Http2BridgeEvent {
18544                        kind: String::from("serverStreamError"),
18545                        id: session_id,
18546                        data: Some(http2_error_payload(error.to_string())),
18547                        ..Http2BridgeEvent::default()
18548                    },
18549                );
18550                remove_http2_session_resources(&shared, session_id);
18551                return;
18552            }
18553        };
18554
18555        runtime.block_on(async move {
18556            if let Err(error) = stream.set_nonblocking(true) {
18557                push_http2_server_event(
18558                    &shared,
18559                    server_id,
18560                    Http2BridgeEvent {
18561                        kind: String::from("serverStreamError"),
18562                        id: session_id,
18563                        data: Some(http2_error_payload(error.to_string())),
18564                        ..Http2BridgeEvent::default()
18565                    },
18566                );
18567                remove_http2_session_resources(&shared, session_id);
18568                return;
18569            }
18570            let stream = match tokio::net::TcpStream::from_std(stream) {
18571                Ok(stream) => stream,
18572                Err(error) => {
18573                    push_http2_server_event(
18574                        &shared,
18575                        server_id,
18576                        Http2BridgeEvent {
18577                            kind: String::from("serverStreamError"),
18578                            id: session_id,
18579                            data: Some(http2_error_payload(error.to_string())),
18580                            ..Http2BridgeEvent::default()
18581                        },
18582                    );
18583                    remove_http2_session_resources(&shared, session_id);
18584                    return;
18585                }
18586            };
18587            let local_addr = match stream.local_addr() {
18588                Ok(addr) => addr,
18589                Err(error) => {
18590                    push_http2_server_event(
18591                        &shared,
18592                        server_id,
18593                        Http2BridgeEvent {
18594                            kind: String::from("serverStreamError"),
18595                            id: session_id,
18596                            data: Some(http2_error_payload(error.to_string())),
18597                            ..Http2BridgeEvent::default()
18598                        },
18599                    );
18600                    remove_http2_session_resources(&shared, session_id);
18601                    return;
18602                }
18603            };
18604            let remote_addr = match stream.peer_addr() {
18605                Ok(addr) => addr,
18606                Err(error) => {
18607                    push_http2_server_event(
18608                        &shared,
18609                        server_id,
18610                        Http2BridgeEvent {
18611                            kind: String::from("serverStreamError"),
18612                            id: session_id,
18613                            data: Some(http2_error_payload(error.to_string())),
18614                            ..Http2BridgeEvent::default()
18615                        },
18616                    );
18617                    remove_http2_session_resources(&shared, session_id);
18618                    return;
18619                }
18620            };
18621            {
18622                let mut snapshot_guard = snapshot.lock().expect("http2 snapshot lock");
18623                snapshot_guard.socket = http2_socket_snapshot(local_addr, remote_addr);
18624                if tls.is_some() {
18625                    snapshot_guard.encrypted = true;
18626                    snapshot_guard.alpn_protocol = Some(String::from("h2"));
18627                    snapshot_guard.socket.encrypted = true;
18628                    snapshot_guard.socket.alpn_protocol = Some(String::from("h2"));
18629                }
18630                snapshot_guard.state = http2_runtime_snapshot();
18631            }
18632            if let Ok(snapshot_json) =
18633                http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())
18634            {
18635                push_http2_server_event(
18636                    &shared,
18637                    server_id,
18638                    Http2BridgeEvent {
18639                        kind: String::from(if tls.is_some() {
18640                            "serverSecureConnection"
18641                        } else {
18642                            "serverConnection"
18643                        }),
18644                        id: server_id,
18645                        data: Some(serde_json::to_string(&http2_socket_snapshot(local_addr, remote_addr)).unwrap_or_default()),
18646                        ..Http2BridgeEvent::default()
18647                    },
18648                );
18649                push_http2_server_event(
18650                    &shared,
18651                    server_id,
18652                    Http2BridgeEvent {
18653                        kind: String::from("serverSession"),
18654                        id: server_id,
18655                        data: Some(snapshot_json),
18656                        extra_number: Some(session_id),
18657                        ..Http2BridgeEvent::default()
18658                    },
18659                );
18660            }
18661
18662            let io: Pin<Box<dyn Http2AsyncIo>> = if let Some(options) = tls.as_ref() {
18663                let acceptor = match build_server_tls_config(options) {
18664                    Ok(config) => TlsAcceptor::from(Arc::new(config)),
18665                    Err(error) => {
18666                        push_http2_server_event(
18667                            &shared,
18668                            server_id,
18669                            Http2BridgeEvent {
18670                                kind: String::from("serverStreamError"),
18671                                id: session_id,
18672                                data: Some(http2_error_payload(error.to_string())),
18673                                ..Http2BridgeEvent::default()
18674                            },
18675                        );
18676                        remove_http2_session_resources(&shared, session_id);
18677                        return;
18678                    }
18679                };
18680                match acceptor.accept(stream).await {
18681                    Ok(tls_stream) => Box::pin(tls_stream),
18682                    Err(error) => {
18683                        push_http2_server_event(
18684                            &shared,
18685                            server_id,
18686                            Http2BridgeEvent {
18687                                kind: String::from("serverStreamError"),
18688                                id: session_id,
18689                                data: Some(http2_error_payload(error.to_string())),
18690                                ..Http2BridgeEvent::default()
18691                            },
18692                        );
18693                        remove_http2_session_resources(&shared, session_id);
18694                        return;
18695                    }
18696                }
18697            } else {
18698                Box::pin(stream)
18699            };
18700
18701            let mut connection = match server::handshake(io).await {
18702                Ok(connection) => connection,
18703                Err(error) => {
18704                    push_http2_server_event(
18705                        &shared,
18706                        server_id,
18707                        Http2BridgeEvent {
18708                            kind: String::from("serverStreamError"),
18709                            id: session_id,
18710                            data: Some(http2_error_payload(error.to_string())),
18711                            ..Http2BridgeEvent::default()
18712                        },
18713                    );
18714                    remove_http2_session_resources(&shared, session_id);
18715                    return;
18716                }
18717            };
18718
18719            let streams: Arc<Mutex<BTreeMap<u64, ServerHttp2StreamState>>> =
18720                Arc::new(Mutex::new(BTreeMap::new()));
18721
18722            loop {
18723                tokio::select! {
18724                    incoming = connection.accept() => {
18725                        match incoming {
18726                            Some(Ok((request, respond))) => {
18727                                let headers_json = match serialize_http2_request_headers(&request) {
18728                                    Ok(headers) => headers,
18729                                    Err(error) => {
18730                                        push_http2_server_event(
18731                                            &shared,
18732                                            server_id,
18733                                            Http2BridgeEvent {
18734                                                kind: String::from("serverStreamError"),
18735                                                id: server_id,
18736                                                data: Some(http2_error_payload(error.to_string())),
18737                                                ..Http2BridgeEvent::default()
18738                                            },
18739                                        );
18740                                        continue;
18741                                    }
18742                                };
18743                                let stream_id = {
18744                                    let mut state = shared.lock().expect("http2 shared state");
18745                                    let stream_id = next_http2_stream_id(&mut state);
18746                                    state.streams.insert(
18747                                        stream_id,
18748                                        ActiveHttp2Stream {
18749                                            session_id,
18750                                            paused: Arc::new(AtomicBool::new(false)),
18751                                        },
18752                                    );
18753                                    stream_id
18754                                };
18755                                streams.lock().expect("http2 server streams").insert(
18756                                    stream_id,
18757                                    ServerHttp2StreamState {
18758                                        send_response: Some(ServerHttp2Responder::Regular(respond)),
18759                                        send_stream: None,
18760                                    },
18761                                );
18762                                let snapshot_json = snapshot
18763                                    .lock()
18764                                    .ok()
18765                                    .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok());
18766                                push_http2_server_event(
18767                                    &shared,
18768                                    server_id,
18769                                    Http2BridgeEvent {
18770                                        kind: String::from("serverStream"),
18771                                        id: server_id,
18772                                        data: Some(stream_id.to_string()),
18773                                        extra: snapshot_json,
18774                                        extra_number: Some(session_id),
18775                                        extra_headers: Some(headers_json),
18776                                        flags: Some(0),
18777                                    },
18778                                );
18779                                let shared_clone = Arc::clone(&shared);
18780                                tokio::spawn(async move {
18781                                    let mut body = request.into_body();
18782                                    while let Some(chunk) = body.data().await {
18783                                        match chunk {
18784                                            Ok(bytes) => {
18785                                                let paused = {
18786                                                    let state = shared_clone.lock().expect("http2 shared state");
18787                                                    state.streams.get(&stream_id).map(|stream| Arc::clone(&stream.paused))
18788                                                };
18789                                                if let Some(paused) = paused {
18790                                                    while paused.load(Ordering::SeqCst) {
18791                                                        tokio::time::sleep(HTTP2_POLL_DELAY).await;
18792                                                    }
18793                                                }
18794                                                let _ = body.flow_control().release_capacity(bytes.len());
18795                                                push_http2_server_event(
18796                                                    &shared_clone,
18797                                                    server_id,
18798                                                    Http2BridgeEvent {
18799                                                        kind: String::from("serverStreamData"),
18800                                                        id: stream_id,
18801                                                        data: Some(base64::engine::general_purpose::STANDARD.encode(bytes)),
18802                                                        ..Http2BridgeEvent::default()
18803                                                    },
18804                                                );
18805                                            }
18806                                            Err(error) => {
18807                                                push_http2_server_event(
18808                                                    &shared_clone,
18809                                                    server_id,
18810                                                    Http2BridgeEvent {
18811                                                        kind: String::from("serverStreamError"),
18812                                                        id: stream_id,
18813                                                        data: Some(http2_error_payload(error.to_string())),
18814                                                        ..Http2BridgeEvent::default()
18815                                                    },
18816                                                );
18817                                                break;
18818                                            }
18819                                        }
18820                                    }
18821                                    push_http2_server_event(
18822                                        &shared_clone,
18823                                        server_id,
18824                                        Http2BridgeEvent {
18825                                            kind: String::from("serverStreamEnd"),
18826                                            id: stream_id,
18827                                            ..Http2BridgeEvent::default()
18828                                        },
18829                                    );
18830                                });
18831                            }
18832                            Some(Err(error)) => {
18833                                push_http2_server_event(
18834                                    &shared,
18835                                    server_id,
18836                                    Http2BridgeEvent {
18837                                        kind: String::from("serverStreamError"),
18838                                        id: server_id,
18839                                        data: Some(http2_error_payload(error.to_string())),
18840                                        ..Http2BridgeEvent::default()
18841                                    },
18842                                );
18843                                break;
18844                            }
18845                            None => {
18846                                push_http2_server_event(
18847                                    &shared,
18848                                    server_id,
18849                                    Http2BridgeEvent {
18850                                        kind: String::from("sessionClose"),
18851                                        id: session_id,
18852                                        ..Http2BridgeEvent::default()
18853                                    },
18854                                );
18855                                remove_http2_session_resources(&shared, session_id);
18856                                break;
18857                            }
18858                        }
18859                    }
18860                    Some(command) = command_rx.recv() => {
18861                        match command {
18862                            Http2SessionCommand::Settings { settings_json, respond_to } => {
18863                                let settings = serde_json::from_str::<BTreeMap<String, Value>>(&settings_json)
18864                                    .unwrap_or_default();
18865                                if let Some(initial_window_size) = settings
18866                                    .get("initialWindowSize")
18867                                    .and_then(Value::as_u64)
18868                                {
18869                                    let _ = connection.set_initial_window_size(initial_window_size as u32);
18870                                }
18871                                {
18872                                    let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18873                                    snapshot.local_settings = http2_settings_from_value(&settings);
18874                                }
18875                                if let Ok(headers_json) = serde_json::to_string(&settings) {
18876                                    push_http2_session_event(
18877                                        &shared,
18878                                        session_id,
18879                                        Http2BridgeEvent {
18880                                            kind: String::from("sessionLocalSettings"),
18881                                            id: session_id,
18882                                            data: Some(headers_json),
18883                                            ..Http2BridgeEvent::default()
18884                                        },
18885                                    );
18886                                }
18887                                let _ = respond_to.send(Ok(Value::Null));
18888                            }
18889                            Http2SessionCommand::SetLocalWindowSize { size, respond_to } => {
18890                                connection.set_target_window_size(size);
18891                                {
18892                                    let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18893                                    snapshot.state.local_window_size = size;
18894                                    snapshot.state.effective_local_window_size = size;
18895                                }
18896                                let value = snapshot
18897                                    .lock()
18898                                    .ok()
18899                                    .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok())
18900                                    .map(Value::String)
18901                                    .unwrap_or(Value::Null);
18902                                let _ = respond_to.send(Ok(value));
18903                            }
18904                            Http2SessionCommand::Goaway { error_code, last_stream_id, opaque_data, respond_to } => {
18905                                connection.abrupt_shutdown(http2_reason(Some(error_code)));
18906                                push_http2_session_event(
18907                                    &shared,
18908                                    session_id,
18909                                    Http2BridgeEvent {
18910                                        kind: String::from("sessionGoaway"),
18911                                        id: session_id,
18912                                        data: opaque_data.map(|value| {
18913                                            base64::engine::general_purpose::STANDARD.encode(value)
18914                                        }),
18915                                        extra_number: Some(error_code as u64),
18916                                        flags: Some(last_stream_id as u64),
18917                                        ..Http2BridgeEvent::default()
18918                                    },
18919                                );
18920                                let _ = respond_to.send(Ok(Value::Null));
18921                            }
18922                            Http2SessionCommand::Close { abrupt, respond_to } => {
18923                                if abrupt {
18924                                    connection.abrupt_shutdown(Reason::NO_ERROR);
18925                                } else {
18926                                    connection.graceful_shutdown();
18927                                }
18928                                let _ = respond_to.send(Ok(Value::Null));
18929                                push_http2_session_event(
18930                                    &shared,
18931                                    session_id,
18932                                    Http2BridgeEvent {
18933                                        kind: String::from("sessionClose"),
18934                                        id: session_id,
18935                                        ..Http2BridgeEvent::default()
18936                                    },
18937                                );
18938                                remove_http2_session_resources(&shared, session_id);
18939                                break;
18940                            }
18941                            Http2SessionCommand::StreamRespond { stream_id, headers_json, respond_to } => {
18942                                let response = match build_http2_response(&headers_json) {
18943                                    Ok(response) => response,
18944                                    Err(error) => {
18945                                        let _ = respond_to.send(Err(error.to_string()));
18946                                        continue;
18947                                    }
18948                                };
18949                                let mut streams = streams.lock().expect("http2 server streams");
18950                                let Some(state) = streams.get_mut(&stream_id) else {
18951                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
18952                                    continue;
18953                                };
18954                                let Some(send_response) = state.send_response.as_mut() else {
18955                                    let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} already responded")));
18956                                    continue;
18957                                };
18958                                match match send_response {
18959                                    ServerHttp2Responder::Regular(send_response) => {
18960                                        send_response.send_response(response, false)
18961                                    }
18962                                    ServerHttp2Responder::Pushed(send_response) => {
18963                                        send_response.send_response(response, false)
18964                                    }
18965                                } {
18966                                    Ok(send_stream) => {
18967                                        state.send_stream = Some(send_stream);
18968                                        state.send_response = None;
18969                                        let _ = respond_to.send(Ok(Value::Null));
18970                                    }
18971                                    Err(error) => {
18972                                        let _ = respond_to.send(Err(error.to_string()));
18973                                    }
18974                                }
18975                            }
18976                            Http2SessionCommand::StreamPush { stream_id, headers_json, respond_to } => {
18977                                let request = match build_http2_request(&headers_json) {
18978                                    Ok(request) => request,
18979                                    Err(error) => {
18980                                        let _ = respond_to.send(Err(error.to_string()));
18981                                        continue;
18982                                    }
18983                                };
18984                                let mut streams_guard = streams.lock().expect("http2 server streams");
18985                                let Some(state) = streams_guard.get_mut(&stream_id) else {
18986                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
18987                                    continue;
18988                                };
18989                                let Some(send_response) = state.send_response.as_mut() else {
18990                                    let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} cannot push after responding")));
18991                                    continue;
18992                                };
18993                                let ServerHttp2Responder::Regular(send_response) = send_response else {
18994                                    let _ = respond_to.send(Err(format!("HTTP/2 pushed stream {stream_id} cannot create nested push promises")));
18995                                    continue;
18996                                };
18997                                match send_response.push_request(request) {
18998                                    Ok(pushed) => {
18999                                        let pushed_stream_id = {
19000                                            let mut state = shared.lock().expect("http2 shared state");
19001                                            let pushed_stream_id = next_http2_stream_id(&mut state);
19002                                            state.streams.insert(
19003                                                pushed_stream_id,
19004                                                ActiveHttp2Stream {
19005                                                    session_id,
19006                                                    paused: Arc::new(AtomicBool::new(false)),
19007                                                },
19008                                            );
19009                                            pushed_stream_id
19010                                        };
19011                                        streams_guard.insert(
19012                                            pushed_stream_id,
19013                                            ServerHttp2StreamState {
19014                                                send_response: Some(ServerHttp2Responder::Pushed(pushed)),
19015                                                send_stream: None,
19016                                            },
19017                                        );
19018                                        let _ = respond_to.send(Ok(json!({
19019                                            "streamId": pushed_stream_id,
19020                                            "headers": headers_json,
19021                                        }).to_string().into()));
19022                                    }
19023                                    Err(error) => {
19024                                        let _ = respond_to.send(Err(error.to_string()));
19025                                    }
19026                                }
19027                            }
19028                            Http2SessionCommand::StreamWrite { stream_id, chunk, end_stream, respond_to } => {
19029                                let mut streams = streams.lock().expect("http2 server streams");
19030                                let Some(state) = streams.get_mut(&stream_id) else {
19031                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
19032                                    continue;
19033                                };
19034                                let Some(send_stream) = state.send_stream.as_mut() else {
19035                                    let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} has not sent response headers")));
19036                                    continue;
19037                                };
19038                                match send_stream.send_data(Bytes::from(chunk), end_stream) {
19039                                    Ok(()) => {
19040                                        if end_stream {
19041                                            streams.remove(&stream_id);
19042                                            if let Ok(mut state) = shared.lock() {
19043                                                state.streams.remove(&stream_id);
19044                                            }
19045                                            push_http2_server_event(
19046                                                &shared,
19047                                                server_id,
19048                                                Http2BridgeEvent {
19049                                                    kind: String::from("serverStreamClose"),
19050                                                    id: stream_id,
19051                                                    extra_number: Some(0),
19052                                                    ..Http2BridgeEvent::default()
19053                                                },
19054                                            );
19055                                        }
19056                                        let _ = respond_to.send(Ok(Value::Bool(true)));
19057                                    }
19058                                    Err(error) => {
19059                                        let _ = respond_to.send(Err(error.to_string()));
19060                                    }
19061                                }
19062                            }
19063                            Http2SessionCommand::StreamClose { stream_id, error_code, respond_to } => {
19064                                let mut streams_guard = streams.lock().expect("http2 server streams");
19065                                let Some(mut state) = streams_guard.remove(&stream_id) else {
19066                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
19067                                    continue;
19068                                };
19069                                let reason = http2_reason(error_code);
19070                                if let Some(send_stream) = state.send_stream.as_mut() {
19071                                    send_stream.send_reset(reason);
19072                                }
19073                                if let Some(send_response) = state.send_response.as_mut() {
19074                                    match send_response {
19075                                        ServerHttp2Responder::Regular(send_response) => {
19076                                            send_response.send_reset(reason)
19077                                        }
19078                                        ServerHttp2Responder::Pushed(send_response) => {
19079                                            send_response.send_reset(reason)
19080                                        }
19081                                    }
19082                                }
19083                                if let Ok(mut shared_guard) = shared.lock() {
19084                                    shared_guard.streams.remove(&stream_id);
19085                                }
19086                                push_http2_server_event(
19087                                    &shared,
19088                                    server_id,
19089                                    Http2BridgeEvent {
19090                                        kind: String::from("serverStreamClose"),
19091                                        id: stream_id,
19092                                        extra_number: Some(u32::from(reason) as u64),
19093                                        ..Http2BridgeEvent::default()
19094                                    },
19095                                );
19096                                let _ = respond_to.send(Ok(Value::Null));
19097                            }
19098                            Http2SessionCommand::StreamRespondWithFile { stream_id, body, headers_json, options_json, respond_to } => {
19099                                let options: JavascriptHttp2FileResponseOptions =
19100                                    serde_json::from_str(&options_json).unwrap_or_default();
19101                                let response = match build_http2_response(&headers_json) {
19102                                    Ok(response) => response,
19103                                    Err(error) => {
19104                                        let _ = respond_to.send(Err(error.to_string()));
19105                                        continue;
19106                                    }
19107                                };
19108                                let offset = usize::try_from(options.offset.unwrap_or_default()).unwrap_or(0);
19109                                let body = if offset >= body.len() {
19110                                    Vec::new()
19111                                } else {
19112                                    let body = &body[offset..];
19113                                    match options.length {
19114                                        Some(length) if length >= 0 => {
19115                                            body[..body.len().min(length as usize)].to_vec()
19116                                        }
19117                                        _ => body.to_vec(),
19118                                    }
19119                                };
19120                                let mut streams_guard = streams.lock().expect("http2 server streams");
19121                                let Some(state) = streams_guard.get_mut(&stream_id) else {
19122                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
19123                                    continue;
19124                                };
19125                                let Some(send_response) = state.send_response.as_mut() else {
19126                                    let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} already responded")));
19127                                    continue;
19128                                };
19129                                match match send_response {
19130                                    ServerHttp2Responder::Regular(send_response) => {
19131                                        send_response.send_response(response, body.is_empty())
19132                                    }
19133                                    ServerHttp2Responder::Pushed(send_response) => {
19134                                        send_response.send_response(response, body.is_empty())
19135                                    }
19136                                } {
19137                                    Ok(mut send_stream) => {
19138                                        state.send_response = None;
19139                                        if body.is_empty() {
19140                                            streams_guard.remove(&stream_id);
19141                                            if let Ok(mut shared_guard) = shared.lock() {
19142                                                shared_guard.streams.remove(&stream_id);
19143                                            }
19144                                        } else {
19145                                            if let Err(error) = send_stream.send_data(Bytes::from(body), true) {
19146                                                let _ = respond_to.send(Err(error.to_string()));
19147                                                continue;
19148                                            }
19149                                            streams_guard.remove(&stream_id);
19150                                            if let Ok(mut shared_guard) = shared.lock() {
19151                                                shared_guard.streams.remove(&stream_id);
19152                                            }
19153                                        }
19154                                        push_http2_server_event(
19155                                            &shared,
19156                                            server_id,
19157                                            Http2BridgeEvent {
19158                                                kind: String::from("serverStreamClose"),
19159                                                id: stream_id,
19160                                                extra_number: Some(0),
19161                                                ..Http2BridgeEvent::default()
19162                                            },
19163                                        );
19164                                        let _ = respond_to.send(Ok(Value::Null));
19165                                    }
19166                                    Err(error) => {
19167                                        let _ = respond_to.send(Err(error.to_string()));
19168                                    }
19169                                }
19170                            }
19171                            Http2SessionCommand::Request { respond_to, .. } => {
19172                                let _ = respond_to.send(Err(String::from("HTTP/2 server sessions cannot initiate client requests")));
19173                            }
19174                        }
19175                    }
19176                    else => break,
19177                }
19178            }
19179        });
19180    });
19181}
19182
19183fn spawn_http2_server_accept_loop(
19184    shared: Arc<Mutex<crate::state::Http2SharedState>>,
19185    server_id: u64,
19186    listener: TcpListener,
19187) {
19188    thread::spawn(move || {
19189        let listener = listener;
19190        loop {
19191            let closed = shared
19192                .lock()
19193                .ok()
19194                .and_then(|state| {
19195                    state
19196                        .servers
19197                        .get(&server_id)
19198                        .map(|server| server.closed.load(Ordering::SeqCst))
19199                })
19200                .unwrap_or(true);
19201            if closed {
19202                break;
19203            }
19204            match listener.accept() {
19205                Ok((stream, _)) => {
19206                    let (command_tx, command_rx) = unbounded_channel();
19207                    let (guest_local_addr, secure, tls) = {
19208                        let state = shared.lock().expect("http2 shared state");
19209                        let server = state.servers.get(&server_id).expect("http2 server state");
19210                        (server.guest_local_addr, server.secure, server.tls.clone())
19211                    };
19212                    let (local_addr, remote_addr) = match (stream.local_addr(), stream.peer_addr())
19213                    {
19214                        (Ok(local_addr), Ok(remote_addr)) => (local_addr, remote_addr),
19215                        _ => continue,
19216                    };
19217                    let session_snapshot = Arc::new(Mutex::new(Http2SessionSnapshot {
19218                        encrypted: secure,
19219                        alpn_protocol: Some(if secure {
19220                            String::from("h2")
19221                        } else {
19222                            String::from("h2c")
19223                        }),
19224                        local_settings: BTreeMap::new(),
19225                        remote_settings: BTreeMap::new(),
19226                        state: http2_runtime_snapshot(),
19227                        socket: Http2SocketSnapshot {
19228                            local_address: Some(guest_local_addr.ip().to_string()),
19229                            local_port: Some(guest_local_addr.port()),
19230                            local_family: Some(socket_addr_family(&guest_local_addr).to_string()),
19231                            remote_address: Some(remote_addr.ip().to_string()),
19232                            remote_port: Some(remote_addr.port()),
19233                            remote_family: Some(socket_addr_family(&remote_addr).to_string()),
19234                            ..http2_socket_snapshot(local_addr, remote_addr)
19235                        },
19236                        ..Http2SessionSnapshot::default()
19237                    }));
19238                    let session_id = {
19239                        let mut state = shared.lock().expect("http2 shared state");
19240                        let session_id = next_http2_session_id(&mut state);
19241                        state
19242                            .sessions
19243                            .insert(session_id, ActiveHttp2Session { command_tx });
19244                        session_id
19245                    };
19246                    spawn_http2_server_session(
19247                        Arc::clone(&shared),
19248                        server_id,
19249                        session_id,
19250                        stream,
19251                        tls,
19252                        session_snapshot,
19253                        command_rx,
19254                    );
19255                }
19256                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
19257                    thread::sleep(HTTP2_POLL_DELAY);
19258                }
19259                Err(error) => {
19260                    push_http2_server_event(
19261                        &shared,
19262                        server_id,
19263                        Http2BridgeEvent {
19264                            kind: String::from("serverStreamError"),
19265                            id: server_id,
19266                            data: Some(http2_error_payload(error.to_string())),
19267                            ..Http2BridgeEvent::default()
19268                        },
19269                    );
19270                    thread::sleep(HTTP2_POLL_DELAY);
19271                }
19272            }
19273        }
19274    });
19275}
19276
19277fn send_http2_command(
19278    session: &ActiveHttp2Session,
19279    command: impl FnOnce(Sender<Result<Value, String>>) -> Http2SessionCommand,
19280) -> Result<Value, SidecarError> {
19281    let (respond_to, response_rx) = mpsc::channel();
19282    session.command_tx.send(command(respond_to)).map_err(|_| {
19283        SidecarError::InvalidState(String::from("HTTP/2 session command channel closed"))
19284    })?;
19285    response_rx
19286        .recv_timeout(Duration::from_secs(30))
19287        .map_err(|_| {
19288            SidecarError::Execution(String::from("timed out waiting for HTTP/2 session command"))
19289        })?
19290        .map_err(SidecarError::Execution)
19291}
19292
19293fn parse_http2_server_listen_payload(
19294    request: &JavascriptSyncRpcRequest,
19295) -> Result<JavascriptHttp2ServerListenRequest, SidecarError> {
19296    let payload_json =
19297        javascript_sync_rpc_arg_str(&request.args, 0, "net.http2_server_listen payload")?;
19298    serde_json::from_str(payload_json).map_err(|error| {
19299        SidecarError::InvalidState(format!(
19300            "net.http2_server_listen payload must be valid JSON: {error}"
19301        ))
19302    })
19303}
19304
19305fn parse_http2_connect_payload(
19306    request: &JavascriptSyncRpcRequest,
19307) -> Result<JavascriptHttp2SessionConnectRequest, SidecarError> {
19308    let payload_json =
19309        javascript_sync_rpc_arg_str(&request.args, 0, "net.http2_session_connect payload")?;
19310    serde_json::from_str(payload_json).map_err(|error| {
19311        SidecarError::InvalidState(format!(
19312            "net.http2_session_connect payload must be valid JSON: {error}"
19313        ))
19314    })
19315}
19316
19317fn http2_session_for_id(
19318    process: &ActiveProcess,
19319    session_id: u64,
19320) -> Result<ActiveHttp2Session, SidecarError> {
19321    let shared = process
19322        .http2
19323        .shared
19324        .lock()
19325        .map_err(|_| SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned")))?;
19326    shared
19327        .sessions
19328        .get(&session_id)
19329        .cloned()
19330        .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 session {session_id}")))
19331}
19332
19333fn http2_stream_for_id(
19334    process: &ActiveProcess,
19335    stream_id: u64,
19336) -> Result<ActiveHttp2Stream, SidecarError> {
19337    let shared = process
19338        .http2
19339        .shared
19340        .lock()
19341        .map_err(|_| SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned")))?;
19342    shared
19343        .streams
19344        .get(&stream_id)
19345        .cloned()
19346        .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 stream {stream_id}")))
19347}
19348
19349fn service_javascript_http2_sync_rpc<B>(
19350    request: JavascriptHttp2SyncRpcServiceRequest<'_, B>,
19351) -> Result<Value, SidecarError>
19352where
19353    B: NativeSidecarBridge + Send + 'static,
19354    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19355{
19356    let JavascriptHttp2SyncRpcServiceRequest {
19357        bridge,
19358        kernel,
19359        vm_id,
19360        dns,
19361        socket_paths,
19362        process,
19363        sync_request: request,
19364        resource_limits,
19365        network_counts,
19366    } = request;
19367    match request.method.as_str() {
19368        "net.http2_server_listen" => {
19369            check_network_resource_limit(
19370                resource_limits.max_sockets,
19371                network_counts.sockets,
19372                1,
19373                "socket",
19374            )?;
19375            let payload = parse_http2_server_listen_payload(request)?;
19376            let (family, bind_host, guest_host) =
19377                normalize_tcp_listen_host(payload.host.as_deref())?;
19378            let requested_port = payload.port.unwrap_or(0);
19379            bridge.require_network_access(
19380                vm_id,
19381                NetworkOperation::Listen,
19382                format_tcp_resource(bind_host, requested_port),
19383            )?;
19384            let port = allocate_guest_listen_port(
19385                requested_port,
19386                family,
19387                &socket_paths.used_tcp_guest_ports,
19388                socket_paths.listen_policy,
19389            )?;
19390            let mut listener =
19391                ActiveTcpListener::bind(bind_host, guest_host, port, payload.backlog)?;
19392            let guest_local_addr = listener.guest_local_addr();
19393            let closed = Arc::new(AtomicBool::new(false));
19394            {
19395                let mut state = process.http2.shared.lock().map_err(|_| {
19396                    SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19397                })?;
19398                state.servers.insert(
19399                    payload.server_id,
19400                    ActiveHttp2Server {
19401                        actual_local_addr: listener.local_addr(),
19402                        guest_local_addr,
19403                        secure: payload.secure,
19404                        tls: payload.tls.clone().map(|mut tls| {
19405                            tls.is_server = payload.secure;
19406                            if payload.secure && tls.alpn_protocols.is_none() {
19407                                tls.alpn_protocols = Some(vec![String::from("h2")]);
19408                            }
19409                            tls
19410                        }),
19411                        closed: Arc::clone(&closed),
19412                    },
19413                );
19414                state.server_events.entry(payload.server_id).or_default();
19415            }
19416            spawn_http2_server_accept_loop(
19417                Arc::clone(&process.http2.shared),
19418                payload.server_id,
19419                listener.listener.take().ok_or_else(|| {
19420                    SidecarError::InvalidState(String::from(
19421                        "HTTP/2 listener missing host TCP socket",
19422                    ))
19423                })?,
19424            );
19425            javascript_net_json_string(
19426                json!({
19427                    "address": {
19428                        "address": guest_local_addr.ip().to_string(),
19429                        "family": socket_addr_family(&guest_local_addr),
19430                        "port": guest_local_addr.port(),
19431                    }
19432                }),
19433                "net.http2_server_listen",
19434            )
19435        }
19436        "net.http2_server_poll" => {
19437            let server_id =
19438                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_poll server id")?;
19439            let wait_ms = javascript_sync_rpc_arg_u64_optional(
19440                &request.args,
19441                1,
19442                "net.http2_server_poll wait ms",
19443            )?
19444            .unwrap_or_default();
19445            match wait_for_http2_event(&process.http2.shared, server_id, true, wait_ms) {
19446                Some(event) => http2_event_value(&event),
19447                None => Ok(Value::Null),
19448            }
19449        }
19450        "net.http2_server_wait" => {
19451            let server_id =
19452                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_wait server id")?;
19453            dispatch_http2_wait_loop(process, server_id, true)
19454        }
19455        "net.http2_server_close" => {
19456            let server_id =
19457                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_close server id")?;
19458            let server = {
19459                let mut state = process.http2.shared.lock().map_err(|_| {
19460                    SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19461                })?;
19462                state.servers.remove(&server_id)
19463            }
19464            .ok_or_else(|| {
19465                SidecarError::InvalidState(format!("unknown HTTP/2 server {server_id}"))
19466            })?;
19467            server.closed.store(true, Ordering::SeqCst);
19468            push_http2_server_event(
19469                &process.http2.shared,
19470                server_id,
19471                Http2BridgeEvent {
19472                    kind: String::from("serverClose"),
19473                    id: server_id,
19474                    ..Http2BridgeEvent::default()
19475                },
19476            );
19477            Ok(Value::Null)
19478        }
19479        "net.http2_server_respond" => {
19480            let server_id = javascript_sync_rpc_arg_u64(
19481                &request.args,
19482                0,
19483                "net.http2_server_respond server id",
19484            )?;
19485            let request_id = javascript_sync_rpc_arg_u64(
19486                &request.args,
19487                1,
19488                "net.http2_server_respond request id",
19489            )?;
19490            let response_json =
19491                javascript_sync_rpc_arg_str(&request.args, 2, "net.http2_server_respond payload")?;
19492            ensure_vm_fetch_response_within_limit(
19493                response_json,
19494                "net.http2_server_respond",
19495                VM_FETCH_BUFFER_LIMIT_BYTES,
19496            )?;
19497            serde_json::from_str::<Value>(response_json).map_err(|error| {
19498                SidecarError::Execution(format!(
19499                    "net.http2_server_respond payload must be valid JSON: {error}"
19500                ))
19501            })?;
19502            let Some(pending) = process
19503                .pending_http_requests
19504                .get_mut(&(server_id, request_id))
19505            else {
19506                return Err(SidecarError::InvalidState(format!(
19507                    "unknown pending HTTP/2 request {request_id} for server {server_id}"
19508                )));
19509            };
19510            *pending = Some(response_json.to_owned());
19511            Ok(Value::Bool(true))
19512        }
19513        "net.http2_session_connect" => {
19514            check_network_resource_limit(
19515                resource_limits.max_sockets,
19516                network_counts.sockets,
19517                1,
19518                "socket",
19519            )?;
19520            check_network_resource_limit(
19521                resource_limits.max_connections,
19522                network_counts.connections,
19523                1,
19524                "connection",
19525            )?;
19526            let payload = parse_http2_connect_payload(request)?;
19527            let authority = payload.authority.clone().unwrap_or_else(|| {
19528                format!(
19529                    "{}://{}:{}",
19530                    payload.protocol.as_deref().unwrap_or("http"),
19531                    payload.host.as_deref().unwrap_or("localhost"),
19532                    payload.port.unwrap_or(80)
19533                )
19534            });
19535            let url = Url::parse(&authority).map_err(|error| {
19536                SidecarError::InvalidState(format!(
19537                    "invalid HTTP/2 authority {authority:?}: {error}"
19538                ))
19539            })?;
19540            let secure = url.scheme() == "https" || payload.protocol.as_deref() == Some("https:");
19541            let host = payload
19542                .host
19543                .as_deref()
19544                .or_else(|| url.host_str())
19545                .unwrap_or("localhost");
19546            let port = payload.port.or_else(|| url.port()).unwrap_or(80);
19547            bridge.require_network_access(
19548                vm_id,
19549                NetworkOperation::Http,
19550                format_tcp_resource(host, port),
19551            )?;
19552            let resolved = {
19553                let shared = process.http2.shared.lock().map_err(|_| {
19554                    SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19555                })?;
19556                shared
19557                    .servers
19558                    .values()
19559                    .find(|server| {
19560                        is_loopback_request_host(host) && server.guest_local_addr.port() == port
19561                    })
19562                    .map(|server| ResolvedTcpConnectAddr {
19563                        actual_addr: server.actual_local_addr,
19564                        guest_remote_addr: server.guest_local_addr,
19565                        use_kernel_loopback: false,
19566                    })
19567            };
19568            let resolved = match resolved {
19569                Some(resolved) => resolved,
19570                None => {
19571                    resolve_tcp_connect_addr(bridge, kernel, vm_id, dns, host, port, socket_paths)?
19572                }
19573            };
19574            let (command_tx, command_rx) = unbounded_channel();
19575            let snapshot = Arc::new(Mutex::new(Http2SessionSnapshot {
19576                encrypted: secure,
19577                alpn_protocol: Some(String::from(if secure { "h2" } else { "h2c" })),
19578                local_settings: http2_settings_from_value(&payload.settings),
19579                remote_settings: BTreeMap::new(),
19580                state: http2_runtime_snapshot(),
19581                socket: Http2SocketSnapshot {
19582                    encrypted: secure,
19583                    remote_address: Some(resolved.guest_remote_addr.ip().to_string()),
19584                    remote_port: Some(resolved.guest_remote_addr.port()),
19585                    remote_family: Some(
19586                        socket_addr_family(&resolved.guest_remote_addr).to_string(),
19587                    ),
19588                    servername: if secure {
19589                        payload
19590                            .tls
19591                            .as_ref()
19592                            .and_then(|tls| tls.servername.clone())
19593                            .or_else(|| Some(host.to_string()))
19594                    } else {
19595                        None
19596                    },
19597                    alpn_protocol: Some(String::from(if secure { "h2" } else { "h2c" })),
19598                    ..Http2SocketSnapshot::default()
19599                },
19600                ..Http2SessionSnapshot::default()
19601            }));
19602            let session_id = {
19603                let mut state = process.http2.shared.lock().map_err(|_| {
19604                    SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19605                })?;
19606                let session_id = next_http2_session_id(&mut state);
19607                state
19608                    .sessions
19609                    .insert(session_id, ActiveHttp2Session { command_tx });
19610                state.session_events.entry(session_id).or_default();
19611                session_id
19612            };
19613            spawn_http2_client_session(
19614                Arc::clone(&process.http2.shared),
19615                session_id,
19616                resolved.actual_addr,
19617                if secure {
19618                    Some(payload.tls.unwrap_or(JavascriptTlsBridgeOptions {
19619                        is_server: false,
19620                        servername: Some(host.to_string()),
19621                        alpn_protocols: Some(vec![String::from("h2")]),
19622                        ..JavascriptTlsBridgeOptions::default()
19623                    }))
19624                } else {
19625                    None
19626                },
19627                Arc::clone(&snapshot),
19628                command_rx,
19629            );
19630            let snapshot_json =
19631                http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())?;
19632            javascript_net_json_string(
19633                json!({
19634                    "sessionId": session_id,
19635                    "state": snapshot_json,
19636                }),
19637                "net.http2_session_connect",
19638            )
19639        }
19640        "net.http2_session_request" => {
19641            let session_id = javascript_sync_rpc_arg_u64(
19642                &request.args,
19643                0,
19644                "net.http2_session_request session id",
19645            )?;
19646            let headers_json =
19647                javascript_sync_rpc_arg_str(&request.args, 1, "net.http2_session_request headers")?;
19648            let options_json =
19649                javascript_sync_rpc_arg_str(&request.args, 2, "net.http2_session_request options")?;
19650            let session = http2_session_for_id(process, session_id)?;
19651            send_http2_command(&session, |respond_to| Http2SessionCommand::Request {
19652                headers_json: headers_json.to_owned(),
19653                options_json: options_json.to_owned(),
19654                respond_to,
19655            })
19656        }
19657        "net.http2_session_settings" => {
19658            let session_id = javascript_sync_rpc_arg_u64(
19659                &request.args,
19660                0,
19661                "net.http2_session_settings session id",
19662            )?;
19663            let settings_json = javascript_sync_rpc_arg_str(
19664                &request.args,
19665                1,
19666                "net.http2_session_settings settings",
19667            )?;
19668            let session = http2_session_for_id(process, session_id)?;
19669            send_http2_command(&session, |respond_to| Http2SessionCommand::Settings {
19670                settings_json: settings_json.to_owned(),
19671                respond_to,
19672            })
19673        }
19674        "net.http2_session_set_local_window_size" => {
19675            let session_id = javascript_sync_rpc_arg_u64(
19676                &request.args,
19677                0,
19678                "net.http2_session_set_local_window_size session id",
19679            )?;
19680            let window_size = javascript_sync_rpc_arg_u64(
19681                &request.args,
19682                1,
19683                "net.http2_session_set_local_window_size window size",
19684            )?;
19685            let session = http2_session_for_id(process, session_id)?;
19686            send_http2_command(&session, |respond_to| {
19687                Http2SessionCommand::SetLocalWindowSize {
19688                    size: window_size as u32,
19689                    respond_to,
19690                }
19691            })
19692        }
19693        "net.http2_session_goaway" => {
19694            let session_id = javascript_sync_rpc_arg_u64(
19695                &request.args,
19696                0,
19697                "net.http2_session_goaway session id",
19698            )?;
19699            let error_code = javascript_sync_rpc_arg_u64(
19700                &request.args,
19701                1,
19702                "net.http2_session_goaway error code",
19703            )?;
19704            let last_stream_id = javascript_sync_rpc_arg_u64(
19705                &request.args,
19706                2,
19707                "net.http2_session_goaway last stream id",
19708            )?;
19709            let opaque_data = request
19710                .args
19711                .get(3)
19712                .and_then(Value::as_str)
19713                .map(|value| {
19714                    base64::engine::general_purpose::STANDARD
19715                        .decode(value)
19716                        .map_err(|error| {
19717                            SidecarError::InvalidState(format!("invalid GOAWAY payload: {error}"))
19718                        })
19719                })
19720                .transpose()?;
19721            let session = http2_session_for_id(process, session_id)?;
19722            send_http2_command(&session, |respond_to| Http2SessionCommand::Goaway {
19723                error_code: error_code as u32,
19724                last_stream_id: last_stream_id as u32,
19725                opaque_data,
19726                respond_to,
19727            })
19728        }
19729        "net.http2_session_close" | "net.http2_session_destroy" => {
19730            let session_id = javascript_sync_rpc_arg_u64(
19731                &request.args,
19732                0,
19733                "net.http2_session_close session id",
19734            )?;
19735            let session = http2_session_for_id(process, session_id)?;
19736            send_http2_command(&session, |respond_to| Http2SessionCommand::Close {
19737                abrupt: request.method == "net.http2_session_destroy",
19738                respond_to,
19739            })
19740        }
19741        "net.http2_session_poll" => {
19742            let session_id =
19743                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_session_poll session id")?;
19744            let wait_ms = javascript_sync_rpc_arg_u64_optional(
19745                &request.args,
19746                1,
19747                "net.http2_session_poll wait ms",
19748            )?
19749            .unwrap_or_default();
19750            match wait_for_http2_event(&process.http2.shared, session_id, false, wait_ms) {
19751                Some(event) => http2_event_value(&event),
19752                None => Ok(Value::Null),
19753            }
19754        }
19755        "net.http2_session_wait" => {
19756            let session_id =
19757                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_session_wait session id")?;
19758            dispatch_http2_wait_loop(process, session_id, false)
19759        }
19760        "net.http2_stream_respond" => {
19761            let stream_id = javascript_sync_rpc_arg_u64(
19762                &request.args,
19763                0,
19764                "net.http2_stream_respond stream id",
19765            )?;
19766            let headers_json =
19767                javascript_sync_rpc_arg_str(&request.args, 1, "net.http2_stream_respond headers")?;
19768            let stream = http2_stream_for_id(process, stream_id)?;
19769            let session = http2_session_for_id(process, stream.session_id)?;
19770            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamRespond {
19771                stream_id,
19772                headers_json: headers_json.to_owned(),
19773                respond_to,
19774            })
19775        }
19776        "net.http2_stream_push_stream" => {
19777            let stream_id = javascript_sync_rpc_arg_u64(
19778                &request.args,
19779                0,
19780                "net.http2_stream_push_stream stream id",
19781            )?;
19782            let headers_json = javascript_sync_rpc_arg_str(
19783                &request.args,
19784                1,
19785                "net.http2_stream_push_stream headers",
19786            )?;
19787            let _options_json = javascript_sync_rpc_arg_str(
19788                &request.args,
19789                2,
19790                "net.http2_stream_push_stream options",
19791            )?;
19792            let stream = http2_stream_for_id(process, stream_id)?;
19793            let session = http2_session_for_id(process, stream.session_id)?;
19794            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamPush {
19795                stream_id,
19796                headers_json: headers_json.to_owned(),
19797                respond_to,
19798            })
19799        }
19800        "net.http2_stream_write" => {
19801            let stream_id =
19802                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_write stream id")?;
19803            let chunk =
19804                javascript_sync_rpc_base64_arg(&request.args, 1, "net.http2_stream_write data")?;
19805            let stream = http2_stream_for_id(process, stream_id)?;
19806            let session = http2_session_for_id(process, stream.session_id)?;
19807            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamWrite {
19808                stream_id,
19809                chunk,
19810                end_stream: false,
19811                respond_to,
19812            })
19813        }
19814        "net.http2_stream_end" => {
19815            let stream_id =
19816                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_end stream id")?;
19817            let chunk = request
19818                .args
19819                .get(1)
19820                .and_then(Value::as_str)
19821                .map(|value| {
19822                    base64::engine::general_purpose::STANDARD
19823                        .decode(value)
19824                        .map_err(|error| {
19825                            SidecarError::InvalidState(format!(
19826                                "invalid HTTP/2 stream payload: {error}"
19827                            ))
19828                        })
19829                })
19830                .transpose()?
19831                .unwrap_or_default();
19832            let stream = http2_stream_for_id(process, stream_id)?;
19833            let session = http2_session_for_id(process, stream.session_id)?;
19834            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamWrite {
19835                stream_id,
19836                chunk,
19837                end_stream: true,
19838                respond_to,
19839            })
19840        }
19841        "net.http2_stream_close" => {
19842            let stream_id =
19843                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_close stream id")?;
19844            let code = javascript_sync_rpc_arg_u64_optional(
19845                &request.args,
19846                1,
19847                "net.http2_stream_close error code",
19848            )?
19849            .map(|value| value as u32);
19850            let stream = http2_stream_for_id(process, stream_id)?;
19851            let session = http2_session_for_id(process, stream.session_id)?;
19852            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamClose {
19853                stream_id,
19854                error_code: code,
19855                respond_to,
19856            })
19857        }
19858        "net.http2_stream_pause" => {
19859            let stream_id =
19860                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_pause stream id")?;
19861            let stream = http2_stream_for_id(process, stream_id)?;
19862            stream.paused.store(true, Ordering::SeqCst);
19863            Ok(Value::Null)
19864        }
19865        "net.http2_stream_resume" => {
19866            let stream_id =
19867                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_resume stream id")?;
19868            let stream = http2_stream_for_id(process, stream_id)?;
19869            stream.paused.store(false, Ordering::SeqCst);
19870            Ok(Value::Null)
19871        }
19872        "net.http2_stream_respond_with_file" => {
19873            let stream_id = javascript_sync_rpc_arg_u64(
19874                &request.args,
19875                0,
19876                "net.http2_stream_respond_with_file stream id",
19877            )?;
19878            let path = javascript_sync_rpc_arg_str(
19879                &request.args,
19880                1,
19881                "net.http2_stream_respond_with_file path",
19882            )?;
19883            let headers_json = javascript_sync_rpc_arg_str(
19884                &request.args,
19885                2,
19886                "net.http2_stream_respond_with_file headers",
19887            )?;
19888            let options_json = javascript_sync_rpc_arg_str(
19889                &request.args,
19890                3,
19891                "net.http2_stream_respond_with_file options",
19892            )?;
19893            let stream = http2_stream_for_id(process, stream_id)?;
19894            let session = http2_session_for_id(process, stream.session_id)?;
19895            let guest_path = resolve_http2_file_response_guest_path(process, path);
19896            let body = kernel.read_file(&guest_path).map_err(kernel_error)?;
19897            send_http2_command(&session, |respond_to| {
19898                Http2SessionCommand::StreamRespondWithFile {
19899                    stream_id,
19900                    body,
19901                    headers_json: headers_json.to_owned(),
19902                    options_json: options_json.to_owned(),
19903                    respond_to,
19904                }
19905            })
19906        }
19907        other => Err(SidecarError::InvalidState(format!(
19908            "unsupported JavaScript HTTP/2 sync RPC method {other}"
19909        ))),
19910    }
19911}
19912
19913const JAVASCRIPT_NET_POLL_MAX_WAIT: Duration = Duration::from_millis(50);
19914const EXITED_PROCESS_SNAPSHOT_RETENTION: Duration = Duration::from_secs(2);
19915
19916fn resolve_http2_file_response_guest_path(process: &ActiveProcess, path: &str) -> String {
19917    if Path::new(path).is_absolute() {
19918        normalize_path(path)
19919    } else {
19920        normalize_path(&format!("{}/{}", process.guest_cwd, path))
19921    }
19922}
19923
19924pub(crate) fn clamp_javascript_net_poll_wait(wait_ms: u64) -> Duration {
19925    // WASM net.poll runs on the sidecar's sync-RPC main thread. Guest-controlled waits
19926    // must stay bounded so one VM cannot stall dispose/shutdown or unrelated VM work.
19927    if wait_ms == 0 {
19928        Duration::ZERO
19929    } else {
19930        Duration::from_millis(wait_ms).min(JAVASCRIPT_NET_POLL_MAX_WAIT)
19931    }
19932}
19933
19934pub(crate) fn service_javascript_net_sync_rpc<B>(
19935    request: JavascriptNetSyncRpcServiceRequest<'_, B>,
19936) -> Result<Value, SidecarError>
19937where
19938    B: NativeSidecarBridge + Send + 'static,
19939    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19940{
19941    let JavascriptNetSyncRpcServiceRequest {
19942        bridge,
19943        vm_id,
19944        dns,
19945        socket_paths,
19946        kernel,
19947        process,
19948        sync_request: request,
19949        resource_limits,
19950        network_counts,
19951    } = request;
19952    match request.method.as_str() {
19953        "net.http_listen" => {
19954            check_network_resource_limit(
19955                resource_limits.max_sockets,
19956                network_counts.sockets,
19957                1,
19958                "socket",
19959            )?;
19960            let payload_json =
19961                javascript_sync_rpc_arg_str(&request.args, 0, "net.http_listen payload")?;
19962            let payload: JavascriptHttpListenRequest =
19963                serde_json::from_str(payload_json).map_err(|error| {
19964                    SidecarError::InvalidState(format!(
19965                        "net.http_listen payload must be valid JSON: {error}"
19966                    ))
19967                })?;
19968            let (family, bind_host, guest_host) =
19969                normalize_tcp_listen_host(payload.hostname.as_deref())?;
19970            let requested_port = payload.port.unwrap_or(0);
19971            bridge.require_network_access(
19972                vm_id,
19973                NetworkOperation::Listen,
19974                format_tcp_resource(bind_host, requested_port),
19975            )?;
19976            let port = allocate_guest_listen_port(
19977                requested_port,
19978                family,
19979                &socket_paths.used_tcp_guest_ports,
19980                socket_paths.listen_policy,
19981            )?;
19982            let mut listener = ActiveTcpListener::bind(
19983                bind_host,
19984                guest_host,
19985                port,
19986                Some(DEFAULT_JAVASCRIPT_NET_BACKLOG),
19987            )?;
19988            let guest_local_addr = listener.guest_local_addr();
19989            process.http_servers.insert(
19990                payload.server_id,
19991                ActiveHttpServer {
19992                    listener: listener.listener.take().ok_or_else(|| {
19993                        SidecarError::InvalidState(String::from(
19994                            "HTTP listener missing host TCP socket",
19995                        ))
19996                    })?,
19997                    guest_local_addr,
19998                    next_request_id: 0,
19999                },
20000            );
20001            serde_json::to_string(&json!({
20002                "address": {
20003                    "address": guest_local_addr.ip().to_string(),
20004                    "family": socket_addr_family(&guest_local_addr),
20005                    "port": guest_local_addr.port(),
20006                }
20007            }))
20008            .map(Value::String)
20009            .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
20010        }
20011        "net.http_close" => {
20012            let server_id =
20013                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_close server id")?;
20014            let server = process.http_servers.remove(&server_id).ok_or_else(|| {
20015                SidecarError::InvalidState(format!("unknown HTTP server {server_id}"))
20016            })?;
20017            drop(server.listener);
20018            process
20019                .pending_http_requests
20020                .retain(|(pending_server_id, _), _| *pending_server_id != server_id);
20021            Ok(Value::Null)
20022        }
20023        "net.http_wait" => {
20024            let server_id =
20025                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_wait server id")?;
20026            dispatch_http_wait_loop(process, server_id)
20027        }
20028        "net.http_respond" => {
20029            let server_id =
20030                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_respond server id")?;
20031            let request_id =
20032                javascript_sync_rpc_arg_u64(&request.args, 1, "net.http_respond request id")?;
20033            let response_json =
20034                javascript_sync_rpc_arg_str(&request.args, 2, "net.http_respond payload")?;
20035            ensure_vm_fetch_response_within_limit(
20036                response_json,
20037                "net.http_respond",
20038                VM_FETCH_BUFFER_LIMIT_BYTES,
20039            )?;
20040            serde_json::from_str::<Value>(response_json).map_err(|error| {
20041                SidecarError::Execution(format!(
20042                    "net.http_respond payload must be valid JSON: {error}"
20043                ))
20044            })?;
20045            let Some(pending) = process
20046                .pending_http_requests
20047                .get_mut(&(server_id, request_id))
20048            else {
20049                return Err(SidecarError::InvalidState(format!(
20050                    "unknown pending HTTP request {request_id} for server {server_id}"
20051                )));
20052            };
20053            *pending = Some(response_json.to_owned());
20054            Ok(Value::Null)
20055        }
20056        "net.reserve_tcp_port" => {
20057            let payload = request
20058                .args
20059                .first()
20060                .cloned()
20061                .ok_or_else(|| {
20062                    SidecarError::InvalidState(String::from(
20063                        "net.reserve_tcp_port requires a request payload",
20064                    ))
20065                })
20066                .and_then(|value| {
20067                    serde_json::from_value::<JavascriptNetReserveTcpPortRequest>(value).map_err(
20068                        |error| {
20069                            SidecarError::InvalidState(format!(
20070                                "invalid net.reserve_tcp_port payload: {error}"
20071                            ))
20072                        },
20073                    )
20074                })?;
20075            let (family, _bind_host, guest_host) =
20076                normalize_tcp_listen_host(payload.host.as_deref())?;
20077            let requested_port = payload.port.unwrap_or(0);
20078            let port = allocate_guest_listen_port(
20079                requested_port,
20080                family,
20081                &socket_paths.used_tcp_guest_ports,
20082                socket_paths.listen_policy,
20083            )?;
20084            let reservation_id = process.allocate_tcp_port_reservation_id();
20085            process
20086                .tcp_port_reservations
20087                .insert(reservation_id.clone(), (family, port));
20088            Ok(json!({
20089                "reservationId": reservation_id,
20090                "localAddress": guest_host,
20091                "localPort": port,
20092                "family": match family {
20093                    JavascriptSocketFamily::Ipv4 => "IPv4",
20094                    JavascriptSocketFamily::Ipv6 => "IPv6",
20095                },
20096            }))
20097        }
20098        "net.release_tcp_port" => {
20099            let reservation_id =
20100                javascript_sync_rpc_arg_str(&request.args, 0, "net.release_tcp_port reservation")?;
20101            process.tcp_port_reservations.remove(reservation_id);
20102            Ok(Value::Null)
20103        }
20104        "net.connect" => {
20105            check_network_resource_limit(
20106                resource_limits.max_sockets,
20107                network_counts.sockets,
20108                1,
20109                "socket",
20110            )?;
20111            check_network_resource_limit(
20112                resource_limits.max_connections,
20113                network_counts.connections,
20114                1,
20115                "connection",
20116            )?;
20117            let payload = request
20118                .args
20119                .first()
20120                .cloned()
20121                .ok_or_else(|| {
20122                    SidecarError::InvalidState(String::from(
20123                        "net.connect requires a request payload",
20124                    ))
20125                })
20126                .and_then(|value| {
20127                    serde_json::from_value::<JavascriptNetConnectRequest>(value).map_err(|error| {
20128                        SidecarError::InvalidState(format!("invalid net.connect payload: {error}"))
20129                    })
20130                })?;
20131            if let Some(path) = payload.path.as_deref() {
20132                let guest_path = normalize_path(path);
20133                let host_path = resolve_guest_socket_host_path(socket_paths, &guest_path);
20134                let socket = ActiveUnixSocket::connect(&host_path, &guest_path)?;
20135                let socket_id = process.allocate_unix_socket_id();
20136                process.unix_sockets.insert(socket_id.clone(), socket);
20137                Ok(json!({
20138                    "socketId": socket_id,
20139                    "remotePath": guest_path,
20140                }))
20141            } else {
20142                let port = payload.port.ok_or_else(|| {
20143                    SidecarError::InvalidState(String::from(
20144                        "net.connect requires either a path or port",
20145                    ))
20146                })?;
20147                let host = payload.host.as_deref().unwrap_or("localhost");
20148                let local_reservation = payload.local_reservation.as_deref().and_then(|id| {
20149                    process
20150                        .tcp_port_reservations
20151                        .remove(id)
20152                        .map(|reservation| (id.to_owned(), reservation))
20153                });
20154                bridge.require_network_access(
20155                    vm_id,
20156                    NetworkOperation::Http,
20157                    format_tcp_resource(host, port),
20158                )?;
20159                if is_loopback_socket_host(host) {
20160                    let families = [JavascriptSocketFamily::Ipv4, JavascriptSocketFamily::Ipv6];
20161                    if let Some((family, target)) = families.iter().find_map(|family| {
20162                        socket_paths
20163                            .http_loopback_target(*family, port)
20164                            .map(|target| (*family, target))
20165                    }) {
20166                        if let Some((reservation_id, reservation)) = local_reservation {
20167                            process
20168                                .tcp_port_reservations
20169                                .insert(reservation_id, reservation);
20170                        }
20171                        let remote_address = match family {
20172                            JavascriptSocketFamily::Ipv4 => "127.0.0.1",
20173                            JavascriptSocketFamily::Ipv6 => "::1",
20174                        };
20175                        return Ok(json!({
20176                            "loopbackHttpTarget": {
20177                                "processId": target.process_id.clone(),
20178                                "serverId": target.server_id,
20179                                "host": remote_address,
20180                                "port": port,
20181                            },
20182                            "localAddress": match family {
20183                                JavascriptSocketFamily::Ipv4 => "127.0.0.1",
20184                                JavascriptSocketFamily::Ipv6 => "::1",
20185                            },
20186                            "localPort": payload.local_port.unwrap_or(0),
20187                            "remoteAddress": remote_address,
20188                            "remotePort": port,
20189                            "remoteFamily": match family {
20190                                JavascriptSocketFamily::Ipv4 => "IPv4",
20191                                JavascriptSocketFamily::Ipv6 => "IPv6",
20192                            },
20193                        }));
20194                    }
20195                }
20196                let connect_result = ActiveTcpSocket::connect(ActiveTcpConnectRequest {
20197                    bridge,
20198                    kernel,
20199                    kernel_pid: process.kernel_pid,
20200                    vm_id,
20201                    dns,
20202                    host,
20203                    port,
20204                    local_address: payload.local_address.as_deref(),
20205                    local_port: payload.local_port,
20206                    local_reservation: local_reservation
20207                        .as_ref()
20208                        .map(|(_, reservation)| *reservation),
20209                    context: socket_paths,
20210                });
20211                if let Err(error) = connect_result {
20212                    if let Some((reservation_id, reservation)) = local_reservation {
20213                        process
20214                            .tcp_port_reservations
20215                            .insert(reservation_id, reservation);
20216                    }
20217                    return Err(error);
20218                }
20219                let socket = connect_result?;
20220                let socket_id = process.allocate_tcp_socket_id();
20221                let local_addr = socket.guest_local_addr;
20222                let remote_addr = socket.guest_remote_addr;
20223                process.tcp_sockets.insert(socket_id.clone(), socket);
20224                Ok(json!({
20225                    "socketId": socket_id,
20226                    "localAddress": local_addr.ip().to_string(),
20227                    "localPort": local_addr.port(),
20228                    "remoteAddress": remote_addr.ip().to_string(),
20229                    "remotePort": remote_addr.port(),
20230                    "remoteFamily": socket_addr_family(&remote_addr),
20231                }))
20232            }
20233        }
20234        "net.listen" => {
20235            check_network_resource_limit(
20236                resource_limits.max_sockets,
20237                network_counts.sockets,
20238                1,
20239                "socket",
20240            )?;
20241            let payload = request
20242                .args
20243                .first()
20244                .cloned()
20245                .ok_or_else(|| {
20246                    SidecarError::InvalidState(String::from(
20247                        "net.listen requires a request payload",
20248                    ))
20249                })
20250                .and_then(|value| match value {
20251                    Value::String(json) => {
20252                        serde_json::from_str::<JavascriptNetListenRequest>(&json).map_err(|error| {
20253                            SidecarError::InvalidState(format!(
20254                                "invalid net.listen payload: {error}"
20255                            ))
20256                        })
20257                    }
20258                    other => serde_json::from_value::<JavascriptNetListenRequest>(other).map_err(
20259                        |error| {
20260                            SidecarError::InvalidState(format!(
20261                                "invalid net.listen payload: {error}"
20262                            ))
20263                        },
20264                    ),
20265                })?;
20266            if let Some(path) = payload.path.as_deref() {
20267                let guest_path = normalize_path(path);
20268                if kernel.exists(&guest_path).map_err(kernel_error)? {
20269                    return Err(sidecar_net_error(std::io::Error::from_raw_os_error(
20270                        libc::EADDRINUSE,
20271                    )));
20272                }
20273
20274                let host_path = resolve_guest_socket_host_path(socket_paths, &guest_path);
20275                let on_host_mount =
20276                    host_mount_path_for_guest_path_from_mounts(&socket_paths.mounts, &guest_path)
20277                        .is_some();
20278                let listener = ActiveUnixListener::bind(&host_path, &guest_path, payload.backlog)?;
20279                if !on_host_mount {
20280                    ensure_kernel_parent_directories(kernel, &guest_path)?;
20281                    kernel
20282                        .write_file(&guest_path, Vec::new())
20283                        .map_err(kernel_error)?;
20284                }
20285                let listener_id = process.allocate_unix_listener_id();
20286                process.unix_listeners.insert(listener_id.clone(), listener);
20287                Ok(json!({
20288                    "serverId": listener_id,
20289                    "path": guest_path,
20290                }))
20291            } else {
20292                let (family, bind_host, guest_host) =
20293                    normalize_tcp_listen_host(payload.host.as_deref())?;
20294                let requested_port = payload.port.unwrap_or(0);
20295                bridge.require_network_access(
20296                    vm_id,
20297                    NetworkOperation::Listen,
20298                    format_tcp_resource(bind_host, requested_port),
20299                )?;
20300                let local_reservation = payload.local_reservation.as_deref().and_then(|id| {
20301                    process
20302                        .tcp_port_reservations
20303                        .remove(id)
20304                        .map(|reservation| (id.to_owned(), reservation))
20305                });
20306                let port = if requested_port != 0
20307                    && local_reservation
20308                        .as_ref()
20309                        .map(|(_, reservation)| *reservation)
20310                        == Some((family, requested_port))
20311                {
20312                    requested_port
20313                } else {
20314                    allocate_guest_listen_port(
20315                        requested_port,
20316                        family,
20317                        &socket_paths.used_tcp_guest_ports,
20318                        socket_paths.listen_policy,
20319                    )?
20320                };
20321                let listener_result = ActiveTcpListener::bind_kernel(
20322                    kernel,
20323                    process.kernel_pid,
20324                    guest_host,
20325                    port,
20326                    payload.backlog,
20327                );
20328                if let Err(error) = listener_result {
20329                    if let Some((reservation_id, reservation)) = local_reservation {
20330                        process
20331                            .tcp_port_reservations
20332                            .insert(reservation_id, reservation);
20333                    }
20334                    return Err(error);
20335                }
20336                let listener = listener_result?;
20337                let listener_id = process.allocate_tcp_listener_id();
20338                let local_addr = listener.guest_local_addr();
20339                process.tcp_listeners.insert(listener_id.clone(), listener);
20340                Ok(json!({
20341                    "serverId": listener_id,
20342                    "localAddress": local_addr.ip().to_string(),
20343                    "localPort": local_addr.port(),
20344                    "family": socket_addr_family(&local_addr),
20345                }))
20346            }
20347        }
20348        "net.poll" => {
20349            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.poll socket id")?;
20350            let wait_ms =
20351                javascript_sync_rpc_arg_u64_optional(&request.args, 1, "net.poll wait ms")?
20352                    .unwrap_or_default();
20353            let wait = clamp_javascript_net_poll_wait(wait_ms);
20354            let event = if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20355                socket.poll(kernel, process.kernel_pid, wait)?
20356            } else if let Some(socket) = process.unix_sockets.get_mut(socket_id) {
20357                socket.poll(wait)?
20358            } else {
20359                return Err(SidecarError::InvalidState(format!(
20360                    "unknown net socket {socket_id}"
20361                )));
20362            };
20363
20364            match event {
20365                Some(JavascriptTcpSocketEvent::Data(chunk)) => Ok(json!({
20366                    "type": "data",
20367                    "data": javascript_sync_rpc_bytes_value(&chunk),
20368                })),
20369                Some(JavascriptTcpSocketEvent::End) => Ok(json!({
20370                    "type": "end",
20371                })),
20372                Some(JavascriptTcpSocketEvent::Error { code, message }) => Ok(json!({
20373                    "type": "error",
20374                    "code": code,
20375                    "message": message,
20376                })),
20377                Some(JavascriptTcpSocketEvent::Close { had_error }) => {
20378                    if let Some(socket) = process.tcp_sockets.remove(socket_id) {
20379                        if let Some(listener_id) = socket.listener_id.as_deref() {
20380                            if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20381                                listener.release_connection(socket_id);
20382                            }
20383                        }
20384                    } else if let Some(socket) = process.unix_sockets.remove(socket_id) {
20385                        if let Some(listener_id) = socket.listener_id.as_deref() {
20386                            if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20387                                listener.release_connection(socket_id);
20388                            }
20389                        }
20390                    }
20391                    Ok(json!({
20392                        "type": "close",
20393                        "hadError": had_error,
20394                    }))
20395                }
20396                None => Ok(Value::Null),
20397            }
20398        }
20399        "net.socket_wait_connect" => {
20400            let socket_id =
20401                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_wait_connect socket id")?;
20402            if let Some(socket) = process.tcp_sockets.get(socket_id) {
20403                javascript_net_json_string(socket.socket_info(), "net.socket_wait_connect")
20404            } else {
20405                let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
20406                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20407                })?;
20408                javascript_net_json_string(socket.socket_info(), "net.socket_wait_connect")
20409            }
20410        }
20411        "net.socket_read" => {
20412            let socket_id =
20413                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_read socket id")?;
20414            if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20415                javascript_net_read_value(socket.poll(
20416                    kernel,
20417                    process.kernel_pid,
20418                    Duration::ZERO,
20419                )?)
20420            } else {
20421                let socket = process.unix_sockets.get_mut(socket_id).ok_or_else(|| {
20422                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20423                })?;
20424                javascript_net_read_value(socket.poll(Duration::ZERO)?)
20425            }
20426        }
20427        "net.socket_set_no_delay" => {
20428            let socket_id =
20429                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_set_no_delay socket id")?;
20430            let enable =
20431                javascript_sync_rpc_arg_bool(&request.args, 1, "net.socket_set_no_delay enabled")?;
20432            if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20433                socket.set_no_delay(enable)?;
20434            } else if !process.unix_sockets.contains_key(socket_id) {
20435                return Err(SidecarError::InvalidState(format!(
20436                    "unknown net socket {socket_id}"
20437                )));
20438            }
20439            Ok(Value::Null)
20440        }
20441        "net.socket_set_keep_alive" => {
20442            let socket_id = javascript_sync_rpc_arg_str(
20443                &request.args,
20444                0,
20445                "net.socket_set_keep_alive socket id",
20446            )?;
20447            let enable = javascript_sync_rpc_arg_bool(
20448                &request.args,
20449                1,
20450                "net.socket_set_keep_alive enabled",
20451            )?;
20452            let initial_delay_secs = javascript_sync_rpc_arg_u64_optional(
20453                &request.args,
20454                2,
20455                "net.socket_set_keep_alive initial delay seconds",
20456            )?;
20457            if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20458                socket.set_keep_alive(enable, initial_delay_secs)?;
20459            } else if !process.unix_sockets.contains_key(socket_id) {
20460                return Err(SidecarError::InvalidState(format!(
20461                    "unknown net socket {socket_id}"
20462                )));
20463            }
20464            Ok(Value::Null)
20465        }
20466        "net.socket_upgrade_tls" => {
20467            let socket_id =
20468                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_upgrade_tls socket id")?;
20469            let options_json =
20470                javascript_sync_rpc_arg_str(&request.args, 1, "net.socket_upgrade_tls options")?;
20471            let options: JavascriptTlsBridgeOptions =
20472                serde_json::from_str(options_json).map_err(|error| {
20473                    SidecarError::InvalidState(format!(
20474                        "net.socket_upgrade_tls options must be valid JSON: {error}"
20475                    ))
20476                })?;
20477            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20478                SidecarError::InvalidState(format!(
20479                    "unknown TCP socket {socket_id} for TLS upgrade"
20480                ))
20481            })?;
20482            socket.upgrade_tls(vm_id, kernel, options)?;
20483            Ok(Value::Null)
20484        }
20485        "net.socket_get_tls_client_hello" => {
20486            let socket_id = javascript_sync_rpc_arg_str(
20487                &request.args,
20488                0,
20489                "net.socket_get_tls_client_hello socket id",
20490            )?;
20491            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20492                SidecarError::InvalidState(format!(
20493                    "unknown TCP socket {socket_id} for TLS client hello query"
20494                ))
20495            })?;
20496            socket.tls_client_hello_json(vm_id, kernel)
20497        }
20498        "net.socket_tls_query" => {
20499            let socket_id =
20500                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_tls_query socket id")?;
20501            let query =
20502                javascript_sync_rpc_arg_str(&request.args, 1, "net.socket_tls_query query")?;
20503            let detailed = request
20504                .args
20505                .get(2)
20506                .and_then(Value::as_bool)
20507                .unwrap_or(false);
20508            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20509                SidecarError::InvalidState(format!("unknown TCP socket {socket_id} for TLS query"))
20510            })?;
20511            socket.tls_query(query, detailed)
20512        }
20513        "net.server_poll" => {
20514            let listener_id =
20515                javascript_sync_rpc_arg_str(&request.args, 0, "net.server_poll listener id")?;
20516            let wait_ms =
20517                javascript_sync_rpc_arg_u64_optional(&request.args, 1, "net.server_poll wait ms")?
20518                    .unwrap_or_default();
20519            let tcp_event = if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20520                Some(listener.poll(kernel, process.kernel_pid, Duration::from_millis(wait_ms))?)
20521            } else {
20522                None
20523            };
20524
20525            if let Some(event) = tcp_event {
20526                return match event {
20527                    Some(JavascriptTcpListenerEvent::Connection(pending)) => {
20528                        let PendingTcpSocket {
20529                            stream,
20530                            kernel_socket_id,
20531                            preallocated,
20532                            guest_local_addr,
20533                            guest_remote_addr,
20534                        } = pending;
20535                        if !preallocated {
20536                            if let Err(error) = check_network_resource_limit(
20537                                resource_limits.max_sockets,
20538                                network_counts.sockets,
20539                                1,
20540                                "socket",
20541                            )
20542                            .and_then(|()| {
20543                                check_network_resource_limit(
20544                                    resource_limits.max_connections,
20545                                    network_counts.connections,
20546                                    1,
20547                                    "connection",
20548                                )
20549                            }) {
20550                                if let Some(stream) = stream {
20551                                    let _ = stream.shutdown(Shutdown::Both);
20552                                }
20553                                return Ok(json!({
20554                                    "type": "error",
20555                                    "code": "EAGAIN",
20556                                    "message": error.to_string(),
20557                                }));
20558                            }
20559                        }
20560                        let socket = if let Some(stream) = stream {
20561                            ActiveTcpSocket::from_stream(
20562                                stream,
20563                                Some(listener_id.to_string()),
20564                                guest_local_addr,
20565                                guest_remote_addr,
20566                            )?
20567                        } else {
20568                            ActiveTcpSocket::from_kernel(
20569                                kernel_socket_id.ok_or_else(|| {
20570                                    SidecarError::InvalidState(String::from(
20571                                        "kernel TCP accept missing socket id",
20572                                    ))
20573                                })?,
20574                                Some(listener_id.to_string()),
20575                                guest_local_addr,
20576                                guest_remote_addr,
20577                            )
20578                        };
20579                        let socket_id = process.allocate_tcp_socket_id();
20580                        if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20581                            listener.register_connection(&socket_id);
20582                        }
20583                        process.tcp_sockets.insert(socket_id.clone(), socket);
20584                        Ok(json!({
20585                            "type": "connection",
20586                            "socketId": socket_id,
20587                            "localAddress": guest_local_addr.ip().to_string(),
20588                            "localPort": guest_local_addr.port(),
20589                            "remoteAddress": guest_remote_addr.ip().to_string(),
20590                            "remotePort": guest_remote_addr.port(),
20591                            "remoteFamily": socket_addr_family(&guest_remote_addr),
20592                        }))
20593                    }
20594                    Some(JavascriptTcpListenerEvent::Error { code, message }) => Ok(json!({
20595                        "type": "error",
20596                        "code": code,
20597                        "message": message,
20598                    })),
20599                    None => Ok(Value::Null),
20600                };
20601            }
20602
20603            let event = {
20604                let listener = process.unix_listeners.get_mut(listener_id).ok_or_else(|| {
20605                    SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20606                })?;
20607                listener.poll(Duration::from_millis(wait_ms))?
20608            };
20609
20610            match event {
20611                Some(JavascriptUnixListenerEvent::Connection(pending)) => {
20612                    if let Err(error) = check_network_resource_limit(
20613                        resource_limits.max_sockets,
20614                        network_counts.sockets,
20615                        1,
20616                        "socket",
20617                    )
20618                    .and_then(|()| {
20619                        check_network_resource_limit(
20620                            resource_limits.max_connections,
20621                            network_counts.connections,
20622                            1,
20623                            "connection",
20624                        )
20625                    }) {
20626                        let _ = pending.stream.shutdown(Shutdown::Both);
20627                        return Ok(json!({
20628                            "type": "error",
20629                            "code": "EAGAIN",
20630                            "message": error.to_string(),
20631                        }));
20632                    }
20633                    let socket = ActiveUnixSocket::from_stream(
20634                        pending.stream,
20635                        Some(listener_id.to_string()),
20636                        pending.local_path.clone(),
20637                        pending.remote_path.clone(),
20638                    )?;
20639                    let socket_id = process.allocate_unix_socket_id();
20640                    if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20641                        listener.register_connection(&socket_id);
20642                    }
20643                    process.unix_sockets.insert(socket_id.clone(), socket);
20644                    Ok(json!({
20645                        "type": "connection",
20646                        "socketId": socket_id,
20647                        "localPath": pending.local_path,
20648                        "remotePath": pending.remote_path,
20649                    }))
20650                }
20651                Some(JavascriptUnixListenerEvent::Error { code, message }) => Ok(json!({
20652                    "type": "error",
20653                    "code": code,
20654                    "message": message,
20655                })),
20656                None => Ok(Value::Null),
20657            }
20658        }
20659        "net.server_accept" => {
20660            let listener_id =
20661                javascript_sync_rpc_arg_str(&request.args, 0, "net.server_accept listener id")?;
20662            if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20663                return match listener.poll(kernel, process.kernel_pid, Duration::ZERO)? {
20664                    Some(JavascriptTcpListenerEvent::Connection(pending)) => {
20665                        let PendingTcpSocket {
20666                            stream,
20667                            kernel_socket_id,
20668                            preallocated,
20669                            guest_local_addr,
20670                            guest_remote_addr,
20671                        } = pending;
20672                        if !preallocated {
20673                            check_network_resource_limit(
20674                                resource_limits.max_sockets,
20675                                network_counts.sockets,
20676                                1,
20677                                "socket",
20678                            )?;
20679                            check_network_resource_limit(
20680                                resource_limits.max_connections,
20681                                network_counts.connections,
20682                                1,
20683                                "connection",
20684                            )?;
20685                        }
20686                        let info = json!({
20687                            "localAddress": guest_local_addr.ip().to_string(),
20688                            "localPort": guest_local_addr.port(),
20689                            "localFamily": socket_addr_family(&guest_local_addr),
20690                            "remoteAddress": guest_remote_addr.ip().to_string(),
20691                            "remotePort": guest_remote_addr.port(),
20692                            "remoteFamily": socket_addr_family(&guest_remote_addr),
20693                        });
20694                        let socket = if let Some(stream) = stream {
20695                            ActiveTcpSocket::from_stream(
20696                                stream,
20697                                Some(listener_id.to_string()),
20698                                guest_local_addr,
20699                                guest_remote_addr,
20700                            )?
20701                        } else {
20702                            ActiveTcpSocket::from_kernel(
20703                                kernel_socket_id.ok_or_else(|| {
20704                                    SidecarError::InvalidState(String::from(
20705                                        "kernel TCP accept missing socket id",
20706                                    ))
20707                                })?,
20708                                Some(listener_id.to_string()),
20709                                guest_local_addr,
20710                                guest_remote_addr,
20711                            )
20712                        };
20713                        let socket_id = process.allocate_tcp_socket_id();
20714                        if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20715                            listener.register_connection(&socket_id);
20716                        }
20717                        process.tcp_sockets.insert(socket_id.clone(), socket);
20718                        javascript_net_json_string(
20719                            json!({
20720                                "socketId": socket_id,
20721                                "info": info,
20722                            }),
20723                            "net.server_accept",
20724                        )
20725                    }
20726                    Some(JavascriptTcpListenerEvent::Error { code, message }) => {
20727                        let detail = code.unwrap_or_else(|| String::from("server accept"));
20728                        Err(SidecarError::Execution(format!("{detail}: {message}")))
20729                    }
20730                    None => Ok(javascript_net_timeout_value()),
20731                };
20732            }
20733
20734            let listener = process.unix_listeners.get_mut(listener_id).ok_or_else(|| {
20735                SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20736            })?;
20737            match listener.poll(Duration::ZERO)? {
20738                Some(JavascriptUnixListenerEvent::Connection(pending)) => {
20739                    check_network_resource_limit(
20740                        resource_limits.max_sockets,
20741                        network_counts.sockets,
20742                        1,
20743                        "socket",
20744                    )?;
20745                    check_network_resource_limit(
20746                        resource_limits.max_connections,
20747                        network_counts.connections,
20748                        1,
20749                        "connection",
20750                    )?;
20751                    let info = json!({
20752                        "localPath": pending.local_path.clone(),
20753                        "remotePath": pending.remote_path.clone(),
20754                    });
20755                    let socket = ActiveUnixSocket::from_stream(
20756                        pending.stream,
20757                        Some(listener_id.to_string()),
20758                        pending.local_path,
20759                        pending.remote_path,
20760                    )?;
20761                    let socket_id = process.allocate_unix_socket_id();
20762                    if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20763                        listener.register_connection(&socket_id);
20764                    }
20765                    process.unix_sockets.insert(socket_id.clone(), socket);
20766                    javascript_net_json_string(
20767                        json!({
20768                            "socketId": socket_id,
20769                            "info": info,
20770                        }),
20771                        "net.server_accept",
20772                    )
20773                }
20774                Some(JavascriptUnixListenerEvent::Error { code, message }) => {
20775                    let detail = code.unwrap_or_else(|| String::from("server accept"));
20776                    Err(SidecarError::Execution(format!("{detail}: {message}")))
20777                }
20778                None => Ok(javascript_net_timeout_value()),
20779            }
20780        }
20781        "net.server_connections" => {
20782            let listener_id = javascript_sync_rpc_arg_str(
20783                &request.args,
20784                0,
20785                "net.server_connections listener id",
20786            )?;
20787            if let Some(listener) = process.tcp_listeners.get(listener_id) {
20788                Ok(json!(listener.active_connection_count()))
20789            } else {
20790                let listener = process.unix_listeners.get(listener_id).ok_or_else(|| {
20791                    SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20792                })?;
20793                Ok(json!(listener.active_connection_count()))
20794            }
20795        }
20796        "net.upgrade_socket_write" => {
20797            let socket_id = javascript_sync_rpc_arg_str(
20798                &request.args,
20799                0,
20800                "net.upgrade_socket_write socket id",
20801            )?;
20802            let chunk =
20803                javascript_sync_rpc_base64_arg(&request.args, 1, "net.upgrade_socket_write chunk")?;
20804            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20805                SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
20806            })?;
20807            socket
20808                .write_all(kernel, process.kernel_pid, &chunk)
20809                .map(|written| json!(written))
20810        }
20811        "net.upgrade_socket_end" => {
20812            let socket_id =
20813                javascript_sync_rpc_arg_str(&request.args, 0, "net.upgrade_socket_end socket id")?;
20814            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20815                SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
20816            })?;
20817            socket.shutdown_write(kernel, process.kernel_pid)?;
20818            Ok(Value::Null)
20819        }
20820        "net.upgrade_socket_destroy" => {
20821            let socket_id = javascript_sync_rpc_arg_str(
20822                &request.args,
20823                0,
20824                "net.upgrade_socket_destroy socket id",
20825            )?;
20826            let socket = process.tcp_sockets.remove(socket_id).ok_or_else(|| {
20827                SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
20828            })?;
20829            if let Some(listener_id) = socket.listener_id.as_deref() {
20830                if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20831                    listener.release_connection(socket_id);
20832                }
20833            }
20834            let _ = socket.close(kernel, process.kernel_pid);
20835            Ok(Value::Null)
20836        }
20837        "net.write" => {
20838            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.write socket id")?;
20839            let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "net.write chunk")?;
20840            if let Some(socket) = process.tcp_sockets.get(socket_id) {
20841                socket
20842                    .write_all(kernel, process.kernel_pid, &chunk)
20843                    .map(|written| json!(written))
20844            } else {
20845                let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
20846                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20847                })?;
20848                socket.write_all(&chunk).map(|written| json!(written))
20849            }
20850        }
20851        "net.shutdown" => {
20852            let socket_id =
20853                javascript_sync_rpc_arg_str(&request.args, 0, "net.shutdown socket id")?;
20854            if let Some(socket) = process.tcp_sockets.get(socket_id) {
20855                socket.shutdown_write(kernel, process.kernel_pid)?;
20856            } else {
20857                let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
20858                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20859                })?;
20860                socket.shutdown_write()?;
20861            }
20862            Ok(Value::Null)
20863        }
20864        "net.destroy" => {
20865            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.destroy socket id")?;
20866            if let Some(socket) = process.tcp_sockets.remove(socket_id) {
20867                if let Some(listener_id) = socket.listener_id.as_deref() {
20868                    if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20869                        listener.release_connection(socket_id);
20870                    }
20871                }
20872                let _ = socket.close(kernel, process.kernel_pid);
20873                Ok(Value::Null)
20874            } else {
20875                let socket = process.unix_sockets.remove(socket_id).ok_or_else(|| {
20876                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20877                })?;
20878                if let Some(listener_id) = socket.listener_id.as_deref() {
20879                    if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20880                        listener.release_connection(socket_id);
20881                    }
20882                }
20883                let _ = socket.close();
20884                Ok(Value::Null)
20885            }
20886        }
20887        "net.server_close" => {
20888            let listener_id =
20889                javascript_sync_rpc_arg_str(&request.args, 0, "net.server_close listener id")?;
20890            if let Some(listener) = process.tcp_listeners.remove(listener_id) {
20891                listener.close(kernel, process.kernel_pid)?;
20892                Ok(Value::Null)
20893            } else {
20894                let listener = process.unix_listeners.remove(listener_id).ok_or_else(|| {
20895                    SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20896                })?;
20897                listener.close()?;
20898                Ok(Value::Null)
20899            }
20900        }
20901        "tls.get_ciphers" => javascript_net_json_string(
20902            Value::Array(
20903                tls_provider()
20904                    .cipher_suites
20905                    .iter()
20906                    .filter_map(|suite| {
20907                        suite
20908                            .suite()
20909                            .as_str()
20910                            .map(|value| Value::String(value.to_owned()))
20911                    })
20912                    .collect(),
20913            ),
20914            "tls.get_ciphers",
20915        ),
20916        _ => Err(SidecarError::InvalidState(format!(
20917            "unsupported JavaScript net sync RPC method {}",
20918            request.method
20919        ))),
20920    }
20921}
20922
20923fn signal_name_for_stream_event(signal: i32) -> Option<&'static str> {
20924    match signal {
20925        libc::SIGHUP => Some("SIGHUP"),
20926        libc::SIGINT => Some("SIGINT"),
20927        libc::SIGUSR1 => Some("SIGUSR1"),
20928        libc::SIGALRM => Some("SIGALRM"),
20929        libc::SIGCONT => Some("SIGCONT"),
20930        libc::SIGTERM => Some("SIGTERM"),
20931        libc::SIGCHLD => Some("SIGCHLD"),
20932        libc::SIGWINCH => Some("SIGWINCH"),
20933        _ => None,
20934    }
20935}
20936
20937pub(crate) fn canonical_signal_name(signal: i32) -> Option<&'static str> {
20938    match signal {
20939        1 => Some("SIGHUP"),
20940        2 => Some("SIGINT"),
20941        3 => Some("SIGQUIT"),
20942        4 => Some("SIGILL"),
20943        5 => Some("SIGTRAP"),
20944        6 => Some("SIGABRT"),
20945        7 => Some("SIGBUS"),
20946        8 => Some("SIGFPE"),
20947        9 => Some("SIGKILL"),
20948        10 => Some("SIGUSR1"),
20949        11 => Some("SIGSEGV"),
20950        12 => Some("SIGUSR2"),
20951        13 => Some("SIGPIPE"),
20952        14 => Some("SIGALRM"),
20953        15 => Some("SIGTERM"),
20954        17 => Some("SIGCHLD"),
20955        18 => Some("SIGCONT"),
20956        19 => Some("SIGSTOP"),
20957        20 => Some("SIGTSTP"),
20958        21 => Some("SIGTTIN"),
20959        22 => Some("SIGTTOU"),
20960        23 => Some("SIGURG"),
20961        24 => Some("SIGXCPU"),
20962        25 => Some("SIGXFSZ"),
20963        26 => Some("SIGVTALRM"),
20964        27 => Some("SIGPROF"),
20965        28 => Some("SIGWINCH"),
20966        29 => Some("SIGIO"),
20967        30 => Some("SIGPWR"),
20968        31 => Some("SIGSYS"),
20969        _ => None,
20970    }
20971}
20972
20973fn dispatch_v8_process_signal(process: &ActiveProcess, signal: i32) -> Result<bool, SidecarError> {
20974    let Some(signal_name) = signal_name_for_stream_event(signal) else {
20975        return Ok(false);
20976    };
20977    process.execution.send_javascript_stream_event(
20978        "signal",
20979        json!({
20980            "signal": signal_name,
20981            "number": signal,
20982            "action": "default",
20983        }),
20984    )?;
20985    Ok(true)
20986}
20987
20988fn dispatch_v8_session_signal_async(session: V8SessionHandle, signal: i32) {
20989    let Some(signal_name) = signal_name_for_stream_event(signal).map(str::to_owned) else {
20990        return;
20991    };
20992    thread::spawn(move || {
20993        thread::sleep(Duration::from_millis(1));
20994        let payload = v8_runtime::json_to_cbor_payload(&json!({
20995            "signal": signal_name,
20996            "number": signal,
20997            "action": "default",
20998        }))
20999        .unwrap_or_default();
21000        let _ = session.send_stream_event("signal", payload);
21001    });
21002}
21003
21004pub(crate) fn parse_signal(signal: &str) -> Result<i32, SidecarError> {
21005    let trimmed = signal.trim();
21006    if trimmed.is_empty() {
21007        return Err(SidecarError::InvalidState(String::from(
21008            "kill_process requires a non-empty signal",
21009        )));
21010    }
21011
21012    if let Ok(value) = trimmed.parse::<i32>() {
21013        return match value {
21014            0..=31 => Ok(value),
21015            _ => Err(SidecarError::InvalidState(format!(
21016                "unsupported kill_process signal {signal}"
21017            ))),
21018        };
21019    }
21020
21021    let upper = trimmed.to_ascii_uppercase();
21022    let normalized = upper.strip_prefix("SIG").unwrap_or(&upper);
21023
21024    signal_number_from_name(normalized).ok_or_else(|| {
21025        SidecarError::InvalidState(format!("unsupported kill_process signal {signal}"))
21026    })
21027}
21028
21029fn signal_number_from_name(signal: &str) -> Option<i32> {
21030    match signal {
21031        "0" => Some(0),
21032        "HUP" => Some(1),
21033        "INT" => Some(2),
21034        "QUIT" => Some(3),
21035        "ILL" => Some(4),
21036        "TRAP" => Some(5),
21037        "ABRT" | "IOT" => Some(6),
21038        "BUS" => Some(7),
21039        "FPE" => Some(8),
21040        "KILL" => Some(9),
21041        "USR1" => Some(10),
21042        "SEGV" => Some(11),
21043        "USR2" => Some(12),
21044        "PIPE" => Some(13),
21045        "ALRM" => Some(14),
21046        "TERM" => Some(15),
21047        "STKFLT" => Some(16),
21048        "CHLD" => Some(17),
21049        "CONT" => Some(18),
21050        "STOP" => Some(19),
21051        "TSTP" => Some(20),
21052        "TTIN" => Some(21),
21053        "TTOU" => Some(22),
21054        "URG" => Some(23),
21055        "XCPU" => Some(24),
21056        "XFSZ" => Some(25),
21057        "VTALRM" => Some(26),
21058        "PROF" => Some(27),
21059        "WINCH" => Some(28),
21060        "IO" | "POLL" => Some(29),
21061        "PWR" => Some(30),
21062        "SYS" => Some(31),
21063        _ => None,
21064    }
21065}
21066
21067pub(crate) fn runtime_child_is_alive(child_pid: u32) -> Result<bool, SidecarError> {
21068    Ok(runtime_child_exit_status(child_pid)?.is_none())
21069}
21070
21071#[cfg(not(target_os = "macos"))]
21072fn runtime_child_exit_status(child_pid: u32) -> Result<Option<i32>, SidecarError> {
21073    if child_pid == 0 {
21074        return Ok(Some(0));
21075    }
21076
21077    let wait_flags = WaitPidFlag::WNOHANG
21078        | WaitPidFlag::WNOWAIT
21079        | WaitPidFlag::WEXITED
21080        | WaitPidFlag::WUNTRACED
21081        | WaitPidFlag::WCONTINUED;
21082    match wait_on_child(WaitId::Pid(Pid::from_raw(child_pid as i32)), wait_flags) {
21083        Ok(WaitStatus::StillAlive)
21084        | Ok(WaitStatus::Stopped(_, _))
21085        | Ok(WaitStatus::Continued(_)) => Ok(None),
21086        Ok(WaitStatus::Exited(_, status)) => Ok(Some(status)),
21087        Ok(WaitStatus::Signaled(_, signal, _)) => Ok(Some(128 + signal as i32)),
21088        #[cfg(any(target_os = "linux", target_os = "android"))]
21089        Ok(WaitStatus::PtraceEvent(_, _, _) | WaitStatus::PtraceSyscall(_)) => Ok(None),
21090        Err(nix::errno::Errno::ECHILD) => Ok(Some(0)),
21091        Err(error) => Err(SidecarError::Execution(format!(
21092            "failed to inspect guest runtime process {child_pid}: {error}"
21093        ))),
21094    }
21095}
21096
21097// macOS nix exposes no `waitid`/`WNOWAIT`, so we poll with `waitpid(WNOHANG)`.
21098// NOTE: unlike Linux's `waitid(WNOWAIT)`, `waitpid` REAPS an exited child rather
21099// than leaving it waitable. That is correct for this poll (the sidecar is the
21100// reaping parent), but a second status query after exit returns ECHILD → treated
21101// as "exited(0)" below.
21102#[cfg(target_os = "macos")]
21103fn runtime_child_exit_status(child_pid: u32) -> Result<Option<i32>, SidecarError> {
21104    if child_pid == 0 {
21105        return Ok(Some(0));
21106    }
21107
21108    match waitpid(Pid::from_raw(child_pid as i32), Some(WaitPidFlag::WNOHANG)) {
21109        Ok(WaitStatus::StillAlive)
21110        | Ok(WaitStatus::Stopped(_, _))
21111        | Ok(WaitStatus::Continued(_)) => Ok(None),
21112        Ok(WaitStatus::Exited(_, status)) => Ok(Some(status)),
21113        Ok(WaitStatus::Signaled(_, signal, _)) => Ok(Some(128 + signal as i32)),
21114        Err(nix::errno::Errno::ECHILD) => Ok(Some(0)),
21115        Err(error) => Err(SidecarError::Execution(format!(
21116            "failed to inspect guest runtime process {child_pid}: {error}"
21117        ))),
21118    }
21119}
21120
21121pub(crate) fn signal_runtime_process(child_pid: u32, signal: i32) -> Result<(), SidecarError> {
21122    if child_pid == 0 {
21123        return Ok(());
21124    }
21125
21126    if !runtime_child_is_alive(child_pid)? {
21127        return Ok(());
21128    }
21129
21130    if signal == 0 {
21131        return Ok(());
21132    }
21133
21134    let parsed = Signal::try_from(signal).map_err(|_| {
21135        SidecarError::InvalidState(format!("unsupported kill_process signal {signal}"))
21136    })?;
21137    let result = send_signal(Pid::from_raw(child_pid as i32), Some(parsed));
21138
21139    match result {
21140        Ok(()) => Ok(()),
21141        Err(nix::errno::Errno::ESRCH) => Ok(()),
21142        Err(error) => Err(SidecarError::Execution(format!(
21143            "failed to signal guest runtime process {child_pid}: {error}"
21144        ))),
21145    }
21146}
21147
21148pub(crate) fn error_code(error: &SidecarError) -> &'static str {
21149    match error {
21150        SidecarError::InvalidState(_) => "invalid_state",
21151        SidecarError::ProtocolVersionMismatch(_) => "protocol_version_mismatch",
21152        SidecarError::BridgeVersionMismatch(_) => "bridge_version_mismatch",
21153        SidecarError::Conflict(_) => "conflict",
21154        SidecarError::Unauthorized(_) => "unauthorized",
21155        SidecarError::Unsupported(_) => "unsupported",
21156        SidecarError::FrameTooLarge(_) => "frame_too_large",
21157        SidecarError::Kernel(_) => "kernel_error",
21158        SidecarError::Plugin(_) => "plugin_error",
21159        SidecarError::Execution(_) => "execution_error",
21160        SidecarError::Bridge(_) => "bridge_error",
21161        SidecarError::Io(_) => "io_error",
21162    }
21163}
21164
21165fn guest_errno_code(message: &str) -> Option<&str> {
21166    const TRUSTED_PREFIXES: &[&str] = &[
21167        "ERR_AGENTOS_NODE_SYNC_RPC",
21168        "ERR_AGENTOS_PYTHON_VFS_RPC",
21169        "ERR_AGENTOS_BRIDGE",
21170    ];
21171
21172    let mut segments = message.split(':').map(str::trim);
21173    let first = segments.next()?;
21174    if is_guest_errno_segment(first) {
21175        return Some(first);
21176    }
21177
21178    if TRUSTED_PREFIXES.contains(&first) {
21179        let second = segments.next()?;
21180        if is_guest_errno_segment(second) {
21181            return Some(second);
21182        }
21183    }
21184
21185    None
21186}
21187
21188fn is_guest_errno_segment(segment: &str) -> bool {
21189    segment.len() >= 2
21190        && segment.starts_with('E')
21191        && !segment.starts_with("ERR_")
21192        && segment[1..]
21193            .bytes()
21194            .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_')
21195}
21196
21197pub(crate) fn javascript_sync_rpc_error_code(error: &SidecarError) -> String {
21198    let message = error.to_string();
21199    if let Some(code) = guest_errno_code(&message) {
21200        return code.to_owned();
21201    }
21202    if message.starts_with("ERR_NATIVE_BINARY_NOT_SUPPORTED:") {
21203        return String::from("ERR_NATIVE_BINARY_NOT_SUPPORTED");
21204    }
21205
21206    let lower = message.to_ascii_lowercase();
21207    if lower.contains("no such file or directory")
21208        || lower.contains("entry not found")
21209        || lower.contains("not found")
21210    {
21211        return String::from("ENOENT");
21212    }
21213    if lower.contains("permission denied") {
21214        return String::from("EACCES");
21215    }
21216    if lower.contains("already exists")
21217        || lower.contains("already registered")
21218        || lower.contains("file exists")
21219    {
21220        return String::from("EEXIST");
21221    }
21222    if lower.contains("invalid argument") {
21223        return String::from("EINVAL");
21224    }
21225
21226    String::from("ERR_AGENTOS_NODE_SYNC_RPC")
21227}
21228
21229pub(crate) fn ignore_stale_javascript_sync_rpc_response(
21230    error: SidecarError,
21231) -> Result<(), SidecarError> {
21232    match error {
21233        SidecarError::Execution(message)
21234            if message.ends_with("is no longer pending")
21235                && message.starts_with("sync RPC request ") =>
21236        {
21237            Ok(())
21238        }
21239        SidecarError::Execution(message) => {
21240            let lower = message.to_ascii_lowercase();
21241            if lower.contains("sync rpc response")
21242                && (lower.contains("broken pipe") || lower.contains("channel closed unexpectedly"))
21243            {
21244                Ok(())
21245            } else {
21246                Err(SidecarError::Execution(message))
21247            }
21248        }
21249        other => Err(other),
21250    }
21251}
21252
21253#[cfg(test)]
21254mod error_code_tests {
21255    use super::{guest_errno_code, javascript_sync_rpc_error_code, SidecarError};
21256
21257    #[test]
21258    fn guest_errno_code_rejects_guest_controlled_errno_segments() {
21259        assert_eq!(guest_errno_code("user said 'EACCES: denied'"), None);
21260        assert_eq!(
21261            guest_errno_code("prefix: user said 'EPERM': more text"),
21262            None
21263        );
21264        assert_eq!(guest_errno_code("ERR_AGENTOS_FAKE: EACCES: denied"), None);
21265    }
21266
21267    #[test]
21268    fn guest_errno_code_accepts_trusted_secure_exec_prefixes() {
21269        assert_eq!(
21270            guest_errno_code("ERR_AGENTOS_NODE_SYNC_RPC: EACCES: permission denied on /foo"),
21271            Some("EACCES")
21272        );
21273        assert_eq!(
21274            guest_errno_code("ERR_AGENTOS_PYTHON_VFS_RPC: ENOENT: missing file"),
21275            Some("ENOENT")
21276        );
21277        assert_eq!(guest_errno_code("EEXIST: already exists"), Some("EEXIST"));
21278    }
21279
21280    #[test]
21281    fn javascript_sync_rpc_error_code_ignores_spoofed_errnos() {
21282        let error = SidecarError::Execution(String::from("user said 'EACCES: denied'"));
21283        assert_eq!(
21284            javascript_sync_rpc_error_code(&error),
21285            "ERR_AGENTOS_NODE_SYNC_RPC"
21286        );
21287    }
21288
21289    #[test]
21290    fn javascript_sync_rpc_error_code_preserves_real_sidecar_errnos() {
21291        let error = SidecarError::Execution(String::from(
21292            "ERR_AGENTOS_NODE_SYNC_RPC: EACCES: permission denied on /foo",
21293        ));
21294        assert_eq!(javascript_sync_rpc_error_code(&error), "EACCES");
21295    }
21296
21297    #[test]
21298    fn javascript_sync_rpc_error_code_maps_file_exists_messages() {
21299        let error = SidecarError::Io(String::from(
21300            "failed to create mapped guest directory /.next/server: File exists (os error 17)",
21301        ));
21302        assert_eq!(javascript_sync_rpc_error_code(&error), "EEXIST");
21303    }
21304
21305    #[test]
21306    fn javascript_sync_rpc_error_code_preserves_native_binary_rejections() {
21307        let error = SidecarError::Execution(String::from(
21308            "ERR_NATIVE_BINARY_NOT_SUPPORTED: refused to execute native ELF guest binary at /tmp/fake-rg inside the VM",
21309        ));
21310        assert_eq!(
21311            javascript_sync_rpc_error_code(&error),
21312            "ERR_NATIVE_BINARY_NOT_SUPPORTED"
21313        );
21314    }
21315}
21316#[cfg(test)]
21317mod ssrf_egress_classifier_tests {
21318    // F-005/006/007 (sec-sidecar T1/T7/T11): the egress classifier must treat the
21319    // unspecified address (0.0.0.0 / ::), CGNAT (100.64.0.0/10), IPv6 spellings of
21320    // restricted IPv4 targets (::a.b.c.d), and reserved/multicast (240/4, 224/4) as
21321    // restricted. 0.0.0.0 routes to 127.0.0.1 on connect(), so leaving it
21322    // unclassified let a guest bypass the loopback port-ownership gate.
21323    //
21324    // These are bounded SAFEGUARD tests: they exercise the classifier and the DNS
21325    // egress filter directly (no network I/O, no Node), so they run fast and
21326    // deterministically. See FAILURES.md#F-005, #F-006, #F-007.
21327    use super::{
21328        filter_dns_safe_ip_addrs, is_loopback_ip, restricted_non_loopback_ip_range, SidecarError,
21329    };
21330    use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
21331
21332    fn assert_restricted(ip: IpAddr, expected_label: &str) {
21333        let classification = restricted_non_loopback_ip_range(ip);
21334        assert!(
21335            classification.is_some(),
21336            "{ip} must be classified as a restricted egress target"
21337        );
21338        let (_cidr, label) = classification.unwrap();
21339        assert_eq!(
21340            label, expected_label,
21341            "{ip} should be labelled {expected_label}, got {label}"
21342        );
21343    }
21344
21345    fn assert_dns_denied(ip: IpAddr, label: &str) {
21346        match filter_dns_safe_ip_addrs(vec![ip], "attacker.example") {
21347            Err(SidecarError::Execution(message)) => assert!(
21348                message.starts_with("EACCES:"),
21349                "{label}: egress filter must deny with EACCES, got: {message}"
21350            ),
21351            other => panic!("{label}: expected EACCES denial, got {other:?}"),
21352        }
21353    }
21354
21355    // F-005 (sec-sidecar T1).
21356    #[test]
21357    fn classifier_denies_unspecified_and_cgnat_targets() {
21358        // 0.0.0.0 (IPv4 unspecified) -> would route to host loopback.
21359        assert_restricted(IpAddr::V4(Ipv4Addr::UNSPECIFIED), "unspecified");
21360        // :: (IPv6 unspecified).
21361        assert_restricted(IpAddr::V6(Ipv6Addr::UNSPECIFIED), "unspecified");
21362
21363        // CGNAT 100.64.0.0/10 spans 100.64.x.x .. 100.127.x.x.
21364        assert_restricted(
21365            IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1)),
21366            "carrier-grade-nat",
21367        );
21368        assert_restricted(
21369            IpAddr::V4(Ipv4Addr::new(100, 127, 255, 254)),
21370            "carrier-grade-nat",
21371        );
21372
21373        // Guard against over-blocking: addresses just outside 100.64/10 stay allowed.
21374        assert!(
21375            restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(100, 63, 255, 255)))
21376                .is_none(),
21377            "100.63.255.255 is outside CGNAT and must remain allowed"
21378        );
21379        assert!(
21380            restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(100, 128, 0, 0))).is_none(),
21381            "100.128.0.0 is outside CGNAT and must remain allowed"
21382        );
21383
21384        // The DNS egress filter must also deny these via EACCES.
21385        assert_dns_denied(IpAddr::V4(Ipv4Addr::UNSPECIFIED), "0.0.0.0 (unspecified)");
21386        assert_dns_denied(IpAddr::V6(Ipv6Addr::UNSPECIFIED), ":: (unspecified)");
21387        assert_dns_denied(
21388            IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1)),
21389            "100.64.0.1 (CGNAT)",
21390        );
21391    }
21392
21393    // F-006 (sec-sidecar T7).
21394    #[test]
21395    fn classifier_denies_ipv6_spelled_metadata_addresses() {
21396        // The IPv4-mapped form (::ffff:169.254.169.254) was already handled; the
21397        // IPv4-compatible form (::169.254.169.254) is the gap this fixes.
21398        let mapped = "::ffff:169.254.169.254".parse::<Ipv6Addr>().unwrap();
21399        assert_restricted(IpAddr::V6(mapped), "link-local");
21400
21401        let compat = "::169.254.169.254".parse::<Ipv6Addr>().unwrap();
21402        assert_restricted(IpAddr::V6(compat), "link-local");
21403
21404        // Other IPv4-compatible private/CGNAT spellings must also be canonicalized.
21405        assert_restricted(
21406            IpAddr::V6("::10.0.0.1".parse::<Ipv6Addr>().unwrap()),
21407            "private",
21408        );
21409        assert_restricted(
21410            IpAddr::V6("::100.64.0.1".parse::<Ipv6Addr>().unwrap()),
21411            "carrier-grade-nat",
21412        );
21413
21414        // Guard against over-blocking: the IPv6 unspecified/loopback addresses
21415        // are not IPv4-compatible host targets, and a public IPv4-compatible
21416        // address must remain allowed.
21417        assert_eq!(
21418            restricted_non_loopback_ip_range(IpAddr::V6(Ipv6Addr::UNSPECIFIED)),
21419            Some(("::/128", "unspecified")),
21420            ":: must classify as unspecified, not via the IPv4-compat path"
21421        );
21422        assert!(
21423            restricted_non_loopback_ip_range(IpAddr::V6(Ipv6Addr::LOCALHOST)).is_none()
21424                || is_loopback_ip(IpAddr::V6(Ipv6Addr::LOCALHOST)),
21425            "::1 must not be classified as a restricted IPv4-compatible target"
21426        );
21427        assert!(
21428            restricted_non_loopback_ip_range(IpAddr::V6("::8.8.8.8".parse::<Ipv6Addr>().unwrap()))
21429                .is_none(),
21430            "::8.8.8.8 (public IPv4-compatible) must remain allowed"
21431        );
21432
21433        // The DNS egress filter must deny the IPv4-compat metadata spelling.
21434        assert_dns_denied(
21435            IpAddr::V6("::169.254.169.254".parse::<Ipv6Addr>().unwrap()),
21436            "::169.254.169.254 (IPv4-compat metadata)",
21437        );
21438    }
21439
21440    // F-007 (sec-sidecar T11).
21441    #[test]
21442    fn classifier_denies_reserved_and_multicast_targets() {
21443        // 224.0.0.0/4 (multicast) and 240.0.0.0/4 (reserved / future use) are not
21444        // legitimate unicast egress targets; a guest connect to them must be
21445        // classified as restricted and denied.
21446        assert_restricted(IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)), "multicast");
21447        assert_restricted(IpAddr::V4(Ipv4Addr::new(239, 255, 255, 255)), "multicast");
21448        assert_restricted(IpAddr::V4(Ipv4Addr::new(240, 0, 0, 1)), "reserved");
21449        // 255.255.255.255 (limited broadcast) falls in 240.0.0.0/4.
21450        assert_restricted(IpAddr::V4(Ipv4Addr::BROADCAST), "reserved");
21451
21452        // IPv4-compatible IPv6 spellings must canonicalize and be denied too.
21453        assert_restricted(
21454            IpAddr::V6("::224.0.0.1".parse::<Ipv6Addr>().unwrap()),
21455            "multicast",
21456        );
21457        assert_restricted(
21458            IpAddr::V6("::240.0.0.1".parse::<Ipv6Addr>().unwrap()),
21459            "reserved",
21460        );
21461
21462        // Guard against over-blocking: addresses just outside 224/4 stay allowed.
21463        assert!(
21464            restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(223, 255, 255, 255)))
21465                .is_none(),
21466            "223.255.255.255 is outside 224/4 and must remain allowed"
21467        );
21468
21469        // The DNS egress filter must also deny these via EACCES.
21470        assert_dns_denied(
21471            IpAddr::V4(Ipv4Addr::new(240, 0, 0, 1)),
21472            "240.0.0.1 (reserved)",
21473        );
21474        assert_dns_denied(
21475            IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)),
21476            "224.0.0.1 (multicast)",
21477        );
21478    }
21479}
21480
21481/// Adversarial coverage for the DNS-rebinding gap (VECTORS.md D.3) on the
21482/// Python/Pyodide `httpRequestSync` outbound HTTP path. The egress range guard
21483/// (`filter_dns_safe_ip_addrs`) runs at resolution time, but `ureq` performs its
21484/// own DNS resolution for the TCP/TLS connect, so a rebinding DNS server could
21485/// previously make the second lookup land on a private/link-local/metadata IP
21486/// the first check rejected. The fix pins `ureq`'s resolver to the vetted
21487/// address set; these tests prove the connect is pinned and refuses any other
21488/// host or an empty (fully-rejected) address set.
21489#[cfg(test)]
21490mod dns_rebinding_pin_tests {
21491    use super::{issue_outbound_http_request, split_netloc, JavascriptHttpRequestOptions};
21492    use std::collections::BTreeMap;
21493    use std::io::{Read, Write};
21494    use std::net::{IpAddr, Ipv4Addr, TcpListener};
21495    use std::thread;
21496    use url::Url;
21497
21498    fn empty_headers() -> super::HttpHeaderCollection {
21499        super::parse_http_header_collection(&BTreeMap::new(), "test headers")
21500            .expect("empty header collection")
21501    }
21502
21503    fn options() -> JavascriptHttpRequestOptions {
21504        JavascriptHttpRequestOptions {
21505            method: Some(String::from("GET")),
21506            headers: BTreeMap::new(),
21507            body: None,
21508            reject_unauthorized: None,
21509        }
21510    }
21511
21512    #[test]
21513    fn split_netloc_handles_hostnames_and_bracketed_ipv6() {
21514        assert_eq!(
21515            split_netloc("attacker.example:80"),
21516            Some(("attacker.example", 80))
21517        );
21518        assert_eq!(split_netloc("[::1]:443"), Some(("::1", 443)));
21519        assert_eq!(split_netloc("10.0.0.1:8080"), Some(("10.0.0.1", 8080)));
21520        assert_eq!(split_netloc("no-port"), None);
21521        assert_eq!(split_netloc("host:notaport"), None);
21522    }
21523
21524    /// A loopback HTTP server stands in for the egress-vetted target. The
21525    /// request URL uses a *different* hostname (`attacker.example`) whose real
21526    /// DNS would resolve elsewhere; pinning forces the connect onto the vetted
21527    /// IP only. If the resolver were unpinned, the request would fail to reach
21528    /// this server (and on a real host could land on a private/metadata IP).
21529    #[test]
21530    fn outbound_http_connect_is_pinned_to_vetted_ip() {
21531        let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).expect("bind loopback server");
21532        let port = listener.local_addr().expect("local addr").port();
21533        let server = thread::spawn(move || {
21534            let (mut stream, _) = listener.accept().expect("accept");
21535            let mut buf = [0u8; 1024];
21536            let _ = stream.read(&mut buf);
21537            stream
21538                .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi")
21539                .expect("write response");
21540            let _ = stream.flush();
21541        });
21542
21543        let url = Url::parse(&format!("http://attacker.example:{port}/")).expect("url");
21544        let pinned = vec![IpAddr::V4(Ipv4Addr::LOCALHOST)];
21545        let result = issue_outbound_http_request(&url, &options(), &empty_headers(), &pinned)
21546            .expect("pinned request should reach the vetted loopback target");
21547        let payload = result.as_str().expect("string payload");
21548        assert!(
21549            payload.contains("\"status\":200"),
21550            "expected 200 from pinned target, got: {payload}"
21551        );
21552        server.join().expect("server thread");
21553    }
21554
21555    /// With no vetted address (every resolved IP was rejected by the range
21556    /// guard, or the literal IP was a blocked range), the pinned resolver must
21557    /// refuse rather than fall back to the host resolver.
21558    #[test]
21559    fn outbound_http_refuses_when_no_vetted_address() {
21560        let url = Url::parse("https://attacker.example/").expect("url");
21561        let error = issue_outbound_http_request(&url, &options(), &empty_headers(), &[])
21562            .expect_err("empty pinned set must be refused");
21563        let message = error.to_string();
21564        assert!(
21565            message.contains("EACCES") || message.contains("ERR_HTTP_REQUEST_FAILED"),
21566            "expected an egress refusal, got: {message}"
21567        );
21568    }
21569}