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, 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 = "/__agent_os_pyodide";
154const PYTHON_PYODIDE_CACHE_GUEST_ROOT: &str = "/__agent_os_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 module_reader = build_module_reader(vm, &resolved)
3069                    .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
3070                let execution = self
3071                    .javascript_engine
3072                    .start_execution_with_module_reader(
3073                        StartJavascriptExecutionRequest {
3074                            guest_runtime: guest_runtime_identity(vm, None, None),
3075                            vm_id: vm_id.clone(),
3076                            context_id: context.context_id,
3077                            argv: std::iter::once(resolved.entrypoint.clone())
3078                                .chain(resolved.execution_args.iter().cloned())
3079                                .collect(),
3080                            env: env.clone(),
3081                            cwd: resolved.host_cwd.clone(),
3082                            limits: javascript_execution_limits(vm),
3083                            inline_code,
3084                        },
3085                        module_reader,
3086                    )
3087                    .map_err(javascript_error)?;
3088                (ActiveExecution::Javascript(execution), env.clone())
3089            }
3090            GuestRuntimeKind::Python => {
3091                let python_file_path = python_file_entrypoint(&resolved.entrypoint);
3092                let pyodide_dist_path = self
3093                    .python_engine
3094                    .bundled_pyodide_dist_path_for_vm(&vm_id)
3095                    .map_err(python_error)?;
3096                let pyodide_cache_path = pyodide_dist_path
3097                    .parent()
3098                    .and_then(Path::parent)
3099                    .unwrap_or(pyodide_dist_path.as_path())
3100                    .join("pyodide-package-cache");
3101                add_runtime_guest_path_mapping(
3102                    &mut env,
3103                    PYTHON_PYODIDE_GUEST_ROOT,
3104                    &pyodide_dist_path,
3105                );
3106                add_runtime_guest_path_mapping(
3107                    &mut env,
3108                    PYTHON_PYODIDE_CACHE_GUEST_ROOT,
3109                    &pyodide_cache_path,
3110                );
3111                add_runtime_host_access_path(
3112                    &mut env,
3113                    "AGENT_OS_EXTRA_FS_READ_PATHS",
3114                    &pyodide_dist_path,
3115                    true,
3116                );
3117                add_runtime_host_access_path(
3118                    &mut env,
3119                    "AGENT_OS_EXTRA_FS_READ_PATHS",
3120                    &pyodide_cache_path,
3121                    true,
3122                );
3123                add_runtime_host_access_path(
3124                    &mut env,
3125                    "AGENT_OS_EXTRA_FS_WRITE_PATHS",
3126                    &pyodide_cache_path,
3127                    false,
3128                );
3129                let context = self
3130                    .python_engine
3131                    .create_context(CreatePythonContextRequest {
3132                        vm_id: vm_id.clone(),
3133                        pyodide_dist_path,
3134                    });
3135                let execution = self
3136                    .python_engine
3137                    .start_execution(StartPythonExecutionRequest {
3138                        vm_id: vm_id.clone(),
3139                        context_id: context.context_id,
3140                        code: resolved.entrypoint.clone(),
3141                        file_path: python_file_path,
3142                        env: env.clone(),
3143                        cwd: resolved.host_cwd.clone(),
3144                        limits: python_execution_limits(vm),
3145                        guest_runtime: guest_runtime_identity(vm, None, None),
3146                    })
3147                    .map_err(python_error)?;
3148                (ActiveExecution::Python(execution), env.clone())
3149            }
3150            GuestRuntimeKind::WebAssembly => {
3151                let wasm_limits = wasm_execution_limits(vm);
3152                let wasm_guest_runtime =
3153                    guest_runtime_identity(vm, Some(u64::from(kernel_pid)), Some(0));
3154                let wasm_permission_tier = resolved.wasm_permission_tier.unwrap_or_else(|| {
3155                    resolve_wasm_permission_tier(
3156                        vm,
3157                        Some(&resolved.command),
3158                        None,
3159                        &resolved.entrypoint,
3160                    )
3161                });
3162                let context = self.wasm_engine.create_context(CreateWasmContextRequest {
3163                    vm_id: vm_id.clone(),
3164                    module_path: Some(resolved.entrypoint.clone()),
3165                });
3166                let execution = self
3167                    .wasm_engine
3168                    .start_execution(StartWasmExecutionRequest {
3169                        vm_id: vm_id.clone(),
3170                        context_id: context.context_id,
3171                        argv: resolved.process_args.clone(),
3172                        env: env.clone(),
3173                        cwd: resolved.host_cwd.clone(),
3174                        permission_tier: execution_wasm_permission_tier(wasm_permission_tier),
3175                        limits: wasm_limits,
3176                        guest_runtime: wasm_guest_runtime,
3177                    })
3178                    .map_err(wasm_error)?;
3179                (ActiveExecution::Wasm(Box::new(execution)), env)
3180            }
3181        };
3182        let child_pid = execution.child_pid();
3183        let kernel_stdin_writer_fd = install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?;
3184        vm.active_processes.insert(
3185            payload.process_id.clone(),
3186            ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
3187                .with_kernel_stdin_writer_fd(kernel_stdin_writer_fd)
3188                .with_guest_cwd(resolved.guest_cwd.clone())
3189                .with_env(process_env)
3190                .with_host_cwd(resolved.host_cwd.clone()),
3191        );
3192        self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy)?;
3193
3194        Ok(DispatchResult {
3195            response: self.respond(
3196                request,
3197                ResponsePayload::ProcessStarted(ProcessStartedResponse {
3198                    process_id: payload.process_id,
3199                    pid: Some(if child_pid == 0 {
3200                        kernel_pid
3201                    } else {
3202                        child_pid
3203                    }),
3204                }),
3205            ),
3206            events: Vec::new(),
3207        })
3208    }
3209
3210    pub(crate) async fn write_stdin(
3211        &mut self,
3212        request: &RequestFrame,
3213        payload: WriteStdinRequest,
3214    ) -> Result<DispatchResult, SidecarError> {
3215        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3216        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3217
3218        let vm = self
3219            .vms
3220            .get_mut(&vm_id)
3221            .ok_or_else(|| missing_vm_error(&vm_id))?;
3222        let process = vm
3223            .active_processes
3224            .get_mut(&payload.process_id)
3225            .ok_or_else(|| {
3226                SidecarError::InvalidState(format!(
3227                    "VM {vm_id} has no active process {}",
3228                    payload.process_id
3229                ))
3230            })?;
3231        process.execution.write_stdin(&payload.chunk)?;
3232        write_kernel_process_stdin(&mut vm.kernel, process, &payload.chunk)?;
3233
3234        Ok(DispatchResult {
3235            response: self.respond(
3236                request,
3237                ResponsePayload::StdinWritten(StdinWrittenResponse {
3238                    process_id: payload.process_id,
3239                    accepted_bytes: payload.chunk.len() as u64,
3240                }),
3241            ),
3242            events: Vec::new(),
3243        })
3244    }
3245
3246    pub(crate) async fn close_stdin(
3247        &mut self,
3248        request: &RequestFrame,
3249        payload: CloseStdinRequest,
3250    ) -> Result<DispatchResult, SidecarError> {
3251        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3252        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3253
3254        let vm = self
3255            .vms
3256            .get_mut(&vm_id)
3257            .ok_or_else(|| missing_vm_error(&vm_id))?;
3258        let process = vm
3259            .active_processes
3260            .get_mut(&payload.process_id)
3261            .ok_or_else(|| {
3262                SidecarError::InvalidState(format!(
3263                    "VM {vm_id} has no active process {}",
3264                    payload.process_id
3265                ))
3266            })?;
3267        process.execution.close_stdin()?;
3268        close_kernel_process_stdin(&mut vm.kernel, process)?;
3269
3270        Ok(DispatchResult {
3271            response: self.respond(
3272                request,
3273                ResponsePayload::StdinClosed(StdinClosedResponse {
3274                    process_id: payload.process_id,
3275                }),
3276            ),
3277            events: Vec::new(),
3278        })
3279    }
3280
3281    pub(crate) async fn kill_process(
3282        &mut self,
3283        request: &RequestFrame,
3284        payload: KillProcessRequest,
3285    ) -> Result<DispatchResult, SidecarError> {
3286        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3287        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3288        self.kill_process_internal(&vm_id, &payload.process_id, &payload.signal)?;
3289
3290        Ok(DispatchResult {
3291            response: self.respond(
3292                request,
3293                ResponsePayload::ProcessKilled(ProcessKilledResponse {
3294                    process_id: payload.process_id,
3295                }),
3296            ),
3297            events: Vec::new(),
3298        })
3299    }
3300
3301    pub(crate) async fn find_listener(
3302        &mut self,
3303        request: &RequestFrame,
3304        payload: FindListenerRequest,
3305    ) -> Result<DispatchResult, SidecarError> {
3306        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3307        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3308        require_vm_inspection_permission(
3309            &self.bridge,
3310            &vm_id,
3311            "network.inspect",
3312            "network",
3313            &socket_query_resource(SocketQueryKind::TcpListener, &payload),
3314        )?;
3315
3316        let listener =
3317            find_socket_state_entry(self.vms.get(&vm_id), SocketQueryKind::TcpListener, &payload)?;
3318
3319        Ok(DispatchResult {
3320            response: self.respond(
3321                request,
3322                ResponsePayload::ListenerSnapshot(ListenerSnapshotResponse { listener }),
3323            ),
3324            events: Vec::new(),
3325        })
3326    }
3327
3328    pub(crate) async fn get_process_snapshot(
3329        &mut self,
3330        request: &RequestFrame,
3331        _payload: GetProcessSnapshotRequest,
3332    ) -> Result<DispatchResult, SidecarError> {
3333        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3334        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3335        require_vm_inspection_permission(
3336            &self.bridge,
3337            &vm_id,
3338            "process.inspect",
3339            "process",
3340            "process://snapshot",
3341        )?;
3342
3343        let processes = self
3344            .vms
3345            .get_mut(&vm_id)
3346            .map(|vm| {
3347                prune_exited_process_snapshots(vm);
3348                snapshot_vm_processes(vm)
3349            })
3350            .unwrap_or_default();
3351
3352        Ok(DispatchResult {
3353            response: self.respond(
3354                request,
3355                ResponsePayload::ProcessSnapshot(ProcessSnapshotResponse { processes }),
3356            ),
3357            events: Vec::new(),
3358        })
3359    }
3360
3361    pub(crate) async fn find_bound_udp(
3362        &mut self,
3363        request: &RequestFrame,
3364        payload: FindBoundUdpRequest,
3365    ) -> Result<DispatchResult, SidecarError> {
3366        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3367        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3368
3369        let lookup_request = FindListenerRequest {
3370            host: payload.host,
3371            port: payload.port,
3372            path: None,
3373        };
3374        require_vm_inspection_permission(
3375            &self.bridge,
3376            &vm_id,
3377            "network.inspect",
3378            "network",
3379            &socket_query_resource(SocketQueryKind::UdpBound, &lookup_request),
3380        )?;
3381        let socket = find_socket_state_entry(
3382            self.vms.get(&vm_id),
3383            SocketQueryKind::UdpBound,
3384            &lookup_request,
3385        )?;
3386
3387        Ok(DispatchResult {
3388            response: self.respond(
3389                request,
3390                ResponsePayload::BoundUdpSnapshot(BoundUdpSnapshotResponse { socket }),
3391            ),
3392            events: Vec::new(),
3393        })
3394    }
3395
3396    pub(crate) async fn vm_fetch(
3397        &mut self,
3398        request: &RequestFrame,
3399        payload: VmFetchRequest,
3400    ) -> Result<DispatchResult, SidecarError> {
3401        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3402        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3403
3404        let vm = self
3405            .vms
3406            .get_mut(&vm_id)
3407            .ok_or_else(|| SidecarError::InvalidState(String::from("unknown sidecar VM")))?;
3408        let target_path = if payload.path.starts_with('/') {
3409            payload.path.clone()
3410        } else {
3411            format!("/{}", payload.path)
3412        };
3413        let request_url = Url::parse(&format!("http://127.0.0.1:{}{target_path}", payload.port))
3414            .map_err(|error| {
3415                SidecarError::InvalidState(format!(
3416                    "invalid vm.fetch target {target_path:?}: {error}"
3417                ))
3418            })?;
3419        let header_values: BTreeMap<String, Value> = serde_json::from_str(&payload.headers_json)
3420            .map_err(|error| {
3421                SidecarError::InvalidState(format!(
3422                    "vm.fetch headers_json must be valid JSON: {error}"
3423                ))
3424            })?;
3425        let options = JavascriptHttpRequestOptions {
3426            method: Some(payload.method),
3427            headers: header_values,
3428            body: payload.body,
3429            reject_unauthorized: None,
3430        };
3431        let headers = parse_http_header_collection(&options.headers, "vm.fetch headers")?;
3432        let target_process_id = find_kernel_http_listener_process(vm, payload.port);
3433        if let Some(target_process_id) = target_process_id {
3434            let max_fetch_response_bytes = vm.limits.http.max_fetch_response_bytes;
3435            let response_json = match dispatch_kernel_http_fetch(
3436                &self.bridge,
3437                &vm_id,
3438                vm,
3439                &target_process_id,
3440                payload.port,
3441                &target_path,
3442                &options,
3443                &headers,
3444                max_fetch_response_bytes,
3445            ) {
3446                Ok(response_json) => response_json,
3447                Err(error) => {
3448                    if let Some(exit_code) = kernel_http_fetch_target_exit_code(&error) {
3449                        let _ = vm;
3450                        self.finish_active_process_exit(&vm_id, &target_process_id, exit_code)?;
3451                    }
3452                    return Err(error);
3453                }
3454            };
3455            let response = self.respond(
3456                request,
3457                ResponsePayload::VmFetchResult(VmFetchResponse { response_json }),
3458            );
3459            ensure_vm_fetch_response_frame_within_limit(&response, self.config.max_frame_bytes)?;
3460
3461            return Ok(DispatchResult {
3462                response,
3463                events: Vec::new(),
3464            });
3465        }
3466
3467        let Some((target_process_id, server_id)) =
3468            vm.active_processes
3469                .iter()
3470                .find_map(|(process_id, process)| {
3471                    process
3472                        .http_servers
3473                        .iter()
3474                        .find(|(_, server)| server.guest_local_addr.port() == payload.port)
3475                        .map(|(server_id, _)| (process_id.clone(), *server_id))
3476                })
3477        else {
3478            return Err(SidecarError::Execution(format!(
3479                "vm.fetch could not find a guest HTTP listener on port {}",
3480                payload.port
3481            )));
3482        };
3483        let socket_paths = build_javascript_socket_path_context(vm)?;
3484        let resource_limits = vm.kernel.resource_limits().clone();
3485        let process = vm
3486            .active_processes
3487            .get_mut(&target_process_id)
3488            .ok_or_else(|| {
3489                SidecarError::InvalidState(format!(
3490                    "vm.fetch target process disappeared: {target_process_id}"
3491                ))
3492            })?;
3493        let request_json = serialize_http_loopback_request(&request_url, &options, &headers)?;
3494        let response_json = dispatch_loopback_http_request(LoopbackHttpDispatchRequest {
3495            bridge: &self.bridge,
3496            vm_id: &vm_id,
3497            dns: &vm.dns,
3498            socket_paths: &socket_paths,
3499            kernel: &mut vm.kernel,
3500            process,
3501            resource_limits: &resource_limits,
3502            server_id,
3503            request_json: &request_json,
3504        })?;
3505
3506        let response = self.respond(
3507            request,
3508            ResponsePayload::VmFetchResult(VmFetchResponse { response_json }),
3509        );
3510        ensure_vm_fetch_response_frame_within_limit(&response, self.config.max_frame_bytes)?;
3511
3512        Ok(DispatchResult {
3513            response,
3514            events: Vec::new(),
3515        })
3516    }
3517
3518    pub(crate) async fn get_signal_state(
3519        &mut self,
3520        request: &RequestFrame,
3521        payload: GetSignalStateRequest,
3522    ) -> Result<DispatchResult, SidecarError> {
3523        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3524        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3525
3526        let handlers = self
3527            .vms
3528            .get(&vm_id)
3529            .and_then(|vm| vm.signal_states.get(&payload.process_id))
3530            .cloned()
3531            .unwrap_or_default();
3532
3533        Ok(DispatchResult {
3534            response: self.respond(
3535                request,
3536                ResponsePayload::SignalState(SignalStateResponse {
3537                    process_id: payload.process_id,
3538                    handlers: handlers.into_iter().collect(),
3539                }),
3540            ),
3541            events: Vec::new(),
3542        })
3543    }
3544
3545    pub(crate) async fn get_zombie_timer_count(
3546        &mut self,
3547        request: &RequestFrame,
3548        _payload: GetZombieTimerCountRequest,
3549    ) -> Result<DispatchResult, SidecarError> {
3550        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3551        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3552
3553        let count = self
3554            .vms
3555            .get(&vm_id)
3556            .map(|vm| vm.kernel.zombie_timer_count() as u64)
3557            .unwrap_or_default();
3558
3559        Ok(DispatchResult {
3560            response: self.respond(
3561                request,
3562                ResponsePayload::ZombieTimerCount(ZombieTimerCountResponse { count }),
3563            ),
3564            events: Vec::new(),
3565        })
3566    }
3567
3568    pub(crate) fn kill_process_internal(
3569        &mut self,
3570        vm_id: &str,
3571        process_id: &str,
3572        signal: &str,
3573    ) -> Result<(), SidecarError> {
3574        let signal_name = signal.to_owned();
3575        let signal = parse_signal(signal)?;
3576        let vm = self
3577            .vms
3578            .get_mut(vm_id)
3579            .ok_or_else(|| SidecarError::InvalidState(format!("unknown sidecar VM {vm_id}")))?;
3580        let process = vm.active_processes.get_mut(process_id).ok_or_else(|| {
3581            SidecarError::InvalidState(format!("VM {vm_id} has no active process {process_id}"))
3582        })?;
3583        let kernel_pid = process.kernel_pid;
3584
3585        enum KillBehavior {
3586            Tool,
3587            SharedV8StateOnly,
3588            SharedV8Continue,
3589            SharedV8Terminate,
3590            SharedV8DispatchOrTerminate,
3591            Noop,
3592            HostPid(u32),
3593        }
3594
3595        let behavior = match &process.execution {
3596            ActiveExecution::Tool(_) => KillBehavior::Tool,
3597            ActiveExecution::Javascript(execution)
3598                if execution.uses_shared_v8_runtime() && matches!(signal, 0 | libc::SIGSTOP) =>
3599            {
3600                KillBehavior::SharedV8StateOnly
3601            }
3602            ActiveExecution::Javascript(execution)
3603                if execution.uses_shared_v8_runtime() && signal == libc::SIGCONT =>
3604            {
3605                KillBehavior::SharedV8Continue
3606            }
3607            ActiveExecution::Wasm(execution)
3608                if execution.uses_shared_v8_runtime()
3609                    && matches!(signal, 0 | libc::SIGSTOP | libc::SIGCONT) =>
3610            {
3611                KillBehavior::SharedV8StateOnly
3612            }
3613            ActiveExecution::Python(execution)
3614                if execution.uses_shared_v8_runtime()
3615                    && matches!(signal, 0 | libc::SIGSTOP | libc::SIGCONT) =>
3616            {
3617                KillBehavior::SharedV8StateOnly
3618            }
3619            ActiveExecution::Javascript(execution)
3620                if execution.uses_shared_v8_runtime() && signal == SIGKILL =>
3621            {
3622                KillBehavior::SharedV8Terminate
3623            }
3624            ActiveExecution::Wasm(execution)
3625                if execution.uses_shared_v8_runtime() && signal == SIGKILL =>
3626            {
3627                KillBehavior::SharedV8Terminate
3628            }
3629            ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime() => {
3630                KillBehavior::SharedV8DispatchOrTerminate
3631            }
3632            ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime() => {
3633                KillBehavior::SharedV8Terminate
3634            }
3635            ActiveExecution::Python(execution) if execution.uses_shared_v8_runtime() => {
3636                KillBehavior::SharedV8Terminate
3637            }
3638            ActiveExecution::Javascript(execution) if execution.child_pid() == 0 => {
3639                KillBehavior::Noop
3640            }
3641            _ => KillBehavior::HostPid(process.execution.child_pid()),
3642        };
3643
3644        match behavior {
3645            KillBehavior::Tool => {
3646                let ActiveExecution::Tool(execution) = &process.execution else {
3647                    unreachable!("kill behavior must match tool execution");
3648                };
3649                if signal != 0 {
3650                    execution.cancelled.store(true, Ordering::Relaxed);
3651                    process.queue_pending_execution_event(ActiveExecutionEvent::Exited(
3652                        128 + signal,
3653                    ))?;
3654                }
3655            }
3656            KillBehavior::SharedV8StateOnly => {
3657                if matches!(signal, libc::SIGSTOP | libc::SIGCONT) {
3658                    vm.kernel
3659                        .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal)
3660                        .map_err(kernel_error)?;
3661                }
3662            }
3663            KillBehavior::SharedV8Continue => {
3664                vm.kernel
3665                    .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal)
3666                    .map_err(kernel_error)?;
3667                if signal != 0 && !dispatch_v8_process_signal(process, signal)? {
3668                    process.execution.terminate()?;
3669                }
3670            }
3671            KillBehavior::SharedV8Terminate => {
3672                if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
3673                    close_kernel_process_stdin(&mut vm.kernel, process)?;
3674                }
3675                process.execution.terminate()?;
3676                let needs_synthetic_exit = matches!(process.execution, ActiveExecution::Wasm(_))
3677                    || (signal == SIGKILL
3678                        && matches!(process.execution, ActiveExecution::Javascript(_)));
3679                if signal != 0 && needs_synthetic_exit {
3680                    process.queue_pending_execution_event(ActiveExecutionEvent::Exited(
3681                        128 + signal,
3682                    ))?;
3683                }
3684            }
3685            KillBehavior::SharedV8DispatchOrTerminate => {
3686                if signal != 0 && !dispatch_v8_process_signal(process, signal)? {
3687                    process.execution.terminate()?;
3688                }
3689            }
3690            KillBehavior::Noop => {}
3691            KillBehavior::HostPid(pid) => {
3692                if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
3693                    close_kernel_process_stdin(&mut vm.kernel, process)?;
3694                }
3695                signal_runtime_process(pid, signal)?;
3696            }
3697        }
3698        emit_security_audit_event(
3699            &self.bridge,
3700            vm_id,
3701            "security.process.kill",
3702            audit_fields([
3703                (String::from("source"), String::from("control_plane")),
3704                (String::from("source_pid"), String::from("0")),
3705                (String::from("target_pid"), process.kernel_pid.to_string()),
3706                (String::from("process_id"), process_id.to_owned()),
3707                (String::from("signal"), signal_name),
3708                (
3709                    String::from("host_pid"),
3710                    process.execution.child_pid().to_string(),
3711                ),
3712            ]),
3713        );
3714        Ok(())
3715    }
3716
3717    pub async fn pump_process_events(
3718        &mut self,
3719        ownership: &OwnershipScope,
3720    ) -> Result<bool, SidecarError> {
3721        let mut emitted_any = false;
3722
3723        let mut queued_envelopes = Vec::new();
3724        {
3725            let pending_capacity = self.pending_process_event_capacity();
3726            let receiver = self.process_event_receiver.as_mut().ok_or_else(|| {
3727                SidecarError::InvalidState(String::from("process event receiver unavailable"))
3728            })?;
3729            loop {
3730                if queued_envelopes.len() >= pending_capacity {
3731                    if receiver.is_empty() {
3732                        break;
3733                    }
3734                    return Err(process_event_queue_overflow_error());
3735                }
3736                match receiver.try_recv() {
3737                    Ok(envelope) => {
3738                        queued_envelopes.push(envelope);
3739                        emitted_any = true;
3740                    }
3741                    Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
3742                    Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
3743                }
3744            }
3745        }
3746        for envelope in queued_envelopes {
3747            self.queue_pending_process_event(envelope)?;
3748        }
3749
3750        let vm_ids = self.vm_ids_for_scope(ownership)?;
3751        for vm_id in vm_ids {
3752            while let Some(vm) = self.vms.get(&vm_id) {
3753                let connection_id = vm.connection_id.clone();
3754                let session_id = vm.session_id.clone();
3755                let process_ids = self
3756                    .vms
3757                    .get(&vm_id)
3758                    .map(|vm| vm.active_processes.keys().cloned().collect::<Vec<_>>())
3759                    .unwrap_or_default();
3760                let mut emitted_this_pass = false;
3761
3762                for process_id in process_ids {
3763                    if self
3764                        .vms
3765                        .get(&vm_id)
3766                        .is_some_and(|vm| vm.detached_child_processes.contains(&process_id))
3767                    {
3768                        continue;
3769                    }
3770                    enum ProcessPollResult {
3771                        Event(Box<Option<ActiveExecutionEvent>>),
3772                        RecoverClosedChannel,
3773                    }
3774                    let poll_result = {
3775                        let Some(vm) = self.vms.get_mut(&vm_id) else {
3776                            continue;
3777                        };
3778                        let Some(process) = vm.active_processes.get_mut(&process_id) else {
3779                            continue;
3780                        };
3781                        if let Some(event) = process.pending_execution_events.pop_front() {
3782                            ProcessPollResult::Event(Box::new(Some(event)))
3783                        } else {
3784                            match process.execution.poll_event(Duration::ZERO).await {
3785                                Ok(event) => ProcessPollResult::Event(Box::new(event)),
3786                                Err(SidecarError::Execution(message))
3787                                    if (process.runtime == GuestRuntimeKind::JavaScript
3788                                        && closed_javascript_event_channel(&message))
3789                                        || (process.runtime == GuestRuntimeKind::Python
3790                                            && closed_python_event_channel(&message))
3791                                        || (process.runtime == GuestRuntimeKind::WebAssembly
3792                                            && closed_wasm_event_channel(&message)) =>
3793                                {
3794                                    ProcessPollResult::RecoverClosedChannel
3795                                }
3796                                Err(other) => return Err(other),
3797                            }
3798                        }
3799                    };
3800                    let event = match poll_result {
3801                        ProcessPollResult::Event(event) => *event,
3802                        ProcessPollResult::RecoverClosedChannel => {
3803                            self.recover_closed_root_runtime_process_event(&vm_id, &process_id)?
3804                        }
3805                    };
3806
3807                    let Some(event) = event else {
3808                        continue;
3809                    };
3810
3811                    if Self::internal_execution_event(&event) {
3812                        // These events are sidecar work items, not client-facing
3813                        // process events. Handle them immediately so a sibling
3814                        // process can service sync RPCs while another request
3815                        // waits on VM-local networking.
3816                        self.handle_execution_event(&vm_id, &process_id, event)?;
3817                    } else {
3818                        self.queue_pending_process_event(ProcessEventEnvelope {
3819                            connection_id: connection_id.clone(),
3820                            session_id: session_id.clone(),
3821                            vm_id: vm_id.clone(),
3822                            process_id: process_id.clone(),
3823                            event,
3824                        })?;
3825                    }
3826                    emitted_any = true;
3827                    emitted_this_pass = true;
3828                }
3829
3830                if !emitted_this_pass {
3831                    break;
3832                }
3833            }
3834
3835            if self.pump_detached_child_process_events(&vm_id)? {
3836                emitted_any = true;
3837            }
3838        }
3839
3840        Ok(emitted_any)
3841    }
3842
3843    fn internal_execution_event(event: &ActiveExecutionEvent) -> bool {
3844        matches!(
3845            event,
3846            ActiveExecutionEvent::JavascriptSyncRpcRequest(_)
3847                | ActiveExecutionEvent::PythonVfsRpcRequest(_)
3848                | ActiveExecutionEvent::SignalState { .. }
3849        )
3850    }
3851
3852    fn recover_closed_root_runtime_process_event(
3853        &mut self,
3854        vm_id: &str,
3855        process_id: &str,
3856    ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
3857        let Some(vm) = self.vms.get_mut(vm_id) else {
3858            return Ok(None);
3859        };
3860        let Some(process) = vm.active_processes.get(process_id) else {
3861            return Ok(None);
3862        };
3863        if process.execution.uses_shared_v8_runtime() {
3864            return Ok(None);
3865        }
3866        if process.runtime != GuestRuntimeKind::JavaScript
3867            && process.runtime != GuestRuntimeKind::Python
3868            && process.runtime != GuestRuntimeKind::WebAssembly
3869        {
3870            return Ok(None);
3871        }
3872        let runtime_child_pid = process.execution.child_pid();
3873        if runtime_child_pid == 0 {
3874            return Ok(None);
3875        }
3876        if let Some(status) = runtime_child_exit_status(runtime_child_pid)? {
3877            return Ok(Some(ActiveExecutionEvent::Exited(status)));
3878        }
3879        if runtime_child_is_alive(runtime_child_pid)? {
3880            return Ok(None);
3881        }
3882        Ok(Some(ActiveExecutionEvent::Exited(0)))
3883    }
3884
3885    fn active_process_by_path<'a>(
3886        process: &'a ActiveProcess,
3887        child_path: &[&str],
3888    ) -> Option<&'a ActiveProcess> {
3889        let mut current = process;
3890        for child_id in child_path {
3891            current = current.child_processes.get(*child_id)?;
3892        }
3893        Some(current)
3894    }
3895
3896    fn active_process_by_path_mut<'a>(
3897        process: &'a mut ActiveProcess,
3898        child_path: &[&str],
3899    ) -> Option<&'a mut ActiveProcess> {
3900        let mut current = process;
3901        for child_id in child_path {
3902            current = current.child_processes.get_mut(*child_id)?;
3903        }
3904        Some(current)
3905    }
3906
3907    fn active_process_by_owned_path_mut<'a>(
3908        process: &'a mut ActiveProcess,
3909        child_path: &[String],
3910    ) -> Option<&'a mut ActiveProcess> {
3911        let mut current = process;
3912        for child_id in child_path {
3913            current = current.child_processes.get_mut(child_id)?;
3914        }
3915        Some(current)
3916    }
3917
3918    fn active_process_path_by_kernel_pid(
3919        process: &ActiveProcess,
3920        kernel_pid: u32,
3921    ) -> Option<Vec<String>> {
3922        if process.kernel_pid == kernel_pid {
3923            return Some(Vec::new());
3924        }
3925
3926        for (child_id, child) in &process.child_processes {
3927            let Some(mut path) = Self::active_process_path_by_kernel_pid(child, kernel_pid) else {
3928                continue;
3929            };
3930            path.insert(0, child_id.clone());
3931            return Some(path);
3932        }
3933
3934        None
3935    }
3936
3937    fn descendant_parent_process<'a>(
3938        vm: &'a VmState,
3939        process_id: &str,
3940        child_path: &[&str],
3941    ) -> Option<&'a ActiveProcess> {
3942        let root = vm.active_processes.get(process_id)?;
3943        Self::active_process_by_path(root, child_path)
3944    }
3945
3946    fn descendant_parent_process_mut<'a>(
3947        vm: &'a mut VmState,
3948        process_id: &str,
3949        child_path: &[&str],
3950    ) -> Option<&'a mut ActiveProcess> {
3951        let root = vm.active_processes.get_mut(process_id)?;
3952        Self::active_process_by_path_mut(root, child_path)
3953    }
3954
3955    fn child_process_path_label(process_id: &str, child_path: &[&str]) -> String {
3956        if child_path.is_empty() {
3957            process_id.to_owned()
3958        } else {
3959            format!("{process_id}/{}", child_path.join("/"))
3960        }
3961    }
3962
3963    fn adopt_detached_child_processes(
3964        current_process_id: &str,
3965        process: &mut ActiveProcess,
3966    ) -> Vec<(String, ActiveProcess)> {
3967        let mut adopted = Vec::new();
3968        let child_ids = process.child_processes.keys().cloned().collect::<Vec<_>>();
3969        for child_id in child_ids {
3970            let child_process_id = format!("{current_process_id}/{child_id}");
3971            let Some(mut child) = process.child_processes.remove(&child_id) else {
3972                continue;
3973            };
3974            if child.detached {
3975                adopted.push((child_process_id, child));
3976                continue;
3977            }
3978
3979            adopted.extend(Self::adopt_detached_child_processes(
3980                &child_process_id,
3981                &mut child,
3982            ));
3983            process.child_processes.insert(child_id, child);
3984        }
3985        adopted
3986    }
3987
3988    fn child_process_signal_key<'a>(process_id: &'a str, child_path: &[&'a str]) -> &'a str {
3989        child_path.last().copied().unwrap_or(process_id)
3990    }
3991
3992    fn resolve_detached_child_process_path(
3993        vm: &VmState,
3994        detached_process_id: &str,
3995    ) -> Option<(String, Vec<String>)> {
3996        let root_process_id = vm
3997            .active_processes
3998            .keys()
3999            .filter(|candidate| {
4000                detached_process_id == candidate.as_str()
4001                    || detached_process_id
4002                        .strip_prefix(candidate.as_str())
4003                        .is_some_and(|remainder| remainder.starts_with('/'))
4004            })
4005            .max_by_key(|candidate| candidate.len())?
4006            .clone();
4007
4008        let remainder = detached_process_id
4009            .strip_prefix(root_process_id.as_str())
4010            .unwrap_or_default();
4011        if remainder.is_empty() {
4012            return Some((root_process_id, Vec::new()));
4013        }
4014
4015        Some((
4016            root_process_id,
4017            remainder
4018                .trim_start_matches('/')
4019                .split('/')
4020                .map(str::to_owned)
4021                .collect(),
4022        ))
4023    }
4024
4025    fn pump_detached_child_process_events(&mut self, vm_id: &str) -> Result<bool, SidecarError> {
4026        let detached_process_ids = self
4027            .vms
4028            .get(vm_id)
4029            .map(|vm| {
4030                vm.detached_child_processes
4031                    .iter()
4032                    .cloned()
4033                    .collect::<Vec<_>>()
4034            })
4035            .unwrap_or_default();
4036        let mut emitted_any = false;
4037        for detached_process_id in detached_process_ids {
4038            let Some((root_process_id, child_path)) = self
4039                .vms
4040                .get(vm_id)
4041                .and_then(|vm| Self::resolve_detached_child_process_path(vm, &detached_process_id))
4042            else {
4043                if let Some(vm) = self.vms.get_mut(vm_id) {
4044                    vm.detached_child_processes.remove(&detached_process_id);
4045                }
4046                continue;
4047            };
4048            if child_path.is_empty() {
4049                loop {
4050                    enum ProcessPollResult {
4051                        Event(Box<Option<ActiveExecutionEvent>>),
4052                        RecoverClosedChannel,
4053                    }
4054                    let poll_result = {
4055                        let Some(vm) = self.vms.get_mut(vm_id) else {
4056                            break;
4057                        };
4058                        let Some(process) = vm.active_processes.get_mut(&root_process_id) else {
4059                            break;
4060                        };
4061                        if let Some(event) = process.pending_execution_events.pop_front() {
4062                            ProcessPollResult::Event(Box::new(Some(event)))
4063                        } else {
4064                            match process.execution.poll_event_blocking(Duration::ZERO) {
4065                                Ok(event) => ProcessPollResult::Event(Box::new(event)),
4066                                Err(SidecarError::Execution(message))
4067                                    if (process.runtime == GuestRuntimeKind::JavaScript
4068                                        && closed_javascript_event_channel(&message))
4069                                        || (process.runtime == GuestRuntimeKind::Python
4070                                            && closed_python_event_channel(&message))
4071                                        || (process.runtime == GuestRuntimeKind::WebAssembly
4072                                            && closed_wasm_event_channel(&message)) =>
4073                                {
4074                                    ProcessPollResult::RecoverClosedChannel
4075                                }
4076                                Err(error) => return Err(error),
4077                            }
4078                        }
4079                    };
4080                    let event = match poll_result {
4081                        ProcessPollResult::Event(event) => *event,
4082                        ProcessPollResult::RecoverClosedChannel => {
4083                            self.recover_closed_root_runtime_process_event(vm_id, &root_process_id)?
4084                        }
4085                    };
4086                    let Some(event) = event else {
4087                        break;
4088                    };
4089                    let Some((connection_id, session_id)) = self
4090                        .vms
4091                        .get(vm_id)
4092                        .map(|vm| (vm.connection_id.clone(), vm.session_id.clone()))
4093                    else {
4094                        break;
4095                    };
4096                    match event {
4097                        ActiveExecutionEvent::Stdout(chunk) => {
4098                            self.queue_pending_process_event(ProcessEventEnvelope {
4099                                connection_id,
4100                                session_id,
4101                                vm_id: vm_id.to_owned(),
4102                                process_id: detached_process_id.clone(),
4103                                event: ActiveExecutionEvent::Stdout(chunk),
4104                            })?;
4105                            emitted_any = true;
4106                        }
4107                        ActiveExecutionEvent::Stderr(chunk) => {
4108                            self.queue_pending_process_event(ProcessEventEnvelope {
4109                                connection_id,
4110                                session_id,
4111                                vm_id: vm_id.to_owned(),
4112                                process_id: detached_process_id.clone(),
4113                                event: ActiveExecutionEvent::Stderr(chunk),
4114                            })?;
4115                            emitted_any = true;
4116                        }
4117                        ActiveExecutionEvent::Exited(exit_code) => {
4118                            if let Some(vm) = self.vms.get_mut(vm_id) {
4119                                vm.detached_child_processes.remove(&detached_process_id);
4120                            }
4121                            self.queue_pending_process_event(ProcessEventEnvelope {
4122                                connection_id,
4123                                session_id,
4124                                vm_id: vm_id.to_owned(),
4125                                process_id: detached_process_id.clone(),
4126                                event: ActiveExecutionEvent::Exited(exit_code),
4127                            })?;
4128                            emitted_any = true;
4129                            break;
4130                        }
4131                        ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
4132                            self.handle_javascript_sync_rpc_request(
4133                                vm_id,
4134                                &root_process_id,
4135                                request,
4136                            )?;
4137                        }
4138                        ActiveExecutionEvent::PythonVfsRpcRequest(request) => {
4139                            self.handle_python_vfs_rpc_request(vm_id, &root_process_id, *request)?;
4140                        }
4141                        ActiveExecutionEvent::SignalState {
4142                            signal,
4143                            registration,
4144                        } => {
4145                            if let Some(vm) = self.vms.get_mut(vm_id) {
4146                                vm.signal_states
4147                                    .entry(root_process_id.clone())
4148                                    .or_default()
4149                                    .insert(signal, registration);
4150                            }
4151                        }
4152                    }
4153                }
4154                continue;
4155            }
4156
4157            let parent_path = child_path[..child_path.len() - 1]
4158                .iter()
4159                .map(String::as_str)
4160                .collect::<Vec<_>>();
4161            let child_process_id = child_path.last().expect("child path cannot be empty");
4162
4163            loop {
4164                let event = match self.poll_descendant_javascript_child_process(
4165                    vm_id,
4166                    &root_process_id,
4167                    &parent_path,
4168                    child_process_id,
4169                    0,
4170                ) {
4171                    Ok(event) => event,
4172                    Err(SidecarError::InvalidState(message))
4173                        if message.contains("unknown child process")
4174                            || message.contains("unknown child process path") =>
4175                    {
4176                        if let Some(vm) = self.vms.get_mut(vm_id) {
4177                            vm.detached_child_processes.remove(&detached_process_id);
4178                        }
4179                        break;
4180                    }
4181                    Err(error) if is_javascript_child_process_gone_error(&error) => {
4182                        if let Some(vm) = self.vms.get_mut(vm_id) {
4183                            vm.detached_child_processes.remove(&detached_process_id);
4184                        }
4185                        break;
4186                    }
4187                    Err(error) => return Err(error),
4188                };
4189
4190                let Some(event_type) = event.get("type").and_then(Value::as_str) else {
4191                    break;
4192                };
4193                let Some((connection_id, session_id)) = self
4194                    .vms
4195                    .get(vm_id)
4196                    .map(|vm| (vm.connection_id.clone(), vm.session_id.clone()))
4197                else {
4198                    break;
4199                };
4200
4201                let envelope = match event_type {
4202                    "stdout" => Some(ProcessEventEnvelope {
4203                        connection_id: connection_id.clone(),
4204                        session_id: session_id.clone(),
4205                        vm_id: vm_id.to_owned(),
4206                        process_id: detached_process_id.clone(),
4207                        event: ActiveExecutionEvent::Stdout(javascript_sync_rpc_bytes_arg(
4208                            &[event.get("data").cloned().unwrap_or(Value::Null)],
4209                            0,
4210                            "detached child_process stdout",
4211                        )?),
4212                    }),
4213                    "stderr" => Some(ProcessEventEnvelope {
4214                        connection_id: connection_id.clone(),
4215                        session_id: session_id.clone(),
4216                        vm_id: vm_id.to_owned(),
4217                        process_id: detached_process_id.clone(),
4218                        event: ActiveExecutionEvent::Stderr(javascript_sync_rpc_bytes_arg(
4219                            &[event.get("data").cloned().unwrap_or(Value::Null)],
4220                            0,
4221                            "detached child_process stderr",
4222                        )?),
4223                    }),
4224                    "exit" => {
4225                        if let Some(vm) = self.vms.get_mut(vm_id) {
4226                            vm.detached_child_processes.remove(&detached_process_id);
4227                        }
4228                        Some(ProcessEventEnvelope {
4229                            connection_id,
4230                            session_id,
4231                            vm_id: vm_id.to_owned(),
4232                            process_id: detached_process_id.clone(),
4233                            event: ActiveExecutionEvent::Exited(
4234                                event
4235                                    .get("exitCode")
4236                                    .and_then(Value::as_i64)
4237                                    .map(|value| value as i32)
4238                                    .unwrap_or(1),
4239                            ),
4240                        })
4241                    }
4242                    _ => None,
4243                };
4244
4245                let Some(envelope) = envelope else {
4246                    break;
4247                };
4248                self.queue_pending_process_event(envelope)?;
4249                emitted_any = true;
4250
4251                if event_type == "exit" {
4252                    break;
4253                }
4254            }
4255        }
4256
4257        Ok(emitted_any)
4258    }
4259    pub(crate) fn drain_queued_descendant_javascript_child_process_events(
4260        &mut self,
4261        vm_id: &str,
4262        process_id: &str,
4263        child_path: &[&str],
4264    ) -> Result<(), SidecarError> {
4265        if child_path.is_empty() {
4266            return Ok(());
4267        }
4268        let target_process_id = Self::child_process_path_label(process_id, child_path);
4269        let mut child_capacity = self
4270            .vms
4271            .get(vm_id)
4272            .and_then(|vm| vm.active_processes.get(process_id))
4273            .and_then(|root| descendant_pending_execution_event_capacity(root, child_path));
4274
4275        let mut deferred = VecDeque::new();
4276        while let Some(envelope) = self.pending_process_events.pop_front() {
4277            if envelope.vm_id == vm_id && envelope.process_id == target_process_id {
4278                if matches!(child_capacity, Some(0)) {
4279                    self.pending_process_events.push_front(envelope);
4280                    while let Some(deferred_envelope) = deferred.pop_back() {
4281                        self.pending_process_events.push_front(deferred_envelope);
4282                    }
4283                    return Err(process_event_queue_overflow_error());
4284                }
4285                if let Some(vm) = self.vms.get_mut(vm_id) {
4286                    if let Some(root) = vm.active_processes.get_mut(process_id) {
4287                        if let Some(child) = Self::active_process_by_path_mut(root, child_path) {
4288                            child.queue_pending_execution_event(envelope.event)?;
4289                            child_capacity = child_capacity.map(|capacity| capacity - 1);
4290                            continue;
4291                        }
4292                    }
4293                }
4294            }
4295            deferred.push_back(envelope);
4296        }
4297        self.pending_process_events = deferred;
4298
4299        let mut queued = Vec::new();
4300        {
4301            let transfer_capacity = self
4302                .pending_process_event_capacity()
4303                .min(child_capacity.unwrap_or(usize::MAX));
4304            let receiver = self.process_event_receiver.as_mut().ok_or_else(|| {
4305                SidecarError::InvalidState(String::from("process event receiver unavailable"))
4306            })?;
4307            loop {
4308                if queued.len() >= transfer_capacity {
4309                    if receiver.is_empty() {
4310                        break;
4311                    }
4312                    return Err(process_event_queue_overflow_error());
4313                }
4314                match receiver.try_recv() {
4315                    Ok(envelope) => queued.push(envelope),
4316                    Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
4317                    Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
4318                }
4319            }
4320        }
4321        for envelope in queued {
4322            if envelope.vm_id == vm_id && envelope.process_id == target_process_id {
4323                if let Some(vm) = self.vms.get_mut(vm_id) {
4324                    if let Some(root) = vm.active_processes.get_mut(process_id) {
4325                        if let Some(child) = Self::active_process_by_path_mut(root, child_path) {
4326                            child.queue_pending_execution_event(envelope.event)?;
4327                            continue;
4328                        }
4329                    }
4330                }
4331            }
4332            self.queue_pending_process_event(envelope)?;
4333        }
4334
4335        Ok(())
4336    }
4337
4338    pub(crate) fn handle_execution_event(
4339        &mut self,
4340        vm_id: &str,
4341        process_id: &str,
4342        event: ActiveExecutionEvent,
4343    ) -> Result<Option<EventFrame>, SidecarError> {
4344        let Some(vm) = self.vms.get(vm_id) else {
4345            log_stale_process_event(&self.bridge, vm_id, process_id, "execution event dispatch");
4346            return Ok(None);
4347        };
4348        if !vm.active_processes.contains_key(process_id) {
4349            log_stale_process_event(&self.bridge, vm_id, process_id, "execution event dispatch");
4350            return Ok(None);
4351        }
4352        let (connection_id, session_id) = { (vm.connection_id.clone(), vm.session_id.clone()) };
4353        let ownership = OwnershipScope::vm(&connection_id, &session_id, vm_id);
4354
4355        if self.capture_extension_process_output_event(vm_id, process_id, &event) {
4356            return Ok(None);
4357        }
4358
4359        match event {
4360            ActiveExecutionEvent::Stdout(chunk) => Ok(Some(EventFrame::new(
4361                ownership,
4362                EventPayload::ProcessOutput(ProcessOutputEvent {
4363                    process_id: process_id.to_owned(),
4364                    channel: StreamChannel::Stdout,
4365                    chunk,
4366                }),
4367            ))),
4368            ActiveExecutionEvent::Stderr(chunk) => Ok(Some(EventFrame::new(
4369                ownership,
4370                EventPayload::ProcessOutput(ProcessOutputEvent {
4371                    process_id: process_id.to_owned(),
4372                    channel: StreamChannel::Stderr,
4373                    chunk,
4374                }),
4375            ))),
4376            ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
4377                self.handle_javascript_sync_rpc_request(vm_id, process_id, request)?;
4378                Ok(None)
4379            }
4380            ActiveExecutionEvent::PythonVfsRpcRequest(request) => {
4381                self.handle_python_vfs_rpc_request(vm_id, process_id, *request)?;
4382                Ok(None)
4383            }
4384            ActiveExecutionEvent::SignalState {
4385                signal,
4386                registration,
4387            } => {
4388                let Some(vm) = self.vms.get_mut(vm_id) else {
4389                    return Ok(None);
4390                };
4391                if !vm.active_processes.contains_key(process_id) {
4392                    return Ok(None);
4393                }
4394                vm.signal_states
4395                    .entry(process_id.to_owned())
4396                    .or_default()
4397                    .insert(signal, registration);
4398                Ok(None)
4399            }
4400            ActiveExecutionEvent::Exited(exit_code) => {
4401                let became_idle = self
4402                    .finish_active_process_exit(vm_id, process_id, exit_code)?
4403                    .unwrap_or(false);
4404
4405                if became_idle {
4406                    self.bridge.emit_lifecycle(vm_id, LifecycleState::Ready)?;
4407                }
4408
4409                Ok(Some(EventFrame::new(
4410                    ownership,
4411                    EventPayload::ProcessExited(ProcessExitedEvent {
4412                        process_id: process_id.to_owned(),
4413                        exit_code,
4414                    }),
4415                )))
4416            }
4417        }
4418    }
4419
4420    pub(crate) fn finish_active_process_exit(
4421        &mut self,
4422        vm_id: &str,
4423        process_id: &str,
4424        exit_code: i32,
4425    ) -> Result<Option<bool>, SidecarError> {
4426        let Some(vm) = self.vms.get_mut(vm_id) else {
4427            log_stale_process_event(&self.bridge, vm_id, process_id, "process exit cleanup");
4428            return Ok(None);
4429        };
4430        if !vm.active_processes.contains_key(process_id) {
4431            log_stale_process_event(&self.bridge, vm_id, process_id, "process exit cleanup");
4432            return Ok(None);
4433        }
4434
4435        prune_exited_process_snapshots(vm);
4436        let process_table = vm.kernel.list_processes();
4437        let Some(mut process) = vm.active_processes.remove(process_id) else {
4438            return Ok(None);
4439        };
4440        if let Some(info) = process_table.get(&process.kernel_pid) {
4441            vm.exited_process_snapshots
4442                .push_back(ExitedProcessSnapshot {
4443                    captured_at: Instant::now(),
4444                    process: build_process_snapshot_entry(
4445                        process_id,
4446                        &process,
4447                        info,
4448                        Some(exit_code),
4449                    ),
4450                });
4451        }
4452        let detached_children = Self::adopt_detached_child_processes(process_id, &mut process);
4453        sync_process_host_writes_to_kernel(vm, &process)?;
4454        terminate_child_process_tree(&mut vm.kernel, &mut process);
4455        process.kernel_handle.finish(exit_code);
4456        let _ = vm.kernel.wait_and_reap(process.kernel_pid);
4457        vm.signal_states.remove(process_id);
4458        for (detached_process_id, detached_child) in detached_children {
4459            vm.detached_child_processes
4460                .insert(detached_process_id.clone());
4461            vm.active_processes
4462                .insert(detached_process_id, detached_child);
4463        }
4464        let became_idle = vm.active_processes.is_empty();
4465        self.prune_extension_process_resource(process_id);
4466
4467        Ok(Some(became_idle))
4468    }
4469
4470    pub(crate) fn drain_process_events_blocking_with_limit(
4471        &mut self,
4472        vm_id: &str,
4473        process_id: &str,
4474        max_events: usize,
4475    ) -> Result<Vec<ActiveExecutionEvent>, SidecarError> {
4476        let mut events = Vec::new();
4477        if max_events == 0 {
4478            return Ok(events);
4479        }
4480        let mut deadline = Instant::now() + Duration::from_millis(150);
4481
4482        loop {
4483            if events.len() >= max_events {
4484                break;
4485            }
4486            let event = {
4487                let Some(vm) = self.vms.get_mut(vm_id) else {
4488                    break;
4489                };
4490                let Some(process) = vm.active_processes.get_mut(process_id) else {
4491                    break;
4492                };
4493                if let Some(event) = process.pending_execution_events.pop_front() {
4494                    Some(event)
4495                } else {
4496                    match process.execution.poll_event_blocking(Duration::ZERO) {
4497                        Ok(event) => event,
4498                        Err(SidecarError::Execution(_)) => None,
4499                        Err(other) => return Err(other),
4500                    }
4501                }
4502            };
4503
4504            let Some(event) = event else {
4505                if Instant::now() >= deadline {
4506                    break;
4507                }
4508                let blocking_wait = deadline.saturating_duration_since(Instant::now());
4509                if blocking_wait.is_zero() {
4510                    break;
4511                }
4512                if events.len() >= max_events {
4513                    break;
4514                }
4515                let delayed_event = {
4516                    let Some(vm) = self.vms.get_mut(vm_id) else {
4517                        break;
4518                    };
4519                    let Some(process) = vm.active_processes.get_mut(process_id) else {
4520                        break;
4521                    };
4522                    if let Some(event) = process.pending_execution_events.pop_front() {
4523                        Some(event)
4524                    } else {
4525                        match process.execution.poll_event_blocking(blocking_wait) {
4526                            Ok(event) => event,
4527                            Err(SidecarError::Execution(_)) => None,
4528                            Err(other) => return Err(other),
4529                        }
4530                    }
4531                };
4532                let Some(event) = delayed_event else {
4533                    break;
4534                };
4535                events.push(event);
4536                deadline = Instant::now() + Duration::from_millis(150);
4537                continue;
4538            };
4539            events.push(event);
4540            deadline = Instant::now() + Duration::from_millis(150);
4541        }
4542
4543        Ok(events)
4544    }
4545
4546    pub(crate) fn handle_python_vfs_rpc_request(
4547        &mut self,
4548        vm_id: &str,
4549        process_id: &str,
4550        request: PythonVfsRpcRequest,
4551    ) -> Result<(), SidecarError> {
4552        match request.method {
4553            PythonVfsRpcMethod::Read
4554            | PythonVfsRpcMethod::Write
4555            | PythonVfsRpcMethod::Stat
4556            | PythonVfsRpcMethod::ReadDir
4557            | PythonVfsRpcMethod::Mkdir => {
4558                filesystem_handle_python_vfs_rpc_request(self, vm_id, process_id, request)
4559            }
4560            PythonVfsRpcMethod::HttpRequest => {
4561                self.handle_python_http_rpc_request(vm_id, process_id, request)
4562            }
4563            PythonVfsRpcMethod::DnsLookup => {
4564                self.handle_python_dns_rpc_request(vm_id, process_id, request)
4565            }
4566            PythonVfsRpcMethod::SubprocessRun => {
4567                self.handle_python_subprocess_rpc_request(vm_id, process_id, request)
4568            }
4569        }
4570    }
4571
4572    fn handle_python_http_rpc_request(
4573        &mut self,
4574        vm_id: &str,
4575        process_id: &str,
4576        request: PythonVfsRpcRequest,
4577    ) -> Result<(), SidecarError> {
4578        let Some(vm) = self.vms.get(vm_id) else {
4579            return Ok(());
4580        };
4581        if !vm.active_processes.contains_key(process_id) {
4582            return Ok(());
4583        }
4584        let response = (|| {
4585            let url_text = request.url.as_deref().ok_or_else(|| {
4586                SidecarError::InvalidState(String::from("python httpRequest requires a url"))
4587            })?;
4588            let url = Url::parse(url_text)
4589                .map_err(|error| SidecarError::Execution(format!("ERR_INVALID_URL: {error}")))?;
4590            let host = url.host_str().ok_or_else(|| {
4591                SidecarError::Execution(String::from("ERR_INVALID_URL: missing host"))
4592            })?;
4593            let port = url.port_or_known_default().ok_or_else(|| {
4594                SidecarError::Execution(String::from("ERR_INVALID_URL: missing port"))
4595            })?;
4596            self.bridge.require_network_access(
4597                vm_id,
4598                NetworkOperation::Http,
4599                format_tcp_resource(host, port),
4600            )?;
4601            // Pin the outbound connection to the IP addresses that pass the
4602            // egress range guard at resolution time. A literal IP is validated
4603            // directly; a hostname is resolved once here and the resulting
4604            // address set is pinned into the HTTP client's resolver below so a
4605            // rebinding DNS server cannot make the second (TLS/TCP) lookup land
4606            // on a private/link-local/metadata IP that this check rejected.
4607            let pinned_addresses = if let Ok(literal_ip) = host.parse::<IpAddr>() {
4608                filter_dns_safe_ip_addrs(vec![literal_ip], host)?
4609            } else {
4610                filter_dns_safe_ip_addrs(
4611                    resolve_dns_ip_addrs(
4612                        &self.bridge,
4613                        &vm.kernel,
4614                        vm_id,
4615                        &vm.dns,
4616                        host,
4617                        DnsLookupPolicy::SkipPermissions,
4618                    )?,
4619                    host,
4620                )?
4621            };
4622            let mut headers = BTreeMap::new();
4623            for (name, value) in &request.headers {
4624                headers.insert(name.clone(), Value::String(value.clone()));
4625            }
4626            let options = JavascriptHttpRequestOptions {
4627                method: Some(
4628                    request
4629                        .http_method
4630                        .clone()
4631                        .unwrap_or_else(|| String::from("GET")),
4632                ),
4633                headers,
4634                body: request.body_base64.as_deref().map(|body| {
4635                    String::from_utf8(
4636                        base64::engine::general_purpose::STANDARD
4637                            .decode(body)
4638                            .unwrap_or_default(),
4639                    )
4640                    .unwrap_or_default()
4641                }),
4642                reject_unauthorized: None,
4643            };
4644            let headers =
4645                parse_http_header_collection(&options.headers, "python httpRequest headers")?;
4646            let response =
4647                issue_outbound_http_request(&url, &options, &headers, &pinned_addresses)?;
4648            let payload_json = response.as_str().ok_or_else(|| {
4649                SidecarError::Execution(String::from(
4650                    "python httpRequest returned a non-string response payload",
4651                ))
4652            })?;
4653            let payload: Value = serde_json::from_str(payload_json).map_err(|error| {
4654                SidecarError::Execution(format!(
4655                    "python httpRequest response must be valid JSON: {error}"
4656                ))
4657            })?;
4658            let header_map = payload
4659                .get("headers")
4660                .and_then(Value::as_array)
4661                .map(|entries| {
4662                    let mut normalized = BTreeMap::<String, Vec<String>>::new();
4663                    for entry in entries {
4664                        let Some(pair) = entry.as_array() else {
4665                            continue;
4666                        };
4667                        let Some(name) = pair.first().and_then(Value::as_str) else {
4668                            continue;
4669                        };
4670                        let Some(value) = pair.get(1).and_then(Value::as_str) else {
4671                            continue;
4672                        };
4673                        normalized
4674                            .entry(name.to_owned())
4675                            .or_default()
4676                            .push(value.to_owned());
4677                    }
4678                    normalized
4679                })
4680                .unwrap_or_default();
4681            Ok(PythonVfsRpcResponsePayload::Http {
4682                status: payload
4683                    .get("status")
4684                    .and_then(Value::as_u64)
4685                    .map(|value| value as u16)
4686                    .unwrap_or_default(),
4687                reason: payload
4688                    .get("statusText")
4689                    .and_then(Value::as_str)
4690                    .unwrap_or_default()
4691                    .to_owned(),
4692                url: payload
4693                    .get("url")
4694                    .and_then(Value::as_str)
4695                    .unwrap_or(url_text)
4696                    .to_owned(),
4697                headers: header_map,
4698                body_base64: payload
4699                    .get("body")
4700                    .and_then(Value::as_str)
4701                    .unwrap_or_default()
4702                    .to_owned(),
4703            })
4704        })();
4705
4706        self.respond_python_rpc(vm_id, process_id, request.id, response)
4707    }
4708
4709    fn handle_python_dns_rpc_request(
4710        &mut self,
4711        vm_id: &str,
4712        process_id: &str,
4713        request: PythonVfsRpcRequest,
4714    ) -> Result<(), SidecarError> {
4715        let Some(vm) = self.vms.get(vm_id) else {
4716            return Ok(());
4717        };
4718        if !vm.active_processes.contains_key(process_id) {
4719            return Ok(());
4720        }
4721        let response = (|| {
4722            let hostname = request.hostname.as_deref().ok_or_else(|| {
4723                SidecarError::InvalidState(String::from("python dnsLookup requires a hostname"))
4724            })?;
4725            let mut addresses = filter_dns_safe_ip_addrs(
4726                resolve_dns_ip_addrs(
4727                    &self.bridge,
4728                    &vm.kernel,
4729                    vm_id,
4730                    &vm.dns,
4731                    hostname,
4732                    DnsLookupPolicy::CheckPermissions,
4733                )?,
4734                hostname,
4735            )?;
4736            if let Some(family) = request.family {
4737                addresses.retain(|address| {
4738                    matches!((family, address), (4, IpAddr::V4(_)) | (6, IpAddr::V6(_)))
4739                });
4740            }
4741            Ok(PythonVfsRpcResponsePayload::DnsLookup {
4742                addresses: addresses
4743                    .into_iter()
4744                    .map(|address| address.to_string())
4745                    .collect(),
4746            })
4747        })();
4748
4749        self.respond_python_rpc(vm_id, process_id, request.id, response)
4750    }
4751
4752    fn handle_python_subprocess_rpc_request(
4753        &mut self,
4754        vm_id: &str,
4755        process_id: &str,
4756        request: PythonVfsRpcRequest,
4757    ) -> Result<(), SidecarError> {
4758        let command = request.command.clone().ok_or_else(|| {
4759            SidecarError::InvalidState(String::from("python subprocessRun requires a command"))
4760        })?;
4761        let (internal_bootstrap_env, cwd) = {
4762            let Some(vm) = self.vms.get(vm_id) else {
4763                return Ok(());
4764            };
4765            let Some(process) = vm.active_processes.get(process_id) else {
4766                return Ok(());
4767            };
4768            let virtual_home = guest_virtual_home(vm);
4769            let cwd = request.cwd.clone().or_else(|| {
4770                guest_runtime_path_for_host_path(
4771                    &vm.guest_env,
4772                    &virtual_home,
4773                    &vm.host_cwd,
4774                    &process.host_cwd.to_string_lossy(),
4775                )
4776            });
4777            (
4778                sanitize_javascript_child_process_internal_bootstrap_env(&vm.guest_env),
4779                cwd,
4780            )
4781        };
4782        let response = self
4783            .spawn_javascript_child_process_sync(
4784                vm_id,
4785                process_id,
4786                JavascriptChildProcessSpawnRequest {
4787                    command,
4788                    args: request.args.clone(),
4789                    options: JavascriptChildProcessSpawnOptions {
4790                        cwd,
4791                        env: request.env.clone(),
4792                        input: None,
4793                        internal_bootstrap_env,
4794                        shell: request.shell,
4795                        detached: false,
4796                        stdio: vec![
4797                            String::from("pipe"),
4798                            String::from("pipe"),
4799                            String::from("pipe"),
4800                        ],
4801                        timeout: None,
4802                        kill_signal: None,
4803                    },
4804                },
4805                request.max_buffer,
4806            )
4807            .map(|payload| PythonVfsRpcResponsePayload::SubprocessRun {
4808                exit_code: payload
4809                    .get("code")
4810                    .and_then(Value::as_i64)
4811                    .map(|value| value as i32)
4812                    .unwrap_or(1),
4813                stdout: payload
4814                    .get("stdout")
4815                    .and_then(Value::as_str)
4816                    .unwrap_or_default()
4817                    .to_owned(),
4818                stderr: payload
4819                    .get("stderr")
4820                    .and_then(Value::as_str)
4821                    .unwrap_or_default()
4822                    .to_owned(),
4823                max_buffer_exceeded: payload
4824                    .get("maxBufferExceeded")
4825                    .and_then(Value::as_bool)
4826                    .unwrap_or(false),
4827            });
4828
4829        self.respond_python_rpc(vm_id, process_id, request.id, response)
4830    }
4831
4832    fn respond_python_rpc(
4833        &mut self,
4834        vm_id: &str,
4835        process_id: &str,
4836        request_id: u64,
4837        response: Result<PythonVfsRpcResponsePayload, SidecarError>,
4838    ) -> Result<(), SidecarError> {
4839        let Some(vm) = self.vms.get_mut(vm_id) else {
4840            return Ok(());
4841        };
4842        let Some(process) = vm.active_processes.get_mut(process_id) else {
4843            return Ok(());
4844        };
4845        let result = match response {
4846            Ok(payload) => process
4847                .execution
4848                .respond_python_vfs_rpc_success(request_id, payload),
4849            Err(error) => process.execution.respond_python_vfs_rpc_error(
4850                request_id,
4851                "ERR_AGENT_OS_PYTHON_VFS_RPC",
4852                error.to_string(),
4853            ),
4854        };
4855        match result {
4856            Ok(()) => Ok(()),
4857            Err(error) if is_broken_pipe_error(&error) => Ok(()),
4858            Err(error) => Err(error),
4859        }
4860    }
4861
4862    pub(crate) fn resolve_javascript_child_process_execution(
4863        &self,
4864        vm: &VmState,
4865        parent_env: &BTreeMap<String, String>,
4866        parent_guest_cwd: &str,
4867        parent_host_cwd: &Path,
4868        request: &JavascriptChildProcessSpawnRequest,
4869    ) -> Result<ResolvedChildProcessExecution, SidecarError> {
4870        let mut runtime_env = parent_env.clone();
4871        runtime_env.extend(request.options.internal_bootstrap_env.clone());
4872        let (guest_cwd, host_cwd_override) = request
4873            .options
4874            .cwd
4875            .as_deref()
4876            .map(|cwd| {
4877                let normalized_parent_host_cwd = normalize_host_path(parent_host_cwd);
4878                let requested_host_cwd = normalize_host_path(Path::new(cwd));
4879                if path_is_within_root(&requested_host_cwd, &normalized_parent_host_cwd) {
4880                    let relative = requested_host_cwd
4881                        .strip_prefix(&normalized_parent_host_cwd)
4882                        .unwrap_or_else(|_| Path::new(""));
4883                    let relative = relative.to_string_lossy().replace('\\', "/");
4884                    let guest_cwd = if relative.is_empty() {
4885                        parent_guest_cwd.to_owned()
4886                    } else {
4887                        normalize_path(&format!("{parent_guest_cwd}/{relative}"))
4888                    };
4889                    (guest_cwd, Some(requested_host_cwd))
4890                } else if Path::new(cwd).is_relative() {
4891                    (
4892                        normalize_path(&format!("{parent_guest_cwd}/{cwd}")),
4893                        Some(normalize_host_path(&parent_host_cwd.join(cwd))),
4894                    )
4895                } else {
4896                    (normalize_path(cwd), None)
4897                }
4898            })
4899            .unwrap_or_else(|| (parent_guest_cwd.to_owned(), None));
4900        let inherited_host_cwd = (host_cwd_override.is_none() && guest_cwd == parent_guest_cwd)
4901            .then(|| normalize_host_path(parent_host_cwd));
4902        let host_cwd = host_cwd_override
4903            .or(inherited_host_cwd)
4904            .or_else(|| {
4905                host_runtime_path_for_guest_path_with_env(
4906                    vm,
4907                    &runtime_env,
4908                    &guest_cwd,
4909                    parent_host_cwd,
4910                )
4911            })
4912            .unwrap_or_else(|| {
4913                let candidate = PathBuf::from(&guest_cwd);
4914                if guest_cwd == parent_guest_cwd {
4915                    normalize_host_path(parent_host_cwd)
4916                } else if candidate.is_absolute() {
4917                    shadow_path_for_guest(vm, &guest_cwd)
4918                } else {
4919                    vm.host_cwd.clone()
4920                }
4921            });
4922        let mut env = parent_env.clone();
4923        env.extend(request.options.env.clone());
4924        // Child JavaScript executions must resolve their own entrypoint/eval state.
4925        // Reusing the parent's values makes the sidecar load the wrong source file.
4926        env.remove("AGENT_OS_GUEST_ENTRYPOINT");
4927        env.remove("AGENT_OS_NODE_EVAL");
4928
4929        let (command, process_args) = if request.options.shell {
4930            let tokens = tokenize_shell_free_command(&request.command);
4931            let requires_shell = command_requires_shell(&request.command)
4932                || tokens.first().is_some_and(|command| {
4933                    is_posix_shell_builtin(command) || shell_first_token_requires_shell(command)
4934                });
4935            if requires_shell {
4936                if !vm.command_guest_paths.contains_key("sh") {
4937                    return Err(SidecarError::InvalidState(format!(
4938                        "shell-mode child_process command requires /bin/sh, which is not \
4939                         installed in this VM (install a software package that provides sh, \
4940                         for example @secure-exec/coreutils): {}",
4941                        request.command
4942                    )));
4943                }
4944                (
4945                    String::from("sh"),
4946                    vec![String::from("-c"), request.command.clone()],
4947                )
4948            } else {
4949                let Some((command, args)) = tokens.split_first() else {
4950                    return Err(SidecarError::InvalidState(String::from(
4951                        "child_process shell command must not be empty",
4952                    )));
4953                };
4954                (command.clone(), args.to_vec())
4955            }
4956        } else {
4957            (request.command.clone(), request.args.clone())
4958        };
4959        let process_args = apply_shell_cwd_prefix(&command, process_args, &guest_cwd);
4960        if is_tool_command(vm, &command) {
4961            let command = normalized_tool_command_name(&command).unwrap_or(command);
4962            return Ok(ResolvedChildProcessExecution {
4963                command: command.clone(),
4964                process_args: std::iter::once(command.clone())
4965                    .chain(process_args.iter().cloned())
4966                    .collect(),
4967                runtime: GuestRuntimeKind::JavaScript,
4968                entrypoint: command,
4969                execution_args: process_args,
4970                env,
4971                guest_cwd,
4972                host_cwd,
4973                wasm_permission_tier: None,
4974                tool_command: true,
4975            });
4976        }
4977
4978        if is_path_like_specifier(&command)
4979            && matches!(
4980                Path::new(&command).extension().and_then(|ext| ext.to_str()),
4981                Some("js" | "mjs" | "cjs" | "ts" | "mts" | "cts")
4982            )
4983        {
4984            let guest_entrypoint = if command.starts_with('/') {
4985                normalize_path(&command)
4986            } else if command.starts_with("file:") {
4987                normalize_path(command.trim_start_matches("file:"))
4988            } else {
4989                normalize_path(&format!("{guest_cwd}/{command}"))
4990            };
4991            let host_entrypoint = if command.starts_with("./") || command.starts_with("../") {
4992                normalize_host_path(&host_cwd.join(&command))
4993            } else {
4994                host_runtime_path_for_guest_path_with_env(
4995                    vm,
4996                    &runtime_env,
4997                    &guest_entrypoint,
4998                    parent_host_cwd,
4999                )
5000                .unwrap_or_else(|| {
5001                    let candidate = PathBuf::from(&guest_entrypoint);
5002                    if candidate.is_absolute() {
5003                        candidate
5004                    } else {
5005                        host_cwd.join(&guest_entrypoint)
5006                    }
5007                })
5008            };
5009            env.insert(String::from("AGENT_OS_GUEST_ENTRYPOINT"), guest_entrypoint);
5010            let guest_entrypoint = env.get("AGENT_OS_GUEST_ENTRYPOINT").cloned();
5011            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
5012
5013            return Ok(ResolvedChildProcessExecution {
5014                command: command.clone(),
5015                process_args: std::iter::once(command)
5016                    .chain(process_args.iter().cloned())
5017                    .collect(),
5018                runtime: GuestRuntimeKind::JavaScript,
5019                entrypoint: host_entrypoint.to_string_lossy().into_owned(),
5020                execution_args: process_args,
5021                env,
5022                guest_cwd,
5023                host_cwd,
5024                wasm_permission_tier: None,
5025                tool_command: false,
5026            });
5027        }
5028
5029        if is_node_runtime_command(&command) {
5030            if let Some(cli) = resolve_host_node_cli_entrypoint(&command) {
5031                env.insert(
5032                    String::from("AGENT_OS_NODE_EVAL"),
5033                    build_host_node_cli_eval(&cli),
5034                );
5035                prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
5036                add_runtime_guest_path_mapping(&mut env, &cli.guest_root, &cli.package_root);
5037                add_runtime_host_access_path(
5038                    &mut env,
5039                    "AGENT_OS_EXTRA_FS_READ_PATHS",
5040                    &cli.package_root,
5041                    true,
5042                );
5043
5044                return Ok(ResolvedChildProcessExecution {
5045                    command: command.clone(),
5046                    process_args: std::iter::once(command.clone())
5047                        .chain(process_args.iter().cloned())
5048                        .collect(),
5049                    runtime: GuestRuntimeKind::JavaScript,
5050                    entrypoint: String::from("-e"),
5051                    execution_args: std::iter::once(cli.guest_entrypoint.clone())
5052                        .chain(process_args.iter().cloned())
5053                        .collect(),
5054                    env,
5055                    guest_cwd,
5056                    host_cwd,
5057                    wasm_permission_tier: None,
5058                    tool_command: false,
5059                });
5060            }
5061
5062            if process_args.is_empty() {
5063                env.insert(String::from("AGENT_OS_NODE_EVAL"), String::new());
5064                prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
5065
5066                return Ok(ResolvedChildProcessExecution {
5067                    command: command.clone(),
5068                    process_args: vec![command.clone()],
5069                    runtime: GuestRuntimeKind::JavaScript,
5070                    entrypoint: String::from("-e"),
5071                    execution_args: Vec::new(),
5072                    env,
5073                    guest_cwd,
5074                    host_cwd,
5075                    wasm_permission_tier: None,
5076                    tool_command: false,
5077                });
5078            }
5079
5080            if let Some((entrypoint, execution_args)) =
5081                resolve_special_node_cli_invocation(&process_args, &mut env)
5082            {
5083                prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
5084
5085                return Ok(ResolvedChildProcessExecution {
5086                    command: command.clone(),
5087                    process_args: std::iter::once(command.clone())
5088                        .chain(process_args.iter().cloned())
5089                        .collect(),
5090                    runtime: GuestRuntimeKind::JavaScript,
5091                    entrypoint,
5092                    execution_args,
5093                    env,
5094                    guest_cwd,
5095                    host_cwd,
5096                    wasm_permission_tier: None,
5097                    tool_command: false,
5098                });
5099            }
5100
5101            let Some(entrypoint_specifier) = process_args.first() else {
5102                return Err(SidecarError::InvalidState(format!(
5103                    "{command} child_process spawn requires an entrypoint"
5104                )));
5105            };
5106
5107            let (entrypoint, execution_args) = if is_path_like_specifier(entrypoint_specifier) {
5108                let guest_entrypoint = if entrypoint_specifier.starts_with('/') {
5109                    normalize_path(entrypoint_specifier)
5110                } else if entrypoint_specifier.starts_with("file:") {
5111                    normalize_path(entrypoint_specifier.trim_start_matches("file:"))
5112                } else {
5113                    normalize_path(&format!("{guest_cwd}/{entrypoint_specifier}"))
5114                };
5115                let host_entrypoint = if entrypoint_specifier.starts_with("./")
5116                    || entrypoint_specifier.starts_with("../")
5117                {
5118                    normalize_host_path(&host_cwd.join(entrypoint_specifier))
5119                } else {
5120                    host_runtime_path_for_guest_path_with_env(
5121                        vm,
5122                        &runtime_env,
5123                        &guest_entrypoint,
5124                        parent_host_cwd,
5125                    )
5126                    .unwrap_or_else(|| {
5127                        let candidate = PathBuf::from(&guest_entrypoint);
5128                        if candidate.is_absolute() {
5129                            candidate
5130                        } else {
5131                            host_cwd.join(&guest_entrypoint)
5132                        }
5133                    })
5134                };
5135                env.insert(String::from("AGENT_OS_GUEST_ENTRYPOINT"), guest_entrypoint);
5136                (
5137                    host_entrypoint.to_string_lossy().into_owned(),
5138                    process_args.iter().skip(1).cloned().collect(),
5139                )
5140            } else {
5141                (
5142                    entrypoint_specifier.clone(),
5143                    process_args.iter().skip(1).cloned().collect(),
5144                )
5145            };
5146            let guest_entrypoint = env.get("AGENT_OS_GUEST_ENTRYPOINT").cloned();
5147            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
5148
5149            return Ok(ResolvedChildProcessExecution {
5150                command: command.clone(),
5151                process_args: std::iter::once(command)
5152                    .chain(process_args.iter().cloned())
5153                    .collect(),
5154                runtime: GuestRuntimeKind::JavaScript,
5155                entrypoint,
5156                execution_args,
5157                env,
5158                guest_cwd,
5159                host_cwd,
5160                wasm_permission_tier: None,
5161                tool_command: false,
5162            });
5163        }
5164
5165        if command == PYTHON_COMMAND {
5166            return Err(SidecarError::InvalidState(String::from(
5167                "nested python child_process execution is not supported yet",
5168            )));
5169        }
5170
5171        let guest_entrypoint = resolve_guest_command_entrypoint(
5172            vm,
5173            &guest_cwd,
5174            &command,
5175            env.get("PATH").map(String::as_str),
5176        )
5177        .ok_or_else(|| SidecarError::InvalidState(format!("command not found: {command}")))?;
5178        let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint);
5179        let wasm_permission_tier = vm.command_permissions.get(&command).copied().or_else(|| {
5180            Path::new(&guest_entrypoint)
5181                .file_name()
5182                .and_then(|name| name.to_str())
5183                .and_then(|name| vm.command_permissions.get(name).copied())
5184        });
5185        if let Some((javascript_guest_entrypoint, javascript_host_entrypoint)) =
5186            resolve_javascript_command_entrypoint(vm, &guest_entrypoint, &host_entrypoint)
5187        {
5188            prepare_guest_runtime_env(
5189                vm,
5190                &mut env,
5191                &guest_cwd,
5192                &host_cwd,
5193                Some(javascript_guest_entrypoint),
5194            )?;
5195
5196            return Ok(ResolvedChildProcessExecution {
5197                command: command.clone(),
5198                process_args: std::iter::once(command)
5199                    .chain(process_args.iter().cloned())
5200                    .collect(),
5201                runtime: GuestRuntimeKind::JavaScript,
5202                entrypoint: javascript_host_entrypoint.to_string_lossy().into_owned(),
5203                execution_args: process_args,
5204                env,
5205                guest_cwd,
5206                host_cwd,
5207                wasm_permission_tier: None,
5208                tool_command: false,
5209            });
5210        }
5211        prepare_guest_runtime_env(
5212            vm,
5213            &mut env,
5214            &guest_cwd,
5215            &host_cwd,
5216            Some(guest_entrypoint.clone()),
5217        )?;
5218
5219        Ok(ResolvedChildProcessExecution {
5220            command: command.clone(),
5221            process_args: std::iter::once(command)
5222                .chain(process_args.iter().cloned())
5223                .collect(),
5224            runtime: GuestRuntimeKind::WebAssembly,
5225            entrypoint: host_entrypoint.to_string_lossy().into_owned(),
5226            execution_args: process_args,
5227            env,
5228            guest_cwd,
5229            host_cwd,
5230            wasm_permission_tier,
5231            tool_command: false,
5232        })
5233    }
5234
5235    pub(crate) fn spawn_javascript_child_process(
5236        &mut self,
5237        vm_id: &str,
5238        process_id: &str,
5239        request: JavascriptChildProcessSpawnRequest,
5240    ) -> Result<Value, SidecarError> {
5241        let resolved = {
5242            let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?;
5243            let parent = vm
5244                .active_processes
5245                .get(process_id)
5246                .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5247            self.resolve_javascript_child_process_execution(
5248                vm,
5249                &parent.env,
5250                &parent.guest_cwd,
5251                &parent.host_cwd,
5252                &request,
5253            )?
5254        };
5255        let (parent_kernel_pid, child_process_id) = {
5256            let vm = self
5257                .vms
5258                .get_mut(vm_id)
5259                .ok_or_else(|| missing_vm_error(vm_id))?;
5260            let process = vm
5261                .active_processes
5262                .get_mut(process_id)
5263                .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5264            (process.kernel_pid, process.allocate_child_process_id())
5265        };
5266        let sidecar_requests = self.sidecar_requests.clone();
5267        let vm = self
5268            .vms
5269            .get_mut(vm_id)
5270            .ok_or_else(|| missing_vm_error(vm_id))?;
5271        let (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd) = if resolved
5272            .tool_command
5273        {
5274            let tool_resolution = resolve_tool_command(
5275                vm,
5276                &resolved.command,
5277                &resolved.execution_args,
5278                Some(&resolved.guest_cwd),
5279            )?
5280            .ok_or_else(|| {
5281                SidecarError::InvalidState(format!(
5282                    "tool command no longer resolves: {}",
5283                    resolved.command
5284                ))
5285            })?;
5286            let kernel_handle = vm
5287                .kernel
5288                .create_virtual_process(
5289                    EXECUTION_DRIVER_NAME,
5290                    TOOL_DRIVER_NAME,
5291                    &resolved.command,
5292                    resolved.process_args.clone(),
5293                    VirtualProcessOptions {
5294                        parent_pid: Some(parent_kernel_pid),
5295                        env: resolved.env.clone(),
5296                        cwd: Some(resolved.guest_cwd.clone()),
5297                    },
5298                )
5299                .map_err(kernel_error)?;
5300            let kernel_pid = kernel_handle.pid();
5301            let tool_execution = ToolExecution::default();
5302            let cancelled = tool_execution.cancelled.clone();
5303            let pending_events = tool_execution.pending_events.clone();
5304            let events_overflowed = tool_execution.events_overflowed.clone();
5305            spawn_tool_process_events(ToolProcessEventRequest {
5306                sidecar_requests: sidecar_requests.clone(),
5307                connection_id: vm.connection_id.clone(),
5308                session_id: vm.session_id.clone(),
5309                vm_id: vm_id.to_owned(),
5310                tool_resolution,
5311                cancelled,
5312                pending_events,
5313                events_overflowed,
5314            });
5315            (
5316                kernel_pid,
5317                kernel_handle,
5318                ActiveExecution::Tool(tool_execution),
5319                None,
5320            )
5321        } else {
5322            let kernel_command = match resolved.runtime {
5323                GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND,
5324                GuestRuntimeKind::WebAssembly => WASM_COMMAND,
5325                GuestRuntimeKind::Python => {
5326                    unreachable!("python child_process execution is rejected")
5327                }
5328            };
5329            let kernel_handle = vm
5330                .kernel
5331                .spawn_process(
5332                    kernel_command,
5333                    resolved.process_args.clone(),
5334                    SpawnOptions {
5335                        requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
5336                        parent_pid: Some(parent_kernel_pid),
5337                        env: resolved.env.clone(),
5338                        cwd: Some(resolved.guest_cwd.clone()),
5339                    },
5340                )
5341                .map_err(kernel_error)?;
5342            let kernel_pid = kernel_handle.pid();
5343            if request.options.detached {
5344                vm.kernel
5345                    .setsid(EXECUTION_DRIVER_NAME, kernel_pid)
5346                    .map_err(kernel_error)?;
5347            }
5348            let mut execution_env = resolved.env.clone();
5349            execution_env.insert(
5350                String::from(EXECUTION_SANDBOX_ROOT_ENV),
5351                normalize_host_path(&vm.cwd).to_string_lossy().into_owned(),
5352            );
5353
5354            let execution = match resolved.runtime {
5355                GuestRuntimeKind::JavaScript => {
5356                    execution_env.extend(sanitize_javascript_child_process_internal_bootstrap_env(
5357                        &request.options.internal_bootstrap_env,
5358                    ));
5359                    execution_env.insert(
5360                        String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
5361                        String::from("1"),
5362                    );
5363                    let context =
5364                        self.javascript_engine
5365                            .create_context(CreateJavascriptContextRequest {
5366                                vm_id: vm_id.to_owned(),
5367                                bootstrap_module: None,
5368                                compile_cache_root: Some(
5369                                    self.cache_root.join("node-compile-cache"),
5370                                ),
5371                            });
5372                    let inline_code = load_javascript_entrypoint_source(
5373                        vm,
5374                        &resolved.host_cwd,
5375                        &resolved.entrypoint,
5376                        &execution_env,
5377                    );
5378                    prepare_javascript_shadow(vm, &resolved)?;
5379
5380                    let module_reader = build_module_reader(vm, &resolved)
5381                        .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
5382                    let execution = self
5383                        .javascript_engine
5384                        .start_execution_with_module_reader(
5385                            StartJavascriptExecutionRequest {
5386                                guest_runtime: guest_runtime_identity(
5387                                    vm,
5388                                    Some(u64::from(kernel_pid)),
5389                                    Some(u64::from(parent_kernel_pid)),
5390                                ),
5391                                vm_id: vm_id.to_owned(),
5392                                context_id: context.context_id,
5393                                argv: std::iter::once(resolved.entrypoint.clone())
5394                                    .chain(resolved.execution_args.clone())
5395                                    .collect(),
5396                                env: execution_env,
5397                                cwd: resolved.host_cwd.clone(),
5398                                limits: javascript_execution_limits(vm),
5399                                inline_code,
5400                            },
5401                            module_reader,
5402                        )
5403                        .map_err(javascript_error)?;
5404                    ActiveExecution::Javascript(execution)
5405                }
5406                GuestRuntimeKind::WebAssembly => {
5407                    execution_env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
5408                    let wasm_limits = wasm_execution_limits(vm);
5409                    let wasm_guest_runtime = guest_runtime_identity(
5410                        vm,
5411                        Some(u64::from(kernel_pid)),
5412                        Some(u64::from(parent_kernel_pid)),
5413                    );
5414                    let context = self.wasm_engine.create_context(CreateWasmContextRequest {
5415                        vm_id: vm_id.to_owned(),
5416                        module_path: Some(resolved.entrypoint.clone()),
5417                    });
5418                    let execution = self
5419                        .wasm_engine
5420                        .start_execution(StartWasmExecutionRequest {
5421                            vm_id: vm_id.to_owned(),
5422                            context_id: context.context_id,
5423                            argv: resolved.process_args.clone(),
5424                            env: execution_env,
5425                            cwd: resolved.host_cwd.clone(),
5426                            permission_tier: execution_wasm_permission_tier(
5427                                resolved
5428                                    .wasm_permission_tier
5429                                    .unwrap_or(WasmPermissionTier::Full),
5430                            ),
5431                            limits: wasm_limits,
5432                            guest_runtime: wasm_guest_runtime,
5433                        })
5434                        .map_err(wasm_error)?;
5435                    ActiveExecution::Wasm(Box::new(execution))
5436                }
5437                GuestRuntimeKind::Python => {
5438                    unreachable!("python child_process execution is rejected")
5439                }
5440            };
5441            let kernel_stdin_writer_fd = match javascript_child_process_stdin_mode(&request) {
5442                "pipe" => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5443                "ignore" => {
5444                    vm.kernel
5445                        .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, 0)
5446                        .map_err(kernel_error)?;
5447                    None
5448                }
5449                "inherit" => None,
5450                _ => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5451            };
5452            (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd)
5453        };
5454
5455        let process = vm
5456            .active_processes
5457            .get_mut(process_id)
5458            .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5459        process.child_processes.insert(
5460            child_process_id.clone(),
5461            ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
5462                .with_detached(request.options.detached)
5463                .with_guest_cwd(resolved.guest_cwd.clone())
5464                .with_env(resolved.env.clone())
5465                .with_host_cwd(resolved.host_cwd.clone()),
5466        );
5467        if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd {
5468            process
5469                .child_processes
5470                .get_mut(&child_process_id)
5471                .ok_or_else(|| {
5472                    SidecarError::InvalidState(format!(
5473                        "child process {child_process_id} disappeared during spawn"
5474                    ))
5475                })?
5476                .kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd);
5477        }
5478        Ok(json!({
5479            "childId": child_process_id,
5480            "pid": kernel_pid,
5481            "command": resolved.command,
5482            "args": resolved.process_args,
5483        }))
5484    }
5485
5486    pub(crate) fn spawn_javascript_child_process_sync(
5487        &mut self,
5488        vm_id: &str,
5489        process_id: &str,
5490        request: JavascriptChildProcessSpawnRequest,
5491        max_buffer: Option<usize>,
5492    ) -> Result<Value, SidecarError> {
5493        let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?;
5494        let timeout_deadline = request
5495            .options
5496            .timeout
5497            .map(|timeout_ms| Instant::now() + Duration::from_millis(timeout_ms));
5498        let timeout_signal = request
5499            .options
5500            .kill_signal
5501            .clone()
5502            .unwrap_or_else(|| String::from("SIGTERM"));
5503        let spawned = self.spawn_javascript_child_process(vm_id, process_id, request)?;
5504        let child_process_id = spawned
5505            .get("childId")
5506            .and_then(Value::as_str)
5507            .ok_or_else(|| {
5508                SidecarError::InvalidState(String::from(
5509                    "child_process.spawn_sync response is missing childId",
5510                ))
5511            })?
5512            .to_owned();
5513
5514        if let Some(input) = sync_input.as_deref() {
5515            self.write_javascript_child_process_stdin(vm_id, process_id, &child_process_id, input)?;
5516        }
5517        self.close_javascript_child_process_stdin(vm_id, process_id, &child_process_id)?;
5518
5519        let max_buffer = max_buffer.unwrap_or(1024 * 1024);
5520        let mut stdout = Vec::new();
5521        let mut stderr = Vec::new();
5522        let mut max_buffer_exceeded = false;
5523        let mut kill_sent = false;
5524        let mut timed_out = false;
5525
5526        let exit_code = loop {
5527            let wait_ms = if let Some(deadline) = timeout_deadline {
5528                let now = Instant::now();
5529                if now >= deadline {
5530                    if !kill_sent {
5531                        timed_out = true;
5532                        self.kill_javascript_child_process(
5533                            vm_id,
5534                            process_id,
5535                            &child_process_id,
5536                            &timeout_signal,
5537                        )?;
5538                        kill_sent = true;
5539                    }
5540                    0
5541                } else {
5542                    u64::try_from(deadline.saturating_duration_since(now).as_millis().min(50))
5543                        .unwrap_or(50)
5544                }
5545            } else {
5546                50
5547            };
5548            let event =
5549                self.poll_javascript_child_process(vm_id, process_id, &child_process_id, wait_ms)?;
5550            if event.is_null() {
5551                continue;
5552            }
5553
5554            match event.get("type").and_then(Value::as_str) {
5555                Some("stdout") => {
5556                    let chunk = javascript_sync_rpc_bytes_arg(
5557                        &[event.get("data").cloned().unwrap_or(Value::Null)],
5558                        0,
5559                        "child_process.spawn_sync stdout",
5560                    )?;
5561                    stdout.extend_from_slice(&chunk);
5562                    if stdout.len() > max_buffer && !kill_sent {
5563                        max_buffer_exceeded = true;
5564                        self.kill_javascript_child_process(
5565                            vm_id,
5566                            process_id,
5567                            &child_process_id,
5568                            "SIGTERM",
5569                        )?;
5570                        kill_sent = true;
5571                    }
5572                }
5573                Some("stderr") => {
5574                    let chunk = javascript_sync_rpc_bytes_arg(
5575                        &[event.get("data").cloned().unwrap_or(Value::Null)],
5576                        0,
5577                        "child_process.spawn_sync stderr",
5578                    )?;
5579                    stderr.extend_from_slice(&chunk);
5580                    if stderr.len() > max_buffer && !kill_sent {
5581                        max_buffer_exceeded = true;
5582                        self.kill_javascript_child_process(
5583                            vm_id,
5584                            process_id,
5585                            &child_process_id,
5586                            "SIGTERM",
5587                        )?;
5588                        kill_sent = true;
5589                    }
5590                }
5591                Some("exit") => {
5592                    break event
5593                        .get("exitCode")
5594                        .and_then(Value::as_i64)
5595                        .map(|value| value as i32)
5596                        .unwrap_or(1);
5597                }
5598                _ => {}
5599            }
5600        };
5601
5602        Ok(json!({
5603            "stdout": String::from_utf8_lossy(&stdout),
5604            "stderr": String::from_utf8_lossy(&stderr),
5605            "code": exit_code,
5606            "signal": if timed_out { Value::String(timeout_signal) } else { Value::Null },
5607            "timedOut": timed_out,
5608            "maxBufferExceeded": max_buffer_exceeded,
5609        }))
5610    }
5611
5612    fn spawn_descendant_javascript_child_process(
5613        &mut self,
5614        vm_id: &str,
5615        process_id: &str,
5616        current_process_path: &[&str],
5617        request: JavascriptChildProcessSpawnRequest,
5618    ) -> Result<Value, SidecarError> {
5619        let current_process_label =
5620            Self::child_process_path_label(process_id, current_process_path);
5621        let (resolved, parent_kernel_pid) = {
5622            let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?;
5623            let root = vm
5624                .active_processes
5625                .get(process_id)
5626                .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5627            let parent =
5628                Self::active_process_by_path(root, current_process_path).ok_or_else(|| {
5629                    SidecarError::InvalidState(format!(
5630                        "unknown child process path {current_process_label} during nested spawn"
5631                    ))
5632                })?;
5633            (
5634                self.resolve_javascript_child_process_execution(
5635                    vm,
5636                    &parent.env,
5637                    &parent.guest_cwd,
5638                    &parent.host_cwd,
5639                    &request,
5640                )?,
5641                parent.kernel_pid,
5642            )
5643        };
5644
5645        let sidecar_requests = self.sidecar_requests.clone();
5646        let vm = self
5647            .vms
5648            .get_mut(vm_id)
5649            .ok_or_else(|| missing_vm_error(vm_id))?;
5650        let child_process_id = {
5651            let root = vm
5652                .active_processes
5653                .get_mut(process_id)
5654                .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5655            let parent =
5656                Self::active_process_by_path_mut(root, current_process_path).ok_or_else(|| {
5657                    SidecarError::InvalidState(format!(
5658                        "unknown child process path {current_process_label} during nested spawn"
5659                    ))
5660                })?;
5661            parent.allocate_child_process_id()
5662        };
5663        let mut child_path = current_process_path.to_vec();
5664        child_path.push(child_process_id.as_str());
5665        let (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd) = if resolved
5666            .tool_command
5667        {
5668            let tool_resolution = resolve_tool_command(
5669                vm,
5670                &resolved.command,
5671                &resolved.execution_args,
5672                Some(&resolved.guest_cwd),
5673            )?
5674            .ok_or_else(|| {
5675                SidecarError::InvalidState(format!(
5676                    "tool command no longer resolves: {}",
5677                    resolved.command
5678                ))
5679            })?;
5680            let kernel_handle = vm
5681                .kernel
5682                .create_virtual_process(
5683                    EXECUTION_DRIVER_NAME,
5684                    TOOL_DRIVER_NAME,
5685                    &resolved.command,
5686                    resolved.process_args.clone(),
5687                    VirtualProcessOptions {
5688                        parent_pid: Some(parent_kernel_pid),
5689                        env: resolved.env.clone(),
5690                        cwd: Some(resolved.guest_cwd.clone()),
5691                    },
5692                )
5693                .map_err(kernel_error)?;
5694            let kernel_pid = kernel_handle.pid();
5695            let tool_execution = ToolExecution::default();
5696            let cancelled = tool_execution.cancelled.clone();
5697            let pending_events = tool_execution.pending_events.clone();
5698            let events_overflowed = tool_execution.events_overflowed.clone();
5699            spawn_tool_process_events(ToolProcessEventRequest {
5700                sidecar_requests: sidecar_requests.clone(),
5701                connection_id: vm.connection_id.clone(),
5702                session_id: vm.session_id.clone(),
5703                vm_id: vm_id.to_owned(),
5704                tool_resolution,
5705                cancelled,
5706                pending_events,
5707                events_overflowed,
5708            });
5709            (
5710                kernel_pid,
5711                kernel_handle,
5712                ActiveExecution::Tool(tool_execution),
5713                None,
5714            )
5715        } else {
5716            let kernel_command = match resolved.runtime {
5717                GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND,
5718                GuestRuntimeKind::WebAssembly => WASM_COMMAND,
5719                GuestRuntimeKind::Python => {
5720                    unreachable!("python child_process execution is rejected")
5721                }
5722            };
5723            let kernel_handle = vm
5724                .kernel
5725                .spawn_process(
5726                    kernel_command,
5727                    resolved.process_args.clone(),
5728                    SpawnOptions {
5729                        requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
5730                        parent_pid: Some(parent_kernel_pid),
5731                        env: resolved.env.clone(),
5732                        cwd: Some(resolved.guest_cwd.clone()),
5733                    },
5734                )
5735                .map_err(kernel_error)?;
5736            let kernel_pid = kernel_handle.pid();
5737            if request.options.detached {
5738                vm.kernel
5739                    .setsid(EXECUTION_DRIVER_NAME, kernel_pid)
5740                    .map_err(kernel_error)?;
5741            }
5742            let mut execution_env = resolved.env.clone();
5743            execution_env.insert(
5744                String::from(EXECUTION_SANDBOX_ROOT_ENV),
5745                normalize_host_path(&vm.cwd).to_string_lossy().into_owned(),
5746            );
5747            let execution = match resolved.runtime {
5748                GuestRuntimeKind::JavaScript => {
5749                    execution_env.extend(sanitize_javascript_child_process_internal_bootstrap_env(
5750                        &request.options.internal_bootstrap_env,
5751                    ));
5752                    execution_env.insert(
5753                        String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
5754                        String::from("1"),
5755                    );
5756                    let context =
5757                        self.javascript_engine
5758                            .create_context(CreateJavascriptContextRequest {
5759                                vm_id: vm_id.to_owned(),
5760                                bootstrap_module: None,
5761                                compile_cache_root: Some(
5762                                    self.cache_root.join("node-compile-cache"),
5763                                ),
5764                            });
5765                    let inline_code = load_javascript_entrypoint_source(
5766                        vm,
5767                        &resolved.host_cwd,
5768                        &resolved.entrypoint,
5769                        &execution_env,
5770                    );
5771                    prepare_javascript_shadow(vm, &resolved)?;
5772
5773                    let module_reader = build_module_reader(vm, &resolved)
5774                        .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
5775                    let execution = self
5776                        .javascript_engine
5777                        .start_execution_with_module_reader(
5778                            StartJavascriptExecutionRequest {
5779                                guest_runtime: guest_runtime_identity(
5780                                    vm,
5781                                    Some(u64::from(kernel_pid)),
5782                                    Some(u64::from(parent_kernel_pid)),
5783                                ),
5784                                vm_id: vm_id.to_owned(),
5785                                context_id: context.context_id,
5786                                argv: std::iter::once(resolved.entrypoint.clone())
5787                                    .chain(resolved.execution_args.clone())
5788                                    .collect(),
5789                                env: execution_env,
5790                                cwd: resolved.host_cwd.clone(),
5791                                limits: javascript_execution_limits(vm),
5792                                inline_code,
5793                            },
5794                            module_reader,
5795                        )
5796                        .map_err(javascript_error)?;
5797                    ActiveExecution::Javascript(execution)
5798                }
5799                GuestRuntimeKind::WebAssembly => {
5800                    execution_env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
5801                    let wasm_limits = wasm_execution_limits(vm);
5802                    let wasm_guest_runtime = guest_runtime_identity(
5803                        vm,
5804                        Some(u64::from(kernel_pid)),
5805                        Some(u64::from(parent_kernel_pid)),
5806                    );
5807                    let context = self.wasm_engine.create_context(CreateWasmContextRequest {
5808                        vm_id: vm_id.to_owned(),
5809                        module_path: Some(resolved.entrypoint.clone()),
5810                    });
5811                    let execution = self
5812                        .wasm_engine
5813                        .start_execution(StartWasmExecutionRequest {
5814                            vm_id: vm_id.to_owned(),
5815                            context_id: context.context_id,
5816                            argv: resolved.process_args.clone(),
5817                            env: execution_env,
5818                            cwd: resolved.host_cwd.clone(),
5819                            permission_tier: execution_wasm_permission_tier(
5820                                resolved
5821                                    .wasm_permission_tier
5822                                    .unwrap_or(WasmPermissionTier::Full),
5823                            ),
5824                            limits: wasm_limits,
5825                            guest_runtime: wasm_guest_runtime,
5826                        })
5827                        .map_err(wasm_error)?;
5828                    ActiveExecution::Wasm(Box::new(execution))
5829                }
5830                GuestRuntimeKind::Python => {
5831                    unreachable!("python child_process execution is rejected")
5832                }
5833            };
5834            let kernel_stdin_writer_fd = match javascript_child_process_stdin_mode(&request) {
5835                "pipe" => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5836                "ignore" => {
5837                    vm.kernel
5838                        .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, 0)
5839                        .map_err(kernel_error)?;
5840                    None
5841                }
5842                "inherit" => None,
5843                _ => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5844            };
5845            (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd)
5846        };
5847
5848        let root = vm
5849            .active_processes
5850            .get_mut(process_id)
5851            .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5852        let parent =
5853            Self::active_process_by_path_mut(root, current_process_path).ok_or_else(|| {
5854                SidecarError::InvalidState(format!(
5855                    "unknown child process path {current_process_label} during nested spawn"
5856                ))
5857            })?;
5858        parent.child_processes.insert(
5859            child_process_id.clone(),
5860            ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
5861                .with_detached(request.options.detached)
5862                .with_guest_cwd(resolved.guest_cwd.clone())
5863                .with_env(resolved.env.clone())
5864                .with_host_cwd(resolved.host_cwd.clone()),
5865        );
5866        if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd {
5867            parent
5868                .child_processes
5869                .get_mut(&child_process_id)
5870                .ok_or_else(|| {
5871                    SidecarError::InvalidState(format!(
5872                        "child process {child_process_id} disappeared during nested spawn"
5873                    ))
5874                })?
5875                .kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd);
5876        }
5877        Ok(json!({
5878            "childId": child_process_id,
5879            "pid": kernel_pid,
5880            "command": resolved.command,
5881            "args": resolved.process_args,
5882        }))
5883    }
5884
5885    fn spawn_descendant_javascript_child_process_sync(
5886        &mut self,
5887        vm_id: &str,
5888        process_id: &str,
5889        current_process_path: &[&str],
5890        request: JavascriptChildProcessSpawnRequest,
5891        max_buffer: Option<usize>,
5892    ) -> Result<Value, SidecarError> {
5893        let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?;
5894        let timeout_deadline = request
5895            .options
5896            .timeout
5897            .map(|timeout_ms| Instant::now() + Duration::from_millis(timeout_ms));
5898        let timeout_signal = request
5899            .options
5900            .kill_signal
5901            .clone()
5902            .unwrap_or_else(|| String::from("SIGTERM"));
5903        let spawned = self.spawn_descendant_javascript_child_process(
5904            vm_id,
5905            process_id,
5906            current_process_path,
5907            request,
5908        )?;
5909        let child_process_id = spawned
5910            .get("childId")
5911            .and_then(Value::as_str)
5912            .ok_or_else(|| {
5913                SidecarError::InvalidState(String::from(
5914                    "child_process.spawn_sync response is missing childId",
5915                ))
5916            })?
5917            .to_owned();
5918
5919        if let Some(input) = sync_input.as_deref() {
5920            self.write_descendant_javascript_child_process_stdin(
5921                vm_id,
5922                process_id,
5923                current_process_path,
5924                &child_process_id,
5925                input,
5926            )?;
5927        }
5928        self.close_descendant_javascript_child_process_stdin(
5929            vm_id,
5930            process_id,
5931            current_process_path,
5932            &child_process_id,
5933        )?;
5934
5935        let max_buffer = max_buffer.unwrap_or(1024 * 1024);
5936        let mut stdout = Vec::new();
5937        let mut stderr = Vec::new();
5938        let mut max_buffer_exceeded = false;
5939        let mut kill_sent = false;
5940        let mut timed_out = false;
5941
5942        let exit_code = loop {
5943            let wait_ms = if let Some(deadline) = timeout_deadline {
5944                let now = Instant::now();
5945                if now >= deadline {
5946                    if !kill_sent {
5947                        timed_out = true;
5948                        self.kill_descendant_javascript_child_process(
5949                            vm_id,
5950                            process_id,
5951                            current_process_path,
5952                            &child_process_id,
5953                            &timeout_signal,
5954                        )?;
5955                        kill_sent = true;
5956                    }
5957                    0
5958                } else {
5959                    u64::try_from(deadline.saturating_duration_since(now).as_millis().min(50))
5960                        .unwrap_or(50)
5961                }
5962            } else {
5963                50
5964            };
5965            let event = self.poll_descendant_javascript_child_process(
5966                vm_id,
5967                process_id,
5968                current_process_path,
5969                &child_process_id,
5970                wait_ms,
5971            )?;
5972            if event.is_null() {
5973                continue;
5974            }
5975
5976            match event.get("type").and_then(Value::as_str) {
5977                Some("stdout") => {
5978                    let chunk = javascript_sync_rpc_bytes_arg(
5979                        &[event.get("data").cloned().unwrap_or(Value::Null)],
5980                        0,
5981                        "child_process.spawn_sync stdout",
5982                    )?;
5983                    stdout.extend_from_slice(&chunk);
5984                    if stdout.len() > max_buffer && !kill_sent {
5985                        max_buffer_exceeded = true;
5986                        self.kill_descendant_javascript_child_process(
5987                            vm_id,
5988                            process_id,
5989                            current_process_path,
5990                            &child_process_id,
5991                            "SIGTERM",
5992                        )?;
5993                        kill_sent = true;
5994                    }
5995                }
5996                Some("stderr") => {
5997                    let chunk = javascript_sync_rpc_bytes_arg(
5998                        &[event.get("data").cloned().unwrap_or(Value::Null)],
5999                        0,
6000                        "child_process.spawn_sync stderr",
6001                    )?;
6002                    stderr.extend_from_slice(&chunk);
6003                    if stderr.len() > max_buffer && !kill_sent {
6004                        max_buffer_exceeded = true;
6005                        self.kill_descendant_javascript_child_process(
6006                            vm_id,
6007                            process_id,
6008                            current_process_path,
6009                            &child_process_id,
6010                            "SIGTERM",
6011                        )?;
6012                        kill_sent = true;
6013                    }
6014                }
6015                Some("exit") => {
6016                    break event
6017                        .get("exitCode")
6018                        .and_then(Value::as_i64)
6019                        .map(|value| value as i32)
6020                        .unwrap_or(1);
6021                }
6022                _ => {}
6023            }
6024        };
6025
6026        Ok(json!({
6027            "stdout": String::from_utf8_lossy(&stdout),
6028            "stderr": String::from_utf8_lossy(&stderr),
6029            "code": exit_code,
6030            "signal": if timed_out { Value::String(timeout_signal) } else { Value::Null },
6031            "timedOut": timed_out,
6032            "maxBufferExceeded": max_buffer_exceeded,
6033        }))
6034    }
6035
6036    fn handle_descendant_javascript_child_process_rpc(
6037        &mut self,
6038        vm_id: &str,
6039        process_id: &str,
6040        current_process_path: &[&str],
6041        request: &JavascriptSyncRpcRequest,
6042    ) -> Result<Value, SidecarError> {
6043        match request.method.as_str() {
6044            "child_process.spawn" => {
6045                let Some(vm) = self.vms.get(vm_id) else {
6046                    return Ok(Value::Null);
6047                };
6048                let (payload, _) = parse_javascript_child_process_spawn_request(vm, &request.args)?;
6049                self.spawn_descendant_javascript_child_process(
6050                    vm_id,
6051                    process_id,
6052                    current_process_path,
6053                    payload,
6054                )
6055            }
6056            "child_process.spawn_sync" => {
6057                let Some(vm) = self.vms.get(vm_id) else {
6058                    return Ok(Value::Null);
6059                };
6060                let (payload, max_buffer) =
6061                    parse_javascript_child_process_spawn_request(vm, &request.args)?;
6062                self.spawn_descendant_javascript_child_process_sync(
6063                    vm_id,
6064                    process_id,
6065                    current_process_path,
6066                    payload,
6067                    max_buffer,
6068                )
6069            }
6070            "child_process.poll" => {
6071                let child_process_id =
6072                    javascript_sync_rpc_arg_str(&request.args, 0, "child_process.poll child id")?;
6073                let wait_ms = javascript_sync_rpc_arg_u64_optional(
6074                    &request.args,
6075                    1,
6076                    "child_process.poll wait ms",
6077                )?
6078                .unwrap_or_default();
6079                self.poll_descendant_javascript_child_process(
6080                    vm_id,
6081                    process_id,
6082                    current_process_path,
6083                    child_process_id,
6084                    wait_ms,
6085                )
6086            }
6087            "child_process.write_stdin" => {
6088                let child_process_id = javascript_sync_rpc_arg_str(
6089                    &request.args,
6090                    0,
6091                    "child_process.write_stdin child id",
6092                )?;
6093                let chunk = javascript_sync_rpc_bytes_arg(
6094                    &request.args,
6095                    1,
6096                    "child_process.write_stdin chunk",
6097                )?;
6098                self.write_descendant_javascript_child_process_stdin(
6099                    vm_id,
6100                    process_id,
6101                    current_process_path,
6102                    child_process_id,
6103                    &chunk,
6104                )?;
6105                Ok(Value::Null)
6106            }
6107            "child_process.close_stdin" => {
6108                let child_process_id = javascript_sync_rpc_arg_str(
6109                    &request.args,
6110                    0,
6111                    "child_process.close_stdin child id",
6112                )?;
6113                self.close_descendant_javascript_child_process_stdin(
6114                    vm_id,
6115                    process_id,
6116                    current_process_path,
6117                    child_process_id,
6118                )?;
6119                Ok(Value::Null)
6120            }
6121            "child_process.kill" => {
6122                let child_process_id =
6123                    javascript_sync_rpc_arg_str(&request.args, 0, "child_process.kill child id")?;
6124                let signal =
6125                    javascript_sync_rpc_arg_str(&request.args, 1, "child_process.kill signal")?;
6126                self.kill_descendant_javascript_child_process(
6127                    vm_id,
6128                    process_id,
6129                    current_process_path,
6130                    child_process_id,
6131                    signal,
6132                )?;
6133                Ok(Value::Null)
6134            }
6135            _ => Err(SidecarError::InvalidState(format!(
6136                "unsupported nested child process RPC method {}",
6137                request.method
6138            ))),
6139        }
6140    }
6141
6142    fn poll_descendant_javascript_child_process(
6143        &mut self,
6144        vm_id: &str,
6145        process_id: &str,
6146        current_process_path: &[&str],
6147        child_process_id: &str,
6148        wait_ms: u64,
6149    ) -> Result<Value, SidecarError> {
6150        let mut child_path = current_process_path.to_vec();
6151        child_path.push(child_process_id);
6152        let child_gone_error = || javascript_child_process_gone_error(process_id, &child_path);
6153        let deadline = Instant::now() + Duration::from_millis(wait_ms);
6154        let mut polled_once = false;
6155
6156        loop {
6157            self.drain_queued_descendant_javascript_child_process_events(
6158                vm_id,
6159                process_id,
6160                &child_path,
6161            )?;
6162            enum ChildPollResult {
6163                Event(Box<Option<ActiveExecutionEvent>>),
6164                RecoverRuntimeExit,
6165                Timeout,
6166            }
6167            let wait = if wait_ms == 0 {
6168                Duration::ZERO
6169            } else {
6170                deadline.saturating_duration_since(Instant::now())
6171            };
6172            let poll_result = {
6173                let Some(vm) = self.vms.get_mut(vm_id) else {
6174                    return Ok(Value::Null);
6175                };
6176                let Some(parent) =
6177                    Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6178                else {
6179                    return Err(child_gone_error());
6180                };
6181                let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6182                    return Err(child_gone_error());
6183                };
6184                if let Some(event) = child.pending_execution_events.pop_front() {
6185                    ChildPollResult::Event(Box::new(Some(event)))
6186                } else if polled_once && wait.is_zero() {
6187                    ChildPollResult::Timeout
6188                } else {
6189                    polled_once = true;
6190                    match child.execution.poll_event_blocking(wait) {
6191                        Ok(Some(event)) => ChildPollResult::Event(Box::new(Some(event))),
6192                        Ok(None) => ChildPollResult::RecoverRuntimeExit,
6193                        Err(SidecarError::Execution(message))
6194                            if (child.runtime == GuestRuntimeKind::JavaScript
6195                                && closed_javascript_event_channel(&message))
6196                                || (child.runtime == GuestRuntimeKind::Python
6197                                    && closed_python_event_channel(&message))
6198                                || (child.runtime == GuestRuntimeKind::WebAssembly
6199                                    && closed_wasm_event_channel(&message)) =>
6200                        {
6201                            ChildPollResult::RecoverRuntimeExit
6202                        }
6203                        Err(error) => return Err(error),
6204                    }
6205                }
6206            };
6207            let event = match poll_result {
6208                ChildPollResult::Event(event) => *event,
6209                ChildPollResult::Timeout => return Ok(Value::Null),
6210                ChildPollResult::RecoverRuntimeExit => self
6211                    .recover_descendant_runtime_child_process_event(
6212                        vm_id,
6213                        process_id,
6214                        current_process_path,
6215                        child_process_id,
6216                        wait.as_millis().try_into().unwrap_or(u64::MAX),
6217                    )?,
6218            };
6219
6220            let Some(event) = event else {
6221                return Ok(Value::Null);
6222            };
6223
6224            match event {
6225                ActiveExecutionEvent::Stdout(chunk) => {
6226                    return Ok(json!({
6227                        "type": "stdout",
6228                        "data": javascript_sync_rpc_bytes_value(&chunk),
6229                    }));
6230                }
6231                ActiveExecutionEvent::Stderr(chunk) => {
6232                    return Ok(json!({
6233                        "type": "stderr",
6234                        "data": javascript_sync_rpc_bytes_value(&chunk),
6235                    }));
6236                }
6237                ActiveExecutionEvent::Exited(exit_code) => {
6238                    let had_trailing_events = {
6239                        let Some(vm) = self.vms.get_mut(vm_id) else {
6240                            return Ok(Value::Null);
6241                        };
6242                        let Some(parent) = Self::descendant_parent_process_mut(
6243                            vm,
6244                            process_id,
6245                            current_process_path,
6246                        ) else {
6247                            return Ok(Value::Null);
6248                        };
6249                        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6250                            return Ok(Value::Null);
6251                        };
6252                        let deadline = Instant::now() + Duration::from_millis(150);
6253                        loop {
6254                            let wait = deadline.saturating_duration_since(Instant::now());
6255                            let next = poll_child_execution_after_exit(child, wait)?;
6256                            let Some(next) = next else {
6257                                break;
6258                            };
6259                            if matches!(next, ActiveExecutionEvent::Exited(_)) {
6260                                continue;
6261                            }
6262                            child.queue_pending_execution_event(next)?;
6263                            if Instant::now() >= deadline {
6264                                break;
6265                            }
6266                        }
6267                        if !child.pending_execution_events.is_empty() {
6268                            child.queue_pending_execution_event(ActiveExecutionEvent::Exited(
6269                                exit_code,
6270                            ))?;
6271                            true
6272                        } else {
6273                            false
6274                        }
6275                    };
6276                    if had_trailing_events {
6277                        continue;
6278                    }
6279
6280                    let parent_signal_key =
6281                        Self::child_process_signal_key(process_id, current_process_path);
6282                    let Some(vm) = self.vms.get_mut(vm_id) else {
6283                        return Ok(Value::Null);
6284                    };
6285                    let signal_name = {
6286                        let Some(parent) = Self::descendant_parent_process_mut(
6287                            vm,
6288                            process_id,
6289                            current_process_path,
6290                        ) else {
6291                            return Ok(Value::Null);
6292                        };
6293                        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6294                            return Ok(Value::Null);
6295                        };
6296                        child.pending_self_signal_exit.take().and_then(|signal| {
6297                            if exit_code == 128 + signal {
6298                                canonical_signal_name(signal).map(str::to_owned)
6299                            } else {
6300                                None
6301                            }
6302                        })
6303                    };
6304                    let (parent_runtime_pid, parent_v8_signal_session, should_signal_parent) = {
6305                        let Some(parent) =
6306                            Self::descendant_parent_process(vm, process_id, current_process_path)
6307                        else {
6308                            return Ok(Value::Null);
6309                        };
6310                        (
6311                            parent.execution.child_pid(),
6312                            parent.execution.javascript_v8_session_handle().filter(|_| {
6313                                matches!(
6314                                    &parent.execution,
6315                                    ActiveExecution::Javascript(execution)
6316                                        if execution.uses_shared_v8_runtime()
6317                                )
6318                            }),
6319                            vm.signal_states
6320                                .get(parent_signal_key)
6321                                .and_then(|handlers| handlers.get(&(libc::SIGCHLD as u32)))
6322                                .is_some_and(|registration| {
6323                                    registration.action != SignalDispositionAction::Default
6324                                }),
6325                        )
6326                    };
6327                    let Some(parent) =
6328                        Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6329                    else {
6330                        return Ok(Value::Null);
6331                    };
6332                    let Some(mut child) = parent.child_processes.remove(child_process_id) else {
6333                        return Ok(Value::Null);
6334                    };
6335                    let child_process_label =
6336                        Self::child_process_path_label(process_id, &child_path);
6337                    let detached_children =
6338                        Self::adopt_detached_child_processes(&child_process_label, &mut child);
6339                    sync_process_host_writes_to_kernel(vm, &child)?;
6340                    terminate_child_process_tree(&mut vm.kernel, &mut child);
6341                    child.kernel_handle.finish(exit_code);
6342                    let _ = vm.kernel.wait_and_reap(child.kernel_pid);
6343                    vm.signal_states.remove(child_process_id);
6344                    for (detached_process_id, detached_child) in detached_children {
6345                        vm.detached_child_processes
6346                            .insert(detached_process_id.clone());
6347                        vm.active_processes
6348                            .insert(detached_process_id, detached_child);
6349                    }
6350                    if should_signal_parent {
6351                        if let Some(session) = parent_v8_signal_session {
6352                            dispatch_v8_session_signal_async(session, libc::SIGCHLD);
6353                        } else {
6354                            signal_runtime_process(parent_runtime_pid, libc::SIGCHLD)?;
6355                        }
6356                    }
6357                    let mut payload = Map::new();
6358                    payload.insert(String::from("type"), Value::String(String::from("exit")));
6359                    payload.insert(String::from("exitCode"), Value::from(exit_code));
6360                    if let Some(signal_name) = signal_name {
6361                        payload.insert(String::from("signal"), Value::String(signal_name));
6362                    }
6363                    return Ok(Value::Object(payload));
6364                }
6365                ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
6366                    let mut current_child_path = current_process_path.to_vec();
6367                    current_child_path.push(child_process_id);
6368                    let response = if request.method == "process.signal_state" {
6369                        let (signal, registration) =
6370                            parse_process_signal_state_request(&request.args)?;
6371                        let Some(vm) = self.vms.get_mut(vm_id) else {
6372                            return Ok(Value::Null);
6373                        };
6374                        let signal_key =
6375                            Self::child_process_signal_key(process_id, &current_child_path)
6376                                .to_owned();
6377                        apply_process_signal_state_update(
6378                            &mut vm.signal_states,
6379                            &signal_key,
6380                            signal,
6381                            registration,
6382                        );
6383                        Ok(Value::Null)
6384                    } else if request.method == "process.kill" {
6385                        self.handle_descendant_process_kill_rpc(
6386                            vm_id,
6387                            process_id,
6388                            current_process_path,
6389                            child_process_id,
6390                            &request,
6391                        )
6392                    } else if request.method.starts_with("child_process.") {
6393                        self.handle_descendant_javascript_child_process_rpc(
6394                            vm_id,
6395                            process_id,
6396                            &current_child_path,
6397                            &request,
6398                        )
6399                    } else {
6400                        let Some(vm) = self.vms.get_mut(vm_id) else {
6401                            return Ok(Value::Null);
6402                        };
6403                        let resource_limits = vm.kernel.resource_limits().clone();
6404                        let network_counts = vm_network_resource_counts(vm);
6405                        let socket_paths = build_javascript_socket_path_context(vm)?;
6406                        let Some(root) = vm.active_processes.get_mut(process_id) else {
6407                            return Ok(Value::Null);
6408                        };
6409                        let Some(parent) =
6410                            Self::active_process_by_path_mut(root, current_process_path)
6411                        else {
6412                            return Ok(Value::Null);
6413                        };
6414                        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6415                            return Ok(Value::Null);
6416                        };
6417                        service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
6418                            bridge: &self.bridge,
6419                            vm_id,
6420                            dns: &vm.dns,
6421                            socket_paths: &socket_paths,
6422                            kernel: &mut vm.kernel,
6423                            process: child,
6424                            sync_request: &request,
6425                            resource_limits: &resource_limits,
6426                            network_counts,
6427                        })
6428                    };
6429
6430                    let Some(vm) = self.vms.get_mut(vm_id) else {
6431                        return Ok(Value::Null);
6432                    };
6433                    let Some(parent) =
6434                        Self::descendant_parent_process_mut(vm, process_id, 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                    let parent_signal_event = response.as_ref().ok().and_then(|result| {
6442                        let target_path_label =
6443                            Self::child_process_path_label(process_id, current_process_path);
6444                        if request.method != "process.kill"
6445                            || result.get("action").and_then(Value::as_str) != Some("user")
6446                            || result.get("targetProcessPath").and_then(Value::as_str)
6447                                != Some(target_path_label.as_str())
6448                        {
6449                            return None;
6450                        }
6451                        Some(json!({
6452                            "type": "signal",
6453                            "signal": result.get("signal").and_then(Value::as_str).unwrap_or_default(),
6454                            "number": result.get("number").and_then(Value::as_i64).unwrap_or_default(),
6455                        }))
6456                    });
6457                    match response {
6458                        Ok(result) => child
6459                            .execution
6460                            .respond_javascript_sync_rpc_success(request.id, result)
6461                            .or_else(ignore_stale_javascript_sync_rpc_response)?,
6462                        Err(error) => child
6463                            .execution
6464                            .respond_javascript_sync_rpc_error(
6465                                request.id,
6466                                javascript_sync_rpc_error_code(&error),
6467                                error.to_string(),
6468                            )
6469                            .or_else(ignore_stale_javascript_sync_rpc_response)?,
6470                    }
6471                    if let Some(event) = parent_signal_event {
6472                        return Ok(event);
6473                    }
6474                }
6475                ActiveExecutionEvent::PythonVfsRpcRequest(_) => {
6476                    return Err(SidecarError::InvalidState(String::from(
6477                        "nested Python child_process execution is not supported yet",
6478                    )));
6479                }
6480                ActiveExecutionEvent::SignalState {
6481                    signal,
6482                    registration,
6483                } => {
6484                    let Some(vm) = self.vms.get_mut(vm_id) else {
6485                        return Ok(Value::Null);
6486                    };
6487                    let signal_key =
6488                        Self::child_process_signal_key(process_id, &child_path).to_owned();
6489                    apply_process_signal_state_update(
6490                        &mut vm.signal_states,
6491                        &signal_key,
6492                        signal,
6493                        registration.clone(),
6494                    );
6495                    return Ok(json!({
6496                        "type": "signal_state",
6497                        "signal": signal,
6498                        "registration": registration,
6499                    }));
6500                }
6501            }
6502        }
6503    }
6504
6505    fn recover_descendant_runtime_child_process_event(
6506        &mut self,
6507        vm_id: &str,
6508        process_id: &str,
6509        current_process_path: &[&str],
6510        child_process_id: &str,
6511        wait_ms: u64,
6512    ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
6513        let (
6514            parent_kernel_pid,
6515            child_kernel_pid,
6516            child_runtime_pid,
6517            child_runtime,
6518            child_shared_runtime,
6519        ) = {
6520            let mut child_path = current_process_path.to_vec();
6521            child_path.push(child_process_id);
6522            let Some(vm) = self.vms.get_mut(vm_id) else {
6523                return Ok(None);
6524            };
6525            let Some(parent) =
6526                Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6527            else {
6528                return Err(javascript_child_process_gone_error(process_id, &child_path));
6529            };
6530            let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6531                return Err(javascript_child_process_gone_error(process_id, &child_path));
6532            };
6533            (
6534                parent.kernel_pid,
6535                child.kernel_pid,
6536                child.execution.child_pid(),
6537                child.runtime.clone(),
6538                child.execution.uses_shared_v8_runtime(),
6539            )
6540        };
6541        if child_runtime != GuestRuntimeKind::JavaScript
6542            && child_runtime != GuestRuntimeKind::Python
6543            && child_runtime != GuestRuntimeKind::WebAssembly
6544        {
6545            return Ok(None);
6546        }
6547        let wait_deadline = Instant::now() + Duration::from_millis(wait_ms.min(25));
6548        loop {
6549            let Some(vm) = self.vms.get_mut(vm_id) else {
6550                return Ok(None);
6551            };
6552            if let Some(process_info) = vm.kernel.list_processes().get(&child_kernel_pid) {
6553                if process_info.status == ProcessStatus::Exited {
6554                    return Ok(Some(ActiveExecutionEvent::Exited(
6555                        process_info.exit_code.unwrap_or(0),
6556                    )));
6557                }
6558            }
6559            if let Some(wait_result) = vm
6560                .kernel
6561                .waitpid_with_options(
6562                    EXECUTION_DRIVER_NAME,
6563                    parent_kernel_pid,
6564                    child_kernel_pid as i32,
6565                    WaitPidFlags::WNOHANG,
6566                )
6567                .map_err(kernel_error)?
6568            {
6569                return Ok(Some(ActiveExecutionEvent::Exited(wait_result.status)));
6570            }
6571
6572            if !child_shared_runtime && child_runtime_pid != 0 {
6573                if let Some(status) = runtime_child_exit_status(child_runtime_pid)? {
6574                    return Ok(Some(ActiveExecutionEvent::Exited(status)));
6575                }
6576                if !runtime_child_is_alive(child_runtime_pid)? {
6577                    return Ok(Some(ActiveExecutionEvent::Exited(0)));
6578                }
6579            }
6580            if Instant::now() >= wait_deadline {
6581                return Ok(None);
6582            }
6583            std::thread::sleep(Duration::from_millis(5));
6584        }
6585    }
6586
6587    fn write_descendant_javascript_child_process_stdin(
6588        &mut self,
6589        vm_id: &str,
6590        process_id: &str,
6591        current_process_path: &[&str],
6592        child_process_id: &str,
6593        chunk: &[u8],
6594    ) -> Result<(), SidecarError> {
6595        let mut child_path = current_process_path.to_vec();
6596        child_path.push(child_process_id);
6597        let Some(vm) = self.vms.get_mut(vm_id) else {
6598            return Err(javascript_child_process_gone_error(process_id, &child_path));
6599        };
6600        let Some(root) = vm.active_processes.get_mut(process_id) else {
6601            return Err(javascript_child_process_gone_error(process_id, &child_path));
6602        };
6603        let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
6604            return Err(javascript_child_process_gone_error(process_id, &child_path));
6605        };
6606        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6607            return Err(javascript_child_process_gone_error(process_id, &child_path));
6608        };
6609        if let Err(error) = child.execution.write_stdin(chunk) {
6610            if is_broken_pipe_error(&error) {
6611                return Ok(());
6612            }
6613            return Err(error);
6614        }
6615        write_kernel_process_stdin(&mut vm.kernel, child, chunk)
6616    }
6617
6618    fn close_descendant_javascript_child_process_stdin(
6619        &mut self,
6620        vm_id: &str,
6621        process_id: &str,
6622        current_process_path: &[&str],
6623        child_process_id: &str,
6624    ) -> Result<(), SidecarError> {
6625        let mut child_path = current_process_path.to_vec();
6626        child_path.push(child_process_id);
6627        let Some(vm) = self.vms.get_mut(vm_id) else {
6628            return Err(javascript_child_process_gone_error(process_id, &child_path));
6629        };
6630        let Some(root) = vm.active_processes.get_mut(process_id) else {
6631            return Err(javascript_child_process_gone_error(process_id, &child_path));
6632        };
6633        let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
6634            return Err(javascript_child_process_gone_error(process_id, &child_path));
6635        };
6636        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6637            return Err(javascript_child_process_gone_error(process_id, &child_path));
6638        };
6639        child.execution.close_stdin()?;
6640        close_kernel_process_stdin(&mut vm.kernel, child)
6641    }
6642
6643    fn kill_descendant_javascript_child_process(
6644        &mut self,
6645        vm_id: &str,
6646        process_id: &str,
6647        current_process_path: &[&str],
6648        child_process_id: &str,
6649        signal: &str,
6650    ) -> Result<(), SidecarError> {
6651        let signal_name = signal.to_owned();
6652        let signal = parse_signal(signal)?;
6653        let Some(vm) = self.vms.get_mut(vm_id) else {
6654            return Ok(());
6655        };
6656        let Some(root) = vm.active_processes.get_mut(process_id) else {
6657            return Ok(());
6658        };
6659        let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
6660            return Ok(());
6661        };
6662        let source_pid = parent.kernel_pid;
6663        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6664            return Ok(());
6665        };
6666        terminate_tracked_child_process_for_signal(&mut vm.kernel, child, signal)?;
6667        let child_process_label = if current_process_path.is_empty() {
6668            child_process_id.to_owned()
6669        } else {
6670            format!("{}/{}", current_process_path.join("/"), child_process_id)
6671        };
6672        emit_security_audit_event(
6673            &self.bridge,
6674            vm_id,
6675            "security.process.kill",
6676            audit_fields([
6677                (String::from("source"), String::from("guest_child_process")),
6678                (String::from("source_pid"), source_pid.to_string()),
6679                (String::from("target_pid"), child.kernel_pid.to_string()),
6680                (String::from("process_id"), process_id.to_owned()),
6681                (String::from("child_process_id"), child_process_label),
6682                (String::from("signal"), signal_name),
6683            ]),
6684        );
6685        Ok(())
6686    }
6687
6688    fn handle_descendant_process_kill_rpc(
6689        &mut self,
6690        vm_id: &str,
6691        process_id: &str,
6692        current_process_path: &[&str],
6693        child_process_id: &str,
6694        request: &JavascriptSyncRpcRequest,
6695    ) -> Result<Value, SidecarError> {
6696        let target_pid = javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?;
6697        let signal_name = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?;
6698        let signal = parse_signal(signal_name)?;
6699
6700        let mut source_path = current_process_path.to_vec();
6701        source_path.push(child_process_id);
6702
6703        if signal != 0 && target_pid < 0 {
6704            let pgid = target_pid.unsigned_abs();
6705            let caller_kernel_pid = {
6706                let Some(vm) = self.vms.get(vm_id) else {
6707                    return Err(SidecarError::InvalidState(String::from(
6708                        "ESRCH: unknown VM during process.kill",
6709                    )));
6710                };
6711                let Some(root) = vm.active_processes.get(process_id) else {
6712                    return Err(SidecarError::InvalidState(format!(
6713                        "ESRCH: unknown process {process_id} during process.kill",
6714                    )));
6715                };
6716                let Some(source) = Self::active_process_by_path(root, &source_path) else {
6717                    return Err(SidecarError::InvalidState(format!(
6718                        "ESRCH: unknown child process {child_process_id} during process.kill",
6719                    )));
6720                };
6721                source.kernel_pid
6722            };
6723            let caller_is_member =
6724                self.signal_vm_process_group(vm_id, caller_kernel_pid, pgid, signal_name)?;
6725            if !caller_is_member {
6726                return Ok(Value::Null);
6727            }
6728            let Some(vm) = self.vms.get_mut(vm_id) else {
6729                return Ok(Value::Null);
6730            };
6731            let Some(root) = vm.active_processes.get_mut(process_id) else {
6732                return Ok(Value::Null);
6733            };
6734            let Some(source) = Self::active_process_by_path_mut(root, &source_path) else {
6735                return Ok(Value::Null);
6736            };
6737            source.pending_self_signal_exit = None;
6738            if !matches!(
6739                canonical_signal_name(signal),
6740                Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
6741            ) {
6742                source.pending_self_signal_exit = Some(signal);
6743            }
6744            return Ok(json!({
6745                "self": true,
6746                "action": "default",
6747            }));
6748        }
6749
6750        let Some(vm) = self.vms.get_mut(vm_id) else {
6751            return Err(SidecarError::InvalidState(String::from(
6752                "ESRCH: unknown VM during process.kill",
6753            )));
6754        };
6755
6756        if signal == 0 {
6757            vm.kernel
6758                .signal_process(EXECUTION_DRIVER_NAME, target_pid, signal)
6759                .map_err(kernel_error)?;
6760            return Ok(Value::Null);
6761        }
6762
6763        let target_kernel_pid = u32::try_from(target_pid).map_err(|_| {
6764            SidecarError::InvalidState(format!("EINVAL: invalid process pid {target_pid}"))
6765        })?;
6766        let (source_pid, located_target_path) = {
6767            let Some(root) = vm.active_processes.get(process_id) else {
6768                return Err(SidecarError::InvalidState(format!(
6769                    "ESRCH: unknown process {process_id} during process.kill",
6770                )));
6771            };
6772            let Some(source) = Self::active_process_by_path(root, &source_path) else {
6773                return Err(SidecarError::InvalidState(format!(
6774                    "ESRCH: unknown child process {child_process_id} during process.kill",
6775                )));
6776            };
6777            vm.kernel
6778                .signal_process(EXECUTION_DRIVER_NAME, target_pid, 0)
6779                .map_err(kernel_error)?;
6780            (
6781                source.kernel_pid,
6782                Self::active_process_path_by_kernel_pid(root, target_kernel_pid),
6783            )
6784        };
6785        let Some(target_path) = located_target_path else {
6786            // The target is alive but not part of this root's process tree.
6787            // Resolve it VM-wide so cross-tree pids and untracked kernel
6788            // processes still receive the signal.
6789            self.signal_vm_kernel_pid(vm_id, target_kernel_pid, signal_name)?;
6790            return Ok(Value::Null);
6791        };
6792        let Some(vm) = self.vms.get_mut(vm_id) else {
6793            return Err(SidecarError::InvalidState(String::from(
6794                "ESRCH: unknown VM during process.kill",
6795            )));
6796        };
6797
6798        if source_pid == target_kernel_pid {
6799            let Some(root) = vm.active_processes.get_mut(process_id) else {
6800                return Ok(Value::Null);
6801            };
6802            let Some(source) = Self::active_process_by_path_mut(root, &source_path) else {
6803                return Ok(Value::Null);
6804            };
6805            source.pending_self_signal_exit = None;
6806            if !matches!(
6807                canonical_signal_name(signal),
6808                Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
6809            ) {
6810                source.pending_self_signal_exit = Some(signal);
6811            }
6812            return Ok(json!({
6813                "self": true,
6814                "action": "default",
6815            }));
6816        }
6817
6818        let signal_key = target_path.last().map(String::as_str).unwrap_or(process_id);
6819        let registration = vm
6820            .signal_states
6821            .get(signal_key)
6822            .and_then(|handlers| handlers.get(&(signal as u32)))
6823            .cloned();
6824
6825        let action = match registration
6826            .as_ref()
6827            .map(|registration| &registration.action)
6828        {
6829            Some(SignalDispositionAction::Ignore) => "ignore",
6830            Some(SignalDispositionAction::User) => {
6831                let Some(root) = vm.active_processes.get_mut(process_id) else {
6832                    return Ok(Value::Null);
6833                };
6834                let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path)
6835                else {
6836                    return Err(SidecarError::InvalidState(format!(
6837                        "ESRCH: unknown process pid {target_pid}"
6838                    )));
6839                };
6840                if let Some(session) = target.execution.javascript_v8_session_handle().filter(
6841                    |_| matches!(&target.execution, ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime())
6842                        || matches!(&target.execution, ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime()),
6843                ) {
6844                    dispatch_v8_session_signal_async(session, signal);
6845                } else if !dispatch_v8_process_signal(target, signal)? {
6846                    return Err(SidecarError::InvalidState(format!(
6847                        "unsupported guest signal delivery for pid {target_pid}"
6848                    )));
6849                }
6850                "user"
6851            }
6852            Some(SignalDispositionAction::Default) | None
6853                if matches!(
6854                    canonical_signal_name(signal),
6855                    Some("SIGWINCH" | "SIGCHLD" | "SIGURG")
6856                ) =>
6857            {
6858                "ignore"
6859            }
6860            Some(SignalDispositionAction::Default) | None => {
6861                let Some(root) = vm.active_processes.get_mut(process_id) else {
6862                    return Ok(Value::Null);
6863                };
6864                let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path)
6865                else {
6866                    return Err(SidecarError::InvalidState(format!(
6867                        "ESRCH: unknown process pid {target_pid}"
6868                    )));
6869                };
6870                apply_active_process_default_signal(&mut vm.kernel, target, signal)?;
6871                "default"
6872            }
6873        };
6874
6875        let target_path_label = Self::child_process_path_label(
6876            process_id,
6877            &target_path.iter().map(String::as_str).collect::<Vec<_>>(),
6878        );
6879        emit_security_audit_event(
6880            &self.bridge,
6881            vm_id,
6882            "security.process.kill",
6883            audit_fields([
6884                (String::from("source"), String::from("guest_process")),
6885                (String::from("source_pid"), source_pid.to_string()),
6886                (String::from("target_pid"), target_pid.to_string()),
6887                (String::from("process_id"), process_id.to_owned()),
6888                (
6889                    String::from("target_process_path"),
6890                    target_path_label.clone(),
6891                ),
6892                (String::from("signal"), signal_name.to_owned()),
6893            ]),
6894        );
6895
6896        Ok(json!({
6897            "self": false,
6898            "action": action,
6899            "signal": signal_name,
6900            "number": signal,
6901            "targetProcessPath": target_path_label,
6902        }))
6903    }
6904
6905    pub(crate) fn poll_javascript_child_process(
6906        &mut self,
6907        vm_id: &str,
6908        process_id: &str,
6909        child_process_id: &str,
6910        wait_ms: u64,
6911    ) -> Result<Value, SidecarError> {
6912        self.poll_descendant_javascript_child_process(
6913            vm_id,
6914            process_id,
6915            &[],
6916            child_process_id,
6917            wait_ms,
6918        )
6919    }
6920
6921    pub(crate) fn write_javascript_child_process_stdin(
6922        &mut self,
6923        vm_id: &str,
6924        process_id: &str,
6925        child_process_id: &str,
6926        chunk: &[u8],
6927    ) -> Result<(), SidecarError> {
6928        let Some(vm) = self.vms.get_mut(vm_id) else {
6929            return Err(javascript_child_process_gone_error(
6930                process_id,
6931                &[child_process_id],
6932            ));
6933        };
6934        let Some(child) = vm
6935            .active_processes
6936            .get_mut(process_id)
6937            .ok_or_else(|| missing_process_error(vm_id, process_id))?
6938            .child_processes
6939            .get_mut(child_process_id)
6940        else {
6941            return Err(javascript_child_process_gone_error(
6942                process_id,
6943                &[child_process_id],
6944            ));
6945        };
6946        if let Err(error) = child.execution.write_stdin(chunk) {
6947            if is_broken_pipe_error(&error) {
6948                return Ok(());
6949            }
6950            return Err(error);
6951        }
6952        write_kernel_process_stdin(&mut vm.kernel, child, chunk)
6953    }
6954
6955    pub(crate) fn close_javascript_child_process_stdin(
6956        &mut self,
6957        vm_id: &str,
6958        process_id: &str,
6959        child_process_id: &str,
6960    ) -> Result<(), SidecarError> {
6961        let Some(vm) = self.vms.get_mut(vm_id) else {
6962            return Err(javascript_child_process_gone_error(
6963                process_id,
6964                &[child_process_id],
6965            ));
6966        };
6967        let Some(child) = vm
6968            .active_processes
6969            .get_mut(process_id)
6970            .ok_or_else(|| missing_process_error(vm_id, process_id))?
6971            .child_processes
6972            .get_mut(child_process_id)
6973        else {
6974            return Err(javascript_child_process_gone_error(
6975                process_id,
6976                &[child_process_id],
6977            ));
6978        };
6979        child.execution.close_stdin()?;
6980        close_kernel_process_stdin(&mut vm.kernel, child)
6981    }
6982
6983    pub(crate) fn kill_javascript_child_process(
6984        &mut self,
6985        vm_id: &str,
6986        process_id: &str,
6987        child_process_id: &str,
6988        signal: &str,
6989    ) -> Result<(), SidecarError> {
6990        let signal_name = signal.to_owned();
6991        let signal = parse_signal(signal)?;
6992        let Some(vm) = self.vms.get_mut(vm_id) else {
6993            return Ok(());
6994        };
6995        let process = vm
6996            .active_processes
6997            .get_mut(process_id)
6998            .ok_or_else(|| missing_process_error(vm_id, process_id))?;
6999        let source_pid = process.kernel_pid;
7000        let child = process
7001            .child_processes
7002            .get_mut(child_process_id)
7003            .ok_or_else(|| {
7004                SidecarError::InvalidState(format!(
7005                    "unknown child process {child_process_id} during kill"
7006                ))
7007            })?;
7008        terminate_tracked_child_process_for_signal(&mut vm.kernel, child, signal)?;
7009        emit_security_audit_event(
7010            &self.bridge,
7011            vm_id,
7012            "security.process.kill",
7013            audit_fields([
7014                (String::from("source"), String::from("guest_child_process")),
7015                (String::from("source_pid"), source_pid.to_string()),
7016                (String::from("target_pid"), child.kernel_pid.to_string()),
7017                (String::from("process_id"), process_id.to_owned()),
7018                (
7019                    String::from("child_process_id"),
7020                    child_process_id.to_owned(),
7021                ),
7022                (String::from("signal"), signal_name),
7023            ]),
7024        );
7025        Ok(())
7026    }
7027
7028    /// Delivers a signal to one kernel pid inside a VM, resolving the target
7029    /// through the active-process tree first so tracked sidecar executions get
7030    /// the same termination handling as a direct `child_process.kill`.
7031    /// Untracked kernel processes (for example WASM subprocess trees) receive
7032    /// the signal through the kernel process table directly.
7033    pub(crate) fn signal_vm_kernel_pid(
7034        &mut self,
7035        vm_id: &str,
7036        target_kernel_pid: u32,
7037        signal_name: &str,
7038    ) -> Result<(), SidecarError> {
7039        let signal = parse_signal(signal_name)?;
7040        let located = {
7041            let Some(vm) = self.vms.get(vm_id) else {
7042                return Err(SidecarError::InvalidState(String::from(
7043                    "ESRCH: unknown VM during process.kill",
7044                )));
7045            };
7046            let alive = vm
7047                .kernel
7048                .list_processes()
7049                .get(&target_kernel_pid)
7050                .is_some_and(|info| info.status != ProcessStatus::Exited);
7051            if !alive {
7052                return Err(SidecarError::InvalidState(format!(
7053                    "ESRCH: no such process {target_kernel_pid}"
7054                )));
7055            }
7056            vm.active_processes.iter().find_map(|(process_id, root)| {
7057                Self::active_process_path_by_kernel_pid(root, target_kernel_pid)
7058                    .map(|path| (process_id.clone(), path))
7059            })
7060        };
7061
7062        match located {
7063            Some((process_id, path)) if path.is_empty() => {
7064                self.kill_process_internal(vm_id, &process_id, signal_name)
7065            }
7066            Some((process_id, path)) => {
7067                let Some(vm) = self.vms.get_mut(vm_id) else {
7068                    return Ok(());
7069                };
7070                let Some(root) = vm.active_processes.get_mut(&process_id) else {
7071                    return Ok(());
7072                };
7073                let Some(target) = Self::active_process_by_owned_path_mut(root, &path) else {
7074                    return Err(SidecarError::InvalidState(format!(
7075                        "ESRCH: no such process {target_kernel_pid}"
7076                    )));
7077                };
7078                terminate_tracked_child_process_for_signal(&mut vm.kernel, target, signal)?;
7079                emit_security_audit_event(
7080                    &self.bridge,
7081                    vm_id,
7082                    "security.process.kill",
7083                    audit_fields([
7084                        (String::from("source"), String::from("guest_process")),
7085                        (String::from("target_pid"), target_kernel_pid.to_string()),
7086                        (String::from("process_id"), process_id),
7087                        (String::from("signal"), signal_name.to_owned()),
7088                    ]),
7089                );
7090                Ok(())
7091            }
7092            None => {
7093                let Some(vm) = self.vms.get_mut(vm_id) else {
7094                    return Ok(());
7095                };
7096                let target_pid = i32::try_from(target_kernel_pid).map_err(|_| {
7097                    SidecarError::InvalidState(format!(
7098                        "EINVAL: invalid process pid {target_kernel_pid}"
7099                    ))
7100                })?;
7101                vm.kernel
7102                    .signal_process(EXECUTION_DRIVER_NAME, target_pid, signal)
7103                    .map_err(kernel_error)?;
7104                emit_security_audit_event(
7105                    &self.bridge,
7106                    vm_id,
7107                    "security.process.kill",
7108                    audit_fields([
7109                        (String::from("source"), String::from("guest_process")),
7110                        (String::from("target_pid"), target_kernel_pid.to_string()),
7111                        (String::from("signal"), signal_name.to_owned()),
7112                    ]),
7113                );
7114                Ok(())
7115            }
7116        }
7117    }
7118
7119    /// Delivers a signal to every live member of a VM process group, matching
7120    /// Linux `kill(-pgid, sig)` semantics. Returns whether the caller itself
7121    /// is a member of the group so entry points can apply self-signal
7122    /// delivery; the caller is intentionally skipped here.
7123    pub(crate) fn signal_vm_process_group(
7124        &mut self,
7125        vm_id: &str,
7126        caller_kernel_pid: u32,
7127        pgid: u32,
7128        signal_name: &str,
7129    ) -> Result<bool, SidecarError> {
7130        parse_signal(signal_name)?;
7131        let members = {
7132            let Some(vm) = self.vms.get(vm_id) else {
7133                return Err(SidecarError::InvalidState(String::from(
7134                    "ESRCH: unknown VM during process.kill",
7135                )));
7136            };
7137            vm.kernel
7138                .list_processes()
7139                .into_iter()
7140                .filter(|(_, info)| info.pgid == pgid && info.status != ProcessStatus::Exited)
7141                .map(|(pid, _)| pid)
7142                .collect::<Vec<_>>()
7143        };
7144        if members.is_empty() {
7145            return Err(SidecarError::InvalidState(format!(
7146                "ESRCH: no such process group {pgid}"
7147            )));
7148        }
7149
7150        let mut caller_is_member = false;
7151        for member_pid in members {
7152            if member_pid == caller_kernel_pid {
7153                caller_is_member = true;
7154                continue;
7155            }
7156            match self.signal_vm_kernel_pid(vm_id, member_pid, signal_name) {
7157                Ok(()) => {}
7158                // Group members can exit while the group is being signaled. A
7159                // vanished member is not an error for the group kill overall.
7160                Err(error) if sidecar_error_is_esrch(&error) => {}
7161                Err(error) => return Err(error),
7162            }
7163        }
7164        Ok(caller_is_member)
7165    }
7166}
7167
7168/// Applies a kill signal to a tracked child execution. Shared-runtime
7169/// executions for lethal signals are terminated directly with a synthetic
7170/// signal exit so child polls observe a prompt close; everything else routes
7171/// through the kernel process table.
7172fn terminate_tracked_child_process_for_signal(
7173    kernel: &mut SidecarKernel,
7174    child: &mut ActiveProcess,
7175    signal: i32,
7176) -> Result<(), SidecarError> {
7177    let should_terminate_shared_runtime = child.execution.uses_shared_v8_runtime()
7178        && signal != 0
7179        && !matches!(
7180            signal,
7181            libc::SIGHUP
7182                | libc::SIGINT
7183                | libc::SIGTERM
7184                | libc::SIGCHLD
7185                | libc::SIGWINCH
7186                | libc::SIGSTOP
7187                | libc::SIGCONT
7188        );
7189    if should_terminate_shared_runtime {
7190        child.execution.terminate()?;
7191        child.pending_self_signal_exit = Some(signal);
7192        child.queue_pending_execution_event(ActiveExecutionEvent::Exited(128 + signal))?;
7193    } else {
7194        kernel
7195            .kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, signal)
7196            .map_err(kernel_error)?;
7197    }
7198    Ok(())
7199}
7200
7201fn sidecar_error_is_esrch(error: &SidecarError) -> bool {
7202    error.to_string().contains("ESRCH")
7203}
7204
7205fn apply_active_process_default_signal(
7206    kernel: &mut SidecarKernel,
7207    process: &mut ActiveProcess,
7208    signal: i32,
7209) -> Result<(), SidecarError> {
7210    if matches!(signal, libc::SIGSTOP | libc::SIGCONT) {
7211        return kernel
7212            .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal)
7213            .map_err(kernel_error);
7214    }
7215
7216    if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
7217        close_kernel_process_stdin(kernel, process)?;
7218    }
7219
7220    if process.execution.uses_shared_v8_runtime() {
7221        process.execution.terminate()?;
7222        if signal != 0 && matches!(process.execution, ActiveExecution::Wasm(_)) {
7223            process.queue_pending_execution_event(ActiveExecutionEvent::Exited(128 + signal))?;
7224        }
7225        return Ok(());
7226    }
7227
7228    kernel
7229        .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal)
7230        .map_err(kernel_error)
7231}
7232
7233fn map_wasm_signal_registration(
7234    registration: secure_exec_execution::wasm::WasmSignalHandlerRegistration,
7235) -> SignalHandlerRegistration {
7236    SignalHandlerRegistration {
7237        action: match registration.action {
7238            secure_exec_execution::wasm::WasmSignalDispositionAction::Default => {
7239                crate::protocol::SignalDispositionAction::Default
7240            }
7241            secure_exec_execution::wasm::WasmSignalDispositionAction::Ignore => {
7242                crate::protocol::SignalDispositionAction::Ignore
7243            }
7244            secure_exec_execution::wasm::WasmSignalDispositionAction::User => {
7245                crate::protocol::SignalDispositionAction::User
7246            }
7247        },
7248        mask: registration.mask,
7249        flags: registration.flags,
7250    }
7251}
7252
7253fn parse_process_signal_state_request(
7254    args: &[Value],
7255) -> Result<(u32, SignalHandlerRegistration), SidecarError> {
7256    let signal = javascript_sync_rpc_arg_u32(args, 0, "process.signal_state signal")?;
7257    let action = javascript_sync_rpc_arg_str(args, 1, "process.signal_state action")?;
7258    let mask_json = javascript_sync_rpc_arg_str(args, 2, "process.signal_state mask")?;
7259    let flags = javascript_sync_rpc_arg_u32(args, 3, "process.signal_state flags")?;
7260    let mask: Vec<u32> = serde_json::from_str(mask_json).map_err(|error| {
7261        SidecarError::InvalidState(format!(
7262            "process.signal_state mask must be valid JSON: {error}"
7263        ))
7264    })?;
7265    let action = match action.trim().to_ascii_lowercase().as_str() {
7266        "default" => SignalDispositionAction::Default,
7267        "ignore" => SignalDispositionAction::Ignore,
7268        "user" => SignalDispositionAction::User,
7269        other => {
7270            return Err(SidecarError::InvalidState(format!(
7271                "unsupported process.signal_state action {other}"
7272            )));
7273        }
7274    };
7275
7276    Ok((
7277        signal,
7278        SignalHandlerRegistration {
7279            action,
7280            mask,
7281            flags,
7282        },
7283    ))
7284}
7285
7286fn apply_process_signal_state_update(
7287    signal_states: &mut BTreeMap<String, BTreeMap<u32, SignalHandlerRegistration>>,
7288    process_id: &str,
7289    signal: u32,
7290    registration: SignalHandlerRegistration,
7291) {
7292    if registration.action == SignalDispositionAction::Default
7293        && registration.mask.is_empty()
7294        && registration.flags == 0
7295    {
7296        let remove_process_entry = signal_states
7297            .get_mut(process_id)
7298            .map(|handlers| {
7299                handlers.remove(&signal);
7300                handlers.is_empty()
7301            })
7302            .unwrap_or(false);
7303        if remove_process_entry {
7304            signal_states.remove(process_id);
7305        }
7306        return;
7307    }
7308
7309    signal_states
7310        .entry(process_id.to_owned())
7311        .or_default()
7312        .insert(signal, registration);
7313}
7314
7315fn map_node_signal_registration(
7316    registration: NodeSignalHandlerRegistration,
7317) -> SignalHandlerRegistration {
7318    SignalHandlerRegistration {
7319        action: match registration.action {
7320            NodeSignalDispositionAction::Default => SignalDispositionAction::Default,
7321            NodeSignalDispositionAction::Ignore => SignalDispositionAction::Ignore,
7322            NodeSignalDispositionAction::User => SignalDispositionAction::User,
7323        },
7324        mask: registration.mask,
7325        flags: registration.flags,
7326    }
7327}
7328
7329fn javascript_child_process_sync_input_bytes(
7330    value: Option<&Value>,
7331) -> Result<Option<Vec<u8>>, SidecarError> {
7332    let Some(value) = value else {
7333        return Ok(None);
7334    };
7335
7336    match value {
7337        Value::Null => Ok(None),
7338        Value::String(text) => Ok(Some(text.as_bytes().to_vec())),
7339        other => javascript_sync_rpc_bytes_arg(
7340            std::slice::from_ref(other),
7341            0,
7342            "child_process.spawn_sync input",
7343        )
7344        .map(Some),
7345    }
7346}
7347
7348// bridge_permissions moved to crate::bridge
7349
7350// reconcile_mounts, resolve_cwd moved to crate::vm
7351
7352fn resolve_execute_request(
7353    vm: &VmState,
7354    payload: &ExecuteRequest,
7355) -> Result<ResolvedChildProcessExecution, SidecarError> {
7356    let payload_env: BTreeMap<String, String> = payload
7357        .env
7358        .iter()
7359        .map(|(k, v)| (k.clone(), v.clone()))
7360        .collect();
7361    if let Some(command) = payload.command.as_deref() {
7362        return resolve_command_execution(
7363            vm,
7364            command,
7365            &payload.args,
7366            &payload_env,
7367            payload.cwd.as_deref(),
7368            payload.wasm_permission_tier,
7369        );
7370    }
7371
7372    let runtime = payload.runtime.clone().ok_or_else(|| {
7373        SidecarError::InvalidState(String::from("execute requires either command or runtime"))
7374    })?;
7375    let entrypoint = payload.entrypoint.clone().ok_or_else(|| {
7376        SidecarError::InvalidState(String::from(
7377            "execute requires either command or entrypoint",
7378        ))
7379    })?;
7380    let (guest_cwd, host_cwd, allow_host_path_overrides) =
7381        resolve_execution_cwds(vm, payload.cwd.as_deref());
7382    let mut env = vm.guest_env.clone();
7383    env.extend(payload_env.clone());
7384
7385    let requested_host_entrypoint = resolve_host_entrypoint_within_vm_host_cwd(vm, &entrypoint);
7386    if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
7387        let requested_cwd = payload.cwd.as_deref().unwrap_or(guest_cwd.as_str());
7388        return Err(SidecarError::InvalidState(format!(
7389            "execution cwd {requested_cwd} is outside sandbox root {}",
7390            vm.host_cwd.to_string_lossy()
7391        )));
7392    }
7393    let host_entrypoint_override = allow_host_path_overrides
7394        .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, &entrypoint))
7395        .flatten();
7396
7397    let guest_entrypoint = host_entrypoint_override
7398        .as_ref()
7399        .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
7400        .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, &entrypoint));
7401    prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
7402
7403    Ok(ResolvedChildProcessExecution {
7404        command: match runtime {
7405            GuestRuntimeKind::JavaScript => String::from(JAVASCRIPT_COMMAND),
7406            GuestRuntimeKind::Python => String::from(PYTHON_COMMAND),
7407            GuestRuntimeKind::WebAssembly => String::from(WASM_COMMAND),
7408        },
7409        process_args: std::iter::once(entrypoint.clone())
7410            .chain(payload.args.iter().cloned())
7411            .collect(),
7412        runtime,
7413        entrypoint: host_entrypoint_override
7414            .map(|(_, host_entrypoint)| host_entrypoint)
7415            .unwrap_or(entrypoint),
7416        execution_args: payload.args.clone(),
7417        env,
7418        guest_cwd,
7419        host_cwd,
7420        wasm_permission_tier: payload.wasm_permission_tier,
7421        tool_command: false,
7422    })
7423}
7424
7425fn resolve_command_execution(
7426    vm: &VmState,
7427    command: &str,
7428    args: &[String],
7429    extra_env: &BTreeMap<String, String>,
7430    cwd: Option<&str>,
7431    explicit_wasm_permission_tier: Option<WasmPermissionTier>,
7432) -> Result<ResolvedChildProcessExecution, SidecarError> {
7433    let (guest_cwd, host_cwd, allow_host_path_overrides) = resolve_execution_cwds(vm, cwd);
7434    let mut env = vm.guest_env.clone();
7435    env.extend(extra_env.clone());
7436    let args = apply_shell_cwd_prefix(command, args.to_vec(), &guest_cwd);
7437
7438    if is_tool_command(vm, command) {
7439        let command = normalized_tool_command_name(command).unwrap_or_else(|| command.to_owned());
7440        return Ok(ResolvedChildProcessExecution {
7441            command: command.clone(),
7442            process_args: std::iter::once(command.clone())
7443                .chain(args.iter().cloned())
7444                .collect(),
7445            runtime: GuestRuntimeKind::JavaScript,
7446            entrypoint: command,
7447            execution_args: args,
7448            env,
7449            guest_cwd,
7450            host_cwd,
7451            wasm_permission_tier: None,
7452            tool_command: true,
7453        });
7454    }
7455
7456    if is_node_runtime_command(command) {
7457        if let Some(cli) = resolve_host_node_cli_entrypoint(command) {
7458            env.insert(
7459                String::from("AGENT_OS_NODE_EVAL"),
7460                build_host_node_cli_eval(&cli),
7461            );
7462            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
7463            add_runtime_guest_path_mapping(&mut env, &cli.guest_root, &cli.package_root);
7464            add_runtime_host_access_path(
7465                &mut env,
7466                "AGENT_OS_EXTRA_FS_READ_PATHS",
7467                &cli.package_root,
7468                true,
7469            );
7470
7471            return Ok(ResolvedChildProcessExecution {
7472                command: String::from(JAVASCRIPT_COMMAND),
7473                process_args: std::iter::once(command.to_owned())
7474                    .chain(args.iter().cloned())
7475                    .collect(),
7476                runtime: GuestRuntimeKind::JavaScript,
7477                entrypoint: String::from("-e"),
7478                execution_args: std::iter::once(cli.guest_entrypoint.clone())
7479                    .chain(args.iter().cloned())
7480                    .collect(),
7481                env,
7482                guest_cwd,
7483                host_cwd,
7484                wasm_permission_tier: None,
7485                tool_command: false,
7486            });
7487        }
7488
7489        if args.is_empty() {
7490            env.insert(String::from("AGENT_OS_NODE_EVAL"), String::new());
7491            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
7492
7493            return Ok(ResolvedChildProcessExecution {
7494                command: String::from(JAVASCRIPT_COMMAND),
7495                process_args: vec![command.to_owned()],
7496                runtime: GuestRuntimeKind::JavaScript,
7497                entrypoint: String::from("-e"),
7498                execution_args: Vec::new(),
7499                env,
7500                guest_cwd,
7501                host_cwd,
7502                wasm_permission_tier: None,
7503                tool_command: false,
7504            });
7505        }
7506
7507        if let Some((entrypoint, execution_args)) =
7508            resolve_special_node_cli_invocation(&args, &mut env)
7509        {
7510            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
7511
7512            return Ok(ResolvedChildProcessExecution {
7513                command: String::from(JAVASCRIPT_COMMAND),
7514                process_args: std::iter::once(command.to_owned())
7515                    .chain(args.iter().cloned())
7516                    .collect(),
7517                runtime: GuestRuntimeKind::JavaScript,
7518                entrypoint,
7519                execution_args,
7520                env,
7521                guest_cwd,
7522                host_cwd,
7523                wasm_permission_tier: None,
7524                tool_command: false,
7525            });
7526        }
7527
7528        let Some(entrypoint_specifier) = args.first() else {
7529            return Err(SidecarError::InvalidState(format!(
7530                "{command} execution requires an entrypoint"
7531            )));
7532        };
7533
7534        let (entrypoint, execution_args, guest_entrypoint) = {
7535            let requested_host_entrypoint =
7536                resolve_host_entrypoint_within_vm_host_cwd(vm, entrypoint_specifier);
7537            if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
7538                let requested_cwd = cwd.unwrap_or(guest_cwd.as_str());
7539                return Err(SidecarError::InvalidState(format!(
7540                    "execution cwd {requested_cwd} is outside sandbox root {}",
7541                    vm.host_cwd.to_string_lossy()
7542                )));
7543            }
7544            let host_entrypoint_override = allow_host_path_overrides
7545                .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, entrypoint_specifier))
7546                .flatten();
7547            let guest_entrypoint = host_entrypoint_override
7548                .as_ref()
7549                .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
7550                .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, entrypoint_specifier));
7551            let entrypoint = host_entrypoint_override.map_or_else(
7552                || {
7553                    guest_entrypoint.as_ref().map_or_else(
7554                        || entrypoint_specifier.clone(),
7555                        |guest_entrypoint| {
7556                            resolve_vm_guest_path_to_host(vm, guest_entrypoint)
7557                                .to_string_lossy()
7558                                .into_owned()
7559                        },
7560                    )
7561                },
7562                |(_, host_entrypoint)| host_entrypoint,
7563            );
7564            (
7565                entrypoint,
7566                args.iter().skip(1).cloned().collect(),
7567                guest_entrypoint,
7568            )
7569        };
7570
7571        prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
7572
7573        return Ok(ResolvedChildProcessExecution {
7574            command: String::from(JAVASCRIPT_COMMAND),
7575            process_args: std::iter::once(command.to_owned())
7576                .chain(args.iter().cloned())
7577                .collect(),
7578            runtime: GuestRuntimeKind::JavaScript,
7579            entrypoint,
7580            execution_args,
7581            env,
7582            guest_cwd,
7583            host_cwd,
7584            wasm_permission_tier: None,
7585            tool_command: false,
7586        });
7587    }
7588
7589    if command.ends_with(".js") || command.ends_with(".mjs") || command.ends_with(".cjs") {
7590        let requested_host_entrypoint = resolve_host_entrypoint_within_vm_host_cwd(vm, command);
7591        if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
7592            let requested_cwd = cwd.unwrap_or(guest_cwd.as_str());
7593            return Err(SidecarError::InvalidState(format!(
7594                "execution cwd {requested_cwd} is outside sandbox root {}",
7595                vm.host_cwd.to_string_lossy()
7596            )));
7597        }
7598        let host_entrypoint_override = allow_host_path_overrides
7599            .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, command))
7600            .flatten();
7601        let guest_entrypoint = host_entrypoint_override
7602            .as_ref()
7603            .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
7604            .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, command));
7605        let entrypoint = host_entrypoint_override.map_or_else(
7606            || {
7607                guest_entrypoint.as_ref().map_or_else(
7608                    || command.to_owned(),
7609                    |guest_entrypoint| {
7610                        resolve_vm_guest_path_to_host(vm, guest_entrypoint)
7611                            .to_string_lossy()
7612                            .into_owned()
7613                    },
7614                )
7615            },
7616            |(_, host_entrypoint)| host_entrypoint,
7617        );
7618        prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
7619
7620        return Ok(ResolvedChildProcessExecution {
7621            command: String::from(JAVASCRIPT_COMMAND),
7622            process_args: std::iter::once(command.to_owned())
7623                .chain(args.iter().cloned())
7624                .collect(),
7625            runtime: GuestRuntimeKind::JavaScript,
7626            entrypoint,
7627            execution_args: args.to_vec(),
7628            env,
7629            guest_cwd,
7630            host_cwd,
7631            wasm_permission_tier: None,
7632            tool_command: false,
7633        });
7634    }
7635
7636    let guest_entrypoint = resolve_guest_command_entrypoint(
7637        vm,
7638        &guest_cwd,
7639        command,
7640        env.get("PATH").map(String::as_str),
7641    )
7642    .ok_or_else(|| {
7643        SidecarError::InvalidState(format!(
7644            "command not found on native sidecar path: {command}"
7645        ))
7646    })?;
7647    let wasm_permission_tier = explicit_wasm_permission_tier
7648        .or_else(|| vm.command_permissions.get(command).copied())
7649        .or_else(|| {
7650            Path::new(&guest_entrypoint)
7651                .file_name()
7652                .and_then(|name| name.to_str())
7653                .and_then(|name| vm.command_permissions.get(name).copied())
7654        });
7655
7656    let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint);
7657    if let Some((javascript_guest_entrypoint, javascript_host_entrypoint)) =
7658        resolve_javascript_command_entrypoint(vm, &guest_entrypoint, &host_entrypoint)
7659    {
7660        prepare_guest_runtime_env(
7661            vm,
7662            &mut env,
7663            &guest_cwd,
7664            &host_cwd,
7665            Some(javascript_guest_entrypoint),
7666        )?;
7667
7668        return Ok(ResolvedChildProcessExecution {
7669            command: command.to_owned(),
7670            process_args: std::iter::once(command.to_owned())
7671                .chain(args.iter().cloned())
7672                .collect(),
7673            runtime: GuestRuntimeKind::JavaScript,
7674            entrypoint: javascript_host_entrypoint.to_string_lossy().into_owned(),
7675            execution_args: args.to_vec(),
7676            env,
7677            guest_cwd,
7678            host_cwd,
7679            wasm_permission_tier: None,
7680            tool_command: false,
7681        });
7682    }
7683    prepare_guest_runtime_env(
7684        vm,
7685        &mut env,
7686        &guest_cwd,
7687        &host_cwd,
7688        Some(guest_entrypoint.clone()),
7689    )?;
7690
7691    Ok(ResolvedChildProcessExecution {
7692        command: command.to_owned(),
7693        process_args: std::iter::once(command.to_owned())
7694            .chain(args.iter().cloned())
7695            .collect(),
7696        runtime: GuestRuntimeKind::WebAssembly,
7697        entrypoint: host_entrypoint.to_string_lossy().into_owned(),
7698        execution_args: args.to_vec(),
7699        env,
7700        guest_cwd,
7701        host_cwd,
7702        wasm_permission_tier,
7703        tool_command: false,
7704    })
7705}
7706
7707const MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH: usize = 4;
7708
7709fn resolve_javascript_command_entrypoint(
7710    vm: &VmState,
7711    guest_entrypoint: &str,
7712    host_entrypoint: &Path,
7713) -> Option<(String, PathBuf)> {
7714    resolve_javascript_command_entrypoint_inner(
7715        vm,
7716        guest_entrypoint,
7717        host_entrypoint,
7718        MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH,
7719    )
7720}
7721
7722fn resolve_javascript_command_entrypoint_inner(
7723    vm: &VmState,
7724    guest_entrypoint: &str,
7725    host_entrypoint: &Path,
7726    redirects_remaining: usize,
7727) -> Option<(String, PathBuf)> {
7728    if redirects_remaining > 0 {
7729        let symlink_target = fs::symlink_metadata(host_entrypoint)
7730            .ok()
7731            .filter(|metadata| metadata.file_type().is_symlink())
7732            .and_then(|_| fs::read_link(host_entrypoint).ok());
7733        if let Some(symlink_target) = symlink_target {
7734            let guest_parent = Path::new(guest_entrypoint)
7735                .parent()
7736                .and_then(|path| path.to_str())
7737                .unwrap_or("/");
7738            let symlink_guest_entrypoint = if symlink_target.is_absolute() {
7739                normalize_path(&symlink_target.to_string_lossy())
7740            } else {
7741                normalize_path(&format!(
7742                    "{guest_parent}/{}",
7743                    symlink_target.to_string_lossy().replace('\\', "/")
7744                ))
7745            };
7746            let symlink_host_entrypoint =
7747                resolve_vm_guest_path_to_host(vm, &symlink_guest_entrypoint);
7748            return resolve_javascript_command_entrypoint_inner(
7749                vm,
7750                &symlink_guest_entrypoint,
7751                &symlink_host_entrypoint,
7752                redirects_remaining - 1,
7753            );
7754        }
7755    }
7756
7757    let script = load_executable_script_preview(host_entrypoint)?;
7758    let interpreter = parse_script_interpreter_name(&script);
7759
7760    if interpreter.is_none() && is_probable_javascript_entrypoint(host_entrypoint, &script) {
7761        return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf()));
7762    }
7763
7764    let interpreter = interpreter?;
7765    if interpreter == "node" {
7766        return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf()));
7767    }
7768
7769    if redirects_remaining == 0 || !matches!(interpreter.as_str(), "sh" | "bash" | "dash") {
7770        return None;
7771    }
7772
7773    let shim_target = parse_node_shell_shim_target(&script)?;
7774    let guest_parent = Path::new(guest_entrypoint)
7775        .parent()
7776        .and_then(|path| path.to_str())
7777        .unwrap_or("/");
7778    let shim_guest_entrypoint = normalize_path(&format!("{guest_parent}/{shim_target}"));
7779    let shim_host_entrypoint = resolve_vm_guest_path_to_host(vm, &shim_guest_entrypoint);
7780    resolve_javascript_command_entrypoint_inner(
7781        vm,
7782        &shim_guest_entrypoint,
7783        &shim_host_entrypoint,
7784        redirects_remaining - 1,
7785    )
7786}
7787
7788fn load_executable_script_preview(path: &Path) -> Option<String> {
7789    let bytes = fs::read(path).ok()?;
7790    let preview_len = bytes.len().min(16 * 1024);
7791    Some(String::from_utf8_lossy(&bytes[..preview_len]).into_owned())
7792}
7793
7794fn parse_script_interpreter_name(script: &str) -> Option<String> {
7795    let shebang = script.lines().next()?.strip_prefix("#!")?.trim();
7796    let mut tokens = shebang.split_whitespace();
7797    let command = tokens.next()?;
7798    let command_name = Path::new(command).file_name()?.to_str()?;
7799    if command_name == "env" {
7800        for token in tokens {
7801            if token.starts_with('-') {
7802                continue;
7803            }
7804            return Path::new(token)
7805                .file_name()
7806                .and_then(|name| name.to_str())
7807                .map(ToOwned::to_owned);
7808        }
7809        return None;
7810    }
7811
7812    Some(command_name.to_owned())
7813}
7814
7815fn parse_node_shell_shim_target(script: &str) -> Option<String> {
7816    for line in script.lines() {
7817        let trimmed = line.trim();
7818        if !trimmed.starts_with("exec ") {
7819            continue;
7820        }
7821
7822        let mut remaining = trimmed;
7823        while let Some(start) = remaining.find("\"$basedir/") {
7824            let after_prefix = &remaining[start + "\"$basedir/".len()..];
7825            let end = after_prefix.find('"')?;
7826            let candidate = &after_prefix[..end];
7827            remaining = &after_prefix[end + 1..];
7828
7829            if candidate.is_empty() || candidate == "node" || candidate.ends_with("/node") {
7830                continue;
7831            }
7832
7833            return Some(candidate.to_owned());
7834        }
7835    }
7836
7837    None
7838}
7839
7840fn is_probable_javascript_entrypoint(path: &Path, script: &str) -> bool {
7841    let extension = path
7842        .extension()
7843        .and_then(|value| value.to_str())
7844        .unwrap_or_default();
7845    if matches!(extension, "js" | "cjs" | "mjs") {
7846        return true;
7847    }
7848
7849    if !path
7850        .components()
7851        .any(|component| component.as_os_str() == "node_modules")
7852    {
7853        return false;
7854    }
7855
7856    let preview = script.trim_start_matches('\u{feff}').trim_start();
7857    !preview.is_empty()
7858        && !preview.starts_with("#!")
7859        && (preview.starts_with("\"use strict\"")
7860            || preview.starts_with("'use strict'")
7861            || preview.starts_with("import ")
7862            || preview.starts_with("export ")
7863            || preview.starts_with("const ")
7864            || preview.starts_with("let ")
7865            || preview.starts_with("var ")
7866            || preview.starts_with("Object.defineProperty(exports")
7867            || preview.starts_with("module.exports")
7868            || preview.starts_with("require("))
7869}
7870
7871fn resolve_guest_execution_cwd(vm: &VmState, value: Option<&str>) -> String {
7872    value
7873        .map(normalize_path)
7874        .unwrap_or_else(|| vm.guest_cwd.clone())
7875}
7876
7877fn resolve_execution_cwds(vm: &VmState, value: Option<&str>) -> (String, PathBuf, bool) {
7878    if let Some(raw_cwd) = value {
7879        let normalized_vm_host_cwd = normalize_host_path(&vm.host_cwd);
7880        let requested_host_cwd = normalize_host_path(Path::new(raw_cwd));
7881        if path_is_within_root(&requested_host_cwd, &normalized_vm_host_cwd) {
7882            let relative = requested_host_cwd
7883                .strip_prefix(&normalized_vm_host_cwd)
7884                .unwrap_or_else(|_| Path::new(""));
7885            let relative = relative.to_string_lossy().replace('\\', "/");
7886            let guest_cwd = if relative.is_empty() {
7887                String::from("/")
7888            } else {
7889                normalize_path(&format!("/{relative}"))
7890            };
7891            return (guest_cwd, requested_host_cwd, true);
7892        }
7893    }
7894
7895    let guest_cwd = resolve_guest_execution_cwd(vm, value);
7896    let host_cwd = if value.is_none() {
7897        vm.host_cwd.clone()
7898    } else {
7899        resolve_vm_guest_path_to_host(vm, &guest_cwd)
7900    };
7901    (guest_cwd, host_cwd, value.is_none())
7902}
7903
7904fn resolve_vm_guest_path_to_host(vm: &VmState, guest_path: &str) -> PathBuf {
7905    host_mount_path_for_guest_path(vm, guest_path)
7906        .unwrap_or_else(|| shadow_path_for_guest(vm, guest_path))
7907}
7908
7909fn shadow_path_for_guest(vm: &VmState, guest_path: &str) -> PathBuf {
7910    let normalized = normalize_path(guest_path);
7911    let relative = normalized.trim_start_matches('/');
7912    if relative.is_empty() {
7913        return vm.cwd.clone();
7914    }
7915    vm.cwd.join(relative)
7916}
7917
7918fn apply_shell_cwd_prefix(command: &str, mut args: Vec<String>, guest_cwd: &str) -> Vec<String> {
7919    if guest_cwd == "/" || !is_shell_command(command) {
7920        return args;
7921    }
7922
7923    let Some(flag) = args.first() else {
7924        return args;
7925    };
7926    if !matches!(flag.as_str(), "-c" | "-lc") || args.len() < 2 {
7927        return args;
7928    }
7929
7930    let command_text = args[1].clone();
7931    let quoted_cwd = shell_single_quote(guest_cwd);
7932    args[1] = format!("cd {quoted_cwd} && {command_text}");
7933    args
7934}
7935
7936fn is_shell_command(command: &str) -> bool {
7937    Path::new(command)
7938        .file_name()
7939        .and_then(|name| name.to_str())
7940        .unwrap_or(command)
7941        .trim_end_matches(".exe")
7942        .eq("sh")
7943        || Path::new(command)
7944            .file_name()
7945            .and_then(|name| name.to_str())
7946            .unwrap_or(command)
7947            .trim_end_matches(".exe")
7948            .eq("bash")
7949}
7950
7951fn shell_single_quote(value: &str) -> String {
7952    if value.is_empty() {
7953        return String::from("''");
7954    }
7955    format!("'{}'", value.replace('\'', "'\"'\"'"))
7956}
7957
7958pub(crate) fn sync_active_process_host_writes_to_kernel(
7959    vm: &mut VmState,
7960) -> Result<(), SidecarError> {
7961    if vm.root_filesystem_mode != RootFilesystemMode::ReadOnly {
7962        let shadow_root = vm.cwd.clone();
7963        sync_host_directory_tree_to_kernel(vm, &shadow_root, "/")?;
7964    }
7965
7966    let normalized_vm_root = normalize_host_path(&vm.cwd);
7967    let extra_roots = collect_active_process_host_sync_roots(vm, &normalized_vm_root);
7968    for (host_cwd, guest_cwd) in extra_roots {
7969        sync_host_directory_tree_to_kernel(vm, &host_cwd, &guest_cwd)?;
7970    }
7971
7972    Ok(())
7973}
7974
7975fn collect_active_process_host_sync_roots(
7976    vm: &VmState,
7977    normalized_vm_root: &Path,
7978) -> Vec<(PathBuf, String)> {
7979    let mut roots = Vec::new();
7980    let mut seen = BTreeSet::new();
7981
7982    for process in vm.active_processes.values() {
7983        collect_process_host_sync_roots(process, normalized_vm_root, &mut seen, &mut roots);
7984    }
7985
7986    roots
7987}
7988
7989fn collect_process_host_sync_roots(
7990    process: &ActiveProcess,
7991    normalized_vm_root: &Path,
7992    seen: &mut BTreeSet<(PathBuf, String)>,
7993    roots: &mut Vec<(PathBuf, String)>,
7994) {
7995    let normalized_host_cwd = normalize_host_path(&process.host_cwd);
7996    if !path_is_within_root(&normalized_host_cwd, normalized_vm_root) {
7997        let guest_cwd = normalize_path(&process.guest_cwd);
7998        if seen.insert((normalized_host_cwd.clone(), guest_cwd.clone())) {
7999            roots.push((normalized_host_cwd, guest_cwd));
8000        }
8001    }
8002
8003    for child in process.child_processes.values() {
8004        collect_process_host_sync_roots(child, normalized_vm_root, seen, roots);
8005    }
8006}
8007
8008fn sync_process_host_writes_to_kernel(
8009    vm: &mut VmState,
8010    process: &ActiveProcess,
8011) -> Result<(), SidecarError> {
8012    if vm.root_filesystem_mode != RootFilesystemMode::ReadOnly {
8013        let shadow_root = vm.cwd.clone();
8014        sync_host_directory_tree_to_kernel(vm, &shadow_root, "/")?;
8015    }
8016
8017    if !path_is_within_root(
8018        &normalize_host_path(&process.host_cwd),
8019        &normalize_host_path(&vm.cwd),
8020    ) {
8021        sync_host_directory_tree_to_kernel(vm, &process.host_cwd, &process.guest_cwd)?;
8022    }
8023
8024    Ok(())
8025}
8026
8027fn sync_host_directory_tree_to_kernel(
8028    vm: &mut VmState,
8029    host_root: &Path,
8030    guest_root: &str,
8031) -> Result<(), SidecarError> {
8032    let normalized_host_root = normalize_host_path(host_root);
8033    let normalized_guest_root = normalize_path(guest_root);
8034    let mut synced_file_times = BTreeMap::new();
8035    sync_host_directory_tree_to_kernel_inner(
8036        vm,
8037        &normalized_host_root,
8038        &normalized_host_root,
8039        &normalized_guest_root,
8040        &mut synced_file_times,
8041    )
8042}
8043
8044fn sync_host_directory_tree_to_kernel_inner(
8045    vm: &mut VmState,
8046    host_root: &Path,
8047    current_host_dir: &Path,
8048    guest_root: &str,
8049    synced_file_times: &mut BTreeMap<(u64, u64), (u64, u64)>,
8050) -> Result<(), SidecarError> {
8051    let entries = match fs::read_dir(current_host_dir) {
8052        Ok(entries) => entries,
8053        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
8054        Err(error) => {
8055            return Err(SidecarError::Io(format!(
8056                "failed to read host shadow directory {}: {error}",
8057                current_host_dir.display()
8058            )));
8059        }
8060    };
8061
8062    for entry in entries {
8063        let entry = entry.map_err(|error| {
8064            SidecarError::Io(format!(
8065                "failed to read host shadow entry in {}: {error}",
8066                current_host_dir.display()
8067            ))
8068        })?;
8069        let host_path = entry.path();
8070        let file_type = entry.file_type().map_err(|error| {
8071            SidecarError::Io(format!(
8072                "failed to stat host shadow entry {}: {error}",
8073                host_path.display()
8074            ))
8075        })?;
8076        let relative_path = host_path
8077            .strip_prefix(host_root)
8078            .map_err(|error| {
8079                SidecarError::InvalidState(format!(
8080                    "failed to relativize host shadow path {} against {}: {error}",
8081                    host_path.display(),
8082                    host_root.display()
8083                ))
8084            })?
8085            .to_string_lossy()
8086            .replace('\\', "/");
8087        let guest_path = if guest_root == "/" {
8088            normalize_path(&format!("/{relative_path}"))
8089        } else {
8090            normalize_path(&format!(
8091                "{}/{}",
8092                guest_root.trim_end_matches('/'),
8093                relative_path
8094            ))
8095        };
8096
8097        if should_skip_shadow_sync_path(vm, &guest_path) {
8098            continue;
8099        }
8100
8101        if file_type.is_dir() {
8102            let metadata = entry.metadata().map_err(|error| {
8103                SidecarError::Io(format!(
8104                    "failed to read host shadow metadata {}: {error}",
8105                    host_path.display()
8106                ))
8107            })?;
8108            if !is_shadow_bootstrap_dir(&guest_path)
8109                && !vm.kernel.exists(&guest_path).unwrap_or(false)
8110            {
8111                vm.kernel.mkdir(&guest_path, true).map_err(|error| {
8112                    SidecarError::InvalidState(format!(
8113                        "failed to sync host shadow directory {} to guest {}: {}",
8114                        host_path.display(),
8115                        guest_path,
8116                        kernel_error(error)
8117                    ))
8118                })?;
8119                vm.kernel
8120                    .chmod(&guest_path, host_shadow_mode(&metadata))
8121                    .map_err(|error| {
8122                        SidecarError::InvalidState(format!(
8123                            "failed to sync host shadow directory mode {} to guest {}: {}",
8124                            host_path.display(),
8125                            guest_path,
8126                            kernel_error(error)
8127                        ))
8128                    })?;
8129            }
8130            sync_host_directory_tree_to_kernel_inner(
8131                vm,
8132                host_root,
8133                &host_path,
8134                guest_root,
8135                synced_file_times,
8136            )?;
8137            continue;
8138        }
8139
8140        if file_type.is_file() {
8141            let metadata = entry.metadata().map_err(|error| {
8142                SidecarError::Io(format!(
8143                    "failed to read host shadow metadata {}: {error}",
8144                    host_path.display()
8145                ))
8146            })?;
8147            let timestamp_key = (metadata.dev(), metadata.ino());
8148            let (atime_ms, mtime_ms) =
8149                *synced_file_times.entry(timestamp_key).or_insert_with(|| {
8150                    (
8151                        metadata_time_ms(metadata.atime(), metadata.atime_nsec()),
8152                        metadata_time_ms(metadata.mtime(), metadata.mtime_nsec()),
8153                    )
8154                });
8155            let desired_mode = host_shadow_mode(&metadata);
8156            let bytes = read_host_shadow_file(&host_path, desired_mode).map_err(|error| {
8157                SidecarError::Io(format!(
8158                    "failed to read host shadow file {}: {error}",
8159                    host_path.display()
8160                ))
8161            })?;
8162            vm.kernel.write_file(&guest_path, bytes).map_err(|error| {
8163                SidecarError::InvalidState(format!(
8164                    "failed to sync host shadow file {} to guest {}: {}",
8165                    host_path.display(),
8166                    guest_path,
8167                    kernel_error(error)
8168                ))
8169            })?;
8170            vm.kernel
8171                .chmod(&guest_path, desired_mode)
8172                .map_err(|error| {
8173                    SidecarError::InvalidState(format!(
8174                        "failed to sync host shadow file mode {} to guest {}: {}",
8175                        host_path.display(),
8176                        guest_path,
8177                        kernel_error(error)
8178                    ))
8179                })?;
8180            vm.kernel
8181                .utimes(&guest_path, atime_ms, mtime_ms)
8182                .map_err(|error| {
8183                    SidecarError::InvalidState(format!(
8184                        "failed to sync host shadow file times {} to guest {}: {}",
8185                        host_path.display(),
8186                        guest_path,
8187                        kernel_error(error)
8188                    ))
8189                })?;
8190            continue;
8191        }
8192
8193        if file_type.is_symlink() {
8194            let target = match fs::read_link(&host_path) {
8195                Ok(target) => target,
8196                Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
8197                Err(error) => {
8198                    return Err(SidecarError::Io(format!(
8199                        "failed to read host shadow symlink {}: {error}",
8200                        host_path.display()
8201                    )));
8202                }
8203            };
8204            replace_kernel_symlink(vm, &guest_path, &target.to_string_lossy())?;
8205        }
8206    }
8207
8208    Ok(())
8209}
8210
8211fn replace_kernel_symlink(
8212    vm: &mut VmState,
8213    guest_path: &str,
8214    target: &str,
8215) -> Result<(), SidecarError> {
8216    if vm.kernel.symlink(target, guest_path).is_ok() {
8217        return Ok(());
8218    }
8219
8220    if let Ok(existing_target) = vm.kernel.read_link(guest_path) {
8221        if existing_target == target {
8222            return Ok(());
8223        }
8224    }
8225
8226    let _ = vm.kernel.remove_file(guest_path);
8227    let _ = vm.kernel.remove_dir(guest_path);
8228    vm.kernel
8229        .symlink(target, guest_path)
8230        .map_err(kernel_error)?;
8231    Ok(())
8232}
8233
8234fn host_shadow_mode(metadata: &fs::Metadata) -> u32 {
8235    metadata.permissions().mode() & 0o7777
8236}
8237
8238/// Reads a shadow-root file back into the kernel even when guest-visible mode
8239/// bits make it unreadable for the host user. The sidecar is the kernel for
8240/// this tree, so guest permission bits (for example a 0o200 write-only file
8241/// produced by `chmod` plus a shell append redirect) must not break the
8242/// exit-time shadow sync. The original mode is restored after the read.
8243fn read_host_shadow_file(host_path: &Path, mode: u32) -> std::io::Result<Vec<u8>> {
8244    match fs::read(host_path) {
8245        Ok(bytes) => Ok(bytes),
8246        Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => {
8247            fs::set_permissions(host_path, fs::Permissions::from_mode(mode | 0o400))?;
8248            let result = fs::read(host_path);
8249            fs::set_permissions(host_path, fs::Permissions::from_mode(mode))?;
8250            result
8251        }
8252        Err(error) => Err(error),
8253    }
8254}
8255
8256fn metadata_time_ms(seconds: i64, nanos: i64) -> u64 {
8257    let seconds = seconds.max(0) as u64;
8258    let nanos = nanos.max(0) as u64;
8259    seconds
8260        .saturating_mul(1_000)
8261        .saturating_add(nanos / 1_000_000)
8262}
8263
8264fn is_shadow_bootstrap_dir(path: &str) -> bool {
8265    matches!(
8266        path,
8267        "/dev"
8268            | "/proc"
8269            | "/tmp"
8270            | "/bin"
8271            | "/lib"
8272            | "/sbin"
8273            | "/boot"
8274            | "/etc"
8275            | "/root"
8276            | "/run"
8277            | "/srv"
8278            | "/sys"
8279            | "/opt"
8280            | "/mnt"
8281            | "/media"
8282            | "/home"
8283            | "/home/user"
8284            | "/usr"
8285            | "/usr/bin"
8286            | "/usr/games"
8287            | "/usr/include"
8288            | "/usr/lib"
8289            | "/usr/libexec"
8290            | "/usr/man"
8291            | "/usr/local"
8292            | "/usr/local/bin"
8293            | "/usr/sbin"
8294            | "/usr/share"
8295            | "/usr/share/man"
8296            | "/var"
8297            | "/var/cache"
8298            | "/var/empty"
8299            | "/var/lib"
8300            | "/var/lock"
8301            | "/var/log"
8302            | "/var/run"
8303            | "/var/spool"
8304            | "/var/tmp"
8305            | "/etc/agentos"
8306    )
8307}
8308
8309#[cfg(test)]
8310mod shadow_sync_tests {
8311    use super::{is_protected_agentos_shadow_sync_path, is_shadow_bootstrap_dir};
8312
8313    #[test]
8314    fn shadow_bootstrap_sync_skips_virtual_home_tree() {
8315        assert!(is_shadow_bootstrap_dir("/home"));
8316        assert!(is_shadow_bootstrap_dir("/home/user"));
8317    }
8318
8319    #[test]
8320    fn protected_agentos_paths_are_not_shadow_synced() {
8321        assert!(is_protected_agentos_shadow_sync_path("/etc/agentos"));
8322        assert!(is_protected_agentos_shadow_sync_path(
8323            "/etc/agentos/instructions.md"
8324        ));
8325        assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos-copy"));
8326        assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos.md"));
8327    }
8328}
8329
8330fn is_kernel_owned_shadow_sync_path(path: &str) -> bool {
8331    matches!(path, "/dev" | "/proc" | "/sys")
8332        || path.starts_with("/dev/")
8333        || path.starts_with("/proc/")
8334        || path.starts_with("/sys/")
8335}
8336
8337pub(crate) fn is_protected_agentos_shadow_sync_path(path: &str) -> bool {
8338    path == "/etc/agentos" || path.starts_with("/etc/agentos/")
8339}
8340
8341fn should_skip_shadow_sync_path(vm: &VmState, guest_path: &str) -> bool {
8342    is_kernel_owned_shadow_sync_path(guest_path)
8343        || is_protected_agentos_shadow_sync_path(guest_path)
8344        || host_mount_path_for_guest_path_from_mounts(&vm.configuration.mounts, guest_path)
8345            .is_some()
8346}
8347
8348fn resolve_path_like_guest_specifier(cwd: &str, specifier: &str) -> String {
8349    if specifier.starts_with("file://") {
8350        normalize_path(specifier.trim_start_matches("file://"))
8351    } else if specifier.starts_with("file:") {
8352        normalize_path(specifier.trim_start_matches("file:"))
8353    } else if specifier.starts_with('/') {
8354        normalize_path(specifier)
8355    } else {
8356        normalize_path(&format!("{cwd}/{specifier}"))
8357    }
8358}
8359
8360fn guest_entrypoint_for_specifier(cwd: &str, specifier: &str) -> Option<String> {
8361    is_path_like_specifier(specifier).then(|| resolve_path_like_guest_specifier(cwd, specifier))
8362}
8363
8364fn is_node_runtime_command(command: &str) -> bool {
8365    matches!(command, "node" | "npm" | "npx")
8366        || Path::new(command)
8367            .file_name()
8368            .and_then(|name| name.to_str())
8369            .is_some_and(|name| matches!(name, "node" | "npm" | "npx"))
8370}
8371
8372fn resolve_special_node_cli_invocation(
8373    args: &[String],
8374    env: &mut BTreeMap<String, String>,
8375) -> Option<(String, Vec<String>)> {
8376    let first = args.first()?;
8377    match first.as_str() {
8378        "-e" | "--eval" => {
8379            env.insert(
8380                String::from("AGENT_OS_NODE_EVAL"),
8381                args.get(1).cloned().unwrap_or_default(),
8382            );
8383            Some((first.clone(), args.iter().skip(2).cloned().collect()))
8384        }
8385        "-v" | "--version" => {
8386            env.insert(
8387                String::from("AGENT_OS_NODE_EVAL"),
8388                String::from("console.log(process.version);"),
8389            );
8390            Some((String::from("-e"), args.to_vec()))
8391        }
8392        _ => None,
8393    }
8394}
8395
8396fn node_runtime_command_name(command: &str) -> Option<&str> {
8397    let name = Path::new(command)
8398        .file_name()
8399        .and_then(|name| name.to_str())?;
8400    matches!(name, "node" | "npm" | "npx").then_some(name)
8401}
8402
8403struct ResolvedHostNodeCliEntrypoint {
8404    command_name: String,
8405    guest_root: String,
8406    guest_entrypoint: String,
8407    package_root: PathBuf,
8408}
8409
8410fn resolve_host_node_cli_entrypoint(command: &str) -> Option<ResolvedHostNodeCliEntrypoint> {
8411    let command_name = node_runtime_command_name(command)?;
8412    if !matches!(command_name, "npm" | "npx") {
8413        return None;
8414    }
8415
8416    let path = std::env::var_os("PATH")?;
8417    for root in std::env::split_paths(&path) {
8418        let candidate = root.join(command_name);
8419        if !candidate.is_file() {
8420            continue;
8421        }
8422        let entrypoint = candidate.canonicalize().ok().unwrap_or(candidate);
8423        let package_root = entrypoint.parent()?.parent()?.to_path_buf();
8424        let guest_root = format!("/__secure_exec/node-runtime/{command_name}");
8425        let relative_entrypoint = entrypoint.strip_prefix(&package_root).ok()?;
8426        let guest_entrypoint = normalize_path(&format!(
8427            "{guest_root}/{}",
8428            relative_entrypoint.to_string_lossy().replace('\\', "/")
8429        ));
8430        return Some(ResolvedHostNodeCliEntrypoint {
8431            command_name: command_name.to_owned(),
8432            guest_root,
8433            guest_entrypoint,
8434            package_root,
8435        });
8436    }
8437
8438    None
8439}
8440
8441fn build_host_node_cli_eval(cli: &ResolvedHostNodeCliEntrypoint) -> String {
8442    let guest_npm_main = normalize_path(&format!("{}/lib/npm.js", cli.guest_root));
8443    let guest_npm_cli = normalize_path(&format!("{}/bin/npm-cli.js", cli.guest_root));
8444    let guest_package_json = normalize_path(&format!("{}/package.json", cli.guest_root));
8445    let guest_display_module = normalize_path(&format!("{}/lib/utils/display.js", cli.guest_root));
8446    let guest_log_file_module =
8447        normalize_path(&format!("{}/lib/utils/log-file.js", cli.guest_root));
8448    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); } } }";
8449    let display_stub = format!(
8450        "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 }};",
8451        display_module = serde_json::to_string(&guest_display_module)
8452            .unwrap_or_else(|_| format!("\"{guest_display_module}\"")),
8453        log_file_module = serde_json::to_string(&guest_log_file_module)
8454            .unwrap_or_else(|_| format!("\"{guest_log_file_module}\"")),
8455    );
8456    let registry_fetch_stub = "const { createRequire: __agentOsCreateRequire } = require('module'); const __agentOsNpmRequire = __agentOsCreateRequire(require.resolve(__AGENT_OS_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)); }";
8457    match cli.command_name.as_str() {
8458        "npx" => format!(
8459            "{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); }});",
8460            debug_preamble = debug_preamble,
8461            display_stub = display_stub,
8462            registry_fetch_stub = registry_fetch_stub.replace(
8463                "__AGENT_OS_NPM_MAIN__",
8464                &serde_json::to_string(&guest_npm_main)
8465                    .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8466            ),
8467            npm_main = serde_json::to_string(&guest_npm_main)
8468                .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8469            npm_cli = serde_json::to_string(&guest_npm_cli)
8470                .unwrap_or_else(|_| format!("\"{guest_npm_cli}\"")),
8471            package_json = serde_json::to_string(&guest_package_json)
8472                .unwrap_or_else(|_| format!("\"{guest_package_json}\"")),
8473        ),
8474        _ => format!(
8475            "{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); }});",
8476            debug_preamble = debug_preamble,
8477            display_stub = display_stub,
8478            registry_fetch_stub = registry_fetch_stub.replace(
8479                "__AGENT_OS_NPM_MAIN__",
8480                &serde_json::to_string(&guest_npm_main)
8481                    .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8482            ),
8483            npm_main = serde_json::to_string(&guest_npm_main)
8484                .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8485            package_json = serde_json::to_string(&guest_package_json)
8486                .unwrap_or_else(|_| format!("\"{guest_package_json}\"")),
8487        ),
8488    }
8489}
8490
8491fn resolve_guest_command_entrypoint(
8492    vm: &VmState,
8493    guest_cwd: &str,
8494    command: &str,
8495    path_env: Option<&str>,
8496) -> Option<String> {
8497    if !is_path_like_specifier(command) {
8498        if let Some(entrypoint) = vm.command_guest_paths.get(command) {
8499            return Some(entrypoint.clone());
8500        }
8501
8502        for search_dir in guest_command_search_dirs(vm, guest_cwd, path_env) {
8503            let candidate = normalize_path(&format!("{search_dir}/{command}"));
8504            if let Some(entrypoint) = resolve_guest_command_path_candidate(vm, &candidate) {
8505                return Some(entrypoint);
8506            }
8507        }
8508
8509        return None;
8510    }
8511
8512    let normalized = resolve_path_like_guest_specifier(guest_cwd, command);
8513    resolve_guest_command_path_candidate(vm, &normalized).or_else(|| {
8514        // Some guest shells materialize PATH lookups into absolute candidate paths.
8515        // If that path points into a searched directory but does not exist, fall
8516        // back to the command basename so the sidecar can remap VM command packages.
8517        let parent_dir = Path::new(&normalized).parent()?.to_str()?;
8518        if !guest_command_search_dirs(vm, guest_cwd, path_env)
8519            .iter()
8520            .any(|search_dir| normalize_path(search_dir) == normalize_path(parent_dir))
8521        {
8522            return None;
8523        }
8524
8525        let file_name = Path::new(&normalized).file_name()?.to_str()?;
8526        vm.command_guest_paths.get(file_name).cloned()
8527    })
8528}
8529
8530fn guest_command_search_dirs(vm: &VmState, guest_cwd: &str, path_env: Option<&str>) -> Vec<String> {
8531    let mut search_dirs = Vec::new();
8532    let mut seen = BTreeSet::new();
8533
8534    if let Some(path) = path_env.or_else(|| vm.guest_env.get("PATH").map(String::as_str)) {
8535        for segment in path.split(':') {
8536            let trimmed = segment.trim();
8537            if trimmed.is_empty() {
8538                continue;
8539            }
8540            let normalized = if trimmed.starts_with('/') {
8541                normalize_path(trimmed)
8542            } else {
8543                normalize_path(&format!("{guest_cwd}/{trimmed}"))
8544            };
8545            if seen.insert(normalized.clone()) {
8546                search_dirs.push(normalized);
8547            }
8548        }
8549    }
8550
8551    for fallback in ["/bin", "/usr/bin", "/usr/local/bin"] {
8552        let normalized = String::from(fallback);
8553        if seen.insert(normalized.clone()) {
8554            search_dirs.push(normalized);
8555        }
8556    }
8557
8558    search_dirs
8559}
8560
8561fn resolve_guest_command_path_candidate(vm: &VmState, candidate: &str) -> Option<String> {
8562    if candidate.starts_with("/bin/")
8563        || candidate.starts_with("/usr/bin/")
8564        || candidate.starts_with("/usr/local/bin/")
8565        || candidate.starts_with("/__secure_exec/commands/")
8566    {
8567        if let Some(file_name) = Path::new(candidate)
8568            .file_name()
8569            .and_then(|name| name.to_str())
8570        {
8571            if let Some(guest_entrypoint) = vm.command_guest_paths.get(file_name) {
8572                return Some(guest_entrypoint.clone());
8573            }
8574        }
8575    }
8576
8577    if vm
8578        .kernel
8579        .exists(candidate)
8580        .ok()
8581        .is_some_and(|exists| exists)
8582    {
8583        return Some(normalize_path(candidate));
8584    }
8585
8586    resolve_vm_guest_path_to_host(vm, candidate)
8587        .is_file()
8588        .then(|| normalize_path(candidate))
8589}
8590
8591fn resolve_host_entrypoint_within_vm_host_cwd(
8592    vm: &VmState,
8593    specifier: &str,
8594) -> Option<(String, String)> {
8595    let candidate = Path::new(specifier);
8596    if !candidate.is_absolute() {
8597        return None;
8598    }
8599
8600    let normalized_entrypoint = normalize_host_path(candidate);
8601    let normalized_host_cwd = normalize_host_path(&vm.host_cwd);
8602    if !path_is_within_root(&normalized_entrypoint, &normalized_host_cwd) {
8603        return None;
8604    }
8605
8606    let relative = normalized_entrypoint
8607        .strip_prefix(&normalized_host_cwd)
8608        .ok()?
8609        .to_string_lossy()
8610        .replace('\\', "/");
8611    let guest_entrypoint = if relative.is_empty() {
8612        String::from("/")
8613    } else {
8614        normalize_path(&format!("/{relative}"))
8615    };
8616    Some((
8617        guest_entrypoint,
8618        normalized_entrypoint.to_string_lossy().into_owned(),
8619    ))
8620}
8621
8622fn prepare_guest_runtime_env(
8623    vm: &VmState,
8624    env: &mut BTreeMap<String, String>,
8625    guest_cwd: &str,
8626    host_cwd: &Path,
8627    guest_entrypoint: Option<String>,
8628) -> Result<(), SidecarError> {
8629    let user = vm.kernel.user_profile();
8630    let path_mappings = runtime_guest_path_mappings(vm);
8631    let read_paths = expand_host_access_paths(
8632        std::iter::once(vm.cwd.clone())
8633            .chain(
8634                path_mappings
8635                    .iter()
8636                    .map(|mapping| PathBuf::from(&mapping.host_path)),
8637            )
8638            .chain(std::iter::once(host_cwd.to_path_buf()))
8639            .collect::<Vec<_>>()
8640            .as_slice(),
8641    );
8642    let write_paths = dedupe_host_paths(
8643        std::iter::once(vm.cwd.clone())
8644            .chain(std::iter::once(host_cwd.to_path_buf()))
8645            .chain(runtime_guest_writable_host_paths(vm))
8646            .collect::<Vec<_>>()
8647            .as_slice(),
8648    );
8649    let allowed_node_builtins = configured_allowed_node_builtins(vm);
8650    let loopback_exempt_ports = configured_loopback_exempt_ports(vm);
8651
8652    env.insert(
8653        String::from("AGENT_OS_GUEST_PATH_MAPPINGS"),
8654        serde_json::to_string(&path_mappings).map_err(|error| {
8655            SidecarError::InvalidState(format!("failed to encode guest path mappings: {error}"))
8656        })?,
8657    );
8658    env.entry(String::from(EXECUTION_SANDBOX_ROOT_ENV))
8659        .or_insert_with(|| normalize_host_path(&vm.cwd).to_string_lossy().into_owned());
8660    env.insert(
8661        String::from("AGENT_OS_EXTRA_FS_READ_PATHS"),
8662        serde_json::to_string(
8663            &read_paths
8664                .iter()
8665                .map(|path| path.to_string_lossy().into_owned())
8666                .collect::<Vec<_>>(),
8667        )
8668        .map_err(|error| {
8669            SidecarError::InvalidState(format!("failed to encode read paths: {error}"))
8670        })?,
8671    );
8672    env.insert(
8673        String::from("AGENT_OS_EXTRA_FS_WRITE_PATHS"),
8674        serde_json::to_string(
8675            &write_paths
8676                .iter()
8677                .map(|path| path.to_string_lossy().into_owned())
8678                .collect::<Vec<_>>(),
8679        )
8680        .map_err(|error| {
8681            SidecarError::InvalidState(format!("failed to encode write paths: {error}"))
8682        })?,
8683    );
8684    env.insert(
8685        String::from("AGENT_OS_ALLOWED_NODE_BUILTINS"),
8686        serde_json::to_string(&allowed_node_builtins).map_err(|error| {
8687            SidecarError::InvalidState(format!("failed to encode allowed builtins: {error}"))
8688        })?,
8689    );
8690    // The guest JS host platform drives subtractive global scrubbing in the
8691    // per-execution runtime shim (see prepend_v8_runtime_shim).
8692    env.insert(
8693        String::from("AGENT_OS_JS_PLATFORM"),
8694        js_runtime_platform_env(vm).to_owned(),
8695    );
8696    // Module-resolution mode (omitted when full Node resolution / the default).
8697    if let Some(resolution) = js_runtime_module_resolution_env(vm) {
8698        env.insert(
8699            String::from("AGENT_OS_JS_MODULE_RESOLUTION"),
8700            resolution.to_owned(),
8701        );
8702    }
8703    // Builtin allow-list gate for the live resolver. Present only when builtins
8704    // should be restricted (non-node platform => deny all; node + explicit
8705    // allow-list => exactly those). Absent => unrestricted (node default).
8706    if let Some(allowlist) = js_runtime_enforced_builtins(vm) {
8707        env.insert(
8708            String::from("AGENT_OS_JS_BUILTIN_ALLOWLIST"),
8709            serde_json::to_string(&allowlist).map_err(|error| {
8710                SidecarError::InvalidState(format!(
8711                    "failed to encode jsRuntime builtin allow-list: {error}"
8712                ))
8713            })?,
8714        );
8715    }
8716    // Virtual OS identity (os.cpus/totalmem/freemem/homedir/userInfo/...) now
8717    // rides the typed `guest_runtime` (see `guest_runtime_identity`), exposed to
8718    // the guest as the `__agentOsVirtualOs` structured global by the runtime
8719    // shim — no longer the `AGENT_OS_VIRTUAL_OS_*` env vars.
8720    // Virtual process uid/gid now ride the typed `guest_runtime` identity
8721    // (see `guest_runtime_identity`), not the `AGENT_OS_VIRTUAL_PROCESS_*` env.
8722    env.entry(String::from("HOME"))
8723        .or_insert_with(|| user.homedir.clone());
8724    env.entry(String::from("USER"))
8725        .or_insert_with(|| user.username.clone());
8726    env.entry(String::from("LOGNAME"))
8727        .or_insert_with(|| user.username.clone());
8728    env.entry(String::from("SHELL"))
8729        .or_insert_with(|| user.shell.clone());
8730    env.entry(String::from("PATH")).or_insert_with(|| {
8731        vm.guest_env
8732            .get("PATH")
8733            .cloned()
8734            .unwrap_or_else(|| crate::vm::DEFAULT_GUEST_PATH_ENV.to_owned())
8735    });
8736    env.entry(String::from("TMPDIR"))
8737        .or_insert_with(|| String::from("/tmp"));
8738    env.insert(String::from("PWD"), guest_cwd.to_owned());
8739    if !loopback_exempt_ports.is_empty() {
8740        env.insert(
8741            String::from(LOOPBACK_EXEMPT_PORTS_ENV),
8742            serde_json::to_string(&loopback_exempt_ports).map_err(|error| {
8743                SidecarError::InvalidState(format!("failed to encode loopback exemptions: {error}"))
8744            })?,
8745        );
8746    }
8747    if let Some(guest_entrypoint) = guest_entrypoint {
8748        env.insert(String::from("AGENT_OS_GUEST_ENTRYPOINT"), guest_entrypoint);
8749    }
8750    Ok(())
8751}
8752
8753fn virtual_os_cpu_count(resource_limits: &ResourceLimits) -> usize {
8754    resource_limits.virtual_cpu_count.unwrap_or(1).max(1)
8755}
8756
8757fn virtual_os_totalmem_bytes(resource_limits: &ResourceLimits) -> u64 {
8758    resource_limits
8759        .max_wasm_memory_bytes
8760        .unwrap_or(1024 * 1024 * 1024)
8761}
8762
8763fn virtual_os_freemem_bytes(resource_limits: &ResourceLimits) -> u64 {
8764    resource_limits
8765        .max_wasm_memory_bytes
8766        .unwrap_or(512 * 1024 * 1024)
8767}
8768
8769/// Build the typed per-execution JavaScript limits from the per-VM `VmLimits`
8770/// (sourced from `CreateVmConfig` on the BARE wire). These ride the execution
8771/// request, not `AGENT_OS_*` env vars — see the env-vs-wire rule in
8772/// `crates/sidecar/CLAUDE.md`.
8773fn javascript_execution_limits(vm: &VmState) -> JavascriptExecutionLimits {
8774    JavascriptExecutionLimits {
8775        v8_heap_limit_mb: vm.limits.js_runtime.v8_heap_limit_mb,
8776        sync_rpc_wait_timeout_ms: vm.limits.js_runtime.sync_rpc_wait_timeout_ms,
8777    }
8778}
8779
8780/// Build the typed per-execution guest-runtime identity (virtual `process.*`)
8781/// from kernel state. Replaces the `AGENT_OS_VIRTUAL_PROCESS_{UID,GID,PID,PPID}`
8782/// env round-trip: the runtime shim reads these from `guest_runtime`, not env.
8783/// `uid`/`gid` come from the VM user profile (applied to every guest);
8784/// `pid`/`ppid` are per-process and only set for paths that assigned them.
8785fn guest_runtime_identity(
8786    vm: &VmState,
8787    virtual_pid: Option<u64>,
8788    virtual_ppid: Option<u64>,
8789) -> GuestRuntimeConfig {
8790    let user = vm.kernel.user_profile();
8791    let resource_limits = vm.kernel.resource_limits();
8792    GuestRuntimeConfig {
8793        virtual_uid: Some(u64::from(user.uid)),
8794        virtual_gid: Some(u64::from(user.gid)),
8795        virtual_pid,
8796        virtual_ppid,
8797        virtual_exec_path: None,
8798        os_cpu_count: Some(virtual_os_cpu_count(resource_limits) as u64),
8799        os_totalmem: Some(virtual_os_totalmem_bytes(resource_limits)),
8800        os_freemem: Some(virtual_os_freemem_bytes(resource_limits)),
8801        os_homedir: Some(user.homedir.clone()),
8802        os_hostname: None,
8803        os_shell: Some(user.shell.clone()),
8804        os_user: Some(user.username.clone()),
8805    }
8806}
8807
8808/// The guest's virtual home directory, sourced from the VM user profile (the
8809/// same value carried to the guest as `os.homedir()` via `guest_runtime`). Used
8810/// by sidecar-internal `~`-path resolution; falls back to `/root` for a
8811/// non-absolute profile value.
8812fn guest_virtual_home(vm: &VmState) -> String {
8813    let homedir = vm.kernel.user_profile().homedir;
8814    if homedir.starts_with('/') {
8815        homedir
8816    } else {
8817        String::from("/root")
8818    }
8819}
8820
8821/// Build the typed per-execution Python limits from the per-VM `VmLimits`.
8822fn python_execution_limits(vm: &VmState) -> PythonExecutionLimits {
8823    PythonExecutionLimits {
8824        output_buffer_max_bytes: Some(vm.limits.python.output_buffer_max_bytes),
8825        execution_timeout_ms: Some(vm.limits.python.execution_timeout_ms),
8826        max_old_space_mb: Some(vm.limits.python.max_old_space_mb),
8827        vfs_rpc_timeout_ms: Some(vm.limits.python.vfs_rpc_timeout_ms),
8828    }
8829}
8830
8831/// Build the typed per-execution WebAssembly limits from the per-VM kernel
8832/// `ResourceLimits`. Replaces the old `apply_wasm_limit_env` env round-trip;
8833/// notably this is the path that finally enforces the stack cap that the
8834/// `AGENT_OS_WASM_MAX_STACK_BYTES` env knob set but no reader consumed.
8835fn wasm_execution_limits(vm: &VmState) -> WasmExecutionLimits {
8836    let resource_limits = vm.kernel.resource_limits();
8837    WasmExecutionLimits {
8838        max_fuel: resource_limits.max_wasm_fuel,
8839        max_memory_bytes: resource_limits.max_wasm_memory_bytes,
8840        max_stack_bytes: resource_limits
8841            .max_wasm_stack_bytes
8842            .map(|value| value as u64),
8843    }
8844}
8845
8846/// The guest JavaScript host platform configured for this VM, defaulting to
8847/// full Node.js emulation when no `jsRuntime` config was supplied at create.
8848fn js_runtime_platform(vm: &VmState) -> vm_config::JsRuntimePlatform {
8849    vm.configuration
8850        .js_runtime
8851        .as_ref()
8852        .map(|cfg| cfg.platform)
8853        .unwrap_or(vm_config::JsRuntimePlatform::Node)
8854}
8855
8856/// Lowercase wire name for the configured platform, mirroring the serde
8857/// representation of `vm_config::JsRuntimePlatform`.
8858fn js_runtime_platform_env(vm: &VmState) -> &'static str {
8859    match js_runtime_platform(vm) {
8860        vm_config::JsRuntimePlatform::Node => "node",
8861        vm_config::JsRuntimePlatform::Browser => "browser",
8862        vm_config::JsRuntimePlatform::Neutral => "neutral",
8863        vm_config::JsRuntimePlatform::Bare => "bare",
8864    }
8865}
8866
8867/// Wire name for the configured module-resolution mode, or `None` when it is the
8868/// full-Node default (which the live resolver also assumes when the env is unset).
8869fn js_runtime_module_resolution_env(vm: &VmState) -> Option<&'static str> {
8870    let resolution = vm
8871        .configuration
8872        .js_runtime
8873        .as_ref()
8874        .map(|cfg| cfg.module_resolution)
8875        .unwrap_or(vm_config::JsModuleResolution::Node);
8876    match resolution {
8877        vm_config::JsModuleResolution::Node => None,
8878        vm_config::JsModuleResolution::Relative => Some("relative"),
8879        vm_config::JsModuleResolution::None => Some("none"),
8880    }
8881}
8882
8883/// The builtin allow-list the live resolver should enforce, or `None` to leave
8884/// builtins unrestricted (full Node default — preserving today's behavior).
8885/// Non-node platforms enforce an empty list (deny all builtins).
8886fn js_runtime_enforced_builtins(vm: &VmState) -> Option<Vec<String>> {
8887    if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node {
8888        return Some(Vec::new());
8889    }
8890    vm.configuration
8891        .js_runtime
8892        .as_ref()
8893        .and_then(|cfg| cfg.allowed_builtins.clone())
8894}
8895
8896fn configured_allowed_node_builtins(vm: &VmState) -> Vec<String> {
8897    // Non-node platforms expose no Node builtin modules at all.
8898    if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node {
8899        return Vec::new();
8900    }
8901    // Under the node platform an explicit allow-list wins — including an explicit
8902    // empty list, which means deny all. Absence falls back to the engine default.
8903    let configured = match vm
8904        .configuration
8905        .js_runtime
8906        .as_ref()
8907        .and_then(|cfg| cfg.allowed_builtins.as_ref())
8908    {
8909        Some(list) => list.clone(),
8910        None => DEFAULT_ALLOWED_NODE_BUILTINS
8911            .iter()
8912            .map(|value| (*value).to_owned())
8913            .collect::<Vec<_>>(),
8914    };
8915    dedupe_strings(&configured)
8916}
8917
8918fn configured_loopback_exempt_ports(vm: &VmState) -> Vec<String> {
8919    if !vm.configuration.loopback_exempt_ports.is_empty() {
8920        return vm
8921            .configuration
8922            .loopback_exempt_ports
8923            .iter()
8924            .map(ToString::to_string)
8925            .collect();
8926    }
8927
8928    vm.create_loopback_exempt_ports
8929        .iter()
8930        .map(ToString::to_string)
8931        .collect()
8932}
8933
8934/// Extract the `hostPath` string from a mount plugin's JSON-encoded config.
8935fn mount_config_host_path(config: &str) -> Option<String> {
8936    serde_json::from_str::<Value>(config)
8937        .ok()?
8938        .get("hostPath")
8939        .and_then(Value::as_str)
8940        .map(str::to_owned)
8941}
8942
8943fn runtime_guest_writable_host_paths(vm: &VmState) -> Vec<PathBuf> {
8944    vm.configuration
8945        .mounts
8946        .iter()
8947        .filter(|mount| !mount.read_only)
8948        .filter_map(|mount| {
8949            ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
8950                .then(|| mount_config_host_path(&mount.plugin.config))
8951                .flatten()
8952                .map(PathBuf::from)
8953        })
8954        .collect()
8955}
8956
8957fn runtime_guest_path_mappings(vm: &VmState) -> Vec<RuntimeGuestPathMapping> {
8958    let mut mappings = vm
8959        .configuration
8960        .mounts
8961        .iter()
8962        .filter_map(|mount| {
8963            ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
8964                .then(|| {
8965                    mount_config_host_path(&mount.plugin.config).map(|host_path| {
8966                        RuntimeGuestPathMapping {
8967                            guest_path: normalize_path(&mount.guest_path),
8968                            host_path,
8969                            read_only: mount.read_only,
8970                        }
8971                    })
8972                })
8973                .flatten()
8974        })
8975        .collect::<Vec<_>>();
8976    let mut command_root_mappings = vm
8977        .command_guest_paths
8978        .values()
8979        .filter_map(|guest_path| {
8980            Path::new(guest_path)
8981                .parent()
8982                .and_then(|parent| parent.to_str())
8983                .map(normalize_path)
8984        })
8985        .collect::<BTreeSet<_>>()
8986        .into_iter()
8987        .map(|guest_path| RuntimeGuestPathMapping {
8988            host_path: resolve_vm_guest_path_to_host(vm, &guest_path)
8989                .to_string_lossy()
8990                .into_owned(),
8991            guest_path,
8992            read_only: false,
8993        })
8994        .collect::<Vec<_>>();
8995    mappings.append(&mut command_root_mappings);
8996    let mut extra_node_modules_roots = mappings
8997        .iter()
8998        .filter(|mapping| mapping.guest_path.starts_with("/root/node_modules/"))
8999        .filter_map(|mapping| {
9000            host_node_modules_root(Path::new(&mapping.host_path)).map(|host_root| {
9001                RuntimeGuestPathMapping {
9002                    guest_path: String::from("/root/node_modules"),
9003                    host_path: host_root.to_string_lossy().into_owned(),
9004                    read_only: mapping.read_only,
9005                }
9006            })
9007        })
9008        .collect::<Vec<_>>();
9009    mappings.append(&mut extra_node_modules_roots);
9010    mappings.push(RuntimeGuestPathMapping {
9011        guest_path: String::from("/"),
9012        host_path: vm.cwd.to_string_lossy().into_owned(),
9013        read_only: false,
9014    });
9015    mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.guest_path.len()));
9016    mappings.dedup_by(|left, right| {
9017        left.guest_path == right.guest_path && left.host_path == right.host_path
9018    });
9019    mappings
9020}
9021
9022/// Build a `Send`-able, read-only VFS module reader over the VM's read-only
9023/// `host_dir`/`module_access` mounts (and the derived `/root/node_modules` root
9024/// for nested mounts). When present, the V8 bridge thread resolves modules
9025/// inline against this reader — concurrently with the service loop — so a large
9026/// cold-start module graph never serializes behind / starves an in-flight ACP
9027/// `session/new` bootstrap on the single service-loop thread. The reader reads
9028/// the same mounted tree the guest sees (anchored `openat2`, escaping-symlink
9029/// refusal), never the host-direct path translator. Returns `None` when the VM
9030/// has no usable read-only mount, so resolution falls back to the service-loop
9031/// kernel reader.
9032fn build_module_reader(
9033    vm: &VmState,
9034    resolved: &ResolvedChildProcessExecution,
9035) -> Option<crate::plugins::host_dir::HostDirModuleReader> {
9036    let mut pairs: Vec<(String, PathBuf)> = vm
9037        .configuration
9038        .mounts
9039        .iter()
9040        .filter(|mount| mount.read_only)
9041        .filter(|mount| (mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
9042        .filter_map(|mount| {
9043            mount_config_host_path(&mount.plugin.config)
9044                .map(|host_path| (normalize_path(&mount.guest_path), PathBuf::from(host_path)))
9045        })
9046        .collect();
9047
9048    let guest_entrypoint = resolved
9049        .env
9050        .get("AGENT_OS_GUEST_ENTRYPOINT")
9051        .map(|path| normalize_path(path));
9052    if let Some(guest_entrypoint) = guest_entrypoint.as_deref() {
9053        let entrypoint_in_read_only_mount = pairs.iter().any(|(guest_path, _)| {
9054            guest_entrypoint == guest_path
9055                || guest_entrypoint.starts_with(&format!("{guest_path}/"))
9056        });
9057        if !entrypoint_in_read_only_mount {
9058            return None;
9059        }
9060    }
9061
9062    // Mirror runtime_guest_path_mappings: a mount nested under
9063    // `/root/node_modules/<pkg>` implies a `/root/node_modules` root the resolver
9064    // walks, so expose that root too (e.g. software-package mounts).
9065    let extra_roots: Vec<(String, PathBuf)> = pairs
9066        .iter()
9067        .filter(|(guest_path, _)| guest_path.starts_with("/root/node_modules/"))
9068        .filter_map(|(_, host_path)| {
9069            host_node_modules_root(host_path).map(|root| (String::from("/root/node_modules"), root))
9070        })
9071        .collect();
9072    pairs.extend(extra_roots);
9073
9074    crate::plugins::host_dir::HostDirModuleReader::from_mounts(pairs)
9075}
9076
9077fn host_node_modules_root(path: &Path) -> Option<PathBuf> {
9078    if let Some(root) = path
9079        .ancestors()
9080        .filter(|candidate| {
9081            candidate.file_name().and_then(|name| name.to_str()) == Some("node_modules")
9082        })
9083        .last()
9084        .map(Path::to_path_buf)
9085    {
9086        return Some(root);
9087    }
9088
9089    fs::canonicalize(path)
9090        .ok()?
9091        .ancestors()
9092        .filter(|candidate| {
9093            candidate.file_name().and_then(|name| name.to_str()) == Some("node_modules")
9094        })
9095        .last()
9096        .map(Path::to_path_buf)
9097}
9098
9099#[cfg(test)]
9100mod runtime_guest_path_mapping_tests {
9101    use super::{host_node_modules_root, javascript_sync_rpc_option_bool};
9102    use serde_json::json;
9103    use std::fs;
9104    use std::time::{SystemTime, UNIX_EPOCH};
9105
9106    #[test]
9107    fn host_node_modules_root_prefers_workspace_root_over_pnpm_package_node_modules() {
9108        let unique = SystemTime::now()
9109            .duration_since(UNIX_EPOCH)
9110            .expect("clock should be monotonic")
9111            .as_nanos();
9112        let temp = std::env::temp_dir().join(format!("secure-exec-sidecar-node-modules-{unique}"));
9113        let workspace_node_modules = temp.join("node_modules");
9114        let package_root = workspace_node_modules
9115            .join(".pnpm")
9116            .join("example@1.0.0")
9117            .join("node_modules")
9118            .join("@scope")
9119            .join("pkg");
9120        fs::create_dir_all(&package_root).expect("package root should be created");
9121
9122        let resolved =
9123            host_node_modules_root(&package_root).expect("node_modules root should resolve");
9124
9125        assert_eq!(resolved, workspace_node_modules);
9126
9127        fs::remove_dir_all(&temp).expect("temp tree should be removed");
9128    }
9129
9130    #[test]
9131    fn host_node_modules_root_preserves_symlinked_workspace_node_modules_path() {
9132        let unique = SystemTime::now()
9133            .duration_since(UNIX_EPOCH)
9134            .expect("clock should be monotonic")
9135            .as_nanos();
9136        let temp =
9137            std::env::temp_dir().join(format!("secure-exec-sidecar-node-modules-symlink-{unique}"));
9138        let workspace_node_modules = temp.join("node_modules");
9139        let package_link = workspace_node_modules.join("@scope").join("pkg");
9140        let real_package = temp.join("registry").join("agent").join("pkg");
9141        fs::create_dir_all(package_link.parent().expect("package parent should exist"))
9142            .expect("scoped parent should be created");
9143        fs::create_dir_all(&real_package).expect("real package root should be created");
9144        std::os::unix::fs::symlink(&real_package, &package_link)
9145            .expect("package symlink should be created");
9146
9147        let resolved =
9148            host_node_modules_root(&package_link).expect("node_modules root should resolve");
9149
9150        assert_eq!(resolved, workspace_node_modules);
9151
9152        fs::remove_dir_all(&temp).expect("temp tree should be removed");
9153    }
9154
9155    #[test]
9156    fn javascript_sync_rpc_option_bool_accepts_boolean_recursive_argument() {
9157        assert_eq!(
9158            javascript_sync_rpc_option_bool(&[json!("/workspace"), json!(true)], 1, "recursive"),
9159            Some(true)
9160        );
9161        assert_eq!(
9162            javascript_sync_rpc_option_bool(
9163                &[json!("/workspace"), json!({ "recursive": false })],
9164                1,
9165                "recursive"
9166            ),
9167            Some(false)
9168        );
9169    }
9170}
9171
9172#[cfg(test)]
9173mod kernel_poll_sync_rpc_tests {
9174    use super::{
9175        service_javascript_kernel_poll_sync_rpc, ActiveExecution, ActiveProcess,
9176        JavascriptSyncRpcRequest, KernelPollFdResponse, SidecarKernel, ToolExecution,
9177        EXECUTION_DRIVER_NAME, JAVASCRIPT_COMMAND,
9178    };
9179    use secure_exec_kernel::command_registry::CommandDriver;
9180    use secure_exec_kernel::kernel::{KernelVmConfig, SpawnOptions};
9181    use secure_exec_kernel::mount_table::MountTable;
9182    use secure_exec_kernel::permissions::Permissions;
9183    use secure_exec_kernel::poll::{POLLHUP, POLLIN};
9184    use secure_exec_kernel::vfs::MemoryFileSystem;
9185    use serde_json::{json, Value};
9186    #[test]
9187    fn javascript_kernel_poll_sync_rpc_reports_multiple_kernel_fds() {
9188        let mut config = KernelVmConfig::new("vm-js-kernel-poll");
9189        config.permissions = Permissions::allow_all();
9190        let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config);
9191        kernel
9192            .register_driver(CommandDriver::new(
9193                EXECUTION_DRIVER_NAME,
9194                [JAVASCRIPT_COMMAND],
9195            ))
9196            .expect("register execution driver");
9197
9198        let kernel_handle = kernel
9199            .spawn_process(
9200                JAVASCRIPT_COMMAND,
9201                Vec::new(),
9202                SpawnOptions {
9203                    requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
9204                    ..SpawnOptions::default()
9205                },
9206            )
9207            .expect("spawn javascript kernel process");
9208        let pid = kernel_handle.pid();
9209
9210        let (stdin_read_fd, stdin_write_fd) = kernel
9211            .open_pipe(EXECUTION_DRIVER_NAME, pid)
9212            .expect("open kernel stdin pipe");
9213        kernel
9214            .fd_dup2(EXECUTION_DRIVER_NAME, pid, stdin_read_fd, 0)
9215            .expect("dup stdin pipe onto fd 0");
9216        kernel
9217            .fd_close(EXECUTION_DRIVER_NAME, pid, stdin_read_fd)
9218            .expect("close original stdin read fd");
9219
9220        let process = ActiveProcess::new(
9221            pid,
9222            kernel_handle,
9223            super::GuestRuntimeKind::JavaScript,
9224            ActiveExecution::Tool(ToolExecution::default()),
9225        );
9226
9227        kernel
9228            .fd_write(EXECUTION_DRIVER_NAME, pid, stdin_write_fd, b"poll-ready")
9229            .expect("write kernel stdin payload");
9230        kernel
9231            .fd_close(EXECUTION_DRIVER_NAME, pid, stdin_write_fd)
9232            .expect("close kernel stdin writer");
9233
9234        let response = service_javascript_kernel_poll_sync_rpc(
9235            &mut kernel,
9236            &process,
9237            &JavascriptSyncRpcRequest {
9238                id: 1,
9239                method: String::from("__kernel_poll"),
9240                args: vec![
9241                    json!([
9242                        { "fd": 0, "events": POLLIN.bits() },
9243                        { "fd": 1, "events": POLLIN.bits() }
9244                    ]),
9245                    json!(250),
9246                ],
9247            },
9248        )
9249        .expect("poll kernel fds");
9250
9251        assert_eq!(response["readyCount"], Value::from(1));
9252        let fds: Vec<KernelPollFdResponse> =
9253            serde_json::from_value(response["fds"].clone()).expect("kernel poll fd response");
9254        assert_eq!(
9255            fds,
9256            vec![
9257                KernelPollFdResponse {
9258                    fd: 0,
9259                    events: POLLIN.bits(),
9260                    revents: (POLLIN | POLLHUP).bits(),
9261                },
9262                KernelPollFdResponse {
9263                    fd: 1,
9264                    events: POLLIN.bits(),
9265                    revents: 0,
9266                },
9267            ]
9268        );
9269
9270        process.kernel_handle.finish(0);
9271        kernel.waitpid(pid).expect("wait javascript kernel process");
9272    }
9273}
9274
9275fn dedupe_strings(values: &[String]) -> Vec<String> {
9276    let mut seen = BTreeSet::new();
9277    let mut deduped = Vec::new();
9278    for value in values {
9279        if seen.insert(value.clone()) {
9280            deduped.push(value.clone());
9281        }
9282    }
9283    deduped
9284}
9285
9286fn dedupe_host_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
9287    let mut seen = BTreeSet::new();
9288    let mut deduped = Vec::new();
9289    for path in paths {
9290        let normalized = normalize_host_path(path);
9291        let key = normalized.to_string_lossy().into_owned();
9292        if seen.insert(key) {
9293            deduped.push(normalized);
9294        }
9295    }
9296    deduped
9297}
9298
9299fn expand_host_access_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
9300    let mut expanded = Vec::new();
9301    let mut seen = BTreeSet::new();
9302
9303    let mut add_path = |candidate: PathBuf| {
9304        let normalized = normalize_host_path(&candidate);
9305        let key = normalized.to_string_lossy().into_owned();
9306        if seen.insert(key) {
9307            expanded.push(normalized);
9308        }
9309    };
9310
9311    for host_path in paths {
9312        add_path(host_path.clone());
9313        if let Ok(realpath) = fs::canonicalize(host_path) {
9314            add_path(realpath);
9315        }
9316
9317        if host_path.file_name().and_then(|name| name.to_str()) != Some("node_modules") {
9318            continue;
9319        }
9320
9321        let mut current = host_path.parent();
9322        while let Some(parent) = current {
9323            let candidate = parent.join("node_modules");
9324            if candidate.exists() {
9325                add_path(candidate.clone());
9326                if let Ok(realpath) = fs::canonicalize(&candidate) {
9327                    add_path(realpath);
9328                }
9329            }
9330            current = parent.parent();
9331        }
9332    }
9333
9334    expanded
9335}
9336
9337fn prepare_javascript_shadow(
9338    vm: &mut VmState,
9339    resolved: &ResolvedChildProcessExecution,
9340) -> Result<(), SidecarError> {
9341    let guest_entrypoint = resolved
9342        .env
9343        .get("AGENT_OS_GUEST_ENTRYPOINT")
9344        .cloned()
9345        // An absolute `entrypoint` may be a host path that lives inside the VM's
9346        // host cwd (callers can pass a fully-qualified host path). The guest sees
9347        // it at its translated guest path (host_cwd -> guest_cwd), so the shadow
9348        // must be keyed by that guest path rather than the raw host path. Falling
9349        // back to the host path here would materialize the file at the wrong guest
9350        // location and the runtime's `require()` would fail with "Cannot find
9351        // module".
9352        .or_else(|| {
9353            resolve_host_entrypoint_within_vm_host_cwd(vm, &resolved.entrypoint)
9354                .map(|(guest_entrypoint, _)| guest_entrypoint)
9355        })
9356        .or_else(|| {
9357            resolved
9358                .entrypoint
9359                .starts_with('/')
9360                .then(|| normalize_path(&resolved.entrypoint))
9361        });
9362    let Some(guest_entrypoint) = guest_entrypoint else {
9363        return Ok(());
9364    };
9365    if host_mount_path_for_guest_path(vm, &guest_entrypoint).is_some() {
9366        return Ok(());
9367    }
9368    if vm.kernel.lstat(&guest_entrypoint).is_err() {
9369        let host_entrypoint = {
9370            let candidate = Path::new(&resolved.entrypoint);
9371            if candidate.is_absolute() {
9372                candidate.to_path_buf()
9373            } else {
9374                resolved.host_cwd.join(candidate)
9375            }
9376        };
9377        if host_entrypoint.exists() {
9378            materialize_host_path_to_shadow(vm, &guest_entrypoint, &host_entrypoint)?;
9379            // The shadow write only stages the file on the host side; the runtime
9380            // resolves modules against the kernel VFS, so the staged entrypoint
9381            // must be synced into the kernel before execution starts (otherwise
9382            // `require()` reports "Cannot find module").
9383            return sync_shadow_entrypoint_into_kernel(vm, &guest_entrypoint);
9384        }
9385    }
9386    materialize_guest_path_to_shadow(vm, &guest_entrypoint)
9387}
9388
9389/// Sync a freshly-staged shadow entrypoint into the kernel VFS so the runtime's
9390/// kernel-backed module resolver can read it. Mirrors the host->kernel file sync
9391/// used by the broader shadow reconciliation, but scoped to the single
9392/// entrypoint we just materialized.
9393fn sync_shadow_entrypoint_into_kernel(
9394    vm: &mut VmState,
9395    guest_entrypoint: &str,
9396) -> Result<(), SidecarError> {
9397    if vm.kernel.exists(guest_entrypoint).unwrap_or(false) {
9398        return Ok(());
9399    }
9400    let shadow_path = shadow_path_for_guest(vm, guest_entrypoint);
9401    let bytes = match fs::read(&shadow_path) {
9402        Ok(bytes) => bytes,
9403        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
9404        Err(error) => {
9405            return Err(SidecarError::Io(format!(
9406                "failed to read staged shadow entrypoint {}: {error}",
9407                shadow_path.display()
9408            )));
9409        }
9410    };
9411    if let Some(parent) = guest_parent_path(guest_entrypoint) {
9412        if !vm.kernel.exists(&parent).unwrap_or(false) {
9413            vm.kernel.mkdir(&parent, true).map_err(kernel_error)?;
9414        }
9415    }
9416    vm.kernel
9417        .write_file(guest_entrypoint, bytes)
9418        .map_err(kernel_error)?;
9419    Ok(())
9420}
9421
9422fn guest_parent_path(guest_path: &str) -> Option<String> {
9423    let parent = Path::new(guest_path).parent()?;
9424    let parent = parent.to_string_lossy();
9425    if parent.is_empty() || parent == "/" {
9426        None
9427    } else {
9428        Some(parent.into_owned())
9429    }
9430}
9431
9432fn materialize_host_path_to_shadow(
9433    vm: &VmState,
9434    guest_path: &str,
9435    host_path: &Path,
9436) -> Result<(), SidecarError> {
9437    let shadow_path = shadow_path_for_guest(vm, guest_path);
9438    let metadata = fs::symlink_metadata(host_path)
9439        .map_err(|error| SidecarError::Io(format!("failed to stat host entrypoint: {error}")))?;
9440
9441    if metadata.file_type().is_symlink() {
9442        if let Some(parent) = shadow_path.parent() {
9443            fs::create_dir_all(parent).map_err(|error| {
9444                SidecarError::Io(format!("failed to create shadow symlink parent: {error}"))
9445            })?;
9446        }
9447        let _ = fs::remove_file(&shadow_path);
9448        let _ = fs::remove_dir_all(&shadow_path);
9449        let target = fs::read_link(host_path)
9450            .map_err(|error| SidecarError::Io(format!("failed to read host symlink: {error}")))?;
9451        std::os::unix::fs::symlink(&target, &shadow_path)
9452            .map_err(|error| SidecarError::Io(format!("failed to mirror host symlink: {error}")))?;
9453        return Ok(());
9454    }
9455
9456    if metadata.is_dir() {
9457        fs::create_dir_all(&shadow_path).map_err(|error| {
9458            SidecarError::Io(format!("failed to create shadow directory: {error}"))
9459        })?;
9460        fs::set_permissions(
9461            &shadow_path,
9462            fs::Permissions::from_mode(metadata.permissions().mode() & 0o7777),
9463        )
9464        .map_err(|error| {
9465            SidecarError::Io(format!(
9466                "failed to set shadow directory mode on {}: {error}",
9467                shadow_path.display()
9468            ))
9469        })?;
9470        return Ok(());
9471    }
9472
9473    if let Some(parent) = shadow_path.parent() {
9474        fs::create_dir_all(parent).map_err(|error| {
9475            SidecarError::Io(format!("failed to create shadow parent: {error}"))
9476        })?;
9477    }
9478    let bytes = fs::read(host_path)
9479        .map_err(|error| SidecarError::Io(format!("failed to read host entrypoint: {error}")))?;
9480    fs::write(&shadow_path, bytes).map_err(|error| {
9481        SidecarError::Io(format!(
9482            "failed to mirror host file into shadow root: {error}"
9483        ))
9484    })?;
9485    fs::set_permissions(
9486        &shadow_path,
9487        fs::Permissions::from_mode(metadata.permissions().mode() & 0o7777),
9488    )
9489    .map_err(|error| {
9490        SidecarError::Io(format!(
9491            "failed to set shadow file mode on {}: {error}",
9492            shadow_path.display()
9493        ))
9494    })?;
9495    Ok(())
9496}
9497
9498fn materialize_guest_path_to_shadow(
9499    vm: &mut VmState,
9500    guest_path: &str,
9501) -> Result<(), SidecarError> {
9502    let stat = vm.kernel.lstat(guest_path).map_err(kernel_error)?;
9503    let shadow_path = shadow_path_for_guest(vm, guest_path);
9504
9505    if stat.is_symbolic_link {
9506        if let Some(parent) = shadow_path.parent() {
9507            fs::create_dir_all(parent).map_err(|error| {
9508                SidecarError::Io(format!("failed to create shadow symlink parent: {error}"))
9509            })?;
9510        }
9511        let _ = fs::remove_file(&shadow_path);
9512        let _ = fs::remove_dir_all(&shadow_path);
9513        let target = vm.kernel.read_link(guest_path).map_err(kernel_error)?;
9514        std::os::unix::fs::symlink(&target, &shadow_path)
9515            .map_err(|error| SidecarError::Io(format!("failed to mirror symlink: {error}")))?;
9516        return Ok(());
9517    }
9518
9519    if stat.is_directory {
9520        fs::create_dir_all(&shadow_path).map_err(|error| {
9521            SidecarError::Io(format!("failed to create shadow directory: {error}"))
9522        })?;
9523        fs::set_permissions(&shadow_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err(
9524            |error| {
9525                SidecarError::Io(format!(
9526                    "failed to set shadow directory mode on {}: {error}",
9527                    shadow_path.display()
9528                ))
9529            },
9530        )?;
9531        return Ok(());
9532    }
9533
9534    if let Some(parent) = shadow_path.parent() {
9535        fs::create_dir_all(parent).map_err(|error| {
9536            SidecarError::Io(format!("failed to create shadow parent: {error}"))
9537        })?;
9538    }
9539    let bytes = vm.kernel.read_file(guest_path).map_err(kernel_error)?;
9540    fs::write(&shadow_path, bytes).map_err(|error| {
9541        SidecarError::Io(format!(
9542            "failed to mirror guest file into shadow root: {error}"
9543        ))
9544    })?;
9545    fs::set_permissions(&shadow_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err(
9546        |error| {
9547            SidecarError::Io(format!(
9548                "failed to set shadow file mode on {}: {error}",
9549                shadow_path.display()
9550            ))
9551        },
9552    )?;
9553    Ok(())
9554}
9555
9556fn load_javascript_entrypoint_source(
9557    vm: &mut VmState,
9558    host_cwd: &Path,
9559    entrypoint: &str,
9560    env: &BTreeMap<String, String>,
9561) -> Option<String> {
9562    let mut read_guest_file = |path: &str| {
9563        vm.kernel
9564            .read_file(path)
9565            .ok()
9566            .and_then(|bytes| String::from_utf8(bytes).ok())
9567    };
9568
9569    if let Some(source) = env
9570        .get("AGENT_OS_GUEST_ENTRYPOINT")
9571        .filter(|path| path.starts_with('/'))
9572        .and_then(|path| read_guest_file(path))
9573    {
9574        return Some(source);
9575    }
9576
9577    if entrypoint.starts_with('/') {
9578        if let Some(source) = read_guest_file(entrypoint) {
9579            return Some(source);
9580        }
9581    }
9582
9583    let host_entrypoint = if Path::new(entrypoint).is_absolute() {
9584        PathBuf::from(entrypoint)
9585    } else {
9586        host_cwd.join(entrypoint)
9587    };
9588    let normalized_entrypoint = normalize_host_path(&host_entrypoint);
9589    let sandbox_root = normalize_host_path(&vm.cwd);
9590    let host_cwd = normalize_host_path(&vm.host_cwd);
9591    if !path_is_within_root(&normalized_entrypoint, &sandbox_root)
9592        && !path_is_within_root(&normalized_entrypoint, &host_cwd)
9593    {
9594        return None;
9595    }
9596
9597    fs::read_to_string(&normalized_entrypoint).ok()
9598}
9599
9600fn emit_dns_resolution_event<B>(
9601    bridge: &SharedBridge<B>,
9602    vm_id: &str,
9603    hostname: &str,
9604    source: KernelDnsResolutionSource,
9605    addresses: &[IpAddr],
9606    dns: &VmDnsConfig,
9607) where
9608    B: NativeSidecarBridge + Send + 'static,
9609    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9610{
9611    let _ = emit_structured_event(
9612        bridge,
9613        vm_id,
9614        "network.dns.resolved",
9615        audit_fields([
9616            ("hostname", hostname.to_owned()),
9617            ("source", source.as_str().to_owned()),
9618            (
9619                "addresses",
9620                addresses
9621                    .iter()
9622                    .map(ToString::to_string)
9623                    .collect::<Vec<_>>()
9624                    .join(","),
9625            ),
9626            ("address_count", addresses.len().to_string()),
9627            ("resolver_count", dns.name_servers.len().to_string()),
9628            (
9629                "resolvers",
9630                dns.name_servers
9631                    .iter()
9632                    .map(ToString::to_string)
9633                    .collect::<Vec<_>>()
9634                    .join(","),
9635            ),
9636        ]),
9637    );
9638}
9639
9640fn emit_dns_record_resolution_event<B>(
9641    bridge: &SharedBridge<B>,
9642    vm_id: &str,
9643    hostname: &str,
9644    resolution: &DnsRecordResolution,
9645    dns: &VmDnsConfig,
9646) where
9647    B: NativeSidecarBridge + Send + 'static,
9648    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9649{
9650    if let Some(addresses) = dns_resolution_ip_addrs(resolution.records()) {
9651        emit_dns_resolution_event(
9652            bridge,
9653            vm_id,
9654            hostname,
9655            resolution.source(),
9656            &addresses,
9657            dns,
9658        );
9659        return;
9660    }
9661
9662    let _ = emit_structured_event(
9663        bridge,
9664        vm_id,
9665        "network.dns.resolved",
9666        audit_fields([
9667            ("hostname", hostname.to_owned()),
9668            ("source", resolution.source().as_str().to_owned()),
9669            (
9670                "addresses",
9671                resolution
9672                    .records()
9673                    .iter()
9674                    .map(summarize_dns_record)
9675                    .collect::<Vec<_>>()
9676                    .join(","),
9677            ),
9678            ("address_count", resolution.records().len().to_string()),
9679            ("resolver_count", dns.name_servers.len().to_string()),
9680            (
9681                "resolvers",
9682                dns.name_servers
9683                    .iter()
9684                    .map(ToString::to_string)
9685                    .collect::<Vec<_>>()
9686                    .join(","),
9687            ),
9688        ]),
9689    );
9690}
9691
9692fn emit_dns_resolution_failure_event<B>(
9693    bridge: &SharedBridge<B>,
9694    vm_id: &str,
9695    hostname: &str,
9696    dns: &VmDnsConfig,
9697    error: &SidecarError,
9698) where
9699    B: NativeSidecarBridge + Send + 'static,
9700    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9701{
9702    let _ = emit_structured_event(
9703        bridge,
9704        vm_id,
9705        "network.dns.resolve_failed",
9706        audit_fields([
9707            ("hostname", hostname.to_owned()),
9708            ("reason", error.to_string()),
9709            ("resolver_count", dns.name_servers.len().to_string()),
9710            (
9711                "resolvers",
9712                dns.name_servers
9713                    .iter()
9714                    .map(ToString::to_string)
9715                    .collect::<Vec<_>>()
9716                    .join(","),
9717            ),
9718        ]),
9719    );
9720}
9721
9722fn parse_dns_record_type(rrtype: &str) -> Result<RecordType, SidecarError> {
9723    match rrtype {
9724        "A" => Ok(RecordType::A),
9725        "AAAA" => Ok(RecordType::AAAA),
9726        "MX" => Ok(RecordType::MX),
9727        "TXT" => Ok(RecordType::TXT),
9728        "SRV" => Ok(RecordType::SRV),
9729        "CNAME" => Ok(RecordType::CNAME),
9730        "PTR" => Ok(RecordType::PTR),
9731        "NS" => Ok(RecordType::NS),
9732        "SOA" => Ok(RecordType::SOA),
9733        "NAPTR" => Ok(RecordType::NAPTR),
9734        "CAA" => Ok(RecordType::CAA),
9735        "ANY" => Ok(RecordType::ANY),
9736        other => Err(SidecarError::Execution(format!(
9737            "ERR_NOT_IMPLEMENTED: dns rrtype {other} is not supported by the secure-exec dns bridge"
9738        ))),
9739    }
9740}
9741
9742fn dns_resolution_to_node_value(
9743    resolution: &DnsRecordResolution,
9744    requested_type: &str,
9745) -> Result<Value, SidecarError> {
9746    let safe_ips = dns_resolution_safe_ip_set(resolution.records(), resolution.hostname())?;
9747    match requested_type {
9748        "A" | "AAAA" => Ok(Value::Array(
9749            resolution
9750                .records()
9751                .iter()
9752                .filter_map(|record| dns_record_ip_string(record, &safe_ips))
9753                .map(Value::String)
9754                .collect(),
9755        )),
9756        "MX" => Ok(Value::Array(
9757            resolution
9758                .records()
9759                .iter()
9760                .filter_map(|record| match record.data() {
9761                    RData::MX(mx) => Some(json!({
9762                        "priority": mx.preference,
9763                        "exchange": normalize_dns_name_for_node(&mx.exchange),
9764                        "type": "MX",
9765                    })),
9766                    _ => None,
9767                })
9768                .collect(),
9769        )),
9770        "TXT" => Ok(Value::Array(
9771            resolution
9772                .records()
9773                .iter()
9774                .filter_map(|record| match record.data() {
9775                    RData::TXT(txt) => Some(Value::Array(
9776                        txt.txt_data
9777                            .iter()
9778                            .map(|entry| Value::String(String::from_utf8_lossy(entry).into_owned()))
9779                            .collect(),
9780                    )),
9781                    _ => None,
9782                })
9783                .collect(),
9784        )),
9785        "SRV" => Ok(Value::Array(
9786            resolution
9787                .records()
9788                .iter()
9789                .filter_map(|record| match record.data() {
9790                    RData::SRV(srv) => Some(json!({
9791                        "priority": srv.priority,
9792                        "weight": srv.weight,
9793                        "port": srv.port,
9794                        "name": normalize_dns_name_for_node(&srv.target),
9795                        "type": "SRV",
9796                    })),
9797                    _ => None,
9798                })
9799                .collect(),
9800        )),
9801        "CNAME" => Ok(Value::Array(
9802            resolution
9803                .records()
9804                .iter()
9805                .filter_map(|record| match record.data() {
9806                    RData::CNAME(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9807                    _ => None,
9808                })
9809                .collect(),
9810        )),
9811        "PTR" => Ok(Value::Array(
9812            resolution
9813                .records()
9814                .iter()
9815                .filter_map(|record| match record.data() {
9816                    RData::PTR(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9817                    _ => None,
9818                })
9819                .collect(),
9820        )),
9821        "NS" => Ok(Value::Array(
9822            resolution
9823                .records()
9824                .iter()
9825                .filter_map(|record| match record.data() {
9826                    RData::NS(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9827                    _ => None,
9828                })
9829                .collect(),
9830        )),
9831        "SOA" => resolution
9832            .records()
9833            .iter()
9834            .find_map(|record| match record.data() {
9835                RData::SOA(soa) => Some(json!({
9836                    "nsname": normalize_dns_name_for_node(&soa.mname),
9837                    "hostmaster": normalize_dns_name_for_node(&soa.rname),
9838                    "serial": soa.serial,
9839                    "refresh": soa.refresh,
9840                    "retry": soa.retry,
9841                    "expire": soa.expire,
9842                    "minttl": soa.minimum,
9843                })),
9844                _ => None,
9845            })
9846            .ok_or_else(|| {
9847                SidecarError::Execution(String::from("failed to resolve DNS SOA record"))
9848            }),
9849        "NAPTR" => Ok(Value::Array(
9850            resolution
9851                .records()
9852                .iter()
9853                .filter_map(|record| match record.data() {
9854                    RData::NAPTR(naptr) => Some(json!({
9855                        "flags": String::from_utf8_lossy(&naptr.flags).into_owned(),
9856                        "service": String::from_utf8_lossy(&naptr.services).into_owned(),
9857                        "regexp": String::from_utf8_lossy(&naptr.regexp).into_owned(),
9858                        "replacement": normalize_dns_name_for_node(&naptr.replacement),
9859                        "order": naptr.order,
9860                        "preference": naptr.preference,
9861                    })),
9862                    _ => None,
9863                })
9864                .collect(),
9865        )),
9866        "CAA" => Ok(Value::Array(
9867            resolution
9868                .records()
9869                .iter()
9870                .filter_map(|record| match record.data() {
9871                    RData::CAA(caa) => {
9872                        let mut value = serde_json::Map::new();
9873                        value.insert(
9874                            "critical".to_owned(),
9875                            Value::from(u8::from(caa.issuer_critical)),
9876                        );
9877                        value.insert("type".to_owned(), Value::String(String::from("CAA")));
9878                        if caa.tag.eq_ignore_ascii_case("iodef") {
9879                            value.insert(
9880                                "iodef".to_owned(),
9881                                Value::String(
9882                                    caa.value_as_iodef()
9883                                        .map(|url| url.to_string())
9884                                        .unwrap_or_else(|_| {
9885                                            String::from_utf8_lossy(&caa.value).into_owned()
9886                                        }),
9887                                ),
9888                            );
9889                        } else if let Ok((issuer, _params)) = caa.value_as_issue() {
9890                            let field = if caa.tag.eq_ignore_ascii_case("issuewild") {
9891                                "issuewild"
9892                            } else {
9893                                "issue"
9894                            };
9895                            value.insert(
9896                                field.to_owned(),
9897                                Value::String(
9898                                    issuer.as_ref().map(ToString::to_string).unwrap_or_else(|| {
9899                                        String::from_utf8_lossy(&caa.value).into_owned()
9900                                    }),
9901                                ),
9902                            );
9903                        } else {
9904                            value.insert(
9905                                caa.tag.to_ascii_lowercase(),
9906                                Value::String(String::from_utf8_lossy(&caa.value).into_owned()),
9907                            );
9908                        }
9909                        Some(Value::Object(value))
9910                    }
9911                    _ => None,
9912                })
9913                .collect(),
9914        )),
9915        "ANY" => Ok(Value::Array(
9916            resolution
9917                .records()
9918                .iter()
9919                .filter_map(|record| dns_any_record_to_value(record, &safe_ips))
9920                .collect(),
9921        )),
9922        other => Err(SidecarError::Execution(format!(
9923            "ERR_NOT_IMPLEMENTED: dns rrtype {other} is not supported by the secure-exec dns bridge"
9924        ))),
9925    }
9926}
9927
9928fn dns_resolution_safe_ip_set(
9929    records: &[Record],
9930    hostname: &str,
9931) -> Result<BTreeSet<IpAddr>, SidecarError> {
9932    let ips = records
9933        .iter()
9934        .filter_map(dns_record_ip_addr)
9935        .collect::<Vec<_>>();
9936    if ips.is_empty() {
9937        return Ok(BTreeSet::new());
9938    }
9939    Ok(filter_dns_safe_ip_addrs(ips, hostname)?
9940        .into_iter()
9941        .collect())
9942}
9943
9944fn dns_resolution_ip_addrs(records: &[Record]) -> Option<Vec<IpAddr>> {
9945    let ips = records
9946        .iter()
9947        .filter_map(dns_record_ip_addr)
9948        .collect::<Vec<_>>();
9949    if ips.is_empty() {
9950        return None;
9951    }
9952    Some(ips)
9953}
9954
9955fn dns_record_ip_addr(record: &Record) -> Option<IpAddr> {
9956    match record.data() {
9957        RData::A(address) => Some(IpAddr::V4(**address)),
9958        RData::AAAA(address) => Some(IpAddr::V6(**address)),
9959        _ => None,
9960    }
9961}
9962
9963fn dns_record_ip_string(record: &Record, safe_ips: &BTreeSet<IpAddr>) -> Option<String> {
9964    let ip = dns_record_ip_addr(record)?;
9965    safe_ips.contains(&ip).then(|| ip.to_string())
9966}
9967
9968fn dns_any_record_to_value(record: &Record, safe_ips: &BTreeSet<IpAddr>) -> Option<Value> {
9969    let value = match record.data() {
9970        RData::A(_) | RData::AAAA(_) => json!({
9971            "address": dns_record_ip_string(record, safe_ips)?,
9972            "ttl": record.ttl(),
9973            "type": record.record_type().to_string(),
9974        }),
9975        RData::MX(mx) => json!({
9976            "exchange": normalize_dns_name_for_node(&mx.exchange),
9977            "priority": mx.preference,
9978            "type": "MX",
9979        }),
9980        RData::TXT(txt) => json!({
9981            "entries": txt
9982                .txt_data
9983                .iter()
9984                .map(|entry| String::from_utf8_lossy(entry).into_owned())
9985                .collect::<Vec<_>>(),
9986            "type": "TXT",
9987        }),
9988        RData::SRV(srv) => json!({
9989            "name": normalize_dns_name_for_node(&srv.target),
9990            "port": srv.port,
9991            "priority": srv.priority,
9992            "weight": srv.weight,
9993            "type": "SRV",
9994        }),
9995        RData::CNAME(name) => json!({
9996            "value": normalize_dns_name_for_node(&name.0),
9997            "type": "CNAME",
9998        }),
9999        RData::PTR(name) => json!({
10000            "value": normalize_dns_name_for_node(&name.0),
10001            "type": "PTR",
10002        }),
10003        RData::NS(name) => json!({
10004            "value": normalize_dns_name_for_node(&name.0),
10005            "type": "NS",
10006        }),
10007        RData::SOA(soa) => json!({
10008            "nsname": normalize_dns_name_for_node(&soa.mname),
10009            "hostmaster": normalize_dns_name_for_node(&soa.rname),
10010            "serial": soa.serial,
10011            "refresh": soa.refresh,
10012            "retry": soa.retry,
10013            "expire": soa.expire,
10014            "minttl": soa.minimum,
10015            "type": "SOA",
10016        }),
10017        RData::NAPTR(naptr) => json!({
10018            "flags": String::from_utf8_lossy(&naptr.flags).into_owned(),
10019            "service": String::from_utf8_lossy(&naptr.services).into_owned(),
10020            "regexp": String::from_utf8_lossy(&naptr.regexp).into_owned(),
10021            "replacement": normalize_dns_name_for_node(&naptr.replacement),
10022            "order": naptr.order,
10023            "preference": naptr.preference,
10024            "type": "NAPTR",
10025        }),
10026        RData::CAA(caa) => {
10027            let mut value = serde_json::Map::new();
10028            value.insert(
10029                "critical".to_owned(),
10030                Value::from(u8::from(caa.issuer_critical)),
10031            );
10032            value.insert("type".to_owned(), Value::String(String::from("CAA")));
10033            if caa.tag.eq_ignore_ascii_case("iodef") {
10034                value.insert(
10035                    "iodef".to_owned(),
10036                    Value::String(
10037                        caa.value_as_iodef()
10038                            .map(|url| url.to_string())
10039                            .unwrap_or_else(|_| String::from_utf8_lossy(&caa.value).into_owned()),
10040                    ),
10041                );
10042            } else if let Ok((issuer, _params)) = caa.value_as_issue() {
10043                let field = if caa.tag.eq_ignore_ascii_case("issuewild") {
10044                    "issuewild"
10045                } else {
10046                    "issue"
10047                };
10048                value.insert(
10049                    field.to_owned(),
10050                    Value::String(
10051                        issuer
10052                            .as_ref()
10053                            .map(ToString::to_string)
10054                            .unwrap_or_else(|| String::from_utf8_lossy(&caa.value).into_owned()),
10055                    ),
10056                );
10057            }
10058            Value::Object(value)
10059        }
10060        _ => return None,
10061    };
10062    Some(value)
10063}
10064
10065fn normalize_dns_name_for_node(name: &impl ToString) -> String {
10066    name.to_string().trim_end_matches('.').to_owned()
10067}
10068
10069fn summarize_dns_record(record: &Record) -> String {
10070    match record.data() {
10071        RData::A(_) | RData::AAAA(_) => record.data().to_string(),
10072        _ => format!("{} {}", record.record_type(), record.data()),
10073    }
10074}
10075
10076// build_root_filesystem, convert_root_lower_descriptor, convert_root_filesystem_entry,
10077// root_snapshot_entry moved to crate::bootstrap
10078
10079// apply_root_filesystem_entry, ensure_parent_directories moved to crate::bootstrap
10080
10081// ProcNetEntry moved to crate::state
10082
10083fn find_socket_state_entry(
10084    vm: Option<&VmState>,
10085    kind: SocketQueryKind,
10086    request: &FindListenerRequest,
10087) -> Result<Option<SocketStateEntry>, SidecarError> {
10088    let vm = vm.ok_or_else(|| SidecarError::InvalidState(String::from("unknown sidecar VM")))?;
10089
10090    for (process_id, process) in &vm.active_processes {
10091        if let Some(path) = request.path.as_deref() {
10092            if matches!(kind, SocketQueryKind::TcpListener) {
10093                for listener in process.unix_listeners.values() {
10094                    if listener.path() != path {
10095                        continue;
10096                    }
10097                    return Ok(Some(SocketStateEntry {
10098                        process_id: process_id.to_owned(),
10099                        host: None,
10100                        port: None,
10101                        path: Some(path.to_owned()),
10102                    }));
10103                }
10104            }
10105        }
10106
10107        if request.path.is_none() {
10108            if let Some(entry) =
10109                find_kernel_socket_state_entry(&vm.kernel, process_id, process, kind, request)?
10110            {
10111                return Ok(Some(entry));
10112            }
10113
10114            match kind {
10115                SocketQueryKind::TcpListener => {
10116                    for server in process.http_servers.values() {
10117                        let local_addr = server.guest_local_addr;
10118                        let local_host = local_addr.ip().to_string();
10119                        if !socket_host_matches(request.host.as_deref(), &local_host) {
10120                            continue;
10121                        }
10122                        if let Some(port) = request.port {
10123                            if local_addr.port() != port {
10124                                continue;
10125                            }
10126                        }
10127                        return Ok(Some(SocketStateEntry {
10128                            process_id: process_id.to_owned(),
10129                            host: Some(local_host),
10130                            port: Some(local_addr.port()),
10131                            path: None,
10132                        }));
10133                    }
10134
10135                    for listener in process.tcp_listeners.values() {
10136                        if listener.kernel_socket_id.is_some() {
10137                            continue;
10138                        }
10139                        let local_addr = listener.guest_local_addr();
10140                        let local_host = local_addr.ip().to_string();
10141                        if !socket_host_matches(request.host.as_deref(), &local_host) {
10142                            continue;
10143                        }
10144                        if let Some(port) = request.port {
10145                            if local_addr.port() != port {
10146                                continue;
10147                            }
10148                        }
10149                        return Ok(Some(SocketStateEntry {
10150                            process_id: process_id.to_owned(),
10151                            host: Some(local_host),
10152                            port: Some(local_addr.port()),
10153                            path: None,
10154                        }));
10155                    }
10156                }
10157                SocketQueryKind::UdpBound => {
10158                    for socket in process.udp_sockets.values() {
10159                        if socket.kernel_socket_id.is_some() {
10160                            continue;
10161                        }
10162                        let Some(local_addr) = socket.local_addr() else {
10163                            continue;
10164                        };
10165                        let local_host = local_addr.ip().to_string();
10166                        if !socket_host_matches(request.host.as_deref(), &local_host) {
10167                            continue;
10168                        }
10169                        if let Some(port) = request.port {
10170                            if local_addr.port() != port {
10171                                continue;
10172                            }
10173                        }
10174                        return Ok(Some(SocketStateEntry {
10175                            process_id: process_id.to_owned(),
10176                            host: Some(local_host),
10177                            port: Some(local_addr.port()),
10178                            path: None,
10179                        }));
10180                    }
10181                }
10182            }
10183        }
10184
10185        let child_pid = process.execution.child_pid();
10186        let inodes = socket_inodes_for_pid(child_pid)?;
10187        if inodes.is_empty() {
10188            continue;
10189        }
10190
10191        if let Some(path) = request.path.as_deref() {
10192            if let Some(listener) = find_unix_socket_for_pid(child_pid, &inodes, path, process_id)?
10193            {
10194                return Ok(Some(listener));
10195            }
10196            continue;
10197        }
10198
10199        let table_paths = match kind {
10200            SocketQueryKind::TcpListener => [
10201                format!("/proc/{child_pid}/net/tcp"),
10202                format!("/proc/{child_pid}/net/tcp6"),
10203            ],
10204            SocketQueryKind::UdpBound => [
10205                format!("/proc/{child_pid}/net/udp"),
10206                format!("/proc/{child_pid}/net/udp6"),
10207            ],
10208        };
10209        for table_path in table_paths {
10210            if let Some(entry) = find_inet_socket_for_pid(
10211                &table_path,
10212                &inodes,
10213                kind,
10214                request.host.as_deref(),
10215                request.port,
10216                process_id,
10217            )? {
10218                return Ok(Some(entry));
10219            }
10220        }
10221    }
10222
10223    Ok(None)
10224}
10225
10226fn require_vm_inspection_permission<B>(
10227    bridge: &SharedBridge<B>,
10228    vm_id: &str,
10229    capability: &str,
10230    domain: &str,
10231    resource: &str,
10232) -> Result<(), SidecarError>
10233where
10234    B: NativeSidecarBridge + Send + 'static,
10235    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
10236{
10237    let decision = bridge.static_permission_decision(vm_id, capability, domain, Some(resource));
10238    if decision.as_ref().is_some_and(|decision| decision.allow) {
10239        return Ok(());
10240    }
10241
10242    let reason = decision
10243        .and_then(|decision| decision.reason)
10244        .unwrap_or_else(|| format!("{capability} permission required"));
10245    Err(SidecarError::Execution(format!(
10246        "EACCES: permission denied, {resource}: {reason}"
10247    )))
10248}
10249
10250fn socket_query_resource(kind: SocketQueryKind, request: &FindListenerRequest) -> String {
10251    if let Some(path) = request.path.as_deref() {
10252        return format!("unix://{path}");
10253    }
10254
10255    let host = request.host.as_deref().unwrap_or("*");
10256    let port = request
10257        .port
10258        .map_or_else(|| String::from("*"), |port| port.to_string());
10259    match kind {
10260        SocketQueryKind::TcpListener => format!("tcp://{host}:{port}"),
10261        SocketQueryKind::UdpBound => format!("udp://{host}:{port}"),
10262    }
10263}
10264
10265fn snapshot_vm_processes(vm: &VmState) -> Vec<ProcessSnapshotEntry> {
10266    let process_table = vm.kernel.list_processes();
10267    snapshot_vm_processes_inner(vm, &process_table)
10268}
10269
10270fn snapshot_vm_processes_inner(
10271    vm: &VmState,
10272    process_table: &BTreeMap<u32, secure_exec_kernel::process_table::ProcessInfo>,
10273) -> Vec<ProcessSnapshotEntry> {
10274    let mut entries = Vec::new();
10275
10276    for (process_id, process) in &vm.active_processes {
10277        collect_process_snapshot_entries(process_id, process, process_table, &mut entries);
10278    }
10279
10280    for exited in &vm.exited_process_snapshots {
10281        entries.push(exited.process.clone());
10282    }
10283
10284    entries
10285}
10286
10287fn prune_exited_process_snapshots(vm: &mut VmState) {
10288    let cutoff = Instant::now() - EXITED_PROCESS_SNAPSHOT_RETENTION;
10289    while vm
10290        .exited_process_snapshots
10291        .front()
10292        .is_some_and(|snapshot| snapshot.captured_at < cutoff)
10293    {
10294        vm.exited_process_snapshots.pop_front();
10295    }
10296}
10297
10298fn build_process_snapshot_entry(
10299    process_id: &str,
10300    process: &ActiveProcess,
10301    info: &secure_exec_kernel::process_table::ProcessInfo,
10302    exit_code: Option<i32>,
10303) -> ProcessSnapshotEntry {
10304    ProcessSnapshotEntry {
10305        process_id: process_id.to_owned(),
10306        pid: info.pid,
10307        ppid: info.ppid,
10308        pgid: info.pgid,
10309        sid: info.sid,
10310        driver: info.driver.clone(),
10311        command: info.command.clone(),
10312        args: Vec::new(),
10313        cwd: process.guest_cwd.clone(),
10314        status: if exit_code.is_some() {
10315            ProcessSnapshotStatus::Exited
10316        } else {
10317            match info.status {
10318                ProcessStatus::Running => ProcessSnapshotStatus::Running,
10319                ProcessStatus::Stopped => ProcessSnapshotStatus::Stopped,
10320                ProcessStatus::Exited => ProcessSnapshotStatus::Exited,
10321            }
10322        },
10323        exit_code: exit_code.or(info.exit_code),
10324    }
10325}
10326
10327fn collect_process_snapshot_entries(
10328    process_id: &str,
10329    process: &ActiveProcess,
10330    process_table: &BTreeMap<u32, secure_exec_kernel::process_table::ProcessInfo>,
10331    entries: &mut Vec<ProcessSnapshotEntry>,
10332) {
10333    if let Some(info) = process_table.get(&process.kernel_pid) {
10334        entries.push(build_process_snapshot_entry(
10335            process_id, process, info, None,
10336        ));
10337    }
10338
10339    for (child_id, child) in &process.child_processes {
10340        let child_process_id = format!("{process_id}/{child_id}");
10341        collect_process_snapshot_entries(&child_process_id, child, process_table, entries);
10342    }
10343}
10344
10345fn find_kernel_socket_state_entry(
10346    kernel: &SidecarKernel,
10347    process_id: &str,
10348    process: &ActiveProcess,
10349    kind: SocketQueryKind,
10350    request: &FindListenerRequest,
10351) -> Result<Option<SocketStateEntry>, SidecarError> {
10352    let entry = match kind {
10353        SocketQueryKind::TcpListener => process
10354            .tcp_listeners
10355            .values()
10356            .filter_map(|listener| listener.kernel_socket_id)
10357            .find_map(|socket_id| {
10358                kernel_socket_state_entry(kernel, process_id, socket_id, kind, request)
10359            }),
10360        SocketQueryKind::UdpBound => process
10361            .udp_sockets
10362            .values()
10363            .filter_map(|socket| socket.kernel_socket_id)
10364            .find_map(|socket_id| {
10365                kernel_socket_state_entry(kernel, process_id, socket_id, kind, request)
10366            }),
10367    };
10368
10369    if entry.is_some() {
10370        return Ok(entry);
10371    }
10372
10373    for child in process.child_processes.values() {
10374        if let Some(entry) =
10375            find_kernel_socket_state_entry(kernel, process_id, child, kind, request)?
10376        {
10377            return Ok(Some(entry));
10378        }
10379    }
10380
10381    Ok(None)
10382}
10383
10384fn kernel_socket_state_entry(
10385    kernel: &SidecarKernel,
10386    process_id: &str,
10387    socket_id: SocketId,
10388    kind: SocketQueryKind,
10389    request: &FindListenerRequest,
10390) -> Option<SocketStateEntry> {
10391    let record = kernel.socket_get(socket_id)?;
10392    let local_address = record.local_address()?;
10393    match kind {
10394        SocketQueryKind::TcpListener if record.state() == SocketState::Listening => {}
10395        SocketQueryKind::TcpListener => return None,
10396        SocketQueryKind::UdpBound => {}
10397    }
10398
10399    if !socket_host_matches(request.host.as_deref(), local_address.host()) {
10400        return None;
10401    }
10402    if request
10403        .port
10404        .is_some_and(|port| local_address.port() != port)
10405    {
10406        return None;
10407    }
10408
10409    Some(SocketStateEntry {
10410        process_id: process_id.to_owned(),
10411        host: Some(local_address.host().to_owned()),
10412        port: Some(local_address.port()),
10413        path: None,
10414    })
10415}
10416
10417fn socket_inodes_for_pid(pid: u32) -> Result<BTreeSet<u64>, SidecarError> {
10418    let fd_dir = PathBuf::from(format!("/proc/{pid}/fd"));
10419    let entries = match fs::read_dir(&fd_dir) {
10420        Ok(entries) => entries,
10421        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(BTreeSet::new()),
10422        Err(error) => {
10423            return Err(SidecarError::Io(format!(
10424                "failed to read socket descriptors for process {pid}: {error}"
10425            )));
10426        }
10427    };
10428
10429    let mut inodes = BTreeSet::new();
10430    for entry in entries {
10431        let entry = entry.map_err(|error| {
10432            SidecarError::Io(format!(
10433                "failed to inspect fd entry for process {pid}: {error}"
10434            ))
10435        })?;
10436        let target = match fs::read_link(entry.path()) {
10437            Ok(target) => target,
10438            Err(_) => continue,
10439        };
10440        if let Some(inode) = parse_socket_inode(&target) {
10441            inodes.insert(inode);
10442        }
10443    }
10444
10445    Ok(inodes)
10446}
10447
10448fn parse_socket_inode(target: &Path) -> Option<u64> {
10449    let value = target.to_string_lossy();
10450    let trimmed = value.strip_prefix("socket:[")?.strip_suffix(']')?;
10451    trimmed.parse().ok()
10452}
10453
10454fn unix_socket_path(addr: &UnixSocketAddr) -> Option<String> {
10455    addr.as_pathname()
10456        .map(|path| path.to_string_lossy().into_owned())
10457}
10458
10459fn find_unix_socket_for_pid(
10460    pid: u32,
10461    inodes: &BTreeSet<u64>,
10462    path: &str,
10463    process_id: &str,
10464) -> Result<Option<SocketStateEntry>, SidecarError> {
10465    let table_path = format!("/proc/{pid}/net/unix");
10466    let contents = match fs::read_to_string(&table_path) {
10467        Ok(contents) => contents,
10468        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
10469        Err(error) => {
10470            return Err(SidecarError::Io(format!(
10471                "failed to inspect unix sockets for process {pid}: {error}"
10472            )));
10473        }
10474    };
10475
10476    for line in contents.lines().skip(1) {
10477        let columns = line.split_whitespace().collect::<Vec<_>>();
10478        if columns.len() < 8 {
10479            continue;
10480        }
10481        let Ok(inode) = columns[6].parse::<u64>() else {
10482            continue;
10483        };
10484        if !inodes.contains(&inode) || columns[7] != path {
10485            continue;
10486        }
10487        return Ok(Some(SocketStateEntry {
10488            process_id: process_id.to_owned(),
10489            host: None,
10490            port: None,
10491            path: Some(path.to_owned()),
10492        }));
10493    }
10494
10495    Ok(None)
10496}
10497
10498fn find_inet_socket_for_pid(
10499    table_path: &str,
10500    inodes: &BTreeSet<u64>,
10501    kind: SocketQueryKind,
10502    requested_host: Option<&str>,
10503    requested_port: Option<u16>,
10504    process_id: &str,
10505) -> Result<Option<SocketStateEntry>, SidecarError> {
10506    for entry in parse_proc_net_entries(table_path)? {
10507        if !inodes.contains(&entry.inode) {
10508            continue;
10509        }
10510        if matches!(kind, SocketQueryKind::TcpListener) && entry.state != "0A" {
10511            continue;
10512        }
10513        if !socket_host_matches(requested_host, &entry.local_host) {
10514            continue;
10515        }
10516        if let Some(port) = requested_port {
10517            if entry.local_port != port {
10518                continue;
10519            }
10520        }
10521        return Ok(Some(SocketStateEntry {
10522            process_id: process_id.to_owned(),
10523            host: Some(entry.local_host),
10524            port: Some(entry.local_port),
10525            path: None,
10526        }));
10527    }
10528
10529    Ok(None)
10530}
10531
10532fn is_unspecified_socket_host(host: &str) -> bool {
10533    host == "0.0.0.0" || host == "::"
10534}
10535
10536fn is_loopback_socket_host(host: &str) -> bool {
10537    host == "127.0.0.1" || host == "::1" || host.eq_ignore_ascii_case("localhost")
10538}
10539
10540pub(crate) fn vm_network_resource_counts(vm: &VmState) -> NetworkResourceCounts {
10541    let snapshot = vm.kernel.resource_snapshot();
10542    let mut counts = NetworkResourceCounts {
10543        sockets: snapshot.sockets,
10544        connections: snapshot.socket_connections,
10545    };
10546    for process in vm.active_processes.values() {
10547        let process_counts = process.sidecar_only_network_resource_counts();
10548        counts.sockets += process_counts.sockets;
10549        counts.connections += process_counts.connections;
10550    }
10551    counts
10552}
10553
10554fn collect_javascript_socket_port_state(
10555    kernel: &SidecarKernel,
10556    process_id: &str,
10557    process: &ActiveProcess,
10558    tcp_guest_to_host: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10559    http_loopback_targets: &mut BTreeMap<
10560        (JavascriptSocketFamily, u16),
10561        JavascriptHttpLoopbackTarget,
10562    >,
10563    udp_guest_to_host: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10564    udp_host_to_guest: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10565    used_tcp_ports: &mut BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10566    used_udp_ports: &mut BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10567) {
10568    for (family, port) in process.tcp_port_reservations.values() {
10569        used_tcp_ports.entry(*family).or_default().insert(*port);
10570    }
10571
10572    let mut record_tcp_listener = |guest_addr: SocketAddr, host_port: u16| {
10573        let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10574        used_tcp_ports
10575            .entry(family)
10576            .or_default()
10577            .insert(guest_addr.port());
10578        // VM-local loopback connects should also resolve listeners bound to
10579        // unspecified guest addresses like 0.0.0.0/::.
10580        tcp_guest_to_host.insert((family, guest_addr.port()), host_port);
10581    };
10582
10583    for listener in process.tcp_listeners.values() {
10584        let local_addr = listener
10585            .kernel_socket_id
10586            .and_then(|socket_id| kernel.socket_get(socket_id))
10587            .and_then(|record| record.local_address().cloned())
10588            .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
10589            .unwrap_or_else(|| listener.guest_local_addr());
10590        record_tcp_listener(local_addr, local_addr.port());
10591    }
10592
10593    for (server_id, server) in &process.http_servers {
10594        let host_port = match server.listener.local_addr() {
10595            Ok(addr) => addr.port(),
10596            Err(_) => continue,
10597        };
10598        record_tcp_listener(server.guest_local_addr, host_port);
10599        let family = JavascriptSocketFamily::from_ip(server.guest_local_addr.ip());
10600        http_loopback_targets.insert(
10601            (family, server.guest_local_addr.port()),
10602            JavascriptHttpLoopbackTarget {
10603                process_id: process_id.to_owned(),
10604                server_id: *server_id,
10605            },
10606        );
10607    }
10608
10609    if let Ok(http2) = process.http2.shared.lock() {
10610        for server in http2.servers.values() {
10611            record_tcp_listener(server.guest_local_addr, server.actual_local_addr.port());
10612        }
10613    }
10614
10615    for socket in process.tcp_sockets.values() {
10616        let guest_addr = socket
10617            .kernel_socket_id
10618            .and_then(|socket_id| kernel.socket_get(socket_id))
10619            .and_then(|record| record.local_address().cloned())
10620            .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
10621            .unwrap_or(socket.guest_local_addr);
10622        let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10623        used_tcp_ports
10624            .entry(family)
10625            .or_default()
10626            .insert(guest_addr.port());
10627    }
10628
10629    for socket in process.udp_sockets.values() {
10630        let guest_addr = socket
10631            .kernel_socket_id
10632            .and_then(|socket_id| kernel.socket_get(socket_id))
10633            .and_then(|record| record.local_address().cloned())
10634            .and_then(|address| {
10635                resolve_udp_bind_addr(address.host(), address.port(), socket.family).ok()
10636            })
10637            .or_else(|| socket.local_addr());
10638        let Some(guest_addr) = guest_addr else {
10639            continue;
10640        };
10641        let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10642        used_udp_ports
10643            .entry(family)
10644            .or_default()
10645            .insert(guest_addr.port());
10646        if let Some(host_addr) = socket
10647            .socket
10648            .as_ref()
10649            .and_then(|socket| socket.local_addr().ok())
10650        {
10651            if is_loopback_ip(guest_addr.ip()) {
10652                udp_guest_to_host.insert((family, guest_addr.port()), host_addr.port());
10653                udp_host_to_guest.insert((family, host_addr.port()), guest_addr.port());
10654            }
10655        } else if socket.kernel_socket_id.is_some() && is_loopback_ip(guest_addr.ip()) {
10656            udp_guest_to_host.insert((family, guest_addr.port()), guest_addr.port());
10657            udp_host_to_guest.insert((family, guest_addr.port()), guest_addr.port());
10658        }
10659    }
10660
10661    for (child_process_id, child) in &process.child_processes {
10662        let child_id = format!("{process_id}/{child_process_id}");
10663        collect_javascript_socket_port_state(
10664            kernel,
10665            &child_id,
10666            child,
10667            tcp_guest_to_host,
10668            http_loopback_targets,
10669            udp_guest_to_host,
10670            udp_host_to_guest,
10671            used_tcp_ports,
10672            used_udp_ports,
10673        );
10674    }
10675}
10676
10677pub(crate) fn build_javascript_socket_path_context(
10678    vm: &VmState,
10679) -> Result<JavascriptSocketPathContext, SidecarError> {
10680    let mut loopback_exempt_ports = vm.create_loopback_exempt_ports.clone();
10681    loopback_exempt_ports.extend(vm.configuration.loopback_exempt_ports.iter().copied());
10682    let mut tcp_loopback_guest_to_host_ports = BTreeMap::new();
10683    let mut http_loopback_targets = BTreeMap::new();
10684    let mut udp_loopback_guest_to_host_ports = BTreeMap::new();
10685    let mut udp_loopback_host_to_guest_ports = BTreeMap::new();
10686    let mut used_tcp_guest_ports = BTreeMap::new();
10687    let mut used_udp_guest_ports = BTreeMap::new();
10688    for (process_id, process) in &vm.active_processes {
10689        collect_javascript_socket_port_state(
10690            &vm.kernel,
10691            process_id,
10692            process,
10693            &mut tcp_loopback_guest_to_host_ports,
10694            &mut http_loopback_targets,
10695            &mut udp_loopback_guest_to_host_ports,
10696            &mut udp_loopback_host_to_guest_ports,
10697            &mut used_tcp_guest_ports,
10698            &mut used_udp_guest_ports,
10699        );
10700    }
10701    Ok(JavascriptSocketPathContext {
10702        sandbox_root: vm.cwd.clone(),
10703        mounts: vm.configuration.mounts.clone(),
10704        listen_policy: vm.listen_policy,
10705        loopback_exempt_ports,
10706        tcp_loopback_guest_to_host_ports,
10707        http_loopback_targets,
10708        udp_loopback_guest_to_host_ports,
10709        udp_loopback_host_to_guest_ports,
10710        used_tcp_guest_ports,
10711        used_udp_guest_ports,
10712    })
10713}
10714
10715fn check_network_resource_limit(
10716    limit: Option<usize>,
10717    current: usize,
10718    additional: usize,
10719    label: &str,
10720) -> Result<(), SidecarError> {
10721    if let Some(limit) = limit {
10722        if current.saturating_add(additional) > limit {
10723            return Err(SidecarError::Execution(format!(
10724                "EAGAIN: maximum {label} count reached"
10725            )));
10726        }
10727    }
10728    Ok(())
10729}
10730
10731fn normalize_tcp_listen_host(
10732    host: Option<&str>,
10733) -> Result<(JavascriptSocketFamily, &'static str, &'static str), SidecarError> {
10734    match host.unwrap_or("127.0.0.1") {
10735        "127.0.0.1" | "localhost" => Ok((JavascriptSocketFamily::Ipv4, "127.0.0.1", "127.0.0.1")),
10736        "::1" => Ok((JavascriptSocketFamily::Ipv6, "::1", "::1")),
10737        "0.0.0.0" => Ok((JavascriptSocketFamily::Ipv4, "127.0.0.1", "0.0.0.0")),
10738        "::" => Ok((JavascriptSocketFamily::Ipv6, "::1", "::")),
10739        other => Err(SidecarError::Execution(format!(
10740            "EACCES: TCP listeners must bind to loopback or unspecified addresses, got {other}"
10741        ))),
10742    }
10743}
10744
10745fn normalize_udp_bind_host(
10746    host: Option<&str>,
10747    family: JavascriptUdpFamily,
10748) -> Result<(&'static str, &'static str, JavascriptSocketFamily), SidecarError> {
10749    match (family, host) {
10750        (JavascriptUdpFamily::Ipv4, None) | (JavascriptUdpFamily::Ipv4, Some("0.0.0.0")) => {
10751            Ok(("127.0.0.1", "0.0.0.0", JavascriptSocketFamily::Ipv4))
10752        }
10753        (JavascriptUdpFamily::Ipv4, Some("127.0.0.1"))
10754        | (JavascriptUdpFamily::Ipv4, Some("localhost")) => {
10755            Ok(("127.0.0.1", "127.0.0.1", JavascriptSocketFamily::Ipv4))
10756        }
10757        (JavascriptUdpFamily::Ipv6, None) | (JavascriptUdpFamily::Ipv6, Some("::")) => {
10758            Ok(("::1", "::", JavascriptSocketFamily::Ipv6))
10759        }
10760        (JavascriptUdpFamily::Ipv6, Some("::1"))
10761        | (JavascriptUdpFamily::Ipv6, Some("localhost")) => {
10762            Ok(("::1", "::1", JavascriptSocketFamily::Ipv6))
10763        }
10764        (JavascriptUdpFamily::Ipv4, Some(other)) => Err(SidecarError::Execution(format!(
10765            "EACCES: udp4 sockets must bind to 127.0.0.1 or 0.0.0.0, got {other}"
10766        ))),
10767        (JavascriptUdpFamily::Ipv6, Some(other)) => Err(SidecarError::Execution(format!(
10768            "EACCES: udp6 sockets must bind to ::1 or ::, got {other}"
10769        ))),
10770    }
10771}
10772
10773fn allocate_guest_listen_port(
10774    requested_port: u16,
10775    family: JavascriptSocketFamily,
10776    used_ports: &BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10777    policy: VmListenPolicy,
10778) -> Result<u16, SidecarError> {
10779    let is_allowed = |port: u16| {
10780        port >= policy.port_min
10781            && port <= policy.port_max
10782            && (policy.allow_privileged || port >= 1024)
10783    };
10784    let used = used_ports.get(&family);
10785
10786    if requested_port != 0 {
10787        if !is_allowed(requested_port) {
10788            let reason = if requested_port < 1024 && !policy.allow_privileged {
10789                format!(
10790                    "EACCES: privileged listen port {requested_port} requires {}=true",
10791                    VM_LISTEN_ALLOW_PRIVILEGED_METADATA_KEY
10792                )
10793            } else {
10794                format!(
10795                    "EACCES: listen port {requested_port} is outside the allowed range {}-{}",
10796                    policy.port_min, policy.port_max
10797                )
10798            };
10799            return Err(SidecarError::Execution(reason));
10800        }
10801        if used.is_some_and(|ports| ports.contains(&requested_port)) {
10802            return Err(sidecar_net_error(std::io::Error::from_raw_os_error(
10803                libc::EADDRINUSE,
10804            )));
10805        }
10806        return Ok(requested_port);
10807    }
10808
10809    let allocation_start = policy
10810        .port_min
10811        .max(if policy.allow_privileged { 1 } else { 1024 });
10812    for candidate in allocation_start..=policy.port_max {
10813        if used.is_some_and(|ports| ports.contains(&candidate)) {
10814            continue;
10815        }
10816        return Ok(candidate);
10817    }
10818
10819    Err(sidecar_net_error(std::io::Error::from_raw_os_error(
10820        libc::EADDRINUSE,
10821    )))
10822}
10823
10824fn socket_host_matches(requested: Option<&str>, actual: &str) -> bool {
10825    match requested {
10826        None => true,
10827        Some(requested) if requested == actual => true,
10828        Some(requested)
10829            if is_unspecified_socket_host(requested) && is_unspecified_socket_host(actual) =>
10830        {
10831            true
10832        }
10833        Some(requested) if is_unspecified_socket_host(requested) => is_loopback_socket_host(actual),
10834        Some(requested) if requested.eq_ignore_ascii_case("localhost") => {
10835            is_loopback_socket_host(actual)
10836        }
10837        _ => false,
10838    }
10839}
10840
10841fn parse_proc_net_entries(table_path: &str) -> Result<Vec<ProcNetEntry>, SidecarError> {
10842    let contents = match fs::read_to_string(table_path) {
10843        Ok(contents) => contents,
10844        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
10845        Err(error) => {
10846            return Err(SidecarError::Io(format!(
10847                "failed to inspect socket table {table_path}: {error}"
10848            )));
10849        }
10850    };
10851
10852    let mut entries = Vec::new();
10853    for line in contents.lines().skip(1) {
10854        let columns = line.split_whitespace().collect::<Vec<_>>();
10855        if columns.len() < 10 {
10856            continue;
10857        }
10858        let Some((host, port)) = parse_proc_ip_port(columns[1]) else {
10859            continue;
10860        };
10861        let Ok(inode) = columns[9].parse::<u64>() else {
10862            continue;
10863        };
10864        entries.push(ProcNetEntry {
10865            local_host: host,
10866            local_port: port,
10867            state: columns[3].to_owned(),
10868            inode,
10869        });
10870    }
10871
10872    Ok(entries)
10873}
10874
10875fn parse_proc_ip_port(value: &str) -> Option<(String, u16)> {
10876    let (raw_ip, raw_port) = value.split_once(':')?;
10877    let port = u16::from_str_radix(raw_port, 16).ok()?;
10878    let host = match raw_ip.len() {
10879        8 => {
10880            let raw = u32::from_str_radix(raw_ip, 16).ok()?;
10881            Ipv4Addr::from(raw.to_le_bytes()).to_string()
10882        }
10883        32 => {
10884            let mut bytes = [0_u8; 16];
10885            for (index, chunk) in raw_ip.as_bytes().chunks(8).enumerate() {
10886                let word = u32::from_str_radix(std::str::from_utf8(chunk).ok()?, 16).ok()?;
10887                bytes[index * 4..(index + 1) * 4].copy_from_slice(&word.to_le_bytes());
10888            }
10889            Ipv6Addr::from(bytes).to_string()
10890        }
10891        _ => return None,
10892    };
10893    Some((host, port))
10894}
10895
10896fn python_file_entrypoint(entrypoint: &str) -> Option<PathBuf> {
10897    let path = Path::new(entrypoint);
10898    (path.extension().and_then(|extension| extension.to_str()) == Some("py"))
10899        .then(|| path.to_path_buf())
10900}
10901
10902fn add_runtime_guest_path_mapping(
10903    env: &mut BTreeMap<String, String>,
10904    guest_path: &str,
10905    host_path: &Path,
10906) {
10907    let mut mappings = env
10908        .get("AGENT_OS_GUEST_PATH_MAPPINGS")
10909        .and_then(|value| serde_json::from_str::<Vec<Value>>(value).ok())
10910        .unwrap_or_default();
10911    mappings.retain(|mapping| {
10912        mapping
10913            .get("guestPath")
10914            .and_then(Value::as_str)
10915            .map(|existing| normalize_path(existing) != normalize_path(guest_path))
10916            .unwrap_or(true)
10917    });
10918    mappings.push(json!({
10919        "guestPath": normalize_path(guest_path),
10920        "hostPath": host_path.display().to_string(),
10921    }));
10922    if let Ok(serialized) = serde_json::to_string(&mappings) {
10923        env.insert(String::from("AGENT_OS_GUEST_PATH_MAPPINGS"), serialized);
10924    }
10925}
10926
10927fn add_runtime_host_access_path(
10928    env: &mut BTreeMap<String, String>,
10929    key: &str,
10930    host_path: &Path,
10931    expand: bool,
10932) {
10933    let existing = env
10934        .get(key)
10935        .and_then(|value| serde_json::from_str::<Vec<String>>(value).ok())
10936        .unwrap_or_default()
10937        .into_iter()
10938        .map(PathBuf::from)
10939        .collect::<Vec<_>>();
10940    let mut paths = existing;
10941    paths.push(host_path.to_path_buf());
10942    let normalized = if expand {
10943        expand_host_access_paths(&paths)
10944    } else {
10945        dedupe_host_paths(&paths)
10946    };
10947    let serialized = normalized
10948        .iter()
10949        .map(|path| path.to_string_lossy().into_owned())
10950        .collect::<Vec<_>>();
10951    if let Ok(serialized) = serde_json::to_string(&serialized) {
10952        env.insert(key.to_owned(), serialized);
10953    }
10954}
10955
10956// discover_command_guest_paths moved to crate::bootstrap
10957
10958fn is_path_like_specifier(specifier: &str) -> bool {
10959    specifier.starts_with('/')
10960        || specifier.starts_with("./")
10961        || specifier.starts_with("../")
10962        || specifier.starts_with("file:")
10963}
10964
10965fn execution_wasm_permission_tier(tier: WasmPermissionTier) -> ExecutionWasmPermissionTier {
10966    match tier {
10967        WasmPermissionTier::Full => ExecutionWasmPermissionTier::Full,
10968        WasmPermissionTier::ReadWrite => ExecutionWasmPermissionTier::ReadWrite,
10969        WasmPermissionTier::ReadOnly => ExecutionWasmPermissionTier::ReadOnly,
10970        WasmPermissionTier::Isolated => ExecutionWasmPermissionTier::Isolated,
10971    }
10972}
10973
10974fn resolve_wasm_permission_tier(
10975    vm: &VmState,
10976    command_name: Option<&str>,
10977    explicit_tier: Option<WasmPermissionTier>,
10978    entrypoint: &str,
10979) -> WasmPermissionTier {
10980    explicit_tier
10981        .or_else(|| command_name.and_then(|command| vm.command_permissions.get(command).copied()))
10982        .or_else(|| {
10983            Path::new(entrypoint)
10984                .file_name()
10985                .and_then(|name| name.to_str())
10986                .and_then(|command| vm.command_permissions.get(command).copied())
10987        })
10988        .unwrap_or(WasmPermissionTier::Full)
10989}
10990
10991fn tokenize_shell_free_command(command: &str) -> Vec<String> {
10992    command
10993        .split_whitespace()
10994        .filter(|segment| !segment.is_empty())
10995        .map(str::to_owned)
10996        .collect()
10997}
10998
10999fn is_posix_shell_builtin(command: &str) -> bool {
11000    matches!(
11001        command,
11002        "." | ":"
11003            | "break"
11004            | "cd"
11005            | "continue"
11006            | "eval"
11007            | "exec"
11008            | "exit"
11009            | "export"
11010            | "readonly"
11011            | "return"
11012            | "set"
11013            | "shift"
11014            | "times"
11015            | "trap"
11016            | "umask"
11017            | "unset"
11018    )
11019}
11020
11021/// Single-token checks for shell-mode commands whose first word forces a real
11022/// shell even when the command string has no shell metacharacters. This is not
11023/// a parser: env-assignment prefixes (`FOO=bar cmd`) and shell reserved words
11024/// have no meaning outside `sh`, so whitespace-tokenizing them would silently
11025/// run the wrong program.
11026fn shell_first_token_requires_shell(token: &str) -> bool {
11027    token.contains('=') || is_shell_reserved_word(token)
11028}
11029
11030fn is_shell_reserved_word(token: &str) -> bool {
11031    matches!(
11032        token,
11033        "if" | "then"
11034            | "elif"
11035            | "else"
11036            | "fi"
11037            | "for"
11038            | "in"
11039            | "do"
11040            | "done"
11041            | "while"
11042            | "until"
11043            | "case"
11044            | "esac"
11045            | "{"
11046            | "}"
11047            | "!"
11048    )
11049}
11050
11051fn command_requires_shell(command: &str) -> bool {
11052    command.chars().any(|ch| {
11053        matches!(
11054            ch,
11055            '|' | '&'
11056                | ';'
11057                | '<'
11058                | '>'
11059                | '('
11060                | ')'
11061                | '$'
11062                | '`'
11063                | '*'
11064                | '?'
11065                | '['
11066                | ']'
11067                | '{'
11068                | '}'
11069                | '~'
11070                | '\''
11071                | '"'
11072                | '\\'
11073                | '\n'
11074        )
11075    })
11076}
11077
11078fn host_mount_path_for_guest_path(vm: &VmState, guest_path: &str) -> Option<PathBuf> {
11079    let normalized = normalize_path(guest_path);
11080
11081    let mut mounts = vm
11082        .configuration
11083        .mounts
11084        .iter()
11085        .filter_map(|mount| {
11086            ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
11087                .then(|| {
11088                    mount_config_host_path(&mount.plugin.config)
11089                        .map(|host_path| (mount.guest_path.as_str(), host_path))
11090                })
11091                .flatten()
11092        })
11093        .collect::<Vec<_>>();
11094    mounts.sort_by_key(|mount| std::cmp::Reverse(mount.0.len()));
11095
11096    for (guest_root, host_root) in mounts {
11097        if normalized != guest_root && !normalized.starts_with(&format!("{guest_root}/")) {
11098            continue;
11099        }
11100
11101        let suffix = normalized
11102            .strip_prefix(guest_root)
11103            .unwrap_or_default()
11104            .trim_start_matches('/');
11105        let mut path = PathBuf::from(host_root);
11106        if !suffix.is_empty() {
11107            path.push(suffix);
11108        }
11109        return Some(path);
11110    }
11111
11112    None
11113}
11114
11115fn host_runtime_path_for_guest_path_with_env(
11116    vm: &VmState,
11117    runtime_env: &BTreeMap<String, String>,
11118    guest_path: &str,
11119    default_host_cwd: &Path,
11120) -> Option<PathBuf> {
11121    if let Some(path) = host_mount_path_for_guest_path(vm, guest_path) {
11122        return Some(path);
11123    }
11124    if let Some(path) = host_path_from_runtime_guest_mappings(runtime_env, guest_path) {
11125        return Some(path);
11126    }
11127
11128    let normalized = normalize_path(guest_path);
11129    let virtual_home = guest_virtual_home(vm);
11130
11131    if normalized == virtual_home || normalized.starts_with(&format!("{virtual_home}/")) {
11132        let suffix = normalized
11133            .strip_prefix(&virtual_home)
11134            .unwrap_or_default()
11135            .trim_start_matches('/');
11136        let mut host_path = default_host_cwd.to_path_buf();
11137        if !suffix.is_empty() {
11138            host_path.push(suffix);
11139        }
11140        return Some(host_path);
11141    }
11142
11143    None
11144}
11145
11146#[derive(Deserialize, Serialize)]
11147struct RuntimeGuestPathMapping {
11148    #[serde(rename = "guestPath")]
11149    guest_path: String,
11150    #[serde(rename = "hostPath")]
11151    host_path: String,
11152    #[serde(rename = "readOnly", default)]
11153    read_only: bool,
11154}
11155
11156pub(crate) fn host_path_from_runtime_guest_mappings(
11157    runtime_env: &BTreeMap<String, String>,
11158    guest_path: &str,
11159) -> Option<PathBuf> {
11160    let mappings = runtime_env
11161        .get("AGENT_OS_GUEST_PATH_MAPPINGS")
11162        .and_then(|value| serde_json::from_str::<Vec<RuntimeGuestPathMapping>>(value).ok())?;
11163    let normalized = normalize_path(guest_path);
11164
11165    let mut sorted_mappings = mappings
11166        .into_iter()
11167        .filter_map(|mapping| {
11168            (!mapping.guest_path.is_empty() && !mapping.host_path.is_empty()).then_some((
11169                normalize_path(&mapping.guest_path),
11170                PathBuf::from(mapping.host_path),
11171            ))
11172        })
11173        .collect::<Vec<_>>();
11174    sorted_mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.0.len()));
11175
11176    for (guest_root, mut host_root) in sorted_mappings {
11177        if guest_root != "/"
11178            && normalized != guest_root
11179            && !normalized.starts_with(&format!("{guest_root}/"))
11180        {
11181            continue;
11182        }
11183        if guest_root == "/" && !normalized.starts_with('/') {
11184            continue;
11185        }
11186
11187        if host_root.is_relative() {
11188            host_root = std::env::current_dir().ok()?.join(host_root);
11189        }
11190
11191        let suffix = if guest_root == "/" {
11192            normalized.trim_start_matches('/')
11193        } else {
11194            normalized
11195                .strip_prefix(&guest_root)
11196                .unwrap_or_default()
11197                .trim_start_matches('/')
11198        };
11199        if !suffix.is_empty() {
11200            host_root.push(suffix);
11201        }
11202        return Some(host_root);
11203    }
11204
11205    None
11206}
11207
11208fn guest_runtime_path_for_host_path(
11209    runtime_env: &BTreeMap<String, String>,
11210    virtual_home: &str,
11211    cwd: &Path,
11212    host_path: &str,
11213) -> Option<String> {
11214    let resolved = if host_path.starts_with("file://") {
11215        PathBuf::from(host_path.trim_start_matches("file://"))
11216    } else if host_path.starts_with("file:") {
11217        PathBuf::from(host_path.trim_start_matches("file:"))
11218    } else {
11219        let candidate = PathBuf::from(host_path);
11220        if candidate.is_absolute() {
11221            candidate
11222        } else if host_path.starts_with("./") || host_path.starts_with("../") {
11223            cwd.join(candidate)
11224        } else {
11225            return None;
11226        }
11227    };
11228    let normalized = normalize_host_path(&resolved);
11229
11230    if let Some(path) = guest_path_from_runtime_host_mappings(runtime_env, &normalized) {
11231        return Some(path);
11232    }
11233
11234    let normalized_cwd = normalize_host_path(cwd);
11235    if !path_is_within_root(&normalized, &normalized_cwd) {
11236        return None;
11237    }
11238
11239    let virtual_home = if virtual_home.starts_with('/') {
11240        virtual_home.to_string()
11241    } else {
11242        String::from("/root")
11243    };
11244    let suffix = normalized
11245        .strip_prefix(&normalized_cwd)
11246        .ok()?
11247        .to_string_lossy()
11248        .replace('\\', "/")
11249        .trim_start_matches('/')
11250        .to_owned();
11251
11252    Some(if suffix.is_empty() {
11253        virtual_home
11254    } else {
11255        normalize_path(&format!("{virtual_home}/{suffix}"))
11256    })
11257}
11258
11259fn guest_path_from_runtime_host_mappings(
11260    runtime_env: &BTreeMap<String, String>,
11261    host_path: &Path,
11262) -> Option<String> {
11263    let mappings = runtime_env
11264        .get("AGENT_OS_GUEST_PATH_MAPPINGS")
11265        .and_then(|value| serde_json::from_str::<Vec<RuntimeGuestPathMapping>>(value).ok())?;
11266    let normalized = normalize_host_path(host_path);
11267
11268    let mut sorted_mappings = mappings
11269        .into_iter()
11270        .filter_map(|mapping| {
11271            (!mapping.guest_path.is_empty() && !mapping.host_path.is_empty()).then_some((
11272                normalize_path(&mapping.guest_path),
11273                normalize_host_path(Path::new(&mapping.host_path)),
11274            ))
11275        })
11276        .collect::<Vec<_>>();
11277    sorted_mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.1.as_os_str().len()));
11278
11279    for (guest_root, host_root) in sorted_mappings {
11280        if !path_is_within_root(&normalized, &host_root) {
11281            continue;
11282        }
11283        let suffix = normalized
11284            .strip_prefix(&host_root)
11285            .ok()?
11286            .to_string_lossy()
11287            .replace('\\', "/")
11288            .trim_start_matches('/')
11289            .to_owned();
11290
11291        return Some(if suffix.is_empty() {
11292            guest_root
11293        } else if guest_root == "/" {
11294            normalize_path(&format!("/{suffix}"))
11295        } else {
11296            normalize_path(&format!("{guest_root}/{suffix}"))
11297        });
11298    }
11299
11300    None
11301}
11302
11303fn host_mount_path_for_guest_path_from_mounts(
11304    mounts: &[crate::protocol::MountDescriptor],
11305    guest_path: &str,
11306) -> Option<PathBuf> {
11307    let normalized = normalize_path(guest_path);
11308
11309    let mut host_mounts = mounts
11310        .iter()
11311        .filter_map(|mount| {
11312            ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
11313                .then(|| {
11314                    mount_config_host_path(&mount.plugin.config)
11315                        .map(|host_path| (mount.guest_path.as_str(), host_path))
11316                })
11317                .flatten()
11318        })
11319        .collect::<Vec<_>>();
11320    host_mounts.sort_by_key(|mount| std::cmp::Reverse(mount.0.len()));
11321
11322    for (guest_root, host_root) in host_mounts {
11323        if normalized != guest_root && !normalized.starts_with(&format!("{guest_root}/")) {
11324            continue;
11325        }
11326
11327        let suffix = normalized
11328            .strip_prefix(guest_root)
11329            .unwrap_or_default()
11330            .trim_start_matches('/');
11331        let mut path = PathBuf::from(host_root);
11332        if !suffix.is_empty() {
11333            path.push(suffix);
11334        }
11335        return Some(path);
11336    }
11337
11338    None
11339}
11340
11341#[cfg(test)]
11342mod host_mount_path_for_guest_path_from_mounts_tests {
11343    use super::host_mount_path_for_guest_path_from_mounts;
11344    use crate::protocol::{MountDescriptor, MountPluginDescriptor};
11345    use serde_json::json;
11346    use std::path::PathBuf;
11347
11348    #[test]
11349    fn resolves_module_access_mount_paths() {
11350        let mounts = vec![MountDescriptor {
11351            guest_path: String::from("/root/node_modules"),
11352            read_only: true,
11353            plugin: MountPluginDescriptor {
11354                id: String::from("module_access"),
11355                config: json!({
11356                    "hostPath": "/tmp/workspace/node_modules",
11357                })
11358                .to_string(),
11359            },
11360        }];
11361
11362        let resolved =
11363            host_mount_path_for_guest_path_from_mounts(&mounts, "/root/node_modules/pkg/index.js")
11364                .expect("module_access mount should resolve");
11365
11366        assert_eq!(
11367            resolved,
11368            PathBuf::from("/tmp/workspace/node_modules/pkg/index.js")
11369        );
11370    }
11371}
11372
11373fn resolve_guest_socket_host_path(
11374    context: &JavascriptSocketPathContext,
11375    guest_path: &str,
11376) -> PathBuf {
11377    if let Some(path) = host_mount_path_for_guest_path_from_mounts(&context.mounts, guest_path) {
11378        return path;
11379    }
11380
11381    let normalized = normalize_path(guest_path);
11382    let mut host_path = context.sandbox_root.clone();
11383    let suffix = normalized.trim_start_matches('/');
11384    if !suffix.is_empty() {
11385        host_path.push(suffix);
11386    }
11387    host_path
11388}
11389
11390fn ensure_kernel_parent_directories(
11391    kernel: &mut SidecarKernel,
11392    path: &str,
11393) -> Result<(), SidecarError> {
11394    let parent = dirname(path);
11395    if parent != "/" && !kernel.exists(&parent).map_err(kernel_error)? {
11396        kernel.mkdir(&parent, true).map_err(kernel_error)?;
11397    }
11398    Ok(())
11399}
11400
11401// JavascriptChildProcessSpawnOptions, JavascriptChildProcessSpawnRequest moved to crate::protocol
11402// ResolvedChildProcessExecution moved to crate::state
11403
11404pub(crate) fn sanitize_javascript_child_process_internal_bootstrap_env(
11405    env: &BTreeMap<String, String>,
11406) -> BTreeMap<String, String> {
11407    const ALLOWED_KEYS: &[&str] = &[
11408        "AGENT_OS_ALLOWED_NODE_BUILTINS",
11409        "AGENT_OS_GUEST_PATH_MAPPINGS",
11410        "AGENT_OS_LOOPBACK_EXEMPT_PORTS",
11411        "AGENT_OS_VIRTUAL_PROCESS_EXEC_PATH",
11412        "AGENT_OS_VIRTUAL_PROCESS_UID",
11413        "AGENT_OS_VIRTUAL_PROCESS_GID",
11414        "AGENT_OS_VIRTUAL_PROCESS_VERSION",
11415    ];
11416
11417    env.iter()
11418        .filter(|(key, _)| {
11419            ALLOWED_KEYS.contains(&key.as_str()) || key.starts_with("AGENT_OS_VIRTUAL_OS_")
11420        })
11421        .map(|(key, value)| (key.clone(), value.clone()))
11422        .collect()
11423}
11424
11425// Network request types moved to crate::protocol
11426
11427// VmDnsConfig, DnsResolutionSource moved to crate::state
11428
11429fn resolve_tcp_bind_addr(host: &str, port: u16) -> Result<SocketAddr, SidecarError> {
11430    (host, port)
11431        .to_socket_addrs()
11432        .map_err(sidecar_net_error)?
11433        .next()
11434        .ok_or_else(|| {
11435            SidecarError::Execution(format!("failed to resolve TCP bind address {host}:{port}"))
11436        })
11437}
11438
11439pub(crate) fn format_dns_resource(hostname: &str) -> String {
11440    format!("dns://{hostname}")
11441}
11442
11443pub(crate) fn format_tcp_resource(host: &str, port: u16) -> String {
11444    format!("tcp://{host}:{port}")
11445}
11446
11447fn is_loopback_ip(ip: IpAddr) -> bool {
11448    match ip {
11449        IpAddr::V4(ip) => ip.is_loopback(),
11450        IpAddr::V6(ip) => {
11451            ip.is_loopback()
11452                || ip
11453                    .to_ipv4_mapped()
11454                    .is_some_and(|mapped| mapped.is_loopback())
11455        }
11456    }
11457}
11458
11459fn loopback_cidr(ip: IpAddr) -> &'static str {
11460    match ip {
11461        IpAddr::V4(ip) if ip.is_loopback() => "127.0.0.0/8",
11462        IpAddr::V6(ip)
11463            if ip
11464                .to_ipv4_mapped()
11465                .is_some_and(|mapped| mapped.is_loopback()) =>
11466        {
11467            "127.0.0.0/8"
11468        }
11469        IpAddr::V6(_) => "::1/128",
11470        IpAddr::V4(_) => "127.0.0.0/8",
11471    }
11472}
11473
11474/// Returns the embedded IPv4 address of an IPv4-compatible IPv6 address
11475/// (`::a.b.c.d`): the first six 16-bit segments are zero and the final 32 bits
11476/// hold the IPv4 address. The all-zero (`::`) and loopback (`::1`) addresses are
11477/// deliberately excluded so they are handled by the unspecified/loopback paths
11478/// rather than treated as IPv4-compatible.
11479fn ipv4_compatible_embedded(ip: Ipv6Addr) -> Option<Ipv4Addr> {
11480    let segments = ip.segments();
11481    if segments[0..6].iter().any(|&s| s != 0) {
11482        return None;
11483    }
11484    let embedded = (u32::from(segments[6]) << 16) | u32::from(segments[7]);
11485    // Skip :: (0.0.0.0) and ::1 (0.0.0.1) — these are the IPv6 unspecified /
11486    // loopback addresses, not IPv4-compatible representations of an IPv4 host.
11487    if embedded == 0 || embedded == 1 {
11488        return None;
11489    }
11490    Some(Ipv4Addr::from(embedded))
11491}
11492
11493fn restricted_non_loopback_ip_range(ip: IpAddr) -> Option<(&'static str, &'static str)> {
11494    match ip {
11495        IpAddr::V4(ip) => {
11496            if ip.is_unspecified() {
11497                // 0.0.0.0 is unspecified; the host stack routes a connect() to
11498                // it back to 127.0.0.1, so it must not bypass the loopback gate.
11499                return Some(("0.0.0.0/32", "unspecified"));
11500            }
11501            let [first, second, ..] = ip.octets();
11502            match (first, second) {
11503                (10, _) => Some(("10.0.0.0/8", "private")),
11504                (100, 64..=127) => Some(("100.64.0.0/10", "carrier-grade-nat")),
11505                (172, 16..=31) => Some(("172.16.0.0/12", "private")),
11506                (192, 168) => Some(("192.168.0.0/16", "private")),
11507                (169, 254) => Some(("169.254.0.0/16", "link-local")),
11508                // 224.0.0.0/4 is the IPv4 multicast range and 240.0.0.0/4 is
11509                // reserved/future-use (255.255.255.255 broadcast falls in it).
11510                // Neither is a legitimate unicast egress target, so a guest
11511                // connect to them must be denied rather than attempted.
11512                (224..=239, _) => Some(("224.0.0.0/4", "multicast")),
11513                (240..=255, _) => Some(("240.0.0.0/4", "reserved")),
11514                _ => None,
11515            }
11516        }
11517        IpAddr::V6(ip) => {
11518            if let Some(mapped) = ip.to_ipv4_mapped() {
11519                return restricted_non_loopback_ip_range(IpAddr::V4(mapped));
11520            }
11521            // IPv4-compatible IPv6 (::a.b.c.d): the first six segments are zero
11522            // and the last two carry an embedded IPv4 address. `to_ipv4_mapped`
11523            // returns None for this form, so without canonicalizing it here a
11524            // guest could spell a restricted IPv4 target (e.g. cloud-metadata
11525            // ::169.254.169.254) and bypass the IPv4 classifier. `::`/`::1` are
11526            // excluded so they fall through to the unspecified/loopback paths.
11527            if let Some(compat) = ipv4_compatible_embedded(ip) {
11528                return restricted_non_loopback_ip_range(IpAddr::V4(compat));
11529            }
11530
11531            if ip.is_unspecified() {
11532                // :: is the IPv6 unspecified address; same routing hazard as
11533                // 0.0.0.0, so deny it rather than letting it reach the host.
11534                return Some(("::/128", "unspecified"));
11535            }
11536
11537            let segments = ip.segments();
11538            if (segments[0] & 0xfe00) == 0xfc00 {
11539                return Some(("fc00::/7", "unique-local"));
11540            }
11541            if (segments[0] & 0xffc0) == 0xfe80 {
11542                return Some(("fe80::/10", "link-local"));
11543            }
11544            None
11545        }
11546    }
11547}
11548
11549fn blocked_dns_resolution_error(
11550    resource: &str,
11551    ip: IpAddr,
11552    cidr: &str,
11553    label: &str,
11554) -> SidecarError {
11555    SidecarError::Execution(format!(
11556        "EACCES: blocked outbound network access to {resource}: {ip} is within restricted {label} range {cidr}"
11557    ))
11558}
11559
11560fn blocked_loopback_connect_error(resource: &str, ip: IpAddr, port: u16) -> SidecarError {
11561    SidecarError::Execution(format!(
11562        "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}",
11563        loopback_cidr(ip)
11564    ))
11565}
11566
11567fn filter_dns_safe_ip_addrs(
11568    addresses: Vec<IpAddr>,
11569    hostname: &str,
11570) -> Result<Vec<IpAddr>, SidecarError> {
11571    let resource = format_dns_resource(hostname);
11572    let mut allowed = Vec::new();
11573    let mut blocked = None;
11574
11575    for ip in addresses {
11576        if let Some((cidr, label)) = restricted_non_loopback_ip_range(ip) {
11577            blocked.get_or_insert((ip, cidr, label));
11578            continue;
11579        }
11580        allowed.push(ip);
11581    }
11582
11583    if allowed.is_empty() {
11584        let (ip, cidr, label) = blocked.expect("blocked DNS results should capture a reason");
11585        return Err(blocked_dns_resolution_error(&resource, ip, cidr, label));
11586    }
11587
11588    Ok(allowed)
11589}
11590
11591fn loopback_connect_allowed(context: &JavascriptSocketPathContext, port: u16) -> bool {
11592    context.loopback_port_allowed(port)
11593}
11594
11595fn filter_tcp_connect_ip_addrs(
11596    addresses: Vec<IpAddr>,
11597    host: &str,
11598    port: u16,
11599    context: &JavascriptSocketPathContext,
11600) -> Result<Vec<IpAddr>, SidecarError> {
11601    let resource = format_tcp_resource(host, port);
11602    let mut allowed = Vec::new();
11603    let mut blocked = None;
11604
11605    for ip in addresses {
11606        if let Some((cidr, label)) = restricted_non_loopback_ip_range(ip) {
11607            blocked.get_or_insert_with(|| blocked_dns_resolution_error(&resource, ip, cidr, label));
11608            continue;
11609        }
11610        if is_loopback_ip(ip) && !loopback_connect_allowed(context, port) {
11611            blocked.get_or_insert_with(|| blocked_loopback_connect_error(&resource, ip, port));
11612            continue;
11613        }
11614        allowed.push(ip);
11615    }
11616
11617    if allowed.is_empty() {
11618        return Err(blocked.expect("blocked TCP connect results should capture a reason"));
11619    }
11620
11621    Ok(allowed)
11622}
11623
11624fn resolve_tcp_connect_addr<B>(
11625    bridge: &SharedBridge<B>,
11626    kernel: &SidecarKernel,
11627    vm_id: &str,
11628    dns: &VmDnsConfig,
11629    host: &str,
11630    port: u16,
11631    context: &JavascriptSocketPathContext,
11632) -> Result<ResolvedTcpConnectAddr, SidecarError>
11633where
11634    B: NativeSidecarBridge + Send + 'static,
11635    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11636{
11637    let allowed = filter_tcp_connect_ip_addrs(
11638        resolve_dns_ip_addrs(
11639            bridge,
11640            kernel,
11641            vm_id,
11642            dns,
11643            host,
11644            DnsLookupPolicy::SkipPermissions,
11645        )?,
11646        host,
11647        port,
11648        context,
11649    )?;
11650    let ip = allowed
11651        .iter()
11652        .copied()
11653        .find(|candidate| {
11654            let family = JavascriptSocketFamily::from_ip(*candidate);
11655            context.translate_tcp_loopback_port(family, port).is_some()
11656        })
11657        // We do not implement Happy Eyeballs yet, so prefer IPv4 over a
11658        // verbatim IPv6-first DNS answer for general outbound TCP connects.
11659        .or_else(|| allowed.iter().copied().find(IpAddr::is_ipv4))
11660        .or_else(|| allowed.first().copied())
11661        .ok_or_else(|| {
11662            SidecarError::Execution(format!("failed to resolve TCP address {host}:{port}"))
11663        })?;
11664    let family = JavascriptSocketFamily::from_ip(ip);
11665    let translated_loopback_port = context.translate_tcp_loopback_port(family, port);
11666    let use_kernel_loopback = is_loopback_ip(ip) && translated_loopback_port == Some(port);
11667    let actual_port = if is_loopback_ip(ip) {
11668        translated_loopback_port.unwrap_or(port)
11669    } else {
11670        port
11671    };
11672    Ok(ResolvedTcpConnectAddr {
11673        actual_addr: SocketAddr::new(ip, actual_port),
11674        guest_remote_addr: SocketAddr::new(ip, port),
11675        use_kernel_loopback,
11676    })
11677}
11678
11679fn resolve_dns_ip_addrs<B>(
11680    bridge: &SharedBridge<B>,
11681    kernel: &SidecarKernel,
11682    vm_id: &str,
11683    dns: &VmDnsConfig,
11684    hostname: &str,
11685    policy: DnsLookupPolicy,
11686) -> Result<Vec<IpAddr>, SidecarError>
11687where
11688    B: NativeSidecarBridge + Send + 'static,
11689    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11690{
11691    let resolution = match kernel.resolve_dns(hostname, policy) {
11692        Ok(resolution) => resolution,
11693        Err(error) => {
11694            let sidecar_error = kernel_error(error.clone());
11695            if error.code() != "EACCES" {
11696                emit_dns_resolution_failure_event(bridge, vm_id, hostname, dns, &sidecar_error);
11697            }
11698            return Err(sidecar_error);
11699        }
11700    };
11701    emit_dns_resolution_event(
11702        bridge,
11703        vm_id,
11704        hostname,
11705        resolution.source(),
11706        resolution.addresses(),
11707        dns,
11708    );
11709    Ok(resolution.addresses().to_vec())
11710}
11711
11712fn resolve_dns_records<B>(
11713    bridge: &SharedBridge<B>,
11714    kernel: &SidecarKernel,
11715    vm_id: &str,
11716    dns: &VmDnsConfig,
11717    hostname: &str,
11718    record_type: RecordType,
11719    policy: DnsLookupPolicy,
11720) -> Result<DnsRecordResolution, SidecarError>
11721where
11722    B: NativeSidecarBridge + Send + 'static,
11723    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11724{
11725    let resolution = match kernel.resolve_dns_records(hostname, record_type, policy) {
11726        Ok(resolution) => resolution,
11727        Err(error) => {
11728            let sidecar_error = kernel_error(error.clone());
11729            if error.code() != "EACCES" {
11730                emit_dns_resolution_failure_event(bridge, vm_id, hostname, dns, &sidecar_error);
11731            }
11732            return Err(sidecar_error);
11733        }
11734    };
11735    emit_dns_record_resolution_event(bridge, vm_id, hostname, &resolution, dns);
11736    Ok(resolution)
11737}
11738
11739fn filter_dns_ip_addrs(
11740    addresses: Vec<IpAddr>,
11741    family: Option<u8>,
11742) -> Result<Vec<IpAddr>, SidecarError> {
11743    let filtered: Vec<_> = match family.unwrap_or(0) {
11744        0 => addresses,
11745        4 => addresses
11746            .into_iter()
11747            .filter(|ip| matches!(ip, IpAddr::V4(_)))
11748            .collect(),
11749        6 => addresses
11750            .into_iter()
11751            .filter(|ip| matches!(ip, IpAddr::V6(_)))
11752            .collect(),
11753        other => {
11754            return Err(SidecarError::InvalidState(format!(
11755                "unsupported dns family {other}"
11756            )));
11757        }
11758    };
11759
11760    if filtered.is_empty() {
11761        return Err(SidecarError::Execution(String::from(
11762            "failed to resolve DNS address for requested family",
11763        )));
11764    }
11765
11766    Ok(filtered)
11767}
11768
11769fn resolve_udp_bind_addr(
11770    host: &str,
11771    port: u16,
11772    family: JavascriptUdpFamily,
11773) -> Result<SocketAddr, SidecarError> {
11774    (host, port)
11775        .to_socket_addrs()
11776        .map_err(sidecar_net_error)?
11777        .find(|addr| family.matches_addr(addr))
11778        .ok_or_else(|| {
11779            SidecarError::Execution(format!(
11780                "failed to resolve {} UDP bind address {host}:{port}",
11781                family.socket_type()
11782            ))
11783        })
11784}
11785
11786fn resolve_udp_addr<B>(request: UdpRemoteAddrRequest<'_, B>) -> Result<SocketAddr, SidecarError>
11787where
11788    B: NativeSidecarBridge + Send + 'static,
11789    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11790{
11791    let UdpRemoteAddrRequest {
11792        bridge,
11793        kernel,
11794        vm_id,
11795        dns,
11796        host,
11797        port,
11798        family,
11799        context,
11800    } = request;
11801    resolve_dns_ip_addrs(
11802        bridge,
11803        kernel,
11804        vm_id,
11805        dns,
11806        host,
11807        DnsLookupPolicy::SkipPermissions,
11808    )?
11809    .into_iter()
11810    .map(|ip| {
11811        let family_key = JavascriptSocketFamily::from_ip(ip);
11812        let actual_port = if is_loopback_ip(ip) {
11813            context
11814                .translate_udp_loopback_port(family_key, port)
11815                .unwrap_or(port)
11816        } else {
11817            port
11818        };
11819        SocketAddr::new(ip, actual_port)
11820    })
11821    .find(|addr| family.matches_addr(addr))
11822    .ok_or_else(|| {
11823        SidecarError::Execution(format!(
11824            "failed to resolve {} UDP address {host}:{port}",
11825            family.socket_type()
11826        ))
11827    })
11828}
11829
11830fn socket_addr_family(addr: &SocketAddr) -> &'static str {
11831    match addr {
11832        SocketAddr::V4(_) => "IPv4",
11833        SocketAddr::V6(_) => "IPv6",
11834    }
11835}
11836
11837fn javascript_net_timeout_value() -> Value {
11838    Value::String(String::from(JAVASCRIPT_NET_TIMEOUT_SENTINEL))
11839}
11840
11841fn javascript_net_json_string(value: Value, label: &str) -> Result<Value, SidecarError> {
11842    serde_json::to_string(&value)
11843        .map(Value::String)
11844        .map_err(|error| {
11845            SidecarError::InvalidState(format!("failed to serialize {label} payload: {error}"))
11846        })
11847}
11848
11849fn javascript_net_read_value(
11850    event: Option<JavascriptTcpSocketEvent>,
11851) -> Result<Value, SidecarError> {
11852    match event {
11853        Some(JavascriptTcpSocketEvent::Data(chunk)) => Ok(Value::String(
11854            base64::engine::general_purpose::STANDARD.encode(chunk),
11855        )),
11856        Some(JavascriptTcpSocketEvent::End | JavascriptTcpSocketEvent::Close { .. }) => {
11857            Ok(Value::Null)
11858        }
11859        Some(JavascriptTcpSocketEvent::Error { code, message }) => {
11860            let detail = code.unwrap_or_else(|| String::from("socket read"));
11861            Err(SidecarError::Execution(format!("{detail}: {message}")))
11862        }
11863        None => Ok(javascript_net_timeout_value()),
11864    }
11865}
11866
11867fn io_error_code(error: &std::io::Error) -> Option<String> {
11868    match error.raw_os_error() {
11869        Some(libc::EADDRINUSE) => Some(String::from("EADDRINUSE")),
11870        Some(libc::EADDRNOTAVAIL) => Some(String::from("EADDRNOTAVAIL")),
11871        Some(libc::ECONNREFUSED) => Some(String::from("ECONNREFUSED")),
11872        Some(libc::ECONNRESET) => Some(String::from("ECONNRESET")),
11873        Some(libc::EINVAL) => Some(String::from("EINVAL")),
11874        Some(libc::EPIPE) => Some(String::from("EPIPE")),
11875        Some(libc::ETIMEDOUT) => Some(String::from("ETIMEDOUT")),
11876        Some(libc::EHOSTUNREACH) => Some(String::from("EHOSTUNREACH")),
11877        Some(libc::ENETUNREACH) => Some(String::from("ENETUNREACH")),
11878        _ => None,
11879    }
11880}
11881
11882fn sidecar_net_error(error: std::io::Error) -> SidecarError {
11883    let message = match io_error_code(&error) {
11884        Some(code) => format!("{code}: {error}"),
11885        None => error.to_string(),
11886    };
11887    SidecarError::Execution(message)
11888}
11889
11890fn tls_provider() -> Arc<rustls::crypto::CryptoProvider> {
11891    Arc::new(aws_lc_rs::default_provider())
11892}
11893
11894fn tls_local_certificates(
11895    options: &JavascriptTlsBridgeOptions,
11896) -> Result<Vec<Vec<u8>>, SidecarError> {
11897    let Some(certificates) = options.cert.as_ref() else {
11898        return Ok(Vec::new());
11899    };
11900    tls_material_entries(certificates)
11901}
11902
11903fn tls_material_entries(material: &JavascriptTlsMaterial) -> Result<Vec<Vec<u8>>, SidecarError> {
11904    match material {
11905        JavascriptTlsMaterial::Single(entry) => tls_data_value(entry).map(|value| vec![value]),
11906        JavascriptTlsMaterial::Many(entries) => entries.iter().map(tls_data_value).collect(),
11907    }
11908}
11909
11910fn tls_data_value(value: &JavascriptTlsDataValue) -> Result<Vec<u8>, SidecarError> {
11911    match value {
11912        JavascriptTlsDataValue::Buffer { data } => base64::engine::general_purpose::STANDARD
11913            .decode(data)
11914            .map_err(|error| {
11915                SidecarError::InvalidState(format!("TLS material contains invalid base64: {error}"))
11916            }),
11917        JavascriptTlsDataValue::String { data } => Ok(data.as_bytes().to_vec()),
11918    }
11919}
11920
11921fn tls_certificates_from_material(
11922    material: &JavascriptTlsMaterial,
11923) -> Result<Vec<CertificateDer<'static>>, SidecarError> {
11924    let mut certificates = Vec::new();
11925    for entry in tls_material_entries(material)? {
11926        let mut reader = std::io::BufReader::new(Cursor::new(entry.clone()));
11927        let parsed = rustls_pemfile::certs(&mut reader)
11928            .collect::<Result<Vec<_>, _>>()
11929            .map_err(sidecar_net_error)?;
11930        if parsed.is_empty() {
11931            certificates.push(CertificateDer::from(entry));
11932        } else {
11933            certificates.extend(parsed);
11934        }
11935    }
11936    if certificates.is_empty() {
11937        return Err(SidecarError::InvalidState(String::from(
11938            "TLS certificate material did not contain any certificates",
11939        )));
11940    }
11941    Ok(certificates)
11942}
11943
11944fn tls_private_key_from_material(
11945    material: &JavascriptTlsMaterial,
11946) -> Result<PrivateKeyDer<'static>, SidecarError> {
11947    for entry in tls_material_entries(material)? {
11948        let mut reader = std::io::BufReader::new(Cursor::new(entry));
11949        if let Some(key) = rustls_pemfile::private_key(&mut reader).map_err(sidecar_net_error)? {
11950            return Ok(key);
11951        }
11952    }
11953    Err(SidecarError::InvalidState(String::from(
11954        "TLS private key material did not contain a supported key",
11955    )))
11956}
11957
11958fn tls_root_store(options: &JavascriptTlsBridgeOptions) -> Result<RootCertStore, SidecarError> {
11959    let mut roots = RootCertStore::empty();
11960    if let Some(ca) = options.ca.as_ref() {
11961        for certificate in tls_certificates_from_material(ca)? {
11962            roots.add(certificate).map_err(|error| {
11963                SidecarError::InvalidState(format!("failed to add TLS CA certificate: {error}"))
11964            })?;
11965        }
11966        return Ok(roots);
11967    }
11968
11969    for certificate in rustls_native_certs::load_native_certs().certs {
11970        roots.add(certificate).map_err(|error| {
11971            SidecarError::InvalidState(format!(
11972                "failed to add native TLS certificate to root store: {error}"
11973            ))
11974        })?;
11975    }
11976    Ok(roots)
11977}
11978
11979fn build_client_tls_stream(
11980    stream: TcpStream,
11981    options: &JavascriptTlsBridgeOptions,
11982) -> Result<rustls::StreamOwned<ClientConnection, TcpStream>, SidecarError> {
11983    let config = build_client_tls_config(options)?;
11984    let server_name = options
11985        .servername
11986        .clone()
11987        .unwrap_or_else(|| String::from("localhost"));
11988    let server_name = ServerName::try_from(server_name)
11989        .map_err(|_| SidecarError::InvalidState(String::from("invalid TLS servername")))?;
11990    stream
11991        .set_read_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
11992        .map_err(sidecar_net_error)?;
11993    stream
11994        .set_write_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
11995        .map_err(sidecar_net_error)?;
11996    let mut tls_stream = rustls::StreamOwned::new(
11997        ClientConnection::new(Arc::new(config), server_name).map_err(|error| {
11998            SidecarError::Execution(format!("failed to start TLS client: {error}"))
11999        })?,
12000        stream,
12001    );
12002    while tls_stream.conn.is_handshaking() {
12003        tls_stream
12004            .conn
12005            .complete_io(&mut tls_stream.sock)
12006            .map_err(sidecar_net_error)?;
12007    }
12008    tls_stream
12009        .sock
12010        .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
12011        .map_err(sidecar_net_error)?;
12012    tls_stream
12013        .sock
12014        .set_write_timeout(None)
12015        .map_err(sidecar_net_error)?;
12016    Ok(tls_stream)
12017}
12018
12019fn build_client_loopback_tls_stream(
12020    transport: crate::state::LoopbackTlsEndpoint,
12021    options: &JavascriptTlsBridgeOptions,
12022) -> Result<rustls::StreamOwned<ClientConnection, crate::state::LoopbackTlsEndpoint>, SidecarError>
12023{
12024    let config = build_client_tls_config(options)?;
12025    let server_name = options
12026        .servername
12027        .clone()
12028        .unwrap_or_else(|| String::from("localhost"));
12029    let server_name = ServerName::try_from(server_name)
12030        .map_err(|_| SidecarError::InvalidState(String::from("invalid TLS servername")))?;
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        transport,
12036    );
12037    match tls_stream.conn.complete_io(&mut tls_stream.sock) {
12038        Ok(_) => {}
12039        Err(error)
12040            if matches!(
12041                error.kind(),
12042                std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
12043            ) => {}
12044        Err(error) => return Err(sidecar_net_error(error)),
12045    }
12046    Ok(tls_stream)
12047}
12048
12049fn build_client_tls_config(
12050    options: &JavascriptTlsBridgeOptions,
12051) -> Result<ClientConfig, SidecarError> {
12052    let provider = tls_provider();
12053    let builder = ClientConfig::builder_with_provider(provider.clone())
12054        .with_safe_default_protocol_versions()
12055        .map_err(|error| {
12056            SidecarError::InvalidState(format!("invalid TLS protocol config: {error}"))
12057        })?;
12058
12059    let mut config = if options.reject_unauthorized == Some(false) {
12060        let verifier = Arc::new(InsecureTlsVerifier {
12061            supported_schemes: provider
12062                .signature_verification_algorithms
12063                .supported_schemes(),
12064        });
12065        builder
12066            .dangerous()
12067            .with_custom_certificate_verifier(verifier)
12068            .with_no_client_auth()
12069    } else {
12070        builder
12071            .with_root_certificates(tls_root_store(options)?)
12072            .with_no_client_auth()
12073    };
12074
12075    if let Some(protocols) = options.alpn_protocols.as_ref() {
12076        config.alpn_protocols = protocols
12077            .iter()
12078            .map(|protocol| protocol.as_bytes().to_vec())
12079            .collect();
12080    }
12081    Ok(config)
12082}
12083
12084fn build_server_tls_stream(
12085    stream: TcpStream,
12086    options: &JavascriptTlsBridgeOptions,
12087) -> Result<rustls::StreamOwned<ServerConnection, TcpStream>, SidecarError> {
12088    let config = build_server_tls_config(options)?;
12089    stream
12090        .set_read_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
12091        .map_err(sidecar_net_error)?;
12092    stream
12093        .set_write_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
12094        .map_err(sidecar_net_error)?;
12095    let mut tls_stream = rustls::StreamOwned::new(
12096        ServerConnection::new(Arc::new(config)).map_err(|error| {
12097            SidecarError::Execution(format!("failed to start TLS server: {error}"))
12098        })?,
12099        stream,
12100    );
12101    while tls_stream.conn.is_handshaking() {
12102        tls_stream
12103            .conn
12104            .complete_io(&mut tls_stream.sock)
12105            .map_err(sidecar_net_error)?;
12106    }
12107    tls_stream
12108        .sock
12109        .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
12110        .map_err(sidecar_net_error)?;
12111    tls_stream
12112        .sock
12113        .set_write_timeout(None)
12114        .map_err(sidecar_net_error)?;
12115    Ok(tls_stream)
12116}
12117
12118fn build_server_loopback_tls_stream(
12119    transport: crate::state::LoopbackTlsEndpoint,
12120    options: &JavascriptTlsBridgeOptions,
12121) -> Result<rustls::StreamOwned<ServerConnection, crate::state::LoopbackTlsEndpoint>, SidecarError>
12122{
12123    let config = build_server_tls_config(options)?;
12124    Ok(rustls::StreamOwned::new(
12125        ServerConnection::new(Arc::new(config)).map_err(|error| {
12126            SidecarError::Execution(format!("failed to start TLS server: {error}"))
12127        })?,
12128        transport,
12129    ))
12130}
12131
12132fn build_server_tls_config(
12133    options: &JavascriptTlsBridgeOptions,
12134) -> Result<ServerConfig, SidecarError> {
12135    let certificates = tls_certificates_from_material(options.cert.as_ref().ok_or_else(|| {
12136        SidecarError::InvalidState(String::from("TLS server upgrade requires a certificate"))
12137    })?)?;
12138    let key = tls_private_key_from_material(options.key.as_ref().ok_or_else(|| {
12139        SidecarError::InvalidState(String::from("TLS server upgrade requires a private key"))
12140    })?)?;
12141
12142    let mut config = ServerConfig::builder_with_provider(tls_provider())
12143        .with_safe_default_protocol_versions()
12144        .map_err(|error| {
12145            SidecarError::InvalidState(format!("invalid TLS protocol config: {error}"))
12146        })?
12147        .with_no_client_auth()
12148        .with_single_cert(certificates, key)
12149        .map_err(|error| {
12150            SidecarError::InvalidState(format!("invalid TLS server config: {error}"))
12151        })?;
12152
12153    if let Some(protocols) = options.alpn_protocols.as_ref() {
12154        config.alpn_protocols = protocols
12155            .iter()
12156            .map(|protocol| protocol.as_bytes().to_vec())
12157            .collect();
12158    }
12159    Ok(config)
12160}
12161
12162fn tls_protocol_name(version: rustls::ProtocolVersion) -> String {
12163    match version {
12164        rustls::ProtocolVersion::TLSv1_2 => String::from("TLSv1.2"),
12165        rustls::ProtocolVersion::TLSv1_3 => String::from("TLSv1.3"),
12166        other => other
12167            .as_str()
12168            .map(str::to_owned)
12169            .unwrap_or_else(|| format!("{other:?}")),
12170    }
12171}
12172
12173fn tls_cipher_bridge_value(suite: rustls::SupportedCipherSuite) -> Value {
12174    tls_bridge_object(vec![
12175        (
12176            "name",
12177            suite
12178                .suite()
12179                .as_str()
12180                .map(|value| Value::String(value.to_owned()))
12181                .unwrap_or(Value::Null),
12182        ),
12183        (
12184            "standardName",
12185            suite
12186                .suite()
12187                .as_str()
12188                .map(|value| Value::String(value.to_owned()))
12189                .unwrap_or(Value::Null),
12190        ),
12191        (
12192            "version",
12193            Value::String(if suite.tls13().is_some() {
12194                String::from("TLSv1.3")
12195            } else {
12196                String::from("TLSv1.2")
12197            }),
12198        ),
12199    ])
12200}
12201
12202fn tls_certificate_bridge_value(certificate: &[u8], detailed: bool) -> Value {
12203    let mut fields = vec![("raw", tls_bridge_buffer_value(certificate))];
12204    if detailed {
12205        fields.push(("issuerCertificate", tls_bridge_undefined_value()));
12206    }
12207    tls_bridge_object(fields)
12208}
12209
12210fn tls_bridge_buffer_value(bytes: &[u8]) -> Value {
12211    json!({
12212        "type": "buffer",
12213        "data": base64::engine::general_purpose::STANDARD.encode(bytes),
12214    })
12215}
12216
12217fn tls_bridge_object(entries: Vec<(&str, Value)>) -> Value {
12218    let value = entries
12219        .into_iter()
12220        .map(|(key, value)| (key.to_owned(), value))
12221        .collect::<serde_json::Map<String, Value>>();
12222    json!({
12223        "type": "object",
12224        "id": 1,
12225        "value": value,
12226    })
12227}
12228
12229fn tls_bridge_undefined_value() -> Value {
12230    json!({
12231        "type": "undefined",
12232    })
12233}
12234
12235fn spawn_tcp_socket_reader(
12236    stream: TcpStream,
12237    sender: Sender<JavascriptTcpSocketEvent>,
12238    tls_mode: Arc<AtomicBool>,
12239    saw_local_shutdown: Arc<AtomicBool>,
12240    saw_remote_end: Arc<AtomicBool>,
12241    close_notified: Arc<AtomicBool>,
12242) {
12243    thread::spawn(move || {
12244        let mut stream = stream;
12245        let mut buffer = vec![0_u8; 64 * 1024];
12246        loop {
12247            if tls_mode.load(Ordering::SeqCst) {
12248                break;
12249            }
12250            match stream.read(&mut buffer) {
12251                Ok(0) => {
12252                    saw_remote_end.store(true, Ordering::SeqCst);
12253                    let _ = sender.send(JavascriptTcpSocketEvent::End);
12254                    if saw_local_shutdown.load(Ordering::SeqCst)
12255                        && !close_notified.swap(true, Ordering::SeqCst)
12256                    {
12257                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12258                    }
12259                    break;
12260                }
12261                Ok(bytes_read) => {
12262                    if sender
12263                        .send(JavascriptTcpSocketEvent::Data(
12264                            buffer[..bytes_read].to_vec(),
12265                        ))
12266                        .is_err()
12267                    {
12268                        break;
12269                    }
12270                }
12271                Err(error)
12272                    if matches!(
12273                        error.kind(),
12274                        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
12275                    ) =>
12276                {
12277                    continue;
12278                }
12279                Err(error) => {
12280                    let code = io_error_code(&error);
12281                    let _ = sender.send(JavascriptTcpSocketEvent::Error {
12282                        code,
12283                        message: error.to_string(),
12284                    });
12285                    if !close_notified.swap(true, Ordering::SeqCst) {
12286                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12287                    }
12288                    break;
12289                }
12290            }
12291        }
12292    });
12293}
12294
12295fn spawn_tls_socket_reader(
12296    tls_stream: Arc<Mutex<Option<ActiveTlsStream>>>,
12297    sender: Sender<JavascriptTcpSocketEvent>,
12298    saw_local_shutdown: Arc<AtomicBool>,
12299    saw_remote_end: Arc<AtomicBool>,
12300    close_notified: Arc<AtomicBool>,
12301) {
12302    thread::spawn(move || {
12303        let mut buffer = vec![0_u8; 64 * 1024];
12304        loop {
12305            let read_result = {
12306                let mut guard = match tls_stream.lock() {
12307                    Ok(guard) => guard,
12308                    Err(_) => return,
12309                };
12310                let Some(stream) = guard.as_mut() else {
12311                    return;
12312                };
12313                stream.read(&mut buffer)
12314            };
12315
12316            match read_result {
12317                Ok(0) => {
12318                    saw_remote_end.store(true, Ordering::SeqCst);
12319                    let _ = sender.send(JavascriptTcpSocketEvent::End);
12320                    if saw_local_shutdown.load(Ordering::SeqCst)
12321                        && !close_notified.swap(true, Ordering::SeqCst)
12322                    {
12323                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12324                    }
12325                    break;
12326                }
12327                Ok(bytes_read) => {
12328                    if sender
12329                        .send(JavascriptTcpSocketEvent::Data(
12330                            buffer[..bytes_read].to_vec(),
12331                        ))
12332                        .is_err()
12333                    {
12334                        break;
12335                    }
12336                }
12337                Err(error)
12338                    if matches!(
12339                        error.kind(),
12340                        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
12341                    ) =>
12342                {
12343                    // The TLS reader and writer share one rustls stream mutex. Yield after
12344                    // timed-out reads so request writes can acquire the lock promptly.
12345                    std::thread::sleep(Duration::from_millis(1));
12346                    continue;
12347                }
12348                Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => {
12349                    saw_remote_end.store(true, Ordering::SeqCst);
12350                    let _ = sender.send(JavascriptTcpSocketEvent::End);
12351                    if saw_local_shutdown.load(Ordering::SeqCst)
12352                        && !close_notified.swap(true, Ordering::SeqCst)
12353                    {
12354                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12355                    }
12356                    break;
12357                }
12358                Err(error) => {
12359                    let code = io_error_code(&error);
12360                    let _ = sender.send(JavascriptTcpSocketEvent::Error {
12361                        code,
12362                        message: error.to_string(),
12363                    });
12364                    if !close_notified.swap(true, Ordering::SeqCst) {
12365                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12366                    }
12367                    break;
12368                }
12369            }
12370        }
12371    });
12372}
12373
12374fn spawn_unix_socket_reader(
12375    stream: UnixStream,
12376    sender: Sender<JavascriptTcpSocketEvent>,
12377    saw_local_shutdown: Arc<AtomicBool>,
12378    saw_remote_end: Arc<AtomicBool>,
12379    close_notified: Arc<AtomicBool>,
12380) {
12381    thread::spawn(move || {
12382        let mut stream = stream;
12383        let mut buffer = vec![0_u8; 64 * 1024];
12384        loop {
12385            match stream.read(&mut buffer) {
12386                Ok(0) => {
12387                    saw_remote_end.store(true, Ordering::SeqCst);
12388                    let _ = sender.send(JavascriptTcpSocketEvent::End);
12389                    if saw_local_shutdown.load(Ordering::SeqCst)
12390                        && !close_notified.swap(true, Ordering::SeqCst)
12391                    {
12392                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12393                    }
12394                    break;
12395                }
12396                Ok(bytes_read) => {
12397                    if sender
12398                        .send(JavascriptTcpSocketEvent::Data(
12399                            buffer[..bytes_read].to_vec(),
12400                        ))
12401                        .is_err()
12402                    {
12403                        break;
12404                    }
12405                }
12406                Err(error) => {
12407                    let code = io_error_code(&error);
12408                    let _ = sender.send(JavascriptTcpSocketEvent::Error {
12409                        code,
12410                        message: error.to_string(),
12411                    });
12412                    if !close_notified.swap(true, Ordering::SeqCst) {
12413                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12414                    }
12415                    break;
12416                }
12417            }
12418        }
12419    });
12420}
12421
12422fn terminate_child_process_tree(kernel: &mut SidecarKernel, process: &mut ActiveProcess) {
12423    let sqlite_database_ids = process.sqlite_databases.keys().copied().collect::<Vec<_>>();
12424    for database_id in sqlite_database_ids {
12425        let _ = close_sqlite_database(kernel, process, database_id);
12426    }
12427    process.sqlite_statements.clear();
12428    process.http_servers.clear();
12429    process.pending_http_requests.clear();
12430    if let Ok(mut http2) = process.http2.shared.lock() {
12431        let sessions = http2.sessions.values().cloned().collect::<Vec<_>>();
12432        http2.server_events.clear();
12433        http2.session_events.clear();
12434        http2.streams.clear();
12435        http2.servers.clear();
12436        http2.sessions.clear();
12437        drop(http2);
12438        for session in sessions {
12439            let (respond_to, _rx) = mpsc::channel();
12440            let _ = session.command_tx.send(Http2SessionCommand::Close {
12441                abrupt: true,
12442                respond_to,
12443            });
12444        }
12445    }
12446
12447    let listener_ids = process.tcp_listeners.keys().cloned().collect::<Vec<_>>();
12448    for listener_id in listener_ids {
12449        if let Some(listener) = process.tcp_listeners.remove(&listener_id) {
12450            let _ = listener.close(kernel, process.kernel_pid);
12451        }
12452    }
12453
12454    let sockets = process.tcp_sockets.keys().cloned().collect::<Vec<_>>();
12455    for socket_id in sockets {
12456        if let Some(socket) = process.tcp_sockets.remove(&socket_id) {
12457            let _ = socket.close(kernel, process.kernel_pid);
12458        }
12459    }
12460
12461    let unix_listener_ids = process.unix_listeners.keys().cloned().collect::<Vec<_>>();
12462    for listener_id in unix_listener_ids {
12463        if let Some(listener) = process.unix_listeners.remove(&listener_id) {
12464            let _ = listener.close();
12465        }
12466    }
12467
12468    let unix_sockets = process.unix_sockets.keys().cloned().collect::<Vec<_>>();
12469    for socket_id in unix_sockets {
12470        if let Some(socket) = process.unix_sockets.remove(&socket_id) {
12471            let _ = socket.close();
12472        }
12473    }
12474
12475    let udp_socket_ids = process.udp_sockets.keys().cloned().collect::<Vec<_>>();
12476    for socket_id in udp_socket_ids {
12477        if let Some(mut socket) = process.udp_sockets.remove(&socket_id) {
12478            socket.close(kernel, process.kernel_pid);
12479        }
12480    }
12481
12482    let child_ids = process.child_processes.keys().cloned().collect::<Vec<_>>();
12483    for child_id in child_ids {
12484        let Some(mut child) = process.child_processes.remove(&child_id) else {
12485            continue;
12486        };
12487        terminate_child_process_tree(kernel, &mut child);
12488        let _ = kernel.kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, SIGTERM);
12489        let _ = signal_runtime_process(child.execution.child_pid(), SIGTERM);
12490        child.kernel_handle.finish(0);
12491        let _ = kernel.wait_and_reap(child.kernel_pid);
12492    }
12493}
12494
12495fn service_javascript_sqlite_sync_rpc(
12496    kernel: &mut SidecarKernel,
12497    process: &mut ActiveProcess,
12498    request: &JavascriptSyncRpcRequest,
12499) -> Result<Value, SidecarError> {
12500    match request.method.as_str() {
12501        "sqlite.constants" => Ok(json!({})),
12502        "sqlite.open" => sqlite_open_database(kernel, process, request),
12503        "sqlite.close" => {
12504            let database_id =
12505                javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.close database id")?;
12506            close_sqlite_database(kernel, process, database_id)?;
12507            Ok(Value::Null)
12508        }
12509        "sqlite.exec" => sqlite_exec_database(kernel, process, request),
12510        "sqlite.query" => sqlite_query_database(process, request),
12511        "sqlite.prepare" => sqlite_prepare_statement(process, request),
12512        "sqlite.location" => {
12513            let database_id =
12514                javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.location database id")?;
12515            let database = sqlite_database(process, database_id)?;
12516            Ok(database
12517                .vm_path
12518                .as_ref()
12519                .map(|path| Value::String(path.clone()))
12520                .unwrap_or(Value::Null))
12521        }
12522        "sqlite.checkpoint" => {
12523            let database_id =
12524                javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.checkpoint database id")?;
12525            let kernel_pid = process.kernel_pid;
12526            let database = sqlite_database_mut(process, database_id)?;
12527            sqlite_sync_database(kernel, kernel_pid, database)?;
12528            Ok(Value::Null)
12529        }
12530        "sqlite.statement.run" => sqlite_run_statement(kernel, process, request),
12531        "sqlite.statement.get" => sqlite_get_statement(process, request),
12532        "sqlite.statement.all" | "sqlite.statement.iterate" => {
12533            sqlite_all_statement(process, request)
12534        }
12535        "sqlite.statement.columns" => sqlite_statement_columns(process, request),
12536        "sqlite.statement.setReturnArrays" => {
12537            let statement_id = javascript_sync_rpc_arg_u64(
12538                &request.args,
12539                0,
12540                "sqlite.statement.setReturnArrays statement id",
12541            )?;
12542            let enabled = javascript_sync_rpc_arg_bool(
12543                &request.args,
12544                1,
12545                "sqlite.statement.setReturnArrays enabled",
12546            )?;
12547            sqlite_statement_mut(process, statement_id)?.return_arrays = enabled;
12548            Ok(Value::Null)
12549        }
12550        "sqlite.statement.setReadBigInts" => {
12551            let statement_id = javascript_sync_rpc_arg_u64(
12552                &request.args,
12553                0,
12554                "sqlite.statement.setReadBigInts statement id",
12555            )?;
12556            let enabled = javascript_sync_rpc_arg_bool(
12557                &request.args,
12558                1,
12559                "sqlite.statement.setReadBigInts enabled",
12560            )?;
12561            sqlite_statement_mut(process, statement_id)?.read_bigints = enabled;
12562            Ok(Value::Null)
12563        }
12564        "sqlite.statement.setAllowBareNamedParameters" => {
12565            let statement_id = javascript_sync_rpc_arg_u64(
12566                &request.args,
12567                0,
12568                "sqlite.statement.setAllowBareNamedParameters statement id",
12569            )?;
12570            let enabled = javascript_sync_rpc_arg_bool(
12571                &request.args,
12572                1,
12573                "sqlite.statement.setAllowBareNamedParameters enabled",
12574            )?;
12575            sqlite_statement_mut(process, statement_id)?.allow_bare_named_parameters = enabled;
12576            Ok(Value::Null)
12577        }
12578        "sqlite.statement.setAllowUnknownNamedParameters" => {
12579            let statement_id = javascript_sync_rpc_arg_u64(
12580                &request.args,
12581                0,
12582                "sqlite.statement.setAllowUnknownNamedParameters statement id",
12583            )?;
12584            let enabled = javascript_sync_rpc_arg_bool(
12585                &request.args,
12586                1,
12587                "sqlite.statement.setAllowUnknownNamedParameters enabled",
12588            )?;
12589            sqlite_statement_mut(process, statement_id)?.allow_unknown_named_parameters = enabled;
12590            Ok(Value::Null)
12591        }
12592        "sqlite.statement.finalize" => {
12593            let statement_id = javascript_sync_rpc_arg_u64(
12594                &request.args,
12595                0,
12596                "sqlite.statement.finalize statement id",
12597            )?;
12598            process
12599                .sqlite_statements
12600                .remove(&statement_id)
12601                .ok_or_else(|| {
12602                    SidecarError::InvalidState(format!(
12603                        "sqlite statement handle not found: {statement_id}"
12604                    ))
12605                })?;
12606            Ok(Value::Null)
12607        }
12608        other => Err(SidecarError::InvalidState(format!(
12609            "unsupported JavaScript sqlite sync RPC method {other}"
12610        ))),
12611    }
12612}
12613
12614fn sqlite_open_database(
12615    kernel: &mut SidecarKernel,
12616    process: &mut ActiveProcess,
12617    request: &JavascriptSyncRpcRequest,
12618) -> Result<Value, SidecarError> {
12619    ensure_per_process_state_handle_capacity(process.sqlite_databases.len(), "sqlite database")?;
12620    let path = request.args.first().and_then(Value::as_str);
12621    let vm_path = path.filter(|value| !value.is_empty() && *value != ":memory:");
12622    let options = request.args.get(1);
12623    let read_only = sqlite_option_bool(options, "readOnly").unwrap_or(false);
12624    let create = sqlite_option_bool(options, "create").unwrap_or(!read_only);
12625    let timeout_ms = sqlite_option_u64(options, "timeout");
12626
12627    process.next_sqlite_database_id += 1;
12628    let database_id = process.next_sqlite_database_id;
12629
12630    let host_path = if vm_path.is_some() {
12631        Some(
12632            std::env::temp_dir()
12633                .join(format!(
12634                    "secure-exec-sidecar-sqlite-{}-{database_id}",
12635                    process.kernel_pid
12636                ))
12637                .join("database.sqlite"),
12638        )
12639    } else {
12640        None
12641    };
12642
12643    if let Some(host_path) = host_path.as_ref() {
12644        if let Some(parent) = host_path.parent() {
12645            fs::create_dir_all(parent).map_err(|error| {
12646                SidecarError::Io(format!(
12647                    "failed to prepare sqlite temp directory {}: {error}",
12648                    parent.display()
12649                ))
12650            })?;
12651        }
12652    }
12653
12654    if let (Some(vm_path), Some(host_path)) = (vm_path, host_path.as_ref()) {
12655        if kernel
12656            .exists_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, vm_path)
12657            .map_err(kernel_error)?
12658        {
12659            let contents = kernel
12660                .read_file_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, vm_path)
12661                .map_err(kernel_error)?;
12662            fs::write(host_path, contents).map_err(|error| {
12663                SidecarError::Io(format!(
12664                    "failed to materialize sqlite database {}: {error}",
12665                    host_path.display()
12666                ))
12667            })?;
12668        } else if read_only && !create {
12669            return Err(SidecarError::InvalidState(format!(
12670                "sqlite database does not exist: {vm_path}"
12671            )));
12672        }
12673    }
12674
12675    let target = host_path
12676        .as_ref()
12677        .map(|path| path.to_string_lossy().into_owned())
12678        .unwrap_or_else(|| String::from(":memory:"));
12679    let mut flags = if read_only {
12680        SqliteOpenFlags::SQLITE_OPEN_READ_ONLY
12681    } else {
12682        SqliteOpenFlags::SQLITE_OPEN_READ_WRITE
12683    };
12684    if create && !read_only {
12685        flags |= SqliteOpenFlags::SQLITE_OPEN_CREATE;
12686    }
12687
12688    let connection = SqliteConnection::open_with_flags(&target, flags).map_err(|error| {
12689        SidecarError::InvalidState(format!(
12690            "sqlite database open failed for {}: {error}",
12691            vm_path.unwrap_or(":memory:")
12692        ))
12693    })?;
12694    if let Some(timeout_ms) = timeout_ms {
12695        connection
12696            .busy_timeout(Duration::from_millis(timeout_ms))
12697            .map_err(sqlite_error)?;
12698    }
12699    if host_path.is_some() && !read_only {
12700        let _ = connection.pragma_update(None, "journal_mode", "WAL");
12701    }
12702
12703    process.sqlite_databases.insert(
12704        database_id,
12705        ActiveSqliteDatabase {
12706            connection,
12707            host_path,
12708            vm_path: vm_path.map(String::from),
12709            dirty: false,
12710            transaction_depth: 0,
12711            read_only,
12712        },
12713    );
12714
12715    Ok(json!(database_id))
12716}
12717
12718fn sqlite_exec_database(
12719    kernel: &mut SidecarKernel,
12720    process: &mut ActiveProcess,
12721    request: &JavascriptSyncRpcRequest,
12722) -> Result<Value, SidecarError> {
12723    let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.exec database id")?;
12724    let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.exec sql")?;
12725    let kernel_pid = process.kernel_pid;
12726    let database = sqlite_database_mut(process, database_id)?;
12727    let before = database.connection.total_changes();
12728    database
12729        .connection
12730        .execute_batch(sql)
12731        .map_err(sqlite_error)?;
12732    mark_sqlite_mutation(database, sql);
12733    sqlite_sync_database(kernel, kernel_pid, database)?;
12734    Ok(json!(database
12735        .connection
12736        .total_changes()
12737        .saturating_sub(before)))
12738}
12739
12740fn sqlite_query_database(
12741    process: &mut ActiveProcess,
12742    request: &JavascriptSyncRpcRequest,
12743) -> Result<Value, SidecarError> {
12744    let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.query database id")?;
12745    let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.query sql")?;
12746    let params = request.args.get(2);
12747    let options = request.args.get(3);
12748    let return_arrays = sqlite_option_bool(options, "returnArrays").unwrap_or(false);
12749    let read_bigints = sqlite_option_bool(options, "readBigInts").unwrap_or(false);
12750    let database = sqlite_database_mut(process, database_id)?;
12751    sqlite_query_rows(
12752        &mut database.connection,
12753        sql,
12754        params,
12755        return_arrays,
12756        read_bigints,
12757        true,
12758        false,
12759    )
12760}
12761
12762fn sqlite_prepare_statement(
12763    process: &mut ActiveProcess,
12764    request: &JavascriptSyncRpcRequest,
12765) -> Result<Value, SidecarError> {
12766    ensure_per_process_state_handle_capacity(process.sqlite_statements.len(), "sqlite statement")?;
12767    let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.prepare database id")?;
12768    let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.prepare sql")?;
12769    let _ = sqlite_database(process, database_id)?;
12770    process.next_sqlite_statement_id += 1;
12771    let statement_id = process.next_sqlite_statement_id;
12772    process.sqlite_statements.insert(
12773        statement_id,
12774        ActiveSqliteStatement {
12775            database_id,
12776            sql: sql.to_owned(),
12777            return_arrays: false,
12778            read_bigints: false,
12779            allow_bare_named_parameters: false,
12780            allow_unknown_named_parameters: false,
12781        },
12782    );
12783    Ok(json!(statement_id))
12784}
12785
12786fn sqlite_run_statement(
12787    kernel: &mut SidecarKernel,
12788    process: &mut ActiveProcess,
12789    request: &JavascriptSyncRpcRequest,
12790) -> Result<Value, SidecarError> {
12791    let statement_id =
12792        javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.run statement id")?;
12793    let params = request.args.get(1);
12794    let statement_state = sqlite_statement(process, statement_id)?.clone();
12795    let kernel_pid = process.kernel_pid;
12796    let database = sqlite_database_mut(process, statement_state.database_id)?;
12797    let before = database.connection.total_changes();
12798    {
12799        let mut statement = database
12800            .connection
12801            .prepare(&statement_state.sql)
12802            .map_err(sqlite_error)?;
12803        bind_sqlite_parameters(
12804            &mut statement,
12805            params,
12806            statement_state.allow_bare_named_parameters,
12807            statement_state.allow_unknown_named_parameters,
12808        )?;
12809        statement.raw_execute().map_err(sqlite_error)?;
12810    }
12811    let changes = database.connection.total_changes().saturating_sub(before);
12812    let last_insert_rowid = database.connection.last_insert_rowid();
12813    mark_sqlite_mutation(database, &statement_state.sql);
12814    sqlite_sync_database(kernel, kernel_pid, database)?;
12815    let result = json!({
12816        "changes": changes,
12817        "lastInsertRowid": encode_sqlite_integer(last_insert_rowid, true),
12818    });
12819    Ok(result)
12820}
12821
12822fn sqlite_get_statement(
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.get statement id")?;
12828    let params = request.args.get(1);
12829    let statement_state = sqlite_statement(process, statement_id)?.clone();
12830    let database = sqlite_database_mut(process, statement_state.database_id)?;
12831    let rows = sqlite_query_rows(
12832        &mut database.connection,
12833        &statement_state.sql,
12834        params,
12835        statement_state.return_arrays,
12836        statement_state.read_bigints,
12837        statement_state.allow_bare_named_parameters,
12838        statement_state.allow_unknown_named_parameters,
12839    )?;
12840    Ok(rows
12841        .as_array()
12842        .and_then(|rows| rows.first().cloned())
12843        .unwrap_or(Value::Null))
12844}
12845
12846fn sqlite_all_statement(
12847    process: &mut ActiveProcess,
12848    request: &JavascriptSyncRpcRequest,
12849) -> Result<Value, SidecarError> {
12850    let statement_id =
12851        javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.all statement id")?;
12852    let params = request.args.get(1);
12853    let statement_state = sqlite_statement(process, statement_id)?.clone();
12854    let database = sqlite_database_mut(process, statement_state.database_id)?;
12855    sqlite_query_rows(
12856        &mut database.connection,
12857        &statement_state.sql,
12858        params,
12859        statement_state.return_arrays,
12860        statement_state.read_bigints,
12861        statement_state.allow_bare_named_parameters,
12862        statement_state.allow_unknown_named_parameters,
12863    )
12864}
12865
12866fn sqlite_statement_columns(
12867    process: &mut ActiveProcess,
12868    request: &JavascriptSyncRpcRequest,
12869) -> Result<Value, SidecarError> {
12870    let statement_id =
12871        javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.columns statement id")?;
12872    let statement_state = sqlite_statement(process, statement_id)?.clone();
12873    let database = sqlite_database_mut(process, statement_state.database_id)?;
12874    let statement = database
12875        .connection
12876        .prepare(&statement_state.sql)
12877        .map_err(sqlite_error)?;
12878    Ok(Value::Array(
12879        statement
12880            .column_names()
12881            .iter()
12882            .map(|name| json!({ "name": name }))
12883            .collect(),
12884    ))
12885}
12886
12887fn sqlite_query_rows(
12888    connection: &mut SqliteConnection,
12889    sql: &str,
12890    params: Option<&Value>,
12891    return_arrays: bool,
12892    read_bigints: bool,
12893    allow_bare_named_parameters: bool,
12894    allow_unknown_named_parameters: bool,
12895) -> Result<Value, SidecarError> {
12896    let mut statement = connection.prepare(sql).map_err(sqlite_error)?;
12897    let column_names = statement
12898        .column_names()
12899        .iter()
12900        .map(|name| (*name).to_owned())
12901        .collect::<Vec<_>>();
12902    let column_count = statement.column_count();
12903    bind_sqlite_parameters(
12904        &mut statement,
12905        params,
12906        allow_bare_named_parameters,
12907        allow_unknown_named_parameters,
12908    )?;
12909    let mut rows = statement.raw_query();
12910    let mut encoded_rows = Vec::new();
12911    while let Some(row) = rows.next().map_err(sqlite_error)? {
12912        encoded_rows.push(encode_sqlite_row(
12913            row,
12914            &column_names,
12915            column_count,
12916            return_arrays,
12917            read_bigints,
12918        )?);
12919    }
12920    Ok(Value::Array(encoded_rows))
12921}
12922
12923fn encode_sqlite_row(
12924    row: &rusqlite::Row<'_>,
12925    column_names: &[String],
12926    column_count: usize,
12927    return_arrays: bool,
12928    read_bigints: bool,
12929) -> Result<Value, SidecarError> {
12930    if return_arrays {
12931        let mut values = Vec::with_capacity(column_count);
12932        for index in 0..column_count {
12933            values.push(encode_sqlite_value_ref(
12934                row.get_ref(index).map_err(sqlite_error)?,
12935                read_bigints,
12936            )?);
12937        }
12938        return Ok(Value::Array(values));
12939    }
12940
12941    let mut object = Map::with_capacity(column_count);
12942    for (index, name) in column_names.iter().enumerate() {
12943        object.insert(
12944            name.clone(),
12945            encode_sqlite_value_ref(row.get_ref(index).map_err(sqlite_error)?, read_bigints)?,
12946        );
12947    }
12948    Ok(Value::Object(object))
12949}
12950
12951fn encode_sqlite_value_ref(
12952    value: SqliteValueRef<'_>,
12953    read_bigints: bool,
12954) -> Result<Value, SidecarError> {
12955    Ok(match value {
12956        SqliteValueRef::Null => Value::Null,
12957        SqliteValueRef::Integer(number) => encode_sqlite_integer(number, read_bigints),
12958        SqliteValueRef::Real(number) => json!(number),
12959        SqliteValueRef::Text(text) => Value::String(String::from_utf8_lossy(text).into_owned()),
12960        SqliteValueRef::Blob(bytes) => json!({
12961            "__agentosSqliteType": "uint8array",
12962            "value": base64::engine::general_purpose::STANDARD.encode(bytes),
12963        }),
12964    })
12965}
12966
12967fn encode_sqlite_integer(number: i64, read_bigints: bool) -> Value {
12968    if read_bigints || number.abs() > SQLITE_JS_SAFE_INTEGER_MAX {
12969        json!({
12970            "__agentosSqliteType": "bigint",
12971            "value": number.to_string(),
12972        })
12973    } else {
12974        json!(number)
12975    }
12976}
12977
12978fn bind_sqlite_parameters(
12979    statement: &mut SqliteStatement<'_>,
12980    params: Option<&Value>,
12981    allow_bare_named_parameters: bool,
12982    allow_unknown_named_parameters: bool,
12983) -> Result<(), SidecarError> {
12984    let Some(params) = params else {
12985        return Ok(());
12986    };
12987    match params {
12988        Value::Null => Ok(()),
12989        Value::Array(values) => {
12990            for (index, value) in values.iter().enumerate() {
12991                statement
12992                    .raw_bind_parameter(index + 1, decode_sqlite_parameter(value)?)
12993                    .map_err(sqlite_error)?;
12994            }
12995            Ok(())
12996        }
12997        Value::Object(map)
12998            if map
12999                .get("__agentosSqliteType")
13000                .and_then(Value::as_str)
13001                .is_none() =>
13002        {
13003            for (key, value) in map {
13004                let index =
13005                    resolve_sqlite_parameter_index(statement, key, allow_bare_named_parameters)?;
13006                let Some(index) = index else {
13007                    if allow_unknown_named_parameters {
13008                        continue;
13009                    }
13010                    return Err(SidecarError::InvalidState(format!(
13011                        "sqlite named parameter not found: {key}"
13012                    )));
13013                };
13014                statement
13015                    .raw_bind_parameter(index, decode_sqlite_parameter(value)?)
13016                    .map_err(sqlite_error)?;
13017            }
13018            Ok(())
13019        }
13020        other => statement
13021            .raw_bind_parameter(1, decode_sqlite_parameter(other)?)
13022            .map_err(sqlite_error),
13023    }
13024}
13025
13026fn resolve_sqlite_parameter_index(
13027    statement: &mut SqliteStatement<'_>,
13028    key: &str,
13029    allow_bare_named_parameters: bool,
13030) -> Result<Option<usize>, SidecarError> {
13031    let mut candidates = vec![key.to_owned()];
13032    if allow_bare_named_parameters
13033        && !key.starts_with(':')
13034        && !key.starts_with('@')
13035        && !key.starts_with('$')
13036    {
13037        candidates.push(format!(":{key}"));
13038        candidates.push(format!("@{key}"));
13039        candidates.push(format!("${key}"));
13040    }
13041    for candidate in candidates {
13042        if let Some(index) = statement
13043            .parameter_index(&candidate)
13044            .map_err(sqlite_error)?
13045        {
13046            return Ok(Some(index));
13047        }
13048    }
13049    Ok(None)
13050}
13051
13052fn decode_sqlite_parameter(value: &Value) -> Result<rusqlite::types::Value, SidecarError> {
13053    Ok(match value {
13054        Value::Null => rusqlite::types::Value::Null,
13055        Value::Bool(value) => rusqlite::types::Value::Integer(i64::from(*value)),
13056        Value::Number(value) => match (value.as_i64(), value.as_f64()) {
13057            (Some(integer), _) => rusqlite::types::Value::Integer(integer),
13058            (_, Some(real)) => rusqlite::types::Value::Real(real),
13059            _ => {
13060                return Err(SidecarError::InvalidState(String::from(
13061                    "sqlite parameter number is not representable",
13062                )));
13063            }
13064        },
13065        Value::String(value) => rusqlite::types::Value::Text(value.clone()),
13066        Value::Array(_) => {
13067            return Err(SidecarError::InvalidState(String::from(
13068                "sqlite parameters do not support nested arrays",
13069            )));
13070        }
13071        Value::Object(map) => match map.get("__agentosSqliteType").and_then(Value::as_str) {
13072            Some("bigint") => rusqlite::types::Value::Integer(
13073                map.get("value")
13074                    .and_then(Value::as_str)
13075                    .ok_or_else(|| {
13076                        SidecarError::InvalidState(String::from(
13077                            "sqlite bigint parameter missing string value",
13078                        ))
13079                    })?
13080                    .parse::<i64>()
13081                    .map_err(|error| {
13082                        SidecarError::InvalidState(format!(
13083                            "sqlite bigint parameter is not a signed 64-bit integer: {error}"
13084                        ))
13085                    })?,
13086            ),
13087            Some("uint8array") => rusqlite::types::Value::Blob(
13088                base64::engine::general_purpose::STANDARD
13089                    .decode(map.get("value").and_then(Value::as_str).ok_or_else(|| {
13090                        SidecarError::InvalidState(String::from(
13091                            "sqlite blob parameter missing base64 value",
13092                        ))
13093                    })?)
13094                    .map_err(|error| {
13095                        SidecarError::InvalidState(format!(
13096                            "sqlite blob parameter contains invalid base64: {error}"
13097                        ))
13098                    })?,
13099            ),
13100            Some(other) => {
13101                return Err(SidecarError::InvalidState(format!(
13102                    "unsupported sqlite tagged parameter type {other}"
13103                )));
13104            }
13105            None => {
13106                return Err(SidecarError::InvalidState(String::from(
13107                    "sqlite named parameter objects must be passed as the top-level params object",
13108                )));
13109            }
13110        },
13111    })
13112}
13113
13114fn close_sqlite_database(
13115    kernel: &mut SidecarKernel,
13116    process: &mut ActiveProcess,
13117    database_id: u64,
13118) -> Result<(), SidecarError> {
13119    let mut database = process
13120        .sqlite_databases
13121        .remove(&database_id)
13122        .ok_or_else(|| {
13123            SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
13124        })?;
13125    process
13126        .sqlite_statements
13127        .retain(|_, statement| statement.database_id != database_id);
13128    sqlite_sync_database(kernel, process.kernel_pid, &mut database)?;
13129    let host_path = database.host_path.clone();
13130    drop(database);
13131    cleanup_sqlite_host_artifacts(host_path.as_deref())?;
13132    Ok(())
13133}
13134
13135fn ensure_per_process_state_handle_capacity(len: usize, label: &str) -> Result<(), SidecarError> {
13136    if len >= MAX_PER_PROCESS_STATE_HANDLES {
13137        return Err(SidecarError::InvalidState(format!(
13138            "{label} handle limit exceeded: limit is {MAX_PER_PROCESS_STATE_HANDLES}"
13139        )));
13140    }
13141    Ok(())
13142}
13143
13144fn sqlite_sync_database(
13145    kernel: &mut SidecarKernel,
13146    kernel_pid: u32,
13147    database: &mut ActiveSqliteDatabase,
13148) -> Result<(), SidecarError> {
13149    if !database.dirty
13150        || database.transaction_depth > 0
13151        || database.read_only
13152        || database.host_path.is_none()
13153        || database.vm_path.is_none()
13154    {
13155        return Ok(());
13156    }
13157
13158    let _ = database
13159        .connection
13160        .execute_batch("PRAGMA wal_checkpoint(TRUNCATE)");
13161    let host_path = database.host_path.as_ref().expect("sqlite host path");
13162    if !host_path.exists() {
13163        return Ok(());
13164    }
13165    ensure_vm_parent_dir(
13166        kernel,
13167        kernel_pid,
13168        database.vm_path.as_deref().expect("sqlite vm path"),
13169    )?;
13170    let contents = fs::read(host_path).map_err(|error| {
13171        SidecarError::Io(format!(
13172            "failed to read sqlite temp database {}: {error}",
13173            host_path.display()
13174        ))
13175    })?;
13176    kernel
13177        .write_file_for_process(
13178            EXECUTION_DRIVER_NAME,
13179            kernel_pid,
13180            database.vm_path.as_deref().expect("sqlite vm path"),
13181            contents,
13182            None,
13183        )
13184        .map_err(kernel_error)?;
13185    database.dirty = false;
13186    Ok(())
13187}
13188
13189fn cleanup_sqlite_host_artifacts(host_path: Option<&Path>) -> Result<(), SidecarError> {
13190    let Some(host_path) = host_path else {
13191        return Ok(());
13192    };
13193    let parent = host_path.parent().map(PathBuf::from);
13194    for suffix in ["", "-wal", "-shm"] {
13195        let path = PathBuf::from(format!("{}{}", host_path.display(), suffix));
13196        if path.exists() {
13197            fs::remove_file(&path).map_err(|error| {
13198                SidecarError::Io(format!(
13199                    "failed to remove sqlite temp artifact {}: {error}",
13200                    path.display()
13201                ))
13202            })?;
13203        }
13204    }
13205    if let Some(parent) = parent {
13206        let _ = fs::remove_dir_all(parent);
13207    }
13208    Ok(())
13209}
13210
13211fn ensure_vm_parent_dir(
13212    kernel: &mut SidecarKernel,
13213    kernel_pid: u32,
13214    path: &str,
13215) -> Result<(), SidecarError> {
13216    let parent = dirname(path);
13217    if parent == "/" || parent == "." {
13218        return Ok(());
13219    }
13220    let mut current = String::new();
13221    for segment in parent.split('/').filter(|segment| !segment.is_empty()) {
13222        current.push('/');
13223        current.push_str(segment);
13224        if !kernel
13225            .exists_for_process(EXECUTION_DRIVER_NAME, kernel_pid, &current)
13226            .map_err(kernel_error)?
13227        {
13228            kernel
13229                .mkdir_for_process(EXECUTION_DRIVER_NAME, kernel_pid, &current, false, None)
13230                .map_err(kernel_error)?;
13231        }
13232    }
13233    Ok(())
13234}
13235
13236fn sqlite_database(
13237    process: &ActiveProcess,
13238    database_id: u64,
13239) -> Result<&ActiveSqliteDatabase, SidecarError> {
13240    process.sqlite_databases.get(&database_id).ok_or_else(|| {
13241        SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
13242    })
13243}
13244
13245fn sqlite_database_mut(
13246    process: &mut ActiveProcess,
13247    database_id: u64,
13248) -> Result<&mut ActiveSqliteDatabase, SidecarError> {
13249    process
13250        .sqlite_databases
13251        .get_mut(&database_id)
13252        .ok_or_else(|| {
13253            SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
13254        })
13255}
13256
13257fn sqlite_statement(
13258    process: &ActiveProcess,
13259    statement_id: u64,
13260) -> Result<&ActiveSqliteStatement, SidecarError> {
13261    process.sqlite_statements.get(&statement_id).ok_or_else(|| {
13262        SidecarError::InvalidState(format!("sqlite statement handle not found: {statement_id}"))
13263    })
13264}
13265
13266fn sqlite_statement_mut(
13267    process: &mut ActiveProcess,
13268    statement_id: u64,
13269) -> Result<&mut ActiveSqliteStatement, SidecarError> {
13270    process
13271        .sqlite_statements
13272        .get_mut(&statement_id)
13273        .ok_or_else(|| {
13274            SidecarError::InvalidState(format!("sqlite statement handle not found: {statement_id}"))
13275        })
13276}
13277
13278fn mark_sqlite_mutation(database: &mut ActiveSqliteDatabase, sql: &str) {
13279    let normalized = sql.trim_start().to_ascii_lowercase();
13280    if normalized.starts_with("begin") || normalized.starts_with("savepoint") {
13281        database.dirty = true;
13282        database.transaction_depth += 1;
13283        return;
13284    }
13285    if normalized.starts_with("commit") || normalized.starts_with("release savepoint") {
13286        database.dirty = true;
13287        database.transaction_depth = database.transaction_depth.saturating_sub(1);
13288        return;
13289    }
13290    if normalized.starts_with("rollback") && !normalized.starts_with("rollback to") {
13291        database.dirty = true;
13292        database.transaction_depth = database.transaction_depth.saturating_sub(1);
13293        return;
13294    }
13295    if normalized.starts_with("insert")
13296        || normalized.starts_with("update")
13297        || normalized.starts_with("delete")
13298        || normalized.starts_with("replace")
13299        || normalized.starts_with("create")
13300        || normalized.starts_with("alter")
13301        || normalized.starts_with("drop")
13302        || normalized.starts_with("vacuum")
13303        || normalized.starts_with("reindex")
13304        || normalized.starts_with("analyze")
13305        || normalized.starts_with("attach")
13306        || normalized.starts_with("detach")
13307        || normalized.starts_with("pragma")
13308    {
13309        database.dirty = true;
13310    }
13311}
13312
13313fn sqlite_option_bool(options: Option<&Value>, key: &str) -> Option<bool> {
13314    options
13315        .and_then(|value| value.get(key))
13316        .and_then(Value::as_bool)
13317}
13318
13319fn sqlite_option_u64(options: Option<&Value>, key: &str) -> Option<u64> {
13320    options
13321        .and_then(|value| value.get(key))
13322        .and_then(Value::as_u64)
13323}
13324
13325fn sqlite_error(error: rusqlite::Error) -> SidecarError {
13326    SidecarError::InvalidState(format!("sqlite error: {error}"))
13327}
13328
13329pub(crate) fn javascript_sync_rpc_arg_str<'a>(
13330    args: &'a [Value],
13331    index: usize,
13332    label: &str,
13333) -> Result<&'a str, SidecarError> {
13334    args.get(index)
13335        .and_then(Value::as_str)
13336        .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a string argument")))
13337}
13338
13339pub(crate) fn javascript_sync_rpc_arg_bool(
13340    args: &[Value],
13341    index: usize,
13342    label: &str,
13343) -> Result<bool, SidecarError> {
13344    args.get(index)
13345        .and_then(Value::as_bool)
13346        .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a boolean argument")))
13347}
13348
13349pub(crate) fn javascript_sync_rpc_encoding(args: &[Value]) -> Option<String> {
13350    args.get(1).and_then(|value| {
13351        value.as_str().map(str::to_owned).or_else(|| {
13352            value
13353                .get("encoding")
13354                .and_then(Value::as_str)
13355                .map(str::to_owned)
13356        })
13357    })
13358}
13359
13360pub(crate) fn javascript_sync_rpc_option_bool(
13361    args: &[Value],
13362    index: usize,
13363    key: &str,
13364) -> Option<bool> {
13365    let value = args.get(index)?;
13366    if key == "recursive" {
13367        if let Some(boolean) = value.as_bool() {
13368            return Some(boolean);
13369        }
13370    }
13371    value.get(key).and_then(Value::as_bool)
13372}
13373
13374pub(crate) fn javascript_sync_rpc_option_u32(
13375    args: &[Value],
13376    index: usize,
13377    key: &str,
13378) -> Result<Option<u32>, SidecarError> {
13379    let Some(value) = args.get(index).and_then(|value| {
13380        if value.is_object() {
13381            value.get(key)
13382        } else if key == "mode" && value.is_number() {
13383            Some(value)
13384        } else {
13385            None
13386        }
13387    }) else {
13388        return Ok(None);
13389    };
13390    if value.is_null() {
13391        return Ok(None);
13392    }
13393
13394    let numeric = value
13395        .as_u64()
13396        .or_else(|| {
13397            value
13398                .as_f64()
13399                .filter(|number| number.is_finite() && *number >= 0.0)
13400                .map(|number| number as u64)
13401        })
13402        .ok_or_else(|| SidecarError::InvalidState(format!("{key} must be numeric")))?;
13403
13404    u32::try_from(numeric)
13405        .map(Some)
13406        .map_err(|_| SidecarError::InvalidState(format!("{key} must fit within u32")))
13407}
13408
13409pub(crate) fn javascript_sync_rpc_arg_u32(
13410    args: &[Value],
13411    index: usize,
13412    label: &str,
13413) -> Result<u32, SidecarError> {
13414    let value = javascript_sync_rpc_arg_u64(args, index, label)?;
13415    u32::try_from(value)
13416        .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")))
13417}
13418
13419pub(crate) fn javascript_sync_rpc_arg_i32(
13420    args: &[Value],
13421    index: usize,
13422    label: &str,
13423) -> Result<i32, SidecarError> {
13424    let Some(value) = args.get(index) else {
13425        return Err(SidecarError::InvalidState(format!("{label} is required")));
13426    };
13427
13428    let numeric = value
13429        .as_i64()
13430        .or_else(|| {
13431            value
13432                .as_f64()
13433                .filter(|number| number.is_finite())
13434                .map(|number| number as i64)
13435        })
13436        .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a numeric argument")))?;
13437
13438    i32::try_from(numeric)
13439        .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within i32")))
13440}
13441
13442pub(crate) fn javascript_sync_rpc_arg_u32_optional(
13443    args: &[Value],
13444    index: usize,
13445    label: &str,
13446) -> Result<Option<u32>, SidecarError> {
13447    javascript_sync_rpc_arg_u64_optional(args, index, label)?
13448        .map(|value| {
13449            u32::try_from(value)
13450                .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")))
13451        })
13452        .transpose()
13453}
13454
13455pub(crate) fn javascript_sync_rpc_arg_u64(
13456    args: &[Value],
13457    index: usize,
13458    label: &str,
13459) -> Result<u64, SidecarError> {
13460    let Some(value) = args.get(index) else {
13461        return Err(SidecarError::InvalidState(format!("{label} is required")));
13462    };
13463
13464    value
13465        .as_u64()
13466        .or_else(|| {
13467            value
13468                .as_f64()
13469                .filter(|number| number.is_finite() && *number >= 0.0)
13470                .map(|number| number as u64)
13471        })
13472        .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a numeric argument")))
13473}
13474
13475pub(crate) fn javascript_sync_rpc_arg_u64_optional(
13476    args: &[Value],
13477    index: usize,
13478    label: &str,
13479) -> Result<Option<u64>, SidecarError> {
13480    let Some(value) = args.get(index) else {
13481        return Ok(None);
13482    };
13483    if value.is_null() {
13484        return Ok(None);
13485    }
13486    javascript_sync_rpc_arg_u64(args, index, label).map(Some)
13487}
13488
13489pub(crate) fn javascript_sync_rpc_bytes_arg(
13490    args: &[Value],
13491    index: usize,
13492    label: &str,
13493) -> Result<Vec<u8>, SidecarError> {
13494    let Some(value) = args.get(index) else {
13495        return Err(SidecarError::InvalidState(format!("{label} is required")));
13496    };
13497
13498    if let Some(text) = value.as_str() {
13499        return Ok(text.as_bytes().to_vec());
13500    }
13501
13502    let Some(base64_value) = value
13503        .get("__agentOsType")
13504        .and_then(Value::as_str)
13505        .filter(|kind| *kind == "bytes")
13506        .and_then(|_| value.get("base64"))
13507        .and_then(Value::as_str)
13508    else {
13509        return Err(SidecarError::InvalidState(format!(
13510            "{label} must be a string or encoded bytes payload"
13511        )));
13512    };
13513
13514    base64::engine::general_purpose::STANDARD
13515        .decode(base64_value)
13516        .map_err(|error| {
13517            SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
13518        })
13519}
13520
13521pub(crate) fn javascript_sync_rpc_bytes_value(bytes: &[u8]) -> Value {
13522    json!({
13523        "__agentOsType": "bytes",
13524        "base64": base64::engine::general_purpose::STANDARD.encode(bytes),
13525    })
13526}
13527
13528#[derive(Debug, Deserialize)]
13529struct KernelPollFdRequest {
13530    fd: u32,
13531    events: u16,
13532}
13533
13534#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
13535struct KernelPollFdResponse {
13536    fd: u32,
13537    events: u16,
13538    revents: u16,
13539}
13540
13541fn javascript_sync_rpc_base64_arg(
13542    args: &[Value],
13543    index: usize,
13544    label: &str,
13545) -> Result<Vec<u8>, SidecarError> {
13546    let value = javascript_sync_rpc_arg_str(args, index, label)?;
13547    base64::engine::general_purpose::STANDARD
13548        .decode(value)
13549        .map_err(|error| {
13550            SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
13551        })
13552}
13553
13554pub(crate) fn service_javascript_sync_rpc<B>(
13555    request: JavascriptSyncRpcServiceRequest<'_, B>,
13556) -> Result<Value, SidecarError>
13557where
13558    B: NativeSidecarBridge + Send + 'static,
13559    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
13560{
13561    let JavascriptSyncRpcServiceRequest {
13562        bridge,
13563        vm_id,
13564        dns,
13565        socket_paths,
13566        kernel,
13567        process,
13568        sync_request: request,
13569        resource_limits,
13570        network_counts,
13571    } = request;
13572    match request.method.as_str() {
13573        // Module resolution / loading / format detection read the kernel VFS so
13574        // the resolver sees exactly what the guest and `kernel.readFile()` see.
13575        "_resolveModule"
13576        | "_resolveModuleSync"
13577        | "__resolve_module"
13578        | "_batchResolveModules"
13579        | "__batch_resolve_modules"
13580        | "_loadFile"
13581        | "_loadFileSync"
13582        | "__load_file"
13583        | "_moduleFormat"
13584        | "__module_format" => service_javascript_module_sync_rpc(kernel, process, request),
13585        // Polyfills are static guest expressions, not VFS reads.
13586        "_loadPolyfill" | "__load_polyfill" => {
13587            service_javascript_internal_bridge_sync_rpc(process, request)
13588        }
13589        "__kernel_stdin_read" => match &process.execution {
13590            ActiveExecution::Javascript(execution) => execution
13591                .read_kernel_stdin_sync_rpc(request)
13592                .map_err(|error| SidecarError::Execution(error.to_string())),
13593            ActiveExecution::Python(_) | ActiveExecution::Wasm(_) | ActiveExecution::Tool(_) => {
13594                service_javascript_kernel_stdin_sync_rpc(kernel, process, request)
13595            }
13596        },
13597        "__kernel_stdio_write" => {
13598            service_javascript_kernel_stdio_write_sync_rpc(kernel, process, request)
13599        }
13600        "__kernel_poll" => service_javascript_kernel_poll_sync_rpc(kernel, process, request),
13601        "__pty_set_raw_mode" => {
13602            service_javascript_pty_set_raw_mode_sync_rpc(kernel, process, request)
13603        }
13604        "crypto.hashDigest"
13605        | "crypto.hmacDigest"
13606        | "crypto.pbkdf2"
13607        | "crypto.scrypt"
13608        | "crypto.cipheriv"
13609        | "crypto.decipheriv"
13610        | "crypto.cipherivCreate"
13611        | "crypto.cipherivUpdate"
13612        | "crypto.cipherivFinal"
13613        | "crypto.sign"
13614        | "crypto.verify"
13615        | "crypto.asymmetricOp"
13616        | "crypto.createKeyObject"
13617        | "crypto.generateKeyPairSync"
13618        | "crypto.generateKeySync"
13619        | "crypto.generatePrimeSync"
13620        | "crypto.diffieHellman"
13621        | "crypto.diffieHellmanGroup"
13622        | "crypto.diffieHellmanSessionCreate"
13623        | "crypto.diffieHellmanSessionCall"
13624        | "crypto.diffieHellmanSessionDestroy"
13625        | "crypto.subtle" => service_javascript_crypto_sync_rpc(process, request),
13626        "dns.lookup" | "dns.resolve" | "dns.resolve4" | "dns.resolve6" => {
13627            service_javascript_dns_sync_rpc(bridge, kernel, vm_id, dns, request)
13628        }
13629        "net.http_listen" | "net.http_close" | "net.http_wait" | "net.http_respond" => {
13630            service_javascript_net_sync_rpc(JavascriptNetSyncRpcServiceRequest {
13631                bridge,
13632                vm_id,
13633                dns,
13634                socket_paths,
13635                kernel,
13636                process,
13637                sync_request: request,
13638                resource_limits,
13639                network_counts,
13640            })
13641        }
13642        "net.http2_server_listen"
13643        | "net.http2_server_poll"
13644        | "net.http2_server_close"
13645        | "net.http2_server_respond"
13646        | "net.http2_server_wait"
13647        | "net.http2_session_connect"
13648        | "net.http2_session_request"
13649        | "net.http2_session_settings"
13650        | "net.http2_session_set_local_window_size"
13651        | "net.http2_session_goaway"
13652        | "net.http2_session_close"
13653        | "net.http2_session_destroy"
13654        | "net.http2_session_poll"
13655        | "net.http2_session_wait"
13656        | "net.http2_stream_respond"
13657        | "net.http2_stream_push_stream"
13658        | "net.http2_stream_write"
13659        | "net.http2_stream_end"
13660        | "net.http2_stream_close"
13661        | "net.http2_stream_pause"
13662        | "net.http2_stream_resume"
13663        | "net.http2_stream_respond_with_file" => {
13664            service_javascript_http2_sync_rpc(JavascriptHttp2SyncRpcServiceRequest {
13665                bridge,
13666                kernel,
13667                vm_id,
13668                dns,
13669                socket_paths,
13670                process,
13671                sync_request: request,
13672                resource_limits,
13673                network_counts,
13674            })
13675        }
13676        "net.connect"
13677        | "net.reserve_tcp_port"
13678        | "net.release_tcp_port"
13679        | "net.listen"
13680        | "net.poll"
13681        | "net.socket_wait_connect"
13682        | "net.socket_read"
13683        | "net.socket_set_no_delay"
13684        | "net.socket_set_keep_alive"
13685        | "net.socket_upgrade_tls"
13686        | "net.socket_get_tls_client_hello"
13687        | "net.socket_tls_query"
13688        | "net.server_poll"
13689        | "net.server_accept"
13690        | "net.server_connections"
13691        | "net.upgrade_socket_write"
13692        | "net.upgrade_socket_end"
13693        | "net.upgrade_socket_destroy"
13694        | "net.write"
13695        | "net.shutdown"
13696        | "net.destroy"
13697        | "net.server_close"
13698        | "tls.get_ciphers" => {
13699            service_javascript_net_sync_rpc(JavascriptNetSyncRpcServiceRequest {
13700                bridge,
13701                vm_id,
13702                dns,
13703                socket_paths,
13704                kernel,
13705                process,
13706                sync_request: request,
13707                resource_limits,
13708                network_counts,
13709            })
13710        }
13711        "dgram.createSocket"
13712        | "dgram.bind"
13713        | "dgram.send"
13714        | "dgram.poll"
13715        | "dgram.close"
13716        | "dgram.address"
13717        | "dgram.setBufferSize"
13718        | "dgram.getBufferSize" => {
13719            service_javascript_dgram_sync_rpc(JavascriptDgramSyncRpcServiceRequest {
13720                bridge,
13721                kernel,
13722                vm_id,
13723                dns,
13724                socket_paths,
13725                process,
13726                sync_request: request,
13727                resource_limits,
13728                network_counts,
13729            })
13730        }
13731        "sqlite.constants"
13732        | "sqlite.open"
13733        | "sqlite.close"
13734        | "sqlite.exec"
13735        | "sqlite.query"
13736        | "sqlite.prepare"
13737        | "sqlite.location"
13738        | "sqlite.checkpoint"
13739        | "sqlite.statement.run"
13740        | "sqlite.statement.get"
13741        | "sqlite.statement.all"
13742        | "sqlite.statement.iterate"
13743        | "sqlite.statement.columns"
13744        | "sqlite.statement.setReturnArrays"
13745        | "sqlite.statement.setReadBigInts"
13746        | "sqlite.statement.setAllowBareNamedParameters"
13747        | "sqlite.statement.setAllowUnknownNamedParameters"
13748        | "sqlite.statement.finalize" => {
13749            service_javascript_sqlite_sync_rpc(kernel, process, request)
13750        }
13751        "process.kill" => {
13752            let target_pid =
13753                javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?;
13754            let signal = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?;
13755            let parsed_signal = parse_signal(signal)?;
13756            if parsed_signal == 0 {
13757                kernel
13758                    .signal_process(EXECUTION_DRIVER_NAME, target_pid, parsed_signal)
13759                    .map_err(kernel_error)?;
13760                return Ok(Value::Null);
13761            }
13762            let process_pid = i32::try_from(process.kernel_pid)
13763                .map_err(|_| SidecarError::InvalidState("process pid exceeds i32".into()))?;
13764            if target_pid != process_pid {
13765                return Err(SidecarError::InvalidState(format!(
13766                    "unknown process pid {target_pid}"
13767                )));
13768            }
13769            process.pending_self_signal_exit = None;
13770            if parsed_signal != 0
13771                && !matches!(
13772                    canonical_signal_name(parsed_signal),
13773                    Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
13774                )
13775            {
13776                process.pending_self_signal_exit = Some(parsed_signal);
13777            }
13778            Ok(json!({
13779                "self": true,
13780                "action": "default",
13781            }))
13782        }
13783        "process.umask" => {
13784            let new_mask = javascript_sync_rpc_arg_u32_optional(&request.args, 0, "process umask")?;
13785            kernel
13786                .umask(EXECUTION_DRIVER_NAME, process.kernel_pid, new_mask)
13787                .map(|mask| json!(mask))
13788                .map_err(kernel_error)
13789        }
13790        "fs.chmodSync" | "fs.promises.chmod" => {
13791            let response =
13792                service_javascript_fs_sync_rpc(kernel, process, process.kernel_pid, request)?;
13793            mirror_process_chmod_to_host(process, request)?;
13794            Ok(response)
13795        }
13796        _ => service_javascript_fs_sync_rpc(kernel, process, process.kernel_pid, request),
13797    }
13798}
13799
13800fn service_javascript_internal_bridge_sync_rpc(
13801    process: &ActiveProcess,
13802    request: &JavascriptSyncRpcRequest,
13803) -> Result<Value, SidecarError> {
13804    // Module resolution / loading / format now reads the kernel VFS via
13805    // `service_javascript_module_sync_rpc`. This host-context path only handles
13806    // polyfills, which are static guest expressions independent of the FS.
13807    let method = match request.method.as_str() {
13808        "_loadPolyfill" | "__load_polyfill" => "_loadPolyfill",
13809        other => {
13810            return Err(SidecarError::InvalidState(format!(
13811                "unsupported JavaScript internal bridge method {other}"
13812            )));
13813        }
13814    };
13815
13816    handle_internal_bridge_call_from_host_context(
13817        &process.host_cwd,
13818        &process.guest_cwd,
13819        &process.env,
13820        method,
13821        &request.args,
13822    )
13823    .ok_or_else(|| {
13824        SidecarError::InvalidState(format!(
13825            "JavaScript internal bridge method {method} returned no value"
13826        ))
13827    })
13828}
13829
13830fn mirror_process_chmod_to_host(
13831    process: &ActiveProcess,
13832    request: &JavascriptSyncRpcRequest,
13833) -> Result<(), SidecarError> {
13834    let guest_path = javascript_sync_rpc_arg_str(&request.args, 0, "filesystem chmod path")?;
13835    let mode = javascript_sync_rpc_arg_u32(&request.args, 1, "filesystem chmod mode")? & 0o7777;
13836    let Some(host_path) = resolve_process_guest_path_to_host(process, guest_path) else {
13837        return Ok(());
13838    };
13839    if !host_path.exists() {
13840        return Ok(());
13841    }
13842    fs::set_permissions(&host_path, fs::Permissions::from_mode(mode)).map_err(|error| {
13843        SidecarError::Io(format!(
13844            "failed to mirror chmod to host path {}: {error}",
13845            host_path.display()
13846        ))
13847    })
13848}
13849
13850fn resolve_process_guest_path_to_host(
13851    process: &ActiveProcess,
13852    guest_path: &str,
13853) -> Option<PathBuf> {
13854    let normalized_guest_path = if guest_path.starts_with('/') {
13855        normalize_path(guest_path)
13856    } else {
13857        normalize_path(&format!(
13858            "{}/{}",
13859            process.guest_cwd.trim_end_matches('/'),
13860            guest_path
13861        ))
13862    };
13863    if let Some(host_path) =
13864        host_path_from_runtime_guest_mappings(&process.env, &normalized_guest_path)
13865    {
13866        return Some(host_path);
13867    }
13868    let normalized_guest_cwd = normalize_path(&process.guest_cwd);
13869    let mut host_root = normalize_host_path(&process.host_cwd);
13870    for _ in normalized_guest_cwd
13871        .trim_start_matches('/')
13872        .split('/')
13873        .filter(|segment| !segment.is_empty())
13874    {
13875        host_root = host_root.parent()?.to_path_buf();
13876    }
13877    if normalized_guest_path == "/" {
13878        Some(host_root)
13879    } else {
13880        Some(host_root.join(normalized_guest_path.trim_start_matches('/')))
13881    }
13882}
13883
13884pub(crate) fn service_javascript_crypto_sync_rpc(
13885    process: &mut ActiveProcess,
13886    request: &JavascriptSyncRpcRequest,
13887) -> Result<Value, SidecarError> {
13888    match request.method.as_str() {
13889        "crypto.hashDigest" => {
13890            let algorithm = javascript_crypto_digest_algorithm(
13891                &request.args,
13892                0,
13893                "crypto.hashDigest algorithm",
13894            )?;
13895            let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.hashDigest data")?;
13896            Ok(Value::String(
13897                base64::engine::general_purpose::STANDARD.encode(algorithm.digest(&data)),
13898            ))
13899        }
13900        "crypto.hmacDigest" => {
13901            let algorithm = javascript_crypto_digest_algorithm(
13902                &request.args,
13903                0,
13904                "crypto.hmacDigest algorithm",
13905            )?;
13906            let key = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.hmacDigest key")?;
13907            let data = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.hmacDigest data")?;
13908            Ok(Value::String(
13909                base64::engine::general_purpose::STANDARD.encode(algorithm.hmac(&key, &data)?),
13910            ))
13911        }
13912        "crypto.pbkdf2" => {
13913            let password =
13914                javascript_sync_rpc_base64_arg(&request.args, 0, "crypto.pbkdf2 password")?;
13915            let salt = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.pbkdf2 salt")?;
13916            let iterations =
13917                javascript_sync_rpc_arg_u32(&request.args, 2, "crypto.pbkdf2 iterations")?;
13918            if iterations == 0 {
13919                return Err(SidecarError::InvalidState(String::from(
13920                    "crypto.pbkdf2 iterations must be greater than zero",
13921                )));
13922            }
13923            let key_len = usize::try_from(javascript_sync_rpc_arg_u64(
13924                &request.args,
13925                3,
13926                "crypto.pbkdf2 key length",
13927            )?)
13928            .map_err(|_| {
13929                SidecarError::InvalidState(String::from(
13930                    "crypto.pbkdf2 key length must fit within usize",
13931                ))
13932            })?;
13933            let algorithm =
13934                javascript_crypto_digest_algorithm(&request.args, 4, "crypto.pbkdf2 digest")?;
13935            let mut output = vec![0u8; key_len];
13936            algorithm.pbkdf2(&password, &salt, iterations, &mut output);
13937            Ok(Value::String(
13938                base64::engine::general_purpose::STANDARD.encode(output),
13939            ))
13940        }
13941        "crypto.scrypt" => {
13942            let password =
13943                javascript_sync_rpc_base64_arg(&request.args, 0, "crypto.scrypt password")?;
13944            let salt = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.scrypt salt")?;
13945            let key_len = usize::try_from(javascript_sync_rpc_arg_u64(
13946                &request.args,
13947                2,
13948                "crypto.scrypt key length",
13949            )?)
13950            .map_err(|_| {
13951                SidecarError::InvalidState(String::from(
13952                    "crypto.scrypt key length must fit within usize",
13953                ))
13954            })?;
13955            let options_json =
13956                javascript_sync_rpc_arg_str(&request.args, 3, "crypto.scrypt options")?;
13957            let options: JavascriptScryptOptions =
13958                serde_json::from_str(options_json).map_err(|error| {
13959                    SidecarError::InvalidState(format!(
13960                        "crypto.scrypt options must be valid JSON: {error}"
13961                    ))
13962                })?;
13963            let cost = options.cost.unwrap_or(DEFAULT_SCRYPT_COST);
13964            if cost == 0 || !cost.is_power_of_two() {
13965                return Err(SidecarError::InvalidState(String::from(
13966                    "crypto.scrypt cost must be a positive power of two",
13967                )));
13968            }
13969            let log_n = u8::try_from(cost.ilog2()).map_err(|_| {
13970                SidecarError::InvalidState(String::from(
13971                    "crypto.scrypt cost exceeds supported parameter range",
13972                ))
13973            })?;
13974            let params = ScryptParams::new(
13975                log_n,
13976                options.block_size.unwrap_or(DEFAULT_SCRYPT_BLOCK_SIZE),
13977                options
13978                    .parallelization
13979                    .unwrap_or(DEFAULT_SCRYPT_PARALLELIZATION),
13980                key_len,
13981            )
13982            .map_err(|error| {
13983                SidecarError::InvalidState(format!("crypto.scrypt options are invalid: {error}"))
13984            })?;
13985            let mut output = vec![0u8; key_len];
13986            scrypt(&password, &salt, &params, &mut output).map_err(|error| {
13987                SidecarError::Execution(format!("crypto.scrypt failed: {error}"))
13988            })?;
13989            Ok(Value::String(
13990                base64::engine::general_purpose::STANDARD.encode(output),
13991            ))
13992        }
13993        "crypto.cipheriv" => service_javascript_crypto_cipheriv_sync_rpc(request),
13994        "crypto.decipheriv" => service_javascript_crypto_decipheriv_sync_rpc(request),
13995        "crypto.cipherivCreate" => {
13996            service_javascript_crypto_cipheriv_create_sync_rpc(process, request)
13997        }
13998        "crypto.cipherivUpdate" => {
13999            service_javascript_crypto_cipheriv_update_sync_rpc(process, request)
14000        }
14001        "crypto.cipherivFinal" => {
14002            service_javascript_crypto_cipheriv_final_sync_rpc(process, request)
14003        }
14004        "crypto.sign" => service_javascript_crypto_sign_sync_rpc(request),
14005        "crypto.verify" => service_javascript_crypto_verify_sync_rpc(request),
14006        "crypto.asymmetricOp" => service_javascript_crypto_asymmetric_op_sync_rpc(request),
14007        "crypto.createKeyObject" => service_javascript_crypto_create_key_object_sync_rpc(request),
14008        "crypto.generateKeyPairSync" => {
14009            service_javascript_crypto_generate_key_pair_sync_rpc(request)
14010        }
14011        "crypto.generateKeySync" => service_javascript_crypto_generate_key_sync_rpc(request),
14012        "crypto.generatePrimeSync" => service_javascript_crypto_generate_prime_sync_rpc(request),
14013        "crypto.diffieHellman" => service_javascript_crypto_diffie_hellman_sync_rpc(request),
14014        "crypto.diffieHellmanGroup" => {
14015            service_javascript_crypto_diffie_hellman_group_sync_rpc(request)
14016        }
14017        "crypto.diffieHellmanSessionCreate" => {
14018            service_javascript_crypto_diffie_hellman_session_create_sync_rpc(process, request)
14019        }
14020        "crypto.diffieHellmanSessionCall" => {
14021            service_javascript_crypto_diffie_hellman_session_call_sync_rpc(process, request)
14022        }
14023        "crypto.diffieHellmanSessionDestroy" => {
14024            service_javascript_crypto_diffie_hellman_session_destroy_sync_rpc(process, request)
14025        }
14026        "crypto.subtle" => service_javascript_crypto_subtle_sync_rpc(request),
14027        _ => Err(SidecarError::InvalidState(format!(
14028            "unsupported JavaScript crypto sync RPC method {}",
14029            request.method
14030        ))),
14031    }
14032}
14033
14034fn javascript_crypto_digest_algorithm(
14035    args: &[Value],
14036    index: usize,
14037    label: &str,
14038) -> Result<JavascriptCryptoDigestAlgorithm, SidecarError> {
14039    JavascriptCryptoDigestAlgorithm::parse(javascript_sync_rpc_arg_str(args, index, label)?)
14040}
14041
14042impl JavascriptCryptoDigestAlgorithm {
14043    fn parse(value: &str) -> Result<Self, SidecarError> {
14044        match value.trim().to_ascii_lowercase().replace('-', "").as_str() {
14045            "md5" => Ok(Self::Md5),
14046            "sha1" => Ok(Self::Sha1),
14047            "sha256" => Ok(Self::Sha256),
14048            "sha512" => Ok(Self::Sha512),
14049            _ => Err(SidecarError::InvalidState(format!(
14050                "unsupported crypto digest algorithm {value}"
14051            ))),
14052        }
14053    }
14054
14055    fn digest(self, data: &[u8]) -> Vec<u8> {
14056        match self {
14057            Self::Md5 => Md5::digest(data).to_vec(),
14058            Self::Sha1 => Sha1::digest(data).to_vec(),
14059            Self::Sha256 => Sha256::digest(data).to_vec(),
14060            Self::Sha512 => Sha512::digest(data).to_vec(),
14061        }
14062    }
14063
14064    fn hmac(self, key: &[u8], data: &[u8]) -> Result<Vec<u8>, SidecarError> {
14065        match self {
14066            Self::Md5 => {
14067                let mut mac = Hmac::<Md5>::new_from_slice(key).map_err(|error| {
14068                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14069                })?;
14070                mac.update(data);
14071                Ok(mac.finalize().into_bytes().to_vec())
14072            }
14073            Self::Sha1 => {
14074                let mut mac = Hmac::<Sha1>::new_from_slice(key).map_err(|error| {
14075                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14076                })?;
14077                mac.update(data);
14078                Ok(mac.finalize().into_bytes().to_vec())
14079            }
14080            Self::Sha256 => {
14081                let mut mac = Hmac::<Sha256>::new_from_slice(key).map_err(|error| {
14082                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14083                })?;
14084                mac.update(data);
14085                Ok(mac.finalize().into_bytes().to_vec())
14086            }
14087            Self::Sha512 => {
14088                let mut mac = Hmac::<Sha512>::new_from_slice(key).map_err(|error| {
14089                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14090                })?;
14091                mac.update(data);
14092                Ok(mac.finalize().into_bytes().to_vec())
14093            }
14094        }
14095    }
14096
14097    fn pbkdf2(self, password: &[u8], salt: &[u8], iterations: u32, output: &mut [u8]) {
14098        match self {
14099            Self::Md5 => pbkdf2_hmac::<Md5>(password, salt, iterations, output),
14100            Self::Sha1 => pbkdf2_hmac::<Sha1>(password, salt, iterations, output),
14101            Self::Sha256 => pbkdf2_hmac::<Sha256>(password, salt, iterations, output),
14102            Self::Sha512 => pbkdf2_hmac::<Sha512>(password, salt, iterations, output),
14103        }
14104    }
14105}
14106
14107#[derive(Debug, Clone)]
14108enum JavascriptCryptoKeyMaterial {
14109    Private(PKey<Private>),
14110    Public(PKey<Public>),
14111    Secret(Vec<u8>),
14112}
14113
14114#[derive(Debug, Clone, Deserialize, Serialize)]
14115struct JavascriptSerializedSandboxKeyObject {
14116    #[serde(rename = "type")]
14117    kind: String,
14118    #[serde(skip_serializing_if = "Option::is_none")]
14119    pem: Option<String>,
14120    #[serde(skip_serializing_if = "Option::is_none")]
14121    raw: Option<String>,
14122    #[serde(skip_serializing_if = "Option::is_none", rename = "asymmetricKeyType")]
14123    asymmetric_key_type: Option<String>,
14124    #[serde(
14125        skip_serializing_if = "Option::is_none",
14126        rename = "asymmetricKeyDetails"
14127    )]
14128    asymmetric_key_details: Option<Map<String, Value>>,
14129    #[serde(skip_serializing_if = "Option::is_none")]
14130    jwk: Option<Value>,
14131}
14132
14133#[derive(Debug, Clone)]
14134struct JavascriptDirectKeyInput {
14135    key: JavascriptCryptoKeyMaterial,
14136    padding: Option<Padding>,
14137}
14138
14139fn service_javascript_crypto_cipheriv_sync_rpc(
14140    request: &JavascriptSyncRpcRequest,
14141) -> Result<Value, SidecarError> {
14142    service_javascript_crypto_cipheriv_inner(request, false)
14143}
14144
14145fn service_javascript_crypto_decipheriv_sync_rpc(
14146    request: &JavascriptSyncRpcRequest,
14147) -> Result<Value, SidecarError> {
14148    service_javascript_crypto_cipheriv_inner(request, true)
14149}
14150
14151fn service_javascript_crypto_cipheriv_create_sync_rpc(
14152    process: &mut ActiveProcess,
14153    request: &JavascriptSyncRpcRequest,
14154) -> Result<Value, SidecarError> {
14155    ensure_per_process_state_handle_capacity(process.cipher_sessions.len(), "cipher session")?;
14156    let mode = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.cipherivCreate mode")?;
14157    let decrypt = mode == "decipher";
14158    let algorithm =
14159        javascript_sync_rpc_arg_str(&request.args, 1, "crypto.cipherivCreate algorithm")?;
14160    let key = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.cipherivCreate key")?;
14161    let iv = javascript_sync_rpc_base64_arg_optional(&request.args, 3, "crypto.cipherivCreate iv")?;
14162    let options =
14163        javascript_sync_rpc_json_arg_optional(&request.args, 4, "crypto.cipherivCreate options")?;
14164    let auth_tag_len = javascript_crypto_requested_aead_tag_len(algorithm, options.as_ref())?;
14165    let context = javascript_crypto_build_cipher_context(
14166        algorithm,
14167        &key,
14168        iv.as_deref(),
14169        decrypt,
14170        options.as_ref(),
14171    )?;
14172    process.next_cipher_session_id += 1;
14173    let session_id = process.next_cipher_session_id;
14174    process.cipher_sessions.insert(
14175        session_id,
14176        ActiveCipherSession {
14177            algorithm: algorithm.to_string(),
14178            auth_tag_len,
14179            context,
14180        },
14181    );
14182    Ok(json!(session_id))
14183}
14184
14185fn service_javascript_crypto_cipheriv_update_sync_rpc(
14186    process: &mut ActiveProcess,
14187    request: &JavascriptSyncRpcRequest,
14188) -> Result<Value, SidecarError> {
14189    let session_id =
14190        javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.cipherivUpdate session id")?;
14191    let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.cipherivUpdate data")?;
14192    let session = process
14193        .cipher_sessions
14194        .get_mut(&session_id)
14195        .ok_or_else(|| {
14196            SidecarError::InvalidState(format!("Cipher session {session_id} not found"))
14197        })?;
14198    let result = javascript_crypto_cipher_update(&mut session.context, &data)?;
14199    Ok(Value::String(
14200        base64::engine::general_purpose::STANDARD.encode(result),
14201    ))
14202}
14203
14204fn service_javascript_crypto_cipheriv_final_sync_rpc(
14205    process: &mut ActiveProcess,
14206    request: &JavascriptSyncRpcRequest,
14207) -> Result<Value, SidecarError> {
14208    let session_id =
14209        javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.cipherivFinal session id")?;
14210    let mut session = process.cipher_sessions.remove(&session_id).ok_or_else(|| {
14211        SidecarError::InvalidState(format!("Cipher session {session_id} not found"))
14212    })?;
14213    let data = javascript_crypto_cipher_finalize(&mut session.context)?;
14214    let mut response = Map::new();
14215    response.insert(
14216        String::from("data"),
14217        Value::String(base64::engine::general_purpose::STANDARD.encode(data)),
14218    );
14219    if javascript_crypto_is_aead(&session.algorithm) {
14220        let mut auth_tag = vec![0_u8; session.auth_tag_len];
14221        session
14222            .context
14223            .get_tag(&mut auth_tag)
14224            .map_err(javascript_crypto_openssl_error)?;
14225        response.insert(
14226            String::from("authTag"),
14227            Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
14228        );
14229    }
14230    Ok(Value::String(serde_json::to_string(&response).map_err(
14231        |error| SidecarError::InvalidState(format!("serialize cipher final response: {error}")),
14232    )?))
14233}
14234
14235fn service_javascript_crypto_sign_sync_rpc(
14236    request: &JavascriptSyncRpcRequest,
14237) -> Result<Value, SidecarError> {
14238    let algorithm = request.args.first().and_then(Value::as_str);
14239    let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.sign data")?;
14240    let key_json = javascript_sync_rpc_arg_str(&request.args, 2, "crypto.sign key")?;
14241    let key_input =
14242        javascript_crypto_parse_direct_key_input(key_json, Some("private"), "crypto.sign key")?;
14243    let private_key = javascript_crypto_expect_private_key(key_input.key, "crypto.sign key")?;
14244    let mut signer = javascript_crypto_new_signer(algorithm, &private_key)?;
14245    if let Some(padding) = key_input.padding {
14246        signer
14247            .set_rsa_padding(padding)
14248            .map_err(javascript_crypto_openssl_error)?;
14249    }
14250    signer
14251        .update(&data)
14252        .map_err(javascript_crypto_openssl_error)?;
14253    Ok(Value::String(
14254        base64::engine::general_purpose::STANDARD.encode(
14255            signer
14256                .sign_to_vec()
14257                .map_err(javascript_crypto_openssl_error)?,
14258        ),
14259    ))
14260}
14261
14262fn service_javascript_crypto_verify_sync_rpc(
14263    request: &JavascriptSyncRpcRequest,
14264) -> Result<Value, SidecarError> {
14265    let algorithm = request.args.first().and_then(Value::as_str);
14266    let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.verify data")?;
14267    let key_json = javascript_sync_rpc_arg_str(&request.args, 2, "crypto.verify key")?;
14268    let signature = javascript_sync_rpc_base64_arg(&request.args, 3, "crypto.verify signature")?;
14269    let key_input =
14270        javascript_crypto_parse_direct_key_input(key_json, Some("public"), "crypto.verify key")?;
14271    let public_key = javascript_crypto_expect_public_key(key_input.key, "crypto.verify key")?;
14272    let mut verifier = javascript_crypto_new_verifier(algorithm, &public_key)?;
14273    if let Some(padding) = key_input.padding {
14274        verifier
14275            .set_rsa_padding(padding)
14276            .map_err(javascript_crypto_openssl_error)?;
14277    }
14278    verifier
14279        .update(&data)
14280        .map_err(javascript_crypto_openssl_error)?;
14281    Ok(json!(verifier
14282        .verify(&signature)
14283        .map_err(javascript_crypto_openssl_error)?))
14284}
14285
14286fn service_javascript_crypto_asymmetric_op_sync_rpc(
14287    request: &JavascriptSyncRpcRequest,
14288) -> Result<Value, SidecarError> {
14289    let operation = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.asymmetricOp operation")?;
14290    let key_json = javascript_sync_rpc_arg_str(&request.args, 1, "crypto.asymmetricOp key")?;
14291    let data = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.asymmetricOp data")?;
14292    let expect_kind = match operation {
14293        "publicEncrypt" | "publicDecrypt" => Some("public"),
14294        "privateEncrypt" | "privateDecrypt" => Some("private"),
14295        other => {
14296            return Err(SidecarError::InvalidState(format!(
14297                "Unsupported asymmetric crypto operation: {other}"
14298            )));
14299        }
14300    };
14301    let key_input =
14302        javascript_crypto_parse_direct_key_input(key_json, expect_kind, "crypto.asymmetricOp key")?;
14303    let padding = key_input.padding.unwrap_or(Padding::PKCS1);
14304    let mut output = vec![0_u8; javascript_crypto_rsa_output_size(&key_input.key)?];
14305    let written = match (operation, key_input.key) {
14306        ("publicEncrypt", JavascriptCryptoKeyMaterial::Public(key))
14307        | ("publicDecrypt", JavascriptCryptoKeyMaterial::Public(key)) => {
14308            let rsa = key.rsa().map_err(javascript_crypto_openssl_error)?;
14309            if operation == "publicEncrypt" {
14310                rsa.public_encrypt(&data, &mut output, padding)
14311                    .map_err(javascript_crypto_openssl_error)?
14312            } else {
14313                rsa.public_decrypt(&data, &mut output, padding)
14314                    .map_err(javascript_crypto_openssl_error)?
14315            }
14316        }
14317        ("privateEncrypt", JavascriptCryptoKeyMaterial::Private(key))
14318        | ("privateDecrypt", JavascriptCryptoKeyMaterial::Private(key)) => {
14319            let rsa = key.rsa().map_err(javascript_crypto_openssl_error)?;
14320            if operation == "privateEncrypt" {
14321                rsa.private_encrypt(&data, &mut output, padding)
14322                    .map_err(javascript_crypto_openssl_error)?
14323            } else {
14324                rsa.private_decrypt(&data, &mut output, padding)
14325                    .map_err(javascript_crypto_openssl_error)?
14326            }
14327        }
14328        _ => {
14329            return Err(SidecarError::InvalidState(format!(
14330                "{operation} requires an RSA {} key",
14331                expect_kind.unwrap_or("asymmetric")
14332            )));
14333        }
14334    };
14335    output.truncate(written);
14336    Ok(Value::String(
14337        base64::engine::general_purpose::STANDARD.encode(output),
14338    ))
14339}
14340
14341fn service_javascript_crypto_create_key_object_sync_rpc(
14342    request: &JavascriptSyncRpcRequest,
14343) -> Result<Value, SidecarError> {
14344    let operation =
14345        javascript_sync_rpc_arg_str(&request.args, 0, "crypto.createKeyObject operation")?;
14346    let key_json = javascript_sync_rpc_arg_str(&request.args, 1, "crypto.createKeyObject key")?;
14347    let expected = match operation {
14348        "createPrivateKey" => Some("private"),
14349        "createPublicKey" => Some("public"),
14350        other => {
14351            return Err(SidecarError::InvalidState(format!(
14352                "Unsupported key creation operation: {other}"
14353            )));
14354        }
14355    };
14356    let key_input =
14357        javascript_crypto_parse_direct_key_input(key_json, expected, "crypto.createKeyObject key")?;
14358    Ok(Value::String(
14359        serde_json::to_string(&javascript_crypto_serialize_sandbox_key_object(
14360            &key_input.key,
14361        )?)
14362        .map_err(|error| {
14363            SidecarError::InvalidState(format!("serialize crypto key object: {error}"))
14364        })?,
14365    ))
14366}
14367
14368fn service_javascript_crypto_generate_key_pair_sync_rpc(
14369    request: &JavascriptSyncRpcRequest,
14370) -> Result<Value, SidecarError> {
14371    let key_type =
14372        javascript_sync_rpc_arg_str(&request.args, 0, "crypto.generateKeyPairSync type")?;
14373    let options = javascript_crypto_parse_serialized_options_arg(
14374        &request.args,
14375        1,
14376        "crypto.generateKeyPairSync options",
14377    )?
14378    .unwrap_or(Value::Object(Map::new()));
14379    let public_encoding = options.get("publicKeyEncoding").cloned();
14380    let private_encoding = options.get("privateKeyEncoding").cloned();
14381
14382    let private_key = match key_type {
14383        "rsa" => {
14384            let bits = options
14385                .get("modulusLength")
14386                .and_then(Value::as_u64)
14387                .unwrap_or(2048) as u32;
14388            let exponent = options
14389                .get("publicExponent")
14390                .map(|value| javascript_crypto_u32_from_bridge_value(value, "rsa publicExponent"))
14391                .transpose()?
14392                .unwrap_or(65_537);
14393            let exponent = BigNum::from_u32(exponent).map_err(javascript_crypto_openssl_error)?;
14394            let rsa =
14395                Rsa::generate_with_e(bits, &exponent).map_err(javascript_crypto_openssl_error)?;
14396            PKey::from_rsa(rsa).map_err(javascript_crypto_openssl_error)?
14397        }
14398        "ec" => {
14399            let named_curve = options
14400                .get("namedCurve")
14401                .and_then(Value::as_str)
14402                .ok_or_else(|| {
14403                    SidecarError::InvalidState(String::from(
14404                        "crypto.generateKeyPairSync ec requires namedCurve",
14405                    ))
14406                })?;
14407            let group = EcGroup::from_curve_name(javascript_crypto_curve_nid(named_curve)?)
14408                .map_err(javascript_crypto_openssl_error)?;
14409            let key = EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?;
14410            PKey::from_ec_key(key).map_err(javascript_crypto_openssl_error)?
14411        }
14412        "ed25519" => PKey::generate_ed25519().map_err(javascript_crypto_openssl_error)?,
14413        "x25519" => PKey::generate_x25519().map_err(javascript_crypto_openssl_error)?,
14414        other => {
14415            return Err(SidecarError::InvalidState(format!(
14416                "unsupported crypto key pair type {other}"
14417            )));
14418        }
14419    };
14420    let public_key = PKey::public_key_from_pem(
14421        &private_key
14422            .public_key_to_pem()
14423            .map_err(javascript_crypto_openssl_error)?,
14424    )
14425    .map_err(javascript_crypto_openssl_error)?;
14426    let response = if public_encoding.is_some() || private_encoding.is_some() {
14427        json!({
14428            "publicKey": javascript_crypto_serialize_encoded_key_value_public(&public_key, public_encoding.as_ref())?,
14429            "privateKey": javascript_crypto_serialize_encoded_key_value_private(&private_key, private_encoding.as_ref())?,
14430        })
14431    } else {
14432        json!({
14433            "publicKey": javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Public(public_key))?,
14434            "privateKey": javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Private(private_key))?,
14435        })
14436    };
14437    Ok(Value::String(serde_json::to_string(&response).map_err(
14438        |error| SidecarError::InvalidState(format!("serialize generated key pair: {error}")),
14439    )?))
14440}
14441
14442fn service_javascript_crypto_generate_key_sync_rpc(
14443    request: &JavascriptSyncRpcRequest,
14444) -> Result<Value, SidecarError> {
14445    let key_type = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.generateKeySync type")?;
14446    let options = javascript_crypto_parse_serialized_options_arg(
14447        &request.args,
14448        1,
14449        "crypto.generateKeySync options",
14450    )?
14451    .unwrap_or(Value::Object(Map::new()));
14452    let bit_length = options
14453        .get("length")
14454        .and_then(Value::as_u64)
14455        .ok_or_else(|| {
14456            SidecarError::InvalidState(String::from(
14457                "crypto.generateKeySync options.length is required",
14458            ))
14459        })? as usize;
14460    let mut raw = vec![0_u8; bit_length.div_ceil(8)];
14461    rand_bytes(&mut raw).map_err(javascript_crypto_openssl_error)?;
14462    let serialized = match key_type {
14463        "hmac" => javascript_crypto_serialize_sandbox_key_object(
14464            &JavascriptCryptoKeyMaterial::Secret(raw),
14465        )?,
14466        "aes" => javascript_crypto_serialize_sandbox_key_object(
14467            &JavascriptCryptoKeyMaterial::Secret(raw),
14468        )?,
14469        other => {
14470            return Err(SidecarError::InvalidState(format!(
14471                "unsupported crypto.generateKeySync type {other}"
14472            )));
14473        }
14474    };
14475    Ok(Value::String(serde_json::to_string(&serialized).map_err(
14476        |error| SidecarError::InvalidState(format!("serialize generated key: {error}")),
14477    )?))
14478}
14479
14480fn service_javascript_crypto_generate_prime_sync_rpc(
14481    request: &JavascriptSyncRpcRequest,
14482) -> Result<Value, SidecarError> {
14483    let bits =
14484        javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.generatePrimeSync size")? as i32;
14485    let options = javascript_crypto_parse_serialized_options_arg(
14486        &request.args,
14487        1,
14488        "crypto.generatePrimeSync options",
14489    )?
14490    .unwrap_or(Value::Object(Map::new()));
14491    let safe = options
14492        .get("safe")
14493        .and_then(Value::as_bool)
14494        .unwrap_or(false);
14495    let add = options
14496        .get("add")
14497        .map(|value| javascript_crypto_bignum_from_bridge_value(value, "prime add"))
14498        .transpose()?;
14499    let rem = options
14500        .get("rem")
14501        .map(|value| javascript_crypto_bignum_from_bridge_value(value, "prime rem"))
14502        .transpose()?;
14503    let mut prime = BigNum::new().map_err(javascript_crypto_openssl_error)?;
14504    prime
14505        .generate_prime(bits, safe, add.as_deref(), rem.as_deref())
14506        .map_err(javascript_crypto_openssl_error)?;
14507    let payload = if options
14508        .get("bigint")
14509        .and_then(Value::as_bool)
14510        .unwrap_or(false)
14511    {
14512        json!({
14513            "__type": "bigint",
14514            "value": prime.to_dec_str().map_err(javascript_crypto_openssl_error)?.to_string(),
14515        })
14516    } else {
14517        json!({
14518            "__type": "buffer",
14519            "value": base64::engine::general_purpose::STANDARD.encode(prime.to_vec()),
14520        })
14521    };
14522    Ok(Value::String(serde_json::to_string(&payload).map_err(
14523        |error| SidecarError::InvalidState(format!("serialize generated prime: {error}")),
14524    )?))
14525}
14526
14527fn service_javascript_crypto_diffie_hellman_sync_rpc(
14528    request: &JavascriptSyncRpcRequest,
14529) -> Result<Value, SidecarError> {
14530    let options = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.diffieHellman options")?;
14531    let parsed: Value = serde_json::from_str(options).map_err(|error| {
14532        SidecarError::InvalidState(format!(
14533            "crypto.diffieHellman options must be valid JSON: {error}"
14534        ))
14535    })?;
14536    let private_key = javascript_crypto_parse_key_material_value(
14537        parsed.get("privateKey").ok_or_else(|| {
14538            SidecarError::InvalidState(String::from("crypto.diffieHellman missing privateKey"))
14539        })?,
14540        Some("private"),
14541        "crypto.diffieHellman privateKey",
14542    )?;
14543    let public_key = javascript_crypto_parse_key_material_value(
14544        parsed.get("publicKey").ok_or_else(|| {
14545            SidecarError::InvalidState(String::from("crypto.diffieHellman missing publicKey"))
14546        })?,
14547        Some("public"),
14548        "crypto.diffieHellman publicKey",
14549    )?;
14550    let private_key =
14551        javascript_crypto_expect_private_key(private_key, "crypto.diffieHellman privateKey")?;
14552    let public_key =
14553        javascript_crypto_expect_public_key(public_key, "crypto.diffieHellman publicKey")?;
14554    let mut deriver = Deriver::new(&private_key).map_err(javascript_crypto_openssl_error)?;
14555    deriver
14556        .set_peer(&public_key)
14557        .map_err(javascript_crypto_openssl_error)?;
14558    let secret = deriver
14559        .derive_to_vec()
14560        .map_err(javascript_crypto_openssl_error)?;
14561    Ok(Value::String(
14562        serde_json::to_string(&json!({
14563            "__type": "buffer",
14564            "value": base64::engine::general_purpose::STANDARD.encode(secret),
14565        }))
14566        .map_err(|error| {
14567            SidecarError::InvalidState(format!("serialize derived secret: {error}"))
14568        })?,
14569    ))
14570}
14571
14572fn service_javascript_crypto_diffie_hellman_group_sync_rpc(
14573    request: &JavascriptSyncRpcRequest,
14574) -> Result<Value, SidecarError> {
14575    let name = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.diffieHellmanGroup name")?;
14576    let params = javascript_crypto_named_dh_group(name)?;
14577    let response = json!({
14578        "prime": {
14579            "__type": "buffer",
14580            "value": base64::engine::general_purpose::STANDARD.encode(params.prime_p().to_vec()),
14581        },
14582        "generator": {
14583            "__type": "buffer",
14584            "value": base64::engine::general_purpose::STANDARD.encode(params.generator().to_vec()),
14585        },
14586    });
14587    Ok(Value::String(serde_json::to_string(&response).map_err(
14588        |error| {
14589            SidecarError::InvalidState(format!("serialize diffieHellmanGroup response: {error}"))
14590        },
14591    )?))
14592}
14593
14594fn service_javascript_crypto_diffie_hellman_session_create_sync_rpc(
14595    process: &mut ActiveProcess,
14596    request: &JavascriptSyncRpcRequest,
14597) -> Result<Value, SidecarError> {
14598    ensure_per_process_state_handle_capacity(
14599        process.diffie_hellman_sessions.len(),
14600        "diffie-hellman session",
14601    )?;
14602    let raw = javascript_sync_rpc_arg_str(
14603        &request.args,
14604        0,
14605        "crypto.diffieHellmanSessionCreate request",
14606    )?;
14607    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14608        SidecarError::InvalidState(format!(
14609            "crypto.diffieHellmanSessionCreate request must be valid JSON: {error}"
14610        ))
14611    })?;
14612    let session = match parsed.get("type").and_then(Value::as_str) {
14613        Some("group") => {
14614            let name = parsed.get("name").and_then(Value::as_str).ok_or_else(|| {
14615                SidecarError::InvalidState(String::from(
14616                    "crypto.diffieHellmanSessionCreate group requires name",
14617                ))
14618            })?;
14619            ActiveDiffieHellmanSession::Dh(ActiveDhSession {
14620                params: javascript_crypto_named_dh_group(name)?,
14621                key_pair: None,
14622            })
14623        }
14624        Some("dh") => {
14625            let args = parsed
14626                .get("args")
14627                .and_then(Value::as_array)
14628                .ok_or_else(|| {
14629                    SidecarError::InvalidState(String::from(
14630                        "crypto.diffieHellmanSessionCreate dh requires args",
14631                    ))
14632                })?;
14633            let params = javascript_crypto_build_dh_params(args)?;
14634            ActiveDiffieHellmanSession::Dh(ActiveDhSession {
14635                params,
14636                key_pair: None,
14637            })
14638        }
14639        Some("ecdh") => {
14640            let curve = parsed.get("name").and_then(Value::as_str).ok_or_else(|| {
14641                SidecarError::InvalidState(String::from(
14642                    "crypto.diffieHellmanSessionCreate ecdh requires name",
14643                ))
14644            })?;
14645            ActiveDiffieHellmanSession::Ecdh(ActiveEcdhSession {
14646                curve: curve.to_string(),
14647                key_pair: None,
14648            })
14649        }
14650        other => {
14651            return Err(SidecarError::InvalidState(format!(
14652                "Unsupported Diffie-Hellman session type: {}",
14653                other.unwrap_or("<missing>")
14654            )));
14655        }
14656    };
14657    process.next_diffie_hellman_session_id += 1;
14658    let session_id = process.next_diffie_hellman_session_id;
14659    process.diffie_hellman_sessions.insert(session_id, session);
14660    Ok(json!(session_id))
14661}
14662
14663fn service_javascript_crypto_diffie_hellman_session_call_sync_rpc(
14664    process: &mut ActiveProcess,
14665    request: &JavascriptSyncRpcRequest,
14666) -> Result<Value, SidecarError> {
14667    let session_id = javascript_sync_rpc_arg_u64(
14668        &request.args,
14669        0,
14670        "crypto.diffieHellmanSessionCall session id",
14671    )?;
14672    let raw =
14673        javascript_sync_rpc_arg_str(&request.args, 1, "crypto.diffieHellmanSessionCall request")?;
14674    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14675        SidecarError::InvalidState(format!(
14676            "crypto.diffieHellmanSessionCall request must be valid JSON: {error}"
14677        ))
14678    })?;
14679    let method = parsed
14680        .get("method")
14681        .and_then(Value::as_str)
14682        .ok_or_else(|| {
14683            SidecarError::InvalidState(String::from(
14684                "crypto.diffieHellmanSessionCall request missing method",
14685            ))
14686        })?;
14687    let args = parsed
14688        .get("args")
14689        .and_then(Value::as_array)
14690        .cloned()
14691        .unwrap_or_default();
14692    let session = process
14693        .diffie_hellman_sessions
14694        .get_mut(&session_id)
14695        .ok_or_else(|| {
14696            SidecarError::InvalidState(format!("Diffie-Hellman session {session_id} not found"))
14697        })?;
14698    let (result, has_result) = match session {
14699        ActiveDiffieHellmanSession::Dh(session) => {
14700            javascript_crypto_call_dh_session(session, method, &args)?
14701        }
14702        ActiveDiffieHellmanSession::Ecdh(session) => {
14703            javascript_crypto_call_ecdh_session(session, method, &args)?
14704        }
14705    };
14706    Ok(Value::String(
14707        serde_json::to_string(&json!({
14708            "result": result,
14709            "hasResult": has_result,
14710        }))
14711        .map_err(|error| {
14712            SidecarError::InvalidState(format!("serialize diffie session result: {error}"))
14713        })?,
14714    ))
14715}
14716
14717fn service_javascript_crypto_diffie_hellman_session_destroy_sync_rpc(
14718    process: &mut ActiveProcess,
14719    request: &JavascriptSyncRpcRequest,
14720) -> Result<Value, SidecarError> {
14721    let session_id = javascript_sync_rpc_arg_u64(
14722        &request.args,
14723        0,
14724        "crypto.diffieHellmanSessionDestroy session id",
14725    )?;
14726    process
14727        .diffie_hellman_sessions
14728        .remove(&session_id)
14729        .ok_or_else(|| {
14730            SidecarError::InvalidState(format!("Diffie-Hellman session {session_id} not found"))
14731        })?;
14732    Ok(Value::Null)
14733}
14734
14735fn service_javascript_crypto_subtle_sync_rpc(
14736    request: &JavascriptSyncRpcRequest,
14737) -> Result<Value, SidecarError> {
14738    let raw = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.subtle request")?;
14739    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14740        SidecarError::InvalidState(format!("crypto.subtle request must be valid JSON: {error}"))
14741    })?;
14742    let op = parsed.get("op").and_then(Value::as_str).ok_or_else(|| {
14743        SidecarError::InvalidState(String::from("crypto.subtle request missing op"))
14744    })?;
14745    match op {
14746        "digest" => {
14747            let algorithm = parsed
14748                .get("algorithm")
14749                .and_then(Value::as_str)
14750                .ok_or_else(|| {
14751                    SidecarError::InvalidState(String::from(
14752                        "crypto.subtle.digest missing algorithm",
14753                    ))
14754                })?;
14755            let data = parsed.get("data").and_then(Value::as_str).ok_or_else(|| {
14756                SidecarError::InvalidState(String::from("crypto.subtle.digest missing data"))
14757            })?;
14758            let bytes = base64::engine::general_purpose::STANDARD
14759                .decode(data)
14760                .map_err(|error| {
14761                    SidecarError::InvalidState(format!("crypto.subtle.digest data base64: {error}"))
14762                })?;
14763            let digest = JavascriptCryptoDigestAlgorithm::parse(algorithm)?.digest(&bytes);
14764            Ok(Value::String(
14765                serde_json::to_string(&json!({
14766                    "data": base64::engine::general_purpose::STANDARD.encode(digest),
14767                }))
14768                .map_err(|error| {
14769                    SidecarError::InvalidState(format!("serialize crypto.subtle digest: {error}"))
14770                })?,
14771            ))
14772        }
14773        "generateKey" => {
14774            let algorithm = parsed.get("algorithm").ok_or_else(|| {
14775                SidecarError::InvalidState(String::from(
14776                    "crypto.subtle.generateKey missing algorithm",
14777                ))
14778            })?;
14779            let name =
14780                javascript_crypto_subtle_algorithm_name(algorithm, "crypto.subtle.generateKey")?;
14781            if !matches!(name, "AES-GCM" | "AES-CBC" | "AES-CTR" | "AES-KW") {
14782                return Err(SidecarError::InvalidState(format!(
14783                    "Unsupported key algorithm: {name}"
14784                )));
14785            }
14786            let length_bits = algorithm
14787                .get("length")
14788                .and_then(Value::as_u64)
14789                .ok_or_else(|| {
14790                    SidecarError::InvalidState(String::from(
14791                        "crypto.subtle.generateKey AES algorithm requires length",
14792                    ))
14793                })?;
14794            if length_bits % 8 != 0 {
14795                return Err(SidecarError::InvalidState(String::from(
14796                    "crypto.subtle.generateKey length must be byte-aligned",
14797                )));
14798            }
14799            let length_bytes = usize::try_from(length_bits / 8).map_err(|_| {
14800                SidecarError::InvalidState(String::from(
14801                    "crypto.subtle.generateKey length is too large",
14802                ))
14803            })?;
14804            let mut raw = vec![0_u8; length_bytes];
14805            rand_bytes(&mut raw).map_err(javascript_crypto_openssl_error)?;
14806            let key = javascript_crypto_serialize_subtle_secret_key(
14807                &raw,
14808                javascript_crypto_normalize_subtle_secret_algorithm(algorithm.clone(), &raw)?,
14809                parsed
14810                    .get("extractable")
14811                    .and_then(Value::as_bool)
14812                    .unwrap_or(false),
14813                parsed.get("usages").cloned().unwrap_or_else(|| json!([])),
14814            )?;
14815            Ok(Value::String(
14816                serde_json::to_string(&json!({ "key": key })).map_err(|error| {
14817                    SidecarError::InvalidState(format!(
14818                        "serialize crypto.subtle generated key: {error}"
14819                    ))
14820                })?,
14821            ))
14822        }
14823        "importKey" => {
14824            let format = parsed
14825                .get("format")
14826                .and_then(Value::as_str)
14827                .ok_or_else(|| {
14828                    SidecarError::InvalidState(String::from(
14829                        "crypto.subtle.importKey missing format",
14830                    ))
14831                })?;
14832            if format != "raw" {
14833                return Err(SidecarError::InvalidState(format!(
14834                    "Unsupported import format: {format}"
14835                )));
14836            }
14837            let key_data = parsed
14838                .get("keyData")
14839                .and_then(Value::as_str)
14840                .ok_or_else(|| {
14841                    SidecarError::InvalidState(String::from(
14842                        "crypto.subtle.importKey missing keyData",
14843                    ))
14844                })?;
14845            let raw = base64::engine::general_purpose::STANDARD
14846                .decode(key_data)
14847                .map_err(|error| {
14848                    SidecarError::InvalidState(format!(
14849                        "crypto.subtle.importKey keyData base64: {error}"
14850                    ))
14851                })?;
14852            let algorithm = parsed.get("algorithm").ok_or_else(|| {
14853                SidecarError::InvalidState(String::from(
14854                    "crypto.subtle.importKey missing algorithm",
14855                ))
14856            })?;
14857            let key = javascript_crypto_serialize_subtle_secret_key(
14858                &raw,
14859                javascript_crypto_normalize_subtle_secret_algorithm(algorithm.clone(), &raw)?,
14860                parsed
14861                    .get("extractable")
14862                    .and_then(Value::as_bool)
14863                    .unwrap_or(false),
14864                parsed.get("usages").cloned().unwrap_or_else(|| json!([])),
14865            )?;
14866            Ok(Value::String(
14867                serde_json::to_string(&json!({ "key": key })).map_err(|error| {
14868                    SidecarError::InvalidState(format!(
14869                        "serialize crypto.subtle imported key: {error}"
14870                    ))
14871                })?,
14872            ))
14873        }
14874        "exportKey" => {
14875            let format = parsed
14876                .get("format")
14877                .and_then(Value::as_str)
14878                .ok_or_else(|| {
14879                    SidecarError::InvalidState(String::from(
14880                        "crypto.subtle.exportKey missing format",
14881                    ))
14882                })?;
14883            if format != "raw" {
14884                return Err(SidecarError::InvalidState(format!(
14885                    "Unsupported export format: {format}"
14886                )));
14887            }
14888            let raw = javascript_crypto_subtle_key_raw(
14889                parsed.get("key").ok_or_else(|| {
14890                    SidecarError::InvalidState(String::from("crypto.subtle.exportKey missing key"))
14891                })?,
14892                "crypto.subtle.exportKey key",
14893            )?;
14894            Ok(Value::String(
14895                serde_json::to_string(&json!({
14896                    "data": base64::engine::general_purpose::STANDARD.encode(raw),
14897                }))
14898                .map_err(|error| {
14899                    SidecarError::InvalidState(format!("serialize crypto.subtle export: {error}"))
14900                })?,
14901            ))
14902        }
14903        "encrypt" | "decrypt" => service_javascript_crypto_subtle_aes_crypt_sync_rpc(op, &parsed),
14904        _ => Err(SidecarError::InvalidState(format!(
14905            "Unsupported subtle operation: {op}"
14906        ))),
14907    }
14908}
14909
14910fn javascript_crypto_subtle_algorithm_name<'a>(
14911    algorithm: &'a Value,
14912    label: &str,
14913) -> Result<&'a str, SidecarError> {
14914    if let Some(name) = algorithm.as_str() {
14915        return Ok(name);
14916    }
14917    algorithm
14918        .get("name")
14919        .and_then(Value::as_str)
14920        .ok_or_else(|| SidecarError::InvalidState(format!("{label} algorithm missing name")))
14921}
14922
14923fn javascript_crypto_normalize_subtle_secret_algorithm(
14924    algorithm: Value,
14925    raw: &[u8],
14926) -> Result<Value, SidecarError> {
14927    let mut object = match algorithm {
14928        Value::String(name) => {
14929            let mut object = Map::new();
14930            object.insert(String::from("name"), Value::String(name));
14931            object
14932        }
14933        Value::Object(object) => object,
14934        _ => {
14935            return Err(SidecarError::InvalidState(String::from(
14936                "crypto.subtle secret algorithm must be a string or object",
14937            )));
14938        }
14939    };
14940    let name = object
14941        .get("name")
14942        .and_then(Value::as_str)
14943        .ok_or_else(|| {
14944            SidecarError::InvalidState(String::from("crypto.subtle secret algorithm missing name"))
14945        })?
14946        .to_string();
14947    if matches!(name.as_str(), "AES-GCM" | "AES-CBC" | "AES-CTR" | "AES-KW")
14948        && !object.contains_key("length")
14949    {
14950        object.insert(String::from("length"), json!(raw.len() * 8));
14951    }
14952    Ok(Value::Object(object))
14953}
14954
14955fn javascript_crypto_serialize_subtle_secret_key(
14956    raw: &[u8],
14957    algorithm: Value,
14958    extractable: bool,
14959    usages: Value,
14960) -> Result<Value, SidecarError> {
14961    let raw_base64 = base64::engine::general_purpose::STANDARD.encode(raw);
14962    let source_key_object_data = javascript_crypto_serialize_sandbox_key_object(
14963        &JavascriptCryptoKeyMaterial::Secret(raw.to_vec()),
14964    )?;
14965    Ok(json!({
14966        "type": "secret",
14967        "algorithm": algorithm,
14968        "extractable": extractable,
14969        "usages": usages,
14970        "_raw": raw_base64,
14971        "_sourceKeyObjectData": source_key_object_data,
14972    }))
14973}
14974
14975fn javascript_crypto_subtle_key_raw(key: &Value, label: &str) -> Result<Vec<u8>, SidecarError> {
14976    let raw = key.get("_raw").and_then(Value::as_str).ok_or_else(|| {
14977        SidecarError::InvalidState(format!("{label} must be a raw secret CryptoKey"))
14978    })?;
14979    base64::engine::general_purpose::STANDARD
14980        .decode(raw)
14981        .map_err(|error| SidecarError::InvalidState(format!("{label} raw base64: {error}")))
14982}
14983
14984fn service_javascript_crypto_subtle_aes_crypt_sync_rpc(
14985    op: &str,
14986    parsed: &Value,
14987) -> Result<Value, SidecarError> {
14988    let algorithm = parsed.get("algorithm").ok_or_else(|| {
14989        SidecarError::InvalidState(format!("crypto.subtle.{op} missing algorithm"))
14990    })?;
14991    let name = javascript_crypto_subtle_algorithm_name(algorithm, &format!("crypto.subtle.{op}"))?;
14992    if name != "AES-GCM" {
14993        return Err(SidecarError::InvalidState(format!(
14994            "Unsupported subtle AES operation algorithm: {name}"
14995        )));
14996    }
14997    let key = javascript_crypto_subtle_key_raw(
14998        parsed
14999            .get("key")
15000            .ok_or_else(|| SidecarError::InvalidState(format!("crypto.subtle.{op} missing key")))?,
15001        &format!("crypto.subtle.{op} key"),
15002    )?;
15003    let iv = algorithm.get("iv").and_then(Value::as_str).ok_or_else(|| {
15004        SidecarError::InvalidState(format!("crypto.subtle.{op} AES-GCM missing iv"))
15005    })?;
15006    let iv = base64::engine::general_purpose::STANDARD
15007        .decode(iv)
15008        .map_err(|error| {
15009            SidecarError::InvalidState(format!("crypto.subtle.{op} iv base64: {error}"))
15010        })?;
15011    let data = parsed
15012        .get("data")
15013        .and_then(Value::as_str)
15014        .ok_or_else(|| SidecarError::InvalidState(format!("crypto.subtle.{op} missing data")))?;
15015    let mut data = base64::engine::general_purpose::STANDARD
15016        .decode(data)
15017        .map_err(|error| {
15018            SidecarError::InvalidState(format!("crypto.subtle.{op} data base64: {error}"))
15019        })?;
15020    let tag_len = javascript_crypto_subtle_aes_gcm_tag_len(algorithm)?;
15021    let mut options = Map::new();
15022    options.insert(String::from("authTagLength"), json!(tag_len));
15023    if let Some(additional_data) = algorithm.get("additionalData").and_then(Value::as_str) {
15024        options.insert(
15025            String::from("aad"),
15026            Value::String(additional_data.to_string()),
15027        );
15028    }
15029    let decrypt = op == "decrypt";
15030    if decrypt {
15031        if data.len() < tag_len {
15032            return Err(SidecarError::InvalidState(String::from(
15033                "crypto.subtle.decrypt AES-GCM data shorter than auth tag",
15034            )));
15035        }
15036        let auth_tag = data.split_off(data.len() - tag_len);
15037        options.insert(
15038            String::from("authTag"),
15039            Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
15040        );
15041    }
15042    let cipher_name = format!("aes-{}-gcm", key.len() * 8);
15043    let mut context = javascript_crypto_build_cipher_context(
15044        &cipher_name,
15045        &key,
15046        Some(&iv),
15047        decrypt,
15048        Some(&Value::Object(options)),
15049    )?;
15050    let mut output = javascript_crypto_cipher_update(&mut context, &data)?;
15051    output.extend(javascript_crypto_cipher_finalize(&mut context)?);
15052    if !decrypt {
15053        let mut auth_tag = vec![0_u8; tag_len];
15054        context
15055            .get_tag(&mut auth_tag)
15056            .map_err(javascript_crypto_openssl_error)?;
15057        output.extend(auth_tag);
15058    }
15059    Ok(Value::String(
15060        serde_json::to_string(&json!({
15061            "data": base64::engine::general_purpose::STANDARD.encode(output),
15062        }))
15063        .map_err(|error| {
15064            SidecarError::InvalidState(format!("serialize crypto.subtle {op}: {error}"))
15065        })?,
15066    ))
15067}
15068
15069fn javascript_crypto_subtle_aes_gcm_tag_len(algorithm: &Value) -> Result<usize, SidecarError> {
15070    let tag_bits = algorithm
15071        .get("tagLength")
15072        .and_then(Value::as_u64)
15073        .unwrap_or(128);
15074    if !tag_bits.is_multiple_of(8) {
15075        return Err(SidecarError::InvalidState(String::from(
15076            "crypto.subtle AES-GCM tagLength must be byte-aligned",
15077        )));
15078    }
15079    usize::try_from(tag_bits / 8).map_err(|_| {
15080        SidecarError::InvalidState(String::from("crypto.subtle AES-GCM tagLength too large"))
15081    })
15082}
15083
15084fn service_javascript_crypto_cipheriv_inner(
15085    request: &JavascriptSyncRpcRequest,
15086    decrypt: bool,
15087) -> Result<Value, SidecarError> {
15088    let label = if decrypt {
15089        "crypto.decipheriv"
15090    } else {
15091        "crypto.cipheriv"
15092    };
15093    let algorithm = javascript_sync_rpc_arg_str(&request.args, 0, &format!("{label} algorithm"))?;
15094    let key = javascript_sync_rpc_base64_arg(&request.args, 1, &format!("{label} key"))?;
15095    let iv = javascript_sync_rpc_base64_arg_optional(&request.args, 2, &format!("{label} iv"))?;
15096    let data = javascript_sync_rpc_base64_arg(&request.args, 3, &format!("{label} data"))?;
15097    let options =
15098        javascript_sync_rpc_json_arg_optional(&request.args, 4, &format!("{label} options"))?;
15099    let auth_tag_len = javascript_crypto_requested_aead_tag_len(algorithm, options.as_ref())?;
15100    let mut context = javascript_crypto_build_cipher_context(
15101        algorithm,
15102        &key,
15103        iv.as_deref(),
15104        decrypt,
15105        options.as_ref(),
15106    )?;
15107    let payload = javascript_crypto_cipher_update(&mut context, &data)?;
15108    let final_bytes = javascript_crypto_cipher_finalize(&mut context)?;
15109    if decrypt {
15110        let mut output = payload;
15111        output.extend(final_bytes);
15112        return Ok(Value::String(
15113            base64::engine::general_purpose::STANDARD.encode(output),
15114        ));
15115    }
15116
15117    let mut response = Map::new();
15118    let mut encrypted = payload;
15119    encrypted.extend(final_bytes);
15120    response.insert(
15121        String::from("data"),
15122        Value::String(base64::engine::general_purpose::STANDARD.encode(encrypted)),
15123    );
15124    if javascript_crypto_is_aead(algorithm) {
15125        let mut auth_tag = vec![0_u8; auth_tag_len];
15126        context
15127            .get_tag(&mut auth_tag)
15128            .map_err(javascript_crypto_openssl_error)?;
15129        response.insert(
15130            String::from("authTag"),
15131            Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
15132        );
15133    }
15134    Ok(Value::String(serde_json::to_string(&response).map_err(
15135        |error| SidecarError::InvalidState(format!("serialize {label} response: {error}")),
15136    )?))
15137}
15138
15139fn javascript_sync_rpc_base64_arg_optional(
15140    args: &[Value],
15141    index: usize,
15142    label: &str,
15143) -> Result<Option<Vec<u8>>, SidecarError> {
15144    if args.get(index).is_none() || args[index].is_null() {
15145        return Ok(None);
15146    }
15147    javascript_sync_rpc_base64_arg(args, index, label).map(Some)
15148}
15149
15150fn javascript_sync_rpc_json_arg_optional(
15151    args: &[Value],
15152    index: usize,
15153    label: &str,
15154) -> Result<Option<Value>, SidecarError> {
15155    if args.get(index).is_none() || args[index].is_null() {
15156        return Ok(None);
15157    }
15158    let raw = javascript_sync_rpc_arg_str(args, index, label)?;
15159    serde_json::from_str(raw)
15160        .map(Some)
15161        .map_err(|error| SidecarError::InvalidState(format!("{label} must be valid JSON: {error}")))
15162}
15163
15164fn javascript_crypto_parse_direct_key_input(
15165    raw: &str,
15166    expected: Option<&str>,
15167    label: &str,
15168) -> Result<JavascriptDirectKeyInput, SidecarError> {
15169    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
15170        SidecarError::InvalidState(format!("{label} must be valid JSON: {error}"))
15171    })?;
15172    let padding = match parsed.as_object().and_then(|value| value.get("padding")) {
15173        Some(value) => javascript_crypto_padding_from_value(value)?,
15174        None => None,
15175    };
15176    Ok(JavascriptDirectKeyInput {
15177        key: javascript_crypto_parse_key_material_value(&parsed, expected, label)?,
15178        padding,
15179    })
15180}
15181
15182fn javascript_crypto_parse_key_material_value(
15183    value: &Value,
15184    expected: Option<&str>,
15185    label: &str,
15186) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15187    if let Some(object) = value.as_object() {
15188        if object.get("__type").and_then(Value::as_str) == Some("keyObject") {
15189            let serialized = object.get("value").ok_or_else(|| {
15190                SidecarError::InvalidState(format!("{label} keyObject is missing a value"))
15191            })?;
15192            return javascript_crypto_parse_serialized_key_object(serialized, expected, label);
15193        }
15194        if object.contains_key("type") && (object.contains_key("pem") || object.contains_key("raw"))
15195        {
15196            return javascript_crypto_parse_serialized_key_object(value, expected, label);
15197        }
15198        if let Some(source) = object.get("key") {
15199            return javascript_crypto_parse_key_source(
15200                source,
15201                object.get("format").and_then(Value::as_str),
15202                object.get("type").and_then(Value::as_str),
15203                expected,
15204                label,
15205            );
15206        }
15207    }
15208    javascript_crypto_parse_key_source(value, None, None, expected, label)
15209}
15210
15211fn javascript_crypto_parse_key_source(
15212    source: &Value,
15213    format: Option<&str>,
15214    kind: Option<&str>,
15215    expected: Option<&str>,
15216    label: &str,
15217) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15218    match source {
15219        Value::String(pem) => javascript_crypto_parse_key_from_pem(pem.as_bytes(), expected, label),
15220        Value::Object(object) if object.get("__type").and_then(Value::as_str) == Some("buffer") => {
15221            let data = javascript_crypto_decode_bridge_buffer(source, label)?;
15222            javascript_crypto_parse_key_from_bytes(&data, format, kind, expected, label)
15223        }
15224        Value::Object(_) => {
15225            if format == Some("jwk") {
15226                return Err(SidecarError::InvalidState(format!(
15227                    "{label} jwk inputs are not supported yet"
15228                )));
15229            }
15230            Err(SidecarError::InvalidState(format!(
15231                "{label} has an unsupported key shape"
15232            )))
15233        }
15234        _ => Err(SidecarError::InvalidState(format!(
15235            "{label} has an unsupported key value"
15236        ))),
15237    }
15238}
15239
15240fn javascript_crypto_parse_key_from_pem(
15241    pem: &[u8],
15242    expected: Option<&str>,
15243    label: &str,
15244) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15245    match expected {
15246        Some("private") => PKey::private_key_from_pem(pem)
15247            .map(JavascriptCryptoKeyMaterial::Private)
15248            .map_err(|error| {
15249                SidecarError::InvalidState(format!("{label} private key is invalid: {error}"))
15250            }),
15251        Some("public") => PKey::public_key_from_pem(pem)
15252            .map(JavascriptCryptoKeyMaterial::Public)
15253            .map_err(|error| {
15254                SidecarError::InvalidState(format!("{label} public key is invalid: {error}"))
15255            }),
15256        _ => PKey::private_key_from_pem(pem)
15257            .map(JavascriptCryptoKeyMaterial::Private)
15258            .or_else(|_| PKey::public_key_from_pem(pem).map(JavascriptCryptoKeyMaterial::Public))
15259            .map_err(|error| {
15260                SidecarError::InvalidState(format!("{label} PEM key is invalid: {error}"))
15261            }),
15262    }
15263}
15264
15265fn javascript_crypto_parse_key_from_bytes(
15266    der: &[u8],
15267    format: Option<&str>,
15268    kind: Option<&str>,
15269    expected: Option<&str>,
15270    label: &str,
15271) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15272    match (format.unwrap_or("der"), kind.or(expected)) {
15273        ("der", Some("pkcs8")) | ("der", Some("private")) => PKey::private_key_from_der(der)
15274            .map(JavascriptCryptoKeyMaterial::Private)
15275            .map_err(|error| {
15276                SidecarError::InvalidState(format!("{label} private key DER is invalid: {error}"))
15277            }),
15278        ("der", Some("spki")) | ("der", Some("public")) => PKey::public_key_from_der(der)
15279            .map(JavascriptCryptoKeyMaterial::Public)
15280            .map_err(|error| {
15281                SidecarError::InvalidState(format!("{label} public key DER is invalid: {error}"))
15282            }),
15283        _ => Err(SidecarError::InvalidState(format!(
15284            "{label} unsupported key bytes format"
15285        ))),
15286    }
15287}
15288
15289fn javascript_crypto_parse_serialized_key_object(
15290    value: &Value,
15291    expected: Option<&str>,
15292    label: &str,
15293) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15294    let serialized: JavascriptSerializedSandboxKeyObject = serde_json::from_value(value.clone())
15295        .map_err(|error| {
15296            SidecarError::InvalidState(format!("{label} keyObject is invalid: {error}"))
15297        })?;
15298    match serialized.kind.as_str() {
15299        "secret" => {
15300            if expected == Some("public") || expected == Some("private") {
15301                return Err(SidecarError::InvalidState(format!(
15302                    "{label} expected an asymmetric key"
15303                )));
15304            }
15305            Ok(JavascriptCryptoKeyMaterial::Secret(
15306                base64::engine::general_purpose::STANDARD
15307                    .decode(serialized.raw.unwrap_or_default())
15308                    .map_err(|error| {
15309                        SidecarError::InvalidState(format!(
15310                            "{label} secret key contains invalid base64: {error}"
15311                        ))
15312                    })?,
15313            ))
15314        }
15315        "private" => {
15316            let pem = serialized.pem.ok_or_else(|| {
15317                SidecarError::InvalidState(format!("{label} private keyObject is missing pem"))
15318            })?;
15319            javascript_crypto_parse_key_from_pem(pem.as_bytes(), Some("private"), label)
15320        }
15321        "public" => {
15322            let pem = serialized.pem.ok_or_else(|| {
15323                SidecarError::InvalidState(format!("{label} public keyObject is missing pem"))
15324            })?;
15325            javascript_crypto_parse_key_from_pem(pem.as_bytes(), Some("public"), label)
15326        }
15327        other => Err(SidecarError::InvalidState(format!(
15328            "{label} has unsupported keyObject type {other}"
15329        ))),
15330    }
15331}
15332
15333fn javascript_crypto_expect_private_key(
15334    key: JavascriptCryptoKeyMaterial,
15335    label: &str,
15336) -> Result<PKey<Private>, SidecarError> {
15337    match key {
15338        JavascriptCryptoKeyMaterial::Private(key) => Ok(key),
15339        _ => Err(SidecarError::InvalidState(format!(
15340            "{label} requires a private key"
15341        ))),
15342    }
15343}
15344
15345fn javascript_crypto_expect_public_key(
15346    key: JavascriptCryptoKeyMaterial,
15347    label: &str,
15348) -> Result<PKey<Public>, SidecarError> {
15349    match key {
15350        JavascriptCryptoKeyMaterial::Public(key) => Ok(key),
15351        JavascriptCryptoKeyMaterial::Private(key) => {
15352            let pem = key
15353                .public_key_to_pem()
15354                .map_err(javascript_crypto_openssl_error)?;
15355            PKey::public_key_from_pem(&pem).map_err(javascript_crypto_openssl_error)
15356        }
15357        _ => Err(SidecarError::InvalidState(format!(
15358            "{label} requires a public key"
15359        ))),
15360    }
15361}
15362
15363fn javascript_crypto_new_signer<'a>(
15364    algorithm: Option<&'a str>,
15365    key: &'a PKey<Private>,
15366) -> Result<Signer<'a>, SidecarError> {
15367    if matches!(key.id(), PKeyId::ED25519 | PKeyId::ED448) || algorithm.is_none() {
15368        return Signer::new_without_digest(key).map_err(javascript_crypto_openssl_error);
15369    }
15370    Signer::new(
15371        javascript_crypto_message_digest_from_name(algorithm.ok_or_else(|| {
15372            SidecarError::InvalidState(String::from("crypto.sign requires a digest algorithm"))
15373        })?)?,
15374        key,
15375    )
15376    .map_err(javascript_crypto_openssl_error)
15377}
15378
15379fn javascript_crypto_new_verifier<'a>(
15380    algorithm: Option<&'a str>,
15381    key: &'a PKey<Public>,
15382) -> Result<Verifier<'a>, SidecarError> {
15383    if matches!(key.id(), PKeyId::ED25519 | PKeyId::ED448) || algorithm.is_none() {
15384        return Verifier::new_without_digest(key).map_err(javascript_crypto_openssl_error);
15385    }
15386    Verifier::new(
15387        javascript_crypto_message_digest_from_name(algorithm.ok_or_else(|| {
15388            SidecarError::InvalidState(String::from("crypto.verify requires a digest algorithm"))
15389        })?)?,
15390        key,
15391    )
15392    .map_err(javascript_crypto_openssl_error)
15393}
15394
15395fn javascript_crypto_message_digest_from_name(name: &str) -> Result<MessageDigest, SidecarError> {
15396    match name.trim().to_ascii_lowercase().replace('-', "").as_str() {
15397        "md5" => Ok(MessageDigest::md5()),
15398        "sha1" => Ok(MessageDigest::sha1()),
15399        "sha256" => Ok(MessageDigest::sha256()),
15400        "sha384" => Ok(MessageDigest::sha384()),
15401        "sha512" => Ok(MessageDigest::sha512()),
15402        other => Err(SidecarError::InvalidState(format!(
15403            "unsupported crypto digest algorithm {other}"
15404        ))),
15405    }
15406}
15407
15408fn javascript_crypto_padding_from_value(value: &Value) -> Result<Option<Padding>, SidecarError> {
15409    let Some(number) = value.as_i64() else {
15410        return Ok(None);
15411    };
15412    let padding = match number {
15413        1 => Padding::PKCS1,
15414        3 => Padding::NONE,
15415        4 => Padding::PKCS1_OAEP,
15416        6 => Padding::PKCS1_PSS,
15417        other => {
15418            return Err(SidecarError::InvalidState(format!(
15419                "unsupported RSA padding constant {other}"
15420            )));
15421        }
15422    };
15423    Ok(Some(padding))
15424}
15425
15426fn javascript_crypto_decode_bridge_buffer(
15427    value: &Value,
15428    label: &str,
15429) -> Result<Vec<u8>, SidecarError> {
15430    let base64_value = value
15431        .as_object()
15432        .filter(|object| object.get("__type").and_then(Value::as_str) == Some("buffer"))
15433        .and_then(|object| object.get("value"))
15434        .and_then(Value::as_str)
15435        .ok_or_else(|| {
15436            SidecarError::InvalidState(format!("{label} must be a serialized bridge buffer"))
15437        })?;
15438    base64::engine::general_purpose::STANDARD
15439        .decode(base64_value)
15440        .map_err(|error| {
15441            SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
15442        })
15443}
15444
15445fn javascript_crypto_serialize_sandbox_key_object(
15446    key: &JavascriptCryptoKeyMaterial,
15447) -> Result<Value, SidecarError> {
15448    let serialized = match key {
15449        JavascriptCryptoKeyMaterial::Private(key) => JavascriptSerializedSandboxKeyObject {
15450            kind: String::from("private"),
15451            pem: Some(
15452                String::from_utf8(
15453                    key.private_key_to_pem_pkcs8()
15454                        .map_err(javascript_crypto_openssl_error)?,
15455                )
15456                .map_err(|error| {
15457                    SidecarError::InvalidState(format!("private key PEM is not utf8: {error}"))
15458                })?,
15459            ),
15460            raw: None,
15461            asymmetric_key_type: javascript_crypto_pkey_type_name(key.id()),
15462            asymmetric_key_details: None,
15463            jwk: None,
15464        },
15465        JavascriptCryptoKeyMaterial::Public(key) => JavascriptSerializedSandboxKeyObject {
15466            kind: String::from("public"),
15467            pem: Some(
15468                String::from_utf8(
15469                    key.public_key_to_pem()
15470                        .map_err(javascript_crypto_openssl_error)?,
15471                )
15472                .map_err(|error| {
15473                    SidecarError::InvalidState(format!("public key PEM is not utf8: {error}"))
15474                })?,
15475            ),
15476            raw: None,
15477            asymmetric_key_type: javascript_crypto_pkey_type_name(key.id()),
15478            asymmetric_key_details: None,
15479            jwk: None,
15480        },
15481        JavascriptCryptoKeyMaterial::Secret(raw) => JavascriptSerializedSandboxKeyObject {
15482            kind: String::from("secret"),
15483            pem: None,
15484            raw: Some(base64::engine::general_purpose::STANDARD.encode(raw)),
15485            asymmetric_key_type: None,
15486            asymmetric_key_details: None,
15487            jwk: None,
15488        },
15489    };
15490    serde_json::to_value(serialized)
15491        .map_err(|error| SidecarError::InvalidState(format!("serialize key object: {error}")))
15492}
15493
15494fn javascript_crypto_pkey_type_name(id: PKeyId) -> Option<String> {
15495    match id {
15496        PKeyId::RSA => Some(String::from("rsa")),
15497        PKeyId::EC => Some(String::from("ec")),
15498        PKeyId::ED25519 => Some(String::from("ed25519")),
15499        PKeyId::ED448 => Some(String::from("ed448")),
15500        PKeyId::X25519 => Some(String::from("x25519")),
15501        PKeyId::X448 => Some(String::from("x448")),
15502        PKeyId::DH => Some(String::from("dh")),
15503        _ => None,
15504    }
15505}
15506
15507fn javascript_crypto_rsa_output_size(
15508    key: &JavascriptCryptoKeyMaterial,
15509) -> Result<usize, SidecarError> {
15510    match key {
15511        JavascriptCryptoKeyMaterial::Private(key) => key
15512            .rsa()
15513            .map(|rsa| rsa.size() as usize)
15514            .map_err(javascript_crypto_openssl_error),
15515        JavascriptCryptoKeyMaterial::Public(key) => key
15516            .rsa()
15517            .map(|rsa| rsa.size() as usize)
15518            .map_err(javascript_crypto_openssl_error),
15519        JavascriptCryptoKeyMaterial::Secret(_) => Err(SidecarError::InvalidState(String::from(
15520            "RSA operations require an asymmetric key",
15521        ))),
15522    }
15523}
15524
15525fn javascript_crypto_parse_serialized_options_arg(
15526    args: &[Value],
15527    index: usize,
15528    label: &str,
15529) -> Result<Option<Value>, SidecarError> {
15530    let Some(raw) = args.get(index).and_then(Value::as_str) else {
15531        return Ok(None);
15532    };
15533    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
15534        SidecarError::InvalidState(format!("{label} must be valid JSON: {error}"))
15535    })?;
15536    if parsed.get("hasOptions").and_then(Value::as_bool) == Some(true) {
15537        Ok(parsed.get("options").cloned())
15538    } else {
15539        Ok(None)
15540    }
15541}
15542
15543fn javascript_crypto_u32_from_bridge_value(
15544    value: &Value,
15545    label: &str,
15546) -> Result<u32, SidecarError> {
15547    if let Some(number) = value.as_u64() {
15548        return u32::try_from(number)
15549            .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")));
15550    }
15551    let bytes = javascript_crypto_decode_bridge_buffer(value, label)?;
15552    if bytes.len() > 4 {
15553        return Err(SidecarError::InvalidState(format!(
15554            "{label} buffer is too large for u32"
15555        )));
15556    }
15557    Ok(bytes
15558        .into_iter()
15559        .fold(0_u32, |acc, byte| (acc << 8) | u32::from(byte)))
15560}
15561
15562fn javascript_crypto_bignum_from_bridge_value(
15563    value: &Value,
15564    label: &str,
15565) -> Result<BigNum, SidecarError> {
15566    if let Some(object) = value.as_object() {
15567        if object.get("__type").and_then(Value::as_str) == Some("bigint") {
15568            let decimal = object.get("value").and_then(Value::as_str).ok_or_else(|| {
15569                SidecarError::InvalidState(format!("{label} bigint is missing a value"))
15570            })?;
15571            return BigNum::from_dec_str(decimal).map_err(javascript_crypto_openssl_error);
15572        }
15573    }
15574    let bytes = javascript_crypto_decode_bridge_buffer(value, label)?;
15575    BigNum::from_slice(&bytes).map_err(javascript_crypto_openssl_error)
15576}
15577
15578fn javascript_crypto_curve_nid(name: &str) -> Result<Nid, SidecarError> {
15579    match name {
15580        "prime256v1" | "P-256" => Ok(Nid::X9_62_PRIME256V1),
15581        "secp384r1" | "P-384" => Ok(Nid::SECP384R1),
15582        "secp521r1" | "P-521" => Ok(Nid::SECP521R1),
15583        "secp256k1" => Ok(Nid::SECP256K1),
15584        other => Err(SidecarError::InvalidState(format!(
15585            "unsupported EC curve {other}"
15586        ))),
15587    }
15588}
15589
15590fn javascript_crypto_named_dh_group(name: &str) -> Result<Dh<Params>, SidecarError> {
15591    match name {
15592        "modp2" => Dh::get_1024_160().map_err(javascript_crypto_openssl_error),
15593        "modp14" | "modp15" | "modp16" | "modp17" | "modp18" => {
15594            Dh::get_2048_256().map_err(javascript_crypto_openssl_error)
15595        }
15596        other => Err(SidecarError::InvalidState(format!(
15597            "unsupported Diffie-Hellman group {other}"
15598        ))),
15599    }
15600}
15601
15602fn javascript_crypto_clone_dh_params(params: &Dh<Params>) -> Result<Dh<Params>, SidecarError> {
15603    Dh::from_pqg(
15604        params
15605            .prime_p()
15606            .to_owned()
15607            .map_err(javascript_crypto_openssl_error)?,
15608        params
15609            .prime_q()
15610            .map(|value| value.to_owned().map_err(javascript_crypto_openssl_error))
15611            .transpose()?,
15612        params
15613            .generator()
15614            .to_owned()
15615            .map_err(javascript_crypto_openssl_error)?,
15616    )
15617    .map_err(javascript_crypto_openssl_error)
15618}
15619
15620fn javascript_crypto_build_dh_params(args: &[Value]) -> Result<Dh<Params>, SidecarError> {
15621    let Some(first) = args.first() else {
15622        return Err(SidecarError::InvalidState(String::from(
15623            "Diffie-Hellman session args are required",
15624        )));
15625    };
15626    if let Some(bits) = first.as_u64() {
15627        let generator = args
15628            .get(1)
15629            .map(|value| javascript_crypto_u32_from_bridge_value(value, "Diffie-Hellman generator"))
15630            .transpose()?
15631            .unwrap_or(2);
15632        return Dh::generate_params(bits as u32, generator)
15633            .map_err(javascript_crypto_openssl_error);
15634    }
15635    let prime = javascript_crypto_bignum_from_bridge_value(first, "Diffie-Hellman prime")?;
15636    let generator = args
15637        .get(1)
15638        .map(|value| javascript_crypto_bignum_from_bridge_value(value, "Diffie-Hellman generator"))
15639        .transpose()?
15640        .unwrap_or(BigNum::from_u32(2).map_err(javascript_crypto_openssl_error)?);
15641    Dh::from_pqg(prime, None, generator).map_err(javascript_crypto_openssl_error)
15642}
15643
15644fn javascript_crypto_call_dh_session(
15645    session: &mut ActiveDhSession,
15646    method: &str,
15647    args: &[Value],
15648) -> Result<(Value, bool), SidecarError> {
15649    match method {
15650        "verifyError" => Ok((Value::Null, false)),
15651        "generateKeys" => {
15652            if session.key_pair.is_none() {
15653                session.key_pair = Some(
15654                    javascript_crypto_clone_dh_params(&session.params)?
15655                        .generate_key()
15656                        .map_err(javascript_crypto_openssl_error)?,
15657                );
15658            }
15659            let public = session
15660                .key_pair
15661                .as_ref()
15662                .expect("dh key pair")
15663                .public_key()
15664                .to_vec();
15665            Ok((javascript_crypto_bridge_buffer_value(&public), true))
15666        }
15667        "computeSecret" => {
15668            if session.key_pair.is_none() {
15669                session.key_pair = Some(
15670                    javascript_crypto_clone_dh_params(&session.params)?
15671                        .generate_key()
15672                        .map_err(javascript_crypto_openssl_error)?,
15673                );
15674            }
15675            let peer = javascript_crypto_bignum_from_bridge_value(
15676                args.first().ok_or_else(|| {
15677                    SidecarError::InvalidState(String::from(
15678                        "computeSecret requires peer public key",
15679                    ))
15680                })?,
15681                "Diffie-Hellman peer public key",
15682            )?;
15683            let secret = session
15684                .key_pair
15685                .as_ref()
15686                .expect("dh key pair")
15687                .compute_key(&peer)
15688                .map_err(javascript_crypto_openssl_error)?;
15689            Ok((javascript_crypto_bridge_buffer_value(&secret), true))
15690        }
15691        "getPrime" => Ok((
15692            javascript_crypto_bridge_buffer_value(&session.params.prime_p().to_vec()),
15693            true,
15694        )),
15695        "getGenerator" => Ok((
15696            javascript_crypto_bridge_buffer_value(&session.params.generator().to_vec()),
15697            true,
15698        )),
15699        "getPublicKey" => {
15700            if session.key_pair.is_none() {
15701                session.key_pair = Some(
15702                    javascript_crypto_clone_dh_params(&session.params)?
15703                        .generate_key()
15704                        .map_err(javascript_crypto_openssl_error)?,
15705                );
15706            }
15707            Ok((
15708                javascript_crypto_bridge_buffer_value(
15709                    &session
15710                        .key_pair
15711                        .as_ref()
15712                        .expect("dh key pair")
15713                        .public_key()
15714                        .to_vec(),
15715                ),
15716                true,
15717            ))
15718        }
15719        "getPrivateKey" => {
15720            if session.key_pair.is_none() {
15721                session.key_pair = Some(
15722                    javascript_crypto_clone_dh_params(&session.params)?
15723                        .generate_key()
15724                        .map_err(javascript_crypto_openssl_error)?,
15725                );
15726            }
15727            Ok((
15728                javascript_crypto_bridge_buffer_value(
15729                    &session
15730                        .key_pair
15731                        .as_ref()
15732                        .expect("dh key pair")
15733                        .private_key()
15734                        .to_vec(),
15735                ),
15736                true,
15737            ))
15738        }
15739        other => Err(SidecarError::InvalidState(format!(
15740            "Unsupported Diffie-Hellman method: {other}"
15741        ))),
15742    }
15743}
15744
15745fn javascript_crypto_call_ecdh_session(
15746    session: &mut ActiveEcdhSession,
15747    method: &str,
15748    args: &[Value],
15749) -> Result<(Value, bool), SidecarError> {
15750    let nid = javascript_crypto_curve_nid(&session.curve)?;
15751    let group = EcGroup::from_curve_name(nid).map_err(javascript_crypto_openssl_error)?;
15752    match method {
15753        "verifyError" => Ok((Value::Null, false)),
15754        "generateKeys" => {
15755            if session.key_pair.is_none() {
15756                session.key_pair =
15757                    Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15758            }
15759            let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
15760            let bytes = session
15761                .key_pair
15762                .as_ref()
15763                .expect("ecdh key pair")
15764                .public_key()
15765                .to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx)
15766                .map_err(javascript_crypto_openssl_error)?;
15767            Ok((javascript_crypto_bridge_buffer_value(&bytes), true))
15768        }
15769        "computeSecret" => {
15770            if session.key_pair.is_none() {
15771                session.key_pair =
15772                    Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15773            }
15774            let peer_bytes = javascript_crypto_decode_bridge_buffer(
15775                args.first().ok_or_else(|| {
15776                    SidecarError::InvalidState(String::from(
15777                        "computeSecret requires peer public key",
15778                    ))
15779                })?,
15780                "ECDH peer public key",
15781            )?;
15782            let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
15783            let peer_point = EcPoint::from_bytes(&group, &peer_bytes, &mut ctx)
15784                .map_err(javascript_crypto_openssl_error)?;
15785            let peer_key = EcKey::from_public_key(&group, &peer_point)
15786                .map_err(javascript_crypto_openssl_error)?;
15787            let private =
15788                PKey::from_ec_key(session.key_pair.as_ref().expect("ecdh key pair").to_owned())
15789                    .map_err(javascript_crypto_openssl_error)?;
15790            let peer = PKey::from_ec_key(peer_key).map_err(javascript_crypto_openssl_error)?;
15791            let mut deriver = Deriver::new(&private).map_err(javascript_crypto_openssl_error)?;
15792            deriver
15793                .set_peer(&peer)
15794                .map_err(javascript_crypto_openssl_error)?;
15795            let secret = deriver
15796                .derive_to_vec()
15797                .map_err(javascript_crypto_openssl_error)?;
15798            Ok((javascript_crypto_bridge_buffer_value(&secret), true))
15799        }
15800        "getPublicKey" => {
15801            if session.key_pair.is_none() {
15802                session.key_pair =
15803                    Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15804            }
15805            let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
15806            let bytes = session
15807                .key_pair
15808                .as_ref()
15809                .expect("ecdh key pair")
15810                .public_key()
15811                .to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx)
15812                .map_err(javascript_crypto_openssl_error)?;
15813            Ok((javascript_crypto_bridge_buffer_value(&bytes), true))
15814        }
15815        "getPrivateKey" => {
15816            if session.key_pair.is_none() {
15817                session.key_pair =
15818                    Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15819            }
15820            Ok((
15821                javascript_crypto_bridge_buffer_value(
15822                    &session
15823                        .key_pair
15824                        .as_ref()
15825                        .expect("ecdh key pair")
15826                        .private_key()
15827                        .to_vec(),
15828                ),
15829                true,
15830            ))
15831        }
15832        other => Err(SidecarError::InvalidState(format!(
15833            "Unsupported Diffie-Hellman method: {other}"
15834        ))),
15835    }
15836}
15837
15838fn javascript_crypto_serialize_encoded_key_value_public(
15839    key: &PKey<Public>,
15840    encoding: Option<&Value>,
15841) -> Result<Value, SidecarError> {
15842    if let Some(encoding) = encoding {
15843        let format = encoding
15844            .get("format")
15845            .and_then(Value::as_str)
15846            .unwrap_or("pem");
15847        return Ok(match format {
15848            "der" => json!({
15849                "kind": "buffer",
15850                "value": base64::engine::general_purpose::STANDARD
15851                    .encode(key.public_key_to_der().map_err(javascript_crypto_openssl_error)?),
15852            }),
15853            _ => json!({
15854                "kind": "string",
15855                "value": String::from_utf8(
15856                    key.public_key_to_pem().map_err(javascript_crypto_openssl_error)?,
15857                )
15858                .map_err(|error| SidecarError::InvalidState(format!("public key PEM utf8: {error}")))?,
15859            }),
15860        });
15861    }
15862    javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Public(
15863        key.to_owned(),
15864    ))
15865}
15866
15867fn javascript_crypto_serialize_encoded_key_value_private(
15868    key: &PKey<Private>,
15869    encoding: Option<&Value>,
15870) -> Result<Value, SidecarError> {
15871    if let Some(encoding) = encoding {
15872        let format = encoding
15873            .get("format")
15874            .and_then(Value::as_str)
15875            .unwrap_or("pem");
15876        return Ok(match format {
15877            "der" => json!({
15878                "kind": "buffer",
15879                "value": base64::engine::general_purpose::STANDARD
15880                    .encode(key.private_key_to_der().map_err(javascript_crypto_openssl_error)?),
15881            }),
15882            _ => json!({
15883                "kind": "string",
15884                "value": String::from_utf8(
15885                    key.private_key_to_pem_pkcs8().map_err(javascript_crypto_openssl_error)?,
15886                )
15887                .map_err(|error| SidecarError::InvalidState(format!("private key PEM utf8: {error}")))?,
15888            }),
15889        });
15890    }
15891    javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Private(
15892        key.to_owned(),
15893    ))
15894}
15895
15896fn javascript_crypto_bridge_buffer_value(bytes: &[u8]) -> Value {
15897    json!({
15898        "__type": "buffer",
15899        "value": base64::engine::general_purpose::STANDARD.encode(bytes),
15900    })
15901}
15902
15903fn javascript_crypto_build_cipher_context(
15904    algorithm: &str,
15905    key: &[u8],
15906    iv: Option<&[u8]>,
15907    decrypt: bool,
15908    options: Option<&Value>,
15909) -> Result<Crypter, SidecarError> {
15910    let cipher = javascript_crypto_cipher_from_name(algorithm)?;
15911    let mode = if decrypt {
15912        Mode::Decrypt
15913    } else {
15914        Mode::Encrypt
15915    };
15916    let mut context =
15917        Crypter::new(cipher, mode, key, iv).map_err(javascript_crypto_openssl_error)?;
15918    if let Some(auto_padding) = options
15919        .and_then(|value| value.get("autoPadding"))
15920        .and_then(Value::as_bool)
15921    {
15922        context.pad(auto_padding);
15923    }
15924    if javascript_crypto_is_aead(algorithm) {
15925        if let Some(aad) = options
15926            .and_then(|value| value.get("aad"))
15927            .and_then(Value::as_str)
15928        {
15929            context
15930                .aad_update(
15931                    &base64::engine::general_purpose::STANDARD
15932                        .decode(aad)
15933                        .map_err(|error| {
15934                            SidecarError::InvalidState(format!(
15935                                "cipher aad contains invalid base64: {error}"
15936                            ))
15937                        })?,
15938                )
15939                .map_err(javascript_crypto_openssl_error)?;
15940        }
15941        if decrypt {
15942            if let Some(auth_tag) = options
15943                .and_then(|value| value.get("authTag"))
15944                .and_then(Value::as_str)
15945            {
15946                let decoded = base64::engine::general_purpose::STANDARD
15947                    .decode(auth_tag)
15948                    .map_err(|error| {
15949                        SidecarError::InvalidState(format!(
15950                            "cipher authTag contains invalid base64: {error}"
15951                        ))
15952                    })?;
15953                context
15954                    .set_tag(&decoded)
15955                    .map_err(javascript_crypto_openssl_error)?;
15956            }
15957        }
15958    }
15959    Ok(context)
15960}
15961
15962fn javascript_crypto_requested_aead_tag_len(
15963    algorithm: &str,
15964    options: Option<&Value>,
15965) -> Result<usize, SidecarError> {
15966    if !javascript_crypto_is_aead(algorithm) {
15967        return Ok(0);
15968    }
15969    let requested = options
15970        .and_then(|value| value.get("authTagLength"))
15971        .and_then(Value::as_u64)
15972        .unwrap_or(javascript_crypto_aead_tag_len(algorithm) as u64);
15973    usize::try_from(requested).map_err(|_| {
15974        SidecarError::InvalidState(String::from("cipher authTagLength must fit within usize"))
15975    })
15976}
15977
15978fn javascript_crypto_cipher_update(
15979    context: &mut Crypter,
15980    data: &[u8],
15981) -> Result<Vec<u8>, SidecarError> {
15982    let mut output = vec![0_u8; data.len() + 32];
15983    let written = context
15984        .update(data, &mut output)
15985        .map_err(javascript_crypto_openssl_error)?;
15986    output.truncate(written);
15987    Ok(output)
15988}
15989
15990fn javascript_crypto_cipher_finalize(context: &mut Crypter) -> Result<Vec<u8>, SidecarError> {
15991    let mut output = vec![0_u8; 32];
15992    let written = context
15993        .finalize(&mut output)
15994        .map_err(javascript_crypto_openssl_error)?;
15995    output.truncate(written);
15996    Ok(output)
15997}
15998
15999fn javascript_crypto_cipher_from_name(name: &str) -> Result<Cipher, SidecarError> {
16000    match name.to_ascii_lowercase().as_str() {
16001        "aes-128-cbc" => Ok(Cipher::aes_128_cbc()),
16002        "aes-192-cbc" => Ok(Cipher::aes_192_cbc()),
16003        "aes-256-cbc" => Ok(Cipher::aes_256_cbc()),
16004        "aes-128-ctr" => Ok(Cipher::aes_128_ctr()),
16005        "aes-192-ctr" => Ok(Cipher::aes_192_ctr()),
16006        "aes-256-ctr" => Ok(Cipher::aes_256_ctr()),
16007        "aes-128-gcm" => Ok(Cipher::aes_128_gcm()),
16008        "aes-192-gcm" => Ok(Cipher::aes_192_gcm()),
16009        "aes-256-gcm" => Ok(Cipher::aes_256_gcm()),
16010        other => Err(SidecarError::InvalidState(format!(
16011            "unsupported crypto cipher algorithm {other}"
16012        ))),
16013    }
16014}
16015
16016fn javascript_crypto_is_aead(algorithm: &str) -> bool {
16017    algorithm.to_ascii_lowercase().ends_with("-gcm")
16018}
16019
16020fn javascript_crypto_aead_tag_len(_algorithm: &str) -> usize {
16021    16
16022}
16023
16024fn javascript_crypto_openssl_error(error: openssl::error::ErrorStack) -> SidecarError {
16025    SidecarError::Execution(format!("crypto operation failed: {error}"))
16026}
16027
16028fn service_javascript_kernel_stdin_sync_rpc(
16029    kernel: &mut SidecarKernel,
16030    process: &mut ActiveProcess,
16031    request: &JavascriptSyncRpcRequest,
16032) -> Result<Value, SidecarError> {
16033    let max_bytes =
16034        javascript_sync_rpc_arg_u64_optional(&request.args, 0, "__kernel_stdin_read max bytes")?
16035            .map(|value| value.clamp(1, DEFAULT_KERNEL_STDIN_READ_MAX_BYTES as u64) as usize)
16036            .unwrap_or(DEFAULT_KERNEL_STDIN_READ_MAX_BYTES);
16037    let timeout_ms =
16038        javascript_sync_rpc_arg_u64_optional(&request.args, 1, "__kernel_stdin_read timeout ms")?
16039            .unwrap_or(DEFAULT_KERNEL_STDIN_READ_TIMEOUT_MS);
16040
16041    match kernel
16042        .fd_read_with_timeout_result(
16043            EXECUTION_DRIVER_NAME,
16044            process.kernel_pid,
16045            0,
16046            max_bytes,
16047            Some(Duration::from_millis(timeout_ms)),
16048        )
16049        .map_err(kernel_error)
16050    {
16051        Ok(Some(chunk)) if !chunk.is_empty() => Ok(json!({
16052            "dataBase64": base64::engine::general_purpose::STANDARD.encode(chunk),
16053        })),
16054        Ok(Some(_)) => Ok(Value::Null),
16055        Ok(None) => Ok(json!({
16056            "done": true,
16057        })),
16058        Err(SidecarError::Kernel(error)) if error.starts_with("EAGAIN:") => Ok(Value::Null),
16059        Err(error) => Err(error),
16060    }
16061}
16062
16063fn service_javascript_pty_set_raw_mode_sync_rpc(
16064    kernel: &mut SidecarKernel,
16065    process: &mut ActiveProcess,
16066    request: &JavascriptSyncRpcRequest,
16067) -> Result<Value, SidecarError> {
16068    let enabled = javascript_sync_rpc_arg_bool(&request.args, 0, "__pty_set_raw_mode enabled")?;
16069    kernel
16070        .pty_set_discipline(
16071            EXECUTION_DRIVER_NAME,
16072            process.kernel_pid,
16073            0,
16074            LineDisciplineConfig {
16075                canonical: Some(!enabled),
16076                echo: Some(!enabled),
16077                isig: Some(!enabled),
16078            },
16079        )
16080        .map_err(kernel_error)?;
16081    Ok(Value::Null)
16082}
16083
16084fn service_javascript_kernel_stdio_write_sync_rpc(
16085    kernel: &mut SidecarKernel,
16086    process: &mut ActiveProcess,
16087    request: &JavascriptSyncRpcRequest,
16088) -> Result<Value, SidecarError> {
16089    let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_stdio_write fd")?;
16090    let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "__kernel_stdio_write chunk")?;
16091
16092    let written = match fd {
16093        1 => kernel
16094            .write_process_stdout(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
16095            .map_err(kernel_error)?,
16096        2 => kernel
16097            .write_process_stderr(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
16098            .map_err(kernel_error)?,
16099        other => {
16100            return Err(SidecarError::InvalidState(format!(
16101                "__kernel_stdio_write only supports fd 1/2, got {other}"
16102            )));
16103        }
16104    };
16105
16106    let event = if fd == 1 {
16107        ActiveExecutionEvent::Stdout(chunk)
16108    } else {
16109        ActiveExecutionEvent::Stderr(chunk)
16110    };
16111    process.queue_pending_execution_event(event)?;
16112
16113    Ok(json!(written))
16114}
16115
16116fn service_javascript_kernel_poll_sync_rpc(
16117    kernel: &mut SidecarKernel,
16118    process: &ActiveProcess,
16119    request: &JavascriptSyncRpcRequest,
16120) -> Result<Value, SidecarError> {
16121    let fd_requests: Vec<KernelPollFdRequest> = serde_json::from_value(
16122        request
16123            .args
16124            .first()
16125            .cloned()
16126            .unwrap_or_else(|| Value::Array(Vec::new())),
16127    )
16128    .map_err(|error| {
16129        SidecarError::InvalidState(format!(
16130            "__kernel_poll fd list must be a JSON array of {{ fd, events }} objects: {error}"
16131        ))
16132    })?;
16133    let timeout_ms =
16134        javascript_sync_rpc_arg_u64_optional(&request.args, 1, "__kernel_poll timeout ms")?
16135            .unwrap_or_default();
16136    let timeout_ms = i32::try_from(timeout_ms).map_err(|_| {
16137        SidecarError::InvalidState(String::from("__kernel_poll timeout ms must fit within i32"))
16138    })?;
16139
16140    let poll_fds = fd_requests
16141        .iter()
16142        .map(|entry| PollFd {
16143            fd: entry.fd,
16144            events: PollEvents::from_bits(entry.events),
16145            revents: PollEvents::empty(),
16146        })
16147        .collect::<Vec<_>>();
16148    let result = kernel
16149        .poll_fds(
16150            EXECUTION_DRIVER_NAME,
16151            process.kernel_pid,
16152            poll_fds,
16153            timeout_ms,
16154        )
16155        .map_err(kernel_error)?;
16156
16157    Ok(json!({
16158        "readyCount": result.ready_count,
16159        "fds": result
16160            .fds
16161            .into_iter()
16162            .map(|entry| KernelPollFdResponse {
16163                fd: entry.fd,
16164                events: entry.events.bits(),
16165                revents: entry.revents.bits(),
16166            })
16167            .collect::<Vec<_>>(),
16168    }))
16169}
16170
16171fn install_kernel_stdin_pipe(kernel: &mut SidecarKernel, pid: u32) -> Result<u32, SidecarError> {
16172    let (read_fd, write_fd) = kernel
16173        .open_pipe(EXECUTION_DRIVER_NAME, pid)
16174        .map_err(kernel_error)?;
16175    kernel
16176        .fd_dup2(EXECUTION_DRIVER_NAME, pid, read_fd, 0)
16177        .map_err(kernel_error)?;
16178    kernel
16179        .fd_close(EXECUTION_DRIVER_NAME, pid, read_fd)
16180        .map_err(kernel_error)?;
16181    Ok(write_fd)
16182}
16183
16184fn javascript_child_process_stdin_mode(request: &JavascriptChildProcessSpawnRequest) -> &str {
16185    request
16186        .options
16187        .stdio
16188        .first()
16189        .map(String::as_str)
16190        .unwrap_or("pipe")
16191}
16192
16193pub(crate) fn write_kernel_process_stdin(
16194    kernel: &mut SidecarKernel,
16195    process: &mut ActiveProcess,
16196    chunk: &[u8],
16197) -> Result<(), SidecarError> {
16198    if process.runtime == GuestRuntimeKind::JavaScript {
16199        return Ok(());
16200    }
16201    let Some(writer_fd) = process.kernel_stdin_writer_fd else {
16202        return Ok(());
16203    };
16204    kernel
16205        .fd_write(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd, chunk)
16206        .map(|_| ())
16207        .map_err(kernel_error)
16208}
16209
16210pub(crate) fn close_kernel_process_stdin(
16211    kernel: &mut SidecarKernel,
16212    process: &mut ActiveProcess,
16213) -> Result<(), SidecarError> {
16214    let Some(writer_fd) = process.kernel_stdin_writer_fd.take() else {
16215        return Ok(());
16216    };
16217    kernel
16218        .fd_close(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd)
16219        .map_err(kernel_error)
16220}
16221
16222fn parse_http_header_collection(
16223    headers: &BTreeMap<String, Value>,
16224    label: &str,
16225) -> Result<HttpHeaderCollection, SidecarError> {
16226    let mut normalized = BTreeMap::<String, Vec<String>>::new();
16227    let mut raw_pairs = Vec::new();
16228
16229    for (raw_name, value) in headers {
16230        let normalized_name = raw_name.to_ascii_lowercase();
16231        let values = match value {
16232            Value::String(text) => vec![text.clone()],
16233            Value::Array(values) => values
16234                .iter()
16235                .map(|entry| {
16236                    entry.as_str().map(str::to_owned).ok_or_else(|| {
16237                        SidecarError::InvalidState(format!(
16238                            "{label} header {raw_name} must contain only strings"
16239                        ))
16240                    })
16241                })
16242                .collect::<Result<Vec<_>, _>>()?,
16243            other => {
16244                return Err(SidecarError::InvalidState(format!(
16245                    "{label} header {raw_name} must be a string or string array, received {other}"
16246                )));
16247            }
16248        };
16249        raw_pairs.extend(
16250            values
16251                .iter()
16252                .cloned()
16253                .map(|entry| (raw_name.clone(), entry)),
16254        );
16255        normalized
16256            .entry(normalized_name)
16257            .or_default()
16258            .extend(values);
16259    }
16260
16261    Ok(HttpHeaderCollection {
16262        normalized,
16263        raw_pairs,
16264    })
16265}
16266
16267fn http_headers_json(headers: &HttpHeaderCollection) -> Value {
16268    let map = headers
16269        .normalized
16270        .iter()
16271        .map(|(name, values)| {
16272            let value = if values.len() == 1 {
16273                Value::String(values[0].clone())
16274            } else {
16275                Value::Array(values.iter().cloned().map(Value::String).collect())
16276            };
16277            (name.clone(), value)
16278        })
16279        .collect::<Map<String, Value>>();
16280    Value::Object(map)
16281}
16282
16283fn http_raw_headers_json(headers: &HttpHeaderCollection) -> Value {
16284    Value::Array(
16285        headers
16286            .raw_pairs
16287            .iter()
16288            .flat_map(|(name, value)| [Value::String(name.clone()), Value::String(value.clone())])
16289            .collect(),
16290    )
16291}
16292
16293fn is_loopback_request_host(host: &str) -> bool {
16294    let bare = host
16295        .strip_prefix('[')
16296        .and_then(|value| value.strip_suffix(']'))
16297        .unwrap_or(host);
16298    matches!(bare, "localhost" | "127.0.0.1" | "::1")
16299}
16300
16301fn serialize_http_loopback_request(
16302    url: &Url,
16303    options: &JavascriptHttpRequestOptions,
16304    headers: &HttpHeaderCollection,
16305) -> Result<String, SidecarError> {
16306    let body_base64 = options
16307        .body
16308        .as_ref()
16309        .map(|body| base64::engine::general_purpose::STANDARD.encode(body.as_bytes()));
16310    serde_json::to_string(&json!({
16311        "method": options.method.clone().unwrap_or_else(|| String::from("GET")),
16312        "url": http_request_target(url),
16313        "headers": http_headers_json(headers),
16314        "rawHeaders": http_raw_headers_json(headers),
16315        "bodyBase64": body_base64,
16316    }))
16317    .map_err(|error| SidecarError::Execution(format!("ERR_AGENT_OS_NODE_SYNC_RPC: {error}")))
16318}
16319
16320fn http_request_target(url: &Url) -> String {
16321    let path = if url.path().is_empty() {
16322        "/"
16323    } else {
16324        url.path()
16325    };
16326    format!(
16327        "{path}{}",
16328        url.query()
16329            .map(|query| format!("?{query}"))
16330            .unwrap_or_default()
16331    )
16332}
16333
16334fn find_kernel_http_listener_process(vm: &VmState, port: u16) -> Option<String> {
16335    vm.active_processes
16336        .iter()
16337        .find_map(|(process_id, process)| {
16338            process.tcp_listeners.values().find_map(|listener| {
16339                let socket_id = listener.kernel_socket_id?;
16340                let record = vm.kernel.socket_get(socket_id)?;
16341                let local_addr = record
16342                    .local_address()
16343                    .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
16344                    .unwrap_or_else(|| listener.guest_local_addr());
16345                if local_addr.port() == port && is_vm_local_http_listener_addr(local_addr.ip()) {
16346                    Some(process_id.to_owned())
16347                } else {
16348                    None
16349                }
16350            })
16351        })
16352}
16353
16354fn is_vm_local_http_listener_addr(ip: IpAddr) -> bool {
16355    ip.is_loopback() || ip.is_unspecified()
16356}
16357
16358fn serialize_kernel_http_fetch_request(
16359    port: u16,
16360    path: &str,
16361    options: &JavascriptHttpRequestOptions,
16362    headers: &HttpHeaderCollection,
16363) -> Vec<u8> {
16364    let method = options.method.as_deref().unwrap_or("GET");
16365    let mut lines = vec![format!("{method} {path} HTTP/1.1")];
16366    let mut has_host = false;
16367    let mut has_connection = false;
16368    let mut has_content_length = false;
16369    for (name, values) in &headers.normalized {
16370        match name.as_str() {
16371            "host" => has_host = true,
16372            "connection" => has_connection = true,
16373            "content-length" => has_content_length = true,
16374            _ => {}
16375        }
16376        lines.push(format!("{name}: {}", values.join(", ")));
16377    }
16378    if !has_host {
16379        lines.push(format!("Host: 127.0.0.1:{port}"));
16380    }
16381    if !has_connection {
16382        lines.push(String::from("Connection: close"));
16383    }
16384    let body = options.body.as_deref().unwrap_or("").as_bytes();
16385    if !has_content_length && !body.is_empty() {
16386        lines.push(format!("Content-Length: {}", body.len()));
16387    }
16388    lines.push(String::new());
16389    lines.push(String::new());
16390
16391    let mut request = lines.join("\r\n").into_bytes();
16392    request.extend_from_slice(body);
16393    request
16394}
16395
16396fn parse_kernel_http_fetch_response(
16397    buffer: &[u8],
16398    peer_closed: bool,
16399    url: &str,
16400) -> Result<Option<String>, SidecarError> {
16401    let Some(header_end) = find_http_header_end(buffer) else {
16402        return Ok(None);
16403    };
16404    let header_bytes = &buffer[..header_end];
16405    let head = String::from_utf8_lossy(header_bytes);
16406    let mut lines = head.split("\r\n");
16407    let status_line = lines.next().unwrap_or_default();
16408    let mut status_parts = status_line.splitn(3, ' ');
16409    let version = status_parts.next().unwrap_or_default();
16410    if !version.starts_with("HTTP/") {
16411        return Err(SidecarError::Execution(format!(
16412            "invalid vm.fetch HTTP response status line: {status_line}"
16413        )));
16414    }
16415    let status = status_parts
16416        .next()
16417        .ok_or_else(|| {
16418            SidecarError::Execution(format!(
16419                "invalid vm.fetch HTTP response status line: {status_line}"
16420            ))
16421        })?
16422        .parse::<u16>()
16423        .map_err(|error| {
16424            SidecarError::Execution(format!(
16425                "invalid vm.fetch HTTP response status code in {status_line:?}: {error}"
16426            ))
16427        })?;
16428    let status_text = status_parts.next().unwrap_or_default();
16429    let mut headers = Vec::new();
16430    let mut raw_headers = Vec::new();
16431    let mut content_length = None;
16432    let mut transfer_encoding_values = Vec::new();
16433    for line in lines {
16434        if line.is_empty() {
16435            continue;
16436        }
16437        let Some((name, value)) = line.split_once(':') else {
16438            return Err(SidecarError::Execution(format!(
16439                "invalid vm.fetch HTTP response header line: {line}"
16440            )));
16441        };
16442        let value = value.trim().to_owned();
16443        let normalized = name.to_ascii_lowercase();
16444        if normalized == "content-length" {
16445            content_length = Some(value.parse::<usize>().map_err(|error| {
16446                SidecarError::Execution(format!(
16447                    "invalid vm.fetch Content-Length header {value:?}: {error}"
16448                ))
16449            })?);
16450        } else if normalized == "transfer-encoding" {
16451            transfer_encoding_values.push(value.clone());
16452        }
16453        headers.push(json!([normalized, value.clone()]));
16454        raw_headers.push(Value::String(name.to_owned()));
16455        raw_headers.push(Value::String(value));
16456    }
16457
16458    let body_start = header_end + 4;
16459    let transfer_encoding = transfer_encoding_tokens(&transfer_encoding_values);
16460    let is_chunked = transfer_encoding.iter().any(|token| token == "chunked");
16461    let body = if is_chunked {
16462        if content_length.is_some() {
16463            return Err(SidecarError::Execution(String::from(
16464                "vm.fetch HTTP response cannot include both Transfer-Encoding: chunked and Content-Length",
16465            )));
16466        }
16467        if transfer_encoding.len() != 1 {
16468            return Err(SidecarError::Execution(format!(
16469                "unsupported vm.fetch Transfer-Encoding: {}",
16470                transfer_encoding.join(", ")
16471            )));
16472        }
16473        let Some(decoded) = decode_kernel_http_chunked_body(&buffer[body_start..])? else {
16474            return Ok(None);
16475        };
16476        decoded
16477    } else if !transfer_encoding.is_empty() {
16478        return Err(SidecarError::Execution(format!(
16479            "unsupported vm.fetch Transfer-Encoding: {}",
16480            transfer_encoding.join(", ")
16481        )));
16482    } else if let Some(content_length) = content_length {
16483        let body_end = body_start.saturating_add(content_length);
16484        if buffer.len() < body_end {
16485            return Ok(None);
16486        }
16487        buffer[body_start..body_end].to_vec()
16488    } else if peer_closed {
16489        buffer[body_start..].to_vec()
16490    } else {
16491        return Ok(None);
16492    };
16493
16494    serde_json::to_string(&json!({
16495        "status": status,
16496        "statusText": status_text,
16497        "headers": headers,
16498        "rawHeaders": raw_headers,
16499        "body": base64::engine::general_purpose::STANDARD.encode(&body),
16500        "bodyEncoding": "base64",
16501        "url": url,
16502    }))
16503    .map(Some)
16504    .map_err(|error| SidecarError::Execution(format!("ERR_AGENT_OS_NODE_SYNC_RPC: {error}")))
16505}
16506
16507fn find_http_header_end(buffer: &[u8]) -> Option<usize> {
16508    buffer.windows(4).position(|window| window == b"\r\n\r\n")
16509}
16510
16511fn find_crlf(buffer: &[u8], start: usize) -> Option<usize> {
16512    buffer
16513        .get(start..)?
16514        .windows(2)
16515        .position(|window| window == b"\r\n")
16516        .map(|offset| start + offset)
16517}
16518
16519fn transfer_encoding_tokens(values: &[String]) -> Vec<String> {
16520    values
16521        .iter()
16522        .flat_map(|value| value.split(','))
16523        .map(|token| token.trim().to_ascii_lowercase())
16524        .filter(|token| !token.is_empty())
16525        .collect()
16526}
16527
16528fn decode_kernel_http_chunked_body(buffer: &[u8]) -> Result<Option<Vec<u8>>, SidecarError> {
16529    let mut offset = 0;
16530    let mut body = Vec::new();
16531    loop {
16532        let Some(line_end) = find_crlf(buffer, offset) else {
16533            return Ok(None);
16534        };
16535        let size_line = std::str::from_utf8(&buffer[offset..line_end]).map_err(|error| {
16536            SidecarError::Execution(format!(
16537                "invalid vm.fetch chunk size line encoding: {error}"
16538            ))
16539        })?;
16540        let size_part = size_line.split(';').next().unwrap_or_default();
16541        if size_part.is_empty() || !size_part.bytes().all(|byte| byte.is_ascii_hexdigit()) {
16542            return Err(SidecarError::Execution(format!(
16543                "invalid vm.fetch chunk size line: {size_line:?}"
16544            )));
16545        }
16546        let chunk_size = usize::from_str_radix(size_part, 16).map_err(|error| {
16547            SidecarError::Execution(format!(
16548                "invalid vm.fetch chunk size {size_part:?}: {error}"
16549            ))
16550        })?;
16551        let chunk_start = line_end + 2;
16552        let chunk_end = chunk_start
16553            .checked_add(chunk_size)
16554            .ok_or_else(|| SidecarError::Execution(String::from("vm.fetch chunk size overflow")))?;
16555        if chunk_size > 0 {
16556            let chunk_terminator_end = chunk_end.checked_add(2).ok_or_else(|| {
16557                SidecarError::Execution(String::from("vm.fetch chunk terminator overflow"))
16558            })?;
16559            if chunk_terminator_end > buffer.len() {
16560                return Ok(None);
16561            }
16562            if buffer.get(chunk_end..chunk_terminator_end) != Some(b"\r\n") {
16563                return Err(SidecarError::Execution(String::from(
16564                    "invalid vm.fetch chunk terminator",
16565                )));
16566            }
16567            body.extend_from_slice(&buffer[chunk_start..chunk_end]);
16568            offset = chunk_terminator_end;
16569            continue;
16570        }
16571
16572        if buffer.get(chunk_start..chunk_start + 2) == Some(b"\r\n") {
16573            return Ok(Some(body));
16574        }
16575        let Some(trailer_end) = find_http_header_end(&buffer[chunk_start..]) else {
16576            return Ok(None);
16577        };
16578        let trailer_bytes = &buffer[chunk_start..chunk_start + trailer_end];
16579        let trailers = String::from_utf8_lossy(trailer_bytes);
16580        for line in trailers.split("\r\n") {
16581            if line.is_empty() {
16582                continue;
16583            }
16584            if line.starts_with(' ') || line.starts_with('\t') || !line.contains(':') {
16585                return Err(SidecarError::Execution(format!(
16586                    "invalid vm.fetch chunk trailer line: {line}"
16587                )));
16588            }
16589        }
16590        return Ok(Some(body));
16591    }
16592}
16593
16594fn kernel_http_fetch_target_exit_code(error: &SidecarError) -> Option<i32> {
16595    let SidecarError::Execution(message) = error else {
16596        return None;
16597    };
16598    message
16599        .strip_prefix("vm.fetch target exited before responding (exit code ")?
16600        .strip_suffix(')')?
16601        .parse()
16602        .ok()
16603}
16604
16605fn service_host_fetch_target_event<B>(
16606    bridge: &SharedBridge<B>,
16607    vm_id: &str,
16608    dns: &VmDnsConfig,
16609    socket_paths: &JavascriptSocketPathContext,
16610    kernel: &mut SidecarKernel,
16611    process: &mut ActiveProcess,
16612    resource_limits: &ResourceLimits,
16613    wait: Duration,
16614) -> Result<bool, SidecarError>
16615where
16616    B: NativeSidecarBridge + Send + 'static,
16617    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16618{
16619    let Some(event) = process
16620        .execution
16621        .poll_event_blocking(wait)
16622        .map_err(|error| SidecarError::Execution(error.to_string()))?
16623    else {
16624        return Ok(false);
16625    };
16626
16627    match event {
16628        ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
16629            let network_counts = process.network_resource_counts();
16630            let response = service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
16631                bridge,
16632                vm_id,
16633                dns,
16634                socket_paths,
16635                kernel,
16636                process,
16637                sync_request: &request,
16638                resource_limits,
16639                network_counts,
16640            });
16641            match response {
16642                Ok(result) => process
16643                    .execution
16644                    .respond_javascript_sync_rpc_success(request.id, result)
16645                    .or_else(ignore_stale_javascript_sync_rpc_response)?,
16646                Err(error) => process
16647                    .execution
16648                    .respond_javascript_sync_rpc_error(
16649                        request.id,
16650                        javascript_sync_rpc_error_code(&error),
16651                        error.to_string(),
16652                    )
16653                    .or_else(ignore_stale_javascript_sync_rpc_response)?,
16654            }
16655        }
16656        ActiveExecutionEvent::Exited(code) => {
16657            return Err(SidecarError::Execution(format!(
16658                "vm.fetch target exited before responding (exit code {code})"
16659            )));
16660        }
16661        other => {
16662            process.queue_pending_execution_event(other)?;
16663        }
16664    }
16665    Ok(true)
16666}
16667
16668fn drain_host_fetch_target_events<B>(
16669    bridge: &SharedBridge<B>,
16670    vm_id: &str,
16671    vm: &mut VmState,
16672    target_process_id: &str,
16673    socket_paths: &JavascriptSocketPathContext,
16674    resource_limits: &ResourceLimits,
16675) -> Result<(), SidecarError>
16676where
16677    B: NativeSidecarBridge + Send + 'static,
16678    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16679{
16680    for _ in 0..32 {
16681        let dns = vm.dns.clone();
16682        let Some(process) = vm.active_processes.get_mut(target_process_id) else {
16683            break;
16684        };
16685        let serviced = service_host_fetch_target_event(
16686            bridge,
16687            vm_id,
16688            &dns,
16689            socket_paths,
16690            &mut vm.kernel,
16691            process,
16692            resource_limits,
16693            Duration::from_millis(1),
16694        )?;
16695        if !serviced {
16696            break;
16697        }
16698    }
16699    Ok(())
16700}
16701
16702fn dispatch_kernel_http_fetch<B>(
16703    bridge: &SharedBridge<B>,
16704    vm_id: &str,
16705    vm: &mut VmState,
16706    target_process_id: &str,
16707    port: u16,
16708    path: &str,
16709    options: &JavascriptHttpRequestOptions,
16710    headers: &HttpHeaderCollection,
16711    max_fetch_response_bytes: usize,
16712) -> Result<String, SidecarError>
16713where
16714    B: NativeSidecarBridge + Send + 'static,
16715    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16716{
16717    let socket_paths = build_javascript_socket_path_context(vm)?;
16718    let family = JavascriptSocketFamily::Ipv4;
16719    let local_port = allocate_guest_listen_port(
16720        0,
16721        family,
16722        &socket_paths.used_tcp_guest_ports,
16723        socket_paths.listen_policy,
16724    )?;
16725    let resource_limits = vm.kernel.resource_limits().clone();
16726    let network_counts = vm_network_resource_counts(vm);
16727    check_network_resource_limit(
16728        resource_limits.max_sockets,
16729        network_counts.sockets,
16730        2,
16731        "socket",
16732    )?;
16733    check_network_resource_limit(
16734        resource_limits.max_connections,
16735        network_counts.connections,
16736        2,
16737        "connection",
16738    )?;
16739
16740    let kernel_pid = vm
16741        .active_processes
16742        .get(target_process_id)
16743        .ok_or_else(|| {
16744            SidecarError::InvalidState(format!(
16745                "vm.fetch target process disappeared: {target_process_id}"
16746            ))
16747        })?
16748        .kernel_pid;
16749    let socket_id = vm
16750        .kernel
16751        .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, SocketSpec::tcp())
16752        .map_err(kernel_error)?;
16753
16754    let result = dispatch_kernel_http_fetch_with_socket(
16755        bridge,
16756        vm_id,
16757        vm,
16758        target_process_id,
16759        kernel_pid,
16760        socket_id,
16761        local_port,
16762        port,
16763        path,
16764        options,
16765        headers,
16766        &socket_paths,
16767        &resource_limits,
16768        max_fetch_response_bytes,
16769    );
16770    let close_result = vm
16771        .kernel
16772        .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id)
16773        .map_err(kernel_error);
16774    let cleanup_result = if result.is_err() {
16775        drain_host_fetch_target_events(
16776            bridge,
16777            vm_id,
16778            vm,
16779            target_process_id,
16780            &socket_paths,
16781            &resource_limits,
16782        )
16783    } else {
16784        Ok(())
16785    };
16786    match (result, close_result) {
16787        (Ok(response), Ok(())) => cleanup_result.map(|()| response),
16788        (Err(error), _) => Err(error),
16789        (Ok(_), Err(error)) => Err(error),
16790    }
16791}
16792
16793#[allow(clippy::too_many_arguments)]
16794fn dispatch_kernel_http_fetch_with_socket<B>(
16795    bridge: &SharedBridge<B>,
16796    vm_id: &str,
16797    vm: &mut VmState,
16798    target_process_id: &str,
16799    kernel_pid: u32,
16800    socket_id: SocketId,
16801    local_port: u16,
16802    port: u16,
16803    path: &str,
16804    options: &JavascriptHttpRequestOptions,
16805    headers: &HttpHeaderCollection,
16806    socket_paths: &JavascriptSocketPathContext,
16807    resource_limits: &ResourceLimits,
16808    max_fetch_response_bytes: usize,
16809) -> Result<String, SidecarError>
16810where
16811    B: NativeSidecarBridge + Send + 'static,
16812    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16813{
16814    vm.kernel
16815        .socket_bind_inet(
16816            EXECUTION_DRIVER_NAME,
16817            kernel_pid,
16818            socket_id,
16819            InetSocketAddress::new("127.0.0.1", local_port),
16820        )
16821        .map_err(kernel_error)?;
16822    vm.kernel
16823        .socket_connect_inet_loopback(
16824            EXECUTION_DRIVER_NAME,
16825            kernel_pid,
16826            socket_id,
16827            InetSocketAddress::new("127.0.0.1", port),
16828        )
16829        .map_err(kernel_error)?;
16830
16831    let request_bytes = serialize_kernel_http_fetch_request(port, path, options, headers);
16832    vm.kernel
16833        .socket_write(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, &request_bytes)
16834        .map_err(kernel_error)?;
16835
16836    let mut response_buffer = Vec::new();
16837    let mut peer_closed = false;
16838    let url = format!("http://127.0.0.1:{port}{path}");
16839    let deadline = Instant::now() + http_loopback_request_timeout();
16840    loop {
16841        if let Some(response) =
16842            parse_kernel_http_fetch_response(&response_buffer, peer_closed, &url)?
16843        {
16844            ensure_vm_fetch_response_within_limit(&response, "vm.fetch", max_fetch_response_bytes)?;
16845            return Ok(response);
16846        }
16847        if Instant::now() >= deadline {
16848            let preview = String::from_utf8_lossy(&response_buffer);
16849            return Err(SidecarError::Execution(format!(
16850                "vm.fetch timed out waiting for kernel TCP HTTP response ({} buffered bytes: {:?})",
16851                response_buffer.len(),
16852                preview.chars().take(200).collect::<String>()
16853            )));
16854        }
16855
16856        {
16857            let dns = vm.dns.clone();
16858            let process = vm
16859                .active_processes
16860                .get_mut(target_process_id)
16861                .ok_or_else(|| {
16862                    SidecarError::InvalidState(format!(
16863                        "vm.fetch target process disappeared: {target_process_id}"
16864                    ))
16865                })?;
16866            service_host_fetch_target_event(
16867                bridge,
16868                vm_id,
16869                &dns,
16870                socket_paths,
16871                &mut vm.kernel,
16872                process,
16873                resource_limits,
16874                Duration::from_millis(5),
16875            )?;
16876        }
16877
16878        let poll = vm
16879            .kernel
16880            .poll_targets(
16881                EXECUTION_DRIVER_NAME,
16882                kernel_pid,
16883                vec![PollTargetEntry::socket(
16884                    socket_id,
16885                    POLLIN | POLLHUP | POLLERR,
16886                )],
16887                5,
16888            )
16889            .map_err(kernel_error)?;
16890        let revents = poll
16891            .targets
16892            .first()
16893            .map(|entry| entry.revents)
16894            .unwrap_or_else(PollEvents::empty);
16895        if revents.intersects(POLLERR) {
16896            return Err(SidecarError::Execution(String::from(
16897                "vm.fetch kernel TCP socket reported POLLERR",
16898            )));
16899        }
16900        if revents.intersects(POLLIN) {
16901            match vm
16902                .kernel
16903                .socket_read(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, 64 * 1024)
16904            {
16905                Ok(Some(bytes)) if !bytes.is_empty() => {
16906                    response_buffer.extend(bytes);
16907                    ensure_vm_fetch_raw_response_buffer_within_limit(
16908                        response_buffer.len(),
16909                        "vm.fetch",
16910                    )?;
16911                }
16912                Ok(Some(_)) => {}
16913                Ok(None) => peer_closed = true,
16914                Err(error) if error.code() == "EAGAIN" => {}
16915                Err(error) => return Err(kernel_error(error)),
16916            }
16917        }
16918        if revents.intersects(POLLHUP) {
16919            peer_closed = true;
16920        }
16921    }
16922}
16923
16924fn outbound_http_response_json(url: &Url, response: ureq::Response) -> Result<Value, SidecarError> {
16925    let status = response.status();
16926    let status_text = response.status_text().to_owned();
16927    let mut header_pairs = Vec::new();
16928    let mut raw_headers = Vec::new();
16929    for raw_name in response.headers_names() {
16930        for value in response.all(&raw_name) {
16931            header_pairs.push(json!([raw_name.to_ascii_lowercase(), value]));
16932            raw_headers.push(Value::String(raw_name.clone()));
16933            raw_headers.push(Value::String(value.to_owned()));
16934        }
16935    }
16936    let mut reader = response.into_reader();
16937    let mut body = Vec::new();
16938    reader.read_to_end(&mut body).map_err(|error| {
16939        SidecarError::Execution(format!("failed to read HTTP response: {error}"))
16940    })?;
16941    serde_json::to_string(&json!({
16942        "status": status,
16943        "statusText": status_text,
16944        "headers": header_pairs,
16945        "rawHeaders": raw_headers,
16946        "body": base64::engine::general_purpose::STANDARD.encode(body),
16947        "bodyEncoding": "base64",
16948        "url": url.as_str(),
16949    }))
16950    .map(Value::String)
16951    .map_err(|error| SidecarError::Execution(format!("ERR_AGENT_OS_NODE_SYNC_RPC: {error}")))
16952}
16953
16954/// Split a ureq resolver `netloc` (`host:port`, with optional `[..]` IPv6
16955/// brackets) into its host and port components. Returns `None` if the port is
16956/// missing or unparseable.
16957fn split_netloc(netloc: &str) -> Option<(&str, u16)> {
16958    let (host, port) = netloc.rsplit_once(':')?;
16959    let port: u16 = port.parse().ok()?;
16960    let host = host
16961        .strip_prefix('[')
16962        .and_then(|rest| rest.strip_suffix(']'))
16963        .unwrap_or(host);
16964    Some((host, port))
16965}
16966
16967fn issue_outbound_http_request(
16968    url: &Url,
16969    options: &JavascriptHttpRequestOptions,
16970    headers: &HttpHeaderCollection,
16971    pinned_addresses: &[IpAddr],
16972) -> Result<Value, SidecarError> {
16973    let method = options.method.as_deref().unwrap_or("GET");
16974    // Pin the underlying resolver to the egress-vetted addresses. ureq performs
16975    // its own DNS resolution for the TCP/TLS connect; without this override an
16976    // https:// request would re-resolve the hostname through the host resolver
16977    // (a rebinding DNS server could then return a private/metadata IP that the
16978    // earlier range check would have rejected). The pinned resolver returns only
16979    // the vetted addresses and refuses any host it was not vetted for, while the
16980    // request URL keeps the original hostname so TLS SNI and the Host header stay
16981    // correct.
16982    let pinned_host = url.host_str().map(str::to_owned);
16983    let pinned: Vec<IpAddr> = pinned_addresses.to_vec();
16984    let resolver = move |netloc: &str| -> std::io::Result<Vec<SocketAddr>> {
16985        let (host, port) = split_netloc(netloc).ok_or_else(|| {
16986            std::io::Error::new(
16987                std::io::ErrorKind::InvalidInput,
16988                format!("invalid network location: {netloc}"),
16989            )
16990        })?;
16991        let expected_host = pinned_host.as_deref();
16992        if expected_host != Some(host) {
16993            return Err(std::io::Error::new(
16994                std::io::ErrorKind::PermissionDenied,
16995                format!(
16996                    "EACCES: outbound HTTP resolver pinned to {expected_host:?}, refusing {host}"
16997                ),
16998            ));
16999        }
17000        if pinned.is_empty() {
17001            return Err(std::io::Error::new(
17002                std::io::ErrorKind::PermissionDenied,
17003                "EACCES: no egress-vetted address available for outbound HTTP request",
17004            ));
17005        }
17006        Ok(pinned.iter().map(|ip| SocketAddr::new(*ip, port)).collect())
17007    };
17008    let mut agent_builder = ureq::AgentBuilder::new()
17009        .resolver(resolver)
17010        .timeout_connect(Duration::from_secs(5))
17011        .timeout_read(Duration::from_secs(15))
17012        .timeout_write(Duration::from_secs(15));
17013    if url.scheme() == "https" {
17014        let tls_options = JavascriptTlsBridgeOptions {
17015            is_server: false,
17016            servername: url.host_str().map(str::to_owned),
17017            alpn_protocols: Some(vec![String::from("http/1.1")]),
17018            reject_unauthorized: options.reject_unauthorized,
17019            ..JavascriptTlsBridgeOptions::default()
17020        };
17021        agent_builder = agent_builder.tls_config(Arc::new(build_client_tls_config(&tls_options)?));
17022    }
17023    let agent = agent_builder.build();
17024    let mut request = agent.request_url(method, url);
17025    for (name, values) in &headers.normalized {
17026        if name == "host" {
17027            continue;
17028        }
17029        let header_value = values.join(", ");
17030        request = request.set(name, &header_value);
17031    }
17032    let response = match options.body.as_deref() {
17033        Some(body) => request.send_string(body),
17034        None => request.call(),
17035    };
17036
17037    match response {
17038        Ok(response) => outbound_http_response_json(url, response),
17039        Err(ureq::Error::Status(_, response)) => outbound_http_response_json(url, response),
17040        Err(ureq::Error::Transport(error)) => Err(SidecarError::Execution(format!(
17041            "ERR_HTTP_REQUEST_FAILED: {error}"
17042        ))),
17043    }
17044}
17045
17046fn wait_for_loopback_http_response<B>(
17047    request: LoopbackHttpResponseWaitRequest<'_, B>,
17048) -> Result<String, SidecarError>
17049where
17050    B: NativeSidecarBridge + Send + 'static,
17051    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17052{
17053    let LoopbackHttpResponseWaitRequest {
17054        bridge,
17055        vm_id,
17056        dns,
17057        socket_paths,
17058        kernel,
17059        process,
17060        resource_limits,
17061        request_key,
17062    } = request;
17063    let deadline = Instant::now() + http_loopback_request_timeout();
17064    loop {
17065        if let Some(response) = process
17066            .pending_http_requests
17067            .get(&request_key)
17068            .and_then(|response| response.clone())
17069        {
17070            process.pending_http_requests.remove(&request_key);
17071            return Ok(response);
17072        }
17073
17074        if Instant::now() >= deadline {
17075            process.pending_http_requests.remove(&request_key);
17076            return Err(SidecarError::Execution(String::from(
17077                "HTTP loopback request timed out waiting for net.http_respond",
17078            )));
17079        }
17080
17081        let Some(event) = process
17082            .execution
17083            .poll_event_blocking(Duration::from_millis(10))
17084            .map_err(|error| SidecarError::Execution(error.to_string()))?
17085        else {
17086            continue;
17087        };
17088
17089        match event {
17090            ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
17091                let network_counts = process.network_resource_counts();
17092                let response = service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
17093                    bridge,
17094                    vm_id,
17095                    dns,
17096                    socket_paths,
17097                    kernel,
17098                    process,
17099                    sync_request: &request,
17100                    resource_limits,
17101                    network_counts,
17102                });
17103                match response {
17104                    Ok(result) => process
17105                        .execution
17106                        .respond_javascript_sync_rpc_success(request.id, result)
17107                        .or_else(ignore_stale_javascript_sync_rpc_response)?,
17108                    Err(error) => process
17109                        .execution
17110                        .respond_javascript_sync_rpc_error(
17111                            request.id,
17112                            javascript_sync_rpc_error_code(&error),
17113                            error.to_string(),
17114                        )
17115                        .or_else(ignore_stale_javascript_sync_rpc_response)?,
17116                }
17117            }
17118            ActiveExecutionEvent::Exited(code) => {
17119                process.pending_http_requests.remove(&request_key);
17120                return Err(SidecarError::Execution(format!(
17121                    "HTTP loopback server exited before responding (exit code {code})"
17122                )));
17123            }
17124            ActiveExecutionEvent::Stdout(_)
17125            | ActiveExecutionEvent::Stderr(_)
17126            | ActiveExecutionEvent::PythonVfsRpcRequest(_)
17127            | ActiveExecutionEvent::SignalState { .. } => {}
17128        }
17129    }
17130}
17131
17132pub(crate) fn dispatch_loopback_http_request<B>(
17133    request: LoopbackHttpDispatchRequest<'_, B>,
17134) -> Result<String, SidecarError>
17135where
17136    B: NativeSidecarBridge + Send + 'static,
17137    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17138{
17139    let LoopbackHttpDispatchRequest {
17140        bridge,
17141        vm_id,
17142        dns,
17143        socket_paths,
17144        kernel,
17145        process,
17146        resource_limits,
17147        server_id,
17148        request_json,
17149    } = request;
17150    let request_id = {
17151        let server = process.http_servers.get_mut(&server_id).ok_or_else(|| {
17152            SidecarError::InvalidState(format!("HTTP target server disappeared: {server_id}"))
17153        })?;
17154        server.next_request_id += 1;
17155        server.next_request_id
17156    };
17157    process
17158        .pending_http_requests
17159        .insert((server_id, request_id), None);
17160    process.execution.send_javascript_stream_event(
17161        "http_request",
17162        json!({
17163            "serverId": server_id,
17164            "requestId": request_id,
17165            "request": request_json,
17166        }),
17167    )?;
17168    wait_for_loopback_http_response(LoopbackHttpResponseWaitRequest {
17169        bridge,
17170        vm_id,
17171        dns,
17172        socket_paths,
17173        kernel,
17174        process,
17175        resource_limits,
17176        request_key: (server_id, request_id),
17177    })
17178}
17179
17180fn ensure_vm_fetch_response_within_limit(
17181    response_json: &str,
17182    operation: &str,
17183    limit: usize,
17184) -> Result<(), SidecarError> {
17185    let size = response_json.len();
17186    if size > limit {
17187        return Err(SidecarError::Execution(format!(
17188            "{operation} payload is {size} bytes, limit is {limit}"
17189        )));
17190    }
17191    Ok(())
17192}
17193
17194fn ensure_vm_fetch_raw_response_buffer_within_limit(
17195    size: usize,
17196    operation: &str,
17197) -> Result<(), SidecarError> {
17198    if size > VM_FETCH_BUFFER_LIMIT_BYTES {
17199        return Err(SidecarError::Execution(format!(
17200            "{operation} raw response buffer is {size} bytes, limit is {VM_FETCH_BUFFER_LIMIT_BYTES}"
17201        )));
17202    }
17203    Ok(())
17204}
17205
17206pub(crate) fn ensure_vm_fetch_response_frame_within_limit(
17207    response: &ResponseFrame,
17208    max_frame_bytes: usize,
17209) -> Result<(), SidecarError> {
17210    let max_frame_bytes = max_frame_bytes.min(VM_FETCH_BUFFER_LIMIT_BYTES);
17211    let frame = crate::protocol::to_generated_protocol_frame(
17212        &crate::protocol::ProtocolFrame::Response(response.clone()),
17213    )
17214    .map_err(|error| SidecarError::FrameTooLarge(error.to_string()))?;
17215    let WireProtocolFrame::ResponseFrame(_) = &frame else {
17216        return Err(SidecarError::FrameTooLarge(String::from(
17217            "vm fetch response converted to non-response wire frame",
17218        )));
17219    };
17220    WireFrameCodec::new(max_frame_bytes)
17221        .encode(&frame)
17222        .map(|_| ())
17223        .map_err(|error| SidecarError::FrameTooLarge(error.to_string()))
17224}
17225
17226fn service_javascript_dns_sync_rpc<B>(
17227    bridge: &SharedBridge<B>,
17228    kernel: &SidecarKernel,
17229    vm_id: &str,
17230    dns: &VmDnsConfig,
17231    request: &JavascriptSyncRpcRequest,
17232) -> Result<Value, SidecarError>
17233where
17234    B: NativeSidecarBridge + Send + 'static,
17235    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17236{
17237    match request.method.as_str() {
17238        "dns.lookup" => {
17239            let payload = request
17240                .args
17241                .first()
17242                .cloned()
17243                .ok_or_else(|| {
17244                    SidecarError::InvalidState(String::from(
17245                        "dns.lookup requires a request payload",
17246                    ))
17247                })
17248                .and_then(|value| {
17249                    serde_json::from_value::<JavascriptDnsLookupRequest>(value).map_err(|error| {
17250                        SidecarError::InvalidState(format!("invalid dns.lookup payload: {error}"))
17251                    })
17252                })?;
17253            let addresses = filter_dns_ip_addrs(
17254                resolve_dns_ip_addrs(
17255                    bridge,
17256                    kernel,
17257                    vm_id,
17258                    dns,
17259                    &payload.hostname,
17260                    DnsLookupPolicy::CheckPermissions,
17261                )?,
17262                payload.family,
17263            )?;
17264            let addresses = filter_dns_safe_ip_addrs(addresses, &payload.hostname)?;
17265            Ok(Value::Array(
17266                addresses
17267                    .into_iter()
17268                    .map(|ip| {
17269                        json!({
17270                            "address": ip.to_string(),
17271                            "family": if ip.is_ipv6() { 6 } else { 4 },
17272                        })
17273                    })
17274                    .collect(),
17275            ))
17276        }
17277        "dns.resolve" | "dns.resolve4" | "dns.resolve6" => {
17278            let payload = request
17279                .args
17280                .first()
17281                .cloned()
17282                .ok_or_else(|| {
17283                    SidecarError::InvalidState(String::from(
17284                        "dns.resolve requires a request payload",
17285                    ))
17286                })
17287                .and_then(|value| {
17288                    serde_json::from_value::<JavascriptDnsResolveRequest>(value).map_err(|error| {
17289                        SidecarError::InvalidState(format!("invalid dns.resolve payload: {error}"))
17290                    })
17291                })?;
17292            let requested_type = match request.method.as_str() {
17293                "dns.resolve4" => String::from("A"),
17294                "dns.resolve6" => String::from("AAAA"),
17295                _ => payload
17296                    .rrtype
17297                    .as_deref()
17298                    .unwrap_or("A")
17299                    .to_ascii_uppercase(),
17300            };
17301            let record_type = parse_dns_record_type(&requested_type)?;
17302            let resolution = resolve_dns_records(
17303                bridge,
17304                kernel,
17305                vm_id,
17306                dns,
17307                &payload.hostname,
17308                record_type,
17309                DnsLookupPolicy::CheckPermissions,
17310            )?;
17311            dns_resolution_to_node_value(&resolution, &requested_type)
17312        }
17313        other => Err(SidecarError::InvalidState(format!(
17314            "unsupported JavaScript dns sync RPC method {other}"
17315        ))),
17316    }
17317}
17318
17319fn service_javascript_dgram_sync_rpc<B>(
17320    request: JavascriptDgramSyncRpcServiceRequest<'_, B>,
17321) -> Result<Value, SidecarError>
17322where
17323    B: NativeSidecarBridge + Send + 'static,
17324    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17325{
17326    let JavascriptDgramSyncRpcServiceRequest {
17327        bridge,
17328        kernel,
17329        vm_id,
17330        dns,
17331        socket_paths,
17332        process,
17333        sync_request: request,
17334        resource_limits,
17335        network_counts,
17336    } = request;
17337    match request.method.as_str() {
17338        "dgram.createSocket" => {
17339            check_network_resource_limit(
17340                resource_limits.max_sockets,
17341                network_counts.sockets,
17342                1,
17343                "socket",
17344            )?;
17345            let payload = request
17346                .args
17347                .first()
17348                .cloned()
17349                .ok_or_else(|| {
17350                    SidecarError::InvalidState(String::from(
17351                        "dgram.createSocket requires a request payload",
17352                    ))
17353                })
17354                .and_then(|value| {
17355                    serde_json::from_value::<JavascriptDgramCreateSocketRequest>(value).map_err(
17356                        |error| {
17357                            SidecarError::InvalidState(format!(
17358                                "invalid dgram.createSocket payload: {error}"
17359                            ))
17360                        },
17361                    )
17362                })?;
17363            let family = JavascriptUdpFamily::from_socket_type(&payload.socket_type)?;
17364            let socket_id = process.allocate_udp_socket_id();
17365            process.udp_sockets.insert(
17366                socket_id.clone(),
17367                ActiveUdpSocket::new(kernel, process.kernel_pid, family)?,
17368            );
17369            Ok(json!({
17370                "socketId": socket_id,
17371                "type": family.socket_type(),
17372            }))
17373        }
17374        "dgram.bind" => {
17375            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.bind socket id")?;
17376            let payload = request
17377                .args
17378                .get(1)
17379                .cloned()
17380                .ok_or_else(|| {
17381                    SidecarError::InvalidState(String::from(
17382                        "dgram.bind requires a request payload",
17383                    ))
17384                })
17385                .and_then(|value| {
17386                    serde_json::from_value::<JavascriptDgramBindRequest>(value).map_err(|error| {
17387                        SidecarError::InvalidState(format!("invalid dgram.bind payload: {error}"))
17388                    })
17389                })?;
17390            let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
17391                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17392            })?;
17393            let local_addr = socket.bind(
17394                kernel,
17395                process.kernel_pid,
17396                payload.address.as_deref(),
17397                payload.port,
17398                socket_paths,
17399            )?;
17400            Ok(json!({
17401                "localAddress": local_addr.ip().to_string(),
17402                "localPort": local_addr.port(),
17403                "family": socket_addr_family(&local_addr),
17404            }))
17405        }
17406        "dgram.send" => {
17407            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.send socket id")?;
17408            let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "dgram.send payload")?;
17409            let payload = request
17410                .args
17411                .get(2)
17412                .cloned()
17413                .ok_or_else(|| {
17414                    SidecarError::InvalidState(String::from(
17415                        "dgram.send requires a request payload",
17416                    ))
17417                })
17418                .and_then(|value| {
17419                    serde_json::from_value::<JavascriptDgramSendRequest>(value).map_err(|error| {
17420                        SidecarError::InvalidState(format!("invalid dgram.send payload: {error}"))
17421                    })
17422                })?;
17423            let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
17424                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17425            })?;
17426            let (written, local_addr) = socket.send_to(ActiveUdpSendToRequest {
17427                bridge,
17428                kernel,
17429                kernel_pid: process.kernel_pid,
17430                vm_id,
17431                dns,
17432                host: payload.address.as_deref().unwrap_or("localhost"),
17433                port: payload.port,
17434                context: socket_paths,
17435                contents: &chunk,
17436            })?;
17437            Ok(json!({
17438                "bytes": written,
17439                "localAddress": local_addr.ip().to_string(),
17440                "localPort": local_addr.port(),
17441                "family": socket_addr_family(&local_addr),
17442            }))
17443        }
17444        "dgram.poll" => {
17445            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.poll socket id")?;
17446            let wait_ms =
17447                javascript_sync_rpc_arg_u64_optional(&request.args, 1, "dgram.poll wait ms")?
17448                    .unwrap_or_default();
17449            let event = {
17450                let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
17451                    SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17452                })?;
17453                socket.poll(kernel, process.kernel_pid, Duration::from_millis(wait_ms))?
17454            };
17455
17456            match event {
17457                Some(JavascriptUdpSocketEvent::Message { data, remote_addr }) => {
17458                    let family = JavascriptSocketFamily::from_ip(remote_addr.ip());
17459                    let guest_remote_port = if is_loopback_ip(remote_addr.ip()) {
17460                        socket_paths
17461                            .guest_udp_port_for_host_port(family, remote_addr.port())
17462                            .unwrap_or(remote_addr.port())
17463                    } else {
17464                        remote_addr.port()
17465                    };
17466                    Ok(json!({
17467                    "type": "message",
17468                    "data": javascript_sync_rpc_bytes_value(&data),
17469                    "remoteAddress": remote_addr.ip().to_string(),
17470                    "remotePort": guest_remote_port,
17471                    "remoteFamily": socket_addr_family(&remote_addr),
17472                    }))
17473                }
17474                Some(JavascriptUdpSocketEvent::Error { code, message }) => Ok(json!({
17475                    "type": "error",
17476                    "code": code,
17477                    "message": message,
17478                })),
17479                None => Ok(Value::Null),
17480            }
17481        }
17482        "dgram.close" => {
17483            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.close socket id")?;
17484            let mut socket = process.udp_sockets.remove(socket_id).ok_or_else(|| {
17485                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17486            })?;
17487            socket.close(kernel, process.kernel_pid);
17488            Ok(Value::Null)
17489        }
17490        "dgram.address" => {
17491            let socket_id =
17492                javascript_sync_rpc_arg_str(&request.args, 0, "dgram.address socket id")?;
17493            let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
17494                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17495            })?;
17496            let local_addr = socket.local_addr().ok_or_else(|| {
17497                SidecarError::Execution(String::from("EBADF: bad file descriptor"))
17498            })?;
17499            javascript_net_json_string(
17500                json!({
17501                    "address": local_addr.ip().to_string(),
17502                    "port": local_addr.port(),
17503                    "family": socket_addr_family(&local_addr),
17504                }),
17505                "dgram.address",
17506            )
17507        }
17508        "dgram.setBufferSize" => {
17509            let socket_id =
17510                javascript_sync_rpc_arg_str(&request.args, 0, "dgram.setBufferSize socket id")?;
17511            let which =
17512                javascript_sync_rpc_arg_str(&request.args, 1, "dgram.setBufferSize buffer kind")?;
17513            let size = javascript_sync_rpc_arg_u64(&request.args, 2, "dgram.setBufferSize size")?;
17514            let size = usize::try_from(size).map_err(|_| {
17515                SidecarError::InvalidState(String::from(
17516                    "dgram.setBufferSize size must fit within usize",
17517                ))
17518            })?;
17519            let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
17520                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17521            })?;
17522            socket.set_buffer_size(which, size)?;
17523            Ok(Value::Null)
17524        }
17525        "dgram.getBufferSize" => {
17526            let socket_id =
17527                javascript_sync_rpc_arg_str(&request.args, 0, "dgram.getBufferSize socket id")?;
17528            let which =
17529                javascript_sync_rpc_arg_str(&request.args, 1, "dgram.getBufferSize buffer kind")?;
17530            let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
17531                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17532            })?;
17533            let size = socket.get_buffer_size(which)?;
17534            Ok(json!(size))
17535        }
17536        other => Err(SidecarError::InvalidState(format!(
17537            "unsupported JavaScript dgram sync RPC method {other}"
17538        ))),
17539    }
17540}
17541
17542#[derive(Debug)]
17543struct ClientHttp2StreamState {
17544    send_stream: Option<h2::SendStream<Bytes>>,
17545}
17546
17547#[derive(Debug)]
17548struct ServerHttp2StreamState {
17549    send_response: Option<ServerHttp2Responder>,
17550    send_stream: Option<h2::SendStream<Bytes>>,
17551}
17552
17553#[derive(Debug)]
17554enum ServerHttp2Responder {
17555    Regular(server::SendResponse<Bytes>),
17556    Pushed(server::SendPushedResponse<Bytes>),
17557}
17558
17559const HTTP2_DEFAULT_WINDOW_SIZE: u32 = 65_535;
17560const HTTP2_POLL_DELAY: Duration = Duration::from_millis(10);
17561
17562fn http2_runtime_snapshot() -> Http2RuntimeSnapshot {
17563    Http2RuntimeSnapshot {
17564        effective_local_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
17565        local_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
17566        remote_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
17567        next_stream_id: 1,
17568        outbound_queue_size: 1,
17569        deflate_dynamic_table_size: 0,
17570        inflate_dynamic_table_size: 0,
17571    }
17572}
17573
17574fn http2_snapshot_json(snapshot: &Http2SessionSnapshot) -> Result<String, SidecarError> {
17575    serde_json::to_string(snapshot)
17576        .map_err(|error| SidecarError::Execution(format!("ERR_AGENT_OS_NODE_SYNC_RPC: {error}")))
17577}
17578
17579fn http2_event_value(event: &Http2BridgeEvent) -> Result<Value, SidecarError> {
17580    serde_json::to_string(event)
17581        .map(Value::String)
17582        .map_err(|error| SidecarError::Execution(format!("ERR_AGENT_OS_NODE_SYNC_RPC: {error}")))
17583}
17584
17585fn push_http2_server_event(
17586    shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17587    server_id: u64,
17588    event: Http2BridgeEvent,
17589) {
17590    if let Ok(mut state) = shared.lock() {
17591        state
17592            .server_events
17593            .entry(server_id)
17594            .or_default()
17595            .push_back(event);
17596    }
17597}
17598
17599fn push_http2_session_event(
17600    shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17601    session_id: u64,
17602    event: Http2BridgeEvent,
17603) {
17604    if let Ok(mut state) = shared.lock() {
17605        state
17606            .session_events
17607            .entry(session_id)
17608            .or_default()
17609            .push_back(event);
17610    }
17611}
17612
17613fn pop_http2_event(
17614    queue: &mut BTreeMap<u64, VecDeque<Http2BridgeEvent>>,
17615    id: u64,
17616) -> Option<Http2BridgeEvent> {
17617    queue.get_mut(&id).and_then(VecDeque::pop_front)
17618}
17619
17620fn wait_for_http2_event(
17621    shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17622    id: u64,
17623    is_server: bool,
17624    wait_ms: u64,
17625) -> Option<Http2BridgeEvent> {
17626    let deadline = Instant::now() + Duration::from_millis(wait_ms);
17627    loop {
17628        if let Ok(mut state) = shared.lock() {
17629            let queue = if is_server {
17630                &mut state.server_events
17631            } else {
17632                &mut state.session_events
17633            };
17634            if let Some(event) = pop_http2_event(queue, id) {
17635                return Some(event);
17636            }
17637        }
17638        if wait_ms == 0 || Instant::now() >= deadline {
17639            return None;
17640        }
17641        thread::sleep(HTTP2_POLL_DELAY);
17642    }
17643}
17644
17645fn next_http2_session_id(shared: &mut crate::state::Http2SharedState) -> u64 {
17646    shared.next_session_id += 1;
17647    shared.next_session_id
17648}
17649
17650fn next_http2_stream_id(shared: &mut crate::state::Http2SharedState) -> u64 {
17651    shared.next_stream_id += 1;
17652    shared.next_stream_id
17653}
17654
17655fn http2_reason(code: Option<u32>) -> Reason {
17656    code.unwrap_or(Reason::NO_ERROR.into()).into()
17657}
17658
17659fn http2_error_payload(message: impl Into<String>) -> String {
17660    serde_json::to_string(&json!({
17661        "name": "Error",
17662        "code": "ERR_HTTP2_ERROR",
17663        "message": message.into(),
17664    }))
17665    .unwrap_or_else(|_| {
17666        String::from(
17667            "{\"name\":\"Error\",\"code\":\"ERR_HTTP2_ERROR\",\"message\":\"HTTP/2 bridge error\"}",
17668        )
17669    })
17670}
17671
17672fn http2_socket_snapshot(local_addr: SocketAddr, remote_addr: SocketAddr) -> Http2SocketSnapshot {
17673    Http2SocketSnapshot {
17674        encrypted: false,
17675        allow_half_open: false,
17676        local_address: Some(local_addr.ip().to_string()),
17677        local_port: Some(local_addr.port()),
17678        local_family: Some(socket_addr_family(&local_addr).to_string()),
17679        remote_address: Some(remote_addr.ip().to_string()),
17680        remote_port: Some(remote_addr.port()),
17681        remote_family: Some(socket_addr_family(&remote_addr).to_string()),
17682        servername: None,
17683        alpn_protocol: Some(String::from("h2c")),
17684    }
17685}
17686
17687fn http2_wait_result(kind: &str, id: u64) -> Value {
17688    json!({
17689        "kind": kind,
17690        "id": id,
17691    })
17692}
17693
17694fn is_http2_terminal_event(event: &Http2BridgeEvent, is_server: bool, id: u64) -> bool {
17695    if is_server {
17696        event.kind == "serverClose" && event.id == id
17697    } else {
17698        event.kind == "sessionClose" && event.id == id
17699    }
17700}
17701
17702fn dispatch_http2_wait_loop(
17703    process: &ActiveProcess,
17704    id: u64,
17705    is_server: bool,
17706) -> Result<Value, SidecarError> {
17707    loop {
17708        if let Some(event) = wait_for_http2_event(&process.http2.shared, id, is_server, 50) {
17709            let payload = serde_json::to_value(&event).map_err(|error| {
17710                SidecarError::Execution(format!("ERR_AGENT_OS_NODE_SYNC_RPC: {error}"))
17711            })?;
17712            process
17713                .execution
17714                .send_javascript_stream_event("http2", payload.clone())?;
17715            if is_http2_terminal_event(&event, is_server, id) {
17716                return Ok(payload);
17717            }
17718            continue;
17719        }
17720
17721        let exists = process
17722            .http2
17723            .shared
17724            .lock()
17725            .map(|state| {
17726                if is_server {
17727                    state.servers.contains_key(&id)
17728                } else {
17729                    state.sessions.contains_key(&id)
17730                }
17731            })
17732            .unwrap_or(false);
17733        if !exists {
17734            return Ok(if is_server {
17735                http2_wait_result("serverClose", id)
17736            } else {
17737                http2_wait_result("sessionClose", id)
17738            });
17739        }
17740    }
17741}
17742
17743fn dispatch_http_wait_loop(process: &ActiveProcess, server_id: u64) -> Result<Value, SidecarError> {
17744    loop {
17745        if !process.http_servers.contains_key(&server_id) {
17746            return Ok(json!({
17747                "kind": "serverClose",
17748                "id": server_id,
17749            }));
17750        }
17751        thread::sleep(Duration::from_millis(25));
17752    }
17753}
17754
17755fn http2_settings_from_value(settings: &BTreeMap<String, Value>) -> BTreeMap<String, Value> {
17756    settings.clone()
17757}
17758
17759fn parse_http2_headers_json(
17760    headers_json: &str,
17761    label: &str,
17762) -> Result<BTreeMap<String, Value>, SidecarError> {
17763    serde_json::from_str::<BTreeMap<String, Value>>(headers_json)
17764        .map_err(|error| SidecarError::InvalidState(format!("{label} must be valid JSON: {error}")))
17765}
17766
17767fn apply_http2_header_values(
17768    header_map: &mut HeaderMap,
17769    name: &str,
17770    value: &Value,
17771) -> Result<(), SidecarError> {
17772    let header_name = HeaderName::from_bytes(name.as_bytes()).map_err(|error| {
17773        SidecarError::InvalidState(format!("invalid HTTP/2 header name {name:?}: {error}"))
17774    })?;
17775    match value {
17776        Value::Array(values) => {
17777            for value in values {
17778                apply_http2_header_values(header_map, name, value)?;
17779            }
17780        }
17781        Value::String(text) => {
17782            let value = HeaderValue::from_str(text).map_err(|error| {
17783                SidecarError::InvalidState(format!(
17784                    "invalid HTTP/2 header value for {name}: {error}"
17785                ))
17786            })?;
17787            header_map.append(header_name.clone(), value);
17788        }
17789        Value::Number(number) => {
17790            let value = HeaderValue::from_str(&number.to_string()).map_err(|error| {
17791                SidecarError::InvalidState(format!(
17792                    "invalid HTTP/2 numeric header value for {name}: {error}"
17793                ))
17794            })?;
17795            header_map.append(header_name.clone(), value);
17796        }
17797        Value::Bool(boolean) => {
17798            let value = HeaderValue::from_str(if *boolean { "true" } else { "false" }).map_err(
17799                |error| {
17800                    SidecarError::InvalidState(format!(
17801                        "invalid HTTP/2 boolean header value for {name}: {error}"
17802                    ))
17803                },
17804            )?;
17805            header_map.append(header_name.clone(), value);
17806        }
17807        Value::Null => {}
17808        Value::Object(_) => {
17809            return Err(SidecarError::InvalidState(format!(
17810                "unsupported HTTP/2 header object value for {name}"
17811            )));
17812        }
17813    }
17814    Ok(())
17815}
17816
17817fn build_http2_request(headers_json: &str) -> Result<Request<()>, SidecarError> {
17818    let headers = parse_http2_headers_json(headers_json, "HTTP/2 request headers")?;
17819    let method = headers
17820        .get(":method")
17821        .and_then(Value::as_str)
17822        .unwrap_or("GET");
17823    let path = headers.get(":path").and_then(Value::as_str).unwrap_or("/");
17824    let mut builder = Request::builder()
17825        .method(Method::from_bytes(method.as_bytes()).map_err(|error| {
17826            SidecarError::InvalidState(format!("invalid HTTP/2 method {method:?}: {error}"))
17827        })?)
17828        .uri(path.parse::<Uri>().map_err(|error| {
17829            SidecarError::InvalidState(format!("invalid HTTP/2 path {path:?}: {error}"))
17830        })?);
17831    {
17832        let header_map = builder.headers_mut().expect("request header map");
17833        for (name, value) in &headers {
17834            if name.starts_with(':') {
17835                continue;
17836            }
17837            apply_http2_header_values(header_map, name, value)?;
17838        }
17839    }
17840    builder
17841        .body(())
17842        .map_err(|error| SidecarError::InvalidState(format!("invalid HTTP/2 request: {error}")))
17843}
17844
17845fn build_http2_response(headers_json: &str) -> Result<Response<()>, SidecarError> {
17846    let headers = parse_http2_headers_json(headers_json, "HTTP/2 response headers")?;
17847    let status = headers
17848        .get(":status")
17849        .and_then(Value::as_u64)
17850        .or_else(|| {
17851            headers
17852                .get(":status")
17853                .and_then(Value::as_str)
17854                .and_then(|value| value.parse::<u16>().ok().map(u64::from))
17855        })
17856        .unwrap_or(200);
17857    let mut builder = Response::builder().status(status as u16);
17858    {
17859        let header_map = builder.headers_mut().expect("response header map");
17860        for (name, value) in &headers {
17861            if name.starts_with(':') {
17862                continue;
17863            }
17864            apply_http2_header_values(header_map, name, value)?;
17865        }
17866    }
17867    builder.body(()).map_err(|error| {
17868        SidecarError::InvalidState(format!("invalid HTTP/2 response headers: {error}"))
17869    })
17870}
17871
17872fn serialize_http2_headers_map(
17873    pseudo: BTreeMap<String, Value>,
17874    headers: &HeaderMap,
17875) -> Result<String, SidecarError> {
17876    let mut serialized = pseudo;
17877    for (name, value) in headers {
17878        let name = name.as_str().to_string();
17879        let value = Value::String(
17880            value
17881                .to_str()
17882                .map_err(|error| {
17883                    SidecarError::Execution(format!("invalid HTTP/2 header value: {error}"))
17884                })?
17885                .to_owned(),
17886        );
17887        match serialized.get_mut(&name) {
17888            Some(Value::Array(values)) => values.push(value),
17889            Some(existing) => {
17890                let first = existing.clone();
17891                *existing = Value::Array(vec![first, value]);
17892            }
17893            None => {
17894                serialized.insert(name, value);
17895            }
17896        }
17897    }
17898    serde_json::to_string(&serialized)
17899        .map_err(|error| SidecarError::Execution(format!("ERR_AGENT_OS_NODE_SYNC_RPC: {error}")))
17900}
17901
17902fn serialize_http2_request_headers(
17903    request: &Request<h2::RecvStream>,
17904) -> Result<String, SidecarError> {
17905    let mut pseudo = BTreeMap::new();
17906    pseudo.insert(
17907        String::from(":method"),
17908        Value::String(request.method().as_str().to_string()),
17909    );
17910    pseudo.insert(
17911        String::from(":path"),
17912        Value::String(
17913            request
17914                .uri()
17915                .path_and_query()
17916                .map(|value| value.as_str().to_string())
17917                .unwrap_or_else(|| String::from("/")),
17918        ),
17919    );
17920    serialize_http2_headers_map(pseudo, request.headers())
17921}
17922
17923fn serialize_http2_response_headers(
17924    response: &Response<h2::RecvStream>,
17925) -> Result<String, SidecarError> {
17926    let mut pseudo = BTreeMap::new();
17927    pseudo.insert(
17928        String::from(":status"),
17929        Value::Number(serde_json::Number::from(response.status().as_u16())),
17930    );
17931    serialize_http2_headers_map(pseudo, response.headers())
17932}
17933
17934fn remove_http2_session_resources(
17935    shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17936    session_id: u64,
17937) {
17938    if let Ok(mut state) = shared.lock() {
17939        state.sessions.remove(&session_id);
17940        state.session_events.remove(&session_id);
17941        let stream_ids = state
17942            .streams
17943            .iter()
17944            .filter_map(|(stream_id, stream)| {
17945                (stream.session_id == session_id).then_some(*stream_id)
17946            })
17947            .collect::<Vec<_>>();
17948        for stream_id in stream_ids {
17949            state.streams.remove(&stream_id);
17950        }
17951    }
17952}
17953
17954fn spawn_http2_client_session(
17955    shared: Arc<Mutex<crate::state::Http2SharedState>>,
17956    session_id: u64,
17957    remote_addr: SocketAddr,
17958    tls: Option<JavascriptTlsBridgeOptions>,
17959    snapshot: Arc<Mutex<Http2SessionSnapshot>>,
17960    mut command_rx: UnboundedReceiver<Http2SessionCommand>,
17961) {
17962    thread::spawn(move || {
17963        let runtime = match TokioRuntimeBuilder::new_current_thread()
17964            .enable_all()
17965            .build()
17966        {
17967            Ok(runtime) => runtime,
17968            Err(error) => {
17969                push_http2_session_event(
17970                    &shared,
17971                    session_id,
17972                    Http2BridgeEvent {
17973                        kind: String::from("sessionError"),
17974                        id: session_id,
17975                        data: Some(http2_error_payload(error.to_string())),
17976                        ..Http2BridgeEvent::default()
17977                    },
17978                );
17979                remove_http2_session_resources(&shared, session_id);
17980                return;
17981            }
17982        };
17983
17984        runtime.block_on(async move {
17985            let stream = match tokio::net::TcpStream::connect(remote_addr).await {
17986                Ok(stream) => stream,
17987                Err(error) => {
17988                    push_http2_session_event(
17989                        &shared,
17990                        session_id,
17991                        Http2BridgeEvent {
17992                            kind: String::from("sessionError"),
17993                            id: session_id,
17994                            data: Some(http2_error_payload(error.to_string())),
17995                            ..Http2BridgeEvent::default()
17996                        },
17997                    );
17998                    remove_http2_session_resources(&shared, session_id);
17999                    return;
18000                }
18001            };
18002
18003            let local_addr = match stream.local_addr() {
18004                Ok(addr) => addr,
18005                Err(error) => {
18006                    push_http2_session_event(
18007                        &shared,
18008                        session_id,
18009                        Http2BridgeEvent {
18010                            kind: String::from("sessionError"),
18011                            id: session_id,
18012                            data: Some(http2_error_payload(error.to_string())),
18013                            ..Http2BridgeEvent::default()
18014                        },
18015                    );
18016                    remove_http2_session_resources(&shared, session_id);
18017                    return;
18018                }
18019            };
18020
18021            {
18022                let mut snapshot_guard = snapshot.lock().expect("http2 snapshot lock");
18023                snapshot_guard.socket = http2_socket_snapshot(local_addr, remote_addr);
18024                if let Some(options) = tls.as_ref() {
18025                    snapshot_guard.encrypted = true;
18026                    snapshot_guard.alpn_protocol = Some(String::from("h2"));
18027                    snapshot_guard.socket.encrypted = true;
18028                    snapshot_guard.socket.servername = options.servername.clone();
18029                    snapshot_guard.socket.alpn_protocol = Some(String::from("h2"));
18030                }
18031                snapshot_guard.state = http2_runtime_snapshot();
18032            }
18033            if let Ok(snapshot_json) =
18034                http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())
18035            {
18036                push_http2_session_event(
18037                    &shared,
18038                    session_id,
18039                    Http2BridgeEvent {
18040                        kind: String::from("sessionConnect"),
18041                        id: session_id,
18042                        data: Some(snapshot_json),
18043                        ..Http2BridgeEvent::default()
18044                    },
18045                );
18046            }
18047
18048            let io: Pin<Box<dyn Http2AsyncIo>> = if let Some(options) = tls.as_ref() {
18049                let server_name = match ServerName::try_from(
18050                    options
18051                        .servername
18052                        .clone()
18053                        .unwrap_or_else(|| String::from("localhost")),
18054                ) {
18055                    Ok(server_name) => server_name,
18056                    Err(_) => {
18057                        push_http2_session_event(
18058                            &shared,
18059                            session_id,
18060                            Http2BridgeEvent {
18061                                kind: String::from("sessionError"),
18062                                id: session_id,
18063                                data: Some(http2_error_payload("invalid TLS servername")),
18064                                ..Http2BridgeEvent::default()
18065                            },
18066                        );
18067                        remove_http2_session_resources(&shared, session_id);
18068                        return;
18069                    }
18070                };
18071                let connector = match build_client_tls_config(options) {
18072                    Ok(config) => TlsConnector::from(Arc::new(config)),
18073                    Err(error) => {
18074                        push_http2_session_event(
18075                            &shared,
18076                            session_id,
18077                            Http2BridgeEvent {
18078                                kind: String::from("sessionError"),
18079                                id: session_id,
18080                                data: Some(http2_error_payload(error.to_string())),
18081                                ..Http2BridgeEvent::default()
18082                            },
18083                        );
18084                        remove_http2_session_resources(&shared, session_id);
18085                        return;
18086                    }
18087                };
18088                match connector.connect(server_name, stream).await {
18089                    Ok(tls_stream) => Box::pin(tls_stream),
18090                    Err(error) => {
18091                        push_http2_session_event(
18092                            &shared,
18093                            session_id,
18094                            Http2BridgeEvent {
18095                                kind: String::from("sessionError"),
18096                                id: session_id,
18097                                data: Some(http2_error_payload(error.to_string())),
18098                                ..Http2BridgeEvent::default()
18099                            },
18100                        );
18101                        remove_http2_session_resources(&shared, session_id);
18102                        return;
18103                    }
18104                }
18105            } else {
18106                Box::pin(stream)
18107            };
18108
18109            let (mut sender, connection) = match client::handshake(io).await {
18110                Ok(parts) => parts,
18111                Err(error) => {
18112                    push_http2_session_event(
18113                        &shared,
18114                        session_id,
18115                        Http2BridgeEvent {
18116                            kind: String::from("sessionError"),
18117                            id: session_id,
18118                            data: Some(http2_error_payload(error.to_string())),
18119                            ..Http2BridgeEvent::default()
18120                        },
18121                    );
18122                    remove_http2_session_resources(&shared, session_id);
18123                    return;
18124                }
18125            };
18126
18127            let (status_tx, mut status_rx) = unbounded_channel::<Result<(), String>>();
18128            tokio::spawn(async move {
18129                let _ = status_tx.send(connection.await.map_err(|error| error.to_string()));
18130            });
18131
18132            let streams: Arc<Mutex<BTreeMap<u64, ClientHttp2StreamState>>> =
18133                Arc::new(Mutex::new(BTreeMap::new()));
18134
18135            loop {
18136                tokio::select! {
18137                    Some(result) = status_rx.recv() => {
18138                        if let Err(message) = result {
18139                            push_http2_session_event(
18140                                &shared,
18141                                session_id,
18142                                Http2BridgeEvent {
18143                                    kind: String::from("sessionError"),
18144                                    id: session_id,
18145                                    data: Some(http2_error_payload(message)),
18146                                    ..Http2BridgeEvent::default()
18147                                },
18148                            );
18149                        }
18150                        push_http2_session_event(
18151                            &shared,
18152                            session_id,
18153                            Http2BridgeEvent {
18154                                kind: String::from("sessionClose"),
18155                                id: session_id,
18156                                ..Http2BridgeEvent::default()
18157                            },
18158                        );
18159                        remove_http2_session_resources(&shared, session_id);
18160                        break;
18161                    }
18162                    Some(command) = command_rx.recv() => {
18163                        match command {
18164                            Http2SessionCommand::Request { headers_json, options_json, respond_to } => {
18165                                let request = match build_http2_request(&headers_json) {
18166                                    Ok(request) => request,
18167                                    Err(error) => {
18168                                        let _ = respond_to.send(Err(error.to_string()));
18169                                        continue;
18170                                    }
18171                                };
18172                                let options: JavascriptHttp2RequestOptions =
18173                                    serde_json::from_str(&options_json).unwrap_or_default();
18174                                let stream_id = {
18175                                    let mut state = shared.lock().expect("http2 shared state");
18176                                    let stream_id = next_http2_stream_id(&mut state);
18177                                    state.streams.insert(
18178                                        stream_id,
18179                                        ActiveHttp2Stream {
18180                                            session_id,
18181                                            paused: Arc::new(AtomicBool::new(false)),
18182                                        },
18183                                    );
18184                                    stream_id
18185                                };
18186                                match sender.send_request(request, options.end_stream) {
18187                                    Ok((response_future, send_stream)) => {
18188                                        if !options.end_stream {
18189                                            streams
18190                                                .lock()
18191                                                .expect("http2 client streams")
18192                                                .insert(stream_id, ClientHttp2StreamState { send_stream: Some(send_stream) });
18193                                        }
18194                                        let shared_clone = Arc::clone(&shared);
18195                                        let snapshot_clone = Arc::clone(&snapshot);
18196                                        tokio::spawn(async move {
18197                                            match response_future.await {
18198                                                Ok(response) => {
18199                                                    if let Ok(headers_json) = serialize_http2_response_headers(&response) {
18200                                                        push_http2_session_event(
18201                                                            &shared_clone,
18202                                                            session_id,
18203                                                            Http2BridgeEvent {
18204                                                                kind: String::from("clientResponseHeaders"),
18205                                                                id: stream_id,
18206                                                                data: Some(headers_json),
18207                                                                ..Http2BridgeEvent::default()
18208                                                            },
18209                                                        );
18210                                                    }
18211                                                    let mut body = response.into_body();
18212                                                    while let Some(chunk) = body.data().await {
18213                                                        match chunk {
18214                                                            Ok(bytes) => {
18215                                                                let paused = {
18216                                                                    let state = shared_clone.lock().expect("http2 shared state");
18217                                                                    state.streams.get(&stream_id).map(|stream| Arc::clone(&stream.paused))
18218                                                                };
18219                                                                if let Some(paused) = paused {
18220                                                                    while paused.load(Ordering::SeqCst) {
18221                                                                        tokio::time::sleep(HTTP2_POLL_DELAY).await;
18222                                                                    }
18223                                                                }
18224                                                                let _ = body.flow_control().release_capacity(bytes.len());
18225                                                                push_http2_session_event(
18226                                                                    &shared_clone,
18227                                                                    session_id,
18228                                                                    Http2BridgeEvent {
18229                                                                        kind: String::from("clientData"),
18230                                                                        id: stream_id,
18231                                                                        data: Some(base64::engine::general_purpose::STANDARD.encode(bytes)),
18232                                                                        ..Http2BridgeEvent::default()
18233                                                                    },
18234                                                                );
18235                                                            }
18236                                                            Err(error) => {
18237                                                                push_http2_session_event(
18238                                                                    &shared_clone,
18239                                                                    session_id,
18240                                                                    Http2BridgeEvent {
18241                                                                        kind: String::from("clientError"),
18242                                                                        id: stream_id,
18243                                                                        data: Some(http2_error_payload(error.to_string())),
18244                                                                        ..Http2BridgeEvent::default()
18245                                                                    },
18246                                                                );
18247                                                                break;
18248                                                            }
18249                                                        }
18250                                                    }
18251                                                    {
18252                                                        let mut snapshot = snapshot_clone.lock().expect("http2 snapshot lock");
18253                                                        snapshot.state.next_stream_id =
18254                                                            snapshot.state.next_stream_id.saturating_add(2);
18255                                                    }
18256                                                    push_http2_session_event(
18257                                                        &shared_clone,
18258                                                        session_id,
18259                                                        Http2BridgeEvent {
18260                                                            kind: String::from("clientEnd"),
18261                                                            id: stream_id,
18262                                                            ..Http2BridgeEvent::default()
18263                                                        },
18264                                                    );
18265                                                    push_http2_session_event(
18266                                                        &shared_clone,
18267                                                        session_id,
18268                                                        Http2BridgeEvent {
18269                                                            kind: String::from("clientClose"),
18270                                                            id: stream_id,
18271                                                            extra_number: Some(0),
18272                                                            ..Http2BridgeEvent::default()
18273                                                        },
18274                                                    );
18275                                                    if let Ok(mut state) = shared_clone.lock() {
18276                                                        state.streams.remove(&stream_id);
18277                                                    }
18278                                                }
18279                                                Err(error) => {
18280                                                    push_http2_session_event(
18281                                                        &shared_clone,
18282                                                        session_id,
18283                                                        Http2BridgeEvent {
18284                                                            kind: String::from("clientError"),
18285                                                            id: stream_id,
18286                                                            data: Some(http2_error_payload(error.to_string())),
18287                                                            ..Http2BridgeEvent::default()
18288                                                        },
18289                                                    );
18290                                                    push_http2_session_event(
18291                                                        &shared_clone,
18292                                                        session_id,
18293                                                        Http2BridgeEvent {
18294                                                            kind: String::from("clientClose"),
18295                                                            id: stream_id,
18296                                                            extra_number: Some(u32::from(Reason::INTERNAL_ERROR) as u64),
18297                                                            ..Http2BridgeEvent::default()
18298                                                        },
18299                                                    );
18300                                                    if let Ok(mut state) = shared_clone.lock() {
18301                                                        state.streams.remove(&stream_id);
18302                                                    }
18303                                                }
18304                                            }
18305                                        });
18306                                        let _ = respond_to.send(Ok(json!(stream_id)));
18307                                    }
18308                                    Err(error) => {
18309                                        if let Ok(mut state) = shared.lock() {
18310                                            state.streams.remove(&stream_id);
18311                                        }
18312                                        let _ = respond_to.send(Err(error.to_string()));
18313                                    }
18314                                }
18315                            }
18316                            Http2SessionCommand::Settings { settings_json, respond_to } => {
18317                                let settings = serde_json::from_str::<BTreeMap<String, Value>>(&settings_json)
18318                                    .unwrap_or_default();
18319                                {
18320                                    let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18321                                    snapshot.local_settings = http2_settings_from_value(&settings);
18322                                }
18323                                if let Ok(headers_json) = serde_json::to_string(&settings) {
18324                                    push_http2_session_event(
18325                                        &shared,
18326                                        session_id,
18327                                        Http2BridgeEvent {
18328                                            kind: String::from("sessionLocalSettings"),
18329                                            id: session_id,
18330                                            data: Some(headers_json.clone()),
18331                                            ..Http2BridgeEvent::default()
18332                                        },
18333                                    );
18334                                    push_http2_session_event(
18335                                        &shared,
18336                                        session_id,
18337                                        Http2BridgeEvent {
18338                                            kind: String::from("sessionSettingsAck"),
18339                                            id: session_id,
18340                                            ..Http2BridgeEvent::default()
18341                                        },
18342                                    );
18343                                }
18344                                let _ = respond_to.send(Ok(Value::Null));
18345                            }
18346                            Http2SessionCommand::SetLocalWindowSize { size, respond_to } => {
18347                                {
18348                                    let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18349                                    snapshot.state.local_window_size = size;
18350                                    snapshot.state.effective_local_window_size = size;
18351                                }
18352                                let value = snapshot
18353                                    .lock()
18354                                    .ok()
18355                                    .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok())
18356                                    .map(Value::String)
18357                                    .unwrap_or(Value::Null);
18358                                let _ = respond_to.send(Ok(value));
18359                            }
18360                            Http2SessionCommand::Goaway { error_code, last_stream_id, opaque_data, respond_to } => {
18361                                push_http2_session_event(
18362                                    &shared,
18363                                    session_id,
18364                                    Http2BridgeEvent {
18365                                        kind: String::from("sessionGoaway"),
18366                                        id: session_id,
18367                                        data: opaque_data.map(|value| {
18368                                            base64::engine::general_purpose::STANDARD.encode(value)
18369                                        }),
18370                                        extra_number: Some(error_code as u64),
18371                                        flags: Some(last_stream_id as u64),
18372                                        ..Http2BridgeEvent::default()
18373                                    },
18374                                );
18375                                let _ = respond_to.send(Ok(Value::Null));
18376                            }
18377                            Http2SessionCommand::Close { respond_to, .. } => {
18378                                let _ = respond_to.send(Ok(Value::Null));
18379                                push_http2_session_event(
18380                                    &shared,
18381                                    session_id,
18382                                    Http2BridgeEvent {
18383                                        kind: String::from("sessionClose"),
18384                                        id: session_id,
18385                                        ..Http2BridgeEvent::default()
18386                                    },
18387                                );
18388                                remove_http2_session_resources(&shared, session_id);
18389                                break;
18390                            }
18391                            Http2SessionCommand::StreamWrite { stream_id, chunk, end_stream, respond_to } => {
18392                                let result = streams
18393                                    .lock()
18394                                    .expect("http2 client streams")
18395                                    .get_mut(&stream_id)
18396                                    .and_then(|stream| stream.send_stream.as_mut())
18397                                    .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 client stream {stream_id}")))
18398                                    .and_then(|stream| stream.send_data(Bytes::from(chunk), end_stream).map_err(|error| SidecarError::Execution(error.to_string())));
18399                                match result {
18400                                    Ok(()) => {
18401                                        if end_stream {
18402                                            streams.lock().expect("http2 client streams").remove(&stream_id);
18403                                        }
18404                                        let _ = respond_to.send(Ok(Value::Bool(true)));
18405                                    }
18406                                    Err(error) => {
18407                                        let _ = respond_to.send(Err(error.to_string()));
18408                                    }
18409                                }
18410                            }
18411                            Http2SessionCommand::StreamClose { stream_id, error_code, respond_to } => {
18412                                let mut streams = streams.lock().expect("http2 client streams");
18413                                let Some(mut state) = streams.remove(&stream_id) else {
18414                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 client stream {stream_id}")));
18415                                    continue;
18416                                };
18417                                if let Some(stream) = state.send_stream.as_mut() {
18418                                    stream.send_reset(http2_reason(error_code));
18419                                }
18420                                if let Ok(mut state) = shared.lock() {
18421                                    state.streams.remove(&stream_id);
18422                                }
18423                                push_http2_session_event(
18424                                    &shared,
18425                                    session_id,
18426                                    Http2BridgeEvent {
18427                                        kind: String::from("clientClose"),
18428                                        id: stream_id,
18429                                        extra_number: Some(u32::from(http2_reason(error_code)) as u64),
18430                                        ..Http2BridgeEvent::default()
18431                                    },
18432                                );
18433                                let _ = respond_to.send(Ok(Value::Null));
18434                            }
18435                            Http2SessionCommand::StreamRespond { respond_to, .. }
18436                            | Http2SessionCommand::StreamPush { respond_to, .. }
18437                            | Http2SessionCommand::StreamRespondWithFile { respond_to, .. } => {
18438                                let _ = respond_to.send(Err(String::from("HTTP/2 client streams cannot send server responses")));
18439                            }
18440                        }
18441                    }
18442                    else => break,
18443                }
18444            }
18445        });
18446    });
18447}
18448
18449fn spawn_http2_server_session(
18450    shared: Arc<Mutex<crate::state::Http2SharedState>>,
18451    server_id: u64,
18452    session_id: u64,
18453    stream: TcpStream,
18454    tls: Option<JavascriptTlsBridgeOptions>,
18455    snapshot: Arc<Mutex<Http2SessionSnapshot>>,
18456    mut command_rx: UnboundedReceiver<Http2SessionCommand>,
18457) {
18458    thread::spawn(move || {
18459        let runtime = match TokioRuntimeBuilder::new_current_thread()
18460            .enable_all()
18461            .build()
18462        {
18463            Ok(runtime) => runtime,
18464            Err(error) => {
18465                push_http2_server_event(
18466                    &shared,
18467                    server_id,
18468                    Http2BridgeEvent {
18469                        kind: String::from("serverStreamError"),
18470                        id: session_id,
18471                        data: Some(http2_error_payload(error.to_string())),
18472                        ..Http2BridgeEvent::default()
18473                    },
18474                );
18475                remove_http2_session_resources(&shared, session_id);
18476                return;
18477            }
18478        };
18479
18480        runtime.block_on(async move {
18481            if let Err(error) = stream.set_nonblocking(true) {
18482                push_http2_server_event(
18483                    &shared,
18484                    server_id,
18485                    Http2BridgeEvent {
18486                        kind: String::from("serverStreamError"),
18487                        id: session_id,
18488                        data: Some(http2_error_payload(error.to_string())),
18489                        ..Http2BridgeEvent::default()
18490                    },
18491                );
18492                remove_http2_session_resources(&shared, session_id);
18493                return;
18494            }
18495            let stream = match tokio::net::TcpStream::from_std(stream) {
18496                Ok(stream) => stream,
18497                Err(error) => {
18498                    push_http2_server_event(
18499                        &shared,
18500                        server_id,
18501                        Http2BridgeEvent {
18502                            kind: String::from("serverStreamError"),
18503                            id: session_id,
18504                            data: Some(http2_error_payload(error.to_string())),
18505                            ..Http2BridgeEvent::default()
18506                        },
18507                    );
18508                    remove_http2_session_resources(&shared, session_id);
18509                    return;
18510                }
18511            };
18512            let local_addr = match stream.local_addr() {
18513                Ok(addr) => addr,
18514                Err(error) => {
18515                    push_http2_server_event(
18516                        &shared,
18517                        server_id,
18518                        Http2BridgeEvent {
18519                            kind: String::from("serverStreamError"),
18520                            id: session_id,
18521                            data: Some(http2_error_payload(error.to_string())),
18522                            ..Http2BridgeEvent::default()
18523                        },
18524                    );
18525                    remove_http2_session_resources(&shared, session_id);
18526                    return;
18527                }
18528            };
18529            let remote_addr = match stream.peer_addr() {
18530                Ok(addr) => addr,
18531                Err(error) => {
18532                    push_http2_server_event(
18533                        &shared,
18534                        server_id,
18535                        Http2BridgeEvent {
18536                            kind: String::from("serverStreamError"),
18537                            id: session_id,
18538                            data: Some(http2_error_payload(error.to_string())),
18539                            ..Http2BridgeEvent::default()
18540                        },
18541                    );
18542                    remove_http2_session_resources(&shared, session_id);
18543                    return;
18544                }
18545            };
18546            {
18547                let mut snapshot_guard = snapshot.lock().expect("http2 snapshot lock");
18548                snapshot_guard.socket = http2_socket_snapshot(local_addr, remote_addr);
18549                if tls.is_some() {
18550                    snapshot_guard.encrypted = true;
18551                    snapshot_guard.alpn_protocol = Some(String::from("h2"));
18552                    snapshot_guard.socket.encrypted = true;
18553                    snapshot_guard.socket.alpn_protocol = Some(String::from("h2"));
18554                }
18555                snapshot_guard.state = http2_runtime_snapshot();
18556            }
18557            if let Ok(snapshot_json) =
18558                http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())
18559            {
18560                push_http2_server_event(
18561                    &shared,
18562                    server_id,
18563                    Http2BridgeEvent {
18564                        kind: String::from(if tls.is_some() {
18565                            "serverSecureConnection"
18566                        } else {
18567                            "serverConnection"
18568                        }),
18569                        id: server_id,
18570                        data: Some(serde_json::to_string(&http2_socket_snapshot(local_addr, remote_addr)).unwrap_or_default()),
18571                        ..Http2BridgeEvent::default()
18572                    },
18573                );
18574                push_http2_server_event(
18575                    &shared,
18576                    server_id,
18577                    Http2BridgeEvent {
18578                        kind: String::from("serverSession"),
18579                        id: server_id,
18580                        data: Some(snapshot_json),
18581                        extra_number: Some(session_id),
18582                        ..Http2BridgeEvent::default()
18583                    },
18584                );
18585            }
18586
18587            let io: Pin<Box<dyn Http2AsyncIo>> = if let Some(options) = tls.as_ref() {
18588                let acceptor = match build_server_tls_config(options) {
18589                    Ok(config) => TlsAcceptor::from(Arc::new(config)),
18590                    Err(error) => {
18591                        push_http2_server_event(
18592                            &shared,
18593                            server_id,
18594                            Http2BridgeEvent {
18595                                kind: String::from("serverStreamError"),
18596                                id: session_id,
18597                                data: Some(http2_error_payload(error.to_string())),
18598                                ..Http2BridgeEvent::default()
18599                            },
18600                        );
18601                        remove_http2_session_resources(&shared, session_id);
18602                        return;
18603                    }
18604                };
18605                match acceptor.accept(stream).await {
18606                    Ok(tls_stream) => Box::pin(tls_stream),
18607                    Err(error) => {
18608                        push_http2_server_event(
18609                            &shared,
18610                            server_id,
18611                            Http2BridgeEvent {
18612                                kind: String::from("serverStreamError"),
18613                                id: session_id,
18614                                data: Some(http2_error_payload(error.to_string())),
18615                                ..Http2BridgeEvent::default()
18616                            },
18617                        );
18618                        remove_http2_session_resources(&shared, session_id);
18619                        return;
18620                    }
18621                }
18622            } else {
18623                Box::pin(stream)
18624            };
18625
18626            let mut connection = match server::handshake(io).await {
18627                Ok(connection) => connection,
18628                Err(error) => {
18629                    push_http2_server_event(
18630                        &shared,
18631                        server_id,
18632                        Http2BridgeEvent {
18633                            kind: String::from("serverStreamError"),
18634                            id: session_id,
18635                            data: Some(http2_error_payload(error.to_string())),
18636                            ..Http2BridgeEvent::default()
18637                        },
18638                    );
18639                    remove_http2_session_resources(&shared, session_id);
18640                    return;
18641                }
18642            };
18643
18644            let streams: Arc<Mutex<BTreeMap<u64, ServerHttp2StreamState>>> =
18645                Arc::new(Mutex::new(BTreeMap::new()));
18646
18647            loop {
18648                tokio::select! {
18649                    incoming = connection.accept() => {
18650                        match incoming {
18651                            Some(Ok((request, respond))) => {
18652                                let headers_json = match serialize_http2_request_headers(&request) {
18653                                    Ok(headers) => headers,
18654                                    Err(error) => {
18655                                        push_http2_server_event(
18656                                            &shared,
18657                                            server_id,
18658                                            Http2BridgeEvent {
18659                                                kind: String::from("serverStreamError"),
18660                                                id: server_id,
18661                                                data: Some(http2_error_payload(error.to_string())),
18662                                                ..Http2BridgeEvent::default()
18663                                            },
18664                                        );
18665                                        continue;
18666                                    }
18667                                };
18668                                let stream_id = {
18669                                    let mut state = shared.lock().expect("http2 shared state");
18670                                    let stream_id = next_http2_stream_id(&mut state);
18671                                    state.streams.insert(
18672                                        stream_id,
18673                                        ActiveHttp2Stream {
18674                                            session_id,
18675                                            paused: Arc::new(AtomicBool::new(false)),
18676                                        },
18677                                    );
18678                                    stream_id
18679                                };
18680                                streams.lock().expect("http2 server streams").insert(
18681                                    stream_id,
18682                                    ServerHttp2StreamState {
18683                                        send_response: Some(ServerHttp2Responder::Regular(respond)),
18684                                        send_stream: None,
18685                                    },
18686                                );
18687                                let snapshot_json = snapshot
18688                                    .lock()
18689                                    .ok()
18690                                    .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok());
18691                                push_http2_server_event(
18692                                    &shared,
18693                                    server_id,
18694                                    Http2BridgeEvent {
18695                                        kind: String::from("serverStream"),
18696                                        id: server_id,
18697                                        data: Some(stream_id.to_string()),
18698                                        extra: snapshot_json,
18699                                        extra_number: Some(session_id),
18700                                        extra_headers: Some(headers_json),
18701                                        flags: Some(0),
18702                                    },
18703                                );
18704                                let shared_clone = Arc::clone(&shared);
18705                                tokio::spawn(async move {
18706                                    let mut body = request.into_body();
18707                                    while let Some(chunk) = body.data().await {
18708                                        match chunk {
18709                                            Ok(bytes) => {
18710                                                let paused = {
18711                                                    let state = shared_clone.lock().expect("http2 shared state");
18712                                                    state.streams.get(&stream_id).map(|stream| Arc::clone(&stream.paused))
18713                                                };
18714                                                if let Some(paused) = paused {
18715                                                    while paused.load(Ordering::SeqCst) {
18716                                                        tokio::time::sleep(HTTP2_POLL_DELAY).await;
18717                                                    }
18718                                                }
18719                                                let _ = body.flow_control().release_capacity(bytes.len());
18720                                                push_http2_server_event(
18721                                                    &shared_clone,
18722                                                    server_id,
18723                                                    Http2BridgeEvent {
18724                                                        kind: String::from("serverStreamData"),
18725                                                        id: stream_id,
18726                                                        data: Some(base64::engine::general_purpose::STANDARD.encode(bytes)),
18727                                                        ..Http2BridgeEvent::default()
18728                                                    },
18729                                                );
18730                                            }
18731                                            Err(error) => {
18732                                                push_http2_server_event(
18733                                                    &shared_clone,
18734                                                    server_id,
18735                                                    Http2BridgeEvent {
18736                                                        kind: String::from("serverStreamError"),
18737                                                        id: stream_id,
18738                                                        data: Some(http2_error_payload(error.to_string())),
18739                                                        ..Http2BridgeEvent::default()
18740                                                    },
18741                                                );
18742                                                break;
18743                                            }
18744                                        }
18745                                    }
18746                                    push_http2_server_event(
18747                                        &shared_clone,
18748                                        server_id,
18749                                        Http2BridgeEvent {
18750                                            kind: String::from("serverStreamEnd"),
18751                                            id: stream_id,
18752                                            ..Http2BridgeEvent::default()
18753                                        },
18754                                    );
18755                                });
18756                            }
18757                            Some(Err(error)) => {
18758                                push_http2_server_event(
18759                                    &shared,
18760                                    server_id,
18761                                    Http2BridgeEvent {
18762                                        kind: String::from("serverStreamError"),
18763                                        id: server_id,
18764                                        data: Some(http2_error_payload(error.to_string())),
18765                                        ..Http2BridgeEvent::default()
18766                                    },
18767                                );
18768                                break;
18769                            }
18770                            None => {
18771                                push_http2_server_event(
18772                                    &shared,
18773                                    server_id,
18774                                    Http2BridgeEvent {
18775                                        kind: String::from("sessionClose"),
18776                                        id: session_id,
18777                                        ..Http2BridgeEvent::default()
18778                                    },
18779                                );
18780                                remove_http2_session_resources(&shared, session_id);
18781                                break;
18782                            }
18783                        }
18784                    }
18785                    Some(command) = command_rx.recv() => {
18786                        match command {
18787                            Http2SessionCommand::Settings { settings_json, respond_to } => {
18788                                let settings = serde_json::from_str::<BTreeMap<String, Value>>(&settings_json)
18789                                    .unwrap_or_default();
18790                                if let Some(initial_window_size) = settings
18791                                    .get("initialWindowSize")
18792                                    .and_then(Value::as_u64)
18793                                {
18794                                    let _ = connection.set_initial_window_size(initial_window_size as u32);
18795                                }
18796                                {
18797                                    let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18798                                    snapshot.local_settings = http2_settings_from_value(&settings);
18799                                }
18800                                if let Ok(headers_json) = serde_json::to_string(&settings) {
18801                                    push_http2_session_event(
18802                                        &shared,
18803                                        session_id,
18804                                        Http2BridgeEvent {
18805                                            kind: String::from("sessionLocalSettings"),
18806                                            id: session_id,
18807                                            data: Some(headers_json),
18808                                            ..Http2BridgeEvent::default()
18809                                        },
18810                                    );
18811                                }
18812                                let _ = respond_to.send(Ok(Value::Null));
18813                            }
18814                            Http2SessionCommand::SetLocalWindowSize { size, respond_to } => {
18815                                connection.set_target_window_size(size);
18816                                {
18817                                    let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18818                                    snapshot.state.local_window_size = size;
18819                                    snapshot.state.effective_local_window_size = size;
18820                                }
18821                                let value = snapshot
18822                                    .lock()
18823                                    .ok()
18824                                    .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok())
18825                                    .map(Value::String)
18826                                    .unwrap_or(Value::Null);
18827                                let _ = respond_to.send(Ok(value));
18828                            }
18829                            Http2SessionCommand::Goaway { error_code, last_stream_id, opaque_data, respond_to } => {
18830                                connection.abrupt_shutdown(http2_reason(Some(error_code)));
18831                                push_http2_session_event(
18832                                    &shared,
18833                                    session_id,
18834                                    Http2BridgeEvent {
18835                                        kind: String::from("sessionGoaway"),
18836                                        id: session_id,
18837                                        data: opaque_data.map(|value| {
18838                                            base64::engine::general_purpose::STANDARD.encode(value)
18839                                        }),
18840                                        extra_number: Some(error_code as u64),
18841                                        flags: Some(last_stream_id as u64),
18842                                        ..Http2BridgeEvent::default()
18843                                    },
18844                                );
18845                                let _ = respond_to.send(Ok(Value::Null));
18846                            }
18847                            Http2SessionCommand::Close { abrupt, respond_to } => {
18848                                if abrupt {
18849                                    connection.abrupt_shutdown(Reason::NO_ERROR);
18850                                } else {
18851                                    connection.graceful_shutdown();
18852                                }
18853                                let _ = respond_to.send(Ok(Value::Null));
18854                                push_http2_session_event(
18855                                    &shared,
18856                                    session_id,
18857                                    Http2BridgeEvent {
18858                                        kind: String::from("sessionClose"),
18859                                        id: session_id,
18860                                        ..Http2BridgeEvent::default()
18861                                    },
18862                                );
18863                                remove_http2_session_resources(&shared, session_id);
18864                                break;
18865                            }
18866                            Http2SessionCommand::StreamRespond { stream_id, headers_json, respond_to } => {
18867                                let response = match build_http2_response(&headers_json) {
18868                                    Ok(response) => response,
18869                                    Err(error) => {
18870                                        let _ = respond_to.send(Err(error.to_string()));
18871                                        continue;
18872                                    }
18873                                };
18874                                let mut streams = streams.lock().expect("http2 server streams");
18875                                let Some(state) = streams.get_mut(&stream_id) else {
18876                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
18877                                    continue;
18878                                };
18879                                let Some(send_response) = state.send_response.as_mut() else {
18880                                    let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} already responded")));
18881                                    continue;
18882                                };
18883                                match match send_response {
18884                                    ServerHttp2Responder::Regular(send_response) => {
18885                                        send_response.send_response(response, false)
18886                                    }
18887                                    ServerHttp2Responder::Pushed(send_response) => {
18888                                        send_response.send_response(response, false)
18889                                    }
18890                                } {
18891                                    Ok(send_stream) => {
18892                                        state.send_stream = Some(send_stream);
18893                                        state.send_response = None;
18894                                        let _ = respond_to.send(Ok(Value::Null));
18895                                    }
18896                                    Err(error) => {
18897                                        let _ = respond_to.send(Err(error.to_string()));
18898                                    }
18899                                }
18900                            }
18901                            Http2SessionCommand::StreamPush { stream_id, headers_json, respond_to } => {
18902                                let request = match build_http2_request(&headers_json) {
18903                                    Ok(request) => request,
18904                                    Err(error) => {
18905                                        let _ = respond_to.send(Err(error.to_string()));
18906                                        continue;
18907                                    }
18908                                };
18909                                let mut streams_guard = streams.lock().expect("http2 server streams");
18910                                let Some(state) = streams_guard.get_mut(&stream_id) else {
18911                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
18912                                    continue;
18913                                };
18914                                let Some(send_response) = state.send_response.as_mut() else {
18915                                    let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} cannot push after responding")));
18916                                    continue;
18917                                };
18918                                let ServerHttp2Responder::Regular(send_response) = send_response else {
18919                                    let _ = respond_to.send(Err(format!("HTTP/2 pushed stream {stream_id} cannot create nested push promises")));
18920                                    continue;
18921                                };
18922                                match send_response.push_request(request) {
18923                                    Ok(pushed) => {
18924                                        let pushed_stream_id = {
18925                                            let mut state = shared.lock().expect("http2 shared state");
18926                                            let pushed_stream_id = next_http2_stream_id(&mut state);
18927                                            state.streams.insert(
18928                                                pushed_stream_id,
18929                                                ActiveHttp2Stream {
18930                                                    session_id,
18931                                                    paused: Arc::new(AtomicBool::new(false)),
18932                                                },
18933                                            );
18934                                            pushed_stream_id
18935                                        };
18936                                        streams_guard.insert(
18937                                            pushed_stream_id,
18938                                            ServerHttp2StreamState {
18939                                                send_response: Some(ServerHttp2Responder::Pushed(pushed)),
18940                                                send_stream: None,
18941                                            },
18942                                        );
18943                                        let _ = respond_to.send(Ok(json!({
18944                                            "streamId": pushed_stream_id,
18945                                            "headers": headers_json,
18946                                        }).to_string().into()));
18947                                    }
18948                                    Err(error) => {
18949                                        let _ = respond_to.send(Err(error.to_string()));
18950                                    }
18951                                }
18952                            }
18953                            Http2SessionCommand::StreamWrite { stream_id, chunk, end_stream, respond_to } => {
18954                                let mut streams = streams.lock().expect("http2 server streams");
18955                                let Some(state) = streams.get_mut(&stream_id) else {
18956                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
18957                                    continue;
18958                                };
18959                                let Some(send_stream) = state.send_stream.as_mut() else {
18960                                    let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} has not sent response headers")));
18961                                    continue;
18962                                };
18963                                match send_stream.send_data(Bytes::from(chunk), end_stream) {
18964                                    Ok(()) => {
18965                                        if end_stream {
18966                                            streams.remove(&stream_id);
18967                                            if let Ok(mut state) = shared.lock() {
18968                                                state.streams.remove(&stream_id);
18969                                            }
18970                                            push_http2_server_event(
18971                                                &shared,
18972                                                server_id,
18973                                                Http2BridgeEvent {
18974                                                    kind: String::from("serverStreamClose"),
18975                                                    id: stream_id,
18976                                                    extra_number: Some(0),
18977                                                    ..Http2BridgeEvent::default()
18978                                                },
18979                                            );
18980                                        }
18981                                        let _ = respond_to.send(Ok(Value::Bool(true)));
18982                                    }
18983                                    Err(error) => {
18984                                        let _ = respond_to.send(Err(error.to_string()));
18985                                    }
18986                                }
18987                            }
18988                            Http2SessionCommand::StreamClose { stream_id, error_code, respond_to } => {
18989                                let mut streams_guard = streams.lock().expect("http2 server streams");
18990                                let Some(mut state) = streams_guard.remove(&stream_id) else {
18991                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
18992                                    continue;
18993                                };
18994                                let reason = http2_reason(error_code);
18995                                if let Some(send_stream) = state.send_stream.as_mut() {
18996                                    send_stream.send_reset(reason);
18997                                }
18998                                if let Some(send_response) = state.send_response.as_mut() {
18999                                    match send_response {
19000                                        ServerHttp2Responder::Regular(send_response) => {
19001                                            send_response.send_reset(reason)
19002                                        }
19003                                        ServerHttp2Responder::Pushed(send_response) => {
19004                                            send_response.send_reset(reason)
19005                                        }
19006                                    }
19007                                }
19008                                if let Ok(mut shared_guard) = shared.lock() {
19009                                    shared_guard.streams.remove(&stream_id);
19010                                }
19011                                push_http2_server_event(
19012                                    &shared,
19013                                    server_id,
19014                                    Http2BridgeEvent {
19015                                        kind: String::from("serverStreamClose"),
19016                                        id: stream_id,
19017                                        extra_number: Some(u32::from(reason) as u64),
19018                                        ..Http2BridgeEvent::default()
19019                                    },
19020                                );
19021                                let _ = respond_to.send(Ok(Value::Null));
19022                            }
19023                            Http2SessionCommand::StreamRespondWithFile { stream_id, body, headers_json, options_json, respond_to } => {
19024                                let options: JavascriptHttp2FileResponseOptions =
19025                                    serde_json::from_str(&options_json).unwrap_or_default();
19026                                let response = match build_http2_response(&headers_json) {
19027                                    Ok(response) => response,
19028                                    Err(error) => {
19029                                        let _ = respond_to.send(Err(error.to_string()));
19030                                        continue;
19031                                    }
19032                                };
19033                                let offset = usize::try_from(options.offset.unwrap_or_default()).unwrap_or(0);
19034                                let body = if offset >= body.len() {
19035                                    Vec::new()
19036                                } else {
19037                                    let body = &body[offset..];
19038                                    match options.length {
19039                                        Some(length) if length >= 0 => {
19040                                            body[..body.len().min(length as usize)].to_vec()
19041                                        }
19042                                        _ => body.to_vec(),
19043                                    }
19044                                };
19045                                let mut streams_guard = streams.lock().expect("http2 server streams");
19046                                let Some(state) = streams_guard.get_mut(&stream_id) else {
19047                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
19048                                    continue;
19049                                };
19050                                let Some(send_response) = state.send_response.as_mut() else {
19051                                    let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} already responded")));
19052                                    continue;
19053                                };
19054                                match match send_response {
19055                                    ServerHttp2Responder::Regular(send_response) => {
19056                                        send_response.send_response(response, body.is_empty())
19057                                    }
19058                                    ServerHttp2Responder::Pushed(send_response) => {
19059                                        send_response.send_response(response, body.is_empty())
19060                                    }
19061                                } {
19062                                    Ok(mut send_stream) => {
19063                                        state.send_response = None;
19064                                        if body.is_empty() {
19065                                            streams_guard.remove(&stream_id);
19066                                            if let Ok(mut shared_guard) = shared.lock() {
19067                                                shared_guard.streams.remove(&stream_id);
19068                                            }
19069                                        } else {
19070                                            if let Err(error) = send_stream.send_data(Bytes::from(body), true) {
19071                                                let _ = respond_to.send(Err(error.to_string()));
19072                                                continue;
19073                                            }
19074                                            streams_guard.remove(&stream_id);
19075                                            if let Ok(mut shared_guard) = shared.lock() {
19076                                                shared_guard.streams.remove(&stream_id);
19077                                            }
19078                                        }
19079                                        push_http2_server_event(
19080                                            &shared,
19081                                            server_id,
19082                                            Http2BridgeEvent {
19083                                                kind: String::from("serverStreamClose"),
19084                                                id: stream_id,
19085                                                extra_number: Some(0),
19086                                                ..Http2BridgeEvent::default()
19087                                            },
19088                                        );
19089                                        let _ = respond_to.send(Ok(Value::Null));
19090                                    }
19091                                    Err(error) => {
19092                                        let _ = respond_to.send(Err(error.to_string()));
19093                                    }
19094                                }
19095                            }
19096                            Http2SessionCommand::Request { respond_to, .. } => {
19097                                let _ = respond_to.send(Err(String::from("HTTP/2 server sessions cannot initiate client requests")));
19098                            }
19099                        }
19100                    }
19101                    else => break,
19102                }
19103            }
19104        });
19105    });
19106}
19107
19108fn spawn_http2_server_accept_loop(
19109    shared: Arc<Mutex<crate::state::Http2SharedState>>,
19110    server_id: u64,
19111    listener: TcpListener,
19112) {
19113    thread::spawn(move || {
19114        let listener = listener;
19115        loop {
19116            let closed = shared
19117                .lock()
19118                .ok()
19119                .and_then(|state| {
19120                    state
19121                        .servers
19122                        .get(&server_id)
19123                        .map(|server| server.closed.load(Ordering::SeqCst))
19124                })
19125                .unwrap_or(true);
19126            if closed {
19127                break;
19128            }
19129            match listener.accept() {
19130                Ok((stream, _)) => {
19131                    let (command_tx, command_rx) = unbounded_channel();
19132                    let (guest_local_addr, secure, tls) = {
19133                        let state = shared.lock().expect("http2 shared state");
19134                        let server = state.servers.get(&server_id).expect("http2 server state");
19135                        (server.guest_local_addr, server.secure, server.tls.clone())
19136                    };
19137                    let (local_addr, remote_addr) = match (stream.local_addr(), stream.peer_addr())
19138                    {
19139                        (Ok(local_addr), Ok(remote_addr)) => (local_addr, remote_addr),
19140                        _ => continue,
19141                    };
19142                    let session_snapshot = Arc::new(Mutex::new(Http2SessionSnapshot {
19143                        encrypted: secure,
19144                        alpn_protocol: Some(if secure {
19145                            String::from("h2")
19146                        } else {
19147                            String::from("h2c")
19148                        }),
19149                        local_settings: BTreeMap::new(),
19150                        remote_settings: BTreeMap::new(),
19151                        state: http2_runtime_snapshot(),
19152                        socket: Http2SocketSnapshot {
19153                            local_address: Some(guest_local_addr.ip().to_string()),
19154                            local_port: Some(guest_local_addr.port()),
19155                            local_family: Some(socket_addr_family(&guest_local_addr).to_string()),
19156                            remote_address: Some(remote_addr.ip().to_string()),
19157                            remote_port: Some(remote_addr.port()),
19158                            remote_family: Some(socket_addr_family(&remote_addr).to_string()),
19159                            ..http2_socket_snapshot(local_addr, remote_addr)
19160                        },
19161                        ..Http2SessionSnapshot::default()
19162                    }));
19163                    let session_id = {
19164                        let mut state = shared.lock().expect("http2 shared state");
19165                        let session_id = next_http2_session_id(&mut state);
19166                        state
19167                            .sessions
19168                            .insert(session_id, ActiveHttp2Session { command_tx });
19169                        session_id
19170                    };
19171                    spawn_http2_server_session(
19172                        Arc::clone(&shared),
19173                        server_id,
19174                        session_id,
19175                        stream,
19176                        tls,
19177                        session_snapshot,
19178                        command_rx,
19179                    );
19180                }
19181                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
19182                    thread::sleep(HTTP2_POLL_DELAY);
19183                }
19184                Err(error) => {
19185                    push_http2_server_event(
19186                        &shared,
19187                        server_id,
19188                        Http2BridgeEvent {
19189                            kind: String::from("serverStreamError"),
19190                            id: server_id,
19191                            data: Some(http2_error_payload(error.to_string())),
19192                            ..Http2BridgeEvent::default()
19193                        },
19194                    );
19195                    thread::sleep(HTTP2_POLL_DELAY);
19196                }
19197            }
19198        }
19199    });
19200}
19201
19202fn send_http2_command(
19203    session: &ActiveHttp2Session,
19204    command: impl FnOnce(Sender<Result<Value, String>>) -> Http2SessionCommand,
19205) -> Result<Value, SidecarError> {
19206    let (respond_to, response_rx) = mpsc::channel();
19207    session.command_tx.send(command(respond_to)).map_err(|_| {
19208        SidecarError::InvalidState(String::from("HTTP/2 session command channel closed"))
19209    })?;
19210    response_rx
19211        .recv_timeout(Duration::from_secs(30))
19212        .map_err(|_| {
19213            SidecarError::Execution(String::from("timed out waiting for HTTP/2 session command"))
19214        })?
19215        .map_err(SidecarError::Execution)
19216}
19217
19218fn parse_http2_server_listen_payload(
19219    request: &JavascriptSyncRpcRequest,
19220) -> Result<JavascriptHttp2ServerListenRequest, SidecarError> {
19221    let payload_json =
19222        javascript_sync_rpc_arg_str(&request.args, 0, "net.http2_server_listen payload")?;
19223    serde_json::from_str(payload_json).map_err(|error| {
19224        SidecarError::InvalidState(format!(
19225            "net.http2_server_listen payload must be valid JSON: {error}"
19226        ))
19227    })
19228}
19229
19230fn parse_http2_connect_payload(
19231    request: &JavascriptSyncRpcRequest,
19232) -> Result<JavascriptHttp2SessionConnectRequest, SidecarError> {
19233    let payload_json =
19234        javascript_sync_rpc_arg_str(&request.args, 0, "net.http2_session_connect payload")?;
19235    serde_json::from_str(payload_json).map_err(|error| {
19236        SidecarError::InvalidState(format!(
19237            "net.http2_session_connect payload must be valid JSON: {error}"
19238        ))
19239    })
19240}
19241
19242fn http2_session_for_id(
19243    process: &ActiveProcess,
19244    session_id: u64,
19245) -> Result<ActiveHttp2Session, SidecarError> {
19246    let shared = process
19247        .http2
19248        .shared
19249        .lock()
19250        .map_err(|_| SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned")))?;
19251    shared
19252        .sessions
19253        .get(&session_id)
19254        .cloned()
19255        .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 session {session_id}")))
19256}
19257
19258fn http2_stream_for_id(
19259    process: &ActiveProcess,
19260    stream_id: u64,
19261) -> Result<ActiveHttp2Stream, SidecarError> {
19262    let shared = process
19263        .http2
19264        .shared
19265        .lock()
19266        .map_err(|_| SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned")))?;
19267    shared
19268        .streams
19269        .get(&stream_id)
19270        .cloned()
19271        .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 stream {stream_id}")))
19272}
19273
19274fn service_javascript_http2_sync_rpc<B>(
19275    request: JavascriptHttp2SyncRpcServiceRequest<'_, B>,
19276) -> Result<Value, SidecarError>
19277where
19278    B: NativeSidecarBridge + Send + 'static,
19279    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19280{
19281    let JavascriptHttp2SyncRpcServiceRequest {
19282        bridge,
19283        kernel,
19284        vm_id,
19285        dns,
19286        socket_paths,
19287        process,
19288        sync_request: request,
19289        resource_limits,
19290        network_counts,
19291    } = request;
19292    match request.method.as_str() {
19293        "net.http2_server_listen" => {
19294            check_network_resource_limit(
19295                resource_limits.max_sockets,
19296                network_counts.sockets,
19297                1,
19298                "socket",
19299            )?;
19300            let payload = parse_http2_server_listen_payload(request)?;
19301            let (family, bind_host, guest_host) =
19302                normalize_tcp_listen_host(payload.host.as_deref())?;
19303            let requested_port = payload.port.unwrap_or(0);
19304            bridge.require_network_access(
19305                vm_id,
19306                NetworkOperation::Listen,
19307                format_tcp_resource(bind_host, requested_port),
19308            )?;
19309            let port = allocate_guest_listen_port(
19310                requested_port,
19311                family,
19312                &socket_paths.used_tcp_guest_ports,
19313                socket_paths.listen_policy,
19314            )?;
19315            let mut listener =
19316                ActiveTcpListener::bind(bind_host, guest_host, port, payload.backlog)?;
19317            let guest_local_addr = listener.guest_local_addr();
19318            let closed = Arc::new(AtomicBool::new(false));
19319            {
19320                let mut state = process.http2.shared.lock().map_err(|_| {
19321                    SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19322                })?;
19323                state.servers.insert(
19324                    payload.server_id,
19325                    ActiveHttp2Server {
19326                        actual_local_addr: listener.local_addr(),
19327                        guest_local_addr,
19328                        secure: payload.secure,
19329                        tls: payload.tls.clone().map(|mut tls| {
19330                            tls.is_server = payload.secure;
19331                            if payload.secure && tls.alpn_protocols.is_none() {
19332                                tls.alpn_protocols = Some(vec![String::from("h2")]);
19333                            }
19334                            tls
19335                        }),
19336                        closed: Arc::clone(&closed),
19337                    },
19338                );
19339                state.server_events.entry(payload.server_id).or_default();
19340            }
19341            spawn_http2_server_accept_loop(
19342                Arc::clone(&process.http2.shared),
19343                payload.server_id,
19344                listener.listener.take().ok_or_else(|| {
19345                    SidecarError::InvalidState(String::from(
19346                        "HTTP/2 listener missing host TCP socket",
19347                    ))
19348                })?,
19349            );
19350            javascript_net_json_string(
19351                json!({
19352                    "address": {
19353                        "address": guest_local_addr.ip().to_string(),
19354                        "family": socket_addr_family(&guest_local_addr),
19355                        "port": guest_local_addr.port(),
19356                    }
19357                }),
19358                "net.http2_server_listen",
19359            )
19360        }
19361        "net.http2_server_poll" => {
19362            let server_id =
19363                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_poll server id")?;
19364            let wait_ms = javascript_sync_rpc_arg_u64_optional(
19365                &request.args,
19366                1,
19367                "net.http2_server_poll wait ms",
19368            )?
19369            .unwrap_or_default();
19370            match wait_for_http2_event(&process.http2.shared, server_id, true, wait_ms) {
19371                Some(event) => http2_event_value(&event),
19372                None => Ok(Value::Null),
19373            }
19374        }
19375        "net.http2_server_wait" => {
19376            let server_id =
19377                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_wait server id")?;
19378            dispatch_http2_wait_loop(process, server_id, true)
19379        }
19380        "net.http2_server_close" => {
19381            let server_id =
19382                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_close server id")?;
19383            let server = {
19384                let mut state = process.http2.shared.lock().map_err(|_| {
19385                    SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19386                })?;
19387                state.servers.remove(&server_id)
19388            }
19389            .ok_or_else(|| {
19390                SidecarError::InvalidState(format!("unknown HTTP/2 server {server_id}"))
19391            })?;
19392            server.closed.store(true, Ordering::SeqCst);
19393            push_http2_server_event(
19394                &process.http2.shared,
19395                server_id,
19396                Http2BridgeEvent {
19397                    kind: String::from("serverClose"),
19398                    id: server_id,
19399                    ..Http2BridgeEvent::default()
19400                },
19401            );
19402            Ok(Value::Null)
19403        }
19404        "net.http2_server_respond" => {
19405            let server_id = javascript_sync_rpc_arg_u64(
19406                &request.args,
19407                0,
19408                "net.http2_server_respond server id",
19409            )?;
19410            let request_id = javascript_sync_rpc_arg_u64(
19411                &request.args,
19412                1,
19413                "net.http2_server_respond request id",
19414            )?;
19415            let response_json =
19416                javascript_sync_rpc_arg_str(&request.args, 2, "net.http2_server_respond payload")?;
19417            ensure_vm_fetch_response_within_limit(
19418                response_json,
19419                "net.http2_server_respond",
19420                VM_FETCH_BUFFER_LIMIT_BYTES,
19421            )?;
19422            serde_json::from_str::<Value>(response_json).map_err(|error| {
19423                SidecarError::Execution(format!(
19424                    "net.http2_server_respond payload must be valid JSON: {error}"
19425                ))
19426            })?;
19427            let Some(pending) = process
19428                .pending_http_requests
19429                .get_mut(&(server_id, request_id))
19430            else {
19431                return Err(SidecarError::InvalidState(format!(
19432                    "unknown pending HTTP/2 request {request_id} for server {server_id}"
19433                )));
19434            };
19435            *pending = Some(response_json.to_owned());
19436            Ok(Value::Bool(true))
19437        }
19438        "net.http2_session_connect" => {
19439            check_network_resource_limit(
19440                resource_limits.max_sockets,
19441                network_counts.sockets,
19442                1,
19443                "socket",
19444            )?;
19445            check_network_resource_limit(
19446                resource_limits.max_connections,
19447                network_counts.connections,
19448                1,
19449                "connection",
19450            )?;
19451            let payload = parse_http2_connect_payload(request)?;
19452            let authority = payload.authority.clone().unwrap_or_else(|| {
19453                format!(
19454                    "{}://{}:{}",
19455                    payload.protocol.as_deref().unwrap_or("http"),
19456                    payload.host.as_deref().unwrap_or("localhost"),
19457                    payload.port.unwrap_or(80)
19458                )
19459            });
19460            let url = Url::parse(&authority).map_err(|error| {
19461                SidecarError::InvalidState(format!(
19462                    "invalid HTTP/2 authority {authority:?}: {error}"
19463                ))
19464            })?;
19465            let secure = url.scheme() == "https" || payload.protocol.as_deref() == Some("https:");
19466            let host = payload
19467                .host
19468                .as_deref()
19469                .or_else(|| url.host_str())
19470                .unwrap_or("localhost");
19471            let port = payload.port.or_else(|| url.port()).unwrap_or(80);
19472            bridge.require_network_access(
19473                vm_id,
19474                NetworkOperation::Http,
19475                format_tcp_resource(host, port),
19476            )?;
19477            let resolved = {
19478                let shared = process.http2.shared.lock().map_err(|_| {
19479                    SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19480                })?;
19481                shared
19482                    .servers
19483                    .values()
19484                    .find(|server| {
19485                        is_loopback_request_host(host) && server.guest_local_addr.port() == port
19486                    })
19487                    .map(|server| ResolvedTcpConnectAddr {
19488                        actual_addr: server.actual_local_addr,
19489                        guest_remote_addr: server.guest_local_addr,
19490                        use_kernel_loopback: false,
19491                    })
19492            };
19493            let resolved = match resolved {
19494                Some(resolved) => resolved,
19495                None => {
19496                    resolve_tcp_connect_addr(bridge, kernel, vm_id, dns, host, port, socket_paths)?
19497                }
19498            };
19499            let (command_tx, command_rx) = unbounded_channel();
19500            let snapshot = Arc::new(Mutex::new(Http2SessionSnapshot {
19501                encrypted: secure,
19502                alpn_protocol: Some(String::from(if secure { "h2" } else { "h2c" })),
19503                local_settings: http2_settings_from_value(&payload.settings),
19504                remote_settings: BTreeMap::new(),
19505                state: http2_runtime_snapshot(),
19506                socket: Http2SocketSnapshot {
19507                    encrypted: secure,
19508                    remote_address: Some(resolved.guest_remote_addr.ip().to_string()),
19509                    remote_port: Some(resolved.guest_remote_addr.port()),
19510                    remote_family: Some(
19511                        socket_addr_family(&resolved.guest_remote_addr).to_string(),
19512                    ),
19513                    servername: if secure {
19514                        payload
19515                            .tls
19516                            .as_ref()
19517                            .and_then(|tls| tls.servername.clone())
19518                            .or_else(|| Some(host.to_string()))
19519                    } else {
19520                        None
19521                    },
19522                    alpn_protocol: Some(String::from(if secure { "h2" } else { "h2c" })),
19523                    ..Http2SocketSnapshot::default()
19524                },
19525                ..Http2SessionSnapshot::default()
19526            }));
19527            let session_id = {
19528                let mut state = process.http2.shared.lock().map_err(|_| {
19529                    SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19530                })?;
19531                let session_id = next_http2_session_id(&mut state);
19532                state
19533                    .sessions
19534                    .insert(session_id, ActiveHttp2Session { command_tx });
19535                state.session_events.entry(session_id).or_default();
19536                session_id
19537            };
19538            spawn_http2_client_session(
19539                Arc::clone(&process.http2.shared),
19540                session_id,
19541                resolved.actual_addr,
19542                if secure {
19543                    Some(payload.tls.unwrap_or(JavascriptTlsBridgeOptions {
19544                        is_server: false,
19545                        servername: Some(host.to_string()),
19546                        alpn_protocols: Some(vec![String::from("h2")]),
19547                        ..JavascriptTlsBridgeOptions::default()
19548                    }))
19549                } else {
19550                    None
19551                },
19552                Arc::clone(&snapshot),
19553                command_rx,
19554            );
19555            let snapshot_json =
19556                http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())?;
19557            javascript_net_json_string(
19558                json!({
19559                    "sessionId": session_id,
19560                    "state": snapshot_json,
19561                }),
19562                "net.http2_session_connect",
19563            )
19564        }
19565        "net.http2_session_request" => {
19566            let session_id = javascript_sync_rpc_arg_u64(
19567                &request.args,
19568                0,
19569                "net.http2_session_request session id",
19570            )?;
19571            let headers_json =
19572                javascript_sync_rpc_arg_str(&request.args, 1, "net.http2_session_request headers")?;
19573            let options_json =
19574                javascript_sync_rpc_arg_str(&request.args, 2, "net.http2_session_request options")?;
19575            let session = http2_session_for_id(process, session_id)?;
19576            send_http2_command(&session, |respond_to| Http2SessionCommand::Request {
19577                headers_json: headers_json.to_owned(),
19578                options_json: options_json.to_owned(),
19579                respond_to,
19580            })
19581        }
19582        "net.http2_session_settings" => {
19583            let session_id = javascript_sync_rpc_arg_u64(
19584                &request.args,
19585                0,
19586                "net.http2_session_settings session id",
19587            )?;
19588            let settings_json = javascript_sync_rpc_arg_str(
19589                &request.args,
19590                1,
19591                "net.http2_session_settings settings",
19592            )?;
19593            let session = http2_session_for_id(process, session_id)?;
19594            send_http2_command(&session, |respond_to| Http2SessionCommand::Settings {
19595                settings_json: settings_json.to_owned(),
19596                respond_to,
19597            })
19598        }
19599        "net.http2_session_set_local_window_size" => {
19600            let session_id = javascript_sync_rpc_arg_u64(
19601                &request.args,
19602                0,
19603                "net.http2_session_set_local_window_size session id",
19604            )?;
19605            let window_size = javascript_sync_rpc_arg_u64(
19606                &request.args,
19607                1,
19608                "net.http2_session_set_local_window_size window size",
19609            )?;
19610            let session = http2_session_for_id(process, session_id)?;
19611            send_http2_command(&session, |respond_to| {
19612                Http2SessionCommand::SetLocalWindowSize {
19613                    size: window_size as u32,
19614                    respond_to,
19615                }
19616            })
19617        }
19618        "net.http2_session_goaway" => {
19619            let session_id = javascript_sync_rpc_arg_u64(
19620                &request.args,
19621                0,
19622                "net.http2_session_goaway session id",
19623            )?;
19624            let error_code = javascript_sync_rpc_arg_u64(
19625                &request.args,
19626                1,
19627                "net.http2_session_goaway error code",
19628            )?;
19629            let last_stream_id = javascript_sync_rpc_arg_u64(
19630                &request.args,
19631                2,
19632                "net.http2_session_goaway last stream id",
19633            )?;
19634            let opaque_data = request
19635                .args
19636                .get(3)
19637                .and_then(Value::as_str)
19638                .map(|value| {
19639                    base64::engine::general_purpose::STANDARD
19640                        .decode(value)
19641                        .map_err(|error| {
19642                            SidecarError::InvalidState(format!("invalid GOAWAY payload: {error}"))
19643                        })
19644                })
19645                .transpose()?;
19646            let session = http2_session_for_id(process, session_id)?;
19647            send_http2_command(&session, |respond_to| Http2SessionCommand::Goaway {
19648                error_code: error_code as u32,
19649                last_stream_id: last_stream_id as u32,
19650                opaque_data,
19651                respond_to,
19652            })
19653        }
19654        "net.http2_session_close" | "net.http2_session_destroy" => {
19655            let session_id = javascript_sync_rpc_arg_u64(
19656                &request.args,
19657                0,
19658                "net.http2_session_close session id",
19659            )?;
19660            let session = http2_session_for_id(process, session_id)?;
19661            send_http2_command(&session, |respond_to| Http2SessionCommand::Close {
19662                abrupt: request.method == "net.http2_session_destroy",
19663                respond_to,
19664            })
19665        }
19666        "net.http2_session_poll" => {
19667            let session_id =
19668                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_session_poll session id")?;
19669            let wait_ms = javascript_sync_rpc_arg_u64_optional(
19670                &request.args,
19671                1,
19672                "net.http2_session_poll wait ms",
19673            )?
19674            .unwrap_or_default();
19675            match wait_for_http2_event(&process.http2.shared, session_id, false, wait_ms) {
19676                Some(event) => http2_event_value(&event),
19677                None => Ok(Value::Null),
19678            }
19679        }
19680        "net.http2_session_wait" => {
19681            let session_id =
19682                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_session_wait session id")?;
19683            dispatch_http2_wait_loop(process, session_id, false)
19684        }
19685        "net.http2_stream_respond" => {
19686            let stream_id = javascript_sync_rpc_arg_u64(
19687                &request.args,
19688                0,
19689                "net.http2_stream_respond stream id",
19690            )?;
19691            let headers_json =
19692                javascript_sync_rpc_arg_str(&request.args, 1, "net.http2_stream_respond headers")?;
19693            let stream = http2_stream_for_id(process, stream_id)?;
19694            let session = http2_session_for_id(process, stream.session_id)?;
19695            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamRespond {
19696                stream_id,
19697                headers_json: headers_json.to_owned(),
19698                respond_to,
19699            })
19700        }
19701        "net.http2_stream_push_stream" => {
19702            let stream_id = javascript_sync_rpc_arg_u64(
19703                &request.args,
19704                0,
19705                "net.http2_stream_push_stream stream id",
19706            )?;
19707            let headers_json = javascript_sync_rpc_arg_str(
19708                &request.args,
19709                1,
19710                "net.http2_stream_push_stream headers",
19711            )?;
19712            let _options_json = javascript_sync_rpc_arg_str(
19713                &request.args,
19714                2,
19715                "net.http2_stream_push_stream options",
19716            )?;
19717            let stream = http2_stream_for_id(process, stream_id)?;
19718            let session = http2_session_for_id(process, stream.session_id)?;
19719            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamPush {
19720                stream_id,
19721                headers_json: headers_json.to_owned(),
19722                respond_to,
19723            })
19724        }
19725        "net.http2_stream_write" => {
19726            let stream_id =
19727                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_write stream id")?;
19728            let chunk =
19729                javascript_sync_rpc_base64_arg(&request.args, 1, "net.http2_stream_write data")?;
19730            let stream = http2_stream_for_id(process, stream_id)?;
19731            let session = http2_session_for_id(process, stream.session_id)?;
19732            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamWrite {
19733                stream_id,
19734                chunk,
19735                end_stream: false,
19736                respond_to,
19737            })
19738        }
19739        "net.http2_stream_end" => {
19740            let stream_id =
19741                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_end stream id")?;
19742            let chunk = request
19743                .args
19744                .get(1)
19745                .and_then(Value::as_str)
19746                .map(|value| {
19747                    base64::engine::general_purpose::STANDARD
19748                        .decode(value)
19749                        .map_err(|error| {
19750                            SidecarError::InvalidState(format!(
19751                                "invalid HTTP/2 stream payload: {error}"
19752                            ))
19753                        })
19754                })
19755                .transpose()?
19756                .unwrap_or_default();
19757            let stream = http2_stream_for_id(process, stream_id)?;
19758            let session = http2_session_for_id(process, stream.session_id)?;
19759            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamWrite {
19760                stream_id,
19761                chunk,
19762                end_stream: true,
19763                respond_to,
19764            })
19765        }
19766        "net.http2_stream_close" => {
19767            let stream_id =
19768                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_close stream id")?;
19769            let code = javascript_sync_rpc_arg_u64_optional(
19770                &request.args,
19771                1,
19772                "net.http2_stream_close error code",
19773            )?
19774            .map(|value| value as u32);
19775            let stream = http2_stream_for_id(process, stream_id)?;
19776            let session = http2_session_for_id(process, stream.session_id)?;
19777            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamClose {
19778                stream_id,
19779                error_code: code,
19780                respond_to,
19781            })
19782        }
19783        "net.http2_stream_pause" => {
19784            let stream_id =
19785                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_pause stream id")?;
19786            let stream = http2_stream_for_id(process, stream_id)?;
19787            stream.paused.store(true, Ordering::SeqCst);
19788            Ok(Value::Null)
19789        }
19790        "net.http2_stream_resume" => {
19791            let stream_id =
19792                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_resume stream id")?;
19793            let stream = http2_stream_for_id(process, stream_id)?;
19794            stream.paused.store(false, Ordering::SeqCst);
19795            Ok(Value::Null)
19796        }
19797        "net.http2_stream_respond_with_file" => {
19798            let stream_id = javascript_sync_rpc_arg_u64(
19799                &request.args,
19800                0,
19801                "net.http2_stream_respond_with_file stream id",
19802            )?;
19803            let path = javascript_sync_rpc_arg_str(
19804                &request.args,
19805                1,
19806                "net.http2_stream_respond_with_file path",
19807            )?;
19808            let headers_json = javascript_sync_rpc_arg_str(
19809                &request.args,
19810                2,
19811                "net.http2_stream_respond_with_file headers",
19812            )?;
19813            let options_json = javascript_sync_rpc_arg_str(
19814                &request.args,
19815                3,
19816                "net.http2_stream_respond_with_file options",
19817            )?;
19818            let stream = http2_stream_for_id(process, stream_id)?;
19819            let session = http2_session_for_id(process, stream.session_id)?;
19820            let guest_path = resolve_http2_file_response_guest_path(process, path);
19821            let body = kernel.read_file(&guest_path).map_err(kernel_error)?;
19822            send_http2_command(&session, |respond_to| {
19823                Http2SessionCommand::StreamRespondWithFile {
19824                    stream_id,
19825                    body,
19826                    headers_json: headers_json.to_owned(),
19827                    options_json: options_json.to_owned(),
19828                    respond_to,
19829                }
19830            })
19831        }
19832        other => Err(SidecarError::InvalidState(format!(
19833            "unsupported JavaScript HTTP/2 sync RPC method {other}"
19834        ))),
19835    }
19836}
19837
19838const JAVASCRIPT_NET_POLL_MAX_WAIT: Duration = Duration::from_millis(50);
19839const EXITED_PROCESS_SNAPSHOT_RETENTION: Duration = Duration::from_secs(2);
19840
19841fn resolve_http2_file_response_guest_path(process: &ActiveProcess, path: &str) -> String {
19842    if Path::new(path).is_absolute() {
19843        normalize_path(path)
19844    } else {
19845        normalize_path(&format!("{}/{}", process.guest_cwd, path))
19846    }
19847}
19848
19849pub(crate) fn clamp_javascript_net_poll_wait(wait_ms: u64) -> Duration {
19850    // WASM net.poll runs on the sidecar's sync-RPC main thread. Guest-controlled waits
19851    // must stay bounded so one VM cannot stall dispose/shutdown or unrelated VM work.
19852    if wait_ms == 0 {
19853        Duration::ZERO
19854    } else {
19855        Duration::from_millis(wait_ms).min(JAVASCRIPT_NET_POLL_MAX_WAIT)
19856    }
19857}
19858
19859pub(crate) fn service_javascript_net_sync_rpc<B>(
19860    request: JavascriptNetSyncRpcServiceRequest<'_, B>,
19861) -> Result<Value, SidecarError>
19862where
19863    B: NativeSidecarBridge + Send + 'static,
19864    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19865{
19866    let JavascriptNetSyncRpcServiceRequest {
19867        bridge,
19868        vm_id,
19869        dns,
19870        socket_paths,
19871        kernel,
19872        process,
19873        sync_request: request,
19874        resource_limits,
19875        network_counts,
19876    } = request;
19877    match request.method.as_str() {
19878        "net.http_listen" => {
19879            check_network_resource_limit(
19880                resource_limits.max_sockets,
19881                network_counts.sockets,
19882                1,
19883                "socket",
19884            )?;
19885            let payload_json =
19886                javascript_sync_rpc_arg_str(&request.args, 0, "net.http_listen payload")?;
19887            let payload: JavascriptHttpListenRequest =
19888                serde_json::from_str(payload_json).map_err(|error| {
19889                    SidecarError::InvalidState(format!(
19890                        "net.http_listen payload must be valid JSON: {error}"
19891                    ))
19892                })?;
19893            let (family, bind_host, guest_host) =
19894                normalize_tcp_listen_host(payload.hostname.as_deref())?;
19895            let requested_port = payload.port.unwrap_or(0);
19896            bridge.require_network_access(
19897                vm_id,
19898                NetworkOperation::Listen,
19899                format_tcp_resource(bind_host, requested_port),
19900            )?;
19901            let port = allocate_guest_listen_port(
19902                requested_port,
19903                family,
19904                &socket_paths.used_tcp_guest_ports,
19905                socket_paths.listen_policy,
19906            )?;
19907            let mut listener = ActiveTcpListener::bind(
19908                bind_host,
19909                guest_host,
19910                port,
19911                Some(DEFAULT_JAVASCRIPT_NET_BACKLOG),
19912            )?;
19913            let guest_local_addr = listener.guest_local_addr();
19914            process.http_servers.insert(
19915                payload.server_id,
19916                ActiveHttpServer {
19917                    listener: listener.listener.take().ok_or_else(|| {
19918                        SidecarError::InvalidState(String::from(
19919                            "HTTP listener missing host TCP socket",
19920                        ))
19921                    })?,
19922                    guest_local_addr,
19923                    next_request_id: 0,
19924                },
19925            );
19926            serde_json::to_string(&json!({
19927                "address": {
19928                    "address": guest_local_addr.ip().to_string(),
19929                    "family": socket_addr_family(&guest_local_addr),
19930                    "port": guest_local_addr.port(),
19931                }
19932            }))
19933            .map(Value::String)
19934            .map_err(|error| {
19935                SidecarError::Execution(format!("ERR_AGENT_OS_NODE_SYNC_RPC: {error}"))
19936            })
19937        }
19938        "net.http_close" => {
19939            let server_id =
19940                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_close server id")?;
19941            let server = process.http_servers.remove(&server_id).ok_or_else(|| {
19942                SidecarError::InvalidState(format!("unknown HTTP server {server_id}"))
19943            })?;
19944            drop(server.listener);
19945            process
19946                .pending_http_requests
19947                .retain(|(pending_server_id, _), _| *pending_server_id != server_id);
19948            Ok(Value::Null)
19949        }
19950        "net.http_wait" => {
19951            let server_id =
19952                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_wait server id")?;
19953            dispatch_http_wait_loop(process, server_id)
19954        }
19955        "net.http_respond" => {
19956            let server_id =
19957                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_respond server id")?;
19958            let request_id =
19959                javascript_sync_rpc_arg_u64(&request.args, 1, "net.http_respond request id")?;
19960            let response_json =
19961                javascript_sync_rpc_arg_str(&request.args, 2, "net.http_respond payload")?;
19962            ensure_vm_fetch_response_within_limit(
19963                response_json,
19964                "net.http_respond",
19965                VM_FETCH_BUFFER_LIMIT_BYTES,
19966            )?;
19967            serde_json::from_str::<Value>(response_json).map_err(|error| {
19968                SidecarError::Execution(format!(
19969                    "net.http_respond payload must be valid JSON: {error}"
19970                ))
19971            })?;
19972            let Some(pending) = process
19973                .pending_http_requests
19974                .get_mut(&(server_id, request_id))
19975            else {
19976                return Err(SidecarError::InvalidState(format!(
19977                    "unknown pending HTTP request {request_id} for server {server_id}"
19978                )));
19979            };
19980            *pending = Some(response_json.to_owned());
19981            Ok(Value::Null)
19982        }
19983        "net.reserve_tcp_port" => {
19984            let payload = request
19985                .args
19986                .first()
19987                .cloned()
19988                .ok_or_else(|| {
19989                    SidecarError::InvalidState(String::from(
19990                        "net.reserve_tcp_port requires a request payload",
19991                    ))
19992                })
19993                .and_then(|value| {
19994                    serde_json::from_value::<JavascriptNetReserveTcpPortRequest>(value).map_err(
19995                        |error| {
19996                            SidecarError::InvalidState(format!(
19997                                "invalid net.reserve_tcp_port payload: {error}"
19998                            ))
19999                        },
20000                    )
20001                })?;
20002            let (family, _bind_host, guest_host) =
20003                normalize_tcp_listen_host(payload.host.as_deref())?;
20004            let requested_port = payload.port.unwrap_or(0);
20005            let port = allocate_guest_listen_port(
20006                requested_port,
20007                family,
20008                &socket_paths.used_tcp_guest_ports,
20009                socket_paths.listen_policy,
20010            )?;
20011            let reservation_id = process.allocate_tcp_port_reservation_id();
20012            process
20013                .tcp_port_reservations
20014                .insert(reservation_id.clone(), (family, port));
20015            Ok(json!({
20016                "reservationId": reservation_id,
20017                "localAddress": guest_host,
20018                "localPort": port,
20019                "family": match family {
20020                    JavascriptSocketFamily::Ipv4 => "IPv4",
20021                    JavascriptSocketFamily::Ipv6 => "IPv6",
20022                },
20023            }))
20024        }
20025        "net.release_tcp_port" => {
20026            let reservation_id =
20027                javascript_sync_rpc_arg_str(&request.args, 0, "net.release_tcp_port reservation")?;
20028            process.tcp_port_reservations.remove(reservation_id);
20029            Ok(Value::Null)
20030        }
20031        "net.connect" => {
20032            check_network_resource_limit(
20033                resource_limits.max_sockets,
20034                network_counts.sockets,
20035                1,
20036                "socket",
20037            )?;
20038            check_network_resource_limit(
20039                resource_limits.max_connections,
20040                network_counts.connections,
20041                1,
20042                "connection",
20043            )?;
20044            let payload = request
20045                .args
20046                .first()
20047                .cloned()
20048                .ok_or_else(|| {
20049                    SidecarError::InvalidState(String::from(
20050                        "net.connect requires a request payload",
20051                    ))
20052                })
20053                .and_then(|value| {
20054                    serde_json::from_value::<JavascriptNetConnectRequest>(value).map_err(|error| {
20055                        SidecarError::InvalidState(format!("invalid net.connect payload: {error}"))
20056                    })
20057                })?;
20058            if let Some(path) = payload.path.as_deref() {
20059                let guest_path = normalize_path(path);
20060                let host_path = resolve_guest_socket_host_path(socket_paths, &guest_path);
20061                let socket = ActiveUnixSocket::connect(&host_path, &guest_path)?;
20062                let socket_id = process.allocate_unix_socket_id();
20063                process.unix_sockets.insert(socket_id.clone(), socket);
20064                Ok(json!({
20065                    "socketId": socket_id,
20066                    "remotePath": guest_path,
20067                }))
20068            } else {
20069                let port = payload.port.ok_or_else(|| {
20070                    SidecarError::InvalidState(String::from(
20071                        "net.connect requires either a path or port",
20072                    ))
20073                })?;
20074                let host = payload.host.as_deref().unwrap_or("localhost");
20075                let local_reservation = payload.local_reservation.as_deref().and_then(|id| {
20076                    process
20077                        .tcp_port_reservations
20078                        .remove(id)
20079                        .map(|reservation| (id.to_owned(), reservation))
20080                });
20081                bridge.require_network_access(
20082                    vm_id,
20083                    NetworkOperation::Http,
20084                    format_tcp_resource(host, port),
20085                )?;
20086                if is_loopback_socket_host(host) {
20087                    let families = [JavascriptSocketFamily::Ipv4, JavascriptSocketFamily::Ipv6];
20088                    if let Some((family, target)) = families.iter().find_map(|family| {
20089                        socket_paths
20090                            .http_loopback_target(*family, port)
20091                            .map(|target| (*family, target))
20092                    }) {
20093                        if let Some((reservation_id, reservation)) = local_reservation {
20094                            process
20095                                .tcp_port_reservations
20096                                .insert(reservation_id, reservation);
20097                        }
20098                        let remote_address = match family {
20099                            JavascriptSocketFamily::Ipv4 => "127.0.0.1",
20100                            JavascriptSocketFamily::Ipv6 => "::1",
20101                        };
20102                        return Ok(json!({
20103                            "loopbackHttpTarget": {
20104                                "processId": target.process_id.clone(),
20105                                "serverId": target.server_id,
20106                                "host": remote_address,
20107                                "port": port,
20108                            },
20109                            "localAddress": match family {
20110                                JavascriptSocketFamily::Ipv4 => "127.0.0.1",
20111                                JavascriptSocketFamily::Ipv6 => "::1",
20112                            },
20113                            "localPort": payload.local_port.unwrap_or(0),
20114                            "remoteAddress": remote_address,
20115                            "remotePort": port,
20116                            "remoteFamily": match family {
20117                                JavascriptSocketFamily::Ipv4 => "IPv4",
20118                                JavascriptSocketFamily::Ipv6 => "IPv6",
20119                            },
20120                        }));
20121                    }
20122                }
20123                let connect_result = ActiveTcpSocket::connect(ActiveTcpConnectRequest {
20124                    bridge,
20125                    kernel,
20126                    kernel_pid: process.kernel_pid,
20127                    vm_id,
20128                    dns,
20129                    host,
20130                    port,
20131                    local_address: payload.local_address.as_deref(),
20132                    local_port: payload.local_port,
20133                    local_reservation: local_reservation
20134                        .as_ref()
20135                        .map(|(_, reservation)| *reservation),
20136                    context: socket_paths,
20137                });
20138                if let Err(error) = connect_result {
20139                    if let Some((reservation_id, reservation)) = local_reservation {
20140                        process
20141                            .tcp_port_reservations
20142                            .insert(reservation_id, reservation);
20143                    }
20144                    return Err(error);
20145                }
20146                let socket = connect_result?;
20147                let socket_id = process.allocate_tcp_socket_id();
20148                let local_addr = socket.guest_local_addr;
20149                let remote_addr = socket.guest_remote_addr;
20150                process.tcp_sockets.insert(socket_id.clone(), socket);
20151                Ok(json!({
20152                    "socketId": socket_id,
20153                    "localAddress": local_addr.ip().to_string(),
20154                    "localPort": local_addr.port(),
20155                    "remoteAddress": remote_addr.ip().to_string(),
20156                    "remotePort": remote_addr.port(),
20157                    "remoteFamily": socket_addr_family(&remote_addr),
20158                }))
20159            }
20160        }
20161        "net.listen" => {
20162            check_network_resource_limit(
20163                resource_limits.max_sockets,
20164                network_counts.sockets,
20165                1,
20166                "socket",
20167            )?;
20168            let payload = request
20169                .args
20170                .first()
20171                .cloned()
20172                .ok_or_else(|| {
20173                    SidecarError::InvalidState(String::from(
20174                        "net.listen requires a request payload",
20175                    ))
20176                })
20177                .and_then(|value| match value {
20178                    Value::String(json) => {
20179                        serde_json::from_str::<JavascriptNetListenRequest>(&json).map_err(|error| {
20180                            SidecarError::InvalidState(format!(
20181                                "invalid net.listen payload: {error}"
20182                            ))
20183                        })
20184                    }
20185                    other => serde_json::from_value::<JavascriptNetListenRequest>(other).map_err(
20186                        |error| {
20187                            SidecarError::InvalidState(format!(
20188                                "invalid net.listen payload: {error}"
20189                            ))
20190                        },
20191                    ),
20192                })?;
20193            if let Some(path) = payload.path.as_deref() {
20194                let guest_path = normalize_path(path);
20195                if kernel.exists(&guest_path).map_err(kernel_error)? {
20196                    return Err(sidecar_net_error(std::io::Error::from_raw_os_error(
20197                        libc::EADDRINUSE,
20198                    )));
20199                }
20200
20201                let host_path = resolve_guest_socket_host_path(socket_paths, &guest_path);
20202                let on_host_mount =
20203                    host_mount_path_for_guest_path_from_mounts(&socket_paths.mounts, &guest_path)
20204                        .is_some();
20205                let listener = ActiveUnixListener::bind(&host_path, &guest_path, payload.backlog)?;
20206                if !on_host_mount {
20207                    ensure_kernel_parent_directories(kernel, &guest_path)?;
20208                    kernel
20209                        .write_file(&guest_path, Vec::new())
20210                        .map_err(kernel_error)?;
20211                }
20212                let listener_id = process.allocate_unix_listener_id();
20213                process.unix_listeners.insert(listener_id.clone(), listener);
20214                Ok(json!({
20215                    "serverId": listener_id,
20216                    "path": guest_path,
20217                }))
20218            } else {
20219                let (family, bind_host, guest_host) =
20220                    normalize_tcp_listen_host(payload.host.as_deref())?;
20221                let requested_port = payload.port.unwrap_or(0);
20222                bridge.require_network_access(
20223                    vm_id,
20224                    NetworkOperation::Listen,
20225                    format_tcp_resource(bind_host, requested_port),
20226                )?;
20227                let local_reservation = payload.local_reservation.as_deref().and_then(|id| {
20228                    process
20229                        .tcp_port_reservations
20230                        .remove(id)
20231                        .map(|reservation| (id.to_owned(), reservation))
20232                });
20233                let port = if requested_port != 0
20234                    && local_reservation
20235                        .as_ref()
20236                        .map(|(_, reservation)| *reservation)
20237                        == Some((family, requested_port))
20238                {
20239                    requested_port
20240                } else {
20241                    allocate_guest_listen_port(
20242                        requested_port,
20243                        family,
20244                        &socket_paths.used_tcp_guest_ports,
20245                        socket_paths.listen_policy,
20246                    )?
20247                };
20248                let listener_result = ActiveTcpListener::bind_kernel(
20249                    kernel,
20250                    process.kernel_pid,
20251                    guest_host,
20252                    port,
20253                    payload.backlog,
20254                );
20255                if let Err(error) = listener_result {
20256                    if let Some((reservation_id, reservation)) = local_reservation {
20257                        process
20258                            .tcp_port_reservations
20259                            .insert(reservation_id, reservation);
20260                    }
20261                    return Err(error);
20262                }
20263                let listener = listener_result?;
20264                let listener_id = process.allocate_tcp_listener_id();
20265                let local_addr = listener.guest_local_addr();
20266                process.tcp_listeners.insert(listener_id.clone(), listener);
20267                Ok(json!({
20268                    "serverId": listener_id,
20269                    "localAddress": local_addr.ip().to_string(),
20270                    "localPort": local_addr.port(),
20271                    "family": socket_addr_family(&local_addr),
20272                }))
20273            }
20274        }
20275        "net.poll" => {
20276            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.poll socket id")?;
20277            let wait_ms =
20278                javascript_sync_rpc_arg_u64_optional(&request.args, 1, "net.poll wait ms")?
20279                    .unwrap_or_default();
20280            let wait = clamp_javascript_net_poll_wait(wait_ms);
20281            let event = if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20282                socket.poll(kernel, process.kernel_pid, wait)?
20283            } else if let Some(socket) = process.unix_sockets.get_mut(socket_id) {
20284                socket.poll(wait)?
20285            } else {
20286                return Err(SidecarError::InvalidState(format!(
20287                    "unknown net socket {socket_id}"
20288                )));
20289            };
20290
20291            match event {
20292                Some(JavascriptTcpSocketEvent::Data(chunk)) => Ok(json!({
20293                    "type": "data",
20294                    "data": javascript_sync_rpc_bytes_value(&chunk),
20295                })),
20296                Some(JavascriptTcpSocketEvent::End) => Ok(json!({
20297                    "type": "end",
20298                })),
20299                Some(JavascriptTcpSocketEvent::Error { code, message }) => Ok(json!({
20300                    "type": "error",
20301                    "code": code,
20302                    "message": message,
20303                })),
20304                Some(JavascriptTcpSocketEvent::Close { had_error }) => {
20305                    if let Some(socket) = process.tcp_sockets.remove(socket_id) {
20306                        if let Some(listener_id) = socket.listener_id.as_deref() {
20307                            if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20308                                listener.release_connection(socket_id);
20309                            }
20310                        }
20311                    } else if let Some(socket) = process.unix_sockets.remove(socket_id) {
20312                        if let Some(listener_id) = socket.listener_id.as_deref() {
20313                            if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20314                                listener.release_connection(socket_id);
20315                            }
20316                        }
20317                    }
20318                    Ok(json!({
20319                        "type": "close",
20320                        "hadError": had_error,
20321                    }))
20322                }
20323                None => Ok(Value::Null),
20324            }
20325        }
20326        "net.socket_wait_connect" => {
20327            let socket_id =
20328                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_wait_connect socket id")?;
20329            if let Some(socket) = process.tcp_sockets.get(socket_id) {
20330                javascript_net_json_string(socket.socket_info(), "net.socket_wait_connect")
20331            } else {
20332                let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
20333                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20334                })?;
20335                javascript_net_json_string(socket.socket_info(), "net.socket_wait_connect")
20336            }
20337        }
20338        "net.socket_read" => {
20339            let socket_id =
20340                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_read socket id")?;
20341            if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20342                javascript_net_read_value(socket.poll(
20343                    kernel,
20344                    process.kernel_pid,
20345                    Duration::ZERO,
20346                )?)
20347            } else {
20348                let socket = process.unix_sockets.get_mut(socket_id).ok_or_else(|| {
20349                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20350                })?;
20351                javascript_net_read_value(socket.poll(Duration::ZERO)?)
20352            }
20353        }
20354        "net.socket_set_no_delay" => {
20355            let socket_id =
20356                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_set_no_delay socket id")?;
20357            let enable =
20358                javascript_sync_rpc_arg_bool(&request.args, 1, "net.socket_set_no_delay enabled")?;
20359            if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20360                socket.set_no_delay(enable)?;
20361            } else if !process.unix_sockets.contains_key(socket_id) {
20362                return Err(SidecarError::InvalidState(format!(
20363                    "unknown net socket {socket_id}"
20364                )));
20365            }
20366            Ok(Value::Null)
20367        }
20368        "net.socket_set_keep_alive" => {
20369            let socket_id = javascript_sync_rpc_arg_str(
20370                &request.args,
20371                0,
20372                "net.socket_set_keep_alive socket id",
20373            )?;
20374            let enable = javascript_sync_rpc_arg_bool(
20375                &request.args,
20376                1,
20377                "net.socket_set_keep_alive enabled",
20378            )?;
20379            let initial_delay_secs = javascript_sync_rpc_arg_u64_optional(
20380                &request.args,
20381                2,
20382                "net.socket_set_keep_alive initial delay seconds",
20383            )?;
20384            if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20385                socket.set_keep_alive(enable, initial_delay_secs)?;
20386            } else if !process.unix_sockets.contains_key(socket_id) {
20387                return Err(SidecarError::InvalidState(format!(
20388                    "unknown net socket {socket_id}"
20389                )));
20390            }
20391            Ok(Value::Null)
20392        }
20393        "net.socket_upgrade_tls" => {
20394            let socket_id =
20395                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_upgrade_tls socket id")?;
20396            let options_json =
20397                javascript_sync_rpc_arg_str(&request.args, 1, "net.socket_upgrade_tls options")?;
20398            let options: JavascriptTlsBridgeOptions =
20399                serde_json::from_str(options_json).map_err(|error| {
20400                    SidecarError::InvalidState(format!(
20401                        "net.socket_upgrade_tls options must be valid JSON: {error}"
20402                    ))
20403                })?;
20404            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20405                SidecarError::InvalidState(format!(
20406                    "unknown TCP socket {socket_id} for TLS upgrade"
20407                ))
20408            })?;
20409            socket.upgrade_tls(vm_id, kernel, options)?;
20410            Ok(Value::Null)
20411        }
20412        "net.socket_get_tls_client_hello" => {
20413            let socket_id = javascript_sync_rpc_arg_str(
20414                &request.args,
20415                0,
20416                "net.socket_get_tls_client_hello socket id",
20417            )?;
20418            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20419                SidecarError::InvalidState(format!(
20420                    "unknown TCP socket {socket_id} for TLS client hello query"
20421                ))
20422            })?;
20423            socket.tls_client_hello_json(vm_id, kernel)
20424        }
20425        "net.socket_tls_query" => {
20426            let socket_id =
20427                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_tls_query socket id")?;
20428            let query =
20429                javascript_sync_rpc_arg_str(&request.args, 1, "net.socket_tls_query query")?;
20430            let detailed = request
20431                .args
20432                .get(2)
20433                .and_then(Value::as_bool)
20434                .unwrap_or(false);
20435            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20436                SidecarError::InvalidState(format!("unknown TCP socket {socket_id} for TLS query"))
20437            })?;
20438            socket.tls_query(query, detailed)
20439        }
20440        "net.server_poll" => {
20441            let listener_id =
20442                javascript_sync_rpc_arg_str(&request.args, 0, "net.server_poll listener id")?;
20443            let wait_ms =
20444                javascript_sync_rpc_arg_u64_optional(&request.args, 1, "net.server_poll wait ms")?
20445                    .unwrap_or_default();
20446            let tcp_event = if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20447                Some(listener.poll(kernel, process.kernel_pid, Duration::from_millis(wait_ms))?)
20448            } else {
20449                None
20450            };
20451
20452            if let Some(event) = tcp_event {
20453                return match event {
20454                    Some(JavascriptTcpListenerEvent::Connection(pending)) => {
20455                        let PendingTcpSocket {
20456                            stream,
20457                            kernel_socket_id,
20458                            preallocated,
20459                            guest_local_addr,
20460                            guest_remote_addr,
20461                        } = pending;
20462                        if !preallocated {
20463                            if let Err(error) = check_network_resource_limit(
20464                                resource_limits.max_sockets,
20465                                network_counts.sockets,
20466                                1,
20467                                "socket",
20468                            )
20469                            .and_then(|()| {
20470                                check_network_resource_limit(
20471                                    resource_limits.max_connections,
20472                                    network_counts.connections,
20473                                    1,
20474                                    "connection",
20475                                )
20476                            }) {
20477                                if let Some(stream) = stream {
20478                                    let _ = stream.shutdown(Shutdown::Both);
20479                                }
20480                                return Ok(json!({
20481                                    "type": "error",
20482                                    "code": "EAGAIN",
20483                                    "message": error.to_string(),
20484                                }));
20485                            }
20486                        }
20487                        let socket = if let Some(stream) = stream {
20488                            ActiveTcpSocket::from_stream(
20489                                stream,
20490                                Some(listener_id.to_string()),
20491                                guest_local_addr,
20492                                guest_remote_addr,
20493                            )?
20494                        } else {
20495                            ActiveTcpSocket::from_kernel(
20496                                kernel_socket_id.ok_or_else(|| {
20497                                    SidecarError::InvalidState(String::from(
20498                                        "kernel TCP accept missing socket id",
20499                                    ))
20500                                })?,
20501                                Some(listener_id.to_string()),
20502                                guest_local_addr,
20503                                guest_remote_addr,
20504                            )
20505                        };
20506                        let socket_id = process.allocate_tcp_socket_id();
20507                        if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20508                            listener.register_connection(&socket_id);
20509                        }
20510                        process.tcp_sockets.insert(socket_id.clone(), socket);
20511                        Ok(json!({
20512                            "type": "connection",
20513                            "socketId": socket_id,
20514                            "localAddress": guest_local_addr.ip().to_string(),
20515                            "localPort": guest_local_addr.port(),
20516                            "remoteAddress": guest_remote_addr.ip().to_string(),
20517                            "remotePort": guest_remote_addr.port(),
20518                            "remoteFamily": socket_addr_family(&guest_remote_addr),
20519                        }))
20520                    }
20521                    Some(JavascriptTcpListenerEvent::Error { code, message }) => Ok(json!({
20522                        "type": "error",
20523                        "code": code,
20524                        "message": message,
20525                    })),
20526                    None => Ok(Value::Null),
20527                };
20528            }
20529
20530            let event = {
20531                let listener = process.unix_listeners.get_mut(listener_id).ok_or_else(|| {
20532                    SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20533                })?;
20534                listener.poll(Duration::from_millis(wait_ms))?
20535            };
20536
20537            match event {
20538                Some(JavascriptUnixListenerEvent::Connection(pending)) => {
20539                    if let Err(error) = check_network_resource_limit(
20540                        resource_limits.max_sockets,
20541                        network_counts.sockets,
20542                        1,
20543                        "socket",
20544                    )
20545                    .and_then(|()| {
20546                        check_network_resource_limit(
20547                            resource_limits.max_connections,
20548                            network_counts.connections,
20549                            1,
20550                            "connection",
20551                        )
20552                    }) {
20553                        let _ = pending.stream.shutdown(Shutdown::Both);
20554                        return Ok(json!({
20555                            "type": "error",
20556                            "code": "EAGAIN",
20557                            "message": error.to_string(),
20558                        }));
20559                    }
20560                    let socket = ActiveUnixSocket::from_stream(
20561                        pending.stream,
20562                        Some(listener_id.to_string()),
20563                        pending.local_path.clone(),
20564                        pending.remote_path.clone(),
20565                    )?;
20566                    let socket_id = process.allocate_unix_socket_id();
20567                    if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20568                        listener.register_connection(&socket_id);
20569                    }
20570                    process.unix_sockets.insert(socket_id.clone(), socket);
20571                    Ok(json!({
20572                        "type": "connection",
20573                        "socketId": socket_id,
20574                        "localPath": pending.local_path,
20575                        "remotePath": pending.remote_path,
20576                    }))
20577                }
20578                Some(JavascriptUnixListenerEvent::Error { code, message }) => Ok(json!({
20579                    "type": "error",
20580                    "code": code,
20581                    "message": message,
20582                })),
20583                None => Ok(Value::Null),
20584            }
20585        }
20586        "net.server_accept" => {
20587            let listener_id =
20588                javascript_sync_rpc_arg_str(&request.args, 0, "net.server_accept listener id")?;
20589            if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20590                return match listener.poll(kernel, process.kernel_pid, Duration::ZERO)? {
20591                    Some(JavascriptTcpListenerEvent::Connection(pending)) => {
20592                        let PendingTcpSocket {
20593                            stream,
20594                            kernel_socket_id,
20595                            preallocated,
20596                            guest_local_addr,
20597                            guest_remote_addr,
20598                        } = pending;
20599                        if !preallocated {
20600                            check_network_resource_limit(
20601                                resource_limits.max_sockets,
20602                                network_counts.sockets,
20603                                1,
20604                                "socket",
20605                            )?;
20606                            check_network_resource_limit(
20607                                resource_limits.max_connections,
20608                                network_counts.connections,
20609                                1,
20610                                "connection",
20611                            )?;
20612                        }
20613                        let info = json!({
20614                            "localAddress": guest_local_addr.ip().to_string(),
20615                            "localPort": guest_local_addr.port(),
20616                            "localFamily": socket_addr_family(&guest_local_addr),
20617                            "remoteAddress": guest_remote_addr.ip().to_string(),
20618                            "remotePort": guest_remote_addr.port(),
20619                            "remoteFamily": socket_addr_family(&guest_remote_addr),
20620                        });
20621                        let socket = if let Some(stream) = stream {
20622                            ActiveTcpSocket::from_stream(
20623                                stream,
20624                                Some(listener_id.to_string()),
20625                                guest_local_addr,
20626                                guest_remote_addr,
20627                            )?
20628                        } else {
20629                            ActiveTcpSocket::from_kernel(
20630                                kernel_socket_id.ok_or_else(|| {
20631                                    SidecarError::InvalidState(String::from(
20632                                        "kernel TCP accept missing socket id",
20633                                    ))
20634                                })?,
20635                                Some(listener_id.to_string()),
20636                                guest_local_addr,
20637                                guest_remote_addr,
20638                            )
20639                        };
20640                        let socket_id = process.allocate_tcp_socket_id();
20641                        if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20642                            listener.register_connection(&socket_id);
20643                        }
20644                        process.tcp_sockets.insert(socket_id.clone(), socket);
20645                        javascript_net_json_string(
20646                            json!({
20647                                "socketId": socket_id,
20648                                "info": info,
20649                            }),
20650                            "net.server_accept",
20651                        )
20652                    }
20653                    Some(JavascriptTcpListenerEvent::Error { code, message }) => {
20654                        let detail = code.unwrap_or_else(|| String::from("server accept"));
20655                        Err(SidecarError::Execution(format!("{detail}: {message}")))
20656                    }
20657                    None => Ok(javascript_net_timeout_value()),
20658                };
20659            }
20660
20661            let listener = process.unix_listeners.get_mut(listener_id).ok_or_else(|| {
20662                SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20663            })?;
20664            match listener.poll(Duration::ZERO)? {
20665                Some(JavascriptUnixListenerEvent::Connection(pending)) => {
20666                    check_network_resource_limit(
20667                        resource_limits.max_sockets,
20668                        network_counts.sockets,
20669                        1,
20670                        "socket",
20671                    )?;
20672                    check_network_resource_limit(
20673                        resource_limits.max_connections,
20674                        network_counts.connections,
20675                        1,
20676                        "connection",
20677                    )?;
20678                    let info = json!({
20679                        "localPath": pending.local_path.clone(),
20680                        "remotePath": pending.remote_path.clone(),
20681                    });
20682                    let socket = ActiveUnixSocket::from_stream(
20683                        pending.stream,
20684                        Some(listener_id.to_string()),
20685                        pending.local_path,
20686                        pending.remote_path,
20687                    )?;
20688                    let socket_id = process.allocate_unix_socket_id();
20689                    if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20690                        listener.register_connection(&socket_id);
20691                    }
20692                    process.unix_sockets.insert(socket_id.clone(), socket);
20693                    javascript_net_json_string(
20694                        json!({
20695                            "socketId": socket_id,
20696                            "info": info,
20697                        }),
20698                        "net.server_accept",
20699                    )
20700                }
20701                Some(JavascriptUnixListenerEvent::Error { code, message }) => {
20702                    let detail = code.unwrap_or_else(|| String::from("server accept"));
20703                    Err(SidecarError::Execution(format!("{detail}: {message}")))
20704                }
20705                None => Ok(javascript_net_timeout_value()),
20706            }
20707        }
20708        "net.server_connections" => {
20709            let listener_id = javascript_sync_rpc_arg_str(
20710                &request.args,
20711                0,
20712                "net.server_connections listener id",
20713            )?;
20714            if let Some(listener) = process.tcp_listeners.get(listener_id) {
20715                Ok(json!(listener.active_connection_count()))
20716            } else {
20717                let listener = process.unix_listeners.get(listener_id).ok_or_else(|| {
20718                    SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20719                })?;
20720                Ok(json!(listener.active_connection_count()))
20721            }
20722        }
20723        "net.upgrade_socket_write" => {
20724            let socket_id = javascript_sync_rpc_arg_str(
20725                &request.args,
20726                0,
20727                "net.upgrade_socket_write socket id",
20728            )?;
20729            let chunk =
20730                javascript_sync_rpc_base64_arg(&request.args, 1, "net.upgrade_socket_write chunk")?;
20731            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20732                SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
20733            })?;
20734            socket
20735                .write_all(kernel, process.kernel_pid, &chunk)
20736                .map(|written| json!(written))
20737        }
20738        "net.upgrade_socket_end" => {
20739            let socket_id =
20740                javascript_sync_rpc_arg_str(&request.args, 0, "net.upgrade_socket_end socket id")?;
20741            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20742                SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
20743            })?;
20744            socket.shutdown_write(kernel, process.kernel_pid)?;
20745            Ok(Value::Null)
20746        }
20747        "net.upgrade_socket_destroy" => {
20748            let socket_id = javascript_sync_rpc_arg_str(
20749                &request.args,
20750                0,
20751                "net.upgrade_socket_destroy socket id",
20752            )?;
20753            let socket = process.tcp_sockets.remove(socket_id).ok_or_else(|| {
20754                SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
20755            })?;
20756            if let Some(listener_id) = socket.listener_id.as_deref() {
20757                if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20758                    listener.release_connection(socket_id);
20759                }
20760            }
20761            let _ = socket.close(kernel, process.kernel_pid);
20762            Ok(Value::Null)
20763        }
20764        "net.write" => {
20765            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.write socket id")?;
20766            let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "net.write chunk")?;
20767            if let Some(socket) = process.tcp_sockets.get(socket_id) {
20768                socket
20769                    .write_all(kernel, process.kernel_pid, &chunk)
20770                    .map(|written| json!(written))
20771            } else {
20772                let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
20773                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20774                })?;
20775                socket.write_all(&chunk).map(|written| json!(written))
20776            }
20777        }
20778        "net.shutdown" => {
20779            let socket_id =
20780                javascript_sync_rpc_arg_str(&request.args, 0, "net.shutdown socket id")?;
20781            if let Some(socket) = process.tcp_sockets.get(socket_id) {
20782                socket.shutdown_write(kernel, process.kernel_pid)?;
20783            } else {
20784                let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
20785                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20786                })?;
20787                socket.shutdown_write()?;
20788            }
20789            Ok(Value::Null)
20790        }
20791        "net.destroy" => {
20792            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.destroy socket id")?;
20793            if let Some(socket) = process.tcp_sockets.remove(socket_id) {
20794                if let Some(listener_id) = socket.listener_id.as_deref() {
20795                    if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20796                        listener.release_connection(socket_id);
20797                    }
20798                }
20799                let _ = socket.close(kernel, process.kernel_pid);
20800                Ok(Value::Null)
20801            } else {
20802                let socket = process.unix_sockets.remove(socket_id).ok_or_else(|| {
20803                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20804                })?;
20805                if let Some(listener_id) = socket.listener_id.as_deref() {
20806                    if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20807                        listener.release_connection(socket_id);
20808                    }
20809                }
20810                let _ = socket.close();
20811                Ok(Value::Null)
20812            }
20813        }
20814        "net.server_close" => {
20815            let listener_id =
20816                javascript_sync_rpc_arg_str(&request.args, 0, "net.server_close listener id")?;
20817            if let Some(listener) = process.tcp_listeners.remove(listener_id) {
20818                listener.close(kernel, process.kernel_pid)?;
20819                Ok(Value::Null)
20820            } else {
20821                let listener = process.unix_listeners.remove(listener_id).ok_or_else(|| {
20822                    SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20823                })?;
20824                listener.close()?;
20825                Ok(Value::Null)
20826            }
20827        }
20828        "tls.get_ciphers" => javascript_net_json_string(
20829            Value::Array(
20830                tls_provider()
20831                    .cipher_suites
20832                    .iter()
20833                    .filter_map(|suite| {
20834                        suite
20835                            .suite()
20836                            .as_str()
20837                            .map(|value| Value::String(value.to_owned()))
20838                    })
20839                    .collect(),
20840            ),
20841            "tls.get_ciphers",
20842        ),
20843        _ => Err(SidecarError::InvalidState(format!(
20844            "unsupported JavaScript net sync RPC method {}",
20845            request.method
20846        ))),
20847    }
20848}
20849
20850fn signal_name_for_stream_event(signal: i32) -> Option<&'static str> {
20851    match signal {
20852        libc::SIGHUP => Some("SIGHUP"),
20853        libc::SIGINT => Some("SIGINT"),
20854        libc::SIGUSR1 => Some("SIGUSR1"),
20855        libc::SIGALRM => Some("SIGALRM"),
20856        libc::SIGCONT => Some("SIGCONT"),
20857        libc::SIGTERM => Some("SIGTERM"),
20858        libc::SIGCHLD => Some("SIGCHLD"),
20859        libc::SIGWINCH => Some("SIGWINCH"),
20860        _ => None,
20861    }
20862}
20863
20864pub(crate) fn canonical_signal_name(signal: i32) -> Option<&'static str> {
20865    match signal {
20866        1 => Some("SIGHUP"),
20867        2 => Some("SIGINT"),
20868        3 => Some("SIGQUIT"),
20869        4 => Some("SIGILL"),
20870        5 => Some("SIGTRAP"),
20871        6 => Some("SIGABRT"),
20872        7 => Some("SIGBUS"),
20873        8 => Some("SIGFPE"),
20874        9 => Some("SIGKILL"),
20875        10 => Some("SIGUSR1"),
20876        11 => Some("SIGSEGV"),
20877        12 => Some("SIGUSR2"),
20878        13 => Some("SIGPIPE"),
20879        14 => Some("SIGALRM"),
20880        15 => Some("SIGTERM"),
20881        17 => Some("SIGCHLD"),
20882        18 => Some("SIGCONT"),
20883        19 => Some("SIGSTOP"),
20884        20 => Some("SIGTSTP"),
20885        21 => Some("SIGTTIN"),
20886        22 => Some("SIGTTOU"),
20887        23 => Some("SIGURG"),
20888        24 => Some("SIGXCPU"),
20889        25 => Some("SIGXFSZ"),
20890        26 => Some("SIGVTALRM"),
20891        27 => Some("SIGPROF"),
20892        28 => Some("SIGWINCH"),
20893        29 => Some("SIGIO"),
20894        30 => Some("SIGPWR"),
20895        31 => Some("SIGSYS"),
20896        _ => None,
20897    }
20898}
20899
20900fn dispatch_v8_process_signal(process: &ActiveProcess, signal: i32) -> Result<bool, SidecarError> {
20901    let Some(signal_name) = signal_name_for_stream_event(signal) else {
20902        return Ok(false);
20903    };
20904    process.execution.send_javascript_stream_event(
20905        "signal",
20906        json!({
20907            "signal": signal_name,
20908            "number": signal,
20909            "action": "default",
20910        }),
20911    )?;
20912    Ok(true)
20913}
20914
20915fn dispatch_v8_session_signal_async(session: V8SessionHandle, signal: i32) {
20916    let Some(signal_name) = signal_name_for_stream_event(signal).map(str::to_owned) else {
20917        return;
20918    };
20919    thread::spawn(move || {
20920        thread::sleep(Duration::from_millis(1));
20921        let payload = v8_runtime::json_to_cbor_payload(&json!({
20922            "signal": signal_name,
20923            "number": signal,
20924            "action": "default",
20925        }))
20926        .unwrap_or_default();
20927        let _ = session.send_stream_event("signal", payload);
20928    });
20929}
20930
20931pub(crate) fn parse_signal(signal: &str) -> Result<i32, SidecarError> {
20932    let trimmed = signal.trim();
20933    if trimmed.is_empty() {
20934        return Err(SidecarError::InvalidState(String::from(
20935            "kill_process requires a non-empty signal",
20936        )));
20937    }
20938
20939    if let Ok(value) = trimmed.parse::<i32>() {
20940        return match value {
20941            0..=31 => Ok(value),
20942            _ => Err(SidecarError::InvalidState(format!(
20943                "unsupported kill_process signal {signal}"
20944            ))),
20945        };
20946    }
20947
20948    let upper = trimmed.to_ascii_uppercase();
20949    let normalized = upper.strip_prefix("SIG").unwrap_or(&upper);
20950
20951    signal_number_from_name(normalized).ok_or_else(|| {
20952        SidecarError::InvalidState(format!("unsupported kill_process signal {signal}"))
20953    })
20954}
20955
20956fn signal_number_from_name(signal: &str) -> Option<i32> {
20957    match signal {
20958        "0" => Some(0),
20959        "HUP" => Some(1),
20960        "INT" => Some(2),
20961        "QUIT" => Some(3),
20962        "ILL" => Some(4),
20963        "TRAP" => Some(5),
20964        "ABRT" | "IOT" => Some(6),
20965        "BUS" => Some(7),
20966        "FPE" => Some(8),
20967        "KILL" => Some(9),
20968        "USR1" => Some(10),
20969        "SEGV" => Some(11),
20970        "USR2" => Some(12),
20971        "PIPE" => Some(13),
20972        "ALRM" => Some(14),
20973        "TERM" => Some(15),
20974        "STKFLT" => Some(16),
20975        "CHLD" => Some(17),
20976        "CONT" => Some(18),
20977        "STOP" => Some(19),
20978        "TSTP" => Some(20),
20979        "TTIN" => Some(21),
20980        "TTOU" => Some(22),
20981        "URG" => Some(23),
20982        "XCPU" => Some(24),
20983        "XFSZ" => Some(25),
20984        "VTALRM" => Some(26),
20985        "PROF" => Some(27),
20986        "WINCH" => Some(28),
20987        "IO" | "POLL" => Some(29),
20988        "PWR" => Some(30),
20989        "SYS" => Some(31),
20990        _ => None,
20991    }
20992}
20993
20994pub(crate) fn runtime_child_is_alive(child_pid: u32) -> Result<bool, SidecarError> {
20995    Ok(runtime_child_exit_status(child_pid)?.is_none())
20996}
20997
20998#[cfg(not(target_os = "macos"))]
20999fn runtime_child_exit_status(child_pid: u32) -> Result<Option<i32>, SidecarError> {
21000    if child_pid == 0 {
21001        return Ok(Some(0));
21002    }
21003
21004    let wait_flags = WaitPidFlag::WNOHANG
21005        | WaitPidFlag::WNOWAIT
21006        | WaitPidFlag::WEXITED
21007        | WaitPidFlag::WUNTRACED
21008        | WaitPidFlag::WCONTINUED;
21009    match wait_on_child(WaitId::Pid(Pid::from_raw(child_pid as i32)), wait_flags) {
21010        Ok(WaitStatus::StillAlive)
21011        | Ok(WaitStatus::Stopped(_, _))
21012        | Ok(WaitStatus::Continued(_)) => Ok(None),
21013        Ok(WaitStatus::Exited(_, status)) => Ok(Some(status)),
21014        Ok(WaitStatus::Signaled(_, signal, _)) => Ok(Some(128 + signal as i32)),
21015        #[cfg(any(target_os = "linux", target_os = "android"))]
21016        Ok(WaitStatus::PtraceEvent(_, _, _) | WaitStatus::PtraceSyscall(_)) => Ok(None),
21017        Err(nix::errno::Errno::ECHILD) => Ok(Some(0)),
21018        Err(error) => Err(SidecarError::Execution(format!(
21019            "failed to inspect guest runtime process {child_pid}: {error}"
21020        ))),
21021    }
21022}
21023
21024// macOS nix exposes no `waitid`/`WNOWAIT`, so we poll with `waitpid(WNOHANG)`.
21025// NOTE: unlike Linux's `waitid(WNOWAIT)`, `waitpid` REAPS an exited child rather
21026// than leaving it waitable. That is correct for this poll (the sidecar is the
21027// reaping parent), but a second status query after exit returns ECHILD → treated
21028// as "exited(0)" below.
21029#[cfg(target_os = "macos")]
21030fn runtime_child_exit_status(child_pid: u32) -> Result<Option<i32>, SidecarError> {
21031    if child_pid == 0 {
21032        return Ok(Some(0));
21033    }
21034
21035    match waitpid(Pid::from_raw(child_pid as i32), Some(WaitPidFlag::WNOHANG)) {
21036        Ok(WaitStatus::StillAlive)
21037        | Ok(WaitStatus::Stopped(_, _))
21038        | Ok(WaitStatus::Continued(_)) => Ok(None),
21039        Ok(WaitStatus::Exited(_, status)) => Ok(Some(status)),
21040        Ok(WaitStatus::Signaled(_, signal, _)) => Ok(Some(128 + signal as i32)),
21041        Err(nix::errno::Errno::ECHILD) => Ok(Some(0)),
21042        Err(error) => Err(SidecarError::Execution(format!(
21043            "failed to inspect guest runtime process {child_pid}: {error}"
21044        ))),
21045    }
21046}
21047
21048pub(crate) fn signal_runtime_process(child_pid: u32, signal: i32) -> Result<(), SidecarError> {
21049    if child_pid == 0 {
21050        return Ok(());
21051    }
21052
21053    if !runtime_child_is_alive(child_pid)? {
21054        return Ok(());
21055    }
21056
21057    if signal == 0 {
21058        return Ok(());
21059    }
21060
21061    let parsed = Signal::try_from(signal).map_err(|_| {
21062        SidecarError::InvalidState(format!("unsupported kill_process signal {signal}"))
21063    })?;
21064    let result = send_signal(Pid::from_raw(child_pid as i32), Some(parsed));
21065
21066    match result {
21067        Ok(()) => Ok(()),
21068        Err(nix::errno::Errno::ESRCH) => Ok(()),
21069        Err(error) => Err(SidecarError::Execution(format!(
21070            "failed to signal guest runtime process {child_pid}: {error}"
21071        ))),
21072    }
21073}
21074
21075pub(crate) fn error_code(error: &SidecarError) -> &'static str {
21076    match error {
21077        SidecarError::InvalidState(_) => "invalid_state",
21078        SidecarError::ProtocolVersionMismatch(_) => "protocol_version_mismatch",
21079        SidecarError::BridgeVersionMismatch(_) => "bridge_version_mismatch",
21080        SidecarError::Conflict(_) => "conflict",
21081        SidecarError::Unauthorized(_) => "unauthorized",
21082        SidecarError::Unsupported(_) => "unsupported",
21083        SidecarError::FrameTooLarge(_) => "frame_too_large",
21084        SidecarError::Kernel(_) => "kernel_error",
21085        SidecarError::Plugin(_) => "plugin_error",
21086        SidecarError::Execution(_) => "execution_error",
21087        SidecarError::Bridge(_) => "bridge_error",
21088        SidecarError::Io(_) => "io_error",
21089    }
21090}
21091
21092fn guest_errno_code(message: &str) -> Option<&str> {
21093    const TRUSTED_PREFIXES: &[&str] = &[
21094        "ERR_AGENT_OS_NODE_SYNC_RPC",
21095        "ERR_AGENT_OS_PYTHON_VFS_RPC",
21096        "ERR_AGENT_OS_BRIDGE",
21097    ];
21098
21099    let mut segments = message.split(':').map(str::trim);
21100    let first = segments.next()?;
21101    if is_guest_errno_segment(first) {
21102        return Some(first);
21103    }
21104
21105    if TRUSTED_PREFIXES.contains(&first) {
21106        let second = segments.next()?;
21107        if is_guest_errno_segment(second) {
21108            return Some(second);
21109        }
21110    }
21111
21112    None
21113}
21114
21115fn is_guest_errno_segment(segment: &str) -> bool {
21116    segment.len() >= 2
21117        && segment.starts_with('E')
21118        && !segment.starts_with("ERR_")
21119        && segment[1..]
21120            .bytes()
21121            .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_')
21122}
21123
21124pub(crate) fn javascript_sync_rpc_error_code(error: &SidecarError) -> String {
21125    let message = error.to_string();
21126    if let Some(code) = guest_errno_code(&message) {
21127        return code.to_owned();
21128    }
21129    if message.starts_with("ERR_NATIVE_BINARY_NOT_SUPPORTED:") {
21130        return String::from("ERR_NATIVE_BINARY_NOT_SUPPORTED");
21131    }
21132
21133    let lower = message.to_ascii_lowercase();
21134    if lower.contains("no such file or directory")
21135        || lower.contains("entry not found")
21136        || lower.contains("not found")
21137    {
21138        return String::from("ENOENT");
21139    }
21140    if lower.contains("permission denied") {
21141        return String::from("EACCES");
21142    }
21143    if lower.contains("already exists")
21144        || lower.contains("already registered")
21145        || lower.contains("file exists")
21146    {
21147        return String::from("EEXIST");
21148    }
21149    if lower.contains("invalid argument") {
21150        return String::from("EINVAL");
21151    }
21152
21153    String::from("ERR_AGENT_OS_NODE_SYNC_RPC")
21154}
21155
21156pub(crate) fn ignore_stale_javascript_sync_rpc_response(
21157    error: SidecarError,
21158) -> Result<(), SidecarError> {
21159    match error {
21160        SidecarError::Execution(message)
21161            if message.ends_with("is no longer pending")
21162                && message.starts_with("sync RPC request ") =>
21163        {
21164            Ok(())
21165        }
21166        SidecarError::Execution(message) => {
21167            let lower = message.to_ascii_lowercase();
21168            if lower.contains("sync rpc response")
21169                && (lower.contains("broken pipe") || lower.contains("channel closed unexpectedly"))
21170            {
21171                Ok(())
21172            } else {
21173                Err(SidecarError::Execution(message))
21174            }
21175        }
21176        other => Err(other),
21177    }
21178}
21179
21180#[cfg(test)]
21181mod error_code_tests {
21182    use super::{guest_errno_code, javascript_sync_rpc_error_code, SidecarError};
21183
21184    #[test]
21185    fn guest_errno_code_rejects_guest_controlled_errno_segments() {
21186        assert_eq!(guest_errno_code("user said 'EACCES: denied'"), None);
21187        assert_eq!(
21188            guest_errno_code("prefix: user said 'EPERM': more text"),
21189            None
21190        );
21191        assert_eq!(guest_errno_code("ERR_AGENT_OS_FAKE: EACCES: denied"), None);
21192    }
21193
21194    #[test]
21195    fn guest_errno_code_accepts_trusted_secure_exec_prefixes() {
21196        assert_eq!(
21197            guest_errno_code("ERR_AGENT_OS_NODE_SYNC_RPC: EACCES: permission denied on /foo"),
21198            Some("EACCES")
21199        );
21200        assert_eq!(
21201            guest_errno_code("ERR_AGENT_OS_PYTHON_VFS_RPC: ENOENT: missing file"),
21202            Some("ENOENT")
21203        );
21204        assert_eq!(guest_errno_code("EEXIST: already exists"), Some("EEXIST"));
21205    }
21206
21207    #[test]
21208    fn javascript_sync_rpc_error_code_ignores_spoofed_errnos() {
21209        let error = SidecarError::Execution(String::from("user said 'EACCES: denied'"));
21210        assert_eq!(
21211            javascript_sync_rpc_error_code(&error),
21212            "ERR_AGENT_OS_NODE_SYNC_RPC"
21213        );
21214    }
21215
21216    #[test]
21217    fn javascript_sync_rpc_error_code_preserves_real_sidecar_errnos() {
21218        let error = SidecarError::Execution(String::from(
21219            "ERR_AGENT_OS_NODE_SYNC_RPC: EACCES: permission denied on /foo",
21220        ));
21221        assert_eq!(javascript_sync_rpc_error_code(&error), "EACCES");
21222    }
21223
21224    #[test]
21225    fn javascript_sync_rpc_error_code_maps_file_exists_messages() {
21226        let error = SidecarError::Io(String::from(
21227            "failed to create mapped guest directory /.next/server: File exists (os error 17)",
21228        ));
21229        assert_eq!(javascript_sync_rpc_error_code(&error), "EEXIST");
21230    }
21231
21232    #[test]
21233    fn javascript_sync_rpc_error_code_preserves_native_binary_rejections() {
21234        let error = SidecarError::Execution(String::from(
21235            "ERR_NATIVE_BINARY_NOT_SUPPORTED: refused to execute native ELF guest binary at /tmp/fake-rg inside the VM",
21236        ));
21237        assert_eq!(
21238            javascript_sync_rpc_error_code(&error),
21239            "ERR_NATIVE_BINARY_NOT_SUPPORTED"
21240        );
21241    }
21242}
21243#[cfg(test)]
21244mod ssrf_egress_classifier_tests {
21245    // F-005/006/007 (sec-sidecar T1/T7/T11): the egress classifier must treat the
21246    // unspecified address (0.0.0.0 / ::), CGNAT (100.64.0.0/10), IPv6 spellings of
21247    // restricted IPv4 targets (::a.b.c.d), and reserved/multicast (240/4, 224/4) as
21248    // restricted. 0.0.0.0 routes to 127.0.0.1 on connect(), so leaving it
21249    // unclassified let a guest bypass the loopback port-ownership gate.
21250    //
21251    // These are bounded SAFEGUARD tests: they exercise the classifier and the DNS
21252    // egress filter directly (no network I/O, no Node), so they run fast and
21253    // deterministically. See FAILURES.md#F-005, #F-006, #F-007.
21254    use super::{
21255        filter_dns_safe_ip_addrs, is_loopback_ip, restricted_non_loopback_ip_range, SidecarError,
21256    };
21257    use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
21258
21259    fn assert_restricted(ip: IpAddr, expected_label: &str) {
21260        let classification = restricted_non_loopback_ip_range(ip);
21261        assert!(
21262            classification.is_some(),
21263            "{ip} must be classified as a restricted egress target"
21264        );
21265        let (_cidr, label) = classification.unwrap();
21266        assert_eq!(
21267            label, expected_label,
21268            "{ip} should be labelled {expected_label}, got {label}"
21269        );
21270    }
21271
21272    fn assert_dns_denied(ip: IpAddr, label: &str) {
21273        match filter_dns_safe_ip_addrs(vec![ip], "attacker.example") {
21274            Err(SidecarError::Execution(message)) => assert!(
21275                message.starts_with("EACCES:"),
21276                "{label}: egress filter must deny with EACCES, got: {message}"
21277            ),
21278            other => panic!("{label}: expected EACCES denial, got {other:?}"),
21279        }
21280    }
21281
21282    // F-005 (sec-sidecar T1).
21283    #[test]
21284    fn classifier_denies_unspecified_and_cgnat_targets() {
21285        // 0.0.0.0 (IPv4 unspecified) -> would route to host loopback.
21286        assert_restricted(IpAddr::V4(Ipv4Addr::UNSPECIFIED), "unspecified");
21287        // :: (IPv6 unspecified).
21288        assert_restricted(IpAddr::V6(Ipv6Addr::UNSPECIFIED), "unspecified");
21289
21290        // CGNAT 100.64.0.0/10 spans 100.64.x.x .. 100.127.x.x.
21291        assert_restricted(
21292            IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1)),
21293            "carrier-grade-nat",
21294        );
21295        assert_restricted(
21296            IpAddr::V4(Ipv4Addr::new(100, 127, 255, 254)),
21297            "carrier-grade-nat",
21298        );
21299
21300        // Guard against over-blocking: addresses just outside 100.64/10 stay allowed.
21301        assert!(
21302            restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(100, 63, 255, 255)))
21303                .is_none(),
21304            "100.63.255.255 is outside CGNAT and must remain allowed"
21305        );
21306        assert!(
21307            restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(100, 128, 0, 0))).is_none(),
21308            "100.128.0.0 is outside CGNAT and must remain allowed"
21309        );
21310
21311        // The DNS egress filter must also deny these via EACCES.
21312        assert_dns_denied(IpAddr::V4(Ipv4Addr::UNSPECIFIED), "0.0.0.0 (unspecified)");
21313        assert_dns_denied(IpAddr::V6(Ipv6Addr::UNSPECIFIED), ":: (unspecified)");
21314        assert_dns_denied(
21315            IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1)),
21316            "100.64.0.1 (CGNAT)",
21317        );
21318    }
21319
21320    // F-006 (sec-sidecar T7).
21321    #[test]
21322    fn classifier_denies_ipv6_spelled_metadata_addresses() {
21323        // The IPv4-mapped form (::ffff:169.254.169.254) was already handled; the
21324        // IPv4-compatible form (::169.254.169.254) is the gap this fixes.
21325        let mapped = "::ffff:169.254.169.254".parse::<Ipv6Addr>().unwrap();
21326        assert_restricted(IpAddr::V6(mapped), "link-local");
21327
21328        let compat = "::169.254.169.254".parse::<Ipv6Addr>().unwrap();
21329        assert_restricted(IpAddr::V6(compat), "link-local");
21330
21331        // Other IPv4-compatible private/CGNAT spellings must also be canonicalized.
21332        assert_restricted(
21333            IpAddr::V6("::10.0.0.1".parse::<Ipv6Addr>().unwrap()),
21334            "private",
21335        );
21336        assert_restricted(
21337            IpAddr::V6("::100.64.0.1".parse::<Ipv6Addr>().unwrap()),
21338            "carrier-grade-nat",
21339        );
21340
21341        // Guard against over-blocking: the IPv6 unspecified/loopback addresses
21342        // are not IPv4-compatible host targets, and a public IPv4-compatible
21343        // address must remain allowed.
21344        assert_eq!(
21345            restricted_non_loopback_ip_range(IpAddr::V6(Ipv6Addr::UNSPECIFIED)),
21346            Some(("::/128", "unspecified")),
21347            ":: must classify as unspecified, not via the IPv4-compat path"
21348        );
21349        assert!(
21350            restricted_non_loopback_ip_range(IpAddr::V6(Ipv6Addr::LOCALHOST)).is_none()
21351                || is_loopback_ip(IpAddr::V6(Ipv6Addr::LOCALHOST)),
21352            "::1 must not be classified as a restricted IPv4-compatible target"
21353        );
21354        assert!(
21355            restricted_non_loopback_ip_range(IpAddr::V6("::8.8.8.8".parse::<Ipv6Addr>().unwrap()))
21356                .is_none(),
21357            "::8.8.8.8 (public IPv4-compatible) must remain allowed"
21358        );
21359
21360        // The DNS egress filter must deny the IPv4-compat metadata spelling.
21361        assert_dns_denied(
21362            IpAddr::V6("::169.254.169.254".parse::<Ipv6Addr>().unwrap()),
21363            "::169.254.169.254 (IPv4-compat metadata)",
21364        );
21365    }
21366
21367    // F-007 (sec-sidecar T11).
21368    #[test]
21369    fn classifier_denies_reserved_and_multicast_targets() {
21370        // 224.0.0.0/4 (multicast) and 240.0.0.0/4 (reserved / future use) are not
21371        // legitimate unicast egress targets; a guest connect to them must be
21372        // classified as restricted and denied.
21373        assert_restricted(IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)), "multicast");
21374        assert_restricted(IpAddr::V4(Ipv4Addr::new(239, 255, 255, 255)), "multicast");
21375        assert_restricted(IpAddr::V4(Ipv4Addr::new(240, 0, 0, 1)), "reserved");
21376        // 255.255.255.255 (limited broadcast) falls in 240.0.0.0/4.
21377        assert_restricted(IpAddr::V4(Ipv4Addr::BROADCAST), "reserved");
21378
21379        // IPv4-compatible IPv6 spellings must canonicalize and be denied too.
21380        assert_restricted(
21381            IpAddr::V6("::224.0.0.1".parse::<Ipv6Addr>().unwrap()),
21382            "multicast",
21383        );
21384        assert_restricted(
21385            IpAddr::V6("::240.0.0.1".parse::<Ipv6Addr>().unwrap()),
21386            "reserved",
21387        );
21388
21389        // Guard against over-blocking: addresses just outside 224/4 stay allowed.
21390        assert!(
21391            restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(223, 255, 255, 255)))
21392                .is_none(),
21393            "223.255.255.255 is outside 224/4 and must remain allowed"
21394        );
21395
21396        // The DNS egress filter must also deny these via EACCES.
21397        assert_dns_denied(
21398            IpAddr::V4(Ipv4Addr::new(240, 0, 0, 1)),
21399            "240.0.0.1 (reserved)",
21400        );
21401        assert_dns_denied(
21402            IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)),
21403            "224.0.0.1 (multicast)",
21404        );
21405    }
21406}
21407
21408/// Adversarial coverage for the DNS-rebinding gap (VECTORS.md D.3) on the
21409/// Python/Pyodide `httpRequestSync` outbound HTTP path. The egress range guard
21410/// (`filter_dns_safe_ip_addrs`) runs at resolution time, but `ureq` performs its
21411/// own DNS resolution for the TCP/TLS connect, so a rebinding DNS server could
21412/// previously make the second lookup land on a private/link-local/metadata IP
21413/// the first check rejected. The fix pins `ureq`'s resolver to the vetted
21414/// address set; these tests prove the connect is pinned and refuses any other
21415/// host or an empty (fully-rejected) address set.
21416#[cfg(test)]
21417mod dns_rebinding_pin_tests {
21418    use super::{issue_outbound_http_request, split_netloc, JavascriptHttpRequestOptions};
21419    use std::collections::BTreeMap;
21420    use std::io::{Read, Write};
21421    use std::net::{IpAddr, Ipv4Addr, TcpListener};
21422    use std::thread;
21423    use url::Url;
21424
21425    fn empty_headers() -> super::HttpHeaderCollection {
21426        super::parse_http_header_collection(&BTreeMap::new(), "test headers")
21427            .expect("empty header collection")
21428    }
21429
21430    fn options() -> JavascriptHttpRequestOptions {
21431        JavascriptHttpRequestOptions {
21432            method: Some(String::from("GET")),
21433            headers: BTreeMap::new(),
21434            body: None,
21435            reject_unauthorized: None,
21436        }
21437    }
21438
21439    #[test]
21440    fn split_netloc_handles_hostnames_and_bracketed_ipv6() {
21441        assert_eq!(
21442            split_netloc("attacker.example:80"),
21443            Some(("attacker.example", 80))
21444        );
21445        assert_eq!(split_netloc("[::1]:443"), Some(("::1", 443)));
21446        assert_eq!(split_netloc("10.0.0.1:8080"), Some(("10.0.0.1", 8080)));
21447        assert_eq!(split_netloc("no-port"), None);
21448        assert_eq!(split_netloc("host:notaport"), None);
21449    }
21450
21451    /// A loopback HTTP server stands in for the egress-vetted target. The
21452    /// request URL uses a *different* hostname (`attacker.example`) whose real
21453    /// DNS would resolve elsewhere; pinning forces the connect onto the vetted
21454    /// IP only. If the resolver were unpinned, the request would fail to reach
21455    /// this server (and on a real host could land on a private/metadata IP).
21456    #[test]
21457    fn outbound_http_connect_is_pinned_to_vetted_ip() {
21458        let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).expect("bind loopback server");
21459        let port = listener.local_addr().expect("local addr").port();
21460        let server = thread::spawn(move || {
21461            let (mut stream, _) = listener.accept().expect("accept");
21462            let mut buf = [0u8; 1024];
21463            let _ = stream.read(&mut buf);
21464            stream
21465                .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi")
21466                .expect("write response");
21467            let _ = stream.flush();
21468        });
21469
21470        let url = Url::parse(&format!("http://attacker.example:{port}/")).expect("url");
21471        let pinned = vec![IpAddr::V4(Ipv4Addr::LOCALHOST)];
21472        let result = issue_outbound_http_request(&url, &options(), &empty_headers(), &pinned)
21473            .expect("pinned request should reach the vetted loopback target");
21474        let payload = result.as_str().expect("string payload");
21475        assert!(
21476            payload.contains("\"status\":200"),
21477            "expected 200 from pinned target, got: {payload}"
21478        );
21479        server.join().expect("server thread");
21480    }
21481
21482    /// With no vetted address (every resolved IP was rejected by the range
21483    /// guard, or the literal IP was a blocked range), the pinned resolver must
21484    /// refuse rather than fall back to the host resolver.
21485    #[test]
21486    fn outbound_http_refuses_when_no_vetted_address() {
21487        let url = Url::parse("https://attacker.example/").expect("url");
21488        let error = issue_outbound_http_request(&url, &options(), &empty_headers(), &[])
21489            .expect_err("empty pinned set must be refused");
21490        let message = error.to_string();
21491        assert!(
21492            message.contains("EACCES") || message.contains("ERR_HTTP_REQUEST_FAILED"),
21493            "expected an egress refusal, got: {message}"
21494        );
21495    }
21496}