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 = "/__agentos_pyodide";
154const PYTHON_PYODIDE_CACHE_GUEST_ROOT: &str = "/__agentos_pyodide_cache";
155const TCP_SOCKET_POLL_TIMEOUT: Duration = Duration::from_millis(100);
156const TLS_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(5);
157const HTTP_LOOPBACK_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
158pub(crate) const MAX_PER_PROCESS_STATE_HANDLES: usize = 1024;
159const VM_FETCH_BUFFER_LIMIT_BYTES: usize = DEFAULT_MAX_FRAME_BYTES;
160const DEFAULT_SCRYPT_COST: u64 = 16_384;
161const DEFAULT_SCRYPT_BLOCK_SIZE: u32 = 8;
162const DEFAULT_SCRYPT_PARALLELIZATION: u32 = 1;
163const SQLITE_JS_SAFE_INTEGER_MAX: i64 = 9_007_199_254_740_991;
164const HTTP_LOOPBACK_REQUEST_TIMEOUT_MS_ENV: &str =
165    "SECURE_EXEC_TEST_HTTP_LOOPBACK_REQUEST_TIMEOUT_MS";
166
167trait Http2AsyncIo: AsyncRead + AsyncWrite + Unpin + Send {}
168
169impl<T> Http2AsyncIo for T where T: AsyncRead + AsyncWrite + Unpin + Send {}
170
171fn http_loopback_request_timeout() -> Duration {
172    static TIMEOUT: OnceLock<Duration> = OnceLock::new();
173    *TIMEOUT.get_or_init(|| {
174        std::env::var(HTTP_LOOPBACK_REQUEST_TIMEOUT_MS_ENV)
175            .ok()
176            .and_then(|value| value.parse::<u64>().ok())
177            .map(Duration::from_millis)
178            .unwrap_or(HTTP_LOOPBACK_REQUEST_TIMEOUT)
179    })
180}
181
182const DEFAULT_ALLOWED_NODE_BUILTINS: &[&str] = &[
183    "assert",
184    "buffer",
185    "console",
186    "child_process",
187    "crypto",
188    "dns",
189    "events",
190    "fs",
191    "http",
192    "http2",
193    "https",
194    "module",
195    "os",
196    "path",
197    "perf_hooks",
198    "querystring",
199    "sqlite",
200    "stream",
201    "string_decoder",
202    "timers",
203    "tls",
204    "tty",
205    "url",
206    "util",
207    "zlib",
208];
209
210#[derive(Debug, Clone, Copy, PartialEq, Eq)]
211enum JavascriptCryptoDigestAlgorithm {
212    Md5,
213    Sha1,
214    Sha256,
215    Sha512,
216}
217
218#[derive(Debug, Default, Deserialize)]
219#[serde(default, rename_all = "camelCase")]
220struct JavascriptScryptOptions {
221    #[serde(alias = "N")]
222    cost: Option<u64>,
223    #[serde(alias = "r")]
224    block_size: Option<u32>,
225    #[serde(alias = "p")]
226    parallelization: Option<u32>,
227}
228
229#[derive(Debug, Deserialize)]
230#[serde(rename_all = "camelCase")]
231struct JavascriptHttpListenRequest {
232    server_id: u64,
233    #[serde(default)]
234    port: Option<u16>,
235    #[serde(default)]
236    hostname: Option<String>,
237}
238
239#[derive(Debug, Default, Deserialize)]
240#[serde(default, rename_all = "camelCase")]
241struct JavascriptHttpRequestOptions {
242    method: Option<String>,
243    headers: BTreeMap<String, Value>,
244    body: Option<String>,
245    reject_unauthorized: Option<bool>,
246}
247
248#[derive(Debug, Default, Deserialize)]
249#[serde(default, rename_all = "camelCase")]
250struct JavascriptHttp2ServerListenRequest {
251    server_id: u64,
252    secure: bool,
253    port: Option<u16>,
254    host: Option<String>,
255    backlog: Option<u32>,
256    timeout: Option<u64>,
257    settings: BTreeMap<String, Value>,
258    tls: Option<JavascriptTlsBridgeOptions>,
259}
260
261#[derive(Debug, Default, Deserialize)]
262#[serde(default, rename_all = "camelCase")]
263struct JavascriptHttp2SessionConnectRequest {
264    authority: Option<String>,
265    protocol: Option<String>,
266    host: Option<String>,
267    port: Option<u16>,
268    settings: BTreeMap<String, Value>,
269    tls: Option<JavascriptTlsBridgeOptions>,
270}
271
272#[derive(Debug, Default, Deserialize)]
273#[serde(default, rename_all = "camelCase")]
274struct JavascriptHttp2RequestOptions {
275    end_stream: bool,
276}
277
278#[derive(Debug, Default, Deserialize)]
279#[serde(default, rename_all = "camelCase")]
280struct JavascriptHttp2FileResponseOptions {
281    offset: Option<u64>,
282    length: Option<i64>,
283}
284
285#[derive(Debug, Clone)]
286struct HttpHeaderCollection {
287    normalized: BTreeMap<String, Vec<String>>,
288    raw_pairs: Vec<(String, String)>,
289}
290
291#[derive(Debug)]
292struct InsecureTlsVerifier {
293    supported_schemes: Vec<SignatureScheme>,
294}
295
296impl ServerCertVerifier for InsecureTlsVerifier {
297    fn verify_server_cert(
298        &self,
299        _end_entity: &CertificateDer<'_>,
300        _intermediates: &[CertificateDer<'_>],
301        _server_name: &ServerName<'_>,
302        _ocsp_response: &[u8],
303        _now: rustls::pki_types::UnixTime,
304    ) -> Result<ServerCertVerified, rustls::Error> {
305        Ok(ServerCertVerified::assertion())
306    }
307
308    fn verify_tls12_signature(
309        &self,
310        _message: &[u8],
311        _cert: &CertificateDer<'_>,
312        _dss: &DigitallySignedStruct,
313    ) -> Result<HandshakeSignatureValid, rustls::Error> {
314        Ok(HandshakeSignatureValid::assertion())
315    }
316
317    fn verify_tls13_signature(
318        &self,
319        _message: &[u8],
320        _cert: &CertificateDer<'_>,
321        _dss: &DigitallySignedStruct,
322    ) -> Result<HandshakeSignatureValid, rustls::Error> {
323        Ok(HandshakeSignatureValid::assertion())
324    }
325
326    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
327        self.supported_schemes.clone()
328    }
329}
330
331impl ActiveProcess {
332    pub(crate) fn new(
333        kernel_pid: u32,
334        kernel_handle: KernelProcessHandle,
335        runtime: GuestRuntimeKind,
336        execution: ActiveExecution,
337    ) -> Self {
338        Self {
339            kernel_pid,
340            kernel_handle,
341            kernel_stdin_writer_fd: None,
342            runtime,
343            detached: false,
344            execution,
345            guest_cwd: String::from("/"),
346            env: BTreeMap::new(),
347            host_cwd: PathBuf::from("/"),
348            mapped_host_fds: BTreeMap::new(),
349            next_mapped_host_fd: MAPPED_HOST_FD_START,
350            pending_execution_events: VecDeque::new(),
351            pending_self_signal_exit: None,
352            child_processes: BTreeMap::new(),
353            next_child_process_id: 0,
354            http_servers: BTreeMap::new(),
355            pending_http_requests: BTreeMap::new(),
356            http2: Default::default(),
357            tcp_listeners: BTreeMap::new(),
358            next_tcp_listener_id: 0,
359            tcp_sockets: BTreeMap::new(),
360            next_tcp_socket_id: 0,
361            tcp_port_reservations: BTreeMap::new(),
362            next_tcp_port_reservation_id: 0,
363            unix_listeners: BTreeMap::new(),
364            next_unix_listener_id: 0,
365            unix_sockets: BTreeMap::new(),
366            next_unix_socket_id: 0,
367            udp_sockets: BTreeMap::new(),
368            next_udp_socket_id: 0,
369            cipher_sessions: BTreeMap::new(),
370            next_cipher_session_id: 0,
371            diffie_hellman_sessions: BTreeMap::new(),
372            next_diffie_hellman_session_id: 0,
373            sqlite_databases: BTreeMap::new(),
374            next_sqlite_database_id: 0,
375            sqlite_statements: BTreeMap::new(),
376            next_sqlite_statement_id: 0,
377            module_resolution_cache: secure_exec_execution::LocalModuleResolutionCache::default(),
378        }
379    }
380
381    pub(crate) fn queue_pending_execution_event(
382        &mut self,
383        event: ActiveExecutionEvent,
384    ) -> Result<(), SidecarError> {
385        if self.pending_execution_events.len() >= MAX_PROCESS_EVENT_QUEUE {
386            return Err(process_event_queue_overflow_error());
387        }
388        self.pending_execution_events.push_back(event);
389        Ok(())
390    }
391
392    pub(crate) fn with_host_cwd(mut self, host_cwd: PathBuf) -> Self {
393        self.host_cwd = host_cwd;
394        self
395    }
396
397    pub(crate) fn with_guest_cwd(mut self, guest_cwd: String) -> Self {
398        self.guest_cwd = guest_cwd;
399        self
400    }
401
402    pub(crate) fn with_env(mut self, env: BTreeMap<String, String>) -> Self {
403        self.env = env;
404        self
405    }
406
407    pub(crate) fn with_kernel_stdin_writer_fd(mut self, fd: u32) -> Self {
408        self.kernel_stdin_writer_fd = Some(fd);
409        self
410    }
411
412    pub(crate) fn with_detached(mut self, detached: bool) -> Self {
413        self.detached = detached;
414        self
415    }
416
417    pub(crate) fn allocate_mapped_host_fd(&mut self, fd: ActiveMappedHostFd) -> u32 {
418        let handle = self.next_mapped_host_fd;
419        self.next_mapped_host_fd = self
420            .next_mapped_host_fd
421            .checked_add(1)
422            .unwrap_or(MAPPED_HOST_FD_START);
423        self.mapped_host_fds.insert(handle, fd);
424        handle
425    }
426
427    pub(crate) fn mapped_host_fd(&self, fd: u32) -> Option<&ActiveMappedHostFd> {
428        self.mapped_host_fds.get(&fd)
429    }
430
431    pub(crate) fn mapped_host_fd_mut(&mut self, fd: u32) -> Option<&mut ActiveMappedHostFd> {
432        self.mapped_host_fds.get_mut(&fd)
433    }
434
435    pub(crate) fn close_mapped_host_fd(&mut self, fd: u32) -> bool {
436        self.mapped_host_fds.remove(&fd).is_some()
437    }
438
439    pub(crate) fn allocate_child_process_id(&mut self) -> String {
440        self.next_child_process_id += 1;
441        format!("child-{}", self.next_child_process_id)
442    }
443
444    fn allocate_tcp_listener_id(&mut self) -> String {
445        self.next_tcp_listener_id += 1;
446        format!("listener-{}", self.next_tcp_listener_id)
447    }
448
449    fn allocate_tcp_socket_id(&mut self) -> String {
450        self.next_tcp_socket_id += 1;
451        format!("socket-{}", self.next_tcp_socket_id)
452    }
453
454    fn allocate_tcp_port_reservation_id(&mut self) -> String {
455        self.next_tcp_port_reservation_id += 1;
456        format!("tcp-port-reservation-{}", self.next_tcp_port_reservation_id)
457    }
458
459    fn allocate_unix_listener_id(&mut self) -> String {
460        self.next_unix_listener_id += 1;
461        format!("unix-listener-{}", self.next_unix_listener_id)
462    }
463
464    fn allocate_unix_socket_id(&mut self) -> String {
465        self.next_unix_socket_id += 1;
466        format!("unix-socket-{}", self.next_unix_socket_id)
467    }
468
469    fn allocate_udp_socket_id(&mut self) -> String {
470        self.next_udp_socket_id += 1;
471        format!("udp-socket-{}", self.next_udp_socket_id)
472    }
473
474    pub(crate) fn network_resource_counts(&self) -> NetworkResourceCounts {
475        let mut counts = NetworkResourceCounts {
476            sockets: self.http_servers.len()
477                + self.tcp_listeners.len()
478                + self.tcp_sockets.len()
479                + self.unix_listeners.len()
480                + self.unix_sockets.len()
481                + self.udp_sockets.len(),
482            connections: self.tcp_sockets.len() + self.unix_sockets.len(),
483        };
484        if let Ok(http2) = self.http2.shared.lock() {
485            counts.sockets += http2.servers.len() + http2.sessions.len();
486            counts.connections += http2.sessions.len();
487        }
488
489        for child in self.child_processes.values() {
490            let child_counts = child.network_resource_counts();
491            counts.sockets += child_counts.sockets;
492            counts.connections += child_counts.connections;
493        }
494
495        counts
496    }
497
498    fn sidecar_only_network_resource_counts(&self) -> NetworkResourceCounts {
499        let mut counts = NetworkResourceCounts {
500            sockets: self.http_servers.len()
501                + self
502                    .tcp_listeners
503                    .values()
504                    .filter(|listener| listener.kernel_socket_id.is_none())
505                    .count()
506                + self
507                    .tcp_sockets
508                    .values()
509                    .filter(|socket| socket.kernel_socket_id.is_none())
510                    .count()
511                + self.unix_listeners.len()
512                + self.unix_sockets.len()
513                + self
514                    .udp_sockets
515                    .values()
516                    .filter(|socket| socket.kernel_socket_id.is_none())
517                    .count(),
518            connections: self
519                .tcp_sockets
520                .values()
521                .filter(|socket| socket.kernel_socket_id.is_none())
522                .count()
523                + self.unix_sockets.len(),
524        };
525        if let Ok(http2) = self.http2.shared.lock() {
526            counts.sockets += http2.servers.len() + http2.sessions.len();
527            counts.connections += http2.sessions.len();
528        }
529
530        for child in self.child_processes.values() {
531            let child_counts = child.sidecar_only_network_resource_counts();
532            counts.sockets += child_counts.sockets;
533            counts.connections += child_counts.connections;
534        }
535
536        counts
537    }
538}
539
540fn poll_tool_process_event(
541    execution: &ToolExecution,
542) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
543    let event = execution
544        .pending_events
545        .lock()
546        .unwrap_or_else(|poisoned| poisoned.into_inner())
547        .pop_front();
548    if event.is_some() {
549        return Ok(event);
550    }
551    if execution.events_overflowed.load(Ordering::Relaxed) {
552        return Err(process_event_queue_overflow_error());
553    }
554    Ok(None)
555}
556
557fn descendant_pending_execution_event_capacity(
558    root: &ActiveProcess,
559    child_path: &[&str],
560) -> Option<usize> {
561    let mut child = root;
562    for child_process_id in child_path {
563        child = child.child_processes.get(*child_process_id)?;
564    }
565    Some(MAX_PROCESS_EVENT_QUEUE.saturating_sub(child.pending_execution_events.len()))
566}
567
568fn poll_child_execution_after_exit(
569    child: &mut ActiveProcess,
570    wait: Duration,
571) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
572    match child.execution.poll_event_blocking(wait) {
573        Ok(event) => Ok(event),
574        Err(SidecarError::Execution(message))
575            if child.runtime == GuestRuntimeKind::WebAssembly
576                && message == WasmExecutionError::EventChannelClosed.to_string() =>
577        {
578            Ok(None)
579        }
580        Err(error) => Err(error),
581    }
582}
583
584fn closed_javascript_event_channel(message: &str) -> bool {
585    message == "guest JavaScript event channel closed unexpectedly"
586}
587
588fn closed_python_event_channel(message: &str) -> bool {
589    message == "guest Python event channel closed unexpectedly"
590}
591
592fn closed_wasm_event_channel(message: &str) -> bool {
593    message == WasmExecutionError::EventChannelClosed.to_string()
594}
595
596fn missing_vm_error(vm_id: &str) -> SidecarError {
597    SidecarError::InvalidState(format!("VM {vm_id} is no longer active"))
598}
599
600fn missing_process_error(vm_id: &str, process_id: &str) -> SidecarError {
601    SidecarError::InvalidState(format!(
602        "VM {vm_id} no longer has active process {process_id}"
603    ))
604}
605
606fn is_broken_pipe_error(error: &SidecarError) -> bool {
607    matches!(error, SidecarError::Execution(message) if message.contains("Broken pipe") || message.contains("os error 32") || message.contains("EPIPE"))
608}
609
610fn javascript_child_process_gone_error(process_id: &str, child_path: &[&str]) -> SidecarError {
611    let child_label = if child_path.is_empty() {
612        process_id.to_owned()
613    } else {
614        format!("{process_id}/{}", child_path.join("/"))
615    };
616    SidecarError::Execution(format!(
617        "ECHILD: child_process {child_label} is no longer available"
618    ))
619}
620
621fn is_javascript_child_process_gone_error(error: &SidecarError) -> bool {
622    matches!(
623        error,
624        SidecarError::Execution(message) if guest_errno_code(message) == Some("ECHILD")
625    )
626}
627
628fn loopback_tls_transport_registry(
629) -> &'static Mutex<BTreeMap<String, Weak<crate::state::LoopbackTlsTransportPair>>> {
630    static REGISTRY: OnceLock<
631        Mutex<BTreeMap<String, Weak<crate::state::LoopbackTlsTransportPair>>>,
632    > = OnceLock::new();
633    REGISTRY.get_or_init(|| Mutex::new(BTreeMap::new()))
634}
635
636fn loopback_tls_transport_key(
637    vm_id: &str,
638    socket_id: SocketId,
639    peer_socket_id: SocketId,
640) -> String {
641    let (lower, higher) = if socket_id <= peer_socket_id {
642        (socket_id, peer_socket_id)
643    } else {
644        (peer_socket_id, socket_id)
645    };
646    format!("{vm_id}:{lower}:{higher}")
647}
648
649fn loopback_tls_endpoint(
650    vm_id: &str,
651    socket_id: SocketId,
652    peer_socket_id: SocketId,
653) -> Result<crate::state::LoopbackTlsEndpoint, SidecarError> {
654    let key = loopback_tls_transport_key(vm_id, socket_id, peer_socket_id);
655    let registry = loopback_tls_transport_registry();
656    let mut transports = registry.lock().map_err(|_| {
657        SidecarError::InvalidState(String::from(
658            "loopback TLS transport registry lock poisoned",
659        ))
660    })?;
661    transports.retain(|_, pair| pair.strong_count() > 0);
662    let pair = transports
663        .get(&key)
664        .and_then(Weak::upgrade)
665        .unwrap_or_else(|| {
666            let pair = Arc::new(crate::state::LoopbackTlsTransportPair {
667                state: Mutex::new(crate::state::LoopbackTlsTransportPairState::default()),
668                ready: std::sync::Condvar::new(),
669            });
670            transports.insert(key, Arc::downgrade(&pair));
671            pair
672        });
673    Ok(crate::state::LoopbackTlsEndpoint {
674        pair,
675        is_lower_socket: socket_id <= peer_socket_id,
676    })
677}
678
679impl crate::state::LoopbackTlsEndpoint {
680    fn shutdown_write(&self) -> Result<(), SidecarError> {
681        let mut state = self.pair.state.lock().map_err(|_| {
682            SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
683        })?;
684        if self.is_lower_socket {
685            state.lower_write_closed = true;
686        } else {
687            state.higher_write_closed = true;
688        }
689        self.pair.ready.notify_all();
690        Ok(())
691    }
692
693    fn close_endpoint(&self) -> Result<(), SidecarError> {
694        let mut state = self.pair.state.lock().map_err(|_| {
695            SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
696        })?;
697        if self.is_lower_socket {
698            state.lower_write_closed = true;
699            state.lower_closed = true;
700        } else {
701            state.higher_write_closed = true;
702            state.higher_closed = true;
703        }
704        self.pair.ready.notify_all();
705        Ok(())
706    }
707}
708
709fn parse_tls_client_hello_from_bytes(
710    buffer: &[u8],
711) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
712    if buffer.is_empty() {
713        return Ok(None);
714    }
715
716    let mut acceptor = rustls::server::Acceptor::default();
717    let mut cursor = Cursor::new(buffer);
718    acceptor.read_tls(&mut cursor).map_err(sidecar_net_error)?;
719    let Some(accepted) = acceptor.accept().map_err(|(error, _)| {
720        SidecarError::Execution(format!("failed to parse TLS client hello: {error}"))
721    })?
722    else {
723        return Ok(None);
724    };
725    let client_hello = accepted.client_hello();
726    let alpn_protocols = client_hello.alpn().map(|protocols| {
727        protocols
728            .filter_map(|protocol| String::from_utf8(protocol.to_vec()).ok())
729            .collect::<Vec<_>>()
730    });
731    Ok(Some(JavascriptTlsClientHello {
732        servername: client_hello.server_name().map(str::to_owned),
733        alpn_protocols,
734    }))
735}
736
737fn peek_loopback_tls_client_hello(
738    vm_id: &str,
739    socket_id: SocketId,
740    peer_socket_id: SocketId,
741) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
742    let key = loopback_tls_transport_key(vm_id, socket_id, peer_socket_id);
743    let registry = loopback_tls_transport_registry();
744    let pair = registry
745        .lock()
746        .map_err(|_| {
747            SidecarError::InvalidState(String::from(
748                "loopback TLS transport registry lock poisoned",
749            ))
750        })?
751        .get(&key)
752        .and_then(Weak::upgrade);
753    let Some(pair) = pair else {
754        return Ok(None);
755    };
756    let is_lower_socket = socket_id <= peer_socket_id;
757    let state = pair.state.lock().map_err(|_| {
758        SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
759    })?;
760    let buffered = if is_lower_socket {
761        state.higher_to_lower.iter().copied().collect::<Vec<_>>()
762    } else {
763        state.lower_to_higher.iter().copied().collect::<Vec<_>>()
764    };
765    drop(state);
766    parse_tls_client_hello_from_bytes(&buffered)
767}
768
769fn wait_for_loopback_peer_socket_id(
770    kernel: &SidecarKernel,
771    socket_id: SocketId,
772) -> Option<SocketId> {
773    for _ in 0..50 {
774        if let Some(peer_socket_id) = kernel
775            .socket_get(socket_id)
776            .and_then(|record| record.peer_socket_id())
777        {
778            return Some(peer_socket_id);
779        }
780        std::thread::sleep(Duration::from_millis(10));
781    }
782    None
783}
784
785impl Drop for crate::state::LoopbackTlsEndpoint {
786    fn drop(&mut self) {
787        let _ = self.close_endpoint();
788    }
789}
790
791impl Read for crate::state::LoopbackTlsEndpoint {
792    fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
793        let mut state = self
794            .pair
795            .state
796            .lock()
797            .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
798
799        loop {
800            let (peer_write_closed, peer_closed) = if self.is_lower_socket {
801                (state.higher_write_closed, state.higher_closed)
802            } else {
803                (state.lower_write_closed, state.lower_closed)
804            };
805
806            let incoming = if self.is_lower_socket {
807                &mut state.higher_to_lower
808            } else {
809                &mut state.lower_to_higher
810            };
811
812            if !incoming.is_empty() {
813                let mut count = 0;
814                while count < buffer.len() {
815                    let Some(byte) = incoming.pop_front() else {
816                        break;
817                    };
818                    buffer[count] = byte;
819                    count += 1;
820                }
821                return Ok(count);
822            }
823
824            if peer_write_closed || peer_closed {
825                return Ok(0);
826            }
827
828            let (next_state, wait_result) = self
829                .pair
830                .ready
831                .wait_timeout(state, TCP_SOCKET_POLL_TIMEOUT)
832                .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
833            state = next_state;
834            if wait_result.timed_out() {
835                return Err(std::io::Error::new(
836                    std::io::ErrorKind::WouldBlock,
837                    "loopback TLS transport read timed out",
838                ));
839            }
840        }
841    }
842}
843
844impl Write for crate::state::LoopbackTlsEndpoint {
845    fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
846        let mut state = self
847            .pair
848            .state
849            .lock()
850            .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
851
852        let peer_closed = if self.is_lower_socket {
853            state.higher_closed
854        } else {
855            state.lower_closed
856        };
857        let outgoing = if self.is_lower_socket {
858            &mut state.lower_to_higher
859        } else {
860            &mut state.higher_to_lower
861        };
862        if peer_closed {
863            return Err(std::io::Error::new(
864                std::io::ErrorKind::BrokenPipe,
865                "loopback TLS peer is closed",
866            ));
867        }
868
869        outgoing.extend(buffer.iter().copied());
870        self.pair.ready.notify_all();
871        Ok(buffer.len())
872    }
873
874    fn flush(&mut self) -> std::io::Result<()> {
875        Ok(())
876    }
877}
878
879// TCP types moved to crate::state
880
881struct ActiveTcpConnectRequest<'a, B> {
882    bridge: &'a SharedBridge<B>,
883    kernel: &'a mut SidecarKernel,
884    kernel_pid: u32,
885    vm_id: &'a str,
886    dns: &'a VmDnsConfig,
887    host: &'a str,
888    port: u16,
889    local_address: Option<&'a str>,
890    local_port: Option<u16>,
891    local_reservation: Option<(JavascriptSocketFamily, u16)>,
892    context: &'a JavascriptSocketPathContext,
893}
894
895struct ActiveUdpSendToRequest<'a, B> {
896    bridge: &'a SharedBridge<B>,
897    kernel: &'a mut SidecarKernel,
898    kernel_pid: u32,
899    vm_id: &'a str,
900    dns: &'a VmDnsConfig,
901    host: &'a str,
902    port: u16,
903    context: &'a JavascriptSocketPathContext,
904    contents: &'a [u8],
905}
906
907struct UdpRemoteAddrRequest<'a, B> {
908    bridge: &'a SharedBridge<B>,
909    kernel: &'a SidecarKernel,
910    vm_id: &'a str,
911    dns: &'a VmDnsConfig,
912    host: &'a str,
913    port: u16,
914    family: JavascriptUdpFamily,
915    context: &'a JavascriptSocketPathContext,
916}
917
918pub(crate) struct JavascriptSyncRpcServiceRequest<'a, B> {
919    pub(crate) bridge: &'a SharedBridge<B>,
920    pub(crate) vm_id: &'a str,
921    pub(crate) dns: &'a VmDnsConfig,
922    pub(crate) socket_paths: &'a JavascriptSocketPathContext,
923    pub(crate) kernel: &'a mut SidecarKernel,
924    pub(crate) process: &'a mut ActiveProcess,
925    pub(crate) sync_request: &'a JavascriptSyncRpcRequest,
926    pub(crate) resource_limits: &'a ResourceLimits,
927    pub(crate) network_counts: NetworkResourceCounts,
928}
929
930pub(crate) struct JavascriptNetSyncRpcServiceRequest<'a, B> {
931    pub(crate) bridge: &'a SharedBridge<B>,
932    pub(crate) vm_id: &'a str,
933    pub(crate) dns: &'a VmDnsConfig,
934    pub(crate) socket_paths: &'a JavascriptSocketPathContext,
935    pub(crate) kernel: &'a mut SidecarKernel,
936    pub(crate) process: &'a mut ActiveProcess,
937    pub(crate) sync_request: &'a JavascriptSyncRpcRequest,
938    pub(crate) resource_limits: &'a ResourceLimits,
939    pub(crate) network_counts: NetworkResourceCounts,
940}
941
942struct LoopbackHttpResponseWaitRequest<'a, B> {
943    bridge: &'a SharedBridge<B>,
944    vm_id: &'a str,
945    dns: &'a VmDnsConfig,
946    socket_paths: &'a JavascriptSocketPathContext,
947    kernel: &'a mut SidecarKernel,
948    process: &'a mut ActiveProcess,
949    resource_limits: &'a ResourceLimits,
950    request_key: (u64, u64),
951}
952
953pub(crate) struct LoopbackHttpDispatchRequest<'a, B> {
954    pub(crate) bridge: &'a SharedBridge<B>,
955    pub(crate) vm_id: &'a str,
956    pub(crate) dns: &'a VmDnsConfig,
957    pub(crate) socket_paths: &'a JavascriptSocketPathContext,
958    pub(crate) kernel: &'a mut SidecarKernel,
959    pub(crate) process: &'a mut ActiveProcess,
960    pub(crate) resource_limits: &'a ResourceLimits,
961    pub(crate) server_id: u64,
962    pub(crate) request_json: &'a str,
963}
964
965struct JavascriptDgramSyncRpcServiceRequest<'a, B> {
966    bridge: &'a SharedBridge<B>,
967    kernel: &'a mut SidecarKernel,
968    vm_id: &'a str,
969    dns: &'a VmDnsConfig,
970    socket_paths: &'a JavascriptSocketPathContext,
971    process: &'a mut ActiveProcess,
972    sync_request: &'a JavascriptSyncRpcRequest,
973    resource_limits: &'a ResourceLimits,
974    network_counts: NetworkResourceCounts,
975}
976
977struct JavascriptHttp2SyncRpcServiceRequest<'a, B> {
978    bridge: &'a SharedBridge<B>,
979    kernel: &'a mut SidecarKernel,
980    vm_id: &'a str,
981    dns: &'a VmDnsConfig,
982    socket_paths: &'a JavascriptSocketPathContext,
983    process: &'a mut ActiveProcess,
984    sync_request: &'a JavascriptSyncRpcRequest,
985    resource_limits: &'a ResourceLimits,
986    network_counts: NetworkResourceCounts,
987}
988
989impl ActiveTcpSocket {
990    fn connect<B>(request: ActiveTcpConnectRequest<'_, B>) -> Result<Self, SidecarError>
991    where
992        B: NativeSidecarBridge + Send + 'static,
993        BridgeError<B>: fmt::Debug + Send + Sync + 'static,
994    {
995        let ActiveTcpConnectRequest {
996            bridge,
997            kernel,
998            kernel_pid,
999            vm_id,
1000            dns,
1001            host,
1002            port,
1003            local_address,
1004            local_port,
1005            local_reservation,
1006            context,
1007        } = request;
1008        let resolved = resolve_tcp_connect_addr(bridge, kernel, vm_id, dns, host, port, context)?;
1009        if resolved.use_kernel_loopback {
1010            let family = JavascriptSocketFamily::from_ip(resolved.guest_remote_addr.ip());
1011            let requested_local_port = local_port.unwrap_or(0);
1012            let local_port = if requested_local_port != 0
1013                && local_reservation == Some((family, requested_local_port))
1014            {
1015                requested_local_port
1016            } else {
1017                allocate_guest_listen_port(
1018                    requested_local_port,
1019                    family,
1020                    &context.used_tcp_guest_ports,
1021                    context.listen_policy,
1022                )?
1023            };
1024            let local_ip = match (family, local_address) {
1025                (JavascriptSocketFamily::Ipv4, Some("0.0.0.0")) => {
1026                    IpAddr::V4(Ipv4Addr::UNSPECIFIED)
1027                }
1028                (JavascriptSocketFamily::Ipv4, Some("127.0.0.1") | Some("localhost") | None) => {
1029                    IpAddr::V4(Ipv4Addr::LOCALHOST)
1030                }
1031                (JavascriptSocketFamily::Ipv6, Some("::")) => IpAddr::V6(Ipv6Addr::UNSPECIFIED),
1032                (JavascriptSocketFamily::Ipv6, Some("::1") | Some("localhost") | None) => {
1033                    IpAddr::V6(Ipv6Addr::LOCALHOST)
1034                }
1035                (JavascriptSocketFamily::Ipv4, Some(other)) => {
1036                    return Err(SidecarError::Execution(format!(
1037                        "EACCES: TCP sockets must bind to loopback or unspecified addresses, got {other}"
1038                    )));
1039                }
1040                (JavascriptSocketFamily::Ipv6, Some(other)) => {
1041                    return Err(SidecarError::Execution(format!(
1042                        "EACCES: TCP sockets must bind to loopback or unspecified addresses, got {other}"
1043                    )));
1044                }
1045            };
1046            let local_addr = SocketAddr::new(local_ip, local_port);
1047            let spec = match family {
1048                JavascriptSocketFamily::Ipv4 => SocketSpec::tcp(),
1049                JavascriptSocketFamily::Ipv6 => {
1050                    SocketSpec::new(SocketDomain::Inet6, SocketType::Stream)
1051                }
1052            };
1053            let socket_id = kernel
1054                .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
1055                .map_err(kernel_error)?;
1056            kernel
1057                .socket_bind_inet(
1058                    EXECUTION_DRIVER_NAME,
1059                    kernel_pid,
1060                    socket_id,
1061                    InetSocketAddress::new(local_ip.to_string(), local_port),
1062                )
1063                .map_err(kernel_error)?;
1064            kernel
1065                .socket_connect_inet_loopback(
1066                    EXECUTION_DRIVER_NAME,
1067                    kernel_pid,
1068                    socket_id,
1069                    InetSocketAddress::new(
1070                        resolved.guest_remote_addr.ip().to_string(),
1071                        resolved.guest_remote_addr.port(),
1072                    ),
1073                )
1074                .map_err(kernel_error)?;
1075            return Ok(Self::from_kernel(
1076                socket_id,
1077                None,
1078                local_addr,
1079                resolved.guest_remote_addr,
1080            ));
1081        }
1082
1083        let stream = TcpStream::connect_timeout(&resolved.actual_addr, Duration::from_secs(30))
1084            .map_err(sidecar_net_error)?;
1085        let guest_local_addr = stream.local_addr().map_err(sidecar_net_error)?;
1086        Self::from_stream(stream, None, guest_local_addr, resolved.guest_remote_addr)
1087    }
1088
1089    fn from_stream(
1090        stream: TcpStream,
1091        listener_id: Option<String>,
1092        guest_local_addr: SocketAddr,
1093        guest_remote_addr: SocketAddr,
1094    ) -> Result<Self, SidecarError> {
1095        let read_stream = stream.try_clone().map_err(sidecar_net_error)?;
1096        read_stream
1097            .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
1098            .map_err(sidecar_net_error)?;
1099        let stream = Arc::new(Mutex::new(stream));
1100        let pending_read_stream = Arc::new(Mutex::new(Some(read_stream)));
1101        let (sender, events) = mpsc::channel();
1102        let tls_mode = Arc::new(AtomicBool::new(false));
1103        let tls_stream = Arc::new(Mutex::new(None));
1104        let tls_state = Arc::new(Mutex::new(None));
1105        let saw_local_shutdown = Arc::new(AtomicBool::new(false));
1106        let saw_remote_end = Arc::new(AtomicBool::new(false));
1107        let close_notified = Arc::new(AtomicBool::new(false));
1108
1109        Ok(Self {
1110            stream: Some(stream),
1111            pending_read_stream: Some(pending_read_stream),
1112            events: Some(events),
1113            event_sender: Some(sender),
1114            kernel_socket_id: None,
1115            no_delay: false,
1116            keep_alive: false,
1117            keep_alive_initial_delay_secs: None,
1118            guest_local_addr,
1119            guest_remote_addr,
1120            listener_id,
1121            tls_mode,
1122            tls_stream,
1123            tls_state,
1124            saw_local_shutdown,
1125            saw_remote_end,
1126            close_notified,
1127        })
1128    }
1129
1130    fn from_kernel(
1131        socket_id: SocketId,
1132        listener_id: Option<String>,
1133        guest_local_addr: SocketAddr,
1134        guest_remote_addr: SocketAddr,
1135    ) -> Self {
1136        let (sender, events) = mpsc::channel();
1137        Self {
1138            stream: None,
1139            pending_read_stream: None,
1140            events: Some(events),
1141            event_sender: Some(sender),
1142            kernel_socket_id: Some(socket_id),
1143            no_delay: false,
1144            keep_alive: false,
1145            keep_alive_initial_delay_secs: None,
1146            guest_local_addr,
1147            guest_remote_addr,
1148            listener_id,
1149            tls_mode: Arc::new(AtomicBool::new(false)),
1150            tls_stream: Arc::new(Mutex::new(None)),
1151            tls_state: Arc::new(Mutex::new(None)),
1152            saw_local_shutdown: Arc::new(AtomicBool::new(false)),
1153            saw_remote_end: Arc::new(AtomicBool::new(false)),
1154            close_notified: Arc::new(AtomicBool::new(false)),
1155        }
1156    }
1157
1158    fn poll(
1159        &mut self,
1160        kernel: &mut SidecarKernel,
1161        kernel_pid: u32,
1162        wait: Duration,
1163    ) -> Result<Option<JavascriptTcpSocketEvent>, SidecarError> {
1164        if self.tls_mode.load(Ordering::SeqCst) {
1165            self.ensure_tcp_reader()?;
1166            return match self
1167                .events
1168                .as_ref()
1169                .ok_or_else(|| {
1170                    SidecarError::InvalidState(String::from("TCP socket event channel missing"))
1171                })?
1172                .recv_timeout(wait)
1173            {
1174                Ok(event) => Ok(Some(event)),
1175                Err(RecvTimeoutError::Timeout) => Ok(None),
1176                Err(RecvTimeoutError::Disconnected) => Ok(None),
1177            };
1178        }
1179
1180        if let Some(socket_id) = self.kernel_socket_id {
1181            let result = kernel
1182                .poll_targets(
1183                    EXECUTION_DRIVER_NAME,
1184                    kernel_pid,
1185                    vec![PollTargetEntry::socket(
1186                        socket_id,
1187                        POLLIN | POLLHUP | POLLERR,
1188                    )],
1189                    i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
1190                )
1191                .map_err(kernel_error)?;
1192            let revents = result
1193                .targets
1194                .first()
1195                .map(|entry| entry.revents)
1196                .unwrap_or_else(PollEvents::empty);
1197            if revents.is_empty() {
1198                return Ok(None);
1199            }
1200            if revents.intersects(POLLIN) {
1201                return match kernel.socket_read(
1202                    EXECUTION_DRIVER_NAME,
1203                    kernel_pid,
1204                    socket_id,
1205                    64 * 1024,
1206                ) {
1207                    Ok(Some(bytes)) if !bytes.is_empty() => {
1208                        Ok(Some(JavascriptTcpSocketEvent::Data(bytes)))
1209                    }
1210                    Ok(Some(_)) => Ok(Some(JavascriptTcpSocketEvent::Data(Vec::new()))),
1211                    Ok(None) => Ok(Some(JavascriptTcpSocketEvent::End)),
1212                    Err(error) if error.code() == "EAGAIN" => Ok(None),
1213                    Err(error) => Ok(Some(JavascriptTcpSocketEvent::Error {
1214                        code: Some(error.code().to_string()),
1215                        message: error.to_string(),
1216                    })),
1217                };
1218            }
1219            if revents.intersects(POLLHUP) {
1220                return Ok(Some(JavascriptTcpSocketEvent::End));
1221            }
1222            if revents.intersects(POLLERR) {
1223                return Ok(Some(JavascriptTcpSocketEvent::Error {
1224                    code: Some(String::from("EPIPE")),
1225                    message: String::from("kernel TCP socket reported POLLERR"),
1226                }));
1227            }
1228            return Ok(None);
1229        }
1230
1231        self.ensure_tcp_reader()?;
1232        match self
1233            .events
1234            .as_ref()
1235            .ok_or_else(|| {
1236                SidecarError::InvalidState(String::from("TCP socket event channel missing"))
1237            })?
1238            .recv_timeout(wait)
1239        {
1240            Ok(event) => Ok(Some(event)),
1241            Err(RecvTimeoutError::Timeout) => Ok(None),
1242            Err(RecvTimeoutError::Disconnected) => Ok(None),
1243        }
1244    }
1245
1246    fn ensure_tcp_reader(&self) -> Result<(), SidecarError> {
1247        if self.kernel_socket_id.is_some() {
1248            return Ok(());
1249        }
1250        if self.tls_mode.load(Ordering::SeqCst) {
1251            return Ok(());
1252        }
1253        let read_stream = self
1254            .pending_read_stream
1255            .as_ref()
1256            .ok_or_else(|| {
1257                SidecarError::InvalidState(String::from("TCP socket reader handle missing"))
1258            })?
1259            .lock()
1260            .map_err(|_| {
1261                SidecarError::InvalidState(String::from("TCP socket reader lock poisoned"))
1262            })?
1263            .take();
1264        if let Some(read_stream) = read_stream {
1265            spawn_tcp_socket_reader(
1266                read_stream,
1267                self.event_sender
1268                    .as_ref()
1269                    .ok_or_else(|| {
1270                        SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1271                    })?
1272                    .clone(),
1273                Arc::clone(&self.tls_mode),
1274                Arc::clone(&self.saw_local_shutdown),
1275                Arc::clone(&self.saw_remote_end),
1276                Arc::clone(&self.close_notified),
1277            );
1278        }
1279        Ok(())
1280    }
1281
1282    fn socket_info(&self) -> Value {
1283        json!({
1284            "localAddress": self.guest_local_addr.ip().to_string(),
1285            "localPort": self.guest_local_addr.port(),
1286            "localFamily": socket_addr_family(&self.guest_local_addr),
1287            "remoteAddress": self.guest_remote_addr.ip().to_string(),
1288            "remotePort": self.guest_remote_addr.port(),
1289            "remoteFamily": socket_addr_family(&self.guest_remote_addr),
1290        })
1291    }
1292
1293    fn set_no_delay(&mut self, enable: bool) -> Result<(), SidecarError> {
1294        self.no_delay = enable;
1295        if self.kernel_socket_id.is_some() {
1296            return Ok(());
1297        }
1298        let stream = self
1299            .stream
1300            .as_ref()
1301            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1302            .lock()
1303            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1304        stream.set_nodelay(enable).map_err(sidecar_net_error)
1305    }
1306
1307    fn set_keep_alive(
1308        &mut self,
1309        enable: bool,
1310        initial_delay_secs: Option<u64>,
1311    ) -> Result<(), SidecarError> {
1312        self.keep_alive = enable;
1313        self.keep_alive_initial_delay_secs = initial_delay_secs;
1314        if self.kernel_socket_id.is_some() {
1315            return Ok(());
1316        }
1317        let stream = self
1318            .stream
1319            .as_ref()
1320            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1321            .lock()
1322            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1323        let socket = SockRef::from(&*stream);
1324        socket.set_keepalive(enable).map_err(sidecar_net_error)?;
1325        if enable {
1326            if let Some(delay_secs) = initial_delay_secs.filter(|delay_secs| *delay_secs > 0) {
1327                socket
1328                    .set_tcp_keepalive(
1329                        &TcpKeepalive::new().with_time(Duration::from_secs(delay_secs)),
1330                    )
1331                    .map_err(sidecar_net_error)?;
1332            }
1333        }
1334        Ok(())
1335    }
1336
1337    fn upgrade_tls(
1338        &self,
1339        vm_id: &str,
1340        kernel: &SidecarKernel,
1341        options: JavascriptTlsBridgeOptions,
1342    ) -> Result<(), SidecarError> {
1343        if self.tls_mode.load(Ordering::SeqCst) {
1344            return Ok(());
1345        }
1346
1347        let client_hello = if options.is_server {
1348            self.peek_tls_client_hello(vm_id, kernel)?
1349        } else {
1350            None
1351        };
1352
1353        let tls_stream = if let Some(socket_id) = self.kernel_socket_id {
1354            let peer_socket_id = wait_for_loopback_peer_socket_id(kernel, socket_id)
1355                .ok_or_else(|| {
1356                    SidecarError::Execution(format!(
1357                        "ERR_NOT_IMPLEMENTED: kernel-backed loopback socket {socket_id} has no peer for TLS upgrade"
1358                    ))
1359                })?;
1360            let endpoint = loopback_tls_endpoint(vm_id, socket_id, peer_socket_id)?;
1361            if options.is_server {
1362                ActiveTlsStream::LoopbackServer(build_server_loopback_tls_stream(
1363                    endpoint, &options,
1364                )?)
1365            } else {
1366                ActiveTlsStream::LoopbackClient(build_client_loopback_tls_stream(
1367                    endpoint, &options,
1368                )?)
1369            }
1370        } else {
1371            self.pending_read_stream
1372                .as_ref()
1373                .ok_or_else(|| {
1374                    SidecarError::InvalidState(String::from("TCP socket reader handle missing"))
1375                })?
1376                .lock()
1377                .map_err(|_| {
1378                    SidecarError::InvalidState(String::from("TCP socket reader lock poisoned"))
1379                })?
1380                .take();
1381            let stream = self
1382                .stream
1383                .as_ref()
1384                .ok_or_else(|| {
1385                    SidecarError::InvalidState(String::from("TCP socket stream missing"))
1386                })?
1387                .lock()
1388                .map_err(|_| {
1389                    SidecarError::InvalidState(String::from("TCP socket lock poisoned"))
1390                })?;
1391            let cloned = stream.try_clone().map_err(sidecar_net_error)?;
1392            drop(stream);
1393
1394            if options.is_server {
1395                ActiveTlsStream::Server(build_server_tls_stream(cloned, &options)?)
1396            } else {
1397                ActiveTlsStream::Client(build_client_tls_stream(cloned, &options)?)
1398            }
1399        };
1400
1401        let tls_state = ActiveTlsState {
1402            client_hello,
1403            local_certificates: tls_local_certificates(&options)?,
1404            session_reused: false,
1405        };
1406
1407        self.tls_mode.store(true, Ordering::SeqCst);
1408        {
1409            let mut state = self
1410                .tls_state
1411                .lock()
1412                .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?;
1413            *state = Some(tls_state);
1414        }
1415        {
1416            let mut stream = self.tls_stream.lock().map_err(|_| {
1417                SidecarError::InvalidState(String::from("TLS stream lock poisoned"))
1418            })?;
1419            *stream = Some(tls_stream);
1420        }
1421
1422        spawn_tls_socket_reader(
1423            Arc::clone(&self.tls_stream),
1424            self.event_sender
1425                .as_ref()
1426                .ok_or_else(|| {
1427                    SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1428                })?
1429                .clone(),
1430            Arc::clone(&self.saw_local_shutdown),
1431            Arc::clone(&self.saw_remote_end),
1432            Arc::clone(&self.close_notified),
1433        );
1434        Ok(())
1435    }
1436
1437    fn peek_tls_client_hello(
1438        &self,
1439        vm_id: &str,
1440        kernel: &SidecarKernel,
1441    ) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
1442        if let Some(socket_id) = self.kernel_socket_id {
1443            let Some(peer_socket_id) = kernel
1444                .socket_get(socket_id)
1445                .and_then(|record| record.peer_socket_id())
1446            else {
1447                return Ok(None);
1448            };
1449            return peek_loopback_tls_client_hello(vm_id, socket_id, peer_socket_id);
1450        }
1451
1452        let stream = self
1453            .stream
1454            .as_ref()
1455            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1456            .lock()
1457            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1458        let mut buffer = vec![0_u8; 16 * 1024];
1459        let bytes = match stream.peek(&mut buffer) {
1460            Ok(0) => return Ok(None),
1461            Ok(bytes) => bytes,
1462            Err(error)
1463                if matches!(
1464                    error.kind(),
1465                    std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
1466                ) =>
1467            {
1468                return Ok(None);
1469            }
1470            Err(error) => return Err(sidecar_net_error(error)),
1471        };
1472        parse_tls_client_hello_from_bytes(&buffer[..bytes])
1473    }
1474
1475    fn tls_client_hello_json(
1476        &self,
1477        vm_id: &str,
1478        kernel: &SidecarKernel,
1479    ) -> Result<Value, SidecarError> {
1480        if let Some(client_hello) = self
1481            .tls_state
1482            .lock()
1483            .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?
1484            .as_ref()
1485            .and_then(|state| state.client_hello.clone())
1486        {
1487            return javascript_net_json_string(
1488                serde_json::to_value(client_hello).map_err(|error| {
1489                    SidecarError::InvalidState(format!(
1490                        "failed to serialize TLS client hello: {error}"
1491                    ))
1492                })?,
1493                "net.socket_get_tls_client_hello",
1494            );
1495        }
1496
1497        javascript_net_json_string(
1498            serde_json::to_value(
1499                self.peek_tls_client_hello(vm_id, kernel)?
1500                    .unwrap_or_default(),
1501            )
1502            .map_err(|error| {
1503                SidecarError::InvalidState(format!("failed to serialize TLS client hello: {error}"))
1504            })?,
1505            "net.socket_get_tls_client_hello",
1506        )
1507    }
1508
1509    fn tls_query(&self, query: &str, detailed: bool) -> Result<Value, SidecarError> {
1510        let state = self
1511            .tls_state
1512            .lock()
1513            .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?
1514            .clone();
1515        let mut tls_stream = self
1516            .tls_stream
1517            .lock()
1518            .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?;
1519        let Some(stream) = tls_stream.as_mut() else {
1520            return javascript_net_json_string(
1521                tls_bridge_undefined_value(),
1522                "net.socket_tls_query",
1523            );
1524        };
1525
1526        let payload = match query {
1527            "getSession" => tls_bridge_undefined_value(),
1528            "isSessionReused" => Value::Bool(
1529                state
1530                    .as_ref()
1531                    .is_some_and(|tls_state| tls_state.session_reused),
1532            ),
1533            "getPeerCertificate" => {
1534                let certificate = stream
1535                    .peer_certificates()
1536                    .and_then(|certificates| certificates.first())
1537                    .map(|certificate| {
1538                        tls_certificate_bridge_value(certificate.as_ref(), detailed)
1539                    });
1540                certificate.unwrap_or_else(tls_bridge_undefined_value)
1541            }
1542            "getCertificate" => state
1543                .as_ref()
1544                .and_then(|tls_state| tls_state.local_certificates.first())
1545                .map(|certificate| tls_certificate_bridge_value(certificate, detailed))
1546                .unwrap_or_else(tls_bridge_undefined_value),
1547            "getProtocol" => stream
1548                .protocol_version()
1549                .map(tls_protocol_name)
1550                .map(Value::String)
1551                .unwrap_or(Value::Null),
1552            "getCipher" => stream
1553                .negotiated_cipher_suite()
1554                .map(tls_cipher_bridge_value)
1555                .unwrap_or_else(tls_bridge_undefined_value),
1556            other => {
1557                return Err(SidecarError::InvalidState(format!(
1558                    "unsupported TLS query {other}"
1559                )));
1560            }
1561        };
1562        javascript_net_json_string(payload, "net.socket_tls_query")
1563    }
1564
1565    fn write_all(
1566        &self,
1567        kernel: &mut SidecarKernel,
1568        kernel_pid: u32,
1569        contents: &[u8],
1570    ) -> Result<usize, SidecarError> {
1571        if self.tls_mode.load(Ordering::SeqCst) {
1572            let mut tls_stream = self.tls_stream.lock().map_err(|_| {
1573                SidecarError::InvalidState(String::from("TLS stream lock poisoned"))
1574            })?;
1575            let stream = tls_stream.as_mut().ok_or_else(|| {
1576                SidecarError::InvalidState(String::from("TLS stream missing for upgraded socket"))
1577            })?;
1578            stream.write_all(contents)?;
1579            return Ok(contents.len());
1580        }
1581        if let Some(socket_id) = self.kernel_socket_id {
1582            return kernel
1583                .socket_write(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, contents)
1584                .map_err(kernel_error);
1585        }
1586
1587        let mut stream = self
1588            .stream
1589            .as_ref()
1590            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1591            .lock()
1592            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1593        stream.write_all(contents).map_err(sidecar_net_error)?;
1594        Ok(contents.len())
1595    }
1596
1597    fn shutdown_write(
1598        &self,
1599        kernel: &mut SidecarKernel,
1600        kernel_pid: u32,
1601    ) -> Result<(), SidecarError> {
1602        if self.tls_mode.load(Ordering::SeqCst) {
1603            if let Some(stream) = self
1604                .tls_stream
1605                .lock()
1606                .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?
1607                .as_mut()
1608            {
1609                let _ = stream.send_close_notify();
1610                let _ = stream.shutdown_write();
1611            }
1612            if self.kernel_socket_id.is_some() {
1613                self.saw_local_shutdown.store(true, Ordering::SeqCst);
1614                return Ok(());
1615            }
1616        }
1617        if let Some(socket_id) = self.kernel_socket_id {
1618            return kernel
1619                .socket_shutdown(
1620                    EXECUTION_DRIVER_NAME,
1621                    kernel_pid,
1622                    socket_id,
1623                    KernelSocketShutdown::Write,
1624                )
1625                .map_err(kernel_error);
1626        }
1627        let stream = self
1628            .stream
1629            .as_ref()
1630            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1631            .lock()
1632            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1633        self.saw_local_shutdown.store(true, Ordering::SeqCst);
1634        match stream.shutdown(Shutdown::Write) {
1635            Ok(()) => {}
1636            Err(error) if error.kind() == std::io::ErrorKind::NotConnected => {}
1637            Err(error) => return Err(sidecar_net_error(error)),
1638        }
1639        if self.saw_remote_end.load(Ordering::SeqCst)
1640            && !self.close_notified.swap(true, Ordering::SeqCst)
1641        {
1642            let _ = self
1643                .event_sender
1644                .as_ref()
1645                .ok_or_else(|| {
1646                    SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1647                })?
1648                .send(JavascriptTcpSocketEvent::Close { had_error: false });
1649        }
1650        Ok(())
1651    }
1652
1653    fn close(&self, kernel: &mut SidecarKernel, kernel_pid: u32) -> Result<(), SidecarError> {
1654        if self.tls_mode.load(Ordering::SeqCst) {
1655            if let Some(stream) = self
1656                .tls_stream
1657                .lock()
1658                .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?
1659                .as_mut()
1660            {
1661                let _ = stream.send_close_notify();
1662                let _ = stream.close();
1663            }
1664            if self.kernel_socket_id.is_some() {
1665                return Ok(());
1666            }
1667        }
1668        if let Some(socket_id) = self.kernel_socket_id {
1669            return kernel
1670                .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id)
1671                .map_err(kernel_error);
1672        }
1673        let stream = self
1674            .stream
1675            .as_ref()
1676            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1677            .lock()
1678            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1679        stream.shutdown(Shutdown::Both).map_err(sidecar_net_error)
1680    }
1681}
1682
1683impl ActiveTlsStream {
1684    fn write_all(&mut self, contents: &[u8]) -> Result<(), SidecarError> {
1685        match self {
1686            Self::Client(stream) => {
1687                stream.write_all(contents).map_err(sidecar_net_error)?;
1688                stream.flush().map_err(sidecar_net_error)
1689            }
1690            Self::Server(stream) => {
1691                stream.write_all(contents).map_err(sidecar_net_error)?;
1692                stream.flush().map_err(sidecar_net_error)
1693            }
1694            Self::LoopbackClient(stream) => {
1695                stream.write_all(contents).map_err(sidecar_net_error)?;
1696                stream.flush().map_err(sidecar_net_error)
1697            }
1698            Self::LoopbackServer(stream) => {
1699                stream.write_all(contents).map_err(sidecar_net_error)?;
1700                stream.flush().map_err(sidecar_net_error)
1701            }
1702        }
1703    }
1704
1705    fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
1706        match self {
1707            Self::Client(stream) => stream.read(buffer),
1708            Self::Server(stream) => stream.read(buffer),
1709            Self::LoopbackClient(stream) => stream.read(buffer),
1710            Self::LoopbackServer(stream) => stream.read(buffer),
1711        }
1712    }
1713
1714    fn send_close_notify(&mut self) -> Result<(), SidecarError> {
1715        match self {
1716            Self::Client(stream) => {
1717                stream.conn.send_close_notify();
1718                let _ = stream.conn.complete_io(&mut stream.sock);
1719            }
1720            Self::Server(stream) => {
1721                stream.conn.send_close_notify();
1722                let _ = stream.conn.complete_io(&mut stream.sock);
1723            }
1724            Self::LoopbackClient(stream) => {
1725                stream.conn.send_close_notify();
1726                let _ = stream.conn.complete_io(&mut stream.sock);
1727            }
1728            Self::LoopbackServer(stream) => {
1729                stream.conn.send_close_notify();
1730                let _ = stream.conn.complete_io(&mut stream.sock);
1731            }
1732        }
1733        Ok(())
1734    }
1735
1736    fn shutdown_write(&mut self) -> Result<(), SidecarError> {
1737        match self {
1738            Self::Client(stream) => stream
1739                .sock
1740                .shutdown(Shutdown::Write)
1741                .map_err(sidecar_net_error),
1742            Self::Server(stream) => stream
1743                .sock
1744                .shutdown(Shutdown::Write)
1745                .map_err(sidecar_net_error),
1746            Self::LoopbackClient(stream) => stream.sock.shutdown_write(),
1747            Self::LoopbackServer(stream) => stream.sock.shutdown_write(),
1748        }
1749    }
1750
1751    fn close(&mut self) -> Result<(), SidecarError> {
1752        match self {
1753            Self::Client(stream) => stream
1754                .sock
1755                .shutdown(Shutdown::Both)
1756                .map_err(sidecar_net_error),
1757            Self::Server(stream) => stream
1758                .sock
1759                .shutdown(Shutdown::Both)
1760                .map_err(sidecar_net_error),
1761            Self::LoopbackClient(stream) => stream.sock.close_endpoint(),
1762            Self::LoopbackServer(stream) => stream.sock.close_endpoint(),
1763        }
1764    }
1765
1766    fn peer_certificates(&self) -> Option<&[CertificateDer<'static>]> {
1767        match self {
1768            Self::Client(stream) => stream.conn.peer_certificates(),
1769            Self::Server(stream) => stream.conn.peer_certificates(),
1770            Self::LoopbackClient(stream) => stream.conn.peer_certificates(),
1771            Self::LoopbackServer(stream) => stream.conn.peer_certificates(),
1772        }
1773    }
1774
1775    fn negotiated_cipher_suite(&self) -> Option<rustls::SupportedCipherSuite> {
1776        match self {
1777            Self::Client(stream) => stream.conn.negotiated_cipher_suite(),
1778            Self::Server(stream) => stream.conn.negotiated_cipher_suite(),
1779            Self::LoopbackClient(stream) => stream.conn.negotiated_cipher_suite(),
1780            Self::LoopbackServer(stream) => stream.conn.negotiated_cipher_suite(),
1781        }
1782    }
1783
1784    fn protocol_version(&self) -> Option<rustls::ProtocolVersion> {
1785        match self {
1786            Self::Client(stream) => stream.conn.protocol_version(),
1787            Self::Server(stream) => stream.conn.protocol_version(),
1788            Self::LoopbackClient(stream) => stream.conn.protocol_version(),
1789            Self::LoopbackServer(stream) => stream.conn.protocol_version(),
1790        }
1791    }
1792}
1793
1794// ActiveTcpListener moved to crate::state
1795
1796// Unix socket types moved to crate::state
1797
1798impl ActiveUnixSocket {
1799    fn connect(host_path: &Path, guest_path: &str) -> Result<Self, SidecarError> {
1800        let stream = UnixStream::connect(host_path).map_err(sidecar_net_error)?;
1801        Self::from_stream(stream, None, None, Some(guest_path.to_owned()))
1802    }
1803
1804    fn from_stream(
1805        stream: UnixStream,
1806        listener_id: Option<String>,
1807        local_path: Option<String>,
1808        remote_path: Option<String>,
1809    ) -> Result<Self, SidecarError> {
1810        let read_stream = stream.try_clone().map_err(sidecar_net_error)?;
1811        let stream = Arc::new(Mutex::new(stream));
1812        let (sender, events) = mpsc::channel();
1813        let saw_local_shutdown = Arc::new(AtomicBool::new(false));
1814        let saw_remote_end = Arc::new(AtomicBool::new(false));
1815        let close_notified = Arc::new(AtomicBool::new(false));
1816        spawn_unix_socket_reader(
1817            read_stream,
1818            sender.clone(),
1819            Arc::clone(&saw_local_shutdown),
1820            Arc::clone(&saw_remote_end),
1821            Arc::clone(&close_notified),
1822        );
1823
1824        Ok(Self {
1825            stream,
1826            events,
1827            event_sender: sender,
1828            listener_id,
1829            local_path,
1830            remote_path,
1831            saw_local_shutdown,
1832            saw_remote_end,
1833            close_notified,
1834        })
1835    }
1836
1837    fn poll(&mut self, wait: Duration) -> Result<Option<JavascriptTcpSocketEvent>, SidecarError> {
1838        match self.events.recv_timeout(wait) {
1839            Ok(event) => Ok(Some(event)),
1840            Err(RecvTimeoutError::Timeout) => Ok(None),
1841            Err(RecvTimeoutError::Disconnected) => Ok(None),
1842        }
1843    }
1844
1845    fn socket_info(&self) -> Value {
1846        json!({
1847            "localPath": self.local_path.clone(),
1848            "remotePath": self.remote_path.clone(),
1849        })
1850    }
1851
1852    fn write_all(&self, contents: &[u8]) -> Result<usize, SidecarError> {
1853        let mut stream = self
1854            .stream
1855            .lock()
1856            .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
1857        stream.write_all(contents).map_err(sidecar_net_error)?;
1858        Ok(contents.len())
1859    }
1860
1861    fn shutdown_write(&self) -> Result<(), SidecarError> {
1862        let stream = self
1863            .stream
1864            .lock()
1865            .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
1866        self.saw_local_shutdown.store(true, Ordering::SeqCst);
1867        stream
1868            .shutdown(Shutdown::Write)
1869            .map_err(sidecar_net_error)?;
1870        if self.saw_remote_end.load(Ordering::SeqCst)
1871            && !self.close_notified.swap(true, Ordering::SeqCst)
1872        {
1873            let _ = self
1874                .event_sender
1875                .send(JavascriptTcpSocketEvent::Close { had_error: false });
1876        }
1877        Ok(())
1878    }
1879
1880    fn close(&self) -> Result<(), SidecarError> {
1881        let stream = self
1882            .stream
1883            .lock()
1884            .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
1885        stream.shutdown(Shutdown::Both).map_err(sidecar_net_error)
1886    }
1887}
1888
1889// ActiveUnixListener moved to crate::state
1890
1891impl ActiveUnixListener {
1892    fn bind(
1893        host_path: &Path,
1894        guest_path: &str,
1895        backlog: Option<u32>,
1896    ) -> Result<Self, SidecarError> {
1897        if let Some(parent) = host_path.parent() {
1898            fs::create_dir_all(parent).map_err(sidecar_net_error)?;
1899        }
1900        let listener = UnixListener::bind(host_path).map_err(sidecar_net_error)?;
1901        listener.set_nonblocking(true).map_err(sidecar_net_error)?;
1902        Ok(Self {
1903            listener,
1904            path: guest_path.to_owned(),
1905            backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
1906                .expect("default backlog fits within usize"),
1907            active_connection_ids: BTreeSet::new(),
1908        })
1909    }
1910
1911    fn path(&self) -> &str {
1912        &self.path
1913    }
1914
1915    fn poll(
1916        &mut self,
1917        wait: Duration,
1918    ) -> Result<Option<JavascriptUnixListenerEvent>, SidecarError> {
1919        let deadline = Instant::now() + wait;
1920        loop {
1921            match self.listener.accept() {
1922                Ok((stream, remote_addr)) => {
1923                    if self.active_connection_ids.len() >= self.backlog {
1924                        let _ = stream.shutdown(Shutdown::Both);
1925                        if wait.is_zero() || Instant::now() >= deadline {
1926                            return Ok(None);
1927                        }
1928                        continue;
1929                    }
1930
1931                    let local_path = Some(self.path.clone());
1932                    let remote_path = unix_socket_path(&remote_addr);
1933                    return Ok(Some(JavascriptUnixListenerEvent::Connection(
1934                        PendingUnixSocket {
1935                            stream,
1936                            local_path,
1937                            remote_path,
1938                        },
1939                    )));
1940                }
1941                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
1942                    if wait.is_zero() || Instant::now() >= deadline {
1943                        return Ok(None);
1944                    }
1945                    thread::sleep(Duration::from_millis(10));
1946                }
1947                Err(error) => {
1948                    return Ok(Some(JavascriptUnixListenerEvent::Error {
1949                        code: io_error_code(&error),
1950                        message: error.to_string(),
1951                    }));
1952                }
1953            }
1954        }
1955    }
1956
1957    fn close(&self) -> Result<(), SidecarError> {
1958        Ok(())
1959    }
1960
1961    fn active_connection_count(&self) -> usize {
1962        self.active_connection_ids.len()
1963    }
1964
1965    fn register_connection(&mut self, socket_id: &str) {
1966        self.active_connection_ids.insert(socket_id.to_string());
1967    }
1968
1969    fn release_connection(&mut self, socket_id: &str) {
1970        self.active_connection_ids.remove(socket_id);
1971    }
1972}
1973
1974impl ActiveTcpListener {
1975    fn bind(
1976        bind_host: &str,
1977        guest_host: &str,
1978        guest_port: u16,
1979        backlog: Option<u32>,
1980    ) -> Result<Self, SidecarError> {
1981        let bind_addr = resolve_tcp_bind_addr(bind_host, 0)?;
1982        let guest_addr = resolve_tcp_bind_addr(guest_host, guest_port)?;
1983        let listener = TcpListener::bind(bind_addr).map_err(sidecar_net_error)?;
1984        listener.set_nonblocking(true).map_err(sidecar_net_error)?;
1985        let local_addr = listener.local_addr().map_err(sidecar_net_error)?;
1986        Ok(Self {
1987            listener: Some(listener),
1988            kernel_socket_id: None,
1989            local_addr: Some(local_addr),
1990            guest_local_addr: guest_addr,
1991            backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
1992                .expect("default backlog fits within usize"),
1993            active_connection_ids: BTreeSet::new(),
1994        })
1995    }
1996
1997    fn bind_kernel(
1998        kernel: &mut SidecarKernel,
1999        kernel_pid: u32,
2000        guest_host: &str,
2001        guest_port: u16,
2002        backlog: Option<u32>,
2003    ) -> Result<Self, SidecarError> {
2004        let guest_addr = resolve_tcp_bind_addr(guest_host, guest_port)?;
2005        let spec = match guest_addr {
2006            SocketAddr::V4(_) => SocketSpec::tcp(),
2007            SocketAddr::V6(_) => SocketSpec::new(SocketDomain::Inet6, SocketType::Stream),
2008        };
2009        let socket_id = kernel
2010            .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
2011            .map_err(kernel_error)?;
2012        kernel
2013            .socket_bind_inet(
2014                EXECUTION_DRIVER_NAME,
2015                kernel_pid,
2016                socket_id,
2017                InetSocketAddress::new(guest_addr.ip().to_string(), guest_addr.port()),
2018            )
2019            .map_err(kernel_error)?;
2020        kernel
2021            .socket_listen(
2022                EXECUTION_DRIVER_NAME,
2023                kernel_pid,
2024                socket_id,
2025                usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
2026                    .expect("default backlog fits within usize"),
2027            )
2028            .map_err(kernel_error)?;
2029        Ok(Self {
2030            listener: None,
2031            kernel_socket_id: Some(socket_id),
2032            local_addr: Some(guest_addr),
2033            guest_local_addr: guest_addr,
2034            backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
2035                .expect("default backlog fits within usize"),
2036            active_connection_ids: BTreeSet::new(),
2037        })
2038    }
2039
2040    pub(crate) fn local_addr(&self) -> SocketAddr {
2041        self.local_addr.unwrap_or(self.guest_local_addr)
2042    }
2043
2044    fn guest_local_addr(&self) -> SocketAddr {
2045        self.guest_local_addr
2046    }
2047
2048    fn poll(
2049        &mut self,
2050        kernel: &mut SidecarKernel,
2051        kernel_pid: u32,
2052        wait: Duration,
2053    ) -> Result<Option<JavascriptTcpListenerEvent>, SidecarError> {
2054        if let Some(socket_id) = self.kernel_socket_id {
2055            let result = kernel
2056                .poll_targets(
2057                    EXECUTION_DRIVER_NAME,
2058                    kernel_pid,
2059                    vec![PollTargetEntry::socket(socket_id, POLLIN)],
2060                    i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
2061                )
2062                .map_err(kernel_error)?;
2063            let revents = result
2064                .targets
2065                .first()
2066                .map(|entry| entry.revents)
2067                .unwrap_or_else(PollEvents::empty);
2068            if revents.is_empty() {
2069                return Ok(None);
2070            }
2071            let accepted_socket_id =
2072                match kernel.socket_accept(EXECUTION_DRIVER_NAME, kernel_pid, socket_id) {
2073                    Ok(accepted_socket_id) => accepted_socket_id,
2074                    Err(error) if error.code() == "EAGAIN" => return Ok(None),
2075                    Err(error) => {
2076                        return Ok(Some(JavascriptTcpListenerEvent::Error {
2077                            code: Some(error.code().to_string()),
2078                            message: error.to_string(),
2079                        }));
2080                    }
2081                };
2082            let accepted = kernel.socket_get(accepted_socket_id).ok_or_else(|| {
2083                SidecarError::InvalidState(format!(
2084                    "accepted kernel TCP socket {accepted_socket_id} is missing"
2085                ))
2086            })?;
2087            let local_addr = accepted.local_address().ok_or_else(|| {
2088                SidecarError::InvalidState(format!(
2089                    "accepted kernel TCP socket {accepted_socket_id} missing local address"
2090                ))
2091            })?;
2092            let remote_addr = accepted.peer_address().ok_or_else(|| {
2093                SidecarError::InvalidState(format!(
2094                    "accepted kernel TCP socket {accepted_socket_id} missing peer address"
2095                ))
2096            })?;
2097            return Ok(Some(JavascriptTcpListenerEvent::Connection(
2098                PendingTcpSocket {
2099                    stream: None,
2100                    kernel_socket_id: Some(accepted_socket_id),
2101                    preallocated: true,
2102                    guest_local_addr: resolve_tcp_bind_addr(local_addr.host(), local_addr.port())?,
2103                    guest_remote_addr: resolve_tcp_bind_addr(
2104                        remote_addr.host(),
2105                        remote_addr.port(),
2106                    )?,
2107                },
2108            )));
2109        }
2110
2111        let deadline = Instant::now() + wait;
2112        loop {
2113            match self
2114                .listener
2115                .as_ref()
2116                .ok_or_else(|| {
2117                    SidecarError::InvalidState(String::from("TCP listener socket missing"))
2118                })?
2119                .accept()
2120            {
2121                Ok((stream, remote_addr)) => {
2122                    if self.active_connection_ids.len() >= self.backlog {
2123                        let _ = stream.shutdown(Shutdown::Both);
2124                        if wait.is_zero() || Instant::now() >= deadline {
2125                            return Ok(None);
2126                        }
2127                        continue;
2128                    }
2129                    return Ok(Some(JavascriptTcpListenerEvent::Connection(
2130                        PendingTcpSocket {
2131                            stream: Some(stream),
2132                            kernel_socket_id: None,
2133                            preallocated: false,
2134                            guest_local_addr: self.guest_local_addr,
2135                            guest_remote_addr: SocketAddr::new(
2136                                remote_addr.ip(),
2137                                remote_addr.port(),
2138                            ),
2139                        },
2140                    )));
2141                }
2142                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
2143                    if wait.is_zero() || Instant::now() >= deadline {
2144                        return Ok(None);
2145                    }
2146                    thread::sleep(Duration::from_millis(10));
2147                }
2148                Err(error) => {
2149                    return Ok(Some(JavascriptTcpListenerEvent::Error {
2150                        code: io_error_code(&error),
2151                        message: error.to_string(),
2152                    }));
2153                }
2154            }
2155        }
2156    }
2157
2158    fn close(&self, kernel: &mut SidecarKernel, kernel_pid: u32) -> Result<(), SidecarError> {
2159        if let Some(socket_id) = self.kernel_socket_id {
2160            kernel
2161                .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id)
2162                .map_err(kernel_error)?;
2163        }
2164        Ok(())
2165    }
2166
2167    fn active_connection_count(&self) -> usize {
2168        self.active_connection_ids.len()
2169    }
2170
2171    fn register_connection(&mut self, socket_id: &str) {
2172        self.active_connection_ids.insert(socket_id.to_string());
2173    }
2174
2175    fn release_connection(&mut self, socket_id: &str) {
2176        self.active_connection_ids.remove(socket_id);
2177    }
2178}
2179
2180// UDP types moved to crate::state
2181
2182impl ActiveUdpSocket {
2183    fn new(
2184        kernel: &mut SidecarKernel,
2185        kernel_pid: u32,
2186        family: JavascriptUdpFamily,
2187    ) -> Result<Self, SidecarError> {
2188        let spec = match family {
2189            JavascriptUdpFamily::Ipv4 => SocketSpec::udp(),
2190            JavascriptUdpFamily::Ipv6 => SocketSpec::new(SocketDomain::Inet6, SocketType::Datagram),
2191        };
2192        let socket_id = kernel
2193            .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
2194            .map_err(kernel_error)?;
2195        Ok(Self {
2196            family,
2197            socket: None,
2198            kernel_socket_id: Some(socket_id),
2199            guest_local_addr: None,
2200            recv_buffer_size: 0,
2201            send_buffer_size: 0,
2202        })
2203    }
2204
2205    fn local_addr(&self) -> Option<SocketAddr> {
2206        self.guest_local_addr
2207    }
2208
2209    fn socket(&self) -> Result<&UdpSocket, SidecarError> {
2210        self.socket
2211            .as_ref()
2212            .ok_or_else(|| SidecarError::Execution(String::from("EBADF: bad file descriptor")))
2213    }
2214
2215    fn bind(
2216        &mut self,
2217        kernel: &mut SidecarKernel,
2218        kernel_pid: u32,
2219        host: Option<&str>,
2220        port: u16,
2221        context: &JavascriptSocketPathContext,
2222    ) -> Result<SocketAddr, SidecarError> {
2223        if self.socket.is_some() || self.guest_local_addr.is_some() {
2224            return Err(SidecarError::Execution(String::from(
2225                "EINVAL: secure-exec dgram socket is already bound",
2226            )));
2227        }
2228
2229        let (bind_host, guest_host, guest_family) = normalize_udp_bind_host(host, self.family)?;
2230        let guest_port = allocate_guest_listen_port(
2231            port,
2232            guest_family,
2233            &context.used_udp_guest_ports,
2234            context.listen_policy,
2235        )?;
2236        let local_addr = resolve_udp_bind_addr(guest_host, guest_port, self.family)?;
2237        if let Some(socket_id) = self.kernel_socket_id {
2238            kernel
2239                .socket_bind_inet(
2240                    EXECUTION_DRIVER_NAME,
2241                    kernel_pid,
2242                    socket_id,
2243                    InetSocketAddress::new(local_addr.ip().to_string(), local_addr.port()),
2244                )
2245                .map_err(kernel_error)?;
2246        } else {
2247            let bind_addr = resolve_udp_bind_addr(bind_host, 0, self.family)?;
2248            let socket = UdpSocket::bind(bind_addr).map_err(sidecar_net_error)?;
2249            socket.set_nonblocking(true).map_err(sidecar_net_error)?;
2250            self.socket = Some(socket);
2251        }
2252        self.guest_local_addr = Some(local_addr);
2253        Ok(local_addr)
2254    }
2255
2256    fn ensure_bound_for_send(
2257        &mut self,
2258        kernel: &mut SidecarKernel,
2259        kernel_pid: u32,
2260        context: &JavascriptSocketPathContext,
2261    ) -> Result<SocketAddr, SidecarError> {
2262        if let Some(local_addr) = self.local_addr() {
2263            return Ok(local_addr);
2264        }
2265
2266        self.bind(kernel, kernel_pid, None, 0, context)
2267    }
2268
2269    fn send_to<B>(
2270        &mut self,
2271        request: ActiveUdpSendToRequest<'_, B>,
2272    ) -> Result<(usize, SocketAddr), SidecarError>
2273    where
2274        B: NativeSidecarBridge + Send + 'static,
2275        BridgeError<B>: fmt::Debug + Send + Sync + 'static,
2276    {
2277        let ActiveUdpSendToRequest {
2278            bridge,
2279            kernel,
2280            kernel_pid,
2281            vm_id,
2282            dns,
2283            host,
2284            port,
2285            context,
2286            contents,
2287        } = request;
2288        let remote_addr = resolve_udp_addr(UdpRemoteAddrRequest {
2289            bridge,
2290            kernel,
2291            vm_id,
2292            dns,
2293            host,
2294            port,
2295            family: self.family,
2296            context,
2297        })?;
2298        let local_addr = self.ensure_bound_for_send(kernel, kernel_pid, context)?;
2299        let written = if let Some(socket_id) = self.kernel_socket_id {
2300            if is_loopback_ip(remote_addr.ip()) && remote_addr.port() == port {
2301                kernel
2302                    .socket_send_to_inet_loopback(
2303                        EXECUTION_DRIVER_NAME,
2304                        kernel_pid,
2305                        socket_id,
2306                        InetSocketAddress::new(remote_addr.ip().to_string(), remote_addr.port()),
2307                        contents,
2308                    )
2309                    .map_err(kernel_error)?
2310            } else {
2311                return Err(SidecarError::Execution(String::from(
2312                    "ERR_NOT_IMPLEMENTED: external UDP datagrams are not yet supported by the kernel-backed V8 bridge",
2313                )));
2314            }
2315        } else {
2316            let socket = self.socket.as_ref().ok_or_else(|| {
2317                SidecarError::InvalidState(String::from("UDP socket is not initialized"))
2318            })?;
2319            socket
2320                .send_to(contents, remote_addr)
2321                .map_err(sidecar_net_error)?
2322        };
2323        Ok((written, local_addr))
2324    }
2325
2326    fn poll(
2327        &self,
2328        kernel: &mut SidecarKernel,
2329        kernel_pid: u32,
2330        wait: Duration,
2331    ) -> Result<Option<JavascriptUdpSocketEvent>, SidecarError> {
2332        if let Some(socket_id) = self.kernel_socket_id {
2333            let result = kernel
2334                .poll_targets(
2335                    EXECUTION_DRIVER_NAME,
2336                    kernel_pid,
2337                    vec![PollTargetEntry::socket(socket_id, POLLIN)],
2338                    i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
2339                )
2340                .map_err(kernel_error)?;
2341            let revents = result
2342                .targets
2343                .first()
2344                .map(|entry| entry.revents)
2345                .unwrap_or_else(PollEvents::empty);
2346            if revents.is_empty() {
2347                return Ok(None);
2348            }
2349            return match kernel.socket_recv_datagram(
2350                EXECUTION_DRIVER_NAME,
2351                kernel_pid,
2352                socket_id,
2353                64 * 1024,
2354            ) {
2355                Ok(Some(datagram)) => {
2356                    let (source_address, payload) = datagram.into_parts();
2357                    let remote_addr = source_address
2358                        .map(|source| {
2359                            resolve_udp_bind_addr(source.host(), source.port(), self.family)
2360                        })
2361                        .transpose()?
2362                        .unwrap_or_else(|| match self.family {
2363                            JavascriptUdpFamily::Ipv4 => {
2364                                SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)
2365                            }
2366                            JavascriptUdpFamily::Ipv6 => {
2367                                SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0)
2368                            }
2369                        });
2370                    Ok(Some(JavascriptUdpSocketEvent::Message {
2371                        data: payload,
2372                        remote_addr,
2373                    }))
2374                }
2375                Ok(None) => Ok(None),
2376                Err(error) if error.code() == "EAGAIN" => Ok(None),
2377                Err(error) => Ok(Some(JavascriptUdpSocketEvent::Error {
2378                    code: Some(error.code().to_string()),
2379                    message: error.to_string(),
2380                })),
2381            };
2382        }
2383        let socket = self.socket()?;
2384        let deadline = Instant::now() + wait;
2385        let mut buffer = vec![0_u8; 64 * 1024];
2386
2387        loop {
2388            match socket.recv_from(&mut buffer) {
2389                Ok((bytes_read, remote_addr)) => {
2390                    return Ok(Some(JavascriptUdpSocketEvent::Message {
2391                        data: buffer[..bytes_read].to_vec(),
2392                        remote_addr,
2393                    }));
2394                }
2395                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
2396                    if wait.is_zero() || Instant::now() >= deadline {
2397                        return Ok(None);
2398                    }
2399                    thread::sleep(Duration::from_millis(10));
2400                }
2401                Err(error) => {
2402                    return Ok(Some(JavascriptUdpSocketEvent::Error {
2403                        code: io_error_code(&error),
2404                        message: error.to_string(),
2405                    }));
2406                }
2407            }
2408        }
2409    }
2410
2411    fn close(&mut self, kernel: &mut SidecarKernel, kernel_pid: u32) {
2412        if let Some(socket_id) = self.kernel_socket_id {
2413            let _ = kernel.socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id);
2414        }
2415        self.socket.take();
2416        self.guest_local_addr = None;
2417    }
2418
2419    fn set_buffer_size(&mut self, which: &str, size: usize) -> Result<(), SidecarError> {
2420        match which {
2421            "recv" => self.recv_buffer_size = size,
2422            "send" => self.send_buffer_size = size,
2423            other => {
2424                return Err(SidecarError::InvalidState(format!(
2425                    "unsupported UDP buffer size kind {other}"
2426                )));
2427            }
2428        }
2429        if self.kernel_socket_id.is_some() {
2430            return Ok(());
2431        }
2432        let socket = self.socket()?;
2433        let socket = SockRef::from(socket);
2434        match which {
2435            "recv" => socket.set_recv_buffer_size(size).map_err(sidecar_net_error),
2436            "send" => socket.set_send_buffer_size(size).map_err(sidecar_net_error),
2437            other => Err(SidecarError::InvalidState(format!(
2438                "unsupported UDP buffer size kind {other}"
2439            ))),
2440        }
2441    }
2442
2443    fn get_buffer_size(&self, which: &str) -> Result<usize, SidecarError> {
2444        if self.kernel_socket_id.is_some() {
2445            return Ok(match which {
2446                "recv" => self.recv_buffer_size,
2447                "send" => self.send_buffer_size,
2448                other => {
2449                    return Err(SidecarError::InvalidState(format!(
2450                        "unsupported UDP buffer size kind {other}"
2451                    )));
2452                }
2453            });
2454        }
2455        let socket = self.socket()?;
2456        let socket = SockRef::from(socket);
2457        match which {
2458            "recv" => socket.recv_buffer_size().map_err(sidecar_net_error),
2459            "send" => socket.send_buffer_size().map_err(sidecar_net_error),
2460            other => Err(SidecarError::InvalidState(format!(
2461                "unsupported UDP buffer size kind {other}"
2462            ))),
2463        }
2464    }
2465}
2466
2467// ActiveExecution, ActiveExecutionEvent, SocketQueryKind moved to crate::state
2468
2469impl ActiveExecution {
2470    pub(crate) fn uses_shared_v8_runtime(&self) -> bool {
2471        match self {
2472            Self::Javascript(execution) => execution.uses_shared_v8_runtime(),
2473            Self::Python(execution) => execution.uses_shared_v8_runtime(),
2474            Self::Wasm(execution) => execution.uses_shared_v8_runtime(),
2475            Self::Tool(_) => false,
2476        }
2477    }
2478
2479    pub(crate) fn child_pid(&self) -> u32 {
2480        match self {
2481            Self::Javascript(execution) => execution.child_pid(),
2482            Self::Python(execution) => execution.child_pid(),
2483            Self::Wasm(execution) => execution.child_pid(),
2484            Self::Tool(_) => 0,
2485        }
2486    }
2487
2488    pub(crate) fn write_stdin(&mut self, chunk: &[u8]) -> Result<(), SidecarError> {
2489        match self {
2490            Self::Javascript(execution) => execution
2491                .write_stdin(chunk)
2492                .map_err(|error| SidecarError::Execution(error.to_string())),
2493            Self::Python(execution) => execution
2494                .write_stdin(chunk)
2495                .map_err(|error| SidecarError::Execution(error.to_string())),
2496            Self::Wasm(execution) => execution
2497                .write_stdin(chunk)
2498                .map_err(|error| SidecarError::Execution(error.to_string())),
2499            Self::Tool(_) => Ok(()),
2500        }
2501    }
2502
2503    pub(crate) fn close_stdin(&mut self) -> Result<(), SidecarError> {
2504        match self {
2505            Self::Javascript(execution) => execution
2506                .close_stdin()
2507                .map_err(|error| SidecarError::Execution(error.to_string())),
2508            Self::Python(execution) => execution
2509                .close_stdin()
2510                .map_err(|error| SidecarError::Execution(error.to_string())),
2511            Self::Wasm(execution) => execution
2512                .close_stdin()
2513                .map_err(|error| SidecarError::Execution(error.to_string())),
2514            Self::Tool(_) => Ok(()),
2515        }
2516    }
2517
2518    pub(crate) fn respond_python_vfs_rpc_success(
2519        &mut self,
2520        id: u64,
2521        payload: PythonVfsRpcResponsePayload,
2522    ) -> Result<(), SidecarError> {
2523        match self {
2524            Self::Python(execution) => execution
2525                .respond_vfs_rpc_success(id, payload)
2526                .map_err(|error| SidecarError::Execution(error.to_string())),
2527            _ => Err(SidecarError::InvalidState(String::from(
2528                "only Python executions can service Python VFS RPC responses",
2529            ))),
2530        }
2531    }
2532
2533    pub(crate) fn respond_python_vfs_rpc_error(
2534        &mut self,
2535        id: u64,
2536        code: impl Into<String>,
2537        message: impl Into<String>,
2538    ) -> Result<(), SidecarError> {
2539        match self {
2540            Self::Python(execution) => execution
2541                .respond_vfs_rpc_error(id, code, message)
2542                .map_err(|error| SidecarError::Execution(error.to_string())),
2543            _ => Err(SidecarError::InvalidState(String::from(
2544                "only Python executions can service Python VFS RPC responses",
2545            ))),
2546        }
2547    }
2548
2549    pub(crate) fn send_javascript_stream_event(
2550        &self,
2551        event_type: &str,
2552        payload: Value,
2553    ) -> Result<(), SidecarError> {
2554        match self {
2555            Self::Javascript(execution) => execution
2556                .send_stream_event(event_type, payload)
2557                .map_err(|error| SidecarError::Execution(error.to_string())),
2558            Self::Wasm(execution) => execution
2559                .send_stream_event(event_type, payload)
2560                .map_err(|error| SidecarError::Execution(error.to_string())),
2561            _ => Err(SidecarError::InvalidState(String::from(
2562                "only embedded V8 executions can receive JavaScript stream events",
2563            ))),
2564        }
2565    }
2566
2567    pub(crate) fn javascript_v8_session_handle(&self) -> Option<V8SessionHandle> {
2568        match self {
2569            Self::Javascript(execution) => Some(execution.v8_session_handle()),
2570            Self::Wasm(execution) => Some(execution.v8_session_handle()),
2571            _ => None,
2572        }
2573    }
2574
2575    pub(crate) fn terminate(&mut self) -> Result<(), SidecarError> {
2576        match self {
2577            Self::Javascript(execution) => execution
2578                .terminate()
2579                .map_err(|error| SidecarError::Execution(error.to_string())),
2580            Self::Python(execution) => execution
2581                .kill()
2582                .map_err(|error| SidecarError::Execution(error.to_string())),
2583            Self::Wasm(execution) => execution
2584                .terminate()
2585                .map_err(|error| SidecarError::Execution(error.to_string())),
2586            Self::Tool(_) => Ok(()),
2587        }
2588    }
2589
2590    pub(crate) fn respond_javascript_sync_rpc_success(
2591        &mut self,
2592        id: u64,
2593        result: Value,
2594    ) -> Result<(), SidecarError> {
2595        match self {
2596            Self::Javascript(execution) => execution
2597                .respond_sync_rpc_success(id, result)
2598                .map_err(|error| SidecarError::Execution(error.to_string())),
2599            Self::Python(execution) => execution
2600                .respond_javascript_sync_rpc_success(id, result)
2601                .map_err(|error| SidecarError::Execution(error.to_string())),
2602            Self::Wasm(execution) => execution
2603                .respond_sync_rpc_success(id, result)
2604                .map_err(|error| SidecarError::Execution(error.to_string())),
2605            _ => Err(SidecarError::InvalidState(String::from(
2606                "only JavaScript, Python, and WebAssembly executions can service JavaScript sync RPC responses",
2607            ))),
2608        }
2609    }
2610
2611    pub(crate) fn respond_javascript_sync_rpc_error(
2612        &mut self,
2613        id: u64,
2614        code: impl Into<String>,
2615        message: impl Into<String>,
2616    ) -> Result<(), SidecarError> {
2617        match self {
2618            Self::Javascript(execution) => execution
2619                .respond_sync_rpc_error(id, code, message)
2620                .map_err(|error| SidecarError::Execution(error.to_string())),
2621            Self::Python(execution) => execution
2622                .respond_javascript_sync_rpc_error(id, code, message)
2623                .map_err(|error| SidecarError::Execution(error.to_string())),
2624            Self::Wasm(execution) => execution
2625                .respond_sync_rpc_error(id, code, message)
2626                .map_err(|error| SidecarError::Execution(error.to_string())),
2627            _ => Err(SidecarError::InvalidState(String::from(
2628                "only JavaScript, Python, and WebAssembly executions can service JavaScript sync RPC responses",
2629            ))),
2630        }
2631    }
2632
2633    pub(crate) async fn poll_event(
2634        &mut self,
2635        timeout: Duration,
2636    ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
2637        match self {
2638            Self::Javascript(execution) => execution
2639                .poll_event(timeout)
2640                .await
2641                .map(|event| {
2642                    event.map(|event| match event {
2643                        JavascriptExecutionEvent::Stdout(chunk) => {
2644                            ActiveExecutionEvent::Stdout(chunk)
2645                        }
2646                        JavascriptExecutionEvent::Stderr(chunk) => {
2647                            ActiveExecutionEvent::Stderr(chunk)
2648                        }
2649                        JavascriptExecutionEvent::SyncRpcRequest(request) => {
2650                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2651                        }
2652                        JavascriptExecutionEvent::SignalState {
2653                            signal,
2654                            registration,
2655                        } => ActiveExecutionEvent::SignalState {
2656                            signal,
2657                            registration: map_node_signal_registration(registration),
2658                        },
2659                        JavascriptExecutionEvent::Exited(code) => {
2660                            ActiveExecutionEvent::Exited(code)
2661                        }
2662                    })
2663                })
2664                .map_err(|error| SidecarError::Execution(error.to_string())),
2665            Self::Python(execution) => execution
2666                .poll_event(timeout)
2667                .await
2668                .map(|event| {
2669                    event.map(|event| match event {
2670                        PythonExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2671                        PythonExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2672                        PythonExecutionEvent::JavascriptSyncRpcRequest(request) => {
2673                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2674                        }
2675                        PythonExecutionEvent::VfsRpcRequest(request) => {
2676                            ActiveExecutionEvent::PythonVfsRpcRequest(request)
2677                        }
2678                        PythonExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2679                    })
2680                })
2681                .map_err(|error| SidecarError::Execution(error.to_string())),
2682            Self::Wasm(execution) => execution
2683                .poll_event(timeout)
2684                .await
2685                .map(|event| {
2686                    event.map(|event| match event {
2687                        WasmExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2688                        WasmExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2689                        WasmExecutionEvent::SyncRpcRequest(request) => {
2690                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2691                        }
2692                        WasmExecutionEvent::SignalState {
2693                            signal,
2694                            registration,
2695                        } => ActiveExecutionEvent::SignalState {
2696                            signal,
2697                            registration: map_wasm_signal_registration(registration),
2698                        },
2699                        WasmExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2700                    })
2701                })
2702                .map_err(|error| SidecarError::Execution(error.to_string())),
2703            Self::Tool(execution) => {
2704                let _ = timeout;
2705                poll_tool_process_event(execution)
2706            }
2707        }
2708    }
2709
2710    pub(crate) fn poll_event_blocking(
2711        &mut self,
2712        timeout: Duration,
2713    ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
2714        match self {
2715            Self::Javascript(execution) => execution
2716                .poll_event_blocking(timeout)
2717                .map(|event| {
2718                    event.map(|event| match event {
2719                        JavascriptExecutionEvent::Stdout(chunk) => {
2720                            ActiveExecutionEvent::Stdout(chunk)
2721                        }
2722                        JavascriptExecutionEvent::Stderr(chunk) => {
2723                            ActiveExecutionEvent::Stderr(chunk)
2724                        }
2725                        JavascriptExecutionEvent::SyncRpcRequest(request) => {
2726                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2727                        }
2728                        JavascriptExecutionEvent::SignalState {
2729                            signal,
2730                            registration,
2731                        } => ActiveExecutionEvent::SignalState {
2732                            signal,
2733                            registration: map_node_signal_registration(registration),
2734                        },
2735                        JavascriptExecutionEvent::Exited(code) => {
2736                            ActiveExecutionEvent::Exited(code)
2737                        }
2738                    })
2739                })
2740                .map_err(|error| SidecarError::Execution(error.to_string())),
2741            Self::Python(execution) => execution
2742                .poll_event_blocking(timeout)
2743                .map(|event| {
2744                    event.map(|event| match event {
2745                        PythonExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2746                        PythonExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2747                        PythonExecutionEvent::JavascriptSyncRpcRequest(request) => {
2748                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2749                        }
2750                        PythonExecutionEvent::VfsRpcRequest(request) => {
2751                            ActiveExecutionEvent::PythonVfsRpcRequest(request)
2752                        }
2753                        PythonExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2754                    })
2755                })
2756                .map_err(|error| SidecarError::Execution(error.to_string())),
2757            Self::Wasm(execution) => execution
2758                .poll_event_blocking(timeout)
2759                .map(|event| {
2760                    event.map(|event| match event {
2761                        WasmExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2762                        WasmExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2763                        WasmExecutionEvent::SyncRpcRequest(request) => {
2764                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2765                        }
2766                        WasmExecutionEvent::SignalState {
2767                            signal,
2768                            registration,
2769                        } => ActiveExecutionEvent::SignalState {
2770                            signal,
2771                            registration: map_wasm_signal_registration(registration),
2772                        },
2773                        WasmExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2774                    })
2775                })
2776                .map_err(|error| SidecarError::Execution(error.to_string())),
2777            Self::Tool(execution) => {
2778                let _ = timeout;
2779                poll_tool_process_event(execution)
2780            }
2781        }
2782    }
2783}
2784
2785struct ToolProcessEventRequest {
2786    sidecar_requests: SharedSidecarRequestClient,
2787    connection_id: String,
2788    session_id: String,
2789    vm_id: String,
2790    tool_resolution: ToolCommandResolution,
2791    cancelled: Arc<AtomicBool>,
2792    pending_events: Arc<Mutex<VecDeque<ActiveExecutionEvent>>>,
2793    events_overflowed: Arc<AtomicBool>,
2794}
2795
2796pub(crate) fn send_tool_process_event(
2797    pending_events: &Arc<Mutex<VecDeque<ActiveExecutionEvent>>>,
2798    events_overflowed: &AtomicBool,
2799    event: ActiveExecutionEvent,
2800) -> bool {
2801    let mut pending_events = pending_events
2802        .lock()
2803        .unwrap_or_else(|poisoned| poisoned.into_inner());
2804    if pending_events.len() >= MAX_PROCESS_EVENT_QUEUE {
2805        events_overflowed.store(true, Ordering::Relaxed);
2806        return false;
2807    }
2808    pending_events.push_back(event);
2809    true
2810}
2811
2812fn spawn_tool_process_events(request: ToolProcessEventRequest) {
2813    let ToolProcessEventRequest {
2814        sidecar_requests,
2815        connection_id,
2816        session_id,
2817        vm_id,
2818        tool_resolution,
2819        cancelled,
2820        pending_events,
2821        events_overflowed,
2822    } = request;
2823    std::thread::spawn(move || match tool_resolution {
2824        ToolCommandResolution::Failure(message) => {
2825            if !send_tool_process_event(
2826                &pending_events,
2827                &events_overflowed,
2828                ActiveExecutionEvent::Stderr(format_tool_failure_output(&message)),
2829            ) {
2830                return;
2831            }
2832            let _ = send_tool_process_event(
2833                &pending_events,
2834                &events_overflowed,
2835                ActiveExecutionEvent::Exited(1),
2836            );
2837        }
2838        ToolCommandResolution::Invoke { request, timeout } => {
2839            let response = sidecar_requests.invoke(
2840                OwnershipScope::vm(connection_id.clone(), session_id.clone(), vm_id.clone()),
2841                SidecarRequestPayload::HostCallback(request.clone()),
2842                timeout,
2843            );
2844            if cancelled.load(Ordering::Relaxed) {
2845                return;
2846            }
2847
2848            match response {
2849                Ok(crate::protocol::SidecarResponsePayload::HostCallbackResult(result)) => {
2850                    if let Some(value) = result.result {
2851                        let value: serde_json::Value = serde_json::from_str(&value)
2852                            .unwrap_or(serde_json::Value::String(value));
2853                        let stdout = serde_json::to_vec(&json!({
2854                            "ok": true,
2855                            "result": value,
2856                        }))
2857                        .unwrap_or_else(|error| {
2858                            format_tool_failure_output(&format!(
2859                                "failed to serialize tool result: {error}"
2860                            ))
2861                        });
2862                        if !send_tool_process_event(
2863                            &pending_events,
2864                            &events_overflowed,
2865                            ActiveExecutionEvent::Stdout(stdout),
2866                        ) {
2867                            return;
2868                        }
2869                        let _ = send_tool_process_event(
2870                            &pending_events,
2871                            &events_overflowed,
2872                            ActiveExecutionEvent::Exited(0),
2873                        );
2874                    } else {
2875                        let message = result
2876                            .error
2877                            .unwrap_or_else(|| String::from("tool invocation returned no result"));
2878                        if !send_tool_process_event(
2879                            &pending_events,
2880                            &events_overflowed,
2881                            ActiveExecutionEvent::Stderr(format_tool_failure_output(&message)),
2882                        ) {
2883                            return;
2884                        }
2885                        let _ = send_tool_process_event(
2886                            &pending_events,
2887                            &events_overflowed,
2888                            ActiveExecutionEvent::Exited(1),
2889                        );
2890                    }
2891                }
2892                Ok(_) => {
2893                    if !send_tool_process_event(
2894                        &pending_events,
2895                        &events_overflowed,
2896                        ActiveExecutionEvent::Stderr(format_tool_failure_output(
2897                            "unexpected sidecar tool response",
2898                        )),
2899                    ) {
2900                        return;
2901                    }
2902                    let _ = send_tool_process_event(
2903                        &pending_events,
2904                        &events_overflowed,
2905                        ActiveExecutionEvent::Exited(1),
2906                    );
2907                }
2908                Err(error) => {
2909                    if !send_tool_process_event(
2910                        &pending_events,
2911                        &events_overflowed,
2912                        ActiveExecutionEvent::Stderr(format_tool_failure_output(
2913                            &error.to_string(),
2914                        )),
2915                    ) {
2916                        return;
2917                    }
2918                    let _ = send_tool_process_event(
2919                        &pending_events,
2920                        &events_overflowed,
2921                        ActiveExecutionEvent::Exited(1),
2922                    );
2923                }
2924            }
2925        }
2926    });
2927}
2928
2929impl<B> NativeSidecar<B>
2930where
2931    B: NativeSidecarBridge + Send + 'static,
2932    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
2933{
2934    pub(crate) async fn execute(
2935        &mut self,
2936        request: &RequestFrame,
2937        payload: ExecuteRequest,
2938    ) -> Result<DispatchResult, SidecarError> {
2939        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
2940        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
2941
2942        let vm = self
2943            .vms
2944            .get_mut(&vm_id)
2945            .ok_or_else(|| missing_vm_error(&vm_id))?;
2946        if vm.active_processes.contains_key(&payload.process_id) {
2947            return Err(SidecarError::InvalidState(format!(
2948                "VM {vm_id} already has an active process with id {}",
2949                payload.process_id
2950            )));
2951        }
2952
2953        if let Some(command) = payload.command.as_deref() {
2954            if let Some(tool_resolution) =
2955                resolve_tool_command(vm, command, &payload.args, payload.cwd.as_deref())?
2956            {
2957                let guest_cwd = payload
2958                    .cwd
2959                    .as_deref()
2960                    .map(normalize_path)
2961                    .unwrap_or_else(|| vm.guest_cwd.clone());
2962                let kernel_handle = vm
2963                    .kernel
2964                    .create_virtual_process(
2965                        EXECUTION_DRIVER_NAME,
2966                        TOOL_DRIVER_NAME,
2967                        command,
2968                        std::iter::once(command.to_owned())
2969                            .chain(payload.args.iter().cloned())
2970                            .collect(),
2971                        VirtualProcessOptions {
2972                            env: vm.guest_env.clone(),
2973                            cwd: Some(guest_cwd.clone()),
2974                            ..VirtualProcessOptions::default()
2975                        },
2976                    )
2977                    .map_err(kernel_error)?;
2978                let kernel_pid = kernel_handle.pid();
2979                let tool_execution = ToolExecution::default();
2980                let cancelled = tool_execution.cancelled.clone();
2981                let pending_events = tool_execution.pending_events.clone();
2982                let events_overflowed = tool_execution.events_overflowed.clone();
2983                vm.active_processes.insert(
2984                    payload.process_id.clone(),
2985                    ActiveProcess::new(
2986                        kernel_pid,
2987                        kernel_handle,
2988                        GuestRuntimeKind::JavaScript,
2989                        ActiveExecution::Tool(tool_execution),
2990                    )
2991                    .with_guest_cwd(guest_cwd.clone())
2992                    .with_host_cwd(resolve_vm_guest_path_to_host(vm, &guest_cwd)),
2993                );
2994                self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy)?;
2995                spawn_tool_process_events(ToolProcessEventRequest {
2996                    sidecar_requests: self.sidecar_requests.clone(),
2997                    connection_id: connection_id.clone(),
2998                    session_id: session_id.clone(),
2999                    vm_id: vm_id.clone(),
3000                    tool_resolution,
3001                    cancelled,
3002                    pending_events,
3003                    events_overflowed,
3004                });
3005
3006                return Ok(DispatchResult {
3007                    response: self.respond(
3008                        request,
3009                        ResponsePayload::ProcessStarted(ProcessStartedResponse {
3010                            process_id: payload.process_id,
3011                            pid: Some(kernel_pid),
3012                        }),
3013                    ),
3014                    events: Vec::new(),
3015                });
3016            }
3017        }
3018
3019        let resolved = resolve_execute_request(vm, &payload)?;
3020        let mut env = resolved.env.clone();
3021        let sandbox_root = normalize_host_path(&vm.cwd);
3022        env.insert(
3023            String::from(EXECUTION_SANDBOX_ROOT_ENV),
3024            sandbox_root.to_string_lossy().into_owned(),
3025        );
3026        if resolved.runtime == GuestRuntimeKind::JavaScript {
3027            env.insert(
3028                String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
3029                String::from("1"),
3030            );
3031        } else if resolved.runtime == GuestRuntimeKind::WebAssembly {
3032            env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
3033        }
3034        let argv = std::iter::once(resolved.entrypoint.clone())
3035            .chain(resolved.execution_args.iter().cloned())
3036            .collect::<Vec<_>>();
3037        let kernel_handle = vm
3038            .kernel
3039            .spawn_process(
3040                &resolved.command,
3041                argv,
3042                SpawnOptions {
3043                    requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
3044                    cwd: Some(resolved.guest_cwd.clone()),
3045                    ..SpawnOptions::default()
3046                },
3047            )
3048            .map_err(kernel_error)?;
3049        let kernel_pid = kernel_handle.pid();
3050
3051        let (execution, process_env) = match resolved.runtime {
3052            GuestRuntimeKind::JavaScript => {
3053                let inline_code = load_javascript_entrypoint_source(
3054                    vm,
3055                    &resolved.host_cwd,
3056                    &resolved.entrypoint,
3057                    &env,
3058                );
3059                prepare_javascript_shadow(vm, &resolved)?;
3060
3061                let context =
3062                    self.javascript_engine
3063                        .create_context(CreateJavascriptContextRequest {
3064                            vm_id: vm_id.clone(),
3065                            bootstrap_module: None,
3066                            compile_cache_root: Some(self.cache_root.join("node-compile-cache")),
3067                        });
3068                let 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                    "AGENTOS_EXTRA_FS_READ_PATHS",
3114                    &pyodide_dist_path,
3115                    true,
3116                );
3117                add_runtime_host_access_path(
3118                    &mut env,
3119                    "AGENTOS_EXTRA_FS_READ_PATHS",
3120                    &pyodide_cache_path,
3121                    true,
3122                );
3123                add_runtime_host_access_path(
3124                    &mut env,
3125                    "AGENTOS_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_AGENTOS_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("AGENTOS_GUEST_ENTRYPOINT");
4927        env.remove("AGENTOS_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("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint);
5010            let guest_entrypoint = env.get("AGENTOS_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("AGENTOS_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                    "AGENTOS_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("AGENTOS_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("AGENTOS_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("AGENTOS_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("AGENTOS_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                "AGENTOS_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("AGENTOS_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/agentos"
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            | "/workspace"
8307    )
8308}
8309
8310#[cfg(test)]
8311mod shadow_sync_tests {
8312    use super::{is_protected_agentos_shadow_sync_path, is_shadow_bootstrap_dir};
8313
8314    #[test]
8315    fn shadow_bootstrap_sync_skips_virtual_home_tree() {
8316        assert!(is_shadow_bootstrap_dir("/home"));
8317        assert!(is_shadow_bootstrap_dir("/home/agentos"));
8318    }
8319
8320    #[test]
8321    fn protected_agentos_paths_are_not_shadow_synced() {
8322        assert!(is_protected_agentos_shadow_sync_path("/etc/agentos"));
8323        assert!(is_protected_agentos_shadow_sync_path(
8324            "/etc/agentos/instructions.md"
8325        ));
8326        assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos-copy"));
8327        assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos.md"));
8328    }
8329}
8330
8331fn is_kernel_owned_shadow_sync_path(path: &str) -> bool {
8332    matches!(path, "/dev" | "/proc" | "/sys")
8333        || path.starts_with("/dev/")
8334        || path.starts_with("/proc/")
8335        || path.starts_with("/sys/")
8336}
8337
8338pub(crate) fn is_protected_agentos_shadow_sync_path(path: &str) -> bool {
8339    path == "/etc/agentos" || path.starts_with("/etc/agentos/")
8340}
8341
8342fn should_skip_shadow_sync_path(vm: &VmState, guest_path: &str) -> bool {
8343    is_kernel_owned_shadow_sync_path(guest_path)
8344        || is_protected_agentos_shadow_sync_path(guest_path)
8345        || host_mount_path_for_guest_path_from_mounts(&vm.configuration.mounts, guest_path)
8346            .is_some()
8347}
8348
8349fn resolve_path_like_guest_specifier(cwd: &str, specifier: &str) -> String {
8350    if specifier.starts_with("file://") {
8351        normalize_path(specifier.trim_start_matches("file://"))
8352    } else if specifier.starts_with("file:") {
8353        normalize_path(specifier.trim_start_matches("file:"))
8354    } else if specifier.starts_with('/') {
8355        normalize_path(specifier)
8356    } else {
8357        normalize_path(&format!("{cwd}/{specifier}"))
8358    }
8359}
8360
8361fn guest_entrypoint_for_specifier(cwd: &str, specifier: &str) -> Option<String> {
8362    is_path_like_specifier(specifier).then(|| resolve_path_like_guest_specifier(cwd, specifier))
8363}
8364
8365fn is_node_runtime_command(command: &str) -> bool {
8366    matches!(command, "node" | "npm" | "npx")
8367        || Path::new(command)
8368            .file_name()
8369            .and_then(|name| name.to_str())
8370            .is_some_and(|name| matches!(name, "node" | "npm" | "npx"))
8371}
8372
8373fn resolve_special_node_cli_invocation(
8374    args: &[String],
8375    env: &mut BTreeMap<String, String>,
8376) -> Option<(String, Vec<String>)> {
8377    let first = args.first()?;
8378    match first.as_str() {
8379        "-e" | "--eval" => {
8380            env.insert(
8381                String::from("AGENTOS_NODE_EVAL"),
8382                args.get(1).cloned().unwrap_or_default(),
8383            );
8384            Some((first.clone(), args.iter().skip(2).cloned().collect()))
8385        }
8386        "-v" | "--version" => {
8387            env.insert(
8388                String::from("AGENTOS_NODE_EVAL"),
8389                String::from("console.log(process.version);"),
8390            );
8391            Some((String::from("-e"), args.to_vec()))
8392        }
8393        _ => None,
8394    }
8395}
8396
8397fn node_runtime_command_name(command: &str) -> Option<&str> {
8398    let name = Path::new(command)
8399        .file_name()
8400        .and_then(|name| name.to_str())?;
8401    matches!(name, "node" | "npm" | "npx").then_some(name)
8402}
8403
8404struct ResolvedHostNodeCliEntrypoint {
8405    command_name: String,
8406    guest_root: String,
8407    guest_entrypoint: String,
8408    package_root: PathBuf,
8409}
8410
8411fn resolve_host_node_cli_entrypoint(command: &str) -> Option<ResolvedHostNodeCliEntrypoint> {
8412    let command_name = node_runtime_command_name(command)?;
8413    if !matches!(command_name, "npm" | "npx") {
8414        return None;
8415    }
8416
8417    let path = std::env::var_os("PATH")?;
8418    for root in std::env::split_paths(&path) {
8419        let candidate = root.join(command_name);
8420        if !candidate.is_file() {
8421            continue;
8422        }
8423        let entrypoint = candidate.canonicalize().ok().unwrap_or(candidate);
8424        let package_root = entrypoint.parent()?.parent()?.to_path_buf();
8425        let guest_root = format!("/__secure_exec/node-runtime/{command_name}");
8426        let relative_entrypoint = entrypoint.strip_prefix(&package_root).ok()?;
8427        let guest_entrypoint = normalize_path(&format!(
8428            "{guest_root}/{}",
8429            relative_entrypoint.to_string_lossy().replace('\\', "/")
8430        ));
8431        return Some(ResolvedHostNodeCliEntrypoint {
8432            command_name: command_name.to_owned(),
8433            guest_root,
8434            guest_entrypoint,
8435            package_root,
8436        });
8437    }
8438
8439    None
8440}
8441
8442fn build_host_node_cli_eval(cli: &ResolvedHostNodeCliEntrypoint) -> String {
8443    let guest_npm_main = normalize_path(&format!("{}/lib/npm.js", cli.guest_root));
8444    let guest_npm_cli = normalize_path(&format!("{}/bin/npm-cli.js", cli.guest_root));
8445    let guest_package_json = normalize_path(&format!("{}/package.json", cli.guest_root));
8446    let guest_display_module = normalize_path(&format!("{}/lib/utils/display.js", cli.guest_root));
8447    let guest_log_file_module =
8448        normalize_path(&format!("{}/lib/utils/log-file.js", cli.guest_root));
8449    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); } } }";
8450    let display_stub = format!(
8451        "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 }};",
8452        display_module = serde_json::to_string(&guest_display_module)
8453            .unwrap_or_else(|_| format!("\"{guest_display_module}\"")),
8454        log_file_module = serde_json::to_string(&guest_log_file_module)
8455            .unwrap_or_else(|_| format!("\"{guest_log_file_module}\"")),
8456    );
8457    let registry_fetch_stub = "const { createRequire: __agentOSCreateRequire } = require('module'); const __agentOSNpmRequire = __agentOSCreateRequire(require.resolve(__AGENTOS_NPM_MAIN__)); try { const __agentOSMinipassFetchPath = __agentOSNpmRequire.resolve('minipass-fetch'); const __agentOSMinipassFetch = __agentOSNpmRequire(__agentOSMinipassFetchPath); const { FetchError: __agentOSFetchError, Headers: __agentOSFetchHeaders, Request: __agentOSFetchRequest, Response: __agentOSFetchResponse, AbortError: __agentOSAbortError } = __agentOSMinipassFetch; const { Minipass: __agentOSMinipass } = __agentOSNpmRequire('minipass'); const __agentOSCreateBinaryMinipass = () => new __agentOSMinipass({ objectMode: false, encoding: null }); const __agentOSCloneBuffer = (buffer) => Buffer.isBuffer(buffer) ? Buffer.from(buffer) : Buffer.from(buffer ?? []); const __agentOSBufferToArrayBuffer = (buffer) => { const bytes = __agentOSCloneBuffer(buffer); return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); }; const __agentOSAttachBufferedBodyMethods = (response, responseBuffer) => { const __agentOSReadBuffer = async () => __agentOSCloneBuffer(responseBuffer); response.__agentOSBufferedBody = __agentOSCloneBuffer(responseBuffer); response.buffer = __agentOSReadBuffer; response.text = async () => (await __agentOSReadBuffer()).toString('utf8'); response.json = async () => JSON.parse(await response.text()); response.arrayBuffer = async () => __agentOSBufferToArrayBuffer(await __agentOSReadBuffer()); response.clone = () => { const clonedBody = __agentOSCreateBinaryMinipass(); const clonedBuffer = __agentOSCloneBuffer(responseBuffer); clonedBody.end(clonedBuffer); const clonedResponse = new __agentOSFetchResponse(clonedBody, { url: response.url, status: response.status, statusText: response.statusText, headers: response.headers, size: response.size, timeout: response.timeout, counter: response.counter, trailer: response.trailer }); return __agentOSAttachBufferedBodyMethods(clonedResponse, clonedBuffer); }; return response; }; const __agentOSNormalizeHeaders = (__agentOSHeaders) => { const normalized = {}; __agentOSHeaders.forEach((value, key) => { if (normalized[key] === undefined) { normalized[key] = value; return; } if (Array.isArray(normalized[key])) { normalized[key].push(value); return; } normalized[key] = [normalized[key], value]; }); return normalized; }; const __agentOSPatchedMinipassFetch = async (input, opts = {}) => { const request = input instanceof __agentOSFetchRequest ? input : new __agentOSFetchRequest(input, opts); const __agentOSController = !request.signal && typeof AbortController === 'function' ? new AbortController() : null; const __agentOSSignal = request.signal ?? __agentOSController?.signal; let __agentOSTimer = null; if (__agentOSController && Number.isFinite(request.timeout) && request.timeout > 0) { __agentOSTimer = setTimeout(() => __agentOSController.abort(new Error(`network timeout at: ${request.url}`)), request.timeout); __agentOSTimer.unref?.(); } try { const requestHeaders = {}; request.headers.forEach((value, key) => { requestHeaders[key] = value; }); const response = await fetch(request.url, { method: request.method, headers: requestHeaders, body: request.body ?? undefined, redirect: request.redirect ?? opts.redirect ?? 'follow', signal: __agentOSSignal, ...(request.body ? { duplex: 'half' } : {}) }); const responseBody = __agentOSCreateBinaryMinipass(); const contentType = String(response.headers.get('content-type') || '').toLowerCase(); const responseBuffer = contentType.includes('json') ? Buffer.from(JSON.stringify(await response.json())) : contentType.startsWith('text/') ? Buffer.from(await response.text()) : Buffer.from(await response.arrayBuffer()); responseBody.end(responseBuffer); return __agentOSAttachBufferedBodyMethods(new __agentOSFetchResponse(responseBody, { url: response.url, status: response.status, statusText: response.statusText, headers: __agentOSNormalizeHeaders(response.headers), size: request.size, timeout: request.timeout, counter: request.counter ?? opts.counter ?? 0, trailer: Promise.resolve(new __agentOSFetchHeaders()) }), responseBuffer); } catch (error) { if (error instanceof Error) { throw error; } throw new __agentOSFetchError(String(error), 'system', error); } finally { if (__agentOSTimer) { clearTimeout(__agentOSTimer); } } }; globalThis.__agentOSPatchedMinipassFetch = __agentOSPatchedMinipassFetch; __agentOSPatchedMinipassFetch.isRedirect = typeof __agentOSMinipassFetch.isRedirect === 'function' ? __agentOSMinipassFetch.isRedirect.bind(__agentOSMinipassFetch) : (code) => code === 301 || code === 302 || code === 303 || code === 307 || code === 308; __agentOSPatchedMinipassFetch.FetchError = __agentOSFetchError; __agentOSPatchedMinipassFetch.Headers = __agentOSFetchHeaders; __agentOSPatchedMinipassFetch.Request = __agentOSFetchRequest; __agentOSPatchedMinipassFetch.Response = __agentOSFetchResponse; __agentOSPatchedMinipassFetch.AbortError = __agentOSAbortError; globalThis._moduleCache[__agentOSMinipassFetchPath] = { exports: __agentOSPatchedMinipassFetch }; __agentOSDebugLog('patched-minipass-fetch', __agentOSMinipassFetchPath); const __agentOSCheckResponsePath = __agentOSNpmRequire.resolve('npm-registry-fetch/lib/check-response.js'); const __agentOSCheckResponse = __agentOSNpmRequire(__agentOSCheckResponsePath); const __agentOSEnsureResponseBodyStream = (response) => { if (!response || (response.body && typeof response.body.on === 'function')) { return response; } const body = __agentOSCreateBinaryMinipass(); const finishWithError = (error) => body.emit('error', error instanceof Error ? error : new Error(String(error))); try { if (typeof response.buffer === 'function') { Promise.resolve(response.buffer()).then((buffer) => body.end(buffer), finishWithError); } else if (Buffer.isBuffer(response.body) || typeof response.body === 'string') { body.end(response.body); } else if (response.body && typeof response.body[Symbol.asyncIterator] === 'function') { (async () => { try { for await (const chunk of response.body) { body.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); } body.end(); } catch (error) { finishWithError(error); body.end(); } })(); } else { body.end(); } } catch (error) { finishWithError(error); body.end(); } return new __agentOSFetchResponse(body, response); }; globalThis._moduleCache[__agentOSCheckResponsePath] = { exports: (payload) => { const normalized = { ...payload, res: __agentOSEnsureResponseBodyStream(payload.res) }; __agentOSDebugLog('check-response-body', normalized.res && normalized.res.status, typeof (normalized.res && normalized.res.body), normalized.res && normalized.res.body && typeof normalized.res.body.on, normalized.res && normalized.res.body && normalized.res.body.constructor && normalized.res.body.constructor.name, !!(normalized.res && normalized.res.__agentOSBufferedBody), normalized.res && typeof normalized.res.json); return __agentOSCheckResponse(normalized); } }; __agentOSDebugLog('patched-check-response', __agentOSCheckResponsePath); } catch (error) { __agentOSDebugLog('patch-minipass-fetch-failed', error && error.stack ? error.stack : String(error)); } try { const __agentOSRegistryFetchPath = __agentOSNpmRequire.resolve('npm-registry-fetch'); const __agentOSRegistryFetch = __agentOSNpmRequire(__agentOSRegistryFetchPath); const __agentOSWrapRegistryFetch = (fn) => { const wrapResult = (promise) => Promise.resolve(promise).then((res) => { __agentOSDebugLog('registry-fetch-result', res && res.status, typeof (res && res.body), res && res.body && typeof res.body.on, res && res.body && res.body.constructor && res.body.constructor.name, !!(res && res.__agentOSBufferedBody), res && typeof res.json); return res; }); const wrapped = (uri, opts = {}) => wrapResult(globalThis.__agentOSPatchedMinipassFetch(uri, { method: opts.method, headers: opts.headers, body: opts.body, redirect: opts.redirect, signal: opts.signal, timeout: opts.timeout, size: opts.size, counter: opts.counter })); if (typeof fn.json === 'function') { wrapped.json = (uri, opts = {}) => wrapped(uri, opts).then((res) => res.json()); } if (fn.json && typeof fn.json.stream === 'function') { wrapped.json = wrapped.json || {}; wrapped.json.stream = (uri, path, opts = {}) => fn.json.stream(uri, path, { ...opts, agent: false }); } if (typeof fn.pickRegistry === 'function') { wrapped.pickRegistry = fn.pickRegistry.bind(fn); } if (typeof fn.getAuth === 'function') { wrapped.getAuth = fn.getAuth.bind(fn); } return wrapped; }; globalThis._moduleCache[__agentOSRegistryFetchPath] = { exports: __agentOSWrapRegistryFetch(__agentOSRegistryFetch) }; __agentOSDebugLog('patched-npm-registry-fetch', __agentOSRegistryFetchPath); } catch (error) { __agentOSDebugLog('patch-npm-registry-fetch-failed', error && error.stack ? error.stack : String(error)); }";
8458    match cli.command_name.as_str() {
8459        "npx" => format!(
8460            "{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); }});",
8461            debug_preamble = debug_preamble,
8462            display_stub = display_stub,
8463            registry_fetch_stub = registry_fetch_stub.replace(
8464                "__AGENTOS_NPM_MAIN__",
8465                &serde_json::to_string(&guest_npm_main)
8466                    .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8467            ),
8468            npm_main = serde_json::to_string(&guest_npm_main)
8469                .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8470            npm_cli = serde_json::to_string(&guest_npm_cli)
8471                .unwrap_or_else(|_| format!("\"{guest_npm_cli}\"")),
8472            package_json = serde_json::to_string(&guest_package_json)
8473                .unwrap_or_else(|_| format!("\"{guest_package_json}\"")),
8474        ),
8475        _ => format!(
8476            "{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); }});",
8477            debug_preamble = debug_preamble,
8478            display_stub = display_stub,
8479            registry_fetch_stub = registry_fetch_stub.replace(
8480                "__AGENTOS_NPM_MAIN__",
8481                &serde_json::to_string(&guest_npm_main)
8482                    .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8483            ),
8484            npm_main = serde_json::to_string(&guest_npm_main)
8485                .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8486            package_json = serde_json::to_string(&guest_package_json)
8487                .unwrap_or_else(|_| format!("\"{guest_package_json}\"")),
8488        ),
8489    }
8490}
8491
8492fn resolve_guest_command_entrypoint(
8493    vm: &VmState,
8494    guest_cwd: &str,
8495    command: &str,
8496    path_env: Option<&str>,
8497) -> Option<String> {
8498    if !is_path_like_specifier(command) {
8499        if let Some(entrypoint) = vm.command_guest_paths.get(command) {
8500            return Some(entrypoint.clone());
8501        }
8502
8503        for search_dir in guest_command_search_dirs(vm, guest_cwd, path_env) {
8504            let candidate = normalize_path(&format!("{search_dir}/{command}"));
8505            if let Some(entrypoint) = resolve_guest_command_path_candidate(vm, &candidate) {
8506                return Some(entrypoint);
8507            }
8508        }
8509
8510        return None;
8511    }
8512
8513    let normalized = resolve_path_like_guest_specifier(guest_cwd, command);
8514    resolve_guest_command_path_candidate(vm, &normalized).or_else(|| {
8515        // Some guest shells materialize PATH lookups into absolute candidate paths.
8516        // If that path points into a searched directory but does not exist, fall
8517        // back to the command basename so the sidecar can remap VM command packages.
8518        let parent_dir = Path::new(&normalized).parent()?.to_str()?;
8519        if !guest_command_search_dirs(vm, guest_cwd, path_env)
8520            .iter()
8521            .any(|search_dir| normalize_path(search_dir) == normalize_path(parent_dir))
8522        {
8523            return None;
8524        }
8525
8526        let file_name = Path::new(&normalized).file_name()?.to_str()?;
8527        vm.command_guest_paths.get(file_name).cloned()
8528    })
8529}
8530
8531fn guest_command_search_dirs(vm: &VmState, guest_cwd: &str, path_env: Option<&str>) -> Vec<String> {
8532    let mut search_dirs = Vec::new();
8533    let mut seen = BTreeSet::new();
8534
8535    if let Some(path) = path_env.or_else(|| vm.guest_env.get("PATH").map(String::as_str)) {
8536        for segment in path.split(':') {
8537            let trimmed = segment.trim();
8538            if trimmed.is_empty() {
8539                continue;
8540            }
8541            let normalized = if trimmed.starts_with('/') {
8542                normalize_path(trimmed)
8543            } else {
8544                normalize_path(&format!("{guest_cwd}/{trimmed}"))
8545            };
8546            if seen.insert(normalized.clone()) {
8547                search_dirs.push(normalized);
8548            }
8549        }
8550    }
8551
8552    for fallback in ["/bin", "/usr/bin", "/usr/local/bin"] {
8553        let normalized = String::from(fallback);
8554        if seen.insert(normalized.clone()) {
8555            search_dirs.push(normalized);
8556        }
8557    }
8558
8559    search_dirs
8560}
8561
8562fn resolve_guest_command_path_candidate(vm: &VmState, candidate: &str) -> Option<String> {
8563    if candidate.starts_with("/bin/")
8564        || candidate.starts_with("/usr/bin/")
8565        || candidate.starts_with("/usr/local/bin/")
8566        || candidate.starts_with("/__secure_exec/commands/")
8567    {
8568        if let Some(file_name) = Path::new(candidate)
8569            .file_name()
8570            .and_then(|name| name.to_str())
8571        {
8572            if let Some(guest_entrypoint) = vm.command_guest_paths.get(file_name) {
8573                return Some(guest_entrypoint.clone());
8574            }
8575        }
8576    }
8577
8578    if vm
8579        .kernel
8580        .exists(candidate)
8581        .ok()
8582        .is_some_and(|exists| exists)
8583    {
8584        return Some(normalize_path(candidate));
8585    }
8586
8587    resolve_vm_guest_path_to_host(vm, candidate)
8588        .is_file()
8589        .then(|| normalize_path(candidate))
8590}
8591
8592fn resolve_host_entrypoint_within_vm_host_cwd(
8593    vm: &VmState,
8594    specifier: &str,
8595) -> Option<(String, String)> {
8596    let candidate = Path::new(specifier);
8597    if !candidate.is_absolute() {
8598        return None;
8599    }
8600
8601    let normalized_entrypoint = normalize_host_path(candidate);
8602    let normalized_host_cwd = normalize_host_path(&vm.host_cwd);
8603    if !path_is_within_root(&normalized_entrypoint, &normalized_host_cwd) {
8604        return None;
8605    }
8606
8607    let relative = normalized_entrypoint
8608        .strip_prefix(&normalized_host_cwd)
8609        .ok()?
8610        .to_string_lossy()
8611        .replace('\\', "/");
8612    let guest_entrypoint = if relative.is_empty() {
8613        String::from("/")
8614    } else {
8615        normalize_path(&format!("/{relative}"))
8616    };
8617    Some((
8618        guest_entrypoint,
8619        normalized_entrypoint.to_string_lossy().into_owned(),
8620    ))
8621}
8622
8623fn prepare_guest_runtime_env(
8624    vm: &VmState,
8625    env: &mut BTreeMap<String, String>,
8626    guest_cwd: &str,
8627    host_cwd: &Path,
8628    guest_entrypoint: Option<String>,
8629) -> Result<(), SidecarError> {
8630    let user = vm.kernel.user_profile();
8631    let path_mappings = runtime_guest_path_mappings(vm);
8632    let read_paths = expand_host_access_paths(
8633        std::iter::once(vm.cwd.clone())
8634            .chain(
8635                path_mappings
8636                    .iter()
8637                    .map(|mapping| PathBuf::from(&mapping.host_path)),
8638            )
8639            .chain(std::iter::once(host_cwd.to_path_buf()))
8640            .collect::<Vec<_>>()
8641            .as_slice(),
8642    );
8643    let write_paths = dedupe_host_paths(
8644        std::iter::once(vm.cwd.clone())
8645            .chain(std::iter::once(host_cwd.to_path_buf()))
8646            .chain(runtime_guest_writable_host_paths(vm))
8647            .collect::<Vec<_>>()
8648            .as_slice(),
8649    );
8650    let allowed_node_builtins = configured_allowed_node_builtins(vm);
8651    let loopback_exempt_ports = configured_loopback_exempt_ports(vm);
8652
8653    env.insert(
8654        String::from("AGENTOS_GUEST_PATH_MAPPINGS"),
8655        serde_json::to_string(&path_mappings).map_err(|error| {
8656            SidecarError::InvalidState(format!("failed to encode guest path mappings: {error}"))
8657        })?,
8658    );
8659    env.entry(String::from(EXECUTION_SANDBOX_ROOT_ENV))
8660        .or_insert_with(|| normalize_host_path(&vm.cwd).to_string_lossy().into_owned());
8661    env.insert(
8662        String::from("AGENTOS_EXTRA_FS_READ_PATHS"),
8663        serde_json::to_string(
8664            &read_paths
8665                .iter()
8666                .map(|path| path.to_string_lossy().into_owned())
8667                .collect::<Vec<_>>(),
8668        )
8669        .map_err(|error| {
8670            SidecarError::InvalidState(format!("failed to encode read paths: {error}"))
8671        })?,
8672    );
8673    env.insert(
8674        String::from("AGENTOS_EXTRA_FS_WRITE_PATHS"),
8675        serde_json::to_string(
8676            &write_paths
8677                .iter()
8678                .map(|path| path.to_string_lossy().into_owned())
8679                .collect::<Vec<_>>(),
8680        )
8681        .map_err(|error| {
8682            SidecarError::InvalidState(format!("failed to encode write paths: {error}"))
8683        })?,
8684    );
8685    env.insert(
8686        String::from("AGENTOS_ALLOWED_NODE_BUILTINS"),
8687        serde_json::to_string(&allowed_node_builtins).map_err(|error| {
8688            SidecarError::InvalidState(format!("failed to encode allowed builtins: {error}"))
8689        })?,
8690    );
8691    // The guest JS host platform drives subtractive global scrubbing in the
8692    // per-execution runtime shim (see prepend_v8_runtime_shim).
8693    env.insert(
8694        String::from("AGENTOS_JS_PLATFORM"),
8695        js_runtime_platform_env(vm).to_owned(),
8696    );
8697    // Module-resolution mode (omitted when full Node resolution / the default).
8698    if let Some(resolution) = js_runtime_module_resolution_env(vm) {
8699        env.insert(
8700            String::from("AGENTOS_JS_MODULE_RESOLUTION"),
8701            resolution.to_owned(),
8702        );
8703    }
8704    // Builtin allow-list gate for the live resolver. Present only when builtins
8705    // should be restricted (non-node platform => deny all; node + explicit
8706    // allow-list => exactly those). Absent => unrestricted (node default).
8707    if let Some(allowlist) = js_runtime_enforced_builtins(vm) {
8708        env.insert(
8709            String::from("AGENTOS_JS_BUILTIN_ALLOWLIST"),
8710            serde_json::to_string(&allowlist).map_err(|error| {
8711                SidecarError::InvalidState(format!(
8712                    "failed to encode jsRuntime builtin allow-list: {error}"
8713                ))
8714            })?,
8715        );
8716    }
8717    // Virtual OS identity (os.cpus/totalmem/freemem/homedir/userInfo/...) now
8718    // rides the typed `guest_runtime` (see `guest_runtime_identity`), exposed to
8719    // the guest as the `__agentOSVirtualOs` structured global by the runtime
8720    // shim — no longer the `AGENTOS_VIRTUAL_OS_*` env vars.
8721    // Virtual process uid/gid now ride the typed `guest_runtime` identity
8722    // (see `guest_runtime_identity`), not the `AGENTOS_VIRTUAL_PROCESS_*` env.
8723    env.entry(String::from("HOME"))
8724        .or_insert_with(|| user.homedir.clone());
8725    env.entry(String::from("USER"))
8726        .or_insert_with(|| user.username.clone());
8727    env.entry(String::from("LOGNAME"))
8728        .or_insert_with(|| user.username.clone());
8729    env.entry(String::from("SHELL"))
8730        .or_insert_with(|| user.shell.clone());
8731    env.entry(String::from("PATH")).or_insert_with(|| {
8732        vm.guest_env
8733            .get("PATH")
8734            .cloned()
8735            .unwrap_or_else(|| crate::vm::DEFAULT_GUEST_PATH_ENV.to_owned())
8736    });
8737    env.entry(String::from("TMPDIR"))
8738        .or_insert_with(|| String::from("/tmp"));
8739    env.insert(String::from("PWD"), guest_cwd.to_owned());
8740    if !loopback_exempt_ports.is_empty() {
8741        env.insert(
8742            String::from(LOOPBACK_EXEMPT_PORTS_ENV),
8743            serde_json::to_string(&loopback_exempt_ports).map_err(|error| {
8744                SidecarError::InvalidState(format!("failed to encode loopback exemptions: {error}"))
8745            })?,
8746        );
8747    }
8748    if let Some(guest_entrypoint) = guest_entrypoint {
8749        env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint);
8750    }
8751    Ok(())
8752}
8753
8754fn virtual_os_cpu_count(resource_limits: &ResourceLimits) -> usize {
8755    resource_limits.virtual_cpu_count.unwrap_or(1).max(1)
8756}
8757
8758fn virtual_os_totalmem_bytes(resource_limits: &ResourceLimits) -> u64 {
8759    resource_limits
8760        .max_wasm_memory_bytes
8761        .unwrap_or(1024 * 1024 * 1024)
8762}
8763
8764fn virtual_os_freemem_bytes(resource_limits: &ResourceLimits) -> u64 {
8765    resource_limits
8766        .max_wasm_memory_bytes
8767        .unwrap_or(512 * 1024 * 1024)
8768}
8769
8770/// Build the typed per-execution JavaScript limits from the per-VM `VmLimits`
8771/// (sourced from `CreateVmConfig` on the BARE wire). These ride the execution
8772/// request, not `AGENTOS_*` env vars — see the env-vs-wire rule in
8773/// `crates/sidecar/CLAUDE.md`.
8774fn javascript_execution_limits(vm: &VmState) -> JavascriptExecutionLimits {
8775    JavascriptExecutionLimits {
8776        v8_heap_limit_mb: vm.limits.js_runtime.v8_heap_limit_mb,
8777        sync_rpc_wait_timeout_ms: vm.limits.js_runtime.sync_rpc_wait_timeout_ms,
8778    }
8779}
8780
8781/// Build the typed per-execution guest-runtime identity (virtual `process.*`)
8782/// from kernel state. Replaces the `AGENTOS_VIRTUAL_PROCESS_{UID,GID,PID,PPID}`
8783/// env round-trip: the runtime shim reads these from `guest_runtime`, not env.
8784/// `uid`/`gid` come from the VM user profile (applied to every guest);
8785/// `pid`/`ppid` are per-process and only set for paths that assigned them.
8786fn guest_runtime_identity(
8787    vm: &VmState,
8788    virtual_pid: Option<u64>,
8789    virtual_ppid: Option<u64>,
8790) -> GuestRuntimeConfig {
8791    let user = vm.kernel.user_profile();
8792    let resource_limits = vm.kernel.resource_limits();
8793    GuestRuntimeConfig {
8794        virtual_uid: Some(u64::from(user.uid)),
8795        virtual_gid: Some(u64::from(user.gid)),
8796        virtual_pid,
8797        virtual_ppid,
8798        virtual_exec_path: None,
8799        os_cpu_count: Some(virtual_os_cpu_count(resource_limits) as u64),
8800        os_totalmem: Some(virtual_os_totalmem_bytes(resource_limits)),
8801        os_freemem: Some(virtual_os_freemem_bytes(resource_limits)),
8802        os_homedir: Some(user.homedir.clone()),
8803        os_hostname: None,
8804        os_shell: Some(user.shell.clone()),
8805        os_user: Some(user.username.clone()),
8806    }
8807}
8808
8809/// The guest's virtual home directory, sourced from the VM user profile (the
8810/// same value carried to the guest as `os.homedir()` via `guest_runtime`). Used
8811/// by sidecar-internal `~`-path resolution; falls back to `/root` for a
8812/// non-absolute profile value.
8813fn guest_virtual_home(vm: &VmState) -> String {
8814    let homedir = vm.kernel.user_profile().homedir;
8815    if homedir.starts_with('/') {
8816        homedir
8817    } else {
8818        String::from("/root")
8819    }
8820}
8821
8822/// Build the typed per-execution Python limits from the per-VM `VmLimits`.
8823fn python_execution_limits(vm: &VmState) -> PythonExecutionLimits {
8824    PythonExecutionLimits {
8825        output_buffer_max_bytes: Some(vm.limits.python.output_buffer_max_bytes),
8826        execution_timeout_ms: Some(vm.limits.python.execution_timeout_ms),
8827        max_old_space_mb: Some(vm.limits.python.max_old_space_mb),
8828        vfs_rpc_timeout_ms: Some(vm.limits.python.vfs_rpc_timeout_ms),
8829    }
8830}
8831
8832/// Build the typed per-execution WebAssembly limits from the per-VM kernel
8833/// `ResourceLimits`. Replaces the old `apply_wasm_limit_env` env round-trip;
8834/// notably this is the path that finally enforces the stack cap that the
8835/// `AGENTOS_WASM_MAX_STACK_BYTES` env knob set but no reader consumed.
8836fn wasm_execution_limits(vm: &VmState) -> WasmExecutionLimits {
8837    let resource_limits = vm.kernel.resource_limits();
8838    WasmExecutionLimits {
8839        max_fuel: resource_limits.max_wasm_fuel,
8840        max_memory_bytes: resource_limits.max_wasm_memory_bytes,
8841        max_stack_bytes: resource_limits
8842            .max_wasm_stack_bytes
8843            .map(|value| value as u64),
8844    }
8845}
8846
8847/// The guest JavaScript host platform configured for this VM, defaulting to
8848/// full Node.js emulation when no `jsRuntime` config was supplied at create.
8849fn js_runtime_platform(vm: &VmState) -> vm_config::JsRuntimePlatform {
8850    vm.configuration
8851        .js_runtime
8852        .as_ref()
8853        .map(|cfg| cfg.platform)
8854        .unwrap_or(vm_config::JsRuntimePlatform::Node)
8855}
8856
8857/// Lowercase wire name for the configured platform, mirroring the serde
8858/// representation of `vm_config::JsRuntimePlatform`.
8859fn js_runtime_platform_env(vm: &VmState) -> &'static str {
8860    match js_runtime_platform(vm) {
8861        vm_config::JsRuntimePlatform::Node => "node",
8862        vm_config::JsRuntimePlatform::Browser => "browser",
8863        vm_config::JsRuntimePlatform::Neutral => "neutral",
8864        vm_config::JsRuntimePlatform::Bare => "bare",
8865    }
8866}
8867
8868/// Wire name for the configured module-resolution mode, or `None` when it is the
8869/// full-Node default (which the live resolver also assumes when the env is unset).
8870fn js_runtime_module_resolution_env(vm: &VmState) -> Option<&'static str> {
8871    let resolution = vm
8872        .configuration
8873        .js_runtime
8874        .as_ref()
8875        .map(|cfg| cfg.module_resolution)
8876        .unwrap_or(vm_config::JsModuleResolution::Node);
8877    match resolution {
8878        vm_config::JsModuleResolution::Node => None,
8879        vm_config::JsModuleResolution::Relative => Some("relative"),
8880        vm_config::JsModuleResolution::None => Some("none"),
8881    }
8882}
8883
8884/// The builtin allow-list the live resolver should enforce, or `None` to leave
8885/// builtins unrestricted (full Node default — preserving today's behavior).
8886/// Non-node platforms enforce an empty list (deny all builtins).
8887fn js_runtime_enforced_builtins(vm: &VmState) -> Option<Vec<String>> {
8888    if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node {
8889        return Some(Vec::new());
8890    }
8891    vm.configuration
8892        .js_runtime
8893        .as_ref()
8894        .and_then(|cfg| cfg.allowed_builtins.clone())
8895}
8896
8897fn configured_allowed_node_builtins(vm: &VmState) -> Vec<String> {
8898    // Non-node platforms expose no Node builtin modules at all.
8899    if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node {
8900        return Vec::new();
8901    }
8902    // Under the node platform an explicit allow-list wins — including an explicit
8903    // empty list, which means deny all. Absence falls back to the engine default.
8904    let configured = match vm
8905        .configuration
8906        .js_runtime
8907        .as_ref()
8908        .and_then(|cfg| cfg.allowed_builtins.as_ref())
8909    {
8910        Some(list) => list.clone(),
8911        None => DEFAULT_ALLOWED_NODE_BUILTINS
8912            .iter()
8913            .map(|value| (*value).to_owned())
8914            .collect::<Vec<_>>(),
8915    };
8916    dedupe_strings(&configured)
8917}
8918
8919fn configured_loopback_exempt_ports(vm: &VmState) -> Vec<String> {
8920    if !vm.configuration.loopback_exempt_ports.is_empty() {
8921        return vm
8922            .configuration
8923            .loopback_exempt_ports
8924            .iter()
8925            .map(ToString::to_string)
8926            .collect();
8927    }
8928
8929    vm.create_loopback_exempt_ports
8930        .iter()
8931        .map(ToString::to_string)
8932        .collect()
8933}
8934
8935/// Extract the `hostPath` string from a mount plugin's JSON-encoded config.
8936fn mount_config_host_path(config: &str) -> Option<String> {
8937    serde_json::from_str::<Value>(config)
8938        .ok()?
8939        .get("hostPath")
8940        .and_then(Value::as_str)
8941        .map(str::to_owned)
8942}
8943
8944fn runtime_guest_writable_host_paths(vm: &VmState) -> Vec<PathBuf> {
8945    vm.configuration
8946        .mounts
8947        .iter()
8948        .filter(|mount| !mount.read_only)
8949        .filter_map(|mount| {
8950            ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
8951                .then(|| mount_config_host_path(&mount.plugin.config))
8952                .flatten()
8953                .map(PathBuf::from)
8954        })
8955        .collect()
8956}
8957
8958fn runtime_guest_path_mappings(vm: &VmState) -> Vec<RuntimeGuestPathMapping> {
8959    let mut mappings = vm
8960        .configuration
8961        .mounts
8962        .iter()
8963        .filter_map(|mount| {
8964            ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
8965                .then(|| {
8966                    mount_config_host_path(&mount.plugin.config).map(|host_path| {
8967                        RuntimeGuestPathMapping {
8968                            guest_path: normalize_path(&mount.guest_path),
8969                            host_path,
8970                            read_only: mount.read_only,
8971                        }
8972                    })
8973                })
8974                .flatten()
8975        })
8976        .collect::<Vec<_>>();
8977    let mut command_root_mappings = vm
8978        .command_guest_paths
8979        .values()
8980        .filter_map(|guest_path| {
8981            Path::new(guest_path)
8982                .parent()
8983                .and_then(|parent| parent.to_str())
8984                .map(normalize_path)
8985        })
8986        .collect::<BTreeSet<_>>()
8987        .into_iter()
8988        .map(|guest_path| RuntimeGuestPathMapping {
8989            host_path: resolve_vm_guest_path_to_host(vm, &guest_path)
8990                .to_string_lossy()
8991                .into_owned(),
8992            guest_path,
8993            read_only: false,
8994        })
8995        .collect::<Vec<_>>();
8996    mappings.append(&mut command_root_mappings);
8997    let mut extra_node_modules_roots = mappings
8998        .iter()
8999        .filter(|mapping| mapping.guest_path.starts_with("/root/node_modules/"))
9000        .filter_map(|mapping| {
9001            host_node_modules_root(Path::new(&mapping.host_path)).map(|host_root| {
9002                RuntimeGuestPathMapping {
9003                    guest_path: String::from("/root/node_modules"),
9004                    host_path: host_root.to_string_lossy().into_owned(),
9005                    read_only: mapping.read_only,
9006                }
9007            })
9008        })
9009        .collect::<Vec<_>>();
9010    mappings.append(&mut extra_node_modules_roots);
9011    mappings.push(RuntimeGuestPathMapping {
9012        guest_path: String::from("/"),
9013        host_path: vm.cwd.to_string_lossy().into_owned(),
9014        read_only: false,
9015    });
9016    mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.guest_path.len()));
9017    mappings.dedup_by(|left, right| {
9018        left.guest_path == right.guest_path && left.host_path == right.host_path
9019    });
9020    mappings
9021}
9022
9023/// Build a `Send`-able, read-only VFS module reader over the VM's read-only
9024/// `host_dir`/`module_access` mounts (and the derived `/root/node_modules` root
9025/// for nested mounts). When present, the V8 bridge thread resolves modules
9026/// inline against this reader — concurrently with the service loop — so a large
9027/// cold-start module graph never serializes behind / starves an in-flight ACP
9028/// `session/new` bootstrap on the single service-loop thread. The reader reads
9029/// the same mounted tree the guest sees (anchored `openat2`, escaping-symlink
9030/// refusal), never the host-direct path translator. Returns `None` when the VM
9031/// has no usable read-only mount, so resolution falls back to the service-loop
9032/// kernel reader.
9033fn build_module_reader(
9034    vm: &VmState,
9035    resolved: &ResolvedChildProcessExecution,
9036) -> Option<crate::plugins::host_dir::HostDirModuleReader> {
9037    let mut pairs: Vec<(String, PathBuf)> = vm
9038        .configuration
9039        .mounts
9040        .iter()
9041        .filter(|mount| mount.read_only)
9042        .filter(|mount| (mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
9043        .filter_map(|mount| {
9044            mount_config_host_path(&mount.plugin.config)
9045                .map(|host_path| (normalize_path(&mount.guest_path), PathBuf::from(host_path)))
9046        })
9047        .collect();
9048
9049    let guest_entrypoint = resolved
9050        .env
9051        .get("AGENTOS_GUEST_ENTRYPOINT")
9052        .map(|path| normalize_path(path));
9053    if let Some(guest_entrypoint) = guest_entrypoint.as_deref() {
9054        let entrypoint_in_read_only_mount = pairs.iter().any(|(guest_path, _)| {
9055            guest_entrypoint == guest_path
9056                || guest_entrypoint.starts_with(&format!("{guest_path}/"))
9057        });
9058        if !entrypoint_in_read_only_mount {
9059            return None;
9060        }
9061    }
9062
9063    // Mirror runtime_guest_path_mappings: a mount nested under
9064    // `/root/node_modules/<pkg>` implies a `/root/node_modules` root the resolver
9065    // walks, so expose that root too (e.g. software-package mounts).
9066    let extra_roots: Vec<(String, PathBuf)> = pairs
9067        .iter()
9068        .filter(|(guest_path, _)| guest_path.starts_with("/root/node_modules/"))
9069        .filter_map(|(_, host_path)| {
9070            host_node_modules_root(host_path).map(|root| (String::from("/root/node_modules"), root))
9071        })
9072        .collect();
9073    pairs.extend(extra_roots);
9074
9075    crate::plugins::host_dir::HostDirModuleReader::from_mounts(pairs)
9076}
9077
9078fn host_node_modules_root(path: &Path) -> Option<PathBuf> {
9079    if let Some(root) = path
9080        .ancestors()
9081        .filter(|candidate| {
9082            candidate.file_name().and_then(|name| name.to_str()) == Some("node_modules")
9083        })
9084        .last()
9085        .map(Path::to_path_buf)
9086    {
9087        return Some(root);
9088    }
9089
9090    fs::canonicalize(path)
9091        .ok()?
9092        .ancestors()
9093        .filter(|candidate| {
9094            candidate.file_name().and_then(|name| name.to_str()) == Some("node_modules")
9095        })
9096        .last()
9097        .map(Path::to_path_buf)
9098}
9099
9100#[cfg(test)]
9101mod runtime_guest_path_mapping_tests {
9102    use super::{host_node_modules_root, javascript_sync_rpc_option_bool};
9103    use serde_json::json;
9104    use std::fs;
9105    use std::time::{SystemTime, UNIX_EPOCH};
9106
9107    #[test]
9108    fn host_node_modules_root_prefers_workspace_root_over_pnpm_package_node_modules() {
9109        let unique = SystemTime::now()
9110            .duration_since(UNIX_EPOCH)
9111            .expect("clock should be monotonic")
9112            .as_nanos();
9113        let temp = std::env::temp_dir().join(format!("secure-exec-sidecar-node-modules-{unique}"));
9114        let workspace_node_modules = temp.join("node_modules");
9115        let package_root = workspace_node_modules
9116            .join(".pnpm")
9117            .join("example@1.0.0")
9118            .join("node_modules")
9119            .join("@scope")
9120            .join("pkg");
9121        fs::create_dir_all(&package_root).expect("package root should be created");
9122
9123        let resolved =
9124            host_node_modules_root(&package_root).expect("node_modules root should resolve");
9125
9126        assert_eq!(resolved, workspace_node_modules);
9127
9128        fs::remove_dir_all(&temp).expect("temp tree should be removed");
9129    }
9130
9131    #[test]
9132    fn host_node_modules_root_preserves_symlinked_workspace_node_modules_path() {
9133        let unique = SystemTime::now()
9134            .duration_since(UNIX_EPOCH)
9135            .expect("clock should be monotonic")
9136            .as_nanos();
9137        let temp =
9138            std::env::temp_dir().join(format!("secure-exec-sidecar-node-modules-symlink-{unique}"));
9139        let workspace_node_modules = temp.join("node_modules");
9140        let package_link = workspace_node_modules.join("@scope").join("pkg");
9141        let real_package = temp.join("registry").join("agent").join("pkg");
9142        fs::create_dir_all(package_link.parent().expect("package parent should exist"))
9143            .expect("scoped parent should be created");
9144        fs::create_dir_all(&real_package).expect("real package root should be created");
9145        std::os::unix::fs::symlink(&real_package, &package_link)
9146            .expect("package symlink should be created");
9147
9148        let resolved =
9149            host_node_modules_root(&package_link).expect("node_modules root should resolve");
9150
9151        assert_eq!(resolved, workspace_node_modules);
9152
9153        fs::remove_dir_all(&temp).expect("temp tree should be removed");
9154    }
9155
9156    #[test]
9157    fn javascript_sync_rpc_option_bool_accepts_boolean_recursive_argument() {
9158        assert_eq!(
9159            javascript_sync_rpc_option_bool(&[json!("/workspace"), json!(true)], 1, "recursive"),
9160            Some(true)
9161        );
9162        assert_eq!(
9163            javascript_sync_rpc_option_bool(
9164                &[json!("/workspace"), json!({ "recursive": false })],
9165                1,
9166                "recursive"
9167            ),
9168            Some(false)
9169        );
9170    }
9171}
9172
9173#[cfg(test)]
9174mod kernel_poll_sync_rpc_tests {
9175    use super::{
9176        service_javascript_kernel_poll_sync_rpc, ActiveExecution, ActiveProcess,
9177        JavascriptSyncRpcRequest, KernelPollFdResponse, SidecarKernel, ToolExecution,
9178        EXECUTION_DRIVER_NAME, JAVASCRIPT_COMMAND,
9179    };
9180    use secure_exec_kernel::command_registry::CommandDriver;
9181    use secure_exec_kernel::kernel::{KernelVmConfig, SpawnOptions};
9182    use secure_exec_kernel::mount_table::MountTable;
9183    use secure_exec_kernel::permissions::Permissions;
9184    use secure_exec_kernel::poll::{POLLHUP, POLLIN};
9185    use secure_exec_kernel::vfs::MemoryFileSystem;
9186    use serde_json::{json, Value};
9187    #[test]
9188    fn javascript_kernel_poll_sync_rpc_reports_multiple_kernel_fds() {
9189        let mut config = KernelVmConfig::new("vm-js-kernel-poll");
9190        config.permissions = Permissions::allow_all();
9191        let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config);
9192        kernel
9193            .register_driver(CommandDriver::new(
9194                EXECUTION_DRIVER_NAME,
9195                [JAVASCRIPT_COMMAND],
9196            ))
9197            .expect("register execution driver");
9198
9199        let kernel_handle = kernel
9200            .spawn_process(
9201                JAVASCRIPT_COMMAND,
9202                Vec::new(),
9203                SpawnOptions {
9204                    requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
9205                    ..SpawnOptions::default()
9206                },
9207            )
9208            .expect("spawn javascript kernel process");
9209        let pid = kernel_handle.pid();
9210
9211        let (stdin_read_fd, stdin_write_fd) = kernel
9212            .open_pipe(EXECUTION_DRIVER_NAME, pid)
9213            .expect("open kernel stdin pipe");
9214        kernel
9215            .fd_dup2(EXECUTION_DRIVER_NAME, pid, stdin_read_fd, 0)
9216            .expect("dup stdin pipe onto fd 0");
9217        kernel
9218            .fd_close(EXECUTION_DRIVER_NAME, pid, stdin_read_fd)
9219            .expect("close original stdin read fd");
9220
9221        let process = ActiveProcess::new(
9222            pid,
9223            kernel_handle,
9224            super::GuestRuntimeKind::JavaScript,
9225            ActiveExecution::Tool(ToolExecution::default()),
9226        );
9227
9228        kernel
9229            .fd_write(EXECUTION_DRIVER_NAME, pid, stdin_write_fd, b"poll-ready")
9230            .expect("write kernel stdin payload");
9231        kernel
9232            .fd_close(EXECUTION_DRIVER_NAME, pid, stdin_write_fd)
9233            .expect("close kernel stdin writer");
9234
9235        let response = service_javascript_kernel_poll_sync_rpc(
9236            &mut kernel,
9237            &process,
9238            &JavascriptSyncRpcRequest {
9239                id: 1,
9240                method: String::from("__kernel_poll"),
9241                args: vec![
9242                    json!([
9243                        { "fd": 0, "events": POLLIN.bits() },
9244                        { "fd": 1, "events": POLLIN.bits() }
9245                    ]),
9246                    json!(250),
9247                ],
9248            },
9249        )
9250        .expect("poll kernel fds");
9251
9252        assert_eq!(response["readyCount"], Value::from(1));
9253        let fds: Vec<KernelPollFdResponse> =
9254            serde_json::from_value(response["fds"].clone()).expect("kernel poll fd response");
9255        assert_eq!(
9256            fds,
9257            vec![
9258                KernelPollFdResponse {
9259                    fd: 0,
9260                    events: POLLIN.bits(),
9261                    revents: (POLLIN | POLLHUP).bits(),
9262                },
9263                KernelPollFdResponse {
9264                    fd: 1,
9265                    events: POLLIN.bits(),
9266                    revents: 0,
9267                },
9268            ]
9269        );
9270
9271        process.kernel_handle.finish(0);
9272        kernel.waitpid(pid).expect("wait javascript kernel process");
9273    }
9274}
9275
9276fn dedupe_strings(values: &[String]) -> Vec<String> {
9277    let mut seen = BTreeSet::new();
9278    let mut deduped = Vec::new();
9279    for value in values {
9280        if seen.insert(value.clone()) {
9281            deduped.push(value.clone());
9282        }
9283    }
9284    deduped
9285}
9286
9287fn dedupe_host_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
9288    let mut seen = BTreeSet::new();
9289    let mut deduped = Vec::new();
9290    for path in paths {
9291        let normalized = normalize_host_path(path);
9292        let key = normalized.to_string_lossy().into_owned();
9293        if seen.insert(key) {
9294            deduped.push(normalized);
9295        }
9296    }
9297    deduped
9298}
9299
9300fn expand_host_access_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
9301    let mut expanded = Vec::new();
9302    let mut seen = BTreeSet::new();
9303
9304    let mut add_path = |candidate: PathBuf| {
9305        let normalized = normalize_host_path(&candidate);
9306        let key = normalized.to_string_lossy().into_owned();
9307        if seen.insert(key) {
9308            expanded.push(normalized);
9309        }
9310    };
9311
9312    for host_path in paths {
9313        add_path(host_path.clone());
9314        if let Ok(realpath) = fs::canonicalize(host_path) {
9315            add_path(realpath);
9316        }
9317
9318        if host_path.file_name().and_then(|name| name.to_str()) != Some("node_modules") {
9319            continue;
9320        }
9321
9322        let mut current = host_path.parent();
9323        while let Some(parent) = current {
9324            let candidate = parent.join("node_modules");
9325            if candidate.exists() {
9326                add_path(candidate.clone());
9327                if let Ok(realpath) = fs::canonicalize(&candidate) {
9328                    add_path(realpath);
9329                }
9330            }
9331            current = parent.parent();
9332        }
9333    }
9334
9335    expanded
9336}
9337
9338fn prepare_javascript_shadow(
9339    vm: &mut VmState,
9340    resolved: &ResolvedChildProcessExecution,
9341) -> Result<(), SidecarError> {
9342    let guest_entrypoint = resolved
9343        .env
9344        .get("AGENTOS_GUEST_ENTRYPOINT")
9345        .cloned()
9346        // An absolute `entrypoint` may be a host path that lives inside the VM's
9347        // host cwd (callers can pass a fully-qualified host path). The guest sees
9348        // it at its translated guest path (host_cwd -> guest_cwd), so the shadow
9349        // must be keyed by that guest path rather than the raw host path. Falling
9350        // back to the host path here would materialize the file at the wrong guest
9351        // location and the runtime's `require()` would fail with "Cannot find
9352        // module".
9353        .or_else(|| {
9354            resolve_host_entrypoint_within_vm_host_cwd(vm, &resolved.entrypoint)
9355                .map(|(guest_entrypoint, _)| guest_entrypoint)
9356        })
9357        .or_else(|| {
9358            resolved
9359                .entrypoint
9360                .starts_with('/')
9361                .then(|| normalize_path(&resolved.entrypoint))
9362        });
9363    let Some(guest_entrypoint) = guest_entrypoint else {
9364        return Ok(());
9365    };
9366    if host_mount_path_for_guest_path(vm, &guest_entrypoint).is_some() {
9367        return Ok(());
9368    }
9369    if vm.kernel.lstat(&guest_entrypoint).is_err() {
9370        let host_entrypoint = {
9371            let candidate = Path::new(&resolved.entrypoint);
9372            if candidate.is_absolute() {
9373                candidate.to_path_buf()
9374            } else {
9375                resolved.host_cwd.join(candidate)
9376            }
9377        };
9378        if host_entrypoint.exists() {
9379            materialize_host_path_to_shadow(vm, &guest_entrypoint, &host_entrypoint)?;
9380            // The shadow write only stages the file on the host side; the runtime
9381            // resolves modules against the kernel VFS, so the staged entrypoint
9382            // must be synced into the kernel before execution starts (otherwise
9383            // `require()` reports "Cannot find module").
9384            return sync_shadow_entrypoint_into_kernel(vm, &guest_entrypoint);
9385        }
9386    }
9387    materialize_guest_path_to_shadow(vm, &guest_entrypoint)
9388}
9389
9390/// Sync a freshly-staged shadow entrypoint into the kernel VFS so the runtime's
9391/// kernel-backed module resolver can read it. Mirrors the host->kernel file sync
9392/// used by the broader shadow reconciliation, but scoped to the single
9393/// entrypoint we just materialized.
9394fn sync_shadow_entrypoint_into_kernel(
9395    vm: &mut VmState,
9396    guest_entrypoint: &str,
9397) -> Result<(), SidecarError> {
9398    if vm.kernel.exists(guest_entrypoint).unwrap_or(false) {
9399        return Ok(());
9400    }
9401    let shadow_path = shadow_path_for_guest(vm, guest_entrypoint);
9402    let bytes = match fs::read(&shadow_path) {
9403        Ok(bytes) => bytes,
9404        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
9405        Err(error) => {
9406            return Err(SidecarError::Io(format!(
9407                "failed to read staged shadow entrypoint {}: {error}",
9408                shadow_path.display()
9409            )));
9410        }
9411    };
9412    if let Some(parent) = guest_parent_path(guest_entrypoint) {
9413        if !vm.kernel.exists(&parent).unwrap_or(false) {
9414            vm.kernel.mkdir(&parent, true).map_err(kernel_error)?;
9415        }
9416    }
9417    vm.kernel
9418        .write_file(guest_entrypoint, bytes)
9419        .map_err(kernel_error)?;
9420    Ok(())
9421}
9422
9423fn guest_parent_path(guest_path: &str) -> Option<String> {
9424    let parent = Path::new(guest_path).parent()?;
9425    let parent = parent.to_string_lossy();
9426    if parent.is_empty() || parent == "/" {
9427        None
9428    } else {
9429        Some(parent.into_owned())
9430    }
9431}
9432
9433fn materialize_host_path_to_shadow(
9434    vm: &VmState,
9435    guest_path: &str,
9436    host_path: &Path,
9437) -> Result<(), SidecarError> {
9438    let shadow_path = shadow_path_for_guest(vm, guest_path);
9439    let metadata = fs::symlink_metadata(host_path)
9440        .map_err(|error| SidecarError::Io(format!("failed to stat host entrypoint: {error}")))?;
9441
9442    if metadata.file_type().is_symlink() {
9443        if let Some(parent) = shadow_path.parent() {
9444            fs::create_dir_all(parent).map_err(|error| {
9445                SidecarError::Io(format!("failed to create shadow symlink parent: {error}"))
9446            })?;
9447        }
9448        let _ = fs::remove_file(&shadow_path);
9449        let _ = fs::remove_dir_all(&shadow_path);
9450        let target = fs::read_link(host_path)
9451            .map_err(|error| SidecarError::Io(format!("failed to read host symlink: {error}")))?;
9452        std::os::unix::fs::symlink(&target, &shadow_path)
9453            .map_err(|error| SidecarError::Io(format!("failed to mirror host symlink: {error}")))?;
9454        return Ok(());
9455    }
9456
9457    if metadata.is_dir() {
9458        fs::create_dir_all(&shadow_path).map_err(|error| {
9459            SidecarError::Io(format!("failed to create shadow directory: {error}"))
9460        })?;
9461        fs::set_permissions(
9462            &shadow_path,
9463            fs::Permissions::from_mode(metadata.permissions().mode() & 0o7777),
9464        )
9465        .map_err(|error| {
9466            SidecarError::Io(format!(
9467                "failed to set shadow directory mode on {}: {error}",
9468                shadow_path.display()
9469            ))
9470        })?;
9471        return Ok(());
9472    }
9473
9474    if let Some(parent) = shadow_path.parent() {
9475        fs::create_dir_all(parent).map_err(|error| {
9476            SidecarError::Io(format!("failed to create shadow parent: {error}"))
9477        })?;
9478    }
9479    let bytes = fs::read(host_path)
9480        .map_err(|error| SidecarError::Io(format!("failed to read host entrypoint: {error}")))?;
9481    fs::write(&shadow_path, bytes).map_err(|error| {
9482        SidecarError::Io(format!(
9483            "failed to mirror host file into shadow root: {error}"
9484        ))
9485    })?;
9486    fs::set_permissions(
9487        &shadow_path,
9488        fs::Permissions::from_mode(metadata.permissions().mode() & 0o7777),
9489    )
9490    .map_err(|error| {
9491        SidecarError::Io(format!(
9492            "failed to set shadow file mode on {}: {error}",
9493            shadow_path.display()
9494        ))
9495    })?;
9496    Ok(())
9497}
9498
9499fn materialize_guest_path_to_shadow(
9500    vm: &mut VmState,
9501    guest_path: &str,
9502) -> Result<(), SidecarError> {
9503    let stat = vm.kernel.lstat(guest_path).map_err(kernel_error)?;
9504    let shadow_path = shadow_path_for_guest(vm, guest_path);
9505
9506    if stat.is_symbolic_link {
9507        if let Some(parent) = shadow_path.parent() {
9508            fs::create_dir_all(parent).map_err(|error| {
9509                SidecarError::Io(format!("failed to create shadow symlink parent: {error}"))
9510            })?;
9511        }
9512        let _ = fs::remove_file(&shadow_path);
9513        let _ = fs::remove_dir_all(&shadow_path);
9514        let target = vm.kernel.read_link(guest_path).map_err(kernel_error)?;
9515        std::os::unix::fs::symlink(&target, &shadow_path)
9516            .map_err(|error| SidecarError::Io(format!("failed to mirror symlink: {error}")))?;
9517        return Ok(());
9518    }
9519
9520    if stat.is_directory {
9521        fs::create_dir_all(&shadow_path).map_err(|error| {
9522            SidecarError::Io(format!("failed to create shadow directory: {error}"))
9523        })?;
9524        fs::set_permissions(&shadow_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err(
9525            |error| {
9526                SidecarError::Io(format!(
9527                    "failed to set shadow directory mode on {}: {error}",
9528                    shadow_path.display()
9529                ))
9530            },
9531        )?;
9532        return Ok(());
9533    }
9534
9535    if let Some(parent) = shadow_path.parent() {
9536        fs::create_dir_all(parent).map_err(|error| {
9537            SidecarError::Io(format!("failed to create shadow parent: {error}"))
9538        })?;
9539    }
9540    let bytes = vm.kernel.read_file(guest_path).map_err(kernel_error)?;
9541    fs::write(&shadow_path, bytes).map_err(|error| {
9542        SidecarError::Io(format!(
9543            "failed to mirror guest file into shadow root: {error}"
9544        ))
9545    })?;
9546    fs::set_permissions(&shadow_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err(
9547        |error| {
9548            SidecarError::Io(format!(
9549                "failed to set shadow file mode on {}: {error}",
9550                shadow_path.display()
9551            ))
9552        },
9553    )?;
9554    Ok(())
9555}
9556
9557fn load_javascript_entrypoint_source(
9558    vm: &mut VmState,
9559    host_cwd: &Path,
9560    entrypoint: &str,
9561    env: &BTreeMap<String, String>,
9562) -> Option<String> {
9563    let mut read_guest_file = |path: &str| {
9564        vm.kernel
9565            .read_file(path)
9566            .ok()
9567            .and_then(|bytes| String::from_utf8(bytes).ok())
9568    };
9569
9570    if let Some(source) = env
9571        .get("AGENTOS_GUEST_ENTRYPOINT")
9572        .filter(|path| path.starts_with('/'))
9573        .and_then(|path| read_guest_file(path))
9574    {
9575        return Some(source);
9576    }
9577
9578    if entrypoint.starts_with('/') {
9579        if let Some(source) = read_guest_file(entrypoint) {
9580            return Some(source);
9581        }
9582    }
9583
9584    let host_entrypoint = if Path::new(entrypoint).is_absolute() {
9585        PathBuf::from(entrypoint)
9586    } else {
9587        host_cwd.join(entrypoint)
9588    };
9589    let normalized_entrypoint = normalize_host_path(&host_entrypoint);
9590    let sandbox_root = normalize_host_path(&vm.cwd);
9591    let host_cwd = normalize_host_path(&vm.host_cwd);
9592    if !path_is_within_root(&normalized_entrypoint, &sandbox_root)
9593        && !path_is_within_root(&normalized_entrypoint, &host_cwd)
9594    {
9595        return None;
9596    }
9597
9598    fs::read_to_string(&normalized_entrypoint).ok()
9599}
9600
9601fn emit_dns_resolution_event<B>(
9602    bridge: &SharedBridge<B>,
9603    vm_id: &str,
9604    hostname: &str,
9605    source: KernelDnsResolutionSource,
9606    addresses: &[IpAddr],
9607    dns: &VmDnsConfig,
9608) where
9609    B: NativeSidecarBridge + Send + 'static,
9610    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9611{
9612    let _ = emit_structured_event(
9613        bridge,
9614        vm_id,
9615        "network.dns.resolved",
9616        audit_fields([
9617            ("hostname", hostname.to_owned()),
9618            ("source", source.as_str().to_owned()),
9619            (
9620                "addresses",
9621                addresses
9622                    .iter()
9623                    .map(ToString::to_string)
9624                    .collect::<Vec<_>>()
9625                    .join(","),
9626            ),
9627            ("address_count", addresses.len().to_string()),
9628            ("resolver_count", dns.name_servers.len().to_string()),
9629            (
9630                "resolvers",
9631                dns.name_servers
9632                    .iter()
9633                    .map(ToString::to_string)
9634                    .collect::<Vec<_>>()
9635                    .join(","),
9636            ),
9637        ]),
9638    );
9639}
9640
9641fn emit_dns_record_resolution_event<B>(
9642    bridge: &SharedBridge<B>,
9643    vm_id: &str,
9644    hostname: &str,
9645    resolution: &DnsRecordResolution,
9646    dns: &VmDnsConfig,
9647) where
9648    B: NativeSidecarBridge + Send + 'static,
9649    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9650{
9651    if let Some(addresses) = dns_resolution_ip_addrs(resolution.records()) {
9652        emit_dns_resolution_event(
9653            bridge,
9654            vm_id,
9655            hostname,
9656            resolution.source(),
9657            &addresses,
9658            dns,
9659        );
9660        return;
9661    }
9662
9663    let _ = emit_structured_event(
9664        bridge,
9665        vm_id,
9666        "network.dns.resolved",
9667        audit_fields([
9668            ("hostname", hostname.to_owned()),
9669            ("source", resolution.source().as_str().to_owned()),
9670            (
9671                "addresses",
9672                resolution
9673                    .records()
9674                    .iter()
9675                    .map(summarize_dns_record)
9676                    .collect::<Vec<_>>()
9677                    .join(","),
9678            ),
9679            ("address_count", resolution.records().len().to_string()),
9680            ("resolver_count", dns.name_servers.len().to_string()),
9681            (
9682                "resolvers",
9683                dns.name_servers
9684                    .iter()
9685                    .map(ToString::to_string)
9686                    .collect::<Vec<_>>()
9687                    .join(","),
9688            ),
9689        ]),
9690    );
9691}
9692
9693fn emit_dns_resolution_failure_event<B>(
9694    bridge: &SharedBridge<B>,
9695    vm_id: &str,
9696    hostname: &str,
9697    dns: &VmDnsConfig,
9698    error: &SidecarError,
9699) where
9700    B: NativeSidecarBridge + Send + 'static,
9701    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9702{
9703    let _ = emit_structured_event(
9704        bridge,
9705        vm_id,
9706        "network.dns.resolve_failed",
9707        audit_fields([
9708            ("hostname", hostname.to_owned()),
9709            ("reason", error.to_string()),
9710            ("resolver_count", dns.name_servers.len().to_string()),
9711            (
9712                "resolvers",
9713                dns.name_servers
9714                    .iter()
9715                    .map(ToString::to_string)
9716                    .collect::<Vec<_>>()
9717                    .join(","),
9718            ),
9719        ]),
9720    );
9721}
9722
9723fn parse_dns_record_type(rrtype: &str) -> Result<RecordType, SidecarError> {
9724    match rrtype {
9725        "A" => Ok(RecordType::A),
9726        "AAAA" => Ok(RecordType::AAAA),
9727        "MX" => Ok(RecordType::MX),
9728        "TXT" => Ok(RecordType::TXT),
9729        "SRV" => Ok(RecordType::SRV),
9730        "CNAME" => Ok(RecordType::CNAME),
9731        "PTR" => Ok(RecordType::PTR),
9732        "NS" => Ok(RecordType::NS),
9733        "SOA" => Ok(RecordType::SOA),
9734        "NAPTR" => Ok(RecordType::NAPTR),
9735        "CAA" => Ok(RecordType::CAA),
9736        "ANY" => Ok(RecordType::ANY),
9737        other => Err(SidecarError::Execution(format!(
9738            "ERR_NOT_IMPLEMENTED: dns rrtype {other} is not supported by the secure-exec dns bridge"
9739        ))),
9740    }
9741}
9742
9743fn dns_resolution_to_node_value(
9744    resolution: &DnsRecordResolution,
9745    requested_type: &str,
9746) -> Result<Value, SidecarError> {
9747    let safe_ips = dns_resolution_safe_ip_set(resolution.records(), resolution.hostname())?;
9748    match requested_type {
9749        "A" | "AAAA" => Ok(Value::Array(
9750            resolution
9751                .records()
9752                .iter()
9753                .filter_map(|record| dns_record_ip_string(record, &safe_ips))
9754                .map(Value::String)
9755                .collect(),
9756        )),
9757        "MX" => Ok(Value::Array(
9758            resolution
9759                .records()
9760                .iter()
9761                .filter_map(|record| match record.data() {
9762                    RData::MX(mx) => Some(json!({
9763                        "priority": mx.preference,
9764                        "exchange": normalize_dns_name_for_node(&mx.exchange),
9765                        "type": "MX",
9766                    })),
9767                    _ => None,
9768                })
9769                .collect(),
9770        )),
9771        "TXT" => Ok(Value::Array(
9772            resolution
9773                .records()
9774                .iter()
9775                .filter_map(|record| match record.data() {
9776                    RData::TXT(txt) => Some(Value::Array(
9777                        txt.txt_data
9778                            .iter()
9779                            .map(|entry| Value::String(String::from_utf8_lossy(entry).into_owned()))
9780                            .collect(),
9781                    )),
9782                    _ => None,
9783                })
9784                .collect(),
9785        )),
9786        "SRV" => Ok(Value::Array(
9787            resolution
9788                .records()
9789                .iter()
9790                .filter_map(|record| match record.data() {
9791                    RData::SRV(srv) => Some(json!({
9792                        "priority": srv.priority,
9793                        "weight": srv.weight,
9794                        "port": srv.port,
9795                        "name": normalize_dns_name_for_node(&srv.target),
9796                        "type": "SRV",
9797                    })),
9798                    _ => None,
9799                })
9800                .collect(),
9801        )),
9802        "CNAME" => Ok(Value::Array(
9803            resolution
9804                .records()
9805                .iter()
9806                .filter_map(|record| match record.data() {
9807                    RData::CNAME(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9808                    _ => None,
9809                })
9810                .collect(),
9811        )),
9812        "PTR" => Ok(Value::Array(
9813            resolution
9814                .records()
9815                .iter()
9816                .filter_map(|record| match record.data() {
9817                    RData::PTR(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9818                    _ => None,
9819                })
9820                .collect(),
9821        )),
9822        "NS" => Ok(Value::Array(
9823            resolution
9824                .records()
9825                .iter()
9826                .filter_map(|record| match record.data() {
9827                    RData::NS(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9828                    _ => None,
9829                })
9830                .collect(),
9831        )),
9832        "SOA" => resolution
9833            .records()
9834            .iter()
9835            .find_map(|record| match record.data() {
9836                RData::SOA(soa) => Some(json!({
9837                    "nsname": normalize_dns_name_for_node(&soa.mname),
9838                    "hostmaster": normalize_dns_name_for_node(&soa.rname),
9839                    "serial": soa.serial,
9840                    "refresh": soa.refresh,
9841                    "retry": soa.retry,
9842                    "expire": soa.expire,
9843                    "minttl": soa.minimum,
9844                })),
9845                _ => None,
9846            })
9847            .ok_or_else(|| {
9848                SidecarError::Execution(String::from("failed to resolve DNS SOA record"))
9849            }),
9850        "NAPTR" => Ok(Value::Array(
9851            resolution
9852                .records()
9853                .iter()
9854                .filter_map(|record| match record.data() {
9855                    RData::NAPTR(naptr) => Some(json!({
9856                        "flags": String::from_utf8_lossy(&naptr.flags).into_owned(),
9857                        "service": String::from_utf8_lossy(&naptr.services).into_owned(),
9858                        "regexp": String::from_utf8_lossy(&naptr.regexp).into_owned(),
9859                        "replacement": normalize_dns_name_for_node(&naptr.replacement),
9860                        "order": naptr.order,
9861                        "preference": naptr.preference,
9862                    })),
9863                    _ => None,
9864                })
9865                .collect(),
9866        )),
9867        "CAA" => Ok(Value::Array(
9868            resolution
9869                .records()
9870                .iter()
9871                .filter_map(|record| match record.data() {
9872                    RData::CAA(caa) => {
9873                        let mut value = serde_json::Map::new();
9874                        value.insert(
9875                            "critical".to_owned(),
9876                            Value::from(u8::from(caa.issuer_critical)),
9877                        );
9878                        value.insert("type".to_owned(), Value::String(String::from("CAA")));
9879                        if caa.tag.eq_ignore_ascii_case("iodef") {
9880                            value.insert(
9881                                "iodef".to_owned(),
9882                                Value::String(
9883                                    caa.value_as_iodef()
9884                                        .map(|url| url.to_string())
9885                                        .unwrap_or_else(|_| {
9886                                            String::from_utf8_lossy(&caa.value).into_owned()
9887                                        }),
9888                                ),
9889                            );
9890                        } else if let Ok((issuer, _params)) = caa.value_as_issue() {
9891                            let field = if caa.tag.eq_ignore_ascii_case("issuewild") {
9892                                "issuewild"
9893                            } else {
9894                                "issue"
9895                            };
9896                            value.insert(
9897                                field.to_owned(),
9898                                Value::String(
9899                                    issuer.as_ref().map(ToString::to_string).unwrap_or_else(|| {
9900                                        String::from_utf8_lossy(&caa.value).into_owned()
9901                                    }),
9902                                ),
9903                            );
9904                        } else {
9905                            value.insert(
9906                                caa.tag.to_ascii_lowercase(),
9907                                Value::String(String::from_utf8_lossy(&caa.value).into_owned()),
9908                            );
9909                        }
9910                        Some(Value::Object(value))
9911                    }
9912                    _ => None,
9913                })
9914                .collect(),
9915        )),
9916        "ANY" => Ok(Value::Array(
9917            resolution
9918                .records()
9919                .iter()
9920                .filter_map(|record| dns_any_record_to_value(record, &safe_ips))
9921                .collect(),
9922        )),
9923        other => Err(SidecarError::Execution(format!(
9924            "ERR_NOT_IMPLEMENTED: dns rrtype {other} is not supported by the secure-exec dns bridge"
9925        ))),
9926    }
9927}
9928
9929fn dns_resolution_safe_ip_set(
9930    records: &[Record],
9931    hostname: &str,
9932) -> Result<BTreeSet<IpAddr>, SidecarError> {
9933    let ips = records
9934        .iter()
9935        .filter_map(dns_record_ip_addr)
9936        .collect::<Vec<_>>();
9937    if ips.is_empty() {
9938        return Ok(BTreeSet::new());
9939    }
9940    Ok(filter_dns_safe_ip_addrs(ips, hostname)?
9941        .into_iter()
9942        .collect())
9943}
9944
9945fn dns_resolution_ip_addrs(records: &[Record]) -> Option<Vec<IpAddr>> {
9946    let ips = records
9947        .iter()
9948        .filter_map(dns_record_ip_addr)
9949        .collect::<Vec<_>>();
9950    if ips.is_empty() {
9951        return None;
9952    }
9953    Some(ips)
9954}
9955
9956fn dns_record_ip_addr(record: &Record) -> Option<IpAddr> {
9957    match record.data() {
9958        RData::A(address) => Some(IpAddr::V4(**address)),
9959        RData::AAAA(address) => Some(IpAddr::V6(**address)),
9960        _ => None,
9961    }
9962}
9963
9964fn dns_record_ip_string(record: &Record, safe_ips: &BTreeSet<IpAddr>) -> Option<String> {
9965    let ip = dns_record_ip_addr(record)?;
9966    safe_ips.contains(&ip).then(|| ip.to_string())
9967}
9968
9969fn dns_any_record_to_value(record: &Record, safe_ips: &BTreeSet<IpAddr>) -> Option<Value> {
9970    let value = match record.data() {
9971        RData::A(_) | RData::AAAA(_) => json!({
9972            "address": dns_record_ip_string(record, safe_ips)?,
9973            "ttl": record.ttl(),
9974            "type": record.record_type().to_string(),
9975        }),
9976        RData::MX(mx) => json!({
9977            "exchange": normalize_dns_name_for_node(&mx.exchange),
9978            "priority": mx.preference,
9979            "type": "MX",
9980        }),
9981        RData::TXT(txt) => json!({
9982            "entries": txt
9983                .txt_data
9984                .iter()
9985                .map(|entry| String::from_utf8_lossy(entry).into_owned())
9986                .collect::<Vec<_>>(),
9987            "type": "TXT",
9988        }),
9989        RData::SRV(srv) => json!({
9990            "name": normalize_dns_name_for_node(&srv.target),
9991            "port": srv.port,
9992            "priority": srv.priority,
9993            "weight": srv.weight,
9994            "type": "SRV",
9995        }),
9996        RData::CNAME(name) => json!({
9997            "value": normalize_dns_name_for_node(&name.0),
9998            "type": "CNAME",
9999        }),
10000        RData::PTR(name) => json!({
10001            "value": normalize_dns_name_for_node(&name.0),
10002            "type": "PTR",
10003        }),
10004        RData::NS(name) => json!({
10005            "value": normalize_dns_name_for_node(&name.0),
10006            "type": "NS",
10007        }),
10008        RData::SOA(soa) => json!({
10009            "nsname": normalize_dns_name_for_node(&soa.mname),
10010            "hostmaster": normalize_dns_name_for_node(&soa.rname),
10011            "serial": soa.serial,
10012            "refresh": soa.refresh,
10013            "retry": soa.retry,
10014            "expire": soa.expire,
10015            "minttl": soa.minimum,
10016            "type": "SOA",
10017        }),
10018        RData::NAPTR(naptr) => json!({
10019            "flags": String::from_utf8_lossy(&naptr.flags).into_owned(),
10020            "service": String::from_utf8_lossy(&naptr.services).into_owned(),
10021            "regexp": String::from_utf8_lossy(&naptr.regexp).into_owned(),
10022            "replacement": normalize_dns_name_for_node(&naptr.replacement),
10023            "order": naptr.order,
10024            "preference": naptr.preference,
10025            "type": "NAPTR",
10026        }),
10027        RData::CAA(caa) => {
10028            let mut value = serde_json::Map::new();
10029            value.insert(
10030                "critical".to_owned(),
10031                Value::from(u8::from(caa.issuer_critical)),
10032            );
10033            value.insert("type".to_owned(), Value::String(String::from("CAA")));
10034            if caa.tag.eq_ignore_ascii_case("iodef") {
10035                value.insert(
10036                    "iodef".to_owned(),
10037                    Value::String(
10038                        caa.value_as_iodef()
10039                            .map(|url| url.to_string())
10040                            .unwrap_or_else(|_| String::from_utf8_lossy(&caa.value).into_owned()),
10041                    ),
10042                );
10043            } else if let Ok((issuer, _params)) = caa.value_as_issue() {
10044                let field = if caa.tag.eq_ignore_ascii_case("issuewild") {
10045                    "issuewild"
10046                } else {
10047                    "issue"
10048                };
10049                value.insert(
10050                    field.to_owned(),
10051                    Value::String(
10052                        issuer
10053                            .as_ref()
10054                            .map(ToString::to_string)
10055                            .unwrap_or_else(|| String::from_utf8_lossy(&caa.value).into_owned()),
10056                    ),
10057                );
10058            }
10059            Value::Object(value)
10060        }
10061        _ => return None,
10062    };
10063    Some(value)
10064}
10065
10066fn normalize_dns_name_for_node(name: &impl ToString) -> String {
10067    name.to_string().trim_end_matches('.').to_owned()
10068}
10069
10070fn summarize_dns_record(record: &Record) -> String {
10071    match record.data() {
10072        RData::A(_) | RData::AAAA(_) => record.data().to_string(),
10073        _ => format!("{} {}", record.record_type(), record.data()),
10074    }
10075}
10076
10077// build_root_filesystem, convert_root_lower_descriptor, convert_root_filesystem_entry,
10078// root_snapshot_entry moved to crate::bootstrap
10079
10080// apply_root_filesystem_entry, ensure_parent_directories moved to crate::bootstrap
10081
10082// ProcNetEntry moved to crate::state
10083
10084fn find_socket_state_entry(
10085    vm: Option<&VmState>,
10086    kind: SocketQueryKind,
10087    request: &FindListenerRequest,
10088) -> Result<Option<SocketStateEntry>, SidecarError> {
10089    let vm = vm.ok_or_else(|| SidecarError::InvalidState(String::from("unknown sidecar VM")))?;
10090
10091    for (process_id, process) in &vm.active_processes {
10092        if let Some(path) = request.path.as_deref() {
10093            if matches!(kind, SocketQueryKind::TcpListener) {
10094                for listener in process.unix_listeners.values() {
10095                    if listener.path() != path {
10096                        continue;
10097                    }
10098                    return Ok(Some(SocketStateEntry {
10099                        process_id: process_id.to_owned(),
10100                        host: None,
10101                        port: None,
10102                        path: Some(path.to_owned()),
10103                    }));
10104                }
10105            }
10106        }
10107
10108        if request.path.is_none() {
10109            if let Some(entry) =
10110                find_kernel_socket_state_entry(&vm.kernel, process_id, process, kind, request)?
10111            {
10112                return Ok(Some(entry));
10113            }
10114
10115            match kind {
10116                SocketQueryKind::TcpListener => {
10117                    for server in process.http_servers.values() {
10118                        let local_addr = server.guest_local_addr;
10119                        let local_host = local_addr.ip().to_string();
10120                        if !socket_host_matches(request.host.as_deref(), &local_host) {
10121                            continue;
10122                        }
10123                        if let Some(port) = request.port {
10124                            if local_addr.port() != port {
10125                                continue;
10126                            }
10127                        }
10128                        return Ok(Some(SocketStateEntry {
10129                            process_id: process_id.to_owned(),
10130                            host: Some(local_host),
10131                            port: Some(local_addr.port()),
10132                            path: None,
10133                        }));
10134                    }
10135
10136                    for listener in process.tcp_listeners.values() {
10137                        if listener.kernel_socket_id.is_some() {
10138                            continue;
10139                        }
10140                        let local_addr = listener.guest_local_addr();
10141                        let local_host = local_addr.ip().to_string();
10142                        if !socket_host_matches(request.host.as_deref(), &local_host) {
10143                            continue;
10144                        }
10145                        if let Some(port) = request.port {
10146                            if local_addr.port() != port {
10147                                continue;
10148                            }
10149                        }
10150                        return Ok(Some(SocketStateEntry {
10151                            process_id: process_id.to_owned(),
10152                            host: Some(local_host),
10153                            port: Some(local_addr.port()),
10154                            path: None,
10155                        }));
10156                    }
10157                }
10158                SocketQueryKind::UdpBound => {
10159                    for socket in process.udp_sockets.values() {
10160                        if socket.kernel_socket_id.is_some() {
10161                            continue;
10162                        }
10163                        let Some(local_addr) = socket.local_addr() else {
10164                            continue;
10165                        };
10166                        let local_host = local_addr.ip().to_string();
10167                        if !socket_host_matches(request.host.as_deref(), &local_host) {
10168                            continue;
10169                        }
10170                        if let Some(port) = request.port {
10171                            if local_addr.port() != port {
10172                                continue;
10173                            }
10174                        }
10175                        return Ok(Some(SocketStateEntry {
10176                            process_id: process_id.to_owned(),
10177                            host: Some(local_host),
10178                            port: Some(local_addr.port()),
10179                            path: None,
10180                        }));
10181                    }
10182                }
10183            }
10184        }
10185
10186        let child_pid = process.execution.child_pid();
10187        let inodes = socket_inodes_for_pid(child_pid)?;
10188        if inodes.is_empty() {
10189            continue;
10190        }
10191
10192        if let Some(path) = request.path.as_deref() {
10193            if let Some(listener) = find_unix_socket_for_pid(child_pid, &inodes, path, process_id)?
10194            {
10195                return Ok(Some(listener));
10196            }
10197            continue;
10198        }
10199
10200        let table_paths = match kind {
10201            SocketQueryKind::TcpListener => [
10202                format!("/proc/{child_pid}/net/tcp"),
10203                format!("/proc/{child_pid}/net/tcp6"),
10204            ],
10205            SocketQueryKind::UdpBound => [
10206                format!("/proc/{child_pid}/net/udp"),
10207                format!("/proc/{child_pid}/net/udp6"),
10208            ],
10209        };
10210        for table_path in table_paths {
10211            if let Some(entry) = find_inet_socket_for_pid(
10212                &table_path,
10213                &inodes,
10214                kind,
10215                request.host.as_deref(),
10216                request.port,
10217                process_id,
10218            )? {
10219                return Ok(Some(entry));
10220            }
10221        }
10222    }
10223
10224    Ok(None)
10225}
10226
10227fn require_vm_inspection_permission<B>(
10228    bridge: &SharedBridge<B>,
10229    vm_id: &str,
10230    capability: &str,
10231    domain: &str,
10232    resource: &str,
10233) -> Result<(), SidecarError>
10234where
10235    B: NativeSidecarBridge + Send + 'static,
10236    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
10237{
10238    let decision = bridge.static_permission_decision(vm_id, capability, domain, Some(resource));
10239    if decision.as_ref().is_some_and(|decision| decision.allow) {
10240        return Ok(());
10241    }
10242
10243    let reason = decision
10244        .and_then(|decision| decision.reason)
10245        .unwrap_or_else(|| format!("{capability} permission required"));
10246    Err(SidecarError::Execution(format!(
10247        "EACCES: permission denied, {resource}: {reason}"
10248    )))
10249}
10250
10251fn socket_query_resource(kind: SocketQueryKind, request: &FindListenerRequest) -> String {
10252    if let Some(path) = request.path.as_deref() {
10253        return format!("unix://{path}");
10254    }
10255
10256    let host = request.host.as_deref().unwrap_or("*");
10257    let port = request
10258        .port
10259        .map_or_else(|| String::from("*"), |port| port.to_string());
10260    match kind {
10261        SocketQueryKind::TcpListener => format!("tcp://{host}:{port}"),
10262        SocketQueryKind::UdpBound => format!("udp://{host}:{port}"),
10263    }
10264}
10265
10266fn snapshot_vm_processes(vm: &VmState) -> Vec<ProcessSnapshotEntry> {
10267    let process_table = vm.kernel.list_processes();
10268    snapshot_vm_processes_inner(vm, &process_table)
10269}
10270
10271fn snapshot_vm_processes_inner(
10272    vm: &VmState,
10273    process_table: &BTreeMap<u32, secure_exec_kernel::process_table::ProcessInfo>,
10274) -> Vec<ProcessSnapshotEntry> {
10275    let mut entries = Vec::new();
10276
10277    for (process_id, process) in &vm.active_processes {
10278        collect_process_snapshot_entries(process_id, process, process_table, &mut entries);
10279    }
10280
10281    for exited in &vm.exited_process_snapshots {
10282        entries.push(exited.process.clone());
10283    }
10284
10285    entries
10286}
10287
10288fn prune_exited_process_snapshots(vm: &mut VmState) {
10289    let cutoff = Instant::now() - EXITED_PROCESS_SNAPSHOT_RETENTION;
10290    while vm
10291        .exited_process_snapshots
10292        .front()
10293        .is_some_and(|snapshot| snapshot.captured_at < cutoff)
10294    {
10295        vm.exited_process_snapshots.pop_front();
10296    }
10297}
10298
10299fn build_process_snapshot_entry(
10300    process_id: &str,
10301    process: &ActiveProcess,
10302    info: &secure_exec_kernel::process_table::ProcessInfo,
10303    exit_code: Option<i32>,
10304) -> ProcessSnapshotEntry {
10305    ProcessSnapshotEntry {
10306        process_id: process_id.to_owned(),
10307        pid: info.pid,
10308        ppid: info.ppid,
10309        pgid: info.pgid,
10310        sid: info.sid,
10311        driver: info.driver.clone(),
10312        command: info.command.clone(),
10313        args: Vec::new(),
10314        cwd: process.guest_cwd.clone(),
10315        status: if exit_code.is_some() {
10316            ProcessSnapshotStatus::Exited
10317        } else {
10318            match info.status {
10319                ProcessStatus::Running => ProcessSnapshotStatus::Running,
10320                ProcessStatus::Stopped => ProcessSnapshotStatus::Stopped,
10321                ProcessStatus::Exited => ProcessSnapshotStatus::Exited,
10322            }
10323        },
10324        exit_code: exit_code.or(info.exit_code),
10325    }
10326}
10327
10328fn collect_process_snapshot_entries(
10329    process_id: &str,
10330    process: &ActiveProcess,
10331    process_table: &BTreeMap<u32, secure_exec_kernel::process_table::ProcessInfo>,
10332    entries: &mut Vec<ProcessSnapshotEntry>,
10333) {
10334    if let Some(info) = process_table.get(&process.kernel_pid) {
10335        entries.push(build_process_snapshot_entry(
10336            process_id, process, info, None,
10337        ));
10338    }
10339
10340    for (child_id, child) in &process.child_processes {
10341        let child_process_id = format!("{process_id}/{child_id}");
10342        collect_process_snapshot_entries(&child_process_id, child, process_table, entries);
10343    }
10344}
10345
10346fn find_kernel_socket_state_entry(
10347    kernel: &SidecarKernel,
10348    process_id: &str,
10349    process: &ActiveProcess,
10350    kind: SocketQueryKind,
10351    request: &FindListenerRequest,
10352) -> Result<Option<SocketStateEntry>, SidecarError> {
10353    let entry = match kind {
10354        SocketQueryKind::TcpListener => process
10355            .tcp_listeners
10356            .values()
10357            .filter_map(|listener| listener.kernel_socket_id)
10358            .find_map(|socket_id| {
10359                kernel_socket_state_entry(kernel, process_id, socket_id, kind, request)
10360            }),
10361        SocketQueryKind::UdpBound => process
10362            .udp_sockets
10363            .values()
10364            .filter_map(|socket| socket.kernel_socket_id)
10365            .find_map(|socket_id| {
10366                kernel_socket_state_entry(kernel, process_id, socket_id, kind, request)
10367            }),
10368    };
10369
10370    if entry.is_some() {
10371        return Ok(entry);
10372    }
10373
10374    for child in process.child_processes.values() {
10375        if let Some(entry) =
10376            find_kernel_socket_state_entry(kernel, process_id, child, kind, request)?
10377        {
10378            return Ok(Some(entry));
10379        }
10380    }
10381
10382    Ok(None)
10383}
10384
10385fn kernel_socket_state_entry(
10386    kernel: &SidecarKernel,
10387    process_id: &str,
10388    socket_id: SocketId,
10389    kind: SocketQueryKind,
10390    request: &FindListenerRequest,
10391) -> Option<SocketStateEntry> {
10392    let record = kernel.socket_get(socket_id)?;
10393    let local_address = record.local_address()?;
10394    match kind {
10395        SocketQueryKind::TcpListener if record.state() == SocketState::Listening => {}
10396        SocketQueryKind::TcpListener => return None,
10397        SocketQueryKind::UdpBound => {}
10398    }
10399
10400    if !socket_host_matches(request.host.as_deref(), local_address.host()) {
10401        return None;
10402    }
10403    if request
10404        .port
10405        .is_some_and(|port| local_address.port() != port)
10406    {
10407        return None;
10408    }
10409
10410    Some(SocketStateEntry {
10411        process_id: process_id.to_owned(),
10412        host: Some(local_address.host().to_owned()),
10413        port: Some(local_address.port()),
10414        path: None,
10415    })
10416}
10417
10418fn socket_inodes_for_pid(pid: u32) -> Result<BTreeSet<u64>, SidecarError> {
10419    let fd_dir = PathBuf::from(format!("/proc/{pid}/fd"));
10420    let entries = match fs::read_dir(&fd_dir) {
10421        Ok(entries) => entries,
10422        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(BTreeSet::new()),
10423        Err(error) => {
10424            return Err(SidecarError::Io(format!(
10425                "failed to read socket descriptors for process {pid}: {error}"
10426            )));
10427        }
10428    };
10429
10430    let mut inodes = BTreeSet::new();
10431    for entry in entries {
10432        let entry = entry.map_err(|error| {
10433            SidecarError::Io(format!(
10434                "failed to inspect fd entry for process {pid}: {error}"
10435            ))
10436        })?;
10437        let target = match fs::read_link(entry.path()) {
10438            Ok(target) => target,
10439            Err(_) => continue,
10440        };
10441        if let Some(inode) = parse_socket_inode(&target) {
10442            inodes.insert(inode);
10443        }
10444    }
10445
10446    Ok(inodes)
10447}
10448
10449fn parse_socket_inode(target: &Path) -> Option<u64> {
10450    let value = target.to_string_lossy();
10451    let trimmed = value.strip_prefix("socket:[")?.strip_suffix(']')?;
10452    trimmed.parse().ok()
10453}
10454
10455fn unix_socket_path(addr: &UnixSocketAddr) -> Option<String> {
10456    addr.as_pathname()
10457        .map(|path| path.to_string_lossy().into_owned())
10458}
10459
10460fn find_unix_socket_for_pid(
10461    pid: u32,
10462    inodes: &BTreeSet<u64>,
10463    path: &str,
10464    process_id: &str,
10465) -> Result<Option<SocketStateEntry>, SidecarError> {
10466    let table_path = format!("/proc/{pid}/net/unix");
10467    let contents = match fs::read_to_string(&table_path) {
10468        Ok(contents) => contents,
10469        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
10470        Err(error) => {
10471            return Err(SidecarError::Io(format!(
10472                "failed to inspect unix sockets for process {pid}: {error}"
10473            )));
10474        }
10475    };
10476
10477    for line in contents.lines().skip(1) {
10478        let columns = line.split_whitespace().collect::<Vec<_>>();
10479        if columns.len() < 8 {
10480            continue;
10481        }
10482        let Ok(inode) = columns[6].parse::<u64>() else {
10483            continue;
10484        };
10485        if !inodes.contains(&inode) || columns[7] != path {
10486            continue;
10487        }
10488        return Ok(Some(SocketStateEntry {
10489            process_id: process_id.to_owned(),
10490            host: None,
10491            port: None,
10492            path: Some(path.to_owned()),
10493        }));
10494    }
10495
10496    Ok(None)
10497}
10498
10499fn find_inet_socket_for_pid(
10500    table_path: &str,
10501    inodes: &BTreeSet<u64>,
10502    kind: SocketQueryKind,
10503    requested_host: Option<&str>,
10504    requested_port: Option<u16>,
10505    process_id: &str,
10506) -> Result<Option<SocketStateEntry>, SidecarError> {
10507    for entry in parse_proc_net_entries(table_path)? {
10508        if !inodes.contains(&entry.inode) {
10509            continue;
10510        }
10511        if matches!(kind, SocketQueryKind::TcpListener) && entry.state != "0A" {
10512            continue;
10513        }
10514        if !socket_host_matches(requested_host, &entry.local_host) {
10515            continue;
10516        }
10517        if let Some(port) = requested_port {
10518            if entry.local_port != port {
10519                continue;
10520            }
10521        }
10522        return Ok(Some(SocketStateEntry {
10523            process_id: process_id.to_owned(),
10524            host: Some(entry.local_host),
10525            port: Some(entry.local_port),
10526            path: None,
10527        }));
10528    }
10529
10530    Ok(None)
10531}
10532
10533fn is_unspecified_socket_host(host: &str) -> bool {
10534    host == "0.0.0.0" || host == "::"
10535}
10536
10537fn is_loopback_socket_host(host: &str) -> bool {
10538    host == "127.0.0.1" || host == "::1" || host.eq_ignore_ascii_case("localhost")
10539}
10540
10541pub(crate) fn vm_network_resource_counts(vm: &VmState) -> NetworkResourceCounts {
10542    let snapshot = vm.kernel.resource_snapshot();
10543    let mut counts = NetworkResourceCounts {
10544        sockets: snapshot.sockets,
10545        connections: snapshot.socket_connections,
10546    };
10547    for process in vm.active_processes.values() {
10548        let process_counts = process.sidecar_only_network_resource_counts();
10549        counts.sockets += process_counts.sockets;
10550        counts.connections += process_counts.connections;
10551    }
10552    counts
10553}
10554
10555fn collect_javascript_socket_port_state(
10556    kernel: &SidecarKernel,
10557    process_id: &str,
10558    process: &ActiveProcess,
10559    tcp_guest_to_host: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10560    http_loopback_targets: &mut BTreeMap<
10561        (JavascriptSocketFamily, u16),
10562        JavascriptHttpLoopbackTarget,
10563    >,
10564    udp_guest_to_host: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10565    udp_host_to_guest: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10566    used_tcp_ports: &mut BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10567    used_udp_ports: &mut BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10568) {
10569    for (family, port) in process.tcp_port_reservations.values() {
10570        used_tcp_ports.entry(*family).or_default().insert(*port);
10571    }
10572
10573    let mut record_tcp_listener = |guest_addr: SocketAddr, host_port: u16| {
10574        let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10575        used_tcp_ports
10576            .entry(family)
10577            .or_default()
10578            .insert(guest_addr.port());
10579        // VM-local loopback connects should also resolve listeners bound to
10580        // unspecified guest addresses like 0.0.0.0/::.
10581        tcp_guest_to_host.insert((family, guest_addr.port()), host_port);
10582    };
10583
10584    for listener in process.tcp_listeners.values() {
10585        let local_addr = listener
10586            .kernel_socket_id
10587            .and_then(|socket_id| kernel.socket_get(socket_id))
10588            .and_then(|record| record.local_address().cloned())
10589            .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
10590            .unwrap_or_else(|| listener.guest_local_addr());
10591        record_tcp_listener(local_addr, local_addr.port());
10592    }
10593
10594    for (server_id, server) in &process.http_servers {
10595        let host_port = match server.listener.local_addr() {
10596            Ok(addr) => addr.port(),
10597            Err(_) => continue,
10598        };
10599        record_tcp_listener(server.guest_local_addr, host_port);
10600        let family = JavascriptSocketFamily::from_ip(server.guest_local_addr.ip());
10601        http_loopback_targets.insert(
10602            (family, server.guest_local_addr.port()),
10603            JavascriptHttpLoopbackTarget {
10604                process_id: process_id.to_owned(),
10605                server_id: *server_id,
10606            },
10607        );
10608    }
10609
10610    if let Ok(http2) = process.http2.shared.lock() {
10611        for server in http2.servers.values() {
10612            record_tcp_listener(server.guest_local_addr, server.actual_local_addr.port());
10613        }
10614    }
10615
10616    for socket in process.tcp_sockets.values() {
10617        let guest_addr = socket
10618            .kernel_socket_id
10619            .and_then(|socket_id| kernel.socket_get(socket_id))
10620            .and_then(|record| record.local_address().cloned())
10621            .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
10622            .unwrap_or(socket.guest_local_addr);
10623        let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10624        used_tcp_ports
10625            .entry(family)
10626            .or_default()
10627            .insert(guest_addr.port());
10628    }
10629
10630    for socket in process.udp_sockets.values() {
10631        let guest_addr = socket
10632            .kernel_socket_id
10633            .and_then(|socket_id| kernel.socket_get(socket_id))
10634            .and_then(|record| record.local_address().cloned())
10635            .and_then(|address| {
10636                resolve_udp_bind_addr(address.host(), address.port(), socket.family).ok()
10637            })
10638            .or_else(|| socket.local_addr());
10639        let Some(guest_addr) = guest_addr else {
10640            continue;
10641        };
10642        let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10643        used_udp_ports
10644            .entry(family)
10645            .or_default()
10646            .insert(guest_addr.port());
10647        if let Some(host_addr) = socket
10648            .socket
10649            .as_ref()
10650            .and_then(|socket| socket.local_addr().ok())
10651        {
10652            if is_loopback_ip(guest_addr.ip()) {
10653                udp_guest_to_host.insert((family, guest_addr.port()), host_addr.port());
10654                udp_host_to_guest.insert((family, host_addr.port()), guest_addr.port());
10655            }
10656        } else if socket.kernel_socket_id.is_some() && is_loopback_ip(guest_addr.ip()) {
10657            udp_guest_to_host.insert((family, guest_addr.port()), guest_addr.port());
10658            udp_host_to_guest.insert((family, guest_addr.port()), guest_addr.port());
10659        }
10660    }
10661
10662    for (child_process_id, child) in &process.child_processes {
10663        let child_id = format!("{process_id}/{child_process_id}");
10664        collect_javascript_socket_port_state(
10665            kernel,
10666            &child_id,
10667            child,
10668            tcp_guest_to_host,
10669            http_loopback_targets,
10670            udp_guest_to_host,
10671            udp_host_to_guest,
10672            used_tcp_ports,
10673            used_udp_ports,
10674        );
10675    }
10676}
10677
10678pub(crate) fn build_javascript_socket_path_context(
10679    vm: &VmState,
10680) -> Result<JavascriptSocketPathContext, SidecarError> {
10681    let mut loopback_exempt_ports = vm.create_loopback_exempt_ports.clone();
10682    loopback_exempt_ports.extend(vm.configuration.loopback_exempt_ports.iter().copied());
10683    let mut tcp_loopback_guest_to_host_ports = BTreeMap::new();
10684    let mut http_loopback_targets = BTreeMap::new();
10685    let mut udp_loopback_guest_to_host_ports = BTreeMap::new();
10686    let mut udp_loopback_host_to_guest_ports = BTreeMap::new();
10687    let mut used_tcp_guest_ports = BTreeMap::new();
10688    let mut used_udp_guest_ports = BTreeMap::new();
10689    for (process_id, process) in &vm.active_processes {
10690        collect_javascript_socket_port_state(
10691            &vm.kernel,
10692            process_id,
10693            process,
10694            &mut tcp_loopback_guest_to_host_ports,
10695            &mut http_loopback_targets,
10696            &mut udp_loopback_guest_to_host_ports,
10697            &mut udp_loopback_host_to_guest_ports,
10698            &mut used_tcp_guest_ports,
10699            &mut used_udp_guest_ports,
10700        );
10701    }
10702    Ok(JavascriptSocketPathContext {
10703        sandbox_root: vm.cwd.clone(),
10704        mounts: vm.configuration.mounts.clone(),
10705        listen_policy: vm.listen_policy,
10706        loopback_exempt_ports,
10707        tcp_loopback_guest_to_host_ports,
10708        http_loopback_targets,
10709        udp_loopback_guest_to_host_ports,
10710        udp_loopback_host_to_guest_ports,
10711        used_tcp_guest_ports,
10712        used_udp_guest_ports,
10713    })
10714}
10715
10716fn check_network_resource_limit(
10717    limit: Option<usize>,
10718    current: usize,
10719    additional: usize,
10720    label: &str,
10721) -> Result<(), SidecarError> {
10722    if let Some(limit) = limit {
10723        if current.saturating_add(additional) > limit {
10724            return Err(SidecarError::Execution(format!(
10725                "EAGAIN: maximum {label} count reached"
10726            )));
10727        }
10728    }
10729    Ok(())
10730}
10731
10732fn normalize_tcp_listen_host(
10733    host: Option<&str>,
10734) -> Result<(JavascriptSocketFamily, &'static str, &'static str), SidecarError> {
10735    match host.unwrap_or("127.0.0.1") {
10736        "127.0.0.1" | "localhost" => Ok((JavascriptSocketFamily::Ipv4, "127.0.0.1", "127.0.0.1")),
10737        "::1" => Ok((JavascriptSocketFamily::Ipv6, "::1", "::1")),
10738        "0.0.0.0" => Ok((JavascriptSocketFamily::Ipv4, "127.0.0.1", "0.0.0.0")),
10739        "::" => Ok((JavascriptSocketFamily::Ipv6, "::1", "::")),
10740        other => Err(SidecarError::Execution(format!(
10741            "EACCES: TCP listeners must bind to loopback or unspecified addresses, got {other}"
10742        ))),
10743    }
10744}
10745
10746fn normalize_udp_bind_host(
10747    host: Option<&str>,
10748    family: JavascriptUdpFamily,
10749) -> Result<(&'static str, &'static str, JavascriptSocketFamily), SidecarError> {
10750    match (family, host) {
10751        (JavascriptUdpFamily::Ipv4, None) | (JavascriptUdpFamily::Ipv4, Some("0.0.0.0")) => {
10752            Ok(("127.0.0.1", "0.0.0.0", JavascriptSocketFamily::Ipv4))
10753        }
10754        (JavascriptUdpFamily::Ipv4, Some("127.0.0.1"))
10755        | (JavascriptUdpFamily::Ipv4, Some("localhost")) => {
10756            Ok(("127.0.0.1", "127.0.0.1", JavascriptSocketFamily::Ipv4))
10757        }
10758        (JavascriptUdpFamily::Ipv6, None) | (JavascriptUdpFamily::Ipv6, Some("::")) => {
10759            Ok(("::1", "::", JavascriptSocketFamily::Ipv6))
10760        }
10761        (JavascriptUdpFamily::Ipv6, Some("::1"))
10762        | (JavascriptUdpFamily::Ipv6, Some("localhost")) => {
10763            Ok(("::1", "::1", JavascriptSocketFamily::Ipv6))
10764        }
10765        (JavascriptUdpFamily::Ipv4, Some(other)) => Err(SidecarError::Execution(format!(
10766            "EACCES: udp4 sockets must bind to 127.0.0.1 or 0.0.0.0, got {other}"
10767        ))),
10768        (JavascriptUdpFamily::Ipv6, Some(other)) => Err(SidecarError::Execution(format!(
10769            "EACCES: udp6 sockets must bind to ::1 or ::, got {other}"
10770        ))),
10771    }
10772}
10773
10774fn allocate_guest_listen_port(
10775    requested_port: u16,
10776    family: JavascriptSocketFamily,
10777    used_ports: &BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10778    policy: VmListenPolicy,
10779) -> Result<u16, SidecarError> {
10780    let is_allowed = |port: u16| {
10781        port >= policy.port_min
10782            && port <= policy.port_max
10783            && (policy.allow_privileged || port >= 1024)
10784    };
10785    let used = used_ports.get(&family);
10786
10787    if requested_port != 0 {
10788        if !is_allowed(requested_port) {
10789            let reason = if requested_port < 1024 && !policy.allow_privileged {
10790                format!(
10791                    "EACCES: privileged listen port {requested_port} requires {}=true",
10792                    VM_LISTEN_ALLOW_PRIVILEGED_METADATA_KEY
10793                )
10794            } else {
10795                format!(
10796                    "EACCES: listen port {requested_port} is outside the allowed range {}-{}",
10797                    policy.port_min, policy.port_max
10798                )
10799            };
10800            return Err(SidecarError::Execution(reason));
10801        }
10802        if used.is_some_and(|ports| ports.contains(&requested_port)) {
10803            return Err(sidecar_net_error(std::io::Error::from_raw_os_error(
10804                libc::EADDRINUSE,
10805            )));
10806        }
10807        return Ok(requested_port);
10808    }
10809
10810    let allocation_start = policy
10811        .port_min
10812        .max(if policy.allow_privileged { 1 } else { 1024 });
10813    for candidate in allocation_start..=policy.port_max {
10814        if used.is_some_and(|ports| ports.contains(&candidate)) {
10815            continue;
10816        }
10817        return Ok(candidate);
10818    }
10819
10820    Err(sidecar_net_error(std::io::Error::from_raw_os_error(
10821        libc::EADDRINUSE,
10822    )))
10823}
10824
10825fn socket_host_matches(requested: Option<&str>, actual: &str) -> bool {
10826    match requested {
10827        None => true,
10828        Some(requested) if requested == actual => true,
10829        Some(requested)
10830            if is_unspecified_socket_host(requested) && is_unspecified_socket_host(actual) =>
10831        {
10832            true
10833        }
10834        Some(requested) if is_unspecified_socket_host(requested) => is_loopback_socket_host(actual),
10835        Some(requested) if requested.eq_ignore_ascii_case("localhost") => {
10836            is_loopback_socket_host(actual)
10837        }
10838        _ => false,
10839    }
10840}
10841
10842fn parse_proc_net_entries(table_path: &str) -> Result<Vec<ProcNetEntry>, SidecarError> {
10843    let contents = match fs::read_to_string(table_path) {
10844        Ok(contents) => contents,
10845        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
10846        Err(error) => {
10847            return Err(SidecarError::Io(format!(
10848                "failed to inspect socket table {table_path}: {error}"
10849            )));
10850        }
10851    };
10852
10853    let mut entries = Vec::new();
10854    for line in contents.lines().skip(1) {
10855        let columns = line.split_whitespace().collect::<Vec<_>>();
10856        if columns.len() < 10 {
10857            continue;
10858        }
10859        let Some((host, port)) = parse_proc_ip_port(columns[1]) else {
10860            continue;
10861        };
10862        let Ok(inode) = columns[9].parse::<u64>() else {
10863            continue;
10864        };
10865        entries.push(ProcNetEntry {
10866            local_host: host,
10867            local_port: port,
10868            state: columns[3].to_owned(),
10869            inode,
10870        });
10871    }
10872
10873    Ok(entries)
10874}
10875
10876fn parse_proc_ip_port(value: &str) -> Option<(String, u16)> {
10877    let (raw_ip, raw_port) = value.split_once(':')?;
10878    let port = u16::from_str_radix(raw_port, 16).ok()?;
10879    let host = match raw_ip.len() {
10880        8 => {
10881            let raw = u32::from_str_radix(raw_ip, 16).ok()?;
10882            Ipv4Addr::from(raw.to_le_bytes()).to_string()
10883        }
10884        32 => {
10885            let mut bytes = [0_u8; 16];
10886            for (index, chunk) in raw_ip.as_bytes().chunks(8).enumerate() {
10887                let word = u32::from_str_radix(std::str::from_utf8(chunk).ok()?, 16).ok()?;
10888                bytes[index * 4..(index + 1) * 4].copy_from_slice(&word.to_le_bytes());
10889            }
10890            Ipv6Addr::from(bytes).to_string()
10891        }
10892        _ => return None,
10893    };
10894    Some((host, port))
10895}
10896
10897fn python_file_entrypoint(entrypoint: &str) -> Option<PathBuf> {
10898    let path = Path::new(entrypoint);
10899    (path.extension().and_then(|extension| extension.to_str()) == Some("py"))
10900        .then(|| path.to_path_buf())
10901}
10902
10903fn add_runtime_guest_path_mapping(
10904    env: &mut BTreeMap<String, String>,
10905    guest_path: &str,
10906    host_path: &Path,
10907) {
10908    let mut mappings = env
10909        .get("AGENTOS_GUEST_PATH_MAPPINGS")
10910        .and_then(|value| serde_json::from_str::<Vec<Value>>(value).ok())
10911        .unwrap_or_default();
10912    mappings.retain(|mapping| {
10913        mapping
10914            .get("guestPath")
10915            .and_then(Value::as_str)
10916            .map(|existing| normalize_path(existing) != normalize_path(guest_path))
10917            .unwrap_or(true)
10918    });
10919    mappings.push(json!({
10920        "guestPath": normalize_path(guest_path),
10921        "hostPath": host_path.display().to_string(),
10922    }));
10923    if let Ok(serialized) = serde_json::to_string(&mappings) {
10924        env.insert(String::from("AGENTOS_GUEST_PATH_MAPPINGS"), serialized);
10925    }
10926}
10927
10928fn add_runtime_host_access_path(
10929    env: &mut BTreeMap<String, String>,
10930    key: &str,
10931    host_path: &Path,
10932    expand: bool,
10933) {
10934    let existing = env
10935        .get(key)
10936        .and_then(|value| serde_json::from_str::<Vec<String>>(value).ok())
10937        .unwrap_or_default()
10938        .into_iter()
10939        .map(PathBuf::from)
10940        .collect::<Vec<_>>();
10941    let mut paths = existing;
10942    paths.push(host_path.to_path_buf());
10943    let normalized = if expand {
10944        expand_host_access_paths(&paths)
10945    } else {
10946        dedupe_host_paths(&paths)
10947    };
10948    let serialized = normalized
10949        .iter()
10950        .map(|path| path.to_string_lossy().into_owned())
10951        .collect::<Vec<_>>();
10952    if let Ok(serialized) = serde_json::to_string(&serialized) {
10953        env.insert(key.to_owned(), serialized);
10954    }
10955}
10956
10957// discover_command_guest_paths moved to crate::bootstrap
10958
10959fn is_path_like_specifier(specifier: &str) -> bool {
10960    specifier.starts_with('/')
10961        || specifier.starts_with("./")
10962        || specifier.starts_with("../")
10963        || specifier.starts_with("file:")
10964}
10965
10966fn execution_wasm_permission_tier(tier: WasmPermissionTier) -> ExecutionWasmPermissionTier {
10967    match tier {
10968        WasmPermissionTier::Full => ExecutionWasmPermissionTier::Full,
10969        WasmPermissionTier::ReadWrite => ExecutionWasmPermissionTier::ReadWrite,
10970        WasmPermissionTier::ReadOnly => ExecutionWasmPermissionTier::ReadOnly,
10971        WasmPermissionTier::Isolated => ExecutionWasmPermissionTier::Isolated,
10972    }
10973}
10974
10975fn resolve_wasm_permission_tier(
10976    vm: &VmState,
10977    command_name: Option<&str>,
10978    explicit_tier: Option<WasmPermissionTier>,
10979    entrypoint: &str,
10980) -> WasmPermissionTier {
10981    explicit_tier
10982        .or_else(|| command_name.and_then(|command| vm.command_permissions.get(command).copied()))
10983        .or_else(|| {
10984            Path::new(entrypoint)
10985                .file_name()
10986                .and_then(|name| name.to_str())
10987                .and_then(|command| vm.command_permissions.get(command).copied())
10988        })
10989        .unwrap_or(WasmPermissionTier::Full)
10990}
10991
10992fn tokenize_shell_free_command(command: &str) -> Vec<String> {
10993    command
10994        .split_whitespace()
10995        .filter(|segment| !segment.is_empty())
10996        .map(str::to_owned)
10997        .collect()
10998}
10999
11000fn is_posix_shell_builtin(command: &str) -> bool {
11001    matches!(
11002        command,
11003        "." | ":"
11004            | "break"
11005            | "cd"
11006            | "continue"
11007            | "eval"
11008            | "exec"
11009            | "exit"
11010            | "export"
11011            | "readonly"
11012            | "return"
11013            | "set"
11014            | "shift"
11015            | "times"
11016            | "trap"
11017            | "umask"
11018            | "unset"
11019    )
11020}
11021
11022/// Single-token checks for shell-mode commands whose first word forces a real
11023/// shell even when the command string has no shell metacharacters. This is not
11024/// a parser: env-assignment prefixes (`FOO=bar cmd`) and shell reserved words
11025/// have no meaning outside `sh`, so whitespace-tokenizing them would silently
11026/// run the wrong program.
11027fn shell_first_token_requires_shell(token: &str) -> bool {
11028    token.contains('=') || is_shell_reserved_word(token)
11029}
11030
11031fn is_shell_reserved_word(token: &str) -> bool {
11032    matches!(
11033        token,
11034        "if" | "then"
11035            | "elif"
11036            | "else"
11037            | "fi"
11038            | "for"
11039            | "in"
11040            | "do"
11041            | "done"
11042            | "while"
11043            | "until"
11044            | "case"
11045            | "esac"
11046            | "{"
11047            | "}"
11048            | "!"
11049    )
11050}
11051
11052fn command_requires_shell(command: &str) -> bool {
11053    command.chars().any(|ch| {
11054        matches!(
11055            ch,
11056            '|' | '&'
11057                | ';'
11058                | '<'
11059                | '>'
11060                | '('
11061                | ')'
11062                | '$'
11063                | '`'
11064                | '*'
11065                | '?'
11066                | '['
11067                | ']'
11068                | '{'
11069                | '}'
11070                | '~'
11071                | '\''
11072                | '"'
11073                | '\\'
11074                | '\n'
11075        )
11076    })
11077}
11078
11079fn host_mount_path_for_guest_path(vm: &VmState, guest_path: &str) -> Option<PathBuf> {
11080    let normalized = normalize_path(guest_path);
11081
11082    let mut mounts = vm
11083        .configuration
11084        .mounts
11085        .iter()
11086        .filter_map(|mount| {
11087            ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
11088                .then(|| {
11089                    mount_config_host_path(&mount.plugin.config)
11090                        .map(|host_path| (mount.guest_path.as_str(), host_path))
11091                })
11092                .flatten()
11093        })
11094        .collect::<Vec<_>>();
11095    mounts.sort_by_key(|mount| std::cmp::Reverse(mount.0.len()));
11096
11097    for (guest_root, host_root) in mounts {
11098        if normalized != guest_root && !normalized.starts_with(&format!("{guest_root}/")) {
11099            continue;
11100        }
11101
11102        let suffix = normalized
11103            .strip_prefix(guest_root)
11104            .unwrap_or_default()
11105            .trim_start_matches('/');
11106        let mut path = PathBuf::from(host_root);
11107        if !suffix.is_empty() {
11108            path.push(suffix);
11109        }
11110        return Some(path);
11111    }
11112
11113    None
11114}
11115
11116fn host_runtime_path_for_guest_path_with_env(
11117    vm: &VmState,
11118    runtime_env: &BTreeMap<String, String>,
11119    guest_path: &str,
11120    default_host_cwd: &Path,
11121) -> Option<PathBuf> {
11122    if let Some(path) = host_mount_path_for_guest_path(vm, guest_path) {
11123        return Some(path);
11124    }
11125    if let Some(path) = host_path_from_runtime_guest_mappings(runtime_env, guest_path) {
11126        return Some(path);
11127    }
11128
11129    let normalized = normalize_path(guest_path);
11130    let virtual_home = guest_virtual_home(vm);
11131
11132    if normalized == virtual_home || normalized.starts_with(&format!("{virtual_home}/")) {
11133        let suffix = normalized
11134            .strip_prefix(&virtual_home)
11135            .unwrap_or_default()
11136            .trim_start_matches('/');
11137        let mut host_path = default_host_cwd.to_path_buf();
11138        if !suffix.is_empty() {
11139            host_path.push(suffix);
11140        }
11141        return Some(host_path);
11142    }
11143
11144    None
11145}
11146
11147#[derive(Deserialize, Serialize)]
11148struct RuntimeGuestPathMapping {
11149    #[serde(rename = "guestPath")]
11150    guest_path: String,
11151    #[serde(rename = "hostPath")]
11152    host_path: String,
11153    #[serde(rename = "readOnly", default)]
11154    read_only: bool,
11155}
11156
11157pub(crate) fn host_path_from_runtime_guest_mappings(
11158    runtime_env: &BTreeMap<String, String>,
11159    guest_path: &str,
11160) -> Option<PathBuf> {
11161    let mappings = runtime_env
11162        .get("AGENTOS_GUEST_PATH_MAPPINGS")
11163        .and_then(|value| serde_json::from_str::<Vec<RuntimeGuestPathMapping>>(value).ok())?;
11164    let normalized = normalize_path(guest_path);
11165
11166    let mut sorted_mappings = mappings
11167        .into_iter()
11168        .filter_map(|mapping| {
11169            (!mapping.guest_path.is_empty() && !mapping.host_path.is_empty()).then_some((
11170                normalize_path(&mapping.guest_path),
11171                PathBuf::from(mapping.host_path),
11172            ))
11173        })
11174        .collect::<Vec<_>>();
11175    sorted_mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.0.len()));
11176
11177    for (guest_root, mut host_root) in sorted_mappings {
11178        if guest_root != "/"
11179            && normalized != guest_root
11180            && !normalized.starts_with(&format!("{guest_root}/"))
11181        {
11182            continue;
11183        }
11184        if guest_root == "/" && !normalized.starts_with('/') {
11185            continue;
11186        }
11187
11188        if host_root.is_relative() {
11189            host_root = std::env::current_dir().ok()?.join(host_root);
11190        }
11191
11192        let suffix = if guest_root == "/" {
11193            normalized.trim_start_matches('/')
11194        } else {
11195            normalized
11196                .strip_prefix(&guest_root)
11197                .unwrap_or_default()
11198                .trim_start_matches('/')
11199        };
11200        if !suffix.is_empty() {
11201            host_root.push(suffix);
11202        }
11203        return Some(host_root);
11204    }
11205
11206    None
11207}
11208
11209fn guest_runtime_path_for_host_path(
11210    runtime_env: &BTreeMap<String, String>,
11211    virtual_home: &str,
11212    cwd: &Path,
11213    host_path: &str,
11214) -> Option<String> {
11215    let resolved = if host_path.starts_with("file://") {
11216        PathBuf::from(host_path.trim_start_matches("file://"))
11217    } else if host_path.starts_with("file:") {
11218        PathBuf::from(host_path.trim_start_matches("file:"))
11219    } else {
11220        let candidate = PathBuf::from(host_path);
11221        if candidate.is_absolute() {
11222            candidate
11223        } else if host_path.starts_with("./") || host_path.starts_with("../") {
11224            cwd.join(candidate)
11225        } else {
11226            return None;
11227        }
11228    };
11229    let normalized = normalize_host_path(&resolved);
11230
11231    if let Some(path) = guest_path_from_runtime_host_mappings(runtime_env, &normalized) {
11232        return Some(path);
11233    }
11234
11235    let normalized_cwd = normalize_host_path(cwd);
11236    if !path_is_within_root(&normalized, &normalized_cwd) {
11237        return None;
11238    }
11239
11240    let virtual_home = if virtual_home.starts_with('/') {
11241        virtual_home.to_string()
11242    } else {
11243        String::from("/root")
11244    };
11245    let suffix = normalized
11246        .strip_prefix(&normalized_cwd)
11247        .ok()?
11248        .to_string_lossy()
11249        .replace('\\', "/")
11250        .trim_start_matches('/')
11251        .to_owned();
11252
11253    Some(if suffix.is_empty() {
11254        virtual_home
11255    } else {
11256        normalize_path(&format!("{virtual_home}/{suffix}"))
11257    })
11258}
11259
11260fn guest_path_from_runtime_host_mappings(
11261    runtime_env: &BTreeMap<String, String>,
11262    host_path: &Path,
11263) -> Option<String> {
11264    let mappings = runtime_env
11265        .get("AGENTOS_GUEST_PATH_MAPPINGS")
11266        .and_then(|value| serde_json::from_str::<Vec<RuntimeGuestPathMapping>>(value).ok())?;
11267    let normalized = normalize_host_path(host_path);
11268
11269    let mut sorted_mappings = mappings
11270        .into_iter()
11271        .filter_map(|mapping| {
11272            (!mapping.guest_path.is_empty() && !mapping.host_path.is_empty()).then_some((
11273                normalize_path(&mapping.guest_path),
11274                normalize_host_path(Path::new(&mapping.host_path)),
11275            ))
11276        })
11277        .collect::<Vec<_>>();
11278    sorted_mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.1.as_os_str().len()));
11279
11280    for (guest_root, host_root) in sorted_mappings {
11281        if !path_is_within_root(&normalized, &host_root) {
11282            continue;
11283        }
11284        let suffix = normalized
11285            .strip_prefix(&host_root)
11286            .ok()?
11287            .to_string_lossy()
11288            .replace('\\', "/")
11289            .trim_start_matches('/')
11290            .to_owned();
11291
11292        return Some(if suffix.is_empty() {
11293            guest_root
11294        } else if guest_root == "/" {
11295            normalize_path(&format!("/{suffix}"))
11296        } else {
11297            normalize_path(&format!("{guest_root}/{suffix}"))
11298        });
11299    }
11300
11301    None
11302}
11303
11304fn host_mount_path_for_guest_path_from_mounts(
11305    mounts: &[crate::protocol::MountDescriptor],
11306    guest_path: &str,
11307) -> Option<PathBuf> {
11308    let normalized = normalize_path(guest_path);
11309
11310    let mut host_mounts = mounts
11311        .iter()
11312        .filter_map(|mount| {
11313            ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
11314                .then(|| {
11315                    mount_config_host_path(&mount.plugin.config)
11316                        .map(|host_path| (mount.guest_path.as_str(), host_path))
11317                })
11318                .flatten()
11319        })
11320        .collect::<Vec<_>>();
11321    host_mounts.sort_by_key(|mount| std::cmp::Reverse(mount.0.len()));
11322
11323    for (guest_root, host_root) in host_mounts {
11324        if normalized != guest_root && !normalized.starts_with(&format!("{guest_root}/")) {
11325            continue;
11326        }
11327
11328        let suffix = normalized
11329            .strip_prefix(guest_root)
11330            .unwrap_or_default()
11331            .trim_start_matches('/');
11332        let mut path = PathBuf::from(host_root);
11333        if !suffix.is_empty() {
11334            path.push(suffix);
11335        }
11336        return Some(path);
11337    }
11338
11339    None
11340}
11341
11342#[cfg(test)]
11343mod host_mount_path_for_guest_path_from_mounts_tests {
11344    use super::host_mount_path_for_guest_path_from_mounts;
11345    use crate::protocol::{MountDescriptor, MountPluginDescriptor};
11346    use serde_json::json;
11347    use std::path::PathBuf;
11348
11349    #[test]
11350    fn resolves_module_access_mount_paths() {
11351        let mounts = vec![MountDescriptor {
11352            guest_path: String::from("/root/node_modules"),
11353            read_only: true,
11354            plugin: MountPluginDescriptor {
11355                id: String::from("module_access"),
11356                config: json!({
11357                    "hostPath": "/tmp/workspace/node_modules",
11358                })
11359                .to_string(),
11360            },
11361        }];
11362
11363        let resolved =
11364            host_mount_path_for_guest_path_from_mounts(&mounts, "/root/node_modules/pkg/index.js")
11365                .expect("module_access mount should resolve");
11366
11367        assert_eq!(
11368            resolved,
11369            PathBuf::from("/tmp/workspace/node_modules/pkg/index.js")
11370        );
11371    }
11372}
11373
11374fn resolve_guest_socket_host_path(
11375    context: &JavascriptSocketPathContext,
11376    guest_path: &str,
11377) -> PathBuf {
11378    if let Some(path) = host_mount_path_for_guest_path_from_mounts(&context.mounts, guest_path) {
11379        return path;
11380    }
11381
11382    let normalized = normalize_path(guest_path);
11383    let mut host_path = context.sandbox_root.clone();
11384    let suffix = normalized.trim_start_matches('/');
11385    if !suffix.is_empty() {
11386        host_path.push(suffix);
11387    }
11388    host_path
11389}
11390
11391fn ensure_kernel_parent_directories(
11392    kernel: &mut SidecarKernel,
11393    path: &str,
11394) -> Result<(), SidecarError> {
11395    let parent = dirname(path);
11396    if parent != "/" && !kernel.exists(&parent).map_err(kernel_error)? {
11397        kernel.mkdir(&parent, true).map_err(kernel_error)?;
11398    }
11399    Ok(())
11400}
11401
11402// JavascriptChildProcessSpawnOptions, JavascriptChildProcessSpawnRequest moved to crate::protocol
11403// ResolvedChildProcessExecution moved to crate::state
11404
11405pub(crate) fn sanitize_javascript_child_process_internal_bootstrap_env(
11406    env: &BTreeMap<String, String>,
11407) -> BTreeMap<String, String> {
11408    const ALLOWED_KEYS: &[&str] = &[
11409        "AGENTOS_ALLOWED_NODE_BUILTINS",
11410        "AGENTOS_GUEST_PATH_MAPPINGS",
11411        "AGENTOS_LOOPBACK_EXEMPT_PORTS",
11412        "AGENTOS_VIRTUAL_PROCESS_EXEC_PATH",
11413        "AGENTOS_VIRTUAL_PROCESS_UID",
11414        "AGENTOS_VIRTUAL_PROCESS_GID",
11415        "AGENTOS_VIRTUAL_PROCESS_VERSION",
11416    ];
11417
11418    env.iter()
11419        .filter(|(key, _)| {
11420            ALLOWED_KEYS.contains(&key.as_str()) || key.starts_with("AGENTOS_VIRTUAL_OS_")
11421        })
11422        .map(|(key, value)| (key.clone(), value.clone()))
11423        .collect()
11424}
11425
11426// Network request types moved to crate::protocol
11427
11428// VmDnsConfig, DnsResolutionSource moved to crate::state
11429
11430fn resolve_tcp_bind_addr(host: &str, port: u16) -> Result<SocketAddr, SidecarError> {
11431    (host, port)
11432        .to_socket_addrs()
11433        .map_err(sidecar_net_error)?
11434        .next()
11435        .ok_or_else(|| {
11436            SidecarError::Execution(format!("failed to resolve TCP bind address {host}:{port}"))
11437        })
11438}
11439
11440pub(crate) fn format_dns_resource(hostname: &str) -> String {
11441    format!("dns://{hostname}")
11442}
11443
11444pub(crate) fn format_tcp_resource(host: &str, port: u16) -> String {
11445    format!("tcp://{host}:{port}")
11446}
11447
11448fn is_loopback_ip(ip: IpAddr) -> bool {
11449    match ip {
11450        IpAddr::V4(ip) => ip.is_loopback(),
11451        IpAddr::V6(ip) => {
11452            ip.is_loopback()
11453                || ip
11454                    .to_ipv4_mapped()
11455                    .is_some_and(|mapped| mapped.is_loopback())
11456        }
11457    }
11458}
11459
11460fn loopback_cidr(ip: IpAddr) -> &'static str {
11461    match ip {
11462        IpAddr::V4(ip) if ip.is_loopback() => "127.0.0.0/8",
11463        IpAddr::V6(ip)
11464            if ip
11465                .to_ipv4_mapped()
11466                .is_some_and(|mapped| mapped.is_loopback()) =>
11467        {
11468            "127.0.0.0/8"
11469        }
11470        IpAddr::V6(_) => "::1/128",
11471        IpAddr::V4(_) => "127.0.0.0/8",
11472    }
11473}
11474
11475/// Returns the embedded IPv4 address of an IPv4-compatible IPv6 address
11476/// (`::a.b.c.d`): the first six 16-bit segments are zero and the final 32 bits
11477/// hold the IPv4 address. The all-zero (`::`) and loopback (`::1`) addresses are
11478/// deliberately excluded so they are handled by the unspecified/loopback paths
11479/// rather than treated as IPv4-compatible.
11480fn ipv4_compatible_embedded(ip: Ipv6Addr) -> Option<Ipv4Addr> {
11481    let segments = ip.segments();
11482    if segments[0..6].iter().any(|&s| s != 0) {
11483        return None;
11484    }
11485    let embedded = (u32::from(segments[6]) << 16) | u32::from(segments[7]);
11486    // Skip :: (0.0.0.0) and ::1 (0.0.0.1) — these are the IPv6 unspecified /
11487    // loopback addresses, not IPv4-compatible representations of an IPv4 host.
11488    if embedded == 0 || embedded == 1 {
11489        return None;
11490    }
11491    Some(Ipv4Addr::from(embedded))
11492}
11493
11494fn restricted_non_loopback_ip_range(ip: IpAddr) -> Option<(&'static str, &'static str)> {
11495    match ip {
11496        IpAddr::V4(ip) => {
11497            if ip.is_unspecified() {
11498                // 0.0.0.0 is unspecified; the host stack routes a connect() to
11499                // it back to 127.0.0.1, so it must not bypass the loopback gate.
11500                return Some(("0.0.0.0/32", "unspecified"));
11501            }
11502            let [first, second, ..] = ip.octets();
11503            match (first, second) {
11504                (10, _) => Some(("10.0.0.0/8", "private")),
11505                (100, 64..=127) => Some(("100.64.0.0/10", "carrier-grade-nat")),
11506                (172, 16..=31) => Some(("172.16.0.0/12", "private")),
11507                (192, 168) => Some(("192.168.0.0/16", "private")),
11508                (169, 254) => Some(("169.254.0.0/16", "link-local")),
11509                // 224.0.0.0/4 is the IPv4 multicast range and 240.0.0.0/4 is
11510                // reserved/future-use (255.255.255.255 broadcast falls in it).
11511                // Neither is a legitimate unicast egress target, so a guest
11512                // connect to them must be denied rather than attempted.
11513                (224..=239, _) => Some(("224.0.0.0/4", "multicast")),
11514                (240..=255, _) => Some(("240.0.0.0/4", "reserved")),
11515                _ => None,
11516            }
11517        }
11518        IpAddr::V6(ip) => {
11519            if let Some(mapped) = ip.to_ipv4_mapped() {
11520                return restricted_non_loopback_ip_range(IpAddr::V4(mapped));
11521            }
11522            // IPv4-compatible IPv6 (::a.b.c.d): the first six segments are zero
11523            // and the last two carry an embedded IPv4 address. `to_ipv4_mapped`
11524            // returns None for this form, so without canonicalizing it here a
11525            // guest could spell a restricted IPv4 target (e.g. cloud-metadata
11526            // ::169.254.169.254) and bypass the IPv4 classifier. `::`/`::1` are
11527            // excluded so they fall through to the unspecified/loopback paths.
11528            if let Some(compat) = ipv4_compatible_embedded(ip) {
11529                return restricted_non_loopback_ip_range(IpAddr::V4(compat));
11530            }
11531
11532            if ip.is_unspecified() {
11533                // :: is the IPv6 unspecified address; same routing hazard as
11534                // 0.0.0.0, so deny it rather than letting it reach the host.
11535                return Some(("::/128", "unspecified"));
11536            }
11537
11538            let segments = ip.segments();
11539            if (segments[0] & 0xfe00) == 0xfc00 {
11540                return Some(("fc00::/7", "unique-local"));
11541            }
11542            if (segments[0] & 0xffc0) == 0xfe80 {
11543                return Some(("fe80::/10", "link-local"));
11544            }
11545            None
11546        }
11547    }
11548}
11549
11550fn blocked_dns_resolution_error(
11551    resource: &str,
11552    ip: IpAddr,
11553    cidr: &str,
11554    label: &str,
11555) -> SidecarError {
11556    SidecarError::Execution(format!(
11557        "EACCES: blocked outbound network access to {resource}: {ip} is within restricted {label} range {cidr}"
11558    ))
11559}
11560
11561fn blocked_loopback_connect_error(resource: &str, ip: IpAddr, port: u16) -> SidecarError {
11562    SidecarError::Execution(format!(
11563        "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}",
11564        loopback_cidr(ip)
11565    ))
11566}
11567
11568fn filter_dns_safe_ip_addrs(
11569    addresses: Vec<IpAddr>,
11570    hostname: &str,
11571) -> Result<Vec<IpAddr>, SidecarError> {
11572    let resource = format_dns_resource(hostname);
11573    let mut allowed = Vec::new();
11574    let mut blocked = None;
11575
11576    for ip in addresses {
11577        if let Some((cidr, label)) = restricted_non_loopback_ip_range(ip) {
11578            blocked.get_or_insert((ip, cidr, label));
11579            continue;
11580        }
11581        allowed.push(ip);
11582    }
11583
11584    if allowed.is_empty() {
11585        let (ip, cidr, label) = blocked.expect("blocked DNS results should capture a reason");
11586        return Err(blocked_dns_resolution_error(&resource, ip, cidr, label));
11587    }
11588
11589    Ok(allowed)
11590}
11591
11592fn loopback_connect_allowed(context: &JavascriptSocketPathContext, port: u16) -> bool {
11593    context.loopback_port_allowed(port)
11594}
11595
11596fn filter_tcp_connect_ip_addrs(
11597    addresses: Vec<IpAddr>,
11598    host: &str,
11599    port: u16,
11600    context: &JavascriptSocketPathContext,
11601) -> Result<Vec<IpAddr>, SidecarError> {
11602    let resource = format_tcp_resource(host, port);
11603    let mut allowed = Vec::new();
11604    let mut blocked = None;
11605
11606    for ip in addresses {
11607        if let Some((cidr, label)) = restricted_non_loopback_ip_range(ip) {
11608            blocked.get_or_insert_with(|| blocked_dns_resolution_error(&resource, ip, cidr, label));
11609            continue;
11610        }
11611        if is_loopback_ip(ip) && !loopback_connect_allowed(context, port) {
11612            blocked.get_or_insert_with(|| blocked_loopback_connect_error(&resource, ip, port));
11613            continue;
11614        }
11615        allowed.push(ip);
11616    }
11617
11618    if allowed.is_empty() {
11619        return Err(blocked.expect("blocked TCP connect results should capture a reason"));
11620    }
11621
11622    Ok(allowed)
11623}
11624
11625fn resolve_tcp_connect_addr<B>(
11626    bridge: &SharedBridge<B>,
11627    kernel: &SidecarKernel,
11628    vm_id: &str,
11629    dns: &VmDnsConfig,
11630    host: &str,
11631    port: u16,
11632    context: &JavascriptSocketPathContext,
11633) -> Result<ResolvedTcpConnectAddr, SidecarError>
11634where
11635    B: NativeSidecarBridge + Send + 'static,
11636    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11637{
11638    let allowed = filter_tcp_connect_ip_addrs(
11639        resolve_dns_ip_addrs(
11640            bridge,
11641            kernel,
11642            vm_id,
11643            dns,
11644            host,
11645            DnsLookupPolicy::SkipPermissions,
11646        )?,
11647        host,
11648        port,
11649        context,
11650    )?;
11651    let ip = allowed
11652        .iter()
11653        .copied()
11654        .find(|candidate| {
11655            let family = JavascriptSocketFamily::from_ip(*candidate);
11656            context.translate_tcp_loopback_port(family, port).is_some()
11657        })
11658        // We do not implement Happy Eyeballs yet, so prefer IPv4 over a
11659        // verbatim IPv6-first DNS answer for general outbound TCP connects.
11660        .or_else(|| allowed.iter().copied().find(IpAddr::is_ipv4))
11661        .or_else(|| allowed.first().copied())
11662        .ok_or_else(|| {
11663            SidecarError::Execution(format!("failed to resolve TCP address {host}:{port}"))
11664        })?;
11665    let family = JavascriptSocketFamily::from_ip(ip);
11666    let translated_loopback_port = context.translate_tcp_loopback_port(family, port);
11667    let use_kernel_loopback = is_loopback_ip(ip) && translated_loopback_port == Some(port);
11668    let actual_port = if is_loopback_ip(ip) {
11669        translated_loopback_port.unwrap_or(port)
11670    } else {
11671        port
11672    };
11673    Ok(ResolvedTcpConnectAddr {
11674        actual_addr: SocketAddr::new(ip, actual_port),
11675        guest_remote_addr: SocketAddr::new(ip, port),
11676        use_kernel_loopback,
11677    })
11678}
11679
11680fn resolve_dns_ip_addrs<B>(
11681    bridge: &SharedBridge<B>,
11682    kernel: &SidecarKernel,
11683    vm_id: &str,
11684    dns: &VmDnsConfig,
11685    hostname: &str,
11686    policy: DnsLookupPolicy,
11687) -> Result<Vec<IpAddr>, SidecarError>
11688where
11689    B: NativeSidecarBridge + Send + 'static,
11690    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11691{
11692    let resolution = match kernel.resolve_dns(hostname, policy) {
11693        Ok(resolution) => resolution,
11694        Err(error) => {
11695            let sidecar_error = kernel_error(error.clone());
11696            if error.code() != "EACCES" {
11697                emit_dns_resolution_failure_event(bridge, vm_id, hostname, dns, &sidecar_error);
11698            }
11699            return Err(sidecar_error);
11700        }
11701    };
11702    emit_dns_resolution_event(
11703        bridge,
11704        vm_id,
11705        hostname,
11706        resolution.source(),
11707        resolution.addresses(),
11708        dns,
11709    );
11710    Ok(resolution.addresses().to_vec())
11711}
11712
11713fn resolve_dns_records<B>(
11714    bridge: &SharedBridge<B>,
11715    kernel: &SidecarKernel,
11716    vm_id: &str,
11717    dns: &VmDnsConfig,
11718    hostname: &str,
11719    record_type: RecordType,
11720    policy: DnsLookupPolicy,
11721) -> Result<DnsRecordResolution, SidecarError>
11722where
11723    B: NativeSidecarBridge + Send + 'static,
11724    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11725{
11726    let resolution = match kernel.resolve_dns_records(hostname, record_type, policy) {
11727        Ok(resolution) => resolution,
11728        Err(error) => {
11729            let sidecar_error = kernel_error(error.clone());
11730            if error.code() != "EACCES" {
11731                emit_dns_resolution_failure_event(bridge, vm_id, hostname, dns, &sidecar_error);
11732            }
11733            return Err(sidecar_error);
11734        }
11735    };
11736    emit_dns_record_resolution_event(bridge, vm_id, hostname, &resolution, dns);
11737    Ok(resolution)
11738}
11739
11740fn filter_dns_ip_addrs(
11741    addresses: Vec<IpAddr>,
11742    family: Option<u8>,
11743) -> Result<Vec<IpAddr>, SidecarError> {
11744    let filtered: Vec<_> = match family.unwrap_or(0) {
11745        0 => addresses,
11746        4 => addresses
11747            .into_iter()
11748            .filter(|ip| matches!(ip, IpAddr::V4(_)))
11749            .collect(),
11750        6 => addresses
11751            .into_iter()
11752            .filter(|ip| matches!(ip, IpAddr::V6(_)))
11753            .collect(),
11754        other => {
11755            return Err(SidecarError::InvalidState(format!(
11756                "unsupported dns family {other}"
11757            )));
11758        }
11759    };
11760
11761    if filtered.is_empty() {
11762        return Err(SidecarError::Execution(String::from(
11763            "failed to resolve DNS address for requested family",
11764        )));
11765    }
11766
11767    Ok(filtered)
11768}
11769
11770fn resolve_udp_bind_addr(
11771    host: &str,
11772    port: u16,
11773    family: JavascriptUdpFamily,
11774) -> Result<SocketAddr, SidecarError> {
11775    (host, port)
11776        .to_socket_addrs()
11777        .map_err(sidecar_net_error)?
11778        .find(|addr| family.matches_addr(addr))
11779        .ok_or_else(|| {
11780            SidecarError::Execution(format!(
11781                "failed to resolve {} UDP bind address {host}:{port}",
11782                family.socket_type()
11783            ))
11784        })
11785}
11786
11787fn resolve_udp_addr<B>(request: UdpRemoteAddrRequest<'_, B>) -> Result<SocketAddr, SidecarError>
11788where
11789    B: NativeSidecarBridge + Send + 'static,
11790    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11791{
11792    let UdpRemoteAddrRequest {
11793        bridge,
11794        kernel,
11795        vm_id,
11796        dns,
11797        host,
11798        port,
11799        family,
11800        context,
11801    } = request;
11802    resolve_dns_ip_addrs(
11803        bridge,
11804        kernel,
11805        vm_id,
11806        dns,
11807        host,
11808        DnsLookupPolicy::SkipPermissions,
11809    )?
11810    .into_iter()
11811    .map(|ip| {
11812        let family_key = JavascriptSocketFamily::from_ip(ip);
11813        let actual_port = if is_loopback_ip(ip) {
11814            context
11815                .translate_udp_loopback_port(family_key, port)
11816                .unwrap_or(port)
11817        } else {
11818            port
11819        };
11820        SocketAddr::new(ip, actual_port)
11821    })
11822    .find(|addr| family.matches_addr(addr))
11823    .ok_or_else(|| {
11824        SidecarError::Execution(format!(
11825            "failed to resolve {} UDP address {host}:{port}",
11826            family.socket_type()
11827        ))
11828    })
11829}
11830
11831fn socket_addr_family(addr: &SocketAddr) -> &'static str {
11832    match addr {
11833        SocketAddr::V4(_) => "IPv4",
11834        SocketAddr::V6(_) => "IPv6",
11835    }
11836}
11837
11838fn javascript_net_timeout_value() -> Value {
11839    Value::String(String::from(JAVASCRIPT_NET_TIMEOUT_SENTINEL))
11840}
11841
11842fn javascript_net_json_string(value: Value, label: &str) -> Result<Value, SidecarError> {
11843    serde_json::to_string(&value)
11844        .map(Value::String)
11845        .map_err(|error| {
11846            SidecarError::InvalidState(format!("failed to serialize {label} payload: {error}"))
11847        })
11848}
11849
11850fn javascript_net_read_value(
11851    event: Option<JavascriptTcpSocketEvent>,
11852) -> Result<Value, SidecarError> {
11853    match event {
11854        Some(JavascriptTcpSocketEvent::Data(chunk)) => Ok(Value::String(
11855            base64::engine::general_purpose::STANDARD.encode(chunk),
11856        )),
11857        Some(JavascriptTcpSocketEvent::End | JavascriptTcpSocketEvent::Close { .. }) => {
11858            Ok(Value::Null)
11859        }
11860        Some(JavascriptTcpSocketEvent::Error { code, message }) => {
11861            let detail = code.unwrap_or_else(|| String::from("socket read"));
11862            Err(SidecarError::Execution(format!("{detail}: {message}")))
11863        }
11864        None => Ok(javascript_net_timeout_value()),
11865    }
11866}
11867
11868fn io_error_code(error: &std::io::Error) -> Option<String> {
11869    match error.raw_os_error() {
11870        Some(libc::EADDRINUSE) => Some(String::from("EADDRINUSE")),
11871        Some(libc::EADDRNOTAVAIL) => Some(String::from("EADDRNOTAVAIL")),
11872        Some(libc::ECONNREFUSED) => Some(String::from("ECONNREFUSED")),
11873        Some(libc::ECONNRESET) => Some(String::from("ECONNRESET")),
11874        Some(libc::EINVAL) => Some(String::from("EINVAL")),
11875        Some(libc::EPIPE) => Some(String::from("EPIPE")),
11876        Some(libc::ETIMEDOUT) => Some(String::from("ETIMEDOUT")),
11877        Some(libc::EHOSTUNREACH) => Some(String::from("EHOSTUNREACH")),
11878        Some(libc::ENETUNREACH) => Some(String::from("ENETUNREACH")),
11879        _ => None,
11880    }
11881}
11882
11883fn sidecar_net_error(error: std::io::Error) -> SidecarError {
11884    let message = match io_error_code(&error) {
11885        Some(code) => format!("{code}: {error}"),
11886        None => error.to_string(),
11887    };
11888    SidecarError::Execution(message)
11889}
11890
11891fn tls_provider() -> Arc<rustls::crypto::CryptoProvider> {
11892    Arc::new(aws_lc_rs::default_provider())
11893}
11894
11895fn tls_local_certificates(
11896    options: &JavascriptTlsBridgeOptions,
11897) -> Result<Vec<Vec<u8>>, SidecarError> {
11898    let Some(certificates) = options.cert.as_ref() else {
11899        return Ok(Vec::new());
11900    };
11901    tls_material_entries(certificates)
11902}
11903
11904fn tls_material_entries(material: &JavascriptTlsMaterial) -> Result<Vec<Vec<u8>>, SidecarError> {
11905    match material {
11906        JavascriptTlsMaterial::Single(entry) => tls_data_value(entry).map(|value| vec![value]),
11907        JavascriptTlsMaterial::Many(entries) => entries.iter().map(tls_data_value).collect(),
11908    }
11909}
11910
11911fn tls_data_value(value: &JavascriptTlsDataValue) -> Result<Vec<u8>, SidecarError> {
11912    match value {
11913        JavascriptTlsDataValue::Buffer { data } => base64::engine::general_purpose::STANDARD
11914            .decode(data)
11915            .map_err(|error| {
11916                SidecarError::InvalidState(format!("TLS material contains invalid base64: {error}"))
11917            }),
11918        JavascriptTlsDataValue::String { data } => Ok(data.as_bytes().to_vec()),
11919    }
11920}
11921
11922fn tls_certificates_from_material(
11923    material: &JavascriptTlsMaterial,
11924) -> Result<Vec<CertificateDer<'static>>, SidecarError> {
11925    let mut certificates = Vec::new();
11926    for entry in tls_material_entries(material)? {
11927        let mut reader = std::io::BufReader::new(Cursor::new(entry.clone()));
11928        let parsed = rustls_pemfile::certs(&mut reader)
11929            .collect::<Result<Vec<_>, _>>()
11930            .map_err(sidecar_net_error)?;
11931        if parsed.is_empty() {
11932            certificates.push(CertificateDer::from(entry));
11933        } else {
11934            certificates.extend(parsed);
11935        }
11936    }
11937    if certificates.is_empty() {
11938        return Err(SidecarError::InvalidState(String::from(
11939            "TLS certificate material did not contain any certificates",
11940        )));
11941    }
11942    Ok(certificates)
11943}
11944
11945fn tls_private_key_from_material(
11946    material: &JavascriptTlsMaterial,
11947) -> Result<PrivateKeyDer<'static>, SidecarError> {
11948    for entry in tls_material_entries(material)? {
11949        let mut reader = std::io::BufReader::new(Cursor::new(entry));
11950        if let Some(key) = rustls_pemfile::private_key(&mut reader).map_err(sidecar_net_error)? {
11951            return Ok(key);
11952        }
11953    }
11954    Err(SidecarError::InvalidState(String::from(
11955        "TLS private key material did not contain a supported key",
11956    )))
11957}
11958
11959fn tls_root_store(options: &JavascriptTlsBridgeOptions) -> Result<RootCertStore, SidecarError> {
11960    let mut roots = RootCertStore::empty();
11961    if let Some(ca) = options.ca.as_ref() {
11962        for certificate in tls_certificates_from_material(ca)? {
11963            roots.add(certificate).map_err(|error| {
11964                SidecarError::InvalidState(format!("failed to add TLS CA certificate: {error}"))
11965            })?;
11966        }
11967        return Ok(roots);
11968    }
11969
11970    for certificate in rustls_native_certs::load_native_certs().certs {
11971        roots.add(certificate).map_err(|error| {
11972            SidecarError::InvalidState(format!(
11973                "failed to add native TLS certificate to root store: {error}"
11974            ))
11975        })?;
11976    }
11977    Ok(roots)
11978}
11979
11980fn build_client_tls_stream(
11981    stream: TcpStream,
11982    options: &JavascriptTlsBridgeOptions,
11983) -> Result<rustls::StreamOwned<ClientConnection, TcpStream>, SidecarError> {
11984    let config = build_client_tls_config(options)?;
11985    let server_name = options
11986        .servername
11987        .clone()
11988        .unwrap_or_else(|| String::from("localhost"));
11989    let server_name = ServerName::try_from(server_name)
11990        .map_err(|_| SidecarError::InvalidState(String::from("invalid TLS servername")))?;
11991    stream
11992        .set_read_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
11993        .map_err(sidecar_net_error)?;
11994    stream
11995        .set_write_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
11996        .map_err(sidecar_net_error)?;
11997    let mut tls_stream = rustls::StreamOwned::new(
11998        ClientConnection::new(Arc::new(config), server_name).map_err(|error| {
11999            SidecarError::Execution(format!("failed to start TLS client: {error}"))
12000        })?,
12001        stream,
12002    );
12003    while tls_stream.conn.is_handshaking() {
12004        tls_stream
12005            .conn
12006            .complete_io(&mut tls_stream.sock)
12007            .map_err(sidecar_net_error)?;
12008    }
12009    tls_stream
12010        .sock
12011        .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
12012        .map_err(sidecar_net_error)?;
12013    tls_stream
12014        .sock
12015        .set_write_timeout(None)
12016        .map_err(sidecar_net_error)?;
12017    Ok(tls_stream)
12018}
12019
12020fn build_client_loopback_tls_stream(
12021    transport: crate::state::LoopbackTlsEndpoint,
12022    options: &JavascriptTlsBridgeOptions,
12023) -> Result<rustls::StreamOwned<ClientConnection, crate::state::LoopbackTlsEndpoint>, SidecarError>
12024{
12025    let config = build_client_tls_config(options)?;
12026    let server_name = options
12027        .servername
12028        .clone()
12029        .unwrap_or_else(|| String::from("localhost"));
12030    let server_name = ServerName::try_from(server_name)
12031        .map_err(|_| SidecarError::InvalidState(String::from("invalid TLS servername")))?;
12032    let mut tls_stream = rustls::StreamOwned::new(
12033        ClientConnection::new(Arc::new(config), server_name).map_err(|error| {
12034            SidecarError::Execution(format!("failed to start TLS client: {error}"))
12035        })?,
12036        transport,
12037    );
12038    match tls_stream.conn.complete_io(&mut tls_stream.sock) {
12039        Ok(_) => {}
12040        Err(error)
12041            if matches!(
12042                error.kind(),
12043                std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
12044            ) => {}
12045        Err(error) => return Err(sidecar_net_error(error)),
12046    }
12047    Ok(tls_stream)
12048}
12049
12050fn build_client_tls_config(
12051    options: &JavascriptTlsBridgeOptions,
12052) -> Result<ClientConfig, SidecarError> {
12053    let provider = tls_provider();
12054    let builder = ClientConfig::builder_with_provider(provider.clone())
12055        .with_safe_default_protocol_versions()
12056        .map_err(|error| {
12057            SidecarError::InvalidState(format!("invalid TLS protocol config: {error}"))
12058        })?;
12059
12060    let mut config = if options.reject_unauthorized == Some(false) {
12061        let verifier = Arc::new(InsecureTlsVerifier {
12062            supported_schemes: provider
12063                .signature_verification_algorithms
12064                .supported_schemes(),
12065        });
12066        builder
12067            .dangerous()
12068            .with_custom_certificate_verifier(verifier)
12069            .with_no_client_auth()
12070    } else {
12071        builder
12072            .with_root_certificates(tls_root_store(options)?)
12073            .with_no_client_auth()
12074    };
12075
12076    if let Some(protocols) = options.alpn_protocols.as_ref() {
12077        config.alpn_protocols = protocols
12078            .iter()
12079            .map(|protocol| protocol.as_bytes().to_vec())
12080            .collect();
12081    }
12082    Ok(config)
12083}
12084
12085fn build_server_tls_stream(
12086    stream: TcpStream,
12087    options: &JavascriptTlsBridgeOptions,
12088) -> Result<rustls::StreamOwned<ServerConnection, TcpStream>, SidecarError> {
12089    let config = build_server_tls_config(options)?;
12090    stream
12091        .set_read_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
12092        .map_err(sidecar_net_error)?;
12093    stream
12094        .set_write_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
12095        .map_err(sidecar_net_error)?;
12096    let mut tls_stream = rustls::StreamOwned::new(
12097        ServerConnection::new(Arc::new(config)).map_err(|error| {
12098            SidecarError::Execution(format!("failed to start TLS server: {error}"))
12099        })?,
12100        stream,
12101    );
12102    while tls_stream.conn.is_handshaking() {
12103        tls_stream
12104            .conn
12105            .complete_io(&mut tls_stream.sock)
12106            .map_err(sidecar_net_error)?;
12107    }
12108    tls_stream
12109        .sock
12110        .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
12111        .map_err(sidecar_net_error)?;
12112    tls_stream
12113        .sock
12114        .set_write_timeout(None)
12115        .map_err(sidecar_net_error)?;
12116    Ok(tls_stream)
12117}
12118
12119fn build_server_loopback_tls_stream(
12120    transport: crate::state::LoopbackTlsEndpoint,
12121    options: &JavascriptTlsBridgeOptions,
12122) -> Result<rustls::StreamOwned<ServerConnection, crate::state::LoopbackTlsEndpoint>, SidecarError>
12123{
12124    let config = build_server_tls_config(options)?;
12125    Ok(rustls::StreamOwned::new(
12126        ServerConnection::new(Arc::new(config)).map_err(|error| {
12127            SidecarError::Execution(format!("failed to start TLS server: {error}"))
12128        })?,
12129        transport,
12130    ))
12131}
12132
12133fn build_server_tls_config(
12134    options: &JavascriptTlsBridgeOptions,
12135) -> Result<ServerConfig, SidecarError> {
12136    let certificates = tls_certificates_from_material(options.cert.as_ref().ok_or_else(|| {
12137        SidecarError::InvalidState(String::from("TLS server upgrade requires a certificate"))
12138    })?)?;
12139    let key = tls_private_key_from_material(options.key.as_ref().ok_or_else(|| {
12140        SidecarError::InvalidState(String::from("TLS server upgrade requires a private key"))
12141    })?)?;
12142
12143    let mut config = ServerConfig::builder_with_provider(tls_provider())
12144        .with_safe_default_protocol_versions()
12145        .map_err(|error| {
12146            SidecarError::InvalidState(format!("invalid TLS protocol config: {error}"))
12147        })?
12148        .with_no_client_auth()
12149        .with_single_cert(certificates, key)
12150        .map_err(|error| {
12151            SidecarError::InvalidState(format!("invalid TLS server config: {error}"))
12152        })?;
12153
12154    if let Some(protocols) = options.alpn_protocols.as_ref() {
12155        config.alpn_protocols = protocols
12156            .iter()
12157            .map(|protocol| protocol.as_bytes().to_vec())
12158            .collect();
12159    }
12160    Ok(config)
12161}
12162
12163fn tls_protocol_name(version: rustls::ProtocolVersion) -> String {
12164    match version {
12165        rustls::ProtocolVersion::TLSv1_2 => String::from("TLSv1.2"),
12166        rustls::ProtocolVersion::TLSv1_3 => String::from("TLSv1.3"),
12167        other => other
12168            .as_str()
12169            .map(str::to_owned)
12170            .unwrap_or_else(|| format!("{other:?}")),
12171    }
12172}
12173
12174fn tls_cipher_bridge_value(suite: rustls::SupportedCipherSuite) -> Value {
12175    tls_bridge_object(vec![
12176        (
12177            "name",
12178            suite
12179                .suite()
12180                .as_str()
12181                .map(|value| Value::String(value.to_owned()))
12182                .unwrap_or(Value::Null),
12183        ),
12184        (
12185            "standardName",
12186            suite
12187                .suite()
12188                .as_str()
12189                .map(|value| Value::String(value.to_owned()))
12190                .unwrap_or(Value::Null),
12191        ),
12192        (
12193            "version",
12194            Value::String(if suite.tls13().is_some() {
12195                String::from("TLSv1.3")
12196            } else {
12197                String::from("TLSv1.2")
12198            }),
12199        ),
12200    ])
12201}
12202
12203fn tls_certificate_bridge_value(certificate: &[u8], detailed: bool) -> Value {
12204    let mut fields = vec![("raw", tls_bridge_buffer_value(certificate))];
12205    if detailed {
12206        fields.push(("issuerCertificate", tls_bridge_undefined_value()));
12207    }
12208    tls_bridge_object(fields)
12209}
12210
12211fn tls_bridge_buffer_value(bytes: &[u8]) -> Value {
12212    json!({
12213        "type": "buffer",
12214        "data": base64::engine::general_purpose::STANDARD.encode(bytes),
12215    })
12216}
12217
12218fn tls_bridge_object(entries: Vec<(&str, Value)>) -> Value {
12219    let value = entries
12220        .into_iter()
12221        .map(|(key, value)| (key.to_owned(), value))
12222        .collect::<serde_json::Map<String, Value>>();
12223    json!({
12224        "type": "object",
12225        "id": 1,
12226        "value": value,
12227    })
12228}
12229
12230fn tls_bridge_undefined_value() -> Value {
12231    json!({
12232        "type": "undefined",
12233    })
12234}
12235
12236fn spawn_tcp_socket_reader(
12237    stream: TcpStream,
12238    sender: Sender<JavascriptTcpSocketEvent>,
12239    tls_mode: Arc<AtomicBool>,
12240    saw_local_shutdown: Arc<AtomicBool>,
12241    saw_remote_end: Arc<AtomicBool>,
12242    close_notified: Arc<AtomicBool>,
12243) {
12244    thread::spawn(move || {
12245        let mut stream = stream;
12246        let mut buffer = vec![0_u8; 64 * 1024];
12247        loop {
12248            if tls_mode.load(Ordering::SeqCst) {
12249                break;
12250            }
12251            match stream.read(&mut buffer) {
12252                Ok(0) => {
12253                    saw_remote_end.store(true, Ordering::SeqCst);
12254                    let _ = sender.send(JavascriptTcpSocketEvent::End);
12255                    if saw_local_shutdown.load(Ordering::SeqCst)
12256                        && !close_notified.swap(true, Ordering::SeqCst)
12257                    {
12258                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12259                    }
12260                    break;
12261                }
12262                Ok(bytes_read) => {
12263                    if sender
12264                        .send(JavascriptTcpSocketEvent::Data(
12265                            buffer[..bytes_read].to_vec(),
12266                        ))
12267                        .is_err()
12268                    {
12269                        break;
12270                    }
12271                }
12272                Err(error)
12273                    if matches!(
12274                        error.kind(),
12275                        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
12276                    ) =>
12277                {
12278                    continue;
12279                }
12280                Err(error) => {
12281                    let code = io_error_code(&error);
12282                    let _ = sender.send(JavascriptTcpSocketEvent::Error {
12283                        code,
12284                        message: error.to_string(),
12285                    });
12286                    if !close_notified.swap(true, Ordering::SeqCst) {
12287                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12288                    }
12289                    break;
12290                }
12291            }
12292        }
12293    });
12294}
12295
12296fn spawn_tls_socket_reader(
12297    tls_stream: Arc<Mutex<Option<ActiveTlsStream>>>,
12298    sender: Sender<JavascriptTcpSocketEvent>,
12299    saw_local_shutdown: Arc<AtomicBool>,
12300    saw_remote_end: Arc<AtomicBool>,
12301    close_notified: Arc<AtomicBool>,
12302) {
12303    thread::spawn(move || {
12304        let mut buffer = vec![0_u8; 64 * 1024];
12305        loop {
12306            let read_result = {
12307                let mut guard = match tls_stream.lock() {
12308                    Ok(guard) => guard,
12309                    Err(_) => return,
12310                };
12311                let Some(stream) = guard.as_mut() else {
12312                    return;
12313                };
12314                stream.read(&mut buffer)
12315            };
12316
12317            match read_result {
12318                Ok(0) => {
12319                    saw_remote_end.store(true, Ordering::SeqCst);
12320                    let _ = sender.send(JavascriptTcpSocketEvent::End);
12321                    if saw_local_shutdown.load(Ordering::SeqCst)
12322                        && !close_notified.swap(true, Ordering::SeqCst)
12323                    {
12324                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12325                    }
12326                    break;
12327                }
12328                Ok(bytes_read) => {
12329                    if sender
12330                        .send(JavascriptTcpSocketEvent::Data(
12331                            buffer[..bytes_read].to_vec(),
12332                        ))
12333                        .is_err()
12334                    {
12335                        break;
12336                    }
12337                }
12338                Err(error)
12339                    if matches!(
12340                        error.kind(),
12341                        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
12342                    ) =>
12343                {
12344                    // The TLS reader and writer share one rustls stream mutex. Yield after
12345                    // timed-out reads so request writes can acquire the lock promptly.
12346                    std::thread::sleep(Duration::from_millis(1));
12347                    continue;
12348                }
12349                Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => {
12350                    saw_remote_end.store(true, Ordering::SeqCst);
12351                    let _ = sender.send(JavascriptTcpSocketEvent::End);
12352                    if saw_local_shutdown.load(Ordering::SeqCst)
12353                        && !close_notified.swap(true, Ordering::SeqCst)
12354                    {
12355                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12356                    }
12357                    break;
12358                }
12359                Err(error) => {
12360                    let code = io_error_code(&error);
12361                    let _ = sender.send(JavascriptTcpSocketEvent::Error {
12362                        code,
12363                        message: error.to_string(),
12364                    });
12365                    if !close_notified.swap(true, Ordering::SeqCst) {
12366                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12367                    }
12368                    break;
12369                }
12370            }
12371        }
12372    });
12373}
12374
12375fn spawn_unix_socket_reader(
12376    stream: UnixStream,
12377    sender: Sender<JavascriptTcpSocketEvent>,
12378    saw_local_shutdown: Arc<AtomicBool>,
12379    saw_remote_end: Arc<AtomicBool>,
12380    close_notified: Arc<AtomicBool>,
12381) {
12382    thread::spawn(move || {
12383        let mut stream = stream;
12384        let mut buffer = vec![0_u8; 64 * 1024];
12385        loop {
12386            match stream.read(&mut buffer) {
12387                Ok(0) => {
12388                    saw_remote_end.store(true, Ordering::SeqCst);
12389                    let _ = sender.send(JavascriptTcpSocketEvent::End);
12390                    if saw_local_shutdown.load(Ordering::SeqCst)
12391                        && !close_notified.swap(true, Ordering::SeqCst)
12392                    {
12393                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12394                    }
12395                    break;
12396                }
12397                Ok(bytes_read) => {
12398                    if sender
12399                        .send(JavascriptTcpSocketEvent::Data(
12400                            buffer[..bytes_read].to_vec(),
12401                        ))
12402                        .is_err()
12403                    {
12404                        break;
12405                    }
12406                }
12407                Err(error) => {
12408                    let code = io_error_code(&error);
12409                    let _ = sender.send(JavascriptTcpSocketEvent::Error {
12410                        code,
12411                        message: error.to_string(),
12412                    });
12413                    if !close_notified.swap(true, Ordering::SeqCst) {
12414                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12415                    }
12416                    break;
12417                }
12418            }
12419        }
12420    });
12421}
12422
12423fn terminate_child_process_tree(kernel: &mut SidecarKernel, process: &mut ActiveProcess) {
12424    let sqlite_database_ids = process.sqlite_databases.keys().copied().collect::<Vec<_>>();
12425    for database_id in sqlite_database_ids {
12426        let _ = close_sqlite_database(kernel, process, database_id);
12427    }
12428    process.sqlite_statements.clear();
12429    process.http_servers.clear();
12430    process.pending_http_requests.clear();
12431    if let Ok(mut http2) = process.http2.shared.lock() {
12432        let sessions = http2.sessions.values().cloned().collect::<Vec<_>>();
12433        http2.server_events.clear();
12434        http2.session_events.clear();
12435        http2.streams.clear();
12436        http2.servers.clear();
12437        http2.sessions.clear();
12438        drop(http2);
12439        for session in sessions {
12440            let (respond_to, _rx) = mpsc::channel();
12441            let _ = session.command_tx.send(Http2SessionCommand::Close {
12442                abrupt: true,
12443                respond_to,
12444            });
12445        }
12446    }
12447
12448    let listener_ids = process.tcp_listeners.keys().cloned().collect::<Vec<_>>();
12449    for listener_id in listener_ids {
12450        if let Some(listener) = process.tcp_listeners.remove(&listener_id) {
12451            let _ = listener.close(kernel, process.kernel_pid);
12452        }
12453    }
12454
12455    let sockets = process.tcp_sockets.keys().cloned().collect::<Vec<_>>();
12456    for socket_id in sockets {
12457        if let Some(socket) = process.tcp_sockets.remove(&socket_id) {
12458            let _ = socket.close(kernel, process.kernel_pid);
12459        }
12460    }
12461
12462    let unix_listener_ids = process.unix_listeners.keys().cloned().collect::<Vec<_>>();
12463    for listener_id in unix_listener_ids {
12464        if let Some(listener) = process.unix_listeners.remove(&listener_id) {
12465            let _ = listener.close();
12466        }
12467    }
12468
12469    let unix_sockets = process.unix_sockets.keys().cloned().collect::<Vec<_>>();
12470    for socket_id in unix_sockets {
12471        if let Some(socket) = process.unix_sockets.remove(&socket_id) {
12472            let _ = socket.close();
12473        }
12474    }
12475
12476    let udp_socket_ids = process.udp_sockets.keys().cloned().collect::<Vec<_>>();
12477    for socket_id in udp_socket_ids {
12478        if let Some(mut socket) = process.udp_sockets.remove(&socket_id) {
12479            socket.close(kernel, process.kernel_pid);
12480        }
12481    }
12482
12483    let child_ids = process.child_processes.keys().cloned().collect::<Vec<_>>();
12484    for child_id in child_ids {
12485        let Some(mut child) = process.child_processes.remove(&child_id) else {
12486            continue;
12487        };
12488        terminate_child_process_tree(kernel, &mut child);
12489        let _ = kernel.kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, SIGTERM);
12490        let _ = signal_runtime_process(child.execution.child_pid(), SIGTERM);
12491        child.kernel_handle.finish(0);
12492        let _ = kernel.wait_and_reap(child.kernel_pid);
12493    }
12494}
12495
12496fn service_javascript_sqlite_sync_rpc(
12497    kernel: &mut SidecarKernel,
12498    process: &mut ActiveProcess,
12499    request: &JavascriptSyncRpcRequest,
12500) -> Result<Value, SidecarError> {
12501    match request.method.as_str() {
12502        "sqlite.constants" => Ok(json!({})),
12503        "sqlite.open" => sqlite_open_database(kernel, process, request),
12504        "sqlite.close" => {
12505            let database_id =
12506                javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.close database id")?;
12507            close_sqlite_database(kernel, process, database_id)?;
12508            Ok(Value::Null)
12509        }
12510        "sqlite.exec" => sqlite_exec_database(kernel, process, request),
12511        "sqlite.query" => sqlite_query_database(process, request),
12512        "sqlite.prepare" => sqlite_prepare_statement(process, request),
12513        "sqlite.location" => {
12514            let database_id =
12515                javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.location database id")?;
12516            let database = sqlite_database(process, database_id)?;
12517            Ok(database
12518                .vm_path
12519                .as_ref()
12520                .map(|path| Value::String(path.clone()))
12521                .unwrap_or(Value::Null))
12522        }
12523        "sqlite.checkpoint" => {
12524            let database_id =
12525                javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.checkpoint database id")?;
12526            let kernel_pid = process.kernel_pid;
12527            let database = sqlite_database_mut(process, database_id)?;
12528            sqlite_sync_database(kernel, kernel_pid, database)?;
12529            Ok(Value::Null)
12530        }
12531        "sqlite.statement.run" => sqlite_run_statement(kernel, process, request),
12532        "sqlite.statement.get" => sqlite_get_statement(process, request),
12533        "sqlite.statement.all" | "sqlite.statement.iterate" => {
12534            sqlite_all_statement(process, request)
12535        }
12536        "sqlite.statement.columns" => sqlite_statement_columns(process, request),
12537        "sqlite.statement.setReturnArrays" => {
12538            let statement_id = javascript_sync_rpc_arg_u64(
12539                &request.args,
12540                0,
12541                "sqlite.statement.setReturnArrays statement id",
12542            )?;
12543            let enabled = javascript_sync_rpc_arg_bool(
12544                &request.args,
12545                1,
12546                "sqlite.statement.setReturnArrays enabled",
12547            )?;
12548            sqlite_statement_mut(process, statement_id)?.return_arrays = enabled;
12549            Ok(Value::Null)
12550        }
12551        "sqlite.statement.setReadBigInts" => {
12552            let statement_id = javascript_sync_rpc_arg_u64(
12553                &request.args,
12554                0,
12555                "sqlite.statement.setReadBigInts statement id",
12556            )?;
12557            let enabled = javascript_sync_rpc_arg_bool(
12558                &request.args,
12559                1,
12560                "sqlite.statement.setReadBigInts enabled",
12561            )?;
12562            sqlite_statement_mut(process, statement_id)?.read_bigints = enabled;
12563            Ok(Value::Null)
12564        }
12565        "sqlite.statement.setAllowBareNamedParameters" => {
12566            let statement_id = javascript_sync_rpc_arg_u64(
12567                &request.args,
12568                0,
12569                "sqlite.statement.setAllowBareNamedParameters statement id",
12570            )?;
12571            let enabled = javascript_sync_rpc_arg_bool(
12572                &request.args,
12573                1,
12574                "sqlite.statement.setAllowBareNamedParameters enabled",
12575            )?;
12576            sqlite_statement_mut(process, statement_id)?.allow_bare_named_parameters = enabled;
12577            Ok(Value::Null)
12578        }
12579        "sqlite.statement.setAllowUnknownNamedParameters" => {
12580            let statement_id = javascript_sync_rpc_arg_u64(
12581                &request.args,
12582                0,
12583                "sqlite.statement.setAllowUnknownNamedParameters statement id",
12584            )?;
12585            let enabled = javascript_sync_rpc_arg_bool(
12586                &request.args,
12587                1,
12588                "sqlite.statement.setAllowUnknownNamedParameters enabled",
12589            )?;
12590            sqlite_statement_mut(process, statement_id)?.allow_unknown_named_parameters = enabled;
12591            Ok(Value::Null)
12592        }
12593        "sqlite.statement.finalize" => {
12594            let statement_id = javascript_sync_rpc_arg_u64(
12595                &request.args,
12596                0,
12597                "sqlite.statement.finalize statement id",
12598            )?;
12599            process
12600                .sqlite_statements
12601                .remove(&statement_id)
12602                .ok_or_else(|| {
12603                    SidecarError::InvalidState(format!(
12604                        "sqlite statement handle not found: {statement_id}"
12605                    ))
12606                })?;
12607            Ok(Value::Null)
12608        }
12609        other => Err(SidecarError::InvalidState(format!(
12610            "unsupported JavaScript sqlite sync RPC method {other}"
12611        ))),
12612    }
12613}
12614
12615fn sqlite_open_database(
12616    kernel: &mut SidecarKernel,
12617    process: &mut ActiveProcess,
12618    request: &JavascriptSyncRpcRequest,
12619) -> Result<Value, SidecarError> {
12620    ensure_per_process_state_handle_capacity(process.sqlite_databases.len(), "sqlite database")?;
12621    let path = request.args.first().and_then(Value::as_str);
12622    let vm_path = path.filter(|value| !value.is_empty() && *value != ":memory:");
12623    let options = request.args.get(1);
12624    let read_only = sqlite_option_bool(options, "readOnly").unwrap_or(false);
12625    let create = sqlite_option_bool(options, "create").unwrap_or(!read_only);
12626    let timeout_ms = sqlite_option_u64(options, "timeout");
12627
12628    process.next_sqlite_database_id += 1;
12629    let database_id = process.next_sqlite_database_id;
12630
12631    let host_path = if vm_path.is_some() {
12632        Some(
12633            std::env::temp_dir()
12634                .join(format!(
12635                    "secure-exec-sidecar-sqlite-{}-{database_id}",
12636                    process.kernel_pid
12637                ))
12638                .join("database.sqlite"),
12639        )
12640    } else {
12641        None
12642    };
12643
12644    if let Some(host_path) = host_path.as_ref() {
12645        if let Some(parent) = host_path.parent() {
12646            fs::create_dir_all(parent).map_err(|error| {
12647                SidecarError::Io(format!(
12648                    "failed to prepare sqlite temp directory {}: {error}",
12649                    parent.display()
12650                ))
12651            })?;
12652        }
12653    }
12654
12655    if let (Some(vm_path), Some(host_path)) = (vm_path, host_path.as_ref()) {
12656        if kernel
12657            .exists_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, vm_path)
12658            .map_err(kernel_error)?
12659        {
12660            let contents = kernel
12661                .read_file_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, vm_path)
12662                .map_err(kernel_error)?;
12663            fs::write(host_path, contents).map_err(|error| {
12664                SidecarError::Io(format!(
12665                    "failed to materialize sqlite database {}: {error}",
12666                    host_path.display()
12667                ))
12668            })?;
12669        } else if read_only && !create {
12670            return Err(SidecarError::InvalidState(format!(
12671                "sqlite database does not exist: {vm_path}"
12672            )));
12673        }
12674    }
12675
12676    let target = host_path
12677        .as_ref()
12678        .map(|path| path.to_string_lossy().into_owned())
12679        .unwrap_or_else(|| String::from(":memory:"));
12680    let mut flags = if read_only {
12681        SqliteOpenFlags::SQLITE_OPEN_READ_ONLY
12682    } else {
12683        SqliteOpenFlags::SQLITE_OPEN_READ_WRITE
12684    };
12685    if create && !read_only {
12686        flags |= SqliteOpenFlags::SQLITE_OPEN_CREATE;
12687    }
12688
12689    let connection = SqliteConnection::open_with_flags(&target, flags).map_err(|error| {
12690        SidecarError::InvalidState(format!(
12691            "sqlite database open failed for {}: {error}",
12692            vm_path.unwrap_or(":memory:")
12693        ))
12694    })?;
12695    if let Some(timeout_ms) = timeout_ms {
12696        connection
12697            .busy_timeout(Duration::from_millis(timeout_ms))
12698            .map_err(sqlite_error)?;
12699    }
12700    if host_path.is_some() && !read_only {
12701        let _ = connection.pragma_update(None, "journal_mode", "WAL");
12702    }
12703
12704    process.sqlite_databases.insert(
12705        database_id,
12706        ActiveSqliteDatabase {
12707            connection,
12708            host_path,
12709            vm_path: vm_path.map(String::from),
12710            dirty: false,
12711            transaction_depth: 0,
12712            read_only,
12713        },
12714    );
12715
12716    Ok(json!(database_id))
12717}
12718
12719fn sqlite_exec_database(
12720    kernel: &mut SidecarKernel,
12721    process: &mut ActiveProcess,
12722    request: &JavascriptSyncRpcRequest,
12723) -> Result<Value, SidecarError> {
12724    let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.exec database id")?;
12725    let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.exec sql")?;
12726    let kernel_pid = process.kernel_pid;
12727    let database = sqlite_database_mut(process, database_id)?;
12728    let before = database.connection.total_changes();
12729    database
12730        .connection
12731        .execute_batch(sql)
12732        .map_err(sqlite_error)?;
12733    mark_sqlite_mutation(database, sql);
12734    sqlite_sync_database(kernel, kernel_pid, database)?;
12735    Ok(json!(database
12736        .connection
12737        .total_changes()
12738        .saturating_sub(before)))
12739}
12740
12741fn sqlite_query_database(
12742    process: &mut ActiveProcess,
12743    request: &JavascriptSyncRpcRequest,
12744) -> Result<Value, SidecarError> {
12745    let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.query database id")?;
12746    let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.query sql")?;
12747    let params = request.args.get(2);
12748    let options = request.args.get(3);
12749    let return_arrays = sqlite_option_bool(options, "returnArrays").unwrap_or(false);
12750    let read_bigints = sqlite_option_bool(options, "readBigInts").unwrap_or(false);
12751    let database = sqlite_database_mut(process, database_id)?;
12752    sqlite_query_rows(
12753        &mut database.connection,
12754        sql,
12755        params,
12756        return_arrays,
12757        read_bigints,
12758        true,
12759        false,
12760    )
12761}
12762
12763fn sqlite_prepare_statement(
12764    process: &mut ActiveProcess,
12765    request: &JavascriptSyncRpcRequest,
12766) -> Result<Value, SidecarError> {
12767    ensure_per_process_state_handle_capacity(process.sqlite_statements.len(), "sqlite statement")?;
12768    let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.prepare database id")?;
12769    let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.prepare sql")?;
12770    let _ = sqlite_database(process, database_id)?;
12771    process.next_sqlite_statement_id += 1;
12772    let statement_id = process.next_sqlite_statement_id;
12773    process.sqlite_statements.insert(
12774        statement_id,
12775        ActiveSqliteStatement {
12776            database_id,
12777            sql: sql.to_owned(),
12778            return_arrays: false,
12779            read_bigints: false,
12780            allow_bare_named_parameters: false,
12781            allow_unknown_named_parameters: false,
12782        },
12783    );
12784    Ok(json!(statement_id))
12785}
12786
12787fn sqlite_run_statement(
12788    kernel: &mut SidecarKernel,
12789    process: &mut ActiveProcess,
12790    request: &JavascriptSyncRpcRequest,
12791) -> Result<Value, SidecarError> {
12792    let statement_id =
12793        javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.run statement id")?;
12794    let params = request.args.get(1);
12795    let statement_state = sqlite_statement(process, statement_id)?.clone();
12796    let kernel_pid = process.kernel_pid;
12797    let database = sqlite_database_mut(process, statement_state.database_id)?;
12798    let before = database.connection.total_changes();
12799    {
12800        let mut statement = database
12801            .connection
12802            .prepare(&statement_state.sql)
12803            .map_err(sqlite_error)?;
12804        bind_sqlite_parameters(
12805            &mut statement,
12806            params,
12807            statement_state.allow_bare_named_parameters,
12808            statement_state.allow_unknown_named_parameters,
12809        )?;
12810        statement.raw_execute().map_err(sqlite_error)?;
12811    }
12812    let changes = database.connection.total_changes().saturating_sub(before);
12813    let last_insert_rowid = database.connection.last_insert_rowid();
12814    mark_sqlite_mutation(database, &statement_state.sql);
12815    sqlite_sync_database(kernel, kernel_pid, database)?;
12816    let result = json!({
12817        "changes": changes,
12818        "lastInsertRowid": encode_sqlite_integer(last_insert_rowid, true),
12819    });
12820    Ok(result)
12821}
12822
12823fn sqlite_get_statement(
12824    process: &mut ActiveProcess,
12825    request: &JavascriptSyncRpcRequest,
12826) -> Result<Value, SidecarError> {
12827    let statement_id =
12828        javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.get statement id")?;
12829    let params = request.args.get(1);
12830    let statement_state = sqlite_statement(process, statement_id)?.clone();
12831    let database = sqlite_database_mut(process, statement_state.database_id)?;
12832    let rows = sqlite_query_rows(
12833        &mut database.connection,
12834        &statement_state.sql,
12835        params,
12836        statement_state.return_arrays,
12837        statement_state.read_bigints,
12838        statement_state.allow_bare_named_parameters,
12839        statement_state.allow_unknown_named_parameters,
12840    )?;
12841    Ok(rows
12842        .as_array()
12843        .and_then(|rows| rows.first().cloned())
12844        .unwrap_or(Value::Null))
12845}
12846
12847fn sqlite_all_statement(
12848    process: &mut ActiveProcess,
12849    request: &JavascriptSyncRpcRequest,
12850) -> Result<Value, SidecarError> {
12851    let statement_id =
12852        javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.all statement id")?;
12853    let params = request.args.get(1);
12854    let statement_state = sqlite_statement(process, statement_id)?.clone();
12855    let database = sqlite_database_mut(process, statement_state.database_id)?;
12856    sqlite_query_rows(
12857        &mut database.connection,
12858        &statement_state.sql,
12859        params,
12860        statement_state.return_arrays,
12861        statement_state.read_bigints,
12862        statement_state.allow_bare_named_parameters,
12863        statement_state.allow_unknown_named_parameters,
12864    )
12865}
12866
12867fn sqlite_statement_columns(
12868    process: &mut ActiveProcess,
12869    request: &JavascriptSyncRpcRequest,
12870) -> Result<Value, SidecarError> {
12871    let statement_id =
12872        javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.columns statement id")?;
12873    let statement_state = sqlite_statement(process, statement_id)?.clone();
12874    let database = sqlite_database_mut(process, statement_state.database_id)?;
12875    let statement = database
12876        .connection
12877        .prepare(&statement_state.sql)
12878        .map_err(sqlite_error)?;
12879    Ok(Value::Array(
12880        statement
12881            .column_names()
12882            .iter()
12883            .map(|name| json!({ "name": name }))
12884            .collect(),
12885    ))
12886}
12887
12888fn sqlite_query_rows(
12889    connection: &mut SqliteConnection,
12890    sql: &str,
12891    params: Option<&Value>,
12892    return_arrays: bool,
12893    read_bigints: bool,
12894    allow_bare_named_parameters: bool,
12895    allow_unknown_named_parameters: bool,
12896) -> Result<Value, SidecarError> {
12897    let mut statement = connection.prepare(sql).map_err(sqlite_error)?;
12898    let column_names = statement
12899        .column_names()
12900        .iter()
12901        .map(|name| (*name).to_owned())
12902        .collect::<Vec<_>>();
12903    let column_count = statement.column_count();
12904    bind_sqlite_parameters(
12905        &mut statement,
12906        params,
12907        allow_bare_named_parameters,
12908        allow_unknown_named_parameters,
12909    )?;
12910    let mut rows = statement.raw_query();
12911    let mut encoded_rows = Vec::new();
12912    while let Some(row) = rows.next().map_err(sqlite_error)? {
12913        encoded_rows.push(encode_sqlite_row(
12914            row,
12915            &column_names,
12916            column_count,
12917            return_arrays,
12918            read_bigints,
12919        )?);
12920    }
12921    Ok(Value::Array(encoded_rows))
12922}
12923
12924fn encode_sqlite_row(
12925    row: &rusqlite::Row<'_>,
12926    column_names: &[String],
12927    column_count: usize,
12928    return_arrays: bool,
12929    read_bigints: bool,
12930) -> Result<Value, SidecarError> {
12931    if return_arrays {
12932        let mut values = Vec::with_capacity(column_count);
12933        for index in 0..column_count {
12934            values.push(encode_sqlite_value_ref(
12935                row.get_ref(index).map_err(sqlite_error)?,
12936                read_bigints,
12937            )?);
12938        }
12939        return Ok(Value::Array(values));
12940    }
12941
12942    let mut object = Map::with_capacity(column_count);
12943    for (index, name) in column_names.iter().enumerate() {
12944        object.insert(
12945            name.clone(),
12946            encode_sqlite_value_ref(row.get_ref(index).map_err(sqlite_error)?, read_bigints)?,
12947        );
12948    }
12949    Ok(Value::Object(object))
12950}
12951
12952fn encode_sqlite_value_ref(
12953    value: SqliteValueRef<'_>,
12954    read_bigints: bool,
12955) -> Result<Value, SidecarError> {
12956    Ok(match value {
12957        SqliteValueRef::Null => Value::Null,
12958        SqliteValueRef::Integer(number) => encode_sqlite_integer(number, read_bigints),
12959        SqliteValueRef::Real(number) => json!(number),
12960        SqliteValueRef::Text(text) => Value::String(String::from_utf8_lossy(text).into_owned()),
12961        SqliteValueRef::Blob(bytes) => json!({
12962            "__agentosSqliteType": "uint8array",
12963            "value": base64::engine::general_purpose::STANDARD.encode(bytes),
12964        }),
12965    })
12966}
12967
12968fn encode_sqlite_integer(number: i64, read_bigints: bool) -> Value {
12969    if read_bigints || number.abs() > SQLITE_JS_SAFE_INTEGER_MAX {
12970        json!({
12971            "__agentosSqliteType": "bigint",
12972            "value": number.to_string(),
12973        })
12974    } else {
12975        json!(number)
12976    }
12977}
12978
12979fn bind_sqlite_parameters(
12980    statement: &mut SqliteStatement<'_>,
12981    params: Option<&Value>,
12982    allow_bare_named_parameters: bool,
12983    allow_unknown_named_parameters: bool,
12984) -> Result<(), SidecarError> {
12985    let Some(params) = params else {
12986        return Ok(());
12987    };
12988    match params {
12989        Value::Null => Ok(()),
12990        Value::Array(values) => {
12991            for (index, value) in values.iter().enumerate() {
12992                statement
12993                    .raw_bind_parameter(index + 1, decode_sqlite_parameter(value)?)
12994                    .map_err(sqlite_error)?;
12995            }
12996            Ok(())
12997        }
12998        Value::Object(map)
12999            if map
13000                .get("__agentosSqliteType")
13001                .and_then(Value::as_str)
13002                .is_none() =>
13003        {
13004            for (key, value) in map {
13005                let index =
13006                    resolve_sqlite_parameter_index(statement, key, allow_bare_named_parameters)?;
13007                let Some(index) = index else {
13008                    if allow_unknown_named_parameters {
13009                        continue;
13010                    }
13011                    return Err(SidecarError::InvalidState(format!(
13012                        "sqlite named parameter not found: {key}"
13013                    )));
13014                };
13015                statement
13016                    .raw_bind_parameter(index, decode_sqlite_parameter(value)?)
13017                    .map_err(sqlite_error)?;
13018            }
13019            Ok(())
13020        }
13021        other => statement
13022            .raw_bind_parameter(1, decode_sqlite_parameter(other)?)
13023            .map_err(sqlite_error),
13024    }
13025}
13026
13027fn resolve_sqlite_parameter_index(
13028    statement: &mut SqliteStatement<'_>,
13029    key: &str,
13030    allow_bare_named_parameters: bool,
13031) -> Result<Option<usize>, SidecarError> {
13032    let mut candidates = vec![key.to_owned()];
13033    if allow_bare_named_parameters
13034        && !key.starts_with(':')
13035        && !key.starts_with('@')
13036        && !key.starts_with('$')
13037    {
13038        candidates.push(format!(":{key}"));
13039        candidates.push(format!("@{key}"));
13040        candidates.push(format!("${key}"));
13041    }
13042    for candidate in candidates {
13043        if let Some(index) = statement
13044            .parameter_index(&candidate)
13045            .map_err(sqlite_error)?
13046        {
13047            return Ok(Some(index));
13048        }
13049    }
13050    Ok(None)
13051}
13052
13053fn decode_sqlite_parameter(value: &Value) -> Result<rusqlite::types::Value, SidecarError> {
13054    Ok(match value {
13055        Value::Null => rusqlite::types::Value::Null,
13056        Value::Bool(value) => rusqlite::types::Value::Integer(i64::from(*value)),
13057        Value::Number(value) => match (value.as_i64(), value.as_f64()) {
13058            (Some(integer), _) => rusqlite::types::Value::Integer(integer),
13059            (_, Some(real)) => rusqlite::types::Value::Real(real),
13060            _ => {
13061                return Err(SidecarError::InvalidState(String::from(
13062                    "sqlite parameter number is not representable",
13063                )));
13064            }
13065        },
13066        Value::String(value) => rusqlite::types::Value::Text(value.clone()),
13067        Value::Array(_) => {
13068            return Err(SidecarError::InvalidState(String::from(
13069                "sqlite parameters do not support nested arrays",
13070            )));
13071        }
13072        Value::Object(map) => match map.get("__agentosSqliteType").and_then(Value::as_str) {
13073            Some("bigint") => rusqlite::types::Value::Integer(
13074                map.get("value")
13075                    .and_then(Value::as_str)
13076                    .ok_or_else(|| {
13077                        SidecarError::InvalidState(String::from(
13078                            "sqlite bigint parameter missing string value",
13079                        ))
13080                    })?
13081                    .parse::<i64>()
13082                    .map_err(|error| {
13083                        SidecarError::InvalidState(format!(
13084                            "sqlite bigint parameter is not a signed 64-bit integer: {error}"
13085                        ))
13086                    })?,
13087            ),
13088            Some("uint8array") => rusqlite::types::Value::Blob(
13089                base64::engine::general_purpose::STANDARD
13090                    .decode(map.get("value").and_then(Value::as_str).ok_or_else(|| {
13091                        SidecarError::InvalidState(String::from(
13092                            "sqlite blob parameter missing base64 value",
13093                        ))
13094                    })?)
13095                    .map_err(|error| {
13096                        SidecarError::InvalidState(format!(
13097                            "sqlite blob parameter contains invalid base64: {error}"
13098                        ))
13099                    })?,
13100            ),
13101            Some(other) => {
13102                return Err(SidecarError::InvalidState(format!(
13103                    "unsupported sqlite tagged parameter type {other}"
13104                )));
13105            }
13106            None => {
13107                return Err(SidecarError::InvalidState(String::from(
13108                    "sqlite named parameter objects must be passed as the top-level params object",
13109                )));
13110            }
13111        },
13112    })
13113}
13114
13115fn close_sqlite_database(
13116    kernel: &mut SidecarKernel,
13117    process: &mut ActiveProcess,
13118    database_id: u64,
13119) -> Result<(), SidecarError> {
13120    let mut database = process
13121        .sqlite_databases
13122        .remove(&database_id)
13123        .ok_or_else(|| {
13124            SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
13125        })?;
13126    process
13127        .sqlite_statements
13128        .retain(|_, statement| statement.database_id != database_id);
13129    sqlite_sync_database(kernel, process.kernel_pid, &mut database)?;
13130    let host_path = database.host_path.clone();
13131    drop(database);
13132    cleanup_sqlite_host_artifacts(host_path.as_deref())?;
13133    Ok(())
13134}
13135
13136fn ensure_per_process_state_handle_capacity(len: usize, label: &str) -> Result<(), SidecarError> {
13137    if len >= MAX_PER_PROCESS_STATE_HANDLES {
13138        return Err(SidecarError::InvalidState(format!(
13139            "{label} handle limit exceeded: limit is {MAX_PER_PROCESS_STATE_HANDLES}"
13140        )));
13141    }
13142    Ok(())
13143}
13144
13145fn sqlite_sync_database(
13146    kernel: &mut SidecarKernel,
13147    kernel_pid: u32,
13148    database: &mut ActiveSqliteDatabase,
13149) -> Result<(), SidecarError> {
13150    if !database.dirty
13151        || database.transaction_depth > 0
13152        || database.read_only
13153        || database.host_path.is_none()
13154        || database.vm_path.is_none()
13155    {
13156        return Ok(());
13157    }
13158
13159    let _ = database
13160        .connection
13161        .execute_batch("PRAGMA wal_checkpoint(TRUNCATE)");
13162    let host_path = database.host_path.as_ref().expect("sqlite host path");
13163    if !host_path.exists() {
13164        return Ok(());
13165    }
13166    ensure_vm_parent_dir(
13167        kernel,
13168        kernel_pid,
13169        database.vm_path.as_deref().expect("sqlite vm path"),
13170    )?;
13171    let contents = fs::read(host_path).map_err(|error| {
13172        SidecarError::Io(format!(
13173            "failed to read sqlite temp database {}: {error}",
13174            host_path.display()
13175        ))
13176    })?;
13177    kernel
13178        .write_file_for_process(
13179            EXECUTION_DRIVER_NAME,
13180            kernel_pid,
13181            database.vm_path.as_deref().expect("sqlite vm path"),
13182            contents,
13183            None,
13184        )
13185        .map_err(kernel_error)?;
13186    database.dirty = false;
13187    Ok(())
13188}
13189
13190fn cleanup_sqlite_host_artifacts(host_path: Option<&Path>) -> Result<(), SidecarError> {
13191    let Some(host_path) = host_path else {
13192        return Ok(());
13193    };
13194    let parent = host_path.parent().map(PathBuf::from);
13195    for suffix in ["", "-wal", "-shm"] {
13196        let path = PathBuf::from(format!("{}{}", host_path.display(), suffix));
13197        if path.exists() {
13198            fs::remove_file(&path).map_err(|error| {
13199                SidecarError::Io(format!(
13200                    "failed to remove sqlite temp artifact {}: {error}",
13201                    path.display()
13202                ))
13203            })?;
13204        }
13205    }
13206    if let Some(parent) = parent {
13207        let _ = fs::remove_dir_all(parent);
13208    }
13209    Ok(())
13210}
13211
13212fn ensure_vm_parent_dir(
13213    kernel: &mut SidecarKernel,
13214    kernel_pid: u32,
13215    path: &str,
13216) -> Result<(), SidecarError> {
13217    let parent = dirname(path);
13218    if parent == "/" || parent == "." {
13219        return Ok(());
13220    }
13221    let mut current = String::new();
13222    for segment in parent.split('/').filter(|segment| !segment.is_empty()) {
13223        current.push('/');
13224        current.push_str(segment);
13225        if !kernel
13226            .exists_for_process(EXECUTION_DRIVER_NAME, kernel_pid, &current)
13227            .map_err(kernel_error)?
13228        {
13229            kernel
13230                .mkdir_for_process(EXECUTION_DRIVER_NAME, kernel_pid, &current, false, None)
13231                .map_err(kernel_error)?;
13232        }
13233    }
13234    Ok(())
13235}
13236
13237fn sqlite_database(
13238    process: &ActiveProcess,
13239    database_id: u64,
13240) -> Result<&ActiveSqliteDatabase, SidecarError> {
13241    process.sqlite_databases.get(&database_id).ok_or_else(|| {
13242        SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
13243    })
13244}
13245
13246fn sqlite_database_mut(
13247    process: &mut ActiveProcess,
13248    database_id: u64,
13249) -> Result<&mut ActiveSqliteDatabase, SidecarError> {
13250    process
13251        .sqlite_databases
13252        .get_mut(&database_id)
13253        .ok_or_else(|| {
13254            SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
13255        })
13256}
13257
13258fn sqlite_statement(
13259    process: &ActiveProcess,
13260    statement_id: u64,
13261) -> Result<&ActiveSqliteStatement, SidecarError> {
13262    process.sqlite_statements.get(&statement_id).ok_or_else(|| {
13263        SidecarError::InvalidState(format!("sqlite statement handle not found: {statement_id}"))
13264    })
13265}
13266
13267fn sqlite_statement_mut(
13268    process: &mut ActiveProcess,
13269    statement_id: u64,
13270) -> Result<&mut ActiveSqliteStatement, SidecarError> {
13271    process
13272        .sqlite_statements
13273        .get_mut(&statement_id)
13274        .ok_or_else(|| {
13275            SidecarError::InvalidState(format!("sqlite statement handle not found: {statement_id}"))
13276        })
13277}
13278
13279fn mark_sqlite_mutation(database: &mut ActiveSqliteDatabase, sql: &str) {
13280    let normalized = sql.trim_start().to_ascii_lowercase();
13281    if normalized.starts_with("begin") || normalized.starts_with("savepoint") {
13282        database.dirty = true;
13283        database.transaction_depth += 1;
13284        return;
13285    }
13286    if normalized.starts_with("commit") || normalized.starts_with("release savepoint") {
13287        database.dirty = true;
13288        database.transaction_depth = database.transaction_depth.saturating_sub(1);
13289        return;
13290    }
13291    if normalized.starts_with("rollback") && !normalized.starts_with("rollback to") {
13292        database.dirty = true;
13293        database.transaction_depth = database.transaction_depth.saturating_sub(1);
13294        return;
13295    }
13296    if normalized.starts_with("insert")
13297        || normalized.starts_with("update")
13298        || normalized.starts_with("delete")
13299        || normalized.starts_with("replace")
13300        || normalized.starts_with("create")
13301        || normalized.starts_with("alter")
13302        || normalized.starts_with("drop")
13303        || normalized.starts_with("vacuum")
13304        || normalized.starts_with("reindex")
13305        || normalized.starts_with("analyze")
13306        || normalized.starts_with("attach")
13307        || normalized.starts_with("detach")
13308        || normalized.starts_with("pragma")
13309    {
13310        database.dirty = true;
13311    }
13312}
13313
13314fn sqlite_option_bool(options: Option<&Value>, key: &str) -> Option<bool> {
13315    options
13316        .and_then(|value| value.get(key))
13317        .and_then(Value::as_bool)
13318}
13319
13320fn sqlite_option_u64(options: Option<&Value>, key: &str) -> Option<u64> {
13321    options
13322        .and_then(|value| value.get(key))
13323        .and_then(Value::as_u64)
13324}
13325
13326fn sqlite_error(error: rusqlite::Error) -> SidecarError {
13327    SidecarError::InvalidState(format!("sqlite error: {error}"))
13328}
13329
13330pub(crate) fn javascript_sync_rpc_arg_str<'a>(
13331    args: &'a [Value],
13332    index: usize,
13333    label: &str,
13334) -> Result<&'a str, SidecarError> {
13335    args.get(index)
13336        .and_then(Value::as_str)
13337        .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a string argument")))
13338}
13339
13340pub(crate) fn javascript_sync_rpc_arg_bool(
13341    args: &[Value],
13342    index: usize,
13343    label: &str,
13344) -> Result<bool, SidecarError> {
13345    args.get(index)
13346        .and_then(Value::as_bool)
13347        .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a boolean argument")))
13348}
13349
13350pub(crate) fn javascript_sync_rpc_encoding(args: &[Value]) -> Option<String> {
13351    args.get(1).and_then(|value| {
13352        value.as_str().map(str::to_owned).or_else(|| {
13353            value
13354                .get("encoding")
13355                .and_then(Value::as_str)
13356                .map(str::to_owned)
13357        })
13358    })
13359}
13360
13361pub(crate) fn javascript_sync_rpc_option_bool(
13362    args: &[Value],
13363    index: usize,
13364    key: &str,
13365) -> Option<bool> {
13366    let value = args.get(index)?;
13367    if key == "recursive" {
13368        if let Some(boolean) = value.as_bool() {
13369            return Some(boolean);
13370        }
13371    }
13372    value.get(key).and_then(Value::as_bool)
13373}
13374
13375pub(crate) fn javascript_sync_rpc_option_u32(
13376    args: &[Value],
13377    index: usize,
13378    key: &str,
13379) -> Result<Option<u32>, SidecarError> {
13380    let Some(value) = args.get(index).and_then(|value| {
13381        if value.is_object() {
13382            value.get(key)
13383        } else if key == "mode" && value.is_number() {
13384            Some(value)
13385        } else {
13386            None
13387        }
13388    }) else {
13389        return Ok(None);
13390    };
13391    if value.is_null() {
13392        return Ok(None);
13393    }
13394
13395    let numeric = value
13396        .as_u64()
13397        .or_else(|| {
13398            value
13399                .as_f64()
13400                .filter(|number| number.is_finite() && *number >= 0.0)
13401                .map(|number| number as u64)
13402        })
13403        .ok_or_else(|| SidecarError::InvalidState(format!("{key} must be numeric")))?;
13404
13405    u32::try_from(numeric)
13406        .map(Some)
13407        .map_err(|_| SidecarError::InvalidState(format!("{key} must fit within u32")))
13408}
13409
13410pub(crate) fn javascript_sync_rpc_arg_u32(
13411    args: &[Value],
13412    index: usize,
13413    label: &str,
13414) -> Result<u32, SidecarError> {
13415    let value = javascript_sync_rpc_arg_u64(args, index, label)?;
13416    u32::try_from(value)
13417        .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")))
13418}
13419
13420pub(crate) fn javascript_sync_rpc_arg_i32(
13421    args: &[Value],
13422    index: usize,
13423    label: &str,
13424) -> Result<i32, SidecarError> {
13425    let Some(value) = args.get(index) else {
13426        return Err(SidecarError::InvalidState(format!("{label} is required")));
13427    };
13428
13429    let numeric = value
13430        .as_i64()
13431        .or_else(|| {
13432            value
13433                .as_f64()
13434                .filter(|number| number.is_finite())
13435                .map(|number| number as i64)
13436        })
13437        .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a numeric argument")))?;
13438
13439    i32::try_from(numeric)
13440        .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within i32")))
13441}
13442
13443pub(crate) fn javascript_sync_rpc_arg_u32_optional(
13444    args: &[Value],
13445    index: usize,
13446    label: &str,
13447) -> Result<Option<u32>, SidecarError> {
13448    javascript_sync_rpc_arg_u64_optional(args, index, label)?
13449        .map(|value| {
13450            u32::try_from(value)
13451                .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")))
13452        })
13453        .transpose()
13454}
13455
13456pub(crate) fn javascript_sync_rpc_arg_u64(
13457    args: &[Value],
13458    index: usize,
13459    label: &str,
13460) -> Result<u64, SidecarError> {
13461    let Some(value) = args.get(index) else {
13462        return Err(SidecarError::InvalidState(format!("{label} is required")));
13463    };
13464
13465    value
13466        .as_u64()
13467        .or_else(|| {
13468            value
13469                .as_f64()
13470                .filter(|number| number.is_finite() && *number >= 0.0)
13471                .map(|number| number as u64)
13472        })
13473        .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a numeric argument")))
13474}
13475
13476pub(crate) fn javascript_sync_rpc_arg_u64_optional(
13477    args: &[Value],
13478    index: usize,
13479    label: &str,
13480) -> Result<Option<u64>, SidecarError> {
13481    let Some(value) = args.get(index) else {
13482        return Ok(None);
13483    };
13484    if value.is_null() {
13485        return Ok(None);
13486    }
13487    javascript_sync_rpc_arg_u64(args, index, label).map(Some)
13488}
13489
13490pub(crate) fn javascript_sync_rpc_bytes_arg(
13491    args: &[Value],
13492    index: usize,
13493    label: &str,
13494) -> Result<Vec<u8>, SidecarError> {
13495    let Some(value) = args.get(index) else {
13496        return Err(SidecarError::InvalidState(format!("{label} is required")));
13497    };
13498
13499    if let Some(text) = value.as_str() {
13500        return Ok(text.as_bytes().to_vec());
13501    }
13502
13503    let Some(base64_value) = value
13504        .get("__agentOSType")
13505        .and_then(Value::as_str)
13506        .filter(|kind| *kind == "bytes")
13507        .and_then(|_| value.get("base64"))
13508        .and_then(Value::as_str)
13509    else {
13510        return Err(SidecarError::InvalidState(format!(
13511            "{label} must be a string or encoded bytes payload"
13512        )));
13513    };
13514
13515    base64::engine::general_purpose::STANDARD
13516        .decode(base64_value)
13517        .map_err(|error| {
13518            SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
13519        })
13520}
13521
13522pub(crate) fn javascript_sync_rpc_bytes_value(bytes: &[u8]) -> Value {
13523    json!({
13524        "__agentOSType": "bytes",
13525        "base64": base64::engine::general_purpose::STANDARD.encode(bytes),
13526    })
13527}
13528
13529#[derive(Debug, Deserialize)]
13530struct KernelPollFdRequest {
13531    fd: u32,
13532    events: u16,
13533}
13534
13535#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
13536struct KernelPollFdResponse {
13537    fd: u32,
13538    events: u16,
13539    revents: u16,
13540}
13541
13542fn javascript_sync_rpc_base64_arg(
13543    args: &[Value],
13544    index: usize,
13545    label: &str,
13546) -> Result<Vec<u8>, SidecarError> {
13547    let value = javascript_sync_rpc_arg_str(args, index, label)?;
13548    base64::engine::general_purpose::STANDARD
13549        .decode(value)
13550        .map_err(|error| {
13551            SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
13552        })
13553}
13554
13555pub(crate) fn service_javascript_sync_rpc<B>(
13556    request: JavascriptSyncRpcServiceRequest<'_, B>,
13557) -> Result<Value, SidecarError>
13558where
13559    B: NativeSidecarBridge + Send + 'static,
13560    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
13561{
13562    let JavascriptSyncRpcServiceRequest {
13563        bridge,
13564        vm_id,
13565        dns,
13566        socket_paths,
13567        kernel,
13568        process,
13569        sync_request: request,
13570        resource_limits,
13571        network_counts,
13572    } = request;
13573    match request.method.as_str() {
13574        // Module resolution / loading / format detection read the kernel VFS so
13575        // the resolver sees exactly what the guest and `kernel.readFile()` see.
13576        "_resolveModule"
13577        | "_resolveModuleSync"
13578        | "__resolve_module"
13579        | "_batchResolveModules"
13580        | "__batch_resolve_modules"
13581        | "_loadFile"
13582        | "_loadFileSync"
13583        | "__load_file"
13584        | "_moduleFormat"
13585        | "__module_format" => service_javascript_module_sync_rpc(kernel, process, request),
13586        // Polyfills are static guest expressions, not VFS reads.
13587        "_loadPolyfill" | "__load_polyfill" => {
13588            service_javascript_internal_bridge_sync_rpc(process, request)
13589        }
13590        "__kernel_stdin_read" => match &process.execution {
13591            ActiveExecution::Javascript(execution) => execution
13592                .read_kernel_stdin_sync_rpc(request)
13593                .map_err(|error| SidecarError::Execution(error.to_string())),
13594            ActiveExecution::Python(_) | ActiveExecution::Wasm(_) | ActiveExecution::Tool(_) => {
13595                service_javascript_kernel_stdin_sync_rpc(kernel, process, request)
13596            }
13597        },
13598        "__kernel_stdio_write" => {
13599            service_javascript_kernel_stdio_write_sync_rpc(kernel, process, request)
13600        }
13601        "__kernel_poll" => service_javascript_kernel_poll_sync_rpc(kernel, process, request),
13602        "__pty_set_raw_mode" => {
13603            service_javascript_pty_set_raw_mode_sync_rpc(kernel, process, request)
13604        }
13605        "crypto.hashDigest"
13606        | "crypto.hmacDigest"
13607        | "crypto.pbkdf2"
13608        | "crypto.scrypt"
13609        | "crypto.cipheriv"
13610        | "crypto.decipheriv"
13611        | "crypto.cipherivCreate"
13612        | "crypto.cipherivUpdate"
13613        | "crypto.cipherivFinal"
13614        | "crypto.sign"
13615        | "crypto.verify"
13616        | "crypto.asymmetricOp"
13617        | "crypto.createKeyObject"
13618        | "crypto.generateKeyPairSync"
13619        | "crypto.generateKeySync"
13620        | "crypto.generatePrimeSync"
13621        | "crypto.diffieHellman"
13622        | "crypto.diffieHellmanGroup"
13623        | "crypto.diffieHellmanSessionCreate"
13624        | "crypto.diffieHellmanSessionCall"
13625        | "crypto.diffieHellmanSessionDestroy"
13626        | "crypto.subtle" => service_javascript_crypto_sync_rpc(process, request),
13627        "dns.lookup" | "dns.resolve" | "dns.resolve4" | "dns.resolve6" => {
13628            service_javascript_dns_sync_rpc(bridge, kernel, vm_id, dns, request)
13629        }
13630        "net.http_listen" | "net.http_close" | "net.http_wait" | "net.http_respond" => {
13631            service_javascript_net_sync_rpc(JavascriptNetSyncRpcServiceRequest {
13632                bridge,
13633                vm_id,
13634                dns,
13635                socket_paths,
13636                kernel,
13637                process,
13638                sync_request: request,
13639                resource_limits,
13640                network_counts,
13641            })
13642        }
13643        "net.http2_server_listen"
13644        | "net.http2_server_poll"
13645        | "net.http2_server_close"
13646        | "net.http2_server_respond"
13647        | "net.http2_server_wait"
13648        | "net.http2_session_connect"
13649        | "net.http2_session_request"
13650        | "net.http2_session_settings"
13651        | "net.http2_session_set_local_window_size"
13652        | "net.http2_session_goaway"
13653        | "net.http2_session_close"
13654        | "net.http2_session_destroy"
13655        | "net.http2_session_poll"
13656        | "net.http2_session_wait"
13657        | "net.http2_stream_respond"
13658        | "net.http2_stream_push_stream"
13659        | "net.http2_stream_write"
13660        | "net.http2_stream_end"
13661        | "net.http2_stream_close"
13662        | "net.http2_stream_pause"
13663        | "net.http2_stream_resume"
13664        | "net.http2_stream_respond_with_file" => {
13665            service_javascript_http2_sync_rpc(JavascriptHttp2SyncRpcServiceRequest {
13666                bridge,
13667                kernel,
13668                vm_id,
13669                dns,
13670                socket_paths,
13671                process,
13672                sync_request: request,
13673                resource_limits,
13674                network_counts,
13675            })
13676        }
13677        "net.connect"
13678        | "net.reserve_tcp_port"
13679        | "net.release_tcp_port"
13680        | "net.listen"
13681        | "net.poll"
13682        | "net.socket_wait_connect"
13683        | "net.socket_read"
13684        | "net.socket_set_no_delay"
13685        | "net.socket_set_keep_alive"
13686        | "net.socket_upgrade_tls"
13687        | "net.socket_get_tls_client_hello"
13688        | "net.socket_tls_query"
13689        | "net.server_poll"
13690        | "net.server_accept"
13691        | "net.server_connections"
13692        | "net.upgrade_socket_write"
13693        | "net.upgrade_socket_end"
13694        | "net.upgrade_socket_destroy"
13695        | "net.write"
13696        | "net.shutdown"
13697        | "net.destroy"
13698        | "net.server_close"
13699        | "tls.get_ciphers" => {
13700            service_javascript_net_sync_rpc(JavascriptNetSyncRpcServiceRequest {
13701                bridge,
13702                vm_id,
13703                dns,
13704                socket_paths,
13705                kernel,
13706                process,
13707                sync_request: request,
13708                resource_limits,
13709                network_counts,
13710            })
13711        }
13712        "dgram.createSocket"
13713        | "dgram.bind"
13714        | "dgram.send"
13715        | "dgram.poll"
13716        | "dgram.close"
13717        | "dgram.address"
13718        | "dgram.setBufferSize"
13719        | "dgram.getBufferSize" => {
13720            service_javascript_dgram_sync_rpc(JavascriptDgramSyncRpcServiceRequest {
13721                bridge,
13722                kernel,
13723                vm_id,
13724                dns,
13725                socket_paths,
13726                process,
13727                sync_request: request,
13728                resource_limits,
13729                network_counts,
13730            })
13731        }
13732        "sqlite.constants"
13733        | "sqlite.open"
13734        | "sqlite.close"
13735        | "sqlite.exec"
13736        | "sqlite.query"
13737        | "sqlite.prepare"
13738        | "sqlite.location"
13739        | "sqlite.checkpoint"
13740        | "sqlite.statement.run"
13741        | "sqlite.statement.get"
13742        | "sqlite.statement.all"
13743        | "sqlite.statement.iterate"
13744        | "sqlite.statement.columns"
13745        | "sqlite.statement.setReturnArrays"
13746        | "sqlite.statement.setReadBigInts"
13747        | "sqlite.statement.setAllowBareNamedParameters"
13748        | "sqlite.statement.setAllowUnknownNamedParameters"
13749        | "sqlite.statement.finalize" => {
13750            service_javascript_sqlite_sync_rpc(kernel, process, request)
13751        }
13752        "process.kill" => {
13753            let target_pid =
13754                javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?;
13755            let signal = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?;
13756            let parsed_signal = parse_signal(signal)?;
13757            if parsed_signal == 0 {
13758                kernel
13759                    .signal_process(EXECUTION_DRIVER_NAME, target_pid, parsed_signal)
13760                    .map_err(kernel_error)?;
13761                return Ok(Value::Null);
13762            }
13763            let process_pid = i32::try_from(process.kernel_pid)
13764                .map_err(|_| SidecarError::InvalidState("process pid exceeds i32".into()))?;
13765            if target_pid != process_pid {
13766                return Err(SidecarError::InvalidState(format!(
13767                    "unknown process pid {target_pid}"
13768                )));
13769            }
13770            process.pending_self_signal_exit = None;
13771            if parsed_signal != 0
13772                && !matches!(
13773                    canonical_signal_name(parsed_signal),
13774                    Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
13775                )
13776            {
13777                process.pending_self_signal_exit = Some(parsed_signal);
13778            }
13779            Ok(json!({
13780                "self": true,
13781                "action": "default",
13782            }))
13783        }
13784        "process.umask" => {
13785            let new_mask = javascript_sync_rpc_arg_u32_optional(&request.args, 0, "process umask")?;
13786            kernel
13787                .umask(EXECUTION_DRIVER_NAME, process.kernel_pid, new_mask)
13788                .map(|mask| json!(mask))
13789                .map_err(kernel_error)
13790        }
13791        "fs.chmodSync" | "fs.promises.chmod" => {
13792            let response =
13793                service_javascript_fs_sync_rpc(kernel, process, process.kernel_pid, request)?;
13794            mirror_process_chmod_to_host(process, request)?;
13795            Ok(response)
13796        }
13797        _ => service_javascript_fs_sync_rpc(kernel, process, process.kernel_pid, request),
13798    }
13799}
13800
13801fn service_javascript_internal_bridge_sync_rpc(
13802    process: &ActiveProcess,
13803    request: &JavascriptSyncRpcRequest,
13804) -> Result<Value, SidecarError> {
13805    // Module resolution / loading / format now reads the kernel VFS via
13806    // `service_javascript_module_sync_rpc`. This host-context path only handles
13807    // polyfills, which are static guest expressions independent of the FS.
13808    let method = match request.method.as_str() {
13809        "_loadPolyfill" | "__load_polyfill" => "_loadPolyfill",
13810        other => {
13811            return Err(SidecarError::InvalidState(format!(
13812                "unsupported JavaScript internal bridge method {other}"
13813            )));
13814        }
13815    };
13816
13817    handle_internal_bridge_call_from_host_context(
13818        &process.host_cwd,
13819        &process.guest_cwd,
13820        &process.env,
13821        method,
13822        &request.args,
13823    )
13824    .ok_or_else(|| {
13825        SidecarError::InvalidState(format!(
13826            "JavaScript internal bridge method {method} returned no value"
13827        ))
13828    })
13829}
13830
13831fn mirror_process_chmod_to_host(
13832    process: &ActiveProcess,
13833    request: &JavascriptSyncRpcRequest,
13834) -> Result<(), SidecarError> {
13835    let guest_path = javascript_sync_rpc_arg_str(&request.args, 0, "filesystem chmod path")?;
13836    let mode = javascript_sync_rpc_arg_u32(&request.args, 1, "filesystem chmod mode")? & 0o7777;
13837    let Some(host_path) = resolve_process_guest_path_to_host(process, guest_path) else {
13838        return Ok(());
13839    };
13840    if !host_path.exists() {
13841        return Ok(());
13842    }
13843    fs::set_permissions(&host_path, fs::Permissions::from_mode(mode)).map_err(|error| {
13844        SidecarError::Io(format!(
13845            "failed to mirror chmod to host path {}: {error}",
13846            host_path.display()
13847        ))
13848    })
13849}
13850
13851fn resolve_process_guest_path_to_host(
13852    process: &ActiveProcess,
13853    guest_path: &str,
13854) -> Option<PathBuf> {
13855    let normalized_guest_path = if guest_path.starts_with('/') {
13856        normalize_path(guest_path)
13857    } else {
13858        normalize_path(&format!(
13859            "{}/{}",
13860            process.guest_cwd.trim_end_matches('/'),
13861            guest_path
13862        ))
13863    };
13864    if let Some(host_path) =
13865        host_path_from_runtime_guest_mappings(&process.env, &normalized_guest_path)
13866    {
13867        return Some(host_path);
13868    }
13869    let normalized_guest_cwd = normalize_path(&process.guest_cwd);
13870    let mut host_root = normalize_host_path(&process.host_cwd);
13871    for _ in normalized_guest_cwd
13872        .trim_start_matches('/')
13873        .split('/')
13874        .filter(|segment| !segment.is_empty())
13875    {
13876        host_root = host_root.parent()?.to_path_buf();
13877    }
13878    if normalized_guest_path == "/" {
13879        Some(host_root)
13880    } else {
13881        Some(host_root.join(normalized_guest_path.trim_start_matches('/')))
13882    }
13883}
13884
13885pub(crate) fn service_javascript_crypto_sync_rpc(
13886    process: &mut ActiveProcess,
13887    request: &JavascriptSyncRpcRequest,
13888) -> Result<Value, SidecarError> {
13889    match request.method.as_str() {
13890        "crypto.hashDigest" => {
13891            let algorithm = javascript_crypto_digest_algorithm(
13892                &request.args,
13893                0,
13894                "crypto.hashDigest algorithm",
13895            )?;
13896            let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.hashDigest data")?;
13897            Ok(Value::String(
13898                base64::engine::general_purpose::STANDARD.encode(algorithm.digest(&data)),
13899            ))
13900        }
13901        "crypto.hmacDigest" => {
13902            let algorithm = javascript_crypto_digest_algorithm(
13903                &request.args,
13904                0,
13905                "crypto.hmacDigest algorithm",
13906            )?;
13907            let key = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.hmacDigest key")?;
13908            let data = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.hmacDigest data")?;
13909            Ok(Value::String(
13910                base64::engine::general_purpose::STANDARD.encode(algorithm.hmac(&key, &data)?),
13911            ))
13912        }
13913        "crypto.pbkdf2" => {
13914            let password =
13915                javascript_sync_rpc_base64_arg(&request.args, 0, "crypto.pbkdf2 password")?;
13916            let salt = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.pbkdf2 salt")?;
13917            let iterations =
13918                javascript_sync_rpc_arg_u32(&request.args, 2, "crypto.pbkdf2 iterations")?;
13919            if iterations == 0 {
13920                return Err(SidecarError::InvalidState(String::from(
13921                    "crypto.pbkdf2 iterations must be greater than zero",
13922                )));
13923            }
13924            let key_len = usize::try_from(javascript_sync_rpc_arg_u64(
13925                &request.args,
13926                3,
13927                "crypto.pbkdf2 key length",
13928            )?)
13929            .map_err(|_| {
13930                SidecarError::InvalidState(String::from(
13931                    "crypto.pbkdf2 key length must fit within usize",
13932                ))
13933            })?;
13934            let algorithm =
13935                javascript_crypto_digest_algorithm(&request.args, 4, "crypto.pbkdf2 digest")?;
13936            let mut output = vec![0u8; key_len];
13937            algorithm.pbkdf2(&password, &salt, iterations, &mut output);
13938            Ok(Value::String(
13939                base64::engine::general_purpose::STANDARD.encode(output),
13940            ))
13941        }
13942        "crypto.scrypt" => {
13943            let password =
13944                javascript_sync_rpc_base64_arg(&request.args, 0, "crypto.scrypt password")?;
13945            let salt = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.scrypt salt")?;
13946            let key_len = usize::try_from(javascript_sync_rpc_arg_u64(
13947                &request.args,
13948                2,
13949                "crypto.scrypt key length",
13950            )?)
13951            .map_err(|_| {
13952                SidecarError::InvalidState(String::from(
13953                    "crypto.scrypt key length must fit within usize",
13954                ))
13955            })?;
13956            let options_json =
13957                javascript_sync_rpc_arg_str(&request.args, 3, "crypto.scrypt options")?;
13958            let options: JavascriptScryptOptions =
13959                serde_json::from_str(options_json).map_err(|error| {
13960                    SidecarError::InvalidState(format!(
13961                        "crypto.scrypt options must be valid JSON: {error}"
13962                    ))
13963                })?;
13964            let cost = options.cost.unwrap_or(DEFAULT_SCRYPT_COST);
13965            if cost == 0 || !cost.is_power_of_two() {
13966                return Err(SidecarError::InvalidState(String::from(
13967                    "crypto.scrypt cost must be a positive power of two",
13968                )));
13969            }
13970            let log_n = u8::try_from(cost.ilog2()).map_err(|_| {
13971                SidecarError::InvalidState(String::from(
13972                    "crypto.scrypt cost exceeds supported parameter range",
13973                ))
13974            })?;
13975            let params = ScryptParams::new(
13976                log_n,
13977                options.block_size.unwrap_or(DEFAULT_SCRYPT_BLOCK_SIZE),
13978                options
13979                    .parallelization
13980                    .unwrap_or(DEFAULT_SCRYPT_PARALLELIZATION),
13981                key_len,
13982            )
13983            .map_err(|error| {
13984                SidecarError::InvalidState(format!("crypto.scrypt options are invalid: {error}"))
13985            })?;
13986            let mut output = vec![0u8; key_len];
13987            scrypt(&password, &salt, &params, &mut output).map_err(|error| {
13988                SidecarError::Execution(format!("crypto.scrypt failed: {error}"))
13989            })?;
13990            Ok(Value::String(
13991                base64::engine::general_purpose::STANDARD.encode(output),
13992            ))
13993        }
13994        "crypto.cipheriv" => service_javascript_crypto_cipheriv_sync_rpc(request),
13995        "crypto.decipheriv" => service_javascript_crypto_decipheriv_sync_rpc(request),
13996        "crypto.cipherivCreate" => {
13997            service_javascript_crypto_cipheriv_create_sync_rpc(process, request)
13998        }
13999        "crypto.cipherivUpdate" => {
14000            service_javascript_crypto_cipheriv_update_sync_rpc(process, request)
14001        }
14002        "crypto.cipherivFinal" => {
14003            service_javascript_crypto_cipheriv_final_sync_rpc(process, request)
14004        }
14005        "crypto.sign" => service_javascript_crypto_sign_sync_rpc(request),
14006        "crypto.verify" => service_javascript_crypto_verify_sync_rpc(request),
14007        "crypto.asymmetricOp" => service_javascript_crypto_asymmetric_op_sync_rpc(request),
14008        "crypto.createKeyObject" => service_javascript_crypto_create_key_object_sync_rpc(request),
14009        "crypto.generateKeyPairSync" => {
14010            service_javascript_crypto_generate_key_pair_sync_rpc(request)
14011        }
14012        "crypto.generateKeySync" => service_javascript_crypto_generate_key_sync_rpc(request),
14013        "crypto.generatePrimeSync" => service_javascript_crypto_generate_prime_sync_rpc(request),
14014        "crypto.diffieHellman" => service_javascript_crypto_diffie_hellman_sync_rpc(request),
14015        "crypto.diffieHellmanGroup" => {
14016            service_javascript_crypto_diffie_hellman_group_sync_rpc(request)
14017        }
14018        "crypto.diffieHellmanSessionCreate" => {
14019            service_javascript_crypto_diffie_hellman_session_create_sync_rpc(process, request)
14020        }
14021        "crypto.diffieHellmanSessionCall" => {
14022            service_javascript_crypto_diffie_hellman_session_call_sync_rpc(process, request)
14023        }
14024        "crypto.diffieHellmanSessionDestroy" => {
14025            service_javascript_crypto_diffie_hellman_session_destroy_sync_rpc(process, request)
14026        }
14027        "crypto.subtle" => service_javascript_crypto_subtle_sync_rpc(request),
14028        _ => Err(SidecarError::InvalidState(format!(
14029            "unsupported JavaScript crypto sync RPC method {}",
14030            request.method
14031        ))),
14032    }
14033}
14034
14035fn javascript_crypto_digest_algorithm(
14036    args: &[Value],
14037    index: usize,
14038    label: &str,
14039) -> Result<JavascriptCryptoDigestAlgorithm, SidecarError> {
14040    JavascriptCryptoDigestAlgorithm::parse(javascript_sync_rpc_arg_str(args, index, label)?)
14041}
14042
14043impl JavascriptCryptoDigestAlgorithm {
14044    fn parse(value: &str) -> Result<Self, SidecarError> {
14045        match value.trim().to_ascii_lowercase().replace('-', "").as_str() {
14046            "md5" => Ok(Self::Md5),
14047            "sha1" => Ok(Self::Sha1),
14048            "sha256" => Ok(Self::Sha256),
14049            "sha512" => Ok(Self::Sha512),
14050            _ => Err(SidecarError::InvalidState(format!(
14051                "unsupported crypto digest algorithm {value}"
14052            ))),
14053        }
14054    }
14055
14056    fn digest(self, data: &[u8]) -> Vec<u8> {
14057        match self {
14058            Self::Md5 => Md5::digest(data).to_vec(),
14059            Self::Sha1 => Sha1::digest(data).to_vec(),
14060            Self::Sha256 => Sha256::digest(data).to_vec(),
14061            Self::Sha512 => Sha512::digest(data).to_vec(),
14062        }
14063    }
14064
14065    fn hmac(self, key: &[u8], data: &[u8]) -> Result<Vec<u8>, SidecarError> {
14066        match self {
14067            Self::Md5 => {
14068                let mut mac = Hmac::<Md5>::new_from_slice(key).map_err(|error| {
14069                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14070                })?;
14071                mac.update(data);
14072                Ok(mac.finalize().into_bytes().to_vec())
14073            }
14074            Self::Sha1 => {
14075                let mut mac = Hmac::<Sha1>::new_from_slice(key).map_err(|error| {
14076                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14077                })?;
14078                mac.update(data);
14079                Ok(mac.finalize().into_bytes().to_vec())
14080            }
14081            Self::Sha256 => {
14082                let mut mac = Hmac::<Sha256>::new_from_slice(key).map_err(|error| {
14083                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14084                })?;
14085                mac.update(data);
14086                Ok(mac.finalize().into_bytes().to_vec())
14087            }
14088            Self::Sha512 => {
14089                let mut mac = Hmac::<Sha512>::new_from_slice(key).map_err(|error| {
14090                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14091                })?;
14092                mac.update(data);
14093                Ok(mac.finalize().into_bytes().to_vec())
14094            }
14095        }
14096    }
14097
14098    fn pbkdf2(self, password: &[u8], salt: &[u8], iterations: u32, output: &mut [u8]) {
14099        match self {
14100            Self::Md5 => pbkdf2_hmac::<Md5>(password, salt, iterations, output),
14101            Self::Sha1 => pbkdf2_hmac::<Sha1>(password, salt, iterations, output),
14102            Self::Sha256 => pbkdf2_hmac::<Sha256>(password, salt, iterations, output),
14103            Self::Sha512 => pbkdf2_hmac::<Sha512>(password, salt, iterations, output),
14104        }
14105    }
14106}
14107
14108#[derive(Debug, Clone)]
14109enum JavascriptCryptoKeyMaterial {
14110    Private(PKey<Private>),
14111    Public(PKey<Public>),
14112    Secret(Vec<u8>),
14113}
14114
14115#[derive(Debug, Clone, Deserialize, Serialize)]
14116struct JavascriptSerializedSandboxKeyObject {
14117    #[serde(rename = "type")]
14118    kind: String,
14119    #[serde(skip_serializing_if = "Option::is_none")]
14120    pem: Option<String>,
14121    #[serde(skip_serializing_if = "Option::is_none")]
14122    raw: Option<String>,
14123    #[serde(skip_serializing_if = "Option::is_none", rename = "asymmetricKeyType")]
14124    asymmetric_key_type: Option<String>,
14125    #[serde(
14126        skip_serializing_if = "Option::is_none",
14127        rename = "asymmetricKeyDetails"
14128    )]
14129    asymmetric_key_details: Option<Map<String, Value>>,
14130    #[serde(skip_serializing_if = "Option::is_none")]
14131    jwk: Option<Value>,
14132}
14133
14134#[derive(Debug, Clone)]
14135struct JavascriptDirectKeyInput {
14136    key: JavascriptCryptoKeyMaterial,
14137    padding: Option<Padding>,
14138}
14139
14140fn service_javascript_crypto_cipheriv_sync_rpc(
14141    request: &JavascriptSyncRpcRequest,
14142) -> Result<Value, SidecarError> {
14143    service_javascript_crypto_cipheriv_inner(request, false)
14144}
14145
14146fn service_javascript_crypto_decipheriv_sync_rpc(
14147    request: &JavascriptSyncRpcRequest,
14148) -> Result<Value, SidecarError> {
14149    service_javascript_crypto_cipheriv_inner(request, true)
14150}
14151
14152fn service_javascript_crypto_cipheriv_create_sync_rpc(
14153    process: &mut ActiveProcess,
14154    request: &JavascriptSyncRpcRequest,
14155) -> Result<Value, SidecarError> {
14156    ensure_per_process_state_handle_capacity(process.cipher_sessions.len(), "cipher session")?;
14157    let mode = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.cipherivCreate mode")?;
14158    let decrypt = mode == "decipher";
14159    let algorithm =
14160        javascript_sync_rpc_arg_str(&request.args, 1, "crypto.cipherivCreate algorithm")?;
14161    let key = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.cipherivCreate key")?;
14162    let iv = javascript_sync_rpc_base64_arg_optional(&request.args, 3, "crypto.cipherivCreate iv")?;
14163    let options =
14164        javascript_sync_rpc_json_arg_optional(&request.args, 4, "crypto.cipherivCreate options")?;
14165    let auth_tag_len = javascript_crypto_requested_aead_tag_len(algorithm, options.as_ref())?;
14166    let context = javascript_crypto_build_cipher_context(
14167        algorithm,
14168        &key,
14169        iv.as_deref(),
14170        decrypt,
14171        options.as_ref(),
14172    )?;
14173    process.next_cipher_session_id += 1;
14174    let session_id = process.next_cipher_session_id;
14175    process.cipher_sessions.insert(
14176        session_id,
14177        ActiveCipherSession {
14178            algorithm: algorithm.to_string(),
14179            auth_tag_len,
14180            context,
14181        },
14182    );
14183    Ok(json!(session_id))
14184}
14185
14186fn service_javascript_crypto_cipheriv_update_sync_rpc(
14187    process: &mut ActiveProcess,
14188    request: &JavascriptSyncRpcRequest,
14189) -> Result<Value, SidecarError> {
14190    let session_id =
14191        javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.cipherivUpdate session id")?;
14192    let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.cipherivUpdate data")?;
14193    let session = process
14194        .cipher_sessions
14195        .get_mut(&session_id)
14196        .ok_or_else(|| {
14197            SidecarError::InvalidState(format!("Cipher session {session_id} not found"))
14198        })?;
14199    let result = javascript_crypto_cipher_update(&mut session.context, &data)?;
14200    Ok(Value::String(
14201        base64::engine::general_purpose::STANDARD.encode(result),
14202    ))
14203}
14204
14205fn service_javascript_crypto_cipheriv_final_sync_rpc(
14206    process: &mut ActiveProcess,
14207    request: &JavascriptSyncRpcRequest,
14208) -> Result<Value, SidecarError> {
14209    let session_id =
14210        javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.cipherivFinal session id")?;
14211    let mut session = process.cipher_sessions.remove(&session_id).ok_or_else(|| {
14212        SidecarError::InvalidState(format!("Cipher session {session_id} not found"))
14213    })?;
14214    let data = javascript_crypto_cipher_finalize(&mut session.context)?;
14215    let mut response = Map::new();
14216    response.insert(
14217        String::from("data"),
14218        Value::String(base64::engine::general_purpose::STANDARD.encode(data)),
14219    );
14220    if javascript_crypto_is_aead(&session.algorithm) {
14221        let mut auth_tag = vec![0_u8; session.auth_tag_len];
14222        session
14223            .context
14224            .get_tag(&mut auth_tag)
14225            .map_err(javascript_crypto_openssl_error)?;
14226        response.insert(
14227            String::from("authTag"),
14228            Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
14229        );
14230    }
14231    Ok(Value::String(serde_json::to_string(&response).map_err(
14232        |error| SidecarError::InvalidState(format!("serialize cipher final response: {error}")),
14233    )?))
14234}
14235
14236fn service_javascript_crypto_sign_sync_rpc(
14237    request: &JavascriptSyncRpcRequest,
14238) -> Result<Value, SidecarError> {
14239    let algorithm = request.args.first().and_then(Value::as_str);
14240    let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.sign data")?;
14241    let key_json = javascript_sync_rpc_arg_str(&request.args, 2, "crypto.sign key")?;
14242    let key_input =
14243        javascript_crypto_parse_direct_key_input(key_json, Some("private"), "crypto.sign key")?;
14244    let private_key = javascript_crypto_expect_private_key(key_input.key, "crypto.sign key")?;
14245    let mut signer = javascript_crypto_new_signer(algorithm, &private_key)?;
14246    if let Some(padding) = key_input.padding {
14247        signer
14248            .set_rsa_padding(padding)
14249            .map_err(javascript_crypto_openssl_error)?;
14250    }
14251    signer
14252        .update(&data)
14253        .map_err(javascript_crypto_openssl_error)?;
14254    Ok(Value::String(
14255        base64::engine::general_purpose::STANDARD.encode(
14256            signer
14257                .sign_to_vec()
14258                .map_err(javascript_crypto_openssl_error)?,
14259        ),
14260    ))
14261}
14262
14263fn service_javascript_crypto_verify_sync_rpc(
14264    request: &JavascriptSyncRpcRequest,
14265) -> Result<Value, SidecarError> {
14266    let algorithm = request.args.first().and_then(Value::as_str);
14267    let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.verify data")?;
14268    let key_json = javascript_sync_rpc_arg_str(&request.args, 2, "crypto.verify key")?;
14269    let signature = javascript_sync_rpc_base64_arg(&request.args, 3, "crypto.verify signature")?;
14270    let key_input =
14271        javascript_crypto_parse_direct_key_input(key_json, Some("public"), "crypto.verify key")?;
14272    let public_key = javascript_crypto_expect_public_key(key_input.key, "crypto.verify key")?;
14273    let mut verifier = javascript_crypto_new_verifier(algorithm, &public_key)?;
14274    if let Some(padding) = key_input.padding {
14275        verifier
14276            .set_rsa_padding(padding)
14277            .map_err(javascript_crypto_openssl_error)?;
14278    }
14279    verifier
14280        .update(&data)
14281        .map_err(javascript_crypto_openssl_error)?;
14282    Ok(json!(verifier
14283        .verify(&signature)
14284        .map_err(javascript_crypto_openssl_error)?))
14285}
14286
14287fn service_javascript_crypto_asymmetric_op_sync_rpc(
14288    request: &JavascriptSyncRpcRequest,
14289) -> Result<Value, SidecarError> {
14290    let operation = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.asymmetricOp operation")?;
14291    let key_json = javascript_sync_rpc_arg_str(&request.args, 1, "crypto.asymmetricOp key")?;
14292    let data = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.asymmetricOp data")?;
14293    let expect_kind = match operation {
14294        "publicEncrypt" | "publicDecrypt" => Some("public"),
14295        "privateEncrypt" | "privateDecrypt" => Some("private"),
14296        other => {
14297            return Err(SidecarError::InvalidState(format!(
14298                "Unsupported asymmetric crypto operation: {other}"
14299            )));
14300        }
14301    };
14302    let key_input =
14303        javascript_crypto_parse_direct_key_input(key_json, expect_kind, "crypto.asymmetricOp key")?;
14304    let padding = key_input.padding.unwrap_or(Padding::PKCS1);
14305    let mut output = vec![0_u8; javascript_crypto_rsa_output_size(&key_input.key)?];
14306    let written = match (operation, key_input.key) {
14307        ("publicEncrypt", JavascriptCryptoKeyMaterial::Public(key))
14308        | ("publicDecrypt", JavascriptCryptoKeyMaterial::Public(key)) => {
14309            let rsa = key.rsa().map_err(javascript_crypto_openssl_error)?;
14310            if operation == "publicEncrypt" {
14311                rsa.public_encrypt(&data, &mut output, padding)
14312                    .map_err(javascript_crypto_openssl_error)?
14313            } else {
14314                rsa.public_decrypt(&data, &mut output, padding)
14315                    .map_err(javascript_crypto_openssl_error)?
14316            }
14317        }
14318        ("privateEncrypt", JavascriptCryptoKeyMaterial::Private(key))
14319        | ("privateDecrypt", JavascriptCryptoKeyMaterial::Private(key)) => {
14320            let rsa = key.rsa().map_err(javascript_crypto_openssl_error)?;
14321            if operation == "privateEncrypt" {
14322                rsa.private_encrypt(&data, &mut output, padding)
14323                    .map_err(javascript_crypto_openssl_error)?
14324            } else {
14325                rsa.private_decrypt(&data, &mut output, padding)
14326                    .map_err(javascript_crypto_openssl_error)?
14327            }
14328        }
14329        _ => {
14330            return Err(SidecarError::InvalidState(format!(
14331                "{operation} requires an RSA {} key",
14332                expect_kind.unwrap_or("asymmetric")
14333            )));
14334        }
14335    };
14336    output.truncate(written);
14337    Ok(Value::String(
14338        base64::engine::general_purpose::STANDARD.encode(output),
14339    ))
14340}
14341
14342fn service_javascript_crypto_create_key_object_sync_rpc(
14343    request: &JavascriptSyncRpcRequest,
14344) -> Result<Value, SidecarError> {
14345    let operation =
14346        javascript_sync_rpc_arg_str(&request.args, 0, "crypto.createKeyObject operation")?;
14347    let key_json = javascript_sync_rpc_arg_str(&request.args, 1, "crypto.createKeyObject key")?;
14348    let expected = match operation {
14349        "createPrivateKey" => Some("private"),
14350        "createPublicKey" => Some("public"),
14351        other => {
14352            return Err(SidecarError::InvalidState(format!(
14353                "Unsupported key creation operation: {other}"
14354            )));
14355        }
14356    };
14357    let key_input =
14358        javascript_crypto_parse_direct_key_input(key_json, expected, "crypto.createKeyObject key")?;
14359    Ok(Value::String(
14360        serde_json::to_string(&javascript_crypto_serialize_sandbox_key_object(
14361            &key_input.key,
14362        )?)
14363        .map_err(|error| {
14364            SidecarError::InvalidState(format!("serialize crypto key object: {error}"))
14365        })?,
14366    ))
14367}
14368
14369fn service_javascript_crypto_generate_key_pair_sync_rpc(
14370    request: &JavascriptSyncRpcRequest,
14371) -> Result<Value, SidecarError> {
14372    let key_type =
14373        javascript_sync_rpc_arg_str(&request.args, 0, "crypto.generateKeyPairSync type")?;
14374    let options = javascript_crypto_parse_serialized_options_arg(
14375        &request.args,
14376        1,
14377        "crypto.generateKeyPairSync options",
14378    )?
14379    .unwrap_or(Value::Object(Map::new()));
14380    let public_encoding = options.get("publicKeyEncoding").cloned();
14381    let private_encoding = options.get("privateKeyEncoding").cloned();
14382
14383    let private_key = match key_type {
14384        "rsa" => {
14385            let bits = options
14386                .get("modulusLength")
14387                .and_then(Value::as_u64)
14388                .unwrap_or(2048) as u32;
14389            let exponent = options
14390                .get("publicExponent")
14391                .map(|value| javascript_crypto_u32_from_bridge_value(value, "rsa publicExponent"))
14392                .transpose()?
14393                .unwrap_or(65_537);
14394            let exponent = BigNum::from_u32(exponent).map_err(javascript_crypto_openssl_error)?;
14395            let rsa =
14396                Rsa::generate_with_e(bits, &exponent).map_err(javascript_crypto_openssl_error)?;
14397            PKey::from_rsa(rsa).map_err(javascript_crypto_openssl_error)?
14398        }
14399        "ec" => {
14400            let named_curve = options
14401                .get("namedCurve")
14402                .and_then(Value::as_str)
14403                .ok_or_else(|| {
14404                    SidecarError::InvalidState(String::from(
14405                        "crypto.generateKeyPairSync ec requires namedCurve",
14406                    ))
14407                })?;
14408            let group = EcGroup::from_curve_name(javascript_crypto_curve_nid(named_curve)?)
14409                .map_err(javascript_crypto_openssl_error)?;
14410            let key = EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?;
14411            PKey::from_ec_key(key).map_err(javascript_crypto_openssl_error)?
14412        }
14413        "ed25519" => PKey::generate_ed25519().map_err(javascript_crypto_openssl_error)?,
14414        "x25519" => PKey::generate_x25519().map_err(javascript_crypto_openssl_error)?,
14415        other => {
14416            return Err(SidecarError::InvalidState(format!(
14417                "unsupported crypto key pair type {other}"
14418            )));
14419        }
14420    };
14421    let public_key = PKey::public_key_from_pem(
14422        &private_key
14423            .public_key_to_pem()
14424            .map_err(javascript_crypto_openssl_error)?,
14425    )
14426    .map_err(javascript_crypto_openssl_error)?;
14427    let response = if public_encoding.is_some() || private_encoding.is_some() {
14428        json!({
14429            "publicKey": javascript_crypto_serialize_encoded_key_value_public(&public_key, public_encoding.as_ref())?,
14430            "privateKey": javascript_crypto_serialize_encoded_key_value_private(&private_key, private_encoding.as_ref())?,
14431        })
14432    } else {
14433        json!({
14434            "publicKey": javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Public(public_key))?,
14435            "privateKey": javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Private(private_key))?,
14436        })
14437    };
14438    Ok(Value::String(serde_json::to_string(&response).map_err(
14439        |error| SidecarError::InvalidState(format!("serialize generated key pair: {error}")),
14440    )?))
14441}
14442
14443fn service_javascript_crypto_generate_key_sync_rpc(
14444    request: &JavascriptSyncRpcRequest,
14445) -> Result<Value, SidecarError> {
14446    let key_type = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.generateKeySync type")?;
14447    let options = javascript_crypto_parse_serialized_options_arg(
14448        &request.args,
14449        1,
14450        "crypto.generateKeySync options",
14451    )?
14452    .unwrap_or(Value::Object(Map::new()));
14453    let bit_length = options
14454        .get("length")
14455        .and_then(Value::as_u64)
14456        .ok_or_else(|| {
14457            SidecarError::InvalidState(String::from(
14458                "crypto.generateKeySync options.length is required",
14459            ))
14460        })? as usize;
14461    let mut raw = vec![0_u8; bit_length.div_ceil(8)];
14462    rand_bytes(&mut raw).map_err(javascript_crypto_openssl_error)?;
14463    let serialized = match key_type {
14464        "hmac" => javascript_crypto_serialize_sandbox_key_object(
14465            &JavascriptCryptoKeyMaterial::Secret(raw),
14466        )?,
14467        "aes" => javascript_crypto_serialize_sandbox_key_object(
14468            &JavascriptCryptoKeyMaterial::Secret(raw),
14469        )?,
14470        other => {
14471            return Err(SidecarError::InvalidState(format!(
14472                "unsupported crypto.generateKeySync type {other}"
14473            )));
14474        }
14475    };
14476    Ok(Value::String(serde_json::to_string(&serialized).map_err(
14477        |error| SidecarError::InvalidState(format!("serialize generated key: {error}")),
14478    )?))
14479}
14480
14481fn service_javascript_crypto_generate_prime_sync_rpc(
14482    request: &JavascriptSyncRpcRequest,
14483) -> Result<Value, SidecarError> {
14484    let bits =
14485        javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.generatePrimeSync size")? as i32;
14486    let options = javascript_crypto_parse_serialized_options_arg(
14487        &request.args,
14488        1,
14489        "crypto.generatePrimeSync options",
14490    )?
14491    .unwrap_or(Value::Object(Map::new()));
14492    let safe = options
14493        .get("safe")
14494        .and_then(Value::as_bool)
14495        .unwrap_or(false);
14496    let add = options
14497        .get("add")
14498        .map(|value| javascript_crypto_bignum_from_bridge_value(value, "prime add"))
14499        .transpose()?;
14500    let rem = options
14501        .get("rem")
14502        .map(|value| javascript_crypto_bignum_from_bridge_value(value, "prime rem"))
14503        .transpose()?;
14504    let mut prime = BigNum::new().map_err(javascript_crypto_openssl_error)?;
14505    prime
14506        .generate_prime(bits, safe, add.as_deref(), rem.as_deref())
14507        .map_err(javascript_crypto_openssl_error)?;
14508    let payload = if options
14509        .get("bigint")
14510        .and_then(Value::as_bool)
14511        .unwrap_or(false)
14512    {
14513        json!({
14514            "__type": "bigint",
14515            "value": prime.to_dec_str().map_err(javascript_crypto_openssl_error)?.to_string(),
14516        })
14517    } else {
14518        json!({
14519            "__type": "buffer",
14520            "value": base64::engine::general_purpose::STANDARD.encode(prime.to_vec()),
14521        })
14522    };
14523    Ok(Value::String(serde_json::to_string(&payload).map_err(
14524        |error| SidecarError::InvalidState(format!("serialize generated prime: {error}")),
14525    )?))
14526}
14527
14528fn service_javascript_crypto_diffie_hellman_sync_rpc(
14529    request: &JavascriptSyncRpcRequest,
14530) -> Result<Value, SidecarError> {
14531    let options = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.diffieHellman options")?;
14532    let parsed: Value = serde_json::from_str(options).map_err(|error| {
14533        SidecarError::InvalidState(format!(
14534            "crypto.diffieHellman options must be valid JSON: {error}"
14535        ))
14536    })?;
14537    let private_key = javascript_crypto_parse_key_material_value(
14538        parsed.get("privateKey").ok_or_else(|| {
14539            SidecarError::InvalidState(String::from("crypto.diffieHellman missing privateKey"))
14540        })?,
14541        Some("private"),
14542        "crypto.diffieHellman privateKey",
14543    )?;
14544    let public_key = javascript_crypto_parse_key_material_value(
14545        parsed.get("publicKey").ok_or_else(|| {
14546            SidecarError::InvalidState(String::from("crypto.diffieHellman missing publicKey"))
14547        })?,
14548        Some("public"),
14549        "crypto.diffieHellman publicKey",
14550    )?;
14551    let private_key =
14552        javascript_crypto_expect_private_key(private_key, "crypto.diffieHellman privateKey")?;
14553    let public_key =
14554        javascript_crypto_expect_public_key(public_key, "crypto.diffieHellman publicKey")?;
14555    let mut deriver = Deriver::new(&private_key).map_err(javascript_crypto_openssl_error)?;
14556    deriver
14557        .set_peer(&public_key)
14558        .map_err(javascript_crypto_openssl_error)?;
14559    let secret = deriver
14560        .derive_to_vec()
14561        .map_err(javascript_crypto_openssl_error)?;
14562    Ok(Value::String(
14563        serde_json::to_string(&json!({
14564            "__type": "buffer",
14565            "value": base64::engine::general_purpose::STANDARD.encode(secret),
14566        }))
14567        .map_err(|error| {
14568            SidecarError::InvalidState(format!("serialize derived secret: {error}"))
14569        })?,
14570    ))
14571}
14572
14573fn service_javascript_crypto_diffie_hellman_group_sync_rpc(
14574    request: &JavascriptSyncRpcRequest,
14575) -> Result<Value, SidecarError> {
14576    let name = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.diffieHellmanGroup name")?;
14577    let params = javascript_crypto_named_dh_group(name)?;
14578    let response = json!({
14579        "prime": {
14580            "__type": "buffer",
14581            "value": base64::engine::general_purpose::STANDARD.encode(params.prime_p().to_vec()),
14582        },
14583        "generator": {
14584            "__type": "buffer",
14585            "value": base64::engine::general_purpose::STANDARD.encode(params.generator().to_vec()),
14586        },
14587    });
14588    Ok(Value::String(serde_json::to_string(&response).map_err(
14589        |error| {
14590            SidecarError::InvalidState(format!("serialize diffieHellmanGroup response: {error}"))
14591        },
14592    )?))
14593}
14594
14595fn service_javascript_crypto_diffie_hellman_session_create_sync_rpc(
14596    process: &mut ActiveProcess,
14597    request: &JavascriptSyncRpcRequest,
14598) -> Result<Value, SidecarError> {
14599    ensure_per_process_state_handle_capacity(
14600        process.diffie_hellman_sessions.len(),
14601        "diffie-hellman session",
14602    )?;
14603    let raw = javascript_sync_rpc_arg_str(
14604        &request.args,
14605        0,
14606        "crypto.diffieHellmanSessionCreate request",
14607    )?;
14608    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14609        SidecarError::InvalidState(format!(
14610            "crypto.diffieHellmanSessionCreate request must be valid JSON: {error}"
14611        ))
14612    })?;
14613    let session = match parsed.get("type").and_then(Value::as_str) {
14614        Some("group") => {
14615            let name = parsed.get("name").and_then(Value::as_str).ok_or_else(|| {
14616                SidecarError::InvalidState(String::from(
14617                    "crypto.diffieHellmanSessionCreate group requires name",
14618                ))
14619            })?;
14620            ActiveDiffieHellmanSession::Dh(ActiveDhSession {
14621                params: javascript_crypto_named_dh_group(name)?,
14622                key_pair: None,
14623            })
14624        }
14625        Some("dh") => {
14626            let args = parsed
14627                .get("args")
14628                .and_then(Value::as_array)
14629                .ok_or_else(|| {
14630                    SidecarError::InvalidState(String::from(
14631                        "crypto.diffieHellmanSessionCreate dh requires args",
14632                    ))
14633                })?;
14634            let params = javascript_crypto_build_dh_params(args)?;
14635            ActiveDiffieHellmanSession::Dh(ActiveDhSession {
14636                params,
14637                key_pair: None,
14638            })
14639        }
14640        Some("ecdh") => {
14641            let curve = parsed.get("name").and_then(Value::as_str).ok_or_else(|| {
14642                SidecarError::InvalidState(String::from(
14643                    "crypto.diffieHellmanSessionCreate ecdh requires name",
14644                ))
14645            })?;
14646            ActiveDiffieHellmanSession::Ecdh(ActiveEcdhSession {
14647                curve: curve.to_string(),
14648                key_pair: None,
14649            })
14650        }
14651        other => {
14652            return Err(SidecarError::InvalidState(format!(
14653                "Unsupported Diffie-Hellman session type: {}",
14654                other.unwrap_or("<missing>")
14655            )));
14656        }
14657    };
14658    process.next_diffie_hellman_session_id += 1;
14659    let session_id = process.next_diffie_hellman_session_id;
14660    process.diffie_hellman_sessions.insert(session_id, session);
14661    Ok(json!(session_id))
14662}
14663
14664fn service_javascript_crypto_diffie_hellman_session_call_sync_rpc(
14665    process: &mut ActiveProcess,
14666    request: &JavascriptSyncRpcRequest,
14667) -> Result<Value, SidecarError> {
14668    let session_id = javascript_sync_rpc_arg_u64(
14669        &request.args,
14670        0,
14671        "crypto.diffieHellmanSessionCall session id",
14672    )?;
14673    let raw =
14674        javascript_sync_rpc_arg_str(&request.args, 1, "crypto.diffieHellmanSessionCall request")?;
14675    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14676        SidecarError::InvalidState(format!(
14677            "crypto.diffieHellmanSessionCall request must be valid JSON: {error}"
14678        ))
14679    })?;
14680    let method = parsed
14681        .get("method")
14682        .and_then(Value::as_str)
14683        .ok_or_else(|| {
14684            SidecarError::InvalidState(String::from(
14685                "crypto.diffieHellmanSessionCall request missing method",
14686            ))
14687        })?;
14688    let args = parsed
14689        .get("args")
14690        .and_then(Value::as_array)
14691        .cloned()
14692        .unwrap_or_default();
14693    let session = process
14694        .diffie_hellman_sessions
14695        .get_mut(&session_id)
14696        .ok_or_else(|| {
14697            SidecarError::InvalidState(format!("Diffie-Hellman session {session_id} not found"))
14698        })?;
14699    let (result, has_result) = match session {
14700        ActiveDiffieHellmanSession::Dh(session) => {
14701            javascript_crypto_call_dh_session(session, method, &args)?
14702        }
14703        ActiveDiffieHellmanSession::Ecdh(session) => {
14704            javascript_crypto_call_ecdh_session(session, method, &args)?
14705        }
14706    };
14707    Ok(Value::String(
14708        serde_json::to_string(&json!({
14709            "result": result,
14710            "hasResult": has_result,
14711        }))
14712        .map_err(|error| {
14713            SidecarError::InvalidState(format!("serialize diffie session result: {error}"))
14714        })?,
14715    ))
14716}
14717
14718fn service_javascript_crypto_diffie_hellman_session_destroy_sync_rpc(
14719    process: &mut ActiveProcess,
14720    request: &JavascriptSyncRpcRequest,
14721) -> Result<Value, SidecarError> {
14722    let session_id = javascript_sync_rpc_arg_u64(
14723        &request.args,
14724        0,
14725        "crypto.diffieHellmanSessionDestroy session id",
14726    )?;
14727    process
14728        .diffie_hellman_sessions
14729        .remove(&session_id)
14730        .ok_or_else(|| {
14731            SidecarError::InvalidState(format!("Diffie-Hellman session {session_id} not found"))
14732        })?;
14733    Ok(Value::Null)
14734}
14735
14736fn service_javascript_crypto_subtle_sync_rpc(
14737    request: &JavascriptSyncRpcRequest,
14738) -> Result<Value, SidecarError> {
14739    let raw = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.subtle request")?;
14740    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14741        SidecarError::InvalidState(format!("crypto.subtle request must be valid JSON: {error}"))
14742    })?;
14743    let op = parsed.get("op").and_then(Value::as_str).ok_or_else(|| {
14744        SidecarError::InvalidState(String::from("crypto.subtle request missing op"))
14745    })?;
14746    match op {
14747        "digest" => {
14748            let algorithm = parsed
14749                .get("algorithm")
14750                .and_then(Value::as_str)
14751                .ok_or_else(|| {
14752                    SidecarError::InvalidState(String::from(
14753                        "crypto.subtle.digest missing algorithm",
14754                    ))
14755                })?;
14756            let data = parsed.get("data").and_then(Value::as_str).ok_or_else(|| {
14757                SidecarError::InvalidState(String::from("crypto.subtle.digest missing data"))
14758            })?;
14759            let bytes = base64::engine::general_purpose::STANDARD
14760                .decode(data)
14761                .map_err(|error| {
14762                    SidecarError::InvalidState(format!("crypto.subtle.digest data base64: {error}"))
14763                })?;
14764            let digest = JavascriptCryptoDigestAlgorithm::parse(algorithm)?.digest(&bytes);
14765            Ok(Value::String(
14766                serde_json::to_string(&json!({
14767                    "data": base64::engine::general_purpose::STANDARD.encode(digest),
14768                }))
14769                .map_err(|error| {
14770                    SidecarError::InvalidState(format!("serialize crypto.subtle digest: {error}"))
14771                })?,
14772            ))
14773        }
14774        "generateKey" => {
14775            let algorithm = parsed.get("algorithm").ok_or_else(|| {
14776                SidecarError::InvalidState(String::from(
14777                    "crypto.subtle.generateKey missing algorithm",
14778                ))
14779            })?;
14780            let name =
14781                javascript_crypto_subtle_algorithm_name(algorithm, "crypto.subtle.generateKey")?;
14782            if !matches!(name, "AES-GCM" | "AES-CBC" | "AES-CTR" | "AES-KW") {
14783                return Err(SidecarError::InvalidState(format!(
14784                    "Unsupported key algorithm: {name}"
14785                )));
14786            }
14787            let length_bits = algorithm
14788                .get("length")
14789                .and_then(Value::as_u64)
14790                .ok_or_else(|| {
14791                    SidecarError::InvalidState(String::from(
14792                        "crypto.subtle.generateKey AES algorithm requires length",
14793                    ))
14794                })?;
14795            if length_bits % 8 != 0 {
14796                return Err(SidecarError::InvalidState(String::from(
14797                    "crypto.subtle.generateKey length must be byte-aligned",
14798                )));
14799            }
14800            let length_bytes = usize::try_from(length_bits / 8).map_err(|_| {
14801                SidecarError::InvalidState(String::from(
14802                    "crypto.subtle.generateKey length is too large",
14803                ))
14804            })?;
14805            let mut raw = vec![0_u8; length_bytes];
14806            rand_bytes(&mut raw).map_err(javascript_crypto_openssl_error)?;
14807            let key = javascript_crypto_serialize_subtle_secret_key(
14808                &raw,
14809                javascript_crypto_normalize_subtle_secret_algorithm(algorithm.clone(), &raw)?,
14810                parsed
14811                    .get("extractable")
14812                    .and_then(Value::as_bool)
14813                    .unwrap_or(false),
14814                parsed.get("usages").cloned().unwrap_or_else(|| json!([])),
14815            )?;
14816            Ok(Value::String(
14817                serde_json::to_string(&json!({ "key": key })).map_err(|error| {
14818                    SidecarError::InvalidState(format!(
14819                        "serialize crypto.subtle generated key: {error}"
14820                    ))
14821                })?,
14822            ))
14823        }
14824        "importKey" => {
14825            let format = parsed
14826                .get("format")
14827                .and_then(Value::as_str)
14828                .ok_or_else(|| {
14829                    SidecarError::InvalidState(String::from(
14830                        "crypto.subtle.importKey missing format",
14831                    ))
14832                })?;
14833            if format != "raw" {
14834                return Err(SidecarError::InvalidState(format!(
14835                    "Unsupported import format: {format}"
14836                )));
14837            }
14838            let key_data = parsed
14839                .get("keyData")
14840                .and_then(Value::as_str)
14841                .ok_or_else(|| {
14842                    SidecarError::InvalidState(String::from(
14843                        "crypto.subtle.importKey missing keyData",
14844                    ))
14845                })?;
14846            let raw = base64::engine::general_purpose::STANDARD
14847                .decode(key_data)
14848                .map_err(|error| {
14849                    SidecarError::InvalidState(format!(
14850                        "crypto.subtle.importKey keyData base64: {error}"
14851                    ))
14852                })?;
14853            let algorithm = parsed.get("algorithm").ok_or_else(|| {
14854                SidecarError::InvalidState(String::from(
14855                    "crypto.subtle.importKey missing algorithm",
14856                ))
14857            })?;
14858            let key = javascript_crypto_serialize_subtle_secret_key(
14859                &raw,
14860                javascript_crypto_normalize_subtle_secret_algorithm(algorithm.clone(), &raw)?,
14861                parsed
14862                    .get("extractable")
14863                    .and_then(Value::as_bool)
14864                    .unwrap_or(false),
14865                parsed.get("usages").cloned().unwrap_or_else(|| json!([])),
14866            )?;
14867            Ok(Value::String(
14868                serde_json::to_string(&json!({ "key": key })).map_err(|error| {
14869                    SidecarError::InvalidState(format!(
14870                        "serialize crypto.subtle imported key: {error}"
14871                    ))
14872                })?,
14873            ))
14874        }
14875        "exportKey" => {
14876            let format = parsed
14877                .get("format")
14878                .and_then(Value::as_str)
14879                .ok_or_else(|| {
14880                    SidecarError::InvalidState(String::from(
14881                        "crypto.subtle.exportKey missing format",
14882                    ))
14883                })?;
14884            if format != "raw" {
14885                return Err(SidecarError::InvalidState(format!(
14886                    "Unsupported export format: {format}"
14887                )));
14888            }
14889            let raw = javascript_crypto_subtle_key_raw(
14890                parsed.get("key").ok_or_else(|| {
14891                    SidecarError::InvalidState(String::from("crypto.subtle.exportKey missing key"))
14892                })?,
14893                "crypto.subtle.exportKey key",
14894            )?;
14895            Ok(Value::String(
14896                serde_json::to_string(&json!({
14897                    "data": base64::engine::general_purpose::STANDARD.encode(raw),
14898                }))
14899                .map_err(|error| {
14900                    SidecarError::InvalidState(format!("serialize crypto.subtle export: {error}"))
14901                })?,
14902            ))
14903        }
14904        "encrypt" | "decrypt" => service_javascript_crypto_subtle_aes_crypt_sync_rpc(op, &parsed),
14905        _ => Err(SidecarError::InvalidState(format!(
14906            "Unsupported subtle operation: {op}"
14907        ))),
14908    }
14909}
14910
14911fn javascript_crypto_subtle_algorithm_name<'a>(
14912    algorithm: &'a Value,
14913    label: &str,
14914) -> Result<&'a str, SidecarError> {
14915    if let Some(name) = algorithm.as_str() {
14916        return Ok(name);
14917    }
14918    algorithm
14919        .get("name")
14920        .and_then(Value::as_str)
14921        .ok_or_else(|| SidecarError::InvalidState(format!("{label} algorithm missing name")))
14922}
14923
14924fn javascript_crypto_normalize_subtle_secret_algorithm(
14925    algorithm: Value,
14926    raw: &[u8],
14927) -> Result<Value, SidecarError> {
14928    let mut object = match algorithm {
14929        Value::String(name) => {
14930            let mut object = Map::new();
14931            object.insert(String::from("name"), Value::String(name));
14932            object
14933        }
14934        Value::Object(object) => object,
14935        _ => {
14936            return Err(SidecarError::InvalidState(String::from(
14937                "crypto.subtle secret algorithm must be a string or object",
14938            )));
14939        }
14940    };
14941    let name = object
14942        .get("name")
14943        .and_then(Value::as_str)
14944        .ok_or_else(|| {
14945            SidecarError::InvalidState(String::from("crypto.subtle secret algorithm missing name"))
14946        })?
14947        .to_string();
14948    if matches!(name.as_str(), "AES-GCM" | "AES-CBC" | "AES-CTR" | "AES-KW")
14949        && !object.contains_key("length")
14950    {
14951        object.insert(String::from("length"), json!(raw.len() * 8));
14952    }
14953    Ok(Value::Object(object))
14954}
14955
14956fn javascript_crypto_serialize_subtle_secret_key(
14957    raw: &[u8],
14958    algorithm: Value,
14959    extractable: bool,
14960    usages: Value,
14961) -> Result<Value, SidecarError> {
14962    let raw_base64 = base64::engine::general_purpose::STANDARD.encode(raw);
14963    let source_key_object_data = javascript_crypto_serialize_sandbox_key_object(
14964        &JavascriptCryptoKeyMaterial::Secret(raw.to_vec()),
14965    )?;
14966    Ok(json!({
14967        "type": "secret",
14968        "algorithm": algorithm,
14969        "extractable": extractable,
14970        "usages": usages,
14971        "_raw": raw_base64,
14972        "_sourceKeyObjectData": source_key_object_data,
14973    }))
14974}
14975
14976fn javascript_crypto_subtle_key_raw(key: &Value, label: &str) -> Result<Vec<u8>, SidecarError> {
14977    let raw = key.get("_raw").and_then(Value::as_str).ok_or_else(|| {
14978        SidecarError::InvalidState(format!("{label} must be a raw secret CryptoKey"))
14979    })?;
14980    base64::engine::general_purpose::STANDARD
14981        .decode(raw)
14982        .map_err(|error| SidecarError::InvalidState(format!("{label} raw base64: {error}")))
14983}
14984
14985fn service_javascript_crypto_subtle_aes_crypt_sync_rpc(
14986    op: &str,
14987    parsed: &Value,
14988) -> Result<Value, SidecarError> {
14989    let algorithm = parsed.get("algorithm").ok_or_else(|| {
14990        SidecarError::InvalidState(format!("crypto.subtle.{op} missing algorithm"))
14991    })?;
14992    let name = javascript_crypto_subtle_algorithm_name(algorithm, &format!("crypto.subtle.{op}"))?;
14993    if name != "AES-GCM" {
14994        return Err(SidecarError::InvalidState(format!(
14995            "Unsupported subtle AES operation algorithm: {name}"
14996        )));
14997    }
14998    let key = javascript_crypto_subtle_key_raw(
14999        parsed
15000            .get("key")
15001            .ok_or_else(|| SidecarError::InvalidState(format!("crypto.subtle.{op} missing key")))?,
15002        &format!("crypto.subtle.{op} key"),
15003    )?;
15004    let iv = algorithm.get("iv").and_then(Value::as_str).ok_or_else(|| {
15005        SidecarError::InvalidState(format!("crypto.subtle.{op} AES-GCM missing iv"))
15006    })?;
15007    let iv = base64::engine::general_purpose::STANDARD
15008        .decode(iv)
15009        .map_err(|error| {
15010            SidecarError::InvalidState(format!("crypto.subtle.{op} iv base64: {error}"))
15011        })?;
15012    let data = parsed
15013        .get("data")
15014        .and_then(Value::as_str)
15015        .ok_or_else(|| SidecarError::InvalidState(format!("crypto.subtle.{op} missing data")))?;
15016    let mut data = base64::engine::general_purpose::STANDARD
15017        .decode(data)
15018        .map_err(|error| {
15019            SidecarError::InvalidState(format!("crypto.subtle.{op} data base64: {error}"))
15020        })?;
15021    let tag_len = javascript_crypto_subtle_aes_gcm_tag_len(algorithm)?;
15022    let mut options = Map::new();
15023    options.insert(String::from("authTagLength"), json!(tag_len));
15024    if let Some(additional_data) = algorithm.get("additionalData").and_then(Value::as_str) {
15025        options.insert(
15026            String::from("aad"),
15027            Value::String(additional_data.to_string()),
15028        );
15029    }
15030    let decrypt = op == "decrypt";
15031    if decrypt {
15032        if data.len() < tag_len {
15033            return Err(SidecarError::InvalidState(String::from(
15034                "crypto.subtle.decrypt AES-GCM data shorter than auth tag",
15035            )));
15036        }
15037        let auth_tag = data.split_off(data.len() - tag_len);
15038        options.insert(
15039            String::from("authTag"),
15040            Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
15041        );
15042    }
15043    let cipher_name = format!("aes-{}-gcm", key.len() * 8);
15044    let mut context = javascript_crypto_build_cipher_context(
15045        &cipher_name,
15046        &key,
15047        Some(&iv),
15048        decrypt,
15049        Some(&Value::Object(options)),
15050    )?;
15051    let mut output = javascript_crypto_cipher_update(&mut context, &data)?;
15052    output.extend(javascript_crypto_cipher_finalize(&mut context)?);
15053    if !decrypt {
15054        let mut auth_tag = vec![0_u8; tag_len];
15055        context
15056            .get_tag(&mut auth_tag)
15057            .map_err(javascript_crypto_openssl_error)?;
15058        output.extend(auth_tag);
15059    }
15060    Ok(Value::String(
15061        serde_json::to_string(&json!({
15062            "data": base64::engine::general_purpose::STANDARD.encode(output),
15063        }))
15064        .map_err(|error| {
15065            SidecarError::InvalidState(format!("serialize crypto.subtle {op}: {error}"))
15066        })?,
15067    ))
15068}
15069
15070fn javascript_crypto_subtle_aes_gcm_tag_len(algorithm: &Value) -> Result<usize, SidecarError> {
15071    let tag_bits = algorithm
15072        .get("tagLength")
15073        .and_then(Value::as_u64)
15074        .unwrap_or(128);
15075    if !tag_bits.is_multiple_of(8) {
15076        return Err(SidecarError::InvalidState(String::from(
15077            "crypto.subtle AES-GCM tagLength must be byte-aligned",
15078        )));
15079    }
15080    usize::try_from(tag_bits / 8).map_err(|_| {
15081        SidecarError::InvalidState(String::from("crypto.subtle AES-GCM tagLength too large"))
15082    })
15083}
15084
15085fn service_javascript_crypto_cipheriv_inner(
15086    request: &JavascriptSyncRpcRequest,
15087    decrypt: bool,
15088) -> Result<Value, SidecarError> {
15089    let label = if decrypt {
15090        "crypto.decipheriv"
15091    } else {
15092        "crypto.cipheriv"
15093    };
15094    let algorithm = javascript_sync_rpc_arg_str(&request.args, 0, &format!("{label} algorithm"))?;
15095    let key = javascript_sync_rpc_base64_arg(&request.args, 1, &format!("{label} key"))?;
15096    let iv = javascript_sync_rpc_base64_arg_optional(&request.args, 2, &format!("{label} iv"))?;
15097    let data = javascript_sync_rpc_base64_arg(&request.args, 3, &format!("{label} data"))?;
15098    let options =
15099        javascript_sync_rpc_json_arg_optional(&request.args, 4, &format!("{label} options"))?;
15100    let auth_tag_len = javascript_crypto_requested_aead_tag_len(algorithm, options.as_ref())?;
15101    let mut context = javascript_crypto_build_cipher_context(
15102        algorithm,
15103        &key,
15104        iv.as_deref(),
15105        decrypt,
15106        options.as_ref(),
15107    )?;
15108    let payload = javascript_crypto_cipher_update(&mut context, &data)?;
15109    let final_bytes = javascript_crypto_cipher_finalize(&mut context)?;
15110    if decrypt {
15111        let mut output = payload;
15112        output.extend(final_bytes);
15113        return Ok(Value::String(
15114            base64::engine::general_purpose::STANDARD.encode(output),
15115        ));
15116    }
15117
15118    let mut response = Map::new();
15119    let mut encrypted = payload;
15120    encrypted.extend(final_bytes);
15121    response.insert(
15122        String::from("data"),
15123        Value::String(base64::engine::general_purpose::STANDARD.encode(encrypted)),
15124    );
15125    if javascript_crypto_is_aead(algorithm) {
15126        let mut auth_tag = vec![0_u8; auth_tag_len];
15127        context
15128            .get_tag(&mut auth_tag)
15129            .map_err(javascript_crypto_openssl_error)?;
15130        response.insert(
15131            String::from("authTag"),
15132            Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
15133        );
15134    }
15135    Ok(Value::String(serde_json::to_string(&response).map_err(
15136        |error| SidecarError::InvalidState(format!("serialize {label} response: {error}")),
15137    )?))
15138}
15139
15140fn javascript_sync_rpc_base64_arg_optional(
15141    args: &[Value],
15142    index: usize,
15143    label: &str,
15144) -> Result<Option<Vec<u8>>, SidecarError> {
15145    if args.get(index).is_none() || args[index].is_null() {
15146        return Ok(None);
15147    }
15148    javascript_sync_rpc_base64_arg(args, index, label).map(Some)
15149}
15150
15151fn javascript_sync_rpc_json_arg_optional(
15152    args: &[Value],
15153    index: usize,
15154    label: &str,
15155) -> Result<Option<Value>, SidecarError> {
15156    if args.get(index).is_none() || args[index].is_null() {
15157        return Ok(None);
15158    }
15159    let raw = javascript_sync_rpc_arg_str(args, index, label)?;
15160    serde_json::from_str(raw)
15161        .map(Some)
15162        .map_err(|error| SidecarError::InvalidState(format!("{label} must be valid JSON: {error}")))
15163}
15164
15165fn javascript_crypto_parse_direct_key_input(
15166    raw: &str,
15167    expected: Option<&str>,
15168    label: &str,
15169) -> Result<JavascriptDirectKeyInput, SidecarError> {
15170    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
15171        SidecarError::InvalidState(format!("{label} must be valid JSON: {error}"))
15172    })?;
15173    let padding = match parsed.as_object().and_then(|value| value.get("padding")) {
15174        Some(value) => javascript_crypto_padding_from_value(value)?,
15175        None => None,
15176    };
15177    Ok(JavascriptDirectKeyInput {
15178        key: javascript_crypto_parse_key_material_value(&parsed, expected, label)?,
15179        padding,
15180    })
15181}
15182
15183fn javascript_crypto_parse_key_material_value(
15184    value: &Value,
15185    expected: Option<&str>,
15186    label: &str,
15187) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15188    if let Some(object) = value.as_object() {
15189        if object.get("__type").and_then(Value::as_str) == Some("keyObject") {
15190            let serialized = object.get("value").ok_or_else(|| {
15191                SidecarError::InvalidState(format!("{label} keyObject is missing a value"))
15192            })?;
15193            return javascript_crypto_parse_serialized_key_object(serialized, expected, label);
15194        }
15195        if object.contains_key("type") && (object.contains_key("pem") || object.contains_key("raw"))
15196        {
15197            return javascript_crypto_parse_serialized_key_object(value, expected, label);
15198        }
15199        if let Some(source) = object.get("key") {
15200            return javascript_crypto_parse_key_source(
15201                source,
15202                object.get("format").and_then(Value::as_str),
15203                object.get("type").and_then(Value::as_str),
15204                expected,
15205                label,
15206            );
15207        }
15208    }
15209    javascript_crypto_parse_key_source(value, None, None, expected, label)
15210}
15211
15212fn javascript_crypto_parse_key_source(
15213    source: &Value,
15214    format: Option<&str>,
15215    kind: Option<&str>,
15216    expected: Option<&str>,
15217    label: &str,
15218) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15219    match source {
15220        Value::String(pem) => javascript_crypto_parse_key_from_pem(pem.as_bytes(), expected, label),
15221        Value::Object(object) if object.get("__type").and_then(Value::as_str) == Some("buffer") => {
15222            let data = javascript_crypto_decode_bridge_buffer(source, label)?;
15223            javascript_crypto_parse_key_from_bytes(&data, format, kind, expected, label)
15224        }
15225        Value::Object(_) => {
15226            if format == Some("jwk") {
15227                return Err(SidecarError::InvalidState(format!(
15228                    "{label} jwk inputs are not supported yet"
15229                )));
15230            }
15231            Err(SidecarError::InvalidState(format!(
15232                "{label} has an unsupported key shape"
15233            )))
15234        }
15235        _ => Err(SidecarError::InvalidState(format!(
15236            "{label} has an unsupported key value"
15237        ))),
15238    }
15239}
15240
15241fn javascript_crypto_parse_key_from_pem(
15242    pem: &[u8],
15243    expected: Option<&str>,
15244    label: &str,
15245) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15246    match expected {
15247        Some("private") => PKey::private_key_from_pem(pem)
15248            .map(JavascriptCryptoKeyMaterial::Private)
15249            .map_err(|error| {
15250                SidecarError::InvalidState(format!("{label} private key is invalid: {error}"))
15251            }),
15252        Some("public") => PKey::public_key_from_pem(pem)
15253            .map(JavascriptCryptoKeyMaterial::Public)
15254            .map_err(|error| {
15255                SidecarError::InvalidState(format!("{label} public key is invalid: {error}"))
15256            }),
15257        _ => PKey::private_key_from_pem(pem)
15258            .map(JavascriptCryptoKeyMaterial::Private)
15259            .or_else(|_| PKey::public_key_from_pem(pem).map(JavascriptCryptoKeyMaterial::Public))
15260            .map_err(|error| {
15261                SidecarError::InvalidState(format!("{label} PEM key is invalid: {error}"))
15262            }),
15263    }
15264}
15265
15266fn javascript_crypto_parse_key_from_bytes(
15267    der: &[u8],
15268    format: Option<&str>,
15269    kind: Option<&str>,
15270    expected: Option<&str>,
15271    label: &str,
15272) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15273    match (format.unwrap_or("der"), kind.or(expected)) {
15274        ("der", Some("pkcs8")) | ("der", Some("private")) => PKey::private_key_from_der(der)
15275            .map(JavascriptCryptoKeyMaterial::Private)
15276            .map_err(|error| {
15277                SidecarError::InvalidState(format!("{label} private key DER is invalid: {error}"))
15278            }),
15279        ("der", Some("spki")) | ("der", Some("public")) => PKey::public_key_from_der(der)
15280            .map(JavascriptCryptoKeyMaterial::Public)
15281            .map_err(|error| {
15282                SidecarError::InvalidState(format!("{label} public key DER is invalid: {error}"))
15283            }),
15284        _ => Err(SidecarError::InvalidState(format!(
15285            "{label} unsupported key bytes format"
15286        ))),
15287    }
15288}
15289
15290fn javascript_crypto_parse_serialized_key_object(
15291    value: &Value,
15292    expected: Option<&str>,
15293    label: &str,
15294) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15295    let serialized: JavascriptSerializedSandboxKeyObject = serde_json::from_value(value.clone())
15296        .map_err(|error| {
15297            SidecarError::InvalidState(format!("{label} keyObject is invalid: {error}"))
15298        })?;
15299    match serialized.kind.as_str() {
15300        "secret" => {
15301            if expected == Some("public") || expected == Some("private") {
15302                return Err(SidecarError::InvalidState(format!(
15303                    "{label} expected an asymmetric key"
15304                )));
15305            }
15306            Ok(JavascriptCryptoKeyMaterial::Secret(
15307                base64::engine::general_purpose::STANDARD
15308                    .decode(serialized.raw.unwrap_or_default())
15309                    .map_err(|error| {
15310                        SidecarError::InvalidState(format!(
15311                            "{label} secret key contains invalid base64: {error}"
15312                        ))
15313                    })?,
15314            ))
15315        }
15316        "private" => {
15317            let pem = serialized.pem.ok_or_else(|| {
15318                SidecarError::InvalidState(format!("{label} private keyObject is missing pem"))
15319            })?;
15320            javascript_crypto_parse_key_from_pem(pem.as_bytes(), Some("private"), label)
15321        }
15322        "public" => {
15323            let pem = serialized.pem.ok_or_else(|| {
15324                SidecarError::InvalidState(format!("{label} public keyObject is missing pem"))
15325            })?;
15326            javascript_crypto_parse_key_from_pem(pem.as_bytes(), Some("public"), label)
15327        }
15328        other => Err(SidecarError::InvalidState(format!(
15329            "{label} has unsupported keyObject type {other}"
15330        ))),
15331    }
15332}
15333
15334fn javascript_crypto_expect_private_key(
15335    key: JavascriptCryptoKeyMaterial,
15336    label: &str,
15337) -> Result<PKey<Private>, SidecarError> {
15338    match key {
15339        JavascriptCryptoKeyMaterial::Private(key) => Ok(key),
15340        _ => Err(SidecarError::InvalidState(format!(
15341            "{label} requires a private key"
15342        ))),
15343    }
15344}
15345
15346fn javascript_crypto_expect_public_key(
15347    key: JavascriptCryptoKeyMaterial,
15348    label: &str,
15349) -> Result<PKey<Public>, SidecarError> {
15350    match key {
15351        JavascriptCryptoKeyMaterial::Public(key) => Ok(key),
15352        JavascriptCryptoKeyMaterial::Private(key) => {
15353            let pem = key
15354                .public_key_to_pem()
15355                .map_err(javascript_crypto_openssl_error)?;
15356            PKey::public_key_from_pem(&pem).map_err(javascript_crypto_openssl_error)
15357        }
15358        _ => Err(SidecarError::InvalidState(format!(
15359            "{label} requires a public key"
15360        ))),
15361    }
15362}
15363
15364fn javascript_crypto_new_signer<'a>(
15365    algorithm: Option<&'a str>,
15366    key: &'a PKey<Private>,
15367) -> Result<Signer<'a>, SidecarError> {
15368    if matches!(key.id(), PKeyId::ED25519 | PKeyId::ED448) || algorithm.is_none() {
15369        return Signer::new_without_digest(key).map_err(javascript_crypto_openssl_error);
15370    }
15371    Signer::new(
15372        javascript_crypto_message_digest_from_name(algorithm.ok_or_else(|| {
15373            SidecarError::InvalidState(String::from("crypto.sign requires a digest algorithm"))
15374        })?)?,
15375        key,
15376    )
15377    .map_err(javascript_crypto_openssl_error)
15378}
15379
15380fn javascript_crypto_new_verifier<'a>(
15381    algorithm: Option<&'a str>,
15382    key: &'a PKey<Public>,
15383) -> Result<Verifier<'a>, SidecarError> {
15384    if matches!(key.id(), PKeyId::ED25519 | PKeyId::ED448) || algorithm.is_none() {
15385        return Verifier::new_without_digest(key).map_err(javascript_crypto_openssl_error);
15386    }
15387    Verifier::new(
15388        javascript_crypto_message_digest_from_name(algorithm.ok_or_else(|| {
15389            SidecarError::InvalidState(String::from("crypto.verify requires a digest algorithm"))
15390        })?)?,
15391        key,
15392    )
15393    .map_err(javascript_crypto_openssl_error)
15394}
15395
15396fn javascript_crypto_message_digest_from_name(name: &str) -> Result<MessageDigest, SidecarError> {
15397    match name.trim().to_ascii_lowercase().replace('-', "").as_str() {
15398        "md5" => Ok(MessageDigest::md5()),
15399        "sha1" => Ok(MessageDigest::sha1()),
15400        "sha256" => Ok(MessageDigest::sha256()),
15401        "sha384" => Ok(MessageDigest::sha384()),
15402        "sha512" => Ok(MessageDigest::sha512()),
15403        other => Err(SidecarError::InvalidState(format!(
15404            "unsupported crypto digest algorithm {other}"
15405        ))),
15406    }
15407}
15408
15409fn javascript_crypto_padding_from_value(value: &Value) -> Result<Option<Padding>, SidecarError> {
15410    let Some(number) = value.as_i64() else {
15411        return Ok(None);
15412    };
15413    let padding = match number {
15414        1 => Padding::PKCS1,
15415        3 => Padding::NONE,
15416        4 => Padding::PKCS1_OAEP,
15417        6 => Padding::PKCS1_PSS,
15418        other => {
15419            return Err(SidecarError::InvalidState(format!(
15420                "unsupported RSA padding constant {other}"
15421            )));
15422        }
15423    };
15424    Ok(Some(padding))
15425}
15426
15427fn javascript_crypto_decode_bridge_buffer(
15428    value: &Value,
15429    label: &str,
15430) -> Result<Vec<u8>, SidecarError> {
15431    let base64_value = value
15432        .as_object()
15433        .filter(|object| object.get("__type").and_then(Value::as_str) == Some("buffer"))
15434        .and_then(|object| object.get("value"))
15435        .and_then(Value::as_str)
15436        .ok_or_else(|| {
15437            SidecarError::InvalidState(format!("{label} must be a serialized bridge buffer"))
15438        })?;
15439    base64::engine::general_purpose::STANDARD
15440        .decode(base64_value)
15441        .map_err(|error| {
15442            SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
15443        })
15444}
15445
15446fn javascript_crypto_serialize_sandbox_key_object(
15447    key: &JavascriptCryptoKeyMaterial,
15448) -> Result<Value, SidecarError> {
15449    let serialized = match key {
15450        JavascriptCryptoKeyMaterial::Private(key) => JavascriptSerializedSandboxKeyObject {
15451            kind: String::from("private"),
15452            pem: Some(
15453                String::from_utf8(
15454                    key.private_key_to_pem_pkcs8()
15455                        .map_err(javascript_crypto_openssl_error)?,
15456                )
15457                .map_err(|error| {
15458                    SidecarError::InvalidState(format!("private key PEM is not utf8: {error}"))
15459                })?,
15460            ),
15461            raw: None,
15462            asymmetric_key_type: javascript_crypto_pkey_type_name(key.id()),
15463            asymmetric_key_details: None,
15464            jwk: None,
15465        },
15466        JavascriptCryptoKeyMaterial::Public(key) => JavascriptSerializedSandboxKeyObject {
15467            kind: String::from("public"),
15468            pem: Some(
15469                String::from_utf8(
15470                    key.public_key_to_pem()
15471                        .map_err(javascript_crypto_openssl_error)?,
15472                )
15473                .map_err(|error| {
15474                    SidecarError::InvalidState(format!("public key PEM is not utf8: {error}"))
15475                })?,
15476            ),
15477            raw: None,
15478            asymmetric_key_type: javascript_crypto_pkey_type_name(key.id()),
15479            asymmetric_key_details: None,
15480            jwk: None,
15481        },
15482        JavascriptCryptoKeyMaterial::Secret(raw) => JavascriptSerializedSandboxKeyObject {
15483            kind: String::from("secret"),
15484            pem: None,
15485            raw: Some(base64::engine::general_purpose::STANDARD.encode(raw)),
15486            asymmetric_key_type: None,
15487            asymmetric_key_details: None,
15488            jwk: None,
15489        },
15490    };
15491    serde_json::to_value(serialized)
15492        .map_err(|error| SidecarError::InvalidState(format!("serialize key object: {error}")))
15493}
15494
15495fn javascript_crypto_pkey_type_name(id: PKeyId) -> Option<String> {
15496    match id {
15497        PKeyId::RSA => Some(String::from("rsa")),
15498        PKeyId::EC => Some(String::from("ec")),
15499        PKeyId::ED25519 => Some(String::from("ed25519")),
15500        PKeyId::ED448 => Some(String::from("ed448")),
15501        PKeyId::X25519 => Some(String::from("x25519")),
15502        PKeyId::X448 => Some(String::from("x448")),
15503        PKeyId::DH => Some(String::from("dh")),
15504        _ => None,
15505    }
15506}
15507
15508fn javascript_crypto_rsa_output_size(
15509    key: &JavascriptCryptoKeyMaterial,
15510) -> Result<usize, SidecarError> {
15511    match key {
15512        JavascriptCryptoKeyMaterial::Private(key) => key
15513            .rsa()
15514            .map(|rsa| rsa.size() as usize)
15515            .map_err(javascript_crypto_openssl_error),
15516        JavascriptCryptoKeyMaterial::Public(key) => key
15517            .rsa()
15518            .map(|rsa| rsa.size() as usize)
15519            .map_err(javascript_crypto_openssl_error),
15520        JavascriptCryptoKeyMaterial::Secret(_) => Err(SidecarError::InvalidState(String::from(
15521            "RSA operations require an asymmetric key",
15522        ))),
15523    }
15524}
15525
15526fn javascript_crypto_parse_serialized_options_arg(
15527    args: &[Value],
15528    index: usize,
15529    label: &str,
15530) -> Result<Option<Value>, SidecarError> {
15531    let Some(raw) = args.get(index).and_then(Value::as_str) else {
15532        return Ok(None);
15533    };
15534    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
15535        SidecarError::InvalidState(format!("{label} must be valid JSON: {error}"))
15536    })?;
15537    if parsed.get("hasOptions").and_then(Value::as_bool) == Some(true) {
15538        Ok(parsed.get("options").cloned())
15539    } else {
15540        Ok(None)
15541    }
15542}
15543
15544fn javascript_crypto_u32_from_bridge_value(
15545    value: &Value,
15546    label: &str,
15547) -> Result<u32, SidecarError> {
15548    if let Some(number) = value.as_u64() {
15549        return u32::try_from(number)
15550            .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")));
15551    }
15552    let bytes = javascript_crypto_decode_bridge_buffer(value, label)?;
15553    if bytes.len() > 4 {
15554        return Err(SidecarError::InvalidState(format!(
15555            "{label} buffer is too large for u32"
15556        )));
15557    }
15558    Ok(bytes
15559        .into_iter()
15560        .fold(0_u32, |acc, byte| (acc << 8) | u32::from(byte)))
15561}
15562
15563fn javascript_crypto_bignum_from_bridge_value(
15564    value: &Value,
15565    label: &str,
15566) -> Result<BigNum, SidecarError> {
15567    if let Some(object) = value.as_object() {
15568        if object.get("__type").and_then(Value::as_str) == Some("bigint") {
15569            let decimal = object.get("value").and_then(Value::as_str).ok_or_else(|| {
15570                SidecarError::InvalidState(format!("{label} bigint is missing a value"))
15571            })?;
15572            return BigNum::from_dec_str(decimal).map_err(javascript_crypto_openssl_error);
15573        }
15574    }
15575    let bytes = javascript_crypto_decode_bridge_buffer(value, label)?;
15576    BigNum::from_slice(&bytes).map_err(javascript_crypto_openssl_error)
15577}
15578
15579fn javascript_crypto_curve_nid(name: &str) -> Result<Nid, SidecarError> {
15580    match name {
15581        "prime256v1" | "P-256" => Ok(Nid::X9_62_PRIME256V1),
15582        "secp384r1" | "P-384" => Ok(Nid::SECP384R1),
15583        "secp521r1" | "P-521" => Ok(Nid::SECP521R1),
15584        "secp256k1" => Ok(Nid::SECP256K1),
15585        other => Err(SidecarError::InvalidState(format!(
15586            "unsupported EC curve {other}"
15587        ))),
15588    }
15589}
15590
15591fn javascript_crypto_named_dh_group(name: &str) -> Result<Dh<Params>, SidecarError> {
15592    match name {
15593        "modp2" => Dh::get_1024_160().map_err(javascript_crypto_openssl_error),
15594        "modp14" | "modp15" | "modp16" | "modp17" | "modp18" => {
15595            Dh::get_2048_256().map_err(javascript_crypto_openssl_error)
15596        }
15597        other => Err(SidecarError::InvalidState(format!(
15598            "unsupported Diffie-Hellman group {other}"
15599        ))),
15600    }
15601}
15602
15603fn javascript_crypto_clone_dh_params(params: &Dh<Params>) -> Result<Dh<Params>, SidecarError> {
15604    Dh::from_pqg(
15605        params
15606            .prime_p()
15607            .to_owned()
15608            .map_err(javascript_crypto_openssl_error)?,
15609        params
15610            .prime_q()
15611            .map(|value| value.to_owned().map_err(javascript_crypto_openssl_error))
15612            .transpose()?,
15613        params
15614            .generator()
15615            .to_owned()
15616            .map_err(javascript_crypto_openssl_error)?,
15617    )
15618    .map_err(javascript_crypto_openssl_error)
15619}
15620
15621fn javascript_crypto_build_dh_params(args: &[Value]) -> Result<Dh<Params>, SidecarError> {
15622    let Some(first) = args.first() else {
15623        return Err(SidecarError::InvalidState(String::from(
15624            "Diffie-Hellman session args are required",
15625        )));
15626    };
15627    if let Some(bits) = first.as_u64() {
15628        let generator = args
15629            .get(1)
15630            .map(|value| javascript_crypto_u32_from_bridge_value(value, "Diffie-Hellman generator"))
15631            .transpose()?
15632            .unwrap_or(2);
15633        return Dh::generate_params(bits as u32, generator)
15634            .map_err(javascript_crypto_openssl_error);
15635    }
15636    let prime = javascript_crypto_bignum_from_bridge_value(first, "Diffie-Hellman prime")?;
15637    let generator = args
15638        .get(1)
15639        .map(|value| javascript_crypto_bignum_from_bridge_value(value, "Diffie-Hellman generator"))
15640        .transpose()?
15641        .unwrap_or(BigNum::from_u32(2).map_err(javascript_crypto_openssl_error)?);
15642    Dh::from_pqg(prime, None, generator).map_err(javascript_crypto_openssl_error)
15643}
15644
15645fn javascript_crypto_call_dh_session(
15646    session: &mut ActiveDhSession,
15647    method: &str,
15648    args: &[Value],
15649) -> Result<(Value, bool), SidecarError> {
15650    match method {
15651        "verifyError" => Ok((Value::Null, false)),
15652        "generateKeys" => {
15653            if session.key_pair.is_none() {
15654                session.key_pair = Some(
15655                    javascript_crypto_clone_dh_params(&session.params)?
15656                        .generate_key()
15657                        .map_err(javascript_crypto_openssl_error)?,
15658                );
15659            }
15660            let public = session
15661                .key_pair
15662                .as_ref()
15663                .expect("dh key pair")
15664                .public_key()
15665                .to_vec();
15666            Ok((javascript_crypto_bridge_buffer_value(&public), true))
15667        }
15668        "computeSecret" => {
15669            if session.key_pair.is_none() {
15670                session.key_pair = Some(
15671                    javascript_crypto_clone_dh_params(&session.params)?
15672                        .generate_key()
15673                        .map_err(javascript_crypto_openssl_error)?,
15674                );
15675            }
15676            let peer = javascript_crypto_bignum_from_bridge_value(
15677                args.first().ok_or_else(|| {
15678                    SidecarError::InvalidState(String::from(
15679                        "computeSecret requires peer public key",
15680                    ))
15681                })?,
15682                "Diffie-Hellman peer public key",
15683            )?;
15684            let secret = session
15685                .key_pair
15686                .as_ref()
15687                .expect("dh key pair")
15688                .compute_key(&peer)
15689                .map_err(javascript_crypto_openssl_error)?;
15690            Ok((javascript_crypto_bridge_buffer_value(&secret), true))
15691        }
15692        "getPrime" => Ok((
15693            javascript_crypto_bridge_buffer_value(&session.params.prime_p().to_vec()),
15694            true,
15695        )),
15696        "getGenerator" => Ok((
15697            javascript_crypto_bridge_buffer_value(&session.params.generator().to_vec()),
15698            true,
15699        )),
15700        "getPublicKey" => {
15701            if session.key_pair.is_none() {
15702                session.key_pair = Some(
15703                    javascript_crypto_clone_dh_params(&session.params)?
15704                        .generate_key()
15705                        .map_err(javascript_crypto_openssl_error)?,
15706                );
15707            }
15708            Ok((
15709                javascript_crypto_bridge_buffer_value(
15710                    &session
15711                        .key_pair
15712                        .as_ref()
15713                        .expect("dh key pair")
15714                        .public_key()
15715                        .to_vec(),
15716                ),
15717                true,
15718            ))
15719        }
15720        "getPrivateKey" => {
15721            if session.key_pair.is_none() {
15722                session.key_pair = Some(
15723                    javascript_crypto_clone_dh_params(&session.params)?
15724                        .generate_key()
15725                        .map_err(javascript_crypto_openssl_error)?,
15726                );
15727            }
15728            Ok((
15729                javascript_crypto_bridge_buffer_value(
15730                    &session
15731                        .key_pair
15732                        .as_ref()
15733                        .expect("dh key pair")
15734                        .private_key()
15735                        .to_vec(),
15736                ),
15737                true,
15738            ))
15739        }
15740        other => Err(SidecarError::InvalidState(format!(
15741            "Unsupported Diffie-Hellman method: {other}"
15742        ))),
15743    }
15744}
15745
15746fn javascript_crypto_call_ecdh_session(
15747    session: &mut ActiveEcdhSession,
15748    method: &str,
15749    args: &[Value],
15750) -> Result<(Value, bool), SidecarError> {
15751    let nid = javascript_crypto_curve_nid(&session.curve)?;
15752    let group = EcGroup::from_curve_name(nid).map_err(javascript_crypto_openssl_error)?;
15753    match method {
15754        "verifyError" => Ok((Value::Null, false)),
15755        "generateKeys" => {
15756            if session.key_pair.is_none() {
15757                session.key_pair =
15758                    Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15759            }
15760            let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
15761            let bytes = session
15762                .key_pair
15763                .as_ref()
15764                .expect("ecdh key pair")
15765                .public_key()
15766                .to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx)
15767                .map_err(javascript_crypto_openssl_error)?;
15768            Ok((javascript_crypto_bridge_buffer_value(&bytes), true))
15769        }
15770        "computeSecret" => {
15771            if session.key_pair.is_none() {
15772                session.key_pair =
15773                    Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15774            }
15775            let peer_bytes = javascript_crypto_decode_bridge_buffer(
15776                args.first().ok_or_else(|| {
15777                    SidecarError::InvalidState(String::from(
15778                        "computeSecret requires peer public key",
15779                    ))
15780                })?,
15781                "ECDH peer public key",
15782            )?;
15783            let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
15784            let peer_point = EcPoint::from_bytes(&group, &peer_bytes, &mut ctx)
15785                .map_err(javascript_crypto_openssl_error)?;
15786            let peer_key = EcKey::from_public_key(&group, &peer_point)
15787                .map_err(javascript_crypto_openssl_error)?;
15788            let private =
15789                PKey::from_ec_key(session.key_pair.as_ref().expect("ecdh key pair").to_owned())
15790                    .map_err(javascript_crypto_openssl_error)?;
15791            let peer = PKey::from_ec_key(peer_key).map_err(javascript_crypto_openssl_error)?;
15792            let mut deriver = Deriver::new(&private).map_err(javascript_crypto_openssl_error)?;
15793            deriver
15794                .set_peer(&peer)
15795                .map_err(javascript_crypto_openssl_error)?;
15796            let secret = deriver
15797                .derive_to_vec()
15798                .map_err(javascript_crypto_openssl_error)?;
15799            Ok((javascript_crypto_bridge_buffer_value(&secret), true))
15800        }
15801        "getPublicKey" => {
15802            if session.key_pair.is_none() {
15803                session.key_pair =
15804                    Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15805            }
15806            let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
15807            let bytes = session
15808                .key_pair
15809                .as_ref()
15810                .expect("ecdh key pair")
15811                .public_key()
15812                .to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx)
15813                .map_err(javascript_crypto_openssl_error)?;
15814            Ok((javascript_crypto_bridge_buffer_value(&bytes), true))
15815        }
15816        "getPrivateKey" => {
15817            if session.key_pair.is_none() {
15818                session.key_pair =
15819                    Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15820            }
15821            Ok((
15822                javascript_crypto_bridge_buffer_value(
15823                    &session
15824                        .key_pair
15825                        .as_ref()
15826                        .expect("ecdh key pair")
15827                        .private_key()
15828                        .to_vec(),
15829                ),
15830                true,
15831            ))
15832        }
15833        other => Err(SidecarError::InvalidState(format!(
15834            "Unsupported Diffie-Hellman method: {other}"
15835        ))),
15836    }
15837}
15838
15839fn javascript_crypto_serialize_encoded_key_value_public(
15840    key: &PKey<Public>,
15841    encoding: Option<&Value>,
15842) -> Result<Value, SidecarError> {
15843    if let Some(encoding) = encoding {
15844        let format = encoding
15845            .get("format")
15846            .and_then(Value::as_str)
15847            .unwrap_or("pem");
15848        return Ok(match format {
15849            "der" => json!({
15850                "kind": "buffer",
15851                "value": base64::engine::general_purpose::STANDARD
15852                    .encode(key.public_key_to_der().map_err(javascript_crypto_openssl_error)?),
15853            }),
15854            _ => json!({
15855                "kind": "string",
15856                "value": String::from_utf8(
15857                    key.public_key_to_pem().map_err(javascript_crypto_openssl_error)?,
15858                )
15859                .map_err(|error| SidecarError::InvalidState(format!("public key PEM utf8: {error}")))?,
15860            }),
15861        });
15862    }
15863    javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Public(
15864        key.to_owned(),
15865    ))
15866}
15867
15868fn javascript_crypto_serialize_encoded_key_value_private(
15869    key: &PKey<Private>,
15870    encoding: Option<&Value>,
15871) -> Result<Value, SidecarError> {
15872    if let Some(encoding) = encoding {
15873        let format = encoding
15874            .get("format")
15875            .and_then(Value::as_str)
15876            .unwrap_or("pem");
15877        return Ok(match format {
15878            "der" => json!({
15879                "kind": "buffer",
15880                "value": base64::engine::general_purpose::STANDARD
15881                    .encode(key.private_key_to_der().map_err(javascript_crypto_openssl_error)?),
15882            }),
15883            _ => json!({
15884                "kind": "string",
15885                "value": String::from_utf8(
15886                    key.private_key_to_pem_pkcs8().map_err(javascript_crypto_openssl_error)?,
15887                )
15888                .map_err(|error| SidecarError::InvalidState(format!("private key PEM utf8: {error}")))?,
15889            }),
15890        });
15891    }
15892    javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Private(
15893        key.to_owned(),
15894    ))
15895}
15896
15897fn javascript_crypto_bridge_buffer_value(bytes: &[u8]) -> Value {
15898    json!({
15899        "__type": "buffer",
15900        "value": base64::engine::general_purpose::STANDARD.encode(bytes),
15901    })
15902}
15903
15904fn javascript_crypto_build_cipher_context(
15905    algorithm: &str,
15906    key: &[u8],
15907    iv: Option<&[u8]>,
15908    decrypt: bool,
15909    options: Option<&Value>,
15910) -> Result<Crypter, SidecarError> {
15911    let cipher = javascript_crypto_cipher_from_name(algorithm)?;
15912    let mode = if decrypt {
15913        Mode::Decrypt
15914    } else {
15915        Mode::Encrypt
15916    };
15917    let mut context =
15918        Crypter::new(cipher, mode, key, iv).map_err(javascript_crypto_openssl_error)?;
15919    if let Some(auto_padding) = options
15920        .and_then(|value| value.get("autoPadding"))
15921        .and_then(Value::as_bool)
15922    {
15923        context.pad(auto_padding);
15924    }
15925    if javascript_crypto_is_aead(algorithm) {
15926        if let Some(aad) = options
15927            .and_then(|value| value.get("aad"))
15928            .and_then(Value::as_str)
15929        {
15930            context
15931                .aad_update(
15932                    &base64::engine::general_purpose::STANDARD
15933                        .decode(aad)
15934                        .map_err(|error| {
15935                            SidecarError::InvalidState(format!(
15936                                "cipher aad contains invalid base64: {error}"
15937                            ))
15938                        })?,
15939                )
15940                .map_err(javascript_crypto_openssl_error)?;
15941        }
15942        if decrypt {
15943            if let Some(auth_tag) = options
15944                .and_then(|value| value.get("authTag"))
15945                .and_then(Value::as_str)
15946            {
15947                let decoded = base64::engine::general_purpose::STANDARD
15948                    .decode(auth_tag)
15949                    .map_err(|error| {
15950                        SidecarError::InvalidState(format!(
15951                            "cipher authTag contains invalid base64: {error}"
15952                        ))
15953                    })?;
15954                context
15955                    .set_tag(&decoded)
15956                    .map_err(javascript_crypto_openssl_error)?;
15957            }
15958        }
15959    }
15960    Ok(context)
15961}
15962
15963fn javascript_crypto_requested_aead_tag_len(
15964    algorithm: &str,
15965    options: Option<&Value>,
15966) -> Result<usize, SidecarError> {
15967    if !javascript_crypto_is_aead(algorithm) {
15968        return Ok(0);
15969    }
15970    let requested = options
15971        .and_then(|value| value.get("authTagLength"))
15972        .and_then(Value::as_u64)
15973        .unwrap_or(javascript_crypto_aead_tag_len(algorithm) as u64);
15974    usize::try_from(requested).map_err(|_| {
15975        SidecarError::InvalidState(String::from("cipher authTagLength must fit within usize"))
15976    })
15977}
15978
15979fn javascript_crypto_cipher_update(
15980    context: &mut Crypter,
15981    data: &[u8],
15982) -> Result<Vec<u8>, SidecarError> {
15983    let mut output = vec![0_u8; data.len() + 32];
15984    let written = context
15985        .update(data, &mut output)
15986        .map_err(javascript_crypto_openssl_error)?;
15987    output.truncate(written);
15988    Ok(output)
15989}
15990
15991fn javascript_crypto_cipher_finalize(context: &mut Crypter) -> Result<Vec<u8>, SidecarError> {
15992    let mut output = vec![0_u8; 32];
15993    let written = context
15994        .finalize(&mut output)
15995        .map_err(javascript_crypto_openssl_error)?;
15996    output.truncate(written);
15997    Ok(output)
15998}
15999
16000fn javascript_crypto_cipher_from_name(name: &str) -> Result<Cipher, SidecarError> {
16001    match name.to_ascii_lowercase().as_str() {
16002        "aes-128-cbc" => Ok(Cipher::aes_128_cbc()),
16003        "aes-192-cbc" => Ok(Cipher::aes_192_cbc()),
16004        "aes-256-cbc" => Ok(Cipher::aes_256_cbc()),
16005        "aes-128-ctr" => Ok(Cipher::aes_128_ctr()),
16006        "aes-192-ctr" => Ok(Cipher::aes_192_ctr()),
16007        "aes-256-ctr" => Ok(Cipher::aes_256_ctr()),
16008        "aes-128-gcm" => Ok(Cipher::aes_128_gcm()),
16009        "aes-192-gcm" => Ok(Cipher::aes_192_gcm()),
16010        "aes-256-gcm" => Ok(Cipher::aes_256_gcm()),
16011        other => Err(SidecarError::InvalidState(format!(
16012            "unsupported crypto cipher algorithm {other}"
16013        ))),
16014    }
16015}
16016
16017fn javascript_crypto_is_aead(algorithm: &str) -> bool {
16018    algorithm.to_ascii_lowercase().ends_with("-gcm")
16019}
16020
16021fn javascript_crypto_aead_tag_len(_algorithm: &str) -> usize {
16022    16
16023}
16024
16025fn javascript_crypto_openssl_error(error: openssl::error::ErrorStack) -> SidecarError {
16026    SidecarError::Execution(format!("crypto operation failed: {error}"))
16027}
16028
16029fn service_javascript_kernel_stdin_sync_rpc(
16030    kernel: &mut SidecarKernel,
16031    process: &mut ActiveProcess,
16032    request: &JavascriptSyncRpcRequest,
16033) -> Result<Value, SidecarError> {
16034    let max_bytes =
16035        javascript_sync_rpc_arg_u64_optional(&request.args, 0, "__kernel_stdin_read max bytes")?
16036            .map(|value| value.clamp(1, DEFAULT_KERNEL_STDIN_READ_MAX_BYTES as u64) as usize)
16037            .unwrap_or(DEFAULT_KERNEL_STDIN_READ_MAX_BYTES);
16038    let timeout_ms =
16039        javascript_sync_rpc_arg_u64_optional(&request.args, 1, "__kernel_stdin_read timeout ms")?
16040            .unwrap_or(DEFAULT_KERNEL_STDIN_READ_TIMEOUT_MS);
16041
16042    match kernel
16043        .fd_read_with_timeout_result(
16044            EXECUTION_DRIVER_NAME,
16045            process.kernel_pid,
16046            0,
16047            max_bytes,
16048            Some(Duration::from_millis(timeout_ms)),
16049        )
16050        .map_err(kernel_error)
16051    {
16052        Ok(Some(chunk)) if !chunk.is_empty() => Ok(json!({
16053            "dataBase64": base64::engine::general_purpose::STANDARD.encode(chunk),
16054        })),
16055        Ok(Some(_)) => Ok(Value::Null),
16056        Ok(None) => Ok(json!({
16057            "done": true,
16058        })),
16059        Err(SidecarError::Kernel(error)) if error.starts_with("EAGAIN:") => Ok(Value::Null),
16060        Err(error) => Err(error),
16061    }
16062}
16063
16064fn service_javascript_pty_set_raw_mode_sync_rpc(
16065    kernel: &mut SidecarKernel,
16066    process: &mut ActiveProcess,
16067    request: &JavascriptSyncRpcRequest,
16068) -> Result<Value, SidecarError> {
16069    let enabled = javascript_sync_rpc_arg_bool(&request.args, 0, "__pty_set_raw_mode enabled")?;
16070    kernel
16071        .pty_set_discipline(
16072            EXECUTION_DRIVER_NAME,
16073            process.kernel_pid,
16074            0,
16075            LineDisciplineConfig {
16076                canonical: Some(!enabled),
16077                echo: Some(!enabled),
16078                isig: Some(!enabled),
16079            },
16080        )
16081        .map_err(kernel_error)?;
16082    Ok(Value::Null)
16083}
16084
16085fn service_javascript_kernel_stdio_write_sync_rpc(
16086    kernel: &mut SidecarKernel,
16087    process: &mut ActiveProcess,
16088    request: &JavascriptSyncRpcRequest,
16089) -> Result<Value, SidecarError> {
16090    let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_stdio_write fd")?;
16091    let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "__kernel_stdio_write chunk")?;
16092
16093    let written = match fd {
16094        1 => kernel
16095            .write_process_stdout(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
16096            .map_err(kernel_error)?,
16097        2 => kernel
16098            .write_process_stderr(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
16099            .map_err(kernel_error)?,
16100        other => {
16101            return Err(SidecarError::InvalidState(format!(
16102                "__kernel_stdio_write only supports fd 1/2, got {other}"
16103            )));
16104        }
16105    };
16106
16107    let event = if fd == 1 {
16108        ActiveExecutionEvent::Stdout(chunk)
16109    } else {
16110        ActiveExecutionEvent::Stderr(chunk)
16111    };
16112    process.queue_pending_execution_event(event)?;
16113
16114    Ok(json!(written))
16115}
16116
16117fn service_javascript_kernel_poll_sync_rpc(
16118    kernel: &mut SidecarKernel,
16119    process: &ActiveProcess,
16120    request: &JavascriptSyncRpcRequest,
16121) -> Result<Value, SidecarError> {
16122    let fd_requests: Vec<KernelPollFdRequest> = serde_json::from_value(
16123        request
16124            .args
16125            .first()
16126            .cloned()
16127            .unwrap_or_else(|| Value::Array(Vec::new())),
16128    )
16129    .map_err(|error| {
16130        SidecarError::InvalidState(format!(
16131            "__kernel_poll fd list must be a JSON array of {{ fd, events }} objects: {error}"
16132        ))
16133    })?;
16134    let timeout_ms =
16135        javascript_sync_rpc_arg_u64_optional(&request.args, 1, "__kernel_poll timeout ms")?
16136            .unwrap_or_default();
16137    let timeout_ms = i32::try_from(timeout_ms).map_err(|_| {
16138        SidecarError::InvalidState(String::from("__kernel_poll timeout ms must fit within i32"))
16139    })?;
16140
16141    let poll_fds = fd_requests
16142        .iter()
16143        .map(|entry| PollFd {
16144            fd: entry.fd,
16145            events: PollEvents::from_bits(entry.events),
16146            revents: PollEvents::empty(),
16147        })
16148        .collect::<Vec<_>>();
16149    let result = kernel
16150        .poll_fds(
16151            EXECUTION_DRIVER_NAME,
16152            process.kernel_pid,
16153            poll_fds,
16154            timeout_ms,
16155        )
16156        .map_err(kernel_error)?;
16157
16158    Ok(json!({
16159        "readyCount": result.ready_count,
16160        "fds": result
16161            .fds
16162            .into_iter()
16163            .map(|entry| KernelPollFdResponse {
16164                fd: entry.fd,
16165                events: entry.events.bits(),
16166                revents: entry.revents.bits(),
16167            })
16168            .collect::<Vec<_>>(),
16169    }))
16170}
16171
16172fn install_kernel_stdin_pipe(kernel: &mut SidecarKernel, pid: u32) -> Result<u32, SidecarError> {
16173    let (read_fd, write_fd) = kernel
16174        .open_pipe(EXECUTION_DRIVER_NAME, pid)
16175        .map_err(kernel_error)?;
16176    kernel
16177        .fd_dup2(EXECUTION_DRIVER_NAME, pid, read_fd, 0)
16178        .map_err(kernel_error)?;
16179    kernel
16180        .fd_close(EXECUTION_DRIVER_NAME, pid, read_fd)
16181        .map_err(kernel_error)?;
16182    Ok(write_fd)
16183}
16184
16185fn javascript_child_process_stdin_mode(request: &JavascriptChildProcessSpawnRequest) -> &str {
16186    request
16187        .options
16188        .stdio
16189        .first()
16190        .map(String::as_str)
16191        .unwrap_or("pipe")
16192}
16193
16194pub(crate) fn write_kernel_process_stdin(
16195    kernel: &mut SidecarKernel,
16196    process: &mut ActiveProcess,
16197    chunk: &[u8],
16198) -> Result<(), SidecarError> {
16199    if process.runtime == GuestRuntimeKind::JavaScript {
16200        return Ok(());
16201    }
16202    let Some(writer_fd) = process.kernel_stdin_writer_fd else {
16203        return Ok(());
16204    };
16205    kernel
16206        .fd_write(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd, chunk)
16207        .map(|_| ())
16208        .map_err(kernel_error)
16209}
16210
16211pub(crate) fn close_kernel_process_stdin(
16212    kernel: &mut SidecarKernel,
16213    process: &mut ActiveProcess,
16214) -> Result<(), SidecarError> {
16215    let Some(writer_fd) = process.kernel_stdin_writer_fd.take() else {
16216        return Ok(());
16217    };
16218    kernel
16219        .fd_close(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd)
16220        .map_err(kernel_error)
16221}
16222
16223fn parse_http_header_collection(
16224    headers: &BTreeMap<String, Value>,
16225    label: &str,
16226) -> Result<HttpHeaderCollection, SidecarError> {
16227    let mut normalized = BTreeMap::<String, Vec<String>>::new();
16228    let mut raw_pairs = Vec::new();
16229
16230    for (raw_name, value) in headers {
16231        let normalized_name = raw_name.to_ascii_lowercase();
16232        let values = match value {
16233            Value::String(text) => vec![text.clone()],
16234            Value::Array(values) => values
16235                .iter()
16236                .map(|entry| {
16237                    entry.as_str().map(str::to_owned).ok_or_else(|| {
16238                        SidecarError::InvalidState(format!(
16239                            "{label} header {raw_name} must contain only strings"
16240                        ))
16241                    })
16242                })
16243                .collect::<Result<Vec<_>, _>>()?,
16244            other => {
16245                return Err(SidecarError::InvalidState(format!(
16246                    "{label} header {raw_name} must be a string or string array, received {other}"
16247                )));
16248            }
16249        };
16250        raw_pairs.extend(
16251            values
16252                .iter()
16253                .cloned()
16254                .map(|entry| (raw_name.clone(), entry)),
16255        );
16256        normalized
16257            .entry(normalized_name)
16258            .or_default()
16259            .extend(values);
16260    }
16261
16262    Ok(HttpHeaderCollection {
16263        normalized,
16264        raw_pairs,
16265    })
16266}
16267
16268fn http_headers_json(headers: &HttpHeaderCollection) -> Value {
16269    let map = headers
16270        .normalized
16271        .iter()
16272        .map(|(name, values)| {
16273            let value = if values.len() == 1 {
16274                Value::String(values[0].clone())
16275            } else {
16276                Value::Array(values.iter().cloned().map(Value::String).collect())
16277            };
16278            (name.clone(), value)
16279        })
16280        .collect::<Map<String, Value>>();
16281    Value::Object(map)
16282}
16283
16284fn http_raw_headers_json(headers: &HttpHeaderCollection) -> Value {
16285    Value::Array(
16286        headers
16287            .raw_pairs
16288            .iter()
16289            .flat_map(|(name, value)| [Value::String(name.clone()), Value::String(value.clone())])
16290            .collect(),
16291    )
16292}
16293
16294fn is_loopback_request_host(host: &str) -> bool {
16295    let bare = host
16296        .strip_prefix('[')
16297        .and_then(|value| value.strip_suffix(']'))
16298        .unwrap_or(host);
16299    matches!(bare, "localhost" | "127.0.0.1" | "::1")
16300}
16301
16302fn serialize_http_loopback_request(
16303    url: &Url,
16304    options: &JavascriptHttpRequestOptions,
16305    headers: &HttpHeaderCollection,
16306) -> Result<String, SidecarError> {
16307    let body_base64 = options
16308        .body
16309        .as_ref()
16310        .map(|body| base64::engine::general_purpose::STANDARD.encode(body.as_bytes()));
16311    serde_json::to_string(&json!({
16312        "method": options.method.clone().unwrap_or_else(|| String::from("GET")),
16313        "url": http_request_target(url),
16314        "headers": http_headers_json(headers),
16315        "rawHeaders": http_raw_headers_json(headers),
16316        "bodyBase64": body_base64,
16317    }))
16318    .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
16319}
16320
16321fn http_request_target(url: &Url) -> String {
16322    let path = if url.path().is_empty() {
16323        "/"
16324    } else {
16325        url.path()
16326    };
16327    format!(
16328        "{path}{}",
16329        url.query()
16330            .map(|query| format!("?{query}"))
16331            .unwrap_or_default()
16332    )
16333}
16334
16335fn find_kernel_http_listener_process(vm: &VmState, port: u16) -> Option<String> {
16336    vm.active_processes
16337        .iter()
16338        .find_map(|(process_id, process)| {
16339            process.tcp_listeners.values().find_map(|listener| {
16340                let socket_id = listener.kernel_socket_id?;
16341                let record = vm.kernel.socket_get(socket_id)?;
16342                let local_addr = record
16343                    .local_address()
16344                    .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
16345                    .unwrap_or_else(|| listener.guest_local_addr());
16346                if local_addr.port() == port && is_vm_local_http_listener_addr(local_addr.ip()) {
16347                    Some(process_id.to_owned())
16348                } else {
16349                    None
16350                }
16351            })
16352        })
16353}
16354
16355fn is_vm_local_http_listener_addr(ip: IpAddr) -> bool {
16356    ip.is_loopback() || ip.is_unspecified()
16357}
16358
16359fn serialize_kernel_http_fetch_request(
16360    port: u16,
16361    path: &str,
16362    options: &JavascriptHttpRequestOptions,
16363    headers: &HttpHeaderCollection,
16364) -> Vec<u8> {
16365    let method = options.method.as_deref().unwrap_or("GET");
16366    let mut lines = vec![format!("{method} {path} HTTP/1.1")];
16367    let mut has_host = false;
16368    let mut has_connection = false;
16369    let mut has_content_length = false;
16370    for (name, values) in &headers.normalized {
16371        match name.as_str() {
16372            "host" => has_host = true,
16373            "connection" => has_connection = true,
16374            "content-length" => has_content_length = true,
16375            _ => {}
16376        }
16377        lines.push(format!("{name}: {}", values.join(", ")));
16378    }
16379    if !has_host {
16380        lines.push(format!("Host: 127.0.0.1:{port}"));
16381    }
16382    if !has_connection {
16383        lines.push(String::from("Connection: close"));
16384    }
16385    let body = options.body.as_deref().unwrap_or("").as_bytes();
16386    if !has_content_length && !body.is_empty() {
16387        lines.push(format!("Content-Length: {}", body.len()));
16388    }
16389    lines.push(String::new());
16390    lines.push(String::new());
16391
16392    let mut request = lines.join("\r\n").into_bytes();
16393    request.extend_from_slice(body);
16394    request
16395}
16396
16397fn parse_kernel_http_fetch_response(
16398    buffer: &[u8],
16399    peer_closed: bool,
16400    url: &str,
16401) -> Result<Option<String>, SidecarError> {
16402    let Some(header_end) = find_http_header_end(buffer) else {
16403        return Ok(None);
16404    };
16405    let header_bytes = &buffer[..header_end];
16406    let head = String::from_utf8_lossy(header_bytes);
16407    let mut lines = head.split("\r\n");
16408    let status_line = lines.next().unwrap_or_default();
16409    let mut status_parts = status_line.splitn(3, ' ');
16410    let version = status_parts.next().unwrap_or_default();
16411    if !version.starts_with("HTTP/") {
16412        return Err(SidecarError::Execution(format!(
16413            "invalid vm.fetch HTTP response status line: {status_line}"
16414        )));
16415    }
16416    let status = status_parts
16417        .next()
16418        .ok_or_else(|| {
16419            SidecarError::Execution(format!(
16420                "invalid vm.fetch HTTP response status line: {status_line}"
16421            ))
16422        })?
16423        .parse::<u16>()
16424        .map_err(|error| {
16425            SidecarError::Execution(format!(
16426                "invalid vm.fetch HTTP response status code in {status_line:?}: {error}"
16427            ))
16428        })?;
16429    let status_text = status_parts.next().unwrap_or_default();
16430    let mut headers = Vec::new();
16431    let mut raw_headers = Vec::new();
16432    let mut content_length = None;
16433    let mut transfer_encoding_values = Vec::new();
16434    for line in lines {
16435        if line.is_empty() {
16436            continue;
16437        }
16438        let Some((name, value)) = line.split_once(':') else {
16439            return Err(SidecarError::Execution(format!(
16440                "invalid vm.fetch HTTP response header line: {line}"
16441            )));
16442        };
16443        let value = value.trim().to_owned();
16444        let normalized = name.to_ascii_lowercase();
16445        if normalized == "content-length" {
16446            content_length = Some(value.parse::<usize>().map_err(|error| {
16447                SidecarError::Execution(format!(
16448                    "invalid vm.fetch Content-Length header {value:?}: {error}"
16449                ))
16450            })?);
16451        } else if normalized == "transfer-encoding" {
16452            transfer_encoding_values.push(value.clone());
16453        }
16454        headers.push(json!([normalized, value.clone()]));
16455        raw_headers.push(Value::String(name.to_owned()));
16456        raw_headers.push(Value::String(value));
16457    }
16458
16459    let body_start = header_end + 4;
16460    let transfer_encoding = transfer_encoding_tokens(&transfer_encoding_values);
16461    let is_chunked = transfer_encoding.iter().any(|token| token == "chunked");
16462    let body = if is_chunked {
16463        if content_length.is_some() {
16464            return Err(SidecarError::Execution(String::from(
16465                "vm.fetch HTTP response cannot include both Transfer-Encoding: chunked and Content-Length",
16466            )));
16467        }
16468        if transfer_encoding.len() != 1 {
16469            return Err(SidecarError::Execution(format!(
16470                "unsupported vm.fetch Transfer-Encoding: {}",
16471                transfer_encoding.join(", ")
16472            )));
16473        }
16474        let Some(decoded) = decode_kernel_http_chunked_body(&buffer[body_start..])? else {
16475            return Ok(None);
16476        };
16477        decoded
16478    } else if !transfer_encoding.is_empty() {
16479        return Err(SidecarError::Execution(format!(
16480            "unsupported vm.fetch Transfer-Encoding: {}",
16481            transfer_encoding.join(", ")
16482        )));
16483    } else if let Some(content_length) = content_length {
16484        let body_end = body_start.saturating_add(content_length);
16485        if buffer.len() < body_end {
16486            return Ok(None);
16487        }
16488        buffer[body_start..body_end].to_vec()
16489    } else if peer_closed {
16490        buffer[body_start..].to_vec()
16491    } else {
16492        return Ok(None);
16493    };
16494
16495    serde_json::to_string(&json!({
16496        "status": status,
16497        "statusText": status_text,
16498        "headers": headers,
16499        "rawHeaders": raw_headers,
16500        "body": base64::engine::general_purpose::STANDARD.encode(&body),
16501        "bodyEncoding": "base64",
16502        "url": url,
16503    }))
16504    .map(Some)
16505    .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
16506}
16507
16508fn find_http_header_end(buffer: &[u8]) -> Option<usize> {
16509    buffer.windows(4).position(|window| window == b"\r\n\r\n")
16510}
16511
16512fn find_crlf(buffer: &[u8], start: usize) -> Option<usize> {
16513    buffer
16514        .get(start..)?
16515        .windows(2)
16516        .position(|window| window == b"\r\n")
16517        .map(|offset| start + offset)
16518}
16519
16520fn transfer_encoding_tokens(values: &[String]) -> Vec<String> {
16521    values
16522        .iter()
16523        .flat_map(|value| value.split(','))
16524        .map(|token| token.trim().to_ascii_lowercase())
16525        .filter(|token| !token.is_empty())
16526        .collect()
16527}
16528
16529fn decode_kernel_http_chunked_body(buffer: &[u8]) -> Result<Option<Vec<u8>>, SidecarError> {
16530    let mut offset = 0;
16531    let mut body = Vec::new();
16532    loop {
16533        let Some(line_end) = find_crlf(buffer, offset) else {
16534            return Ok(None);
16535        };
16536        let size_line = std::str::from_utf8(&buffer[offset..line_end]).map_err(|error| {
16537            SidecarError::Execution(format!(
16538                "invalid vm.fetch chunk size line encoding: {error}"
16539            ))
16540        })?;
16541        let size_part = size_line.split(';').next().unwrap_or_default();
16542        if size_part.is_empty() || !size_part.bytes().all(|byte| byte.is_ascii_hexdigit()) {
16543            return Err(SidecarError::Execution(format!(
16544                "invalid vm.fetch chunk size line: {size_line:?}"
16545            )));
16546        }
16547        let chunk_size = usize::from_str_radix(size_part, 16).map_err(|error| {
16548            SidecarError::Execution(format!(
16549                "invalid vm.fetch chunk size {size_part:?}: {error}"
16550            ))
16551        })?;
16552        let chunk_start = line_end + 2;
16553        let chunk_end = chunk_start
16554            .checked_add(chunk_size)
16555            .ok_or_else(|| SidecarError::Execution(String::from("vm.fetch chunk size overflow")))?;
16556        if chunk_size > 0 {
16557            let chunk_terminator_end = chunk_end.checked_add(2).ok_or_else(|| {
16558                SidecarError::Execution(String::from("vm.fetch chunk terminator overflow"))
16559            })?;
16560            if chunk_terminator_end > buffer.len() {
16561                return Ok(None);
16562            }
16563            if buffer.get(chunk_end..chunk_terminator_end) != Some(b"\r\n") {
16564                return Err(SidecarError::Execution(String::from(
16565                    "invalid vm.fetch chunk terminator",
16566                )));
16567            }
16568            body.extend_from_slice(&buffer[chunk_start..chunk_end]);
16569            offset = chunk_terminator_end;
16570            continue;
16571        }
16572
16573        if buffer.get(chunk_start..chunk_start + 2) == Some(b"\r\n") {
16574            return Ok(Some(body));
16575        }
16576        let Some(trailer_end) = find_http_header_end(&buffer[chunk_start..]) else {
16577            return Ok(None);
16578        };
16579        let trailer_bytes = &buffer[chunk_start..chunk_start + trailer_end];
16580        let trailers = String::from_utf8_lossy(trailer_bytes);
16581        for line in trailers.split("\r\n") {
16582            if line.is_empty() {
16583                continue;
16584            }
16585            if line.starts_with(' ') || line.starts_with('\t') || !line.contains(':') {
16586                return Err(SidecarError::Execution(format!(
16587                    "invalid vm.fetch chunk trailer line: {line}"
16588                )));
16589            }
16590        }
16591        return Ok(Some(body));
16592    }
16593}
16594
16595fn kernel_http_fetch_target_exit_code(error: &SidecarError) -> Option<i32> {
16596    let SidecarError::Execution(message) = error else {
16597        return None;
16598    };
16599    message
16600        .strip_prefix("vm.fetch target exited before responding (exit code ")?
16601        .strip_suffix(')')?
16602        .parse()
16603        .ok()
16604}
16605
16606fn service_host_fetch_target_event<B>(
16607    bridge: &SharedBridge<B>,
16608    vm_id: &str,
16609    dns: &VmDnsConfig,
16610    socket_paths: &JavascriptSocketPathContext,
16611    kernel: &mut SidecarKernel,
16612    process: &mut ActiveProcess,
16613    resource_limits: &ResourceLimits,
16614    wait: Duration,
16615) -> Result<bool, SidecarError>
16616where
16617    B: NativeSidecarBridge + Send + 'static,
16618    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16619{
16620    let Some(event) = process
16621        .execution
16622        .poll_event_blocking(wait)
16623        .map_err(|error| SidecarError::Execution(error.to_string()))?
16624    else {
16625        return Ok(false);
16626    };
16627
16628    match event {
16629        ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
16630            let network_counts = process.network_resource_counts();
16631            let response = service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
16632                bridge,
16633                vm_id,
16634                dns,
16635                socket_paths,
16636                kernel,
16637                process,
16638                sync_request: &request,
16639                resource_limits,
16640                network_counts,
16641            });
16642            match response {
16643                Ok(result) => process
16644                    .execution
16645                    .respond_javascript_sync_rpc_success(request.id, result)
16646                    .or_else(ignore_stale_javascript_sync_rpc_response)?,
16647                Err(error) => process
16648                    .execution
16649                    .respond_javascript_sync_rpc_error(
16650                        request.id,
16651                        javascript_sync_rpc_error_code(&error),
16652                        error.to_string(),
16653                    )
16654                    .or_else(ignore_stale_javascript_sync_rpc_response)?,
16655            }
16656        }
16657        ActiveExecutionEvent::Exited(code) => {
16658            return Err(SidecarError::Execution(format!(
16659                "vm.fetch target exited before responding (exit code {code})"
16660            )));
16661        }
16662        other => {
16663            process.queue_pending_execution_event(other)?;
16664        }
16665    }
16666    Ok(true)
16667}
16668
16669fn drain_host_fetch_target_events<B>(
16670    bridge: &SharedBridge<B>,
16671    vm_id: &str,
16672    vm: &mut VmState,
16673    target_process_id: &str,
16674    socket_paths: &JavascriptSocketPathContext,
16675    resource_limits: &ResourceLimits,
16676) -> Result<(), SidecarError>
16677where
16678    B: NativeSidecarBridge + Send + 'static,
16679    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16680{
16681    for _ in 0..32 {
16682        let dns = vm.dns.clone();
16683        let Some(process) = vm.active_processes.get_mut(target_process_id) else {
16684            break;
16685        };
16686        let serviced = service_host_fetch_target_event(
16687            bridge,
16688            vm_id,
16689            &dns,
16690            socket_paths,
16691            &mut vm.kernel,
16692            process,
16693            resource_limits,
16694            Duration::from_millis(1),
16695        )?;
16696        if !serviced {
16697            break;
16698        }
16699    }
16700    Ok(())
16701}
16702
16703fn dispatch_kernel_http_fetch<B>(
16704    bridge: &SharedBridge<B>,
16705    vm_id: &str,
16706    vm: &mut VmState,
16707    target_process_id: &str,
16708    port: u16,
16709    path: &str,
16710    options: &JavascriptHttpRequestOptions,
16711    headers: &HttpHeaderCollection,
16712    max_fetch_response_bytes: usize,
16713) -> Result<String, SidecarError>
16714where
16715    B: NativeSidecarBridge + Send + 'static,
16716    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16717{
16718    let socket_paths = build_javascript_socket_path_context(vm)?;
16719    let family = JavascriptSocketFamily::Ipv4;
16720    let local_port = allocate_guest_listen_port(
16721        0,
16722        family,
16723        &socket_paths.used_tcp_guest_ports,
16724        socket_paths.listen_policy,
16725    )?;
16726    let resource_limits = vm.kernel.resource_limits().clone();
16727    let network_counts = vm_network_resource_counts(vm);
16728    check_network_resource_limit(
16729        resource_limits.max_sockets,
16730        network_counts.sockets,
16731        2,
16732        "socket",
16733    )?;
16734    check_network_resource_limit(
16735        resource_limits.max_connections,
16736        network_counts.connections,
16737        2,
16738        "connection",
16739    )?;
16740
16741    let kernel_pid = vm
16742        .active_processes
16743        .get(target_process_id)
16744        .ok_or_else(|| {
16745            SidecarError::InvalidState(format!(
16746                "vm.fetch target process disappeared: {target_process_id}"
16747            ))
16748        })?
16749        .kernel_pid;
16750    let socket_id = vm
16751        .kernel
16752        .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, SocketSpec::tcp())
16753        .map_err(kernel_error)?;
16754
16755    let result = dispatch_kernel_http_fetch_with_socket(
16756        bridge,
16757        vm_id,
16758        vm,
16759        target_process_id,
16760        kernel_pid,
16761        socket_id,
16762        local_port,
16763        port,
16764        path,
16765        options,
16766        headers,
16767        &socket_paths,
16768        &resource_limits,
16769        max_fetch_response_bytes,
16770    );
16771    let close_result = vm
16772        .kernel
16773        .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id)
16774        .map_err(kernel_error);
16775    let cleanup_result = if result.is_err() {
16776        drain_host_fetch_target_events(
16777            bridge,
16778            vm_id,
16779            vm,
16780            target_process_id,
16781            &socket_paths,
16782            &resource_limits,
16783        )
16784    } else {
16785        Ok(())
16786    };
16787    match (result, close_result) {
16788        (Ok(response), Ok(())) => cleanup_result.map(|()| response),
16789        (Err(error), _) => Err(error),
16790        (Ok(_), Err(error)) => Err(error),
16791    }
16792}
16793
16794#[allow(clippy::too_many_arguments)]
16795fn dispatch_kernel_http_fetch_with_socket<B>(
16796    bridge: &SharedBridge<B>,
16797    vm_id: &str,
16798    vm: &mut VmState,
16799    target_process_id: &str,
16800    kernel_pid: u32,
16801    socket_id: SocketId,
16802    local_port: u16,
16803    port: u16,
16804    path: &str,
16805    options: &JavascriptHttpRequestOptions,
16806    headers: &HttpHeaderCollection,
16807    socket_paths: &JavascriptSocketPathContext,
16808    resource_limits: &ResourceLimits,
16809    max_fetch_response_bytes: usize,
16810) -> Result<String, SidecarError>
16811where
16812    B: NativeSidecarBridge + Send + 'static,
16813    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16814{
16815    vm.kernel
16816        .socket_bind_inet(
16817            EXECUTION_DRIVER_NAME,
16818            kernel_pid,
16819            socket_id,
16820            InetSocketAddress::new("127.0.0.1", local_port),
16821        )
16822        .map_err(kernel_error)?;
16823    vm.kernel
16824        .socket_connect_inet_loopback(
16825            EXECUTION_DRIVER_NAME,
16826            kernel_pid,
16827            socket_id,
16828            InetSocketAddress::new("127.0.0.1", port),
16829        )
16830        .map_err(kernel_error)?;
16831
16832    let request_bytes = serialize_kernel_http_fetch_request(port, path, options, headers);
16833    vm.kernel
16834        .socket_write(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, &request_bytes)
16835        .map_err(kernel_error)?;
16836
16837    let mut response_buffer = Vec::new();
16838    let mut peer_closed = false;
16839    let url = format!("http://127.0.0.1:{port}{path}");
16840    let deadline = Instant::now() + http_loopback_request_timeout();
16841    loop {
16842        if let Some(response) =
16843            parse_kernel_http_fetch_response(&response_buffer, peer_closed, &url)?
16844        {
16845            ensure_vm_fetch_response_within_limit(&response, "vm.fetch", max_fetch_response_bytes)?;
16846            return Ok(response);
16847        }
16848        if Instant::now() >= deadline {
16849            let preview = String::from_utf8_lossy(&response_buffer);
16850            return Err(SidecarError::Execution(format!(
16851                "vm.fetch timed out waiting for kernel TCP HTTP response ({} buffered bytes: {:?})",
16852                response_buffer.len(),
16853                preview.chars().take(200).collect::<String>()
16854            )));
16855        }
16856
16857        {
16858            let dns = vm.dns.clone();
16859            let process = vm
16860                .active_processes
16861                .get_mut(target_process_id)
16862                .ok_or_else(|| {
16863                    SidecarError::InvalidState(format!(
16864                        "vm.fetch target process disappeared: {target_process_id}"
16865                    ))
16866                })?;
16867            service_host_fetch_target_event(
16868                bridge,
16869                vm_id,
16870                &dns,
16871                socket_paths,
16872                &mut vm.kernel,
16873                process,
16874                resource_limits,
16875                Duration::from_millis(5),
16876            )?;
16877        }
16878
16879        let poll = vm
16880            .kernel
16881            .poll_targets(
16882                EXECUTION_DRIVER_NAME,
16883                kernel_pid,
16884                vec![PollTargetEntry::socket(
16885                    socket_id,
16886                    POLLIN | POLLHUP | POLLERR,
16887                )],
16888                5,
16889            )
16890            .map_err(kernel_error)?;
16891        let revents = poll
16892            .targets
16893            .first()
16894            .map(|entry| entry.revents)
16895            .unwrap_or_else(PollEvents::empty);
16896        if revents.intersects(POLLERR) {
16897            return Err(SidecarError::Execution(String::from(
16898                "vm.fetch kernel TCP socket reported POLLERR",
16899            )));
16900        }
16901        if revents.intersects(POLLIN) {
16902            match vm
16903                .kernel
16904                .socket_read(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, 64 * 1024)
16905            {
16906                Ok(Some(bytes)) if !bytes.is_empty() => {
16907                    response_buffer.extend(bytes);
16908                    ensure_vm_fetch_raw_response_buffer_within_limit(
16909                        response_buffer.len(),
16910                        "vm.fetch",
16911                    )?;
16912                }
16913                Ok(Some(_)) => {}
16914                Ok(None) => peer_closed = true,
16915                Err(error) if error.code() == "EAGAIN" => {}
16916                Err(error) => return Err(kernel_error(error)),
16917            }
16918        }
16919        if revents.intersects(POLLHUP) {
16920            peer_closed = true;
16921        }
16922    }
16923}
16924
16925fn outbound_http_response_json(url: &Url, response: ureq::Response) -> Result<Value, SidecarError> {
16926    let status = response.status();
16927    let status_text = response.status_text().to_owned();
16928    let mut header_pairs = Vec::new();
16929    let mut raw_headers = Vec::new();
16930    for raw_name in response.headers_names() {
16931        for value in response.all(&raw_name) {
16932            header_pairs.push(json!([raw_name.to_ascii_lowercase(), value]));
16933            raw_headers.push(Value::String(raw_name.clone()));
16934            raw_headers.push(Value::String(value.to_owned()));
16935        }
16936    }
16937    let mut reader = response.into_reader();
16938    let mut body = Vec::new();
16939    reader.read_to_end(&mut body).map_err(|error| {
16940        SidecarError::Execution(format!("failed to read HTTP response: {error}"))
16941    })?;
16942    serde_json::to_string(&json!({
16943        "status": status,
16944        "statusText": status_text,
16945        "headers": header_pairs,
16946        "rawHeaders": raw_headers,
16947        "body": base64::engine::general_purpose::STANDARD.encode(body),
16948        "bodyEncoding": "base64",
16949        "url": url.as_str(),
16950    }))
16951    .map(Value::String)
16952    .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
16953}
16954
16955/// Split a ureq resolver `netloc` (`host:port`, with optional `[..]` IPv6
16956/// brackets) into its host and port components. Returns `None` if the port is
16957/// missing or unparseable.
16958fn split_netloc(netloc: &str) -> Option<(&str, u16)> {
16959    let (host, port) = netloc.rsplit_once(':')?;
16960    let port: u16 = port.parse().ok()?;
16961    let host = host
16962        .strip_prefix('[')
16963        .and_then(|rest| rest.strip_suffix(']'))
16964        .unwrap_or(host);
16965    Some((host, port))
16966}
16967
16968fn issue_outbound_http_request(
16969    url: &Url,
16970    options: &JavascriptHttpRequestOptions,
16971    headers: &HttpHeaderCollection,
16972    pinned_addresses: &[IpAddr],
16973) -> Result<Value, SidecarError> {
16974    let method = options.method.as_deref().unwrap_or("GET");
16975    // Pin the underlying resolver to the egress-vetted addresses. ureq performs
16976    // its own DNS resolution for the TCP/TLS connect; without this override an
16977    // https:// request would re-resolve the hostname through the host resolver
16978    // (a rebinding DNS server could then return a private/metadata IP that the
16979    // earlier range check would have rejected). The pinned resolver returns only
16980    // the vetted addresses and refuses any host it was not vetted for, while the
16981    // request URL keeps the original hostname so TLS SNI and the Host header stay
16982    // correct.
16983    let pinned_host = url.host_str().map(str::to_owned);
16984    let pinned: Vec<IpAddr> = pinned_addresses.to_vec();
16985    let resolver = move |netloc: &str| -> std::io::Result<Vec<SocketAddr>> {
16986        let (host, port) = split_netloc(netloc).ok_or_else(|| {
16987            std::io::Error::new(
16988                std::io::ErrorKind::InvalidInput,
16989                format!("invalid network location: {netloc}"),
16990            )
16991        })?;
16992        let expected_host = pinned_host.as_deref();
16993        if expected_host != Some(host) {
16994            return Err(std::io::Error::new(
16995                std::io::ErrorKind::PermissionDenied,
16996                format!(
16997                    "EACCES: outbound HTTP resolver pinned to {expected_host:?}, refusing {host}"
16998                ),
16999            ));
17000        }
17001        if pinned.is_empty() {
17002            return Err(std::io::Error::new(
17003                std::io::ErrorKind::PermissionDenied,
17004                "EACCES: no egress-vetted address available for outbound HTTP request",
17005            ));
17006        }
17007        Ok(pinned.iter().map(|ip| SocketAddr::new(*ip, port)).collect())
17008    };
17009    let mut agent_builder = ureq::AgentBuilder::new()
17010        .resolver(resolver)
17011        .timeout_connect(Duration::from_secs(5))
17012        .timeout_read(Duration::from_secs(15))
17013        .timeout_write(Duration::from_secs(15));
17014    if url.scheme() == "https" {
17015        let tls_options = JavascriptTlsBridgeOptions {
17016            is_server: false,
17017            servername: url.host_str().map(str::to_owned),
17018            alpn_protocols: Some(vec![String::from("http/1.1")]),
17019            reject_unauthorized: options.reject_unauthorized,
17020            ..JavascriptTlsBridgeOptions::default()
17021        };
17022        agent_builder = agent_builder.tls_config(Arc::new(build_client_tls_config(&tls_options)?));
17023    }
17024    let agent = agent_builder.build();
17025    let mut request = agent.request_url(method, url);
17026    for (name, values) in &headers.normalized {
17027        if name == "host" {
17028            continue;
17029        }
17030        let header_value = values.join(", ");
17031        request = request.set(name, &header_value);
17032    }
17033    let response = match options.body.as_deref() {
17034        Some(body) => request.send_string(body),
17035        None => request.call(),
17036    };
17037
17038    match response {
17039        Ok(response) => outbound_http_response_json(url, response),
17040        Err(ureq::Error::Status(_, response)) => outbound_http_response_json(url, response),
17041        Err(ureq::Error::Transport(error)) => Err(SidecarError::Execution(format!(
17042            "ERR_HTTP_REQUEST_FAILED: {error}"
17043        ))),
17044    }
17045}
17046
17047fn wait_for_loopback_http_response<B>(
17048    request: LoopbackHttpResponseWaitRequest<'_, B>,
17049) -> Result<String, SidecarError>
17050where
17051    B: NativeSidecarBridge + Send + 'static,
17052    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17053{
17054    let LoopbackHttpResponseWaitRequest {
17055        bridge,
17056        vm_id,
17057        dns,
17058        socket_paths,
17059        kernel,
17060        process,
17061        resource_limits,
17062        request_key,
17063    } = request;
17064    let deadline = Instant::now() + http_loopback_request_timeout();
17065    loop {
17066        if let Some(response) = process
17067            .pending_http_requests
17068            .get(&request_key)
17069            .and_then(|response| response.clone())
17070        {
17071            process.pending_http_requests.remove(&request_key);
17072            return Ok(response);
17073        }
17074
17075        if Instant::now() >= deadline {
17076            process.pending_http_requests.remove(&request_key);
17077            return Err(SidecarError::Execution(String::from(
17078                "HTTP loopback request timed out waiting for net.http_respond",
17079            )));
17080        }
17081
17082        let Some(event) = process
17083            .execution
17084            .poll_event_blocking(Duration::from_millis(10))
17085            .map_err(|error| SidecarError::Execution(error.to_string()))?
17086        else {
17087            continue;
17088        };
17089
17090        match event {
17091            ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
17092                let network_counts = process.network_resource_counts();
17093                let response = service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
17094                    bridge,
17095                    vm_id,
17096                    dns,
17097                    socket_paths,
17098                    kernel,
17099                    process,
17100                    sync_request: &request,
17101                    resource_limits,
17102                    network_counts,
17103                });
17104                match response {
17105                    Ok(result) => process
17106                        .execution
17107                        .respond_javascript_sync_rpc_success(request.id, result)
17108                        .or_else(ignore_stale_javascript_sync_rpc_response)?,
17109                    Err(error) => process
17110                        .execution
17111                        .respond_javascript_sync_rpc_error(
17112                            request.id,
17113                            javascript_sync_rpc_error_code(&error),
17114                            error.to_string(),
17115                        )
17116                        .or_else(ignore_stale_javascript_sync_rpc_response)?,
17117                }
17118            }
17119            ActiveExecutionEvent::Exited(code) => {
17120                process.pending_http_requests.remove(&request_key);
17121                return Err(SidecarError::Execution(format!(
17122                    "HTTP loopback server exited before responding (exit code {code})"
17123                )));
17124            }
17125            ActiveExecutionEvent::Stdout(_)
17126            | ActiveExecutionEvent::Stderr(_)
17127            | ActiveExecutionEvent::PythonVfsRpcRequest(_)
17128            | ActiveExecutionEvent::SignalState { .. } => {}
17129        }
17130    }
17131}
17132
17133pub(crate) fn dispatch_loopback_http_request<B>(
17134    request: LoopbackHttpDispatchRequest<'_, B>,
17135) -> Result<String, SidecarError>
17136where
17137    B: NativeSidecarBridge + Send + 'static,
17138    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17139{
17140    let LoopbackHttpDispatchRequest {
17141        bridge,
17142        vm_id,
17143        dns,
17144        socket_paths,
17145        kernel,
17146        process,
17147        resource_limits,
17148        server_id,
17149        request_json,
17150    } = request;
17151    let request_id = {
17152        let server = process.http_servers.get_mut(&server_id).ok_or_else(|| {
17153            SidecarError::InvalidState(format!("HTTP target server disappeared: {server_id}"))
17154        })?;
17155        server.next_request_id += 1;
17156        server.next_request_id
17157    };
17158    process
17159        .pending_http_requests
17160        .insert((server_id, request_id), None);
17161    process.execution.send_javascript_stream_event(
17162        "http_request",
17163        json!({
17164            "serverId": server_id,
17165            "requestId": request_id,
17166            "request": request_json,
17167        }),
17168    )?;
17169    wait_for_loopback_http_response(LoopbackHttpResponseWaitRequest {
17170        bridge,
17171        vm_id,
17172        dns,
17173        socket_paths,
17174        kernel,
17175        process,
17176        resource_limits,
17177        request_key: (server_id, request_id),
17178    })
17179}
17180
17181fn ensure_vm_fetch_response_within_limit(
17182    response_json: &str,
17183    operation: &str,
17184    limit: usize,
17185) -> Result<(), SidecarError> {
17186    let size = response_json.len();
17187    if size > limit {
17188        return Err(SidecarError::Execution(format!(
17189            "{operation} payload is {size} bytes, limit is {limit}"
17190        )));
17191    }
17192    Ok(())
17193}
17194
17195fn ensure_vm_fetch_raw_response_buffer_within_limit(
17196    size: usize,
17197    operation: &str,
17198) -> Result<(), SidecarError> {
17199    if size > VM_FETCH_BUFFER_LIMIT_BYTES {
17200        return Err(SidecarError::Execution(format!(
17201            "{operation} raw response buffer is {size} bytes, limit is {VM_FETCH_BUFFER_LIMIT_BYTES}"
17202        )));
17203    }
17204    Ok(())
17205}
17206
17207pub(crate) fn ensure_vm_fetch_response_frame_within_limit(
17208    response: &ResponseFrame,
17209    max_frame_bytes: usize,
17210) -> Result<(), SidecarError> {
17211    let max_frame_bytes = max_frame_bytes.min(VM_FETCH_BUFFER_LIMIT_BYTES);
17212    let frame = crate::protocol::to_generated_protocol_frame(
17213        &crate::protocol::ProtocolFrame::Response(response.clone()),
17214    )
17215    .map_err(|error| SidecarError::FrameTooLarge(error.to_string()))?;
17216    let WireProtocolFrame::ResponseFrame(_) = &frame else {
17217        return Err(SidecarError::FrameTooLarge(String::from(
17218            "vm fetch response converted to non-response wire frame",
17219        )));
17220    };
17221    WireFrameCodec::new(max_frame_bytes)
17222        .encode(&frame)
17223        .map(|_| ())
17224        .map_err(|error| SidecarError::FrameTooLarge(error.to_string()))
17225}
17226
17227fn service_javascript_dns_sync_rpc<B>(
17228    bridge: &SharedBridge<B>,
17229    kernel: &SidecarKernel,
17230    vm_id: &str,
17231    dns: &VmDnsConfig,
17232    request: &JavascriptSyncRpcRequest,
17233) -> Result<Value, SidecarError>
17234where
17235    B: NativeSidecarBridge + Send + 'static,
17236    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17237{
17238    match request.method.as_str() {
17239        "dns.lookup" => {
17240            let payload = request
17241                .args
17242                .first()
17243                .cloned()
17244                .ok_or_else(|| {
17245                    SidecarError::InvalidState(String::from(
17246                        "dns.lookup requires a request payload",
17247                    ))
17248                })
17249                .and_then(|value| {
17250                    serde_json::from_value::<JavascriptDnsLookupRequest>(value).map_err(|error| {
17251                        SidecarError::InvalidState(format!("invalid dns.lookup payload: {error}"))
17252                    })
17253                })?;
17254            let addresses = filter_dns_ip_addrs(
17255                resolve_dns_ip_addrs(
17256                    bridge,
17257                    kernel,
17258                    vm_id,
17259                    dns,
17260                    &payload.hostname,
17261                    DnsLookupPolicy::CheckPermissions,
17262                )?,
17263                payload.family,
17264            )?;
17265            let addresses = filter_dns_safe_ip_addrs(addresses, &payload.hostname)?;
17266            Ok(Value::Array(
17267                addresses
17268                    .into_iter()
17269                    .map(|ip| {
17270                        json!({
17271                            "address": ip.to_string(),
17272                            "family": if ip.is_ipv6() { 6 } else { 4 },
17273                        })
17274                    })
17275                    .collect(),
17276            ))
17277        }
17278        "dns.resolve" | "dns.resolve4" | "dns.resolve6" => {
17279            let payload = request
17280                .args
17281                .first()
17282                .cloned()
17283                .ok_or_else(|| {
17284                    SidecarError::InvalidState(String::from(
17285                        "dns.resolve requires a request payload",
17286                    ))
17287                })
17288                .and_then(|value| {
17289                    serde_json::from_value::<JavascriptDnsResolveRequest>(value).map_err(|error| {
17290                        SidecarError::InvalidState(format!("invalid dns.resolve payload: {error}"))
17291                    })
17292                })?;
17293            let requested_type = match request.method.as_str() {
17294                "dns.resolve4" => String::from("A"),
17295                "dns.resolve6" => String::from("AAAA"),
17296                _ => payload
17297                    .rrtype
17298                    .as_deref()
17299                    .unwrap_or("A")
17300                    .to_ascii_uppercase(),
17301            };
17302            let record_type = parse_dns_record_type(&requested_type)?;
17303            let resolution = resolve_dns_records(
17304                bridge,
17305                kernel,
17306                vm_id,
17307                dns,
17308                &payload.hostname,
17309                record_type,
17310                DnsLookupPolicy::CheckPermissions,
17311            )?;
17312            dns_resolution_to_node_value(&resolution, &requested_type)
17313        }
17314        other => Err(SidecarError::InvalidState(format!(
17315            "unsupported JavaScript dns sync RPC method {other}"
17316        ))),
17317    }
17318}
17319
17320fn service_javascript_dgram_sync_rpc<B>(
17321    request: JavascriptDgramSyncRpcServiceRequest<'_, B>,
17322) -> Result<Value, SidecarError>
17323where
17324    B: NativeSidecarBridge + Send + 'static,
17325    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17326{
17327    let JavascriptDgramSyncRpcServiceRequest {
17328        bridge,
17329        kernel,
17330        vm_id,
17331        dns,
17332        socket_paths,
17333        process,
17334        sync_request: request,
17335        resource_limits,
17336        network_counts,
17337    } = request;
17338    match request.method.as_str() {
17339        "dgram.createSocket" => {
17340            check_network_resource_limit(
17341                resource_limits.max_sockets,
17342                network_counts.sockets,
17343                1,
17344                "socket",
17345            )?;
17346            let payload = request
17347                .args
17348                .first()
17349                .cloned()
17350                .ok_or_else(|| {
17351                    SidecarError::InvalidState(String::from(
17352                        "dgram.createSocket requires a request payload",
17353                    ))
17354                })
17355                .and_then(|value| {
17356                    serde_json::from_value::<JavascriptDgramCreateSocketRequest>(value).map_err(
17357                        |error| {
17358                            SidecarError::InvalidState(format!(
17359                                "invalid dgram.createSocket payload: {error}"
17360                            ))
17361                        },
17362                    )
17363                })?;
17364            let family = JavascriptUdpFamily::from_socket_type(&payload.socket_type)?;
17365            let socket_id = process.allocate_udp_socket_id();
17366            process.udp_sockets.insert(
17367                socket_id.clone(),
17368                ActiveUdpSocket::new(kernel, process.kernel_pid, family)?,
17369            );
17370            Ok(json!({
17371                "socketId": socket_id,
17372                "type": family.socket_type(),
17373            }))
17374        }
17375        "dgram.bind" => {
17376            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.bind socket id")?;
17377            let payload = request
17378                .args
17379                .get(1)
17380                .cloned()
17381                .ok_or_else(|| {
17382                    SidecarError::InvalidState(String::from(
17383                        "dgram.bind requires a request payload",
17384                    ))
17385                })
17386                .and_then(|value| {
17387                    serde_json::from_value::<JavascriptDgramBindRequest>(value).map_err(|error| {
17388                        SidecarError::InvalidState(format!("invalid dgram.bind payload: {error}"))
17389                    })
17390                })?;
17391            let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
17392                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17393            })?;
17394            let local_addr = socket.bind(
17395                kernel,
17396                process.kernel_pid,
17397                payload.address.as_deref(),
17398                payload.port,
17399                socket_paths,
17400            )?;
17401            Ok(json!({
17402                "localAddress": local_addr.ip().to_string(),
17403                "localPort": local_addr.port(),
17404                "family": socket_addr_family(&local_addr),
17405            }))
17406        }
17407        "dgram.send" => {
17408            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.send socket id")?;
17409            let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "dgram.send payload")?;
17410            let payload = request
17411                .args
17412                .get(2)
17413                .cloned()
17414                .ok_or_else(|| {
17415                    SidecarError::InvalidState(String::from(
17416                        "dgram.send requires a request payload",
17417                    ))
17418                })
17419                .and_then(|value| {
17420                    serde_json::from_value::<JavascriptDgramSendRequest>(value).map_err(|error| {
17421                        SidecarError::InvalidState(format!("invalid dgram.send payload: {error}"))
17422                    })
17423                })?;
17424            let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
17425                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17426            })?;
17427            let (written, local_addr) = socket.send_to(ActiveUdpSendToRequest {
17428                bridge,
17429                kernel,
17430                kernel_pid: process.kernel_pid,
17431                vm_id,
17432                dns,
17433                host: payload.address.as_deref().unwrap_or("localhost"),
17434                port: payload.port,
17435                context: socket_paths,
17436                contents: &chunk,
17437            })?;
17438            Ok(json!({
17439                "bytes": written,
17440                "localAddress": local_addr.ip().to_string(),
17441                "localPort": local_addr.port(),
17442                "family": socket_addr_family(&local_addr),
17443            }))
17444        }
17445        "dgram.poll" => {
17446            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.poll socket id")?;
17447            let wait_ms =
17448                javascript_sync_rpc_arg_u64_optional(&request.args, 1, "dgram.poll wait ms")?
17449                    .unwrap_or_default();
17450            let event = {
17451                let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
17452                    SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17453                })?;
17454                socket.poll(kernel, process.kernel_pid, Duration::from_millis(wait_ms))?
17455            };
17456
17457            match event {
17458                Some(JavascriptUdpSocketEvent::Message { data, remote_addr }) => {
17459                    let family = JavascriptSocketFamily::from_ip(remote_addr.ip());
17460                    let guest_remote_port = if is_loopback_ip(remote_addr.ip()) {
17461                        socket_paths
17462                            .guest_udp_port_for_host_port(family, remote_addr.port())
17463                            .unwrap_or(remote_addr.port())
17464                    } else {
17465                        remote_addr.port()
17466                    };
17467                    Ok(json!({
17468                    "type": "message",
17469                    "data": javascript_sync_rpc_bytes_value(&data),
17470                    "remoteAddress": remote_addr.ip().to_string(),
17471                    "remotePort": guest_remote_port,
17472                    "remoteFamily": socket_addr_family(&remote_addr),
17473                    }))
17474                }
17475                Some(JavascriptUdpSocketEvent::Error { code, message }) => Ok(json!({
17476                    "type": "error",
17477                    "code": code,
17478                    "message": message,
17479                })),
17480                None => Ok(Value::Null),
17481            }
17482        }
17483        "dgram.close" => {
17484            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.close socket id")?;
17485            let mut socket = process.udp_sockets.remove(socket_id).ok_or_else(|| {
17486                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17487            })?;
17488            socket.close(kernel, process.kernel_pid);
17489            Ok(Value::Null)
17490        }
17491        "dgram.address" => {
17492            let socket_id =
17493                javascript_sync_rpc_arg_str(&request.args, 0, "dgram.address socket id")?;
17494            let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
17495                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17496            })?;
17497            let local_addr = socket.local_addr().ok_or_else(|| {
17498                SidecarError::Execution(String::from("EBADF: bad file descriptor"))
17499            })?;
17500            javascript_net_json_string(
17501                json!({
17502                    "address": local_addr.ip().to_string(),
17503                    "port": local_addr.port(),
17504                    "family": socket_addr_family(&local_addr),
17505                }),
17506                "dgram.address",
17507            )
17508        }
17509        "dgram.setBufferSize" => {
17510            let socket_id =
17511                javascript_sync_rpc_arg_str(&request.args, 0, "dgram.setBufferSize socket id")?;
17512            let which =
17513                javascript_sync_rpc_arg_str(&request.args, 1, "dgram.setBufferSize buffer kind")?;
17514            let size = javascript_sync_rpc_arg_u64(&request.args, 2, "dgram.setBufferSize size")?;
17515            let size = usize::try_from(size).map_err(|_| {
17516                SidecarError::InvalidState(String::from(
17517                    "dgram.setBufferSize size must fit within usize",
17518                ))
17519            })?;
17520            let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
17521                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17522            })?;
17523            socket.set_buffer_size(which, size)?;
17524            Ok(Value::Null)
17525        }
17526        "dgram.getBufferSize" => {
17527            let socket_id =
17528                javascript_sync_rpc_arg_str(&request.args, 0, "dgram.getBufferSize socket id")?;
17529            let which =
17530                javascript_sync_rpc_arg_str(&request.args, 1, "dgram.getBufferSize buffer kind")?;
17531            let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
17532                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17533            })?;
17534            let size = socket.get_buffer_size(which)?;
17535            Ok(json!(size))
17536        }
17537        other => Err(SidecarError::InvalidState(format!(
17538            "unsupported JavaScript dgram sync RPC method {other}"
17539        ))),
17540    }
17541}
17542
17543#[derive(Debug)]
17544struct ClientHttp2StreamState {
17545    send_stream: Option<h2::SendStream<Bytes>>,
17546}
17547
17548#[derive(Debug)]
17549struct ServerHttp2StreamState {
17550    send_response: Option<ServerHttp2Responder>,
17551    send_stream: Option<h2::SendStream<Bytes>>,
17552}
17553
17554#[derive(Debug)]
17555enum ServerHttp2Responder {
17556    Regular(server::SendResponse<Bytes>),
17557    Pushed(server::SendPushedResponse<Bytes>),
17558}
17559
17560const HTTP2_DEFAULT_WINDOW_SIZE: u32 = 65_535;
17561const HTTP2_POLL_DELAY: Duration = Duration::from_millis(10);
17562
17563fn http2_runtime_snapshot() -> Http2RuntimeSnapshot {
17564    Http2RuntimeSnapshot {
17565        effective_local_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
17566        local_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
17567        remote_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
17568        next_stream_id: 1,
17569        outbound_queue_size: 1,
17570        deflate_dynamic_table_size: 0,
17571        inflate_dynamic_table_size: 0,
17572    }
17573}
17574
17575fn http2_snapshot_json(snapshot: &Http2SessionSnapshot) -> Result<String, SidecarError> {
17576    serde_json::to_string(snapshot)
17577        .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
17578}
17579
17580fn http2_event_value(event: &Http2BridgeEvent) -> Result<Value, SidecarError> {
17581    serde_json::to_string(event)
17582        .map(Value::String)
17583        .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
17584}
17585
17586fn push_http2_server_event(
17587    shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17588    server_id: u64,
17589    event: Http2BridgeEvent,
17590) {
17591    if let Ok(mut state) = shared.lock() {
17592        state
17593            .server_events
17594            .entry(server_id)
17595            .or_default()
17596            .push_back(event);
17597    }
17598}
17599
17600fn push_http2_session_event(
17601    shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17602    session_id: u64,
17603    event: Http2BridgeEvent,
17604) {
17605    if let Ok(mut state) = shared.lock() {
17606        state
17607            .session_events
17608            .entry(session_id)
17609            .or_default()
17610            .push_back(event);
17611    }
17612}
17613
17614fn pop_http2_event(
17615    queue: &mut BTreeMap<u64, VecDeque<Http2BridgeEvent>>,
17616    id: u64,
17617) -> Option<Http2BridgeEvent> {
17618    queue.get_mut(&id).and_then(VecDeque::pop_front)
17619}
17620
17621fn wait_for_http2_event(
17622    shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17623    id: u64,
17624    is_server: bool,
17625    wait_ms: u64,
17626) -> Option<Http2BridgeEvent> {
17627    let deadline = Instant::now() + Duration::from_millis(wait_ms);
17628    loop {
17629        if let Ok(mut state) = shared.lock() {
17630            let queue = if is_server {
17631                &mut state.server_events
17632            } else {
17633                &mut state.session_events
17634            };
17635            if let Some(event) = pop_http2_event(queue, id) {
17636                return Some(event);
17637            }
17638        }
17639        if wait_ms == 0 || Instant::now() >= deadline {
17640            return None;
17641        }
17642        thread::sleep(HTTP2_POLL_DELAY);
17643    }
17644}
17645
17646fn next_http2_session_id(shared: &mut crate::state::Http2SharedState) -> u64 {
17647    shared.next_session_id += 1;
17648    shared.next_session_id
17649}
17650
17651fn next_http2_stream_id(shared: &mut crate::state::Http2SharedState) -> u64 {
17652    shared.next_stream_id += 1;
17653    shared.next_stream_id
17654}
17655
17656fn http2_reason(code: Option<u32>) -> Reason {
17657    code.unwrap_or(Reason::NO_ERROR.into()).into()
17658}
17659
17660fn http2_error_payload(message: impl Into<String>) -> String {
17661    serde_json::to_string(&json!({
17662        "name": "Error",
17663        "code": "ERR_HTTP2_ERROR",
17664        "message": message.into(),
17665    }))
17666    .unwrap_or_else(|_| {
17667        String::from(
17668            "{\"name\":\"Error\",\"code\":\"ERR_HTTP2_ERROR\",\"message\":\"HTTP/2 bridge error\"}",
17669        )
17670    })
17671}
17672
17673fn http2_socket_snapshot(local_addr: SocketAddr, remote_addr: SocketAddr) -> Http2SocketSnapshot {
17674    Http2SocketSnapshot {
17675        encrypted: false,
17676        allow_half_open: false,
17677        local_address: Some(local_addr.ip().to_string()),
17678        local_port: Some(local_addr.port()),
17679        local_family: Some(socket_addr_family(&local_addr).to_string()),
17680        remote_address: Some(remote_addr.ip().to_string()),
17681        remote_port: Some(remote_addr.port()),
17682        remote_family: Some(socket_addr_family(&remote_addr).to_string()),
17683        servername: None,
17684        alpn_protocol: Some(String::from("h2c")),
17685    }
17686}
17687
17688fn http2_wait_result(kind: &str, id: u64) -> Value {
17689    json!({
17690        "kind": kind,
17691        "id": id,
17692    })
17693}
17694
17695fn is_http2_terminal_event(event: &Http2BridgeEvent, is_server: bool, id: u64) -> bool {
17696    if is_server {
17697        event.kind == "serverClose" && event.id == id
17698    } else {
17699        event.kind == "sessionClose" && event.id == id
17700    }
17701}
17702
17703fn dispatch_http2_wait_loop(
17704    process: &ActiveProcess,
17705    id: u64,
17706    is_server: bool,
17707) -> Result<Value, SidecarError> {
17708    loop {
17709        if let Some(event) = wait_for_http2_event(&process.http2.shared, id, is_server, 50) {
17710            let payload = serde_json::to_value(&event).map_err(|error| {
17711                SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}"))
17712            })?;
17713            process
17714                .execution
17715                .send_javascript_stream_event("http2", payload.clone())?;
17716            if is_http2_terminal_event(&event, is_server, id) {
17717                return Ok(payload);
17718            }
17719            continue;
17720        }
17721
17722        let exists = process
17723            .http2
17724            .shared
17725            .lock()
17726            .map(|state| {
17727                if is_server {
17728                    state.servers.contains_key(&id)
17729                } else {
17730                    state.sessions.contains_key(&id)
17731                }
17732            })
17733            .unwrap_or(false);
17734        if !exists {
17735            return Ok(if is_server {
17736                http2_wait_result("serverClose", id)
17737            } else {
17738                http2_wait_result("sessionClose", id)
17739            });
17740        }
17741    }
17742}
17743
17744fn dispatch_http_wait_loop(process: &ActiveProcess, server_id: u64) -> Result<Value, SidecarError> {
17745    loop {
17746        if !process.http_servers.contains_key(&server_id) {
17747            return Ok(json!({
17748                "kind": "serverClose",
17749                "id": server_id,
17750            }));
17751        }
17752        thread::sleep(Duration::from_millis(25));
17753    }
17754}
17755
17756fn http2_settings_from_value(settings: &BTreeMap<String, Value>) -> BTreeMap<String, Value> {
17757    settings.clone()
17758}
17759
17760fn parse_http2_headers_json(
17761    headers_json: &str,
17762    label: &str,
17763) -> Result<BTreeMap<String, Value>, SidecarError> {
17764    serde_json::from_str::<BTreeMap<String, Value>>(headers_json)
17765        .map_err(|error| SidecarError::InvalidState(format!("{label} must be valid JSON: {error}")))
17766}
17767
17768fn apply_http2_header_values(
17769    header_map: &mut HeaderMap,
17770    name: &str,
17771    value: &Value,
17772) -> Result<(), SidecarError> {
17773    let header_name = HeaderName::from_bytes(name.as_bytes()).map_err(|error| {
17774        SidecarError::InvalidState(format!("invalid HTTP/2 header name {name:?}: {error}"))
17775    })?;
17776    match value {
17777        Value::Array(values) => {
17778            for value in values {
17779                apply_http2_header_values(header_map, name, value)?;
17780            }
17781        }
17782        Value::String(text) => {
17783            let value = HeaderValue::from_str(text).map_err(|error| {
17784                SidecarError::InvalidState(format!(
17785                    "invalid HTTP/2 header value for {name}: {error}"
17786                ))
17787            })?;
17788            header_map.append(header_name.clone(), value);
17789        }
17790        Value::Number(number) => {
17791            let value = HeaderValue::from_str(&number.to_string()).map_err(|error| {
17792                SidecarError::InvalidState(format!(
17793                    "invalid HTTP/2 numeric header value for {name}: {error}"
17794                ))
17795            })?;
17796            header_map.append(header_name.clone(), value);
17797        }
17798        Value::Bool(boolean) => {
17799            let value = HeaderValue::from_str(if *boolean { "true" } else { "false" }).map_err(
17800                |error| {
17801                    SidecarError::InvalidState(format!(
17802                        "invalid HTTP/2 boolean header value for {name}: {error}"
17803                    ))
17804                },
17805            )?;
17806            header_map.append(header_name.clone(), value);
17807        }
17808        Value::Null => {}
17809        Value::Object(_) => {
17810            return Err(SidecarError::InvalidState(format!(
17811                "unsupported HTTP/2 header object value for {name}"
17812            )));
17813        }
17814    }
17815    Ok(())
17816}
17817
17818fn build_http2_request(headers_json: &str) -> Result<Request<()>, SidecarError> {
17819    let headers = parse_http2_headers_json(headers_json, "HTTP/2 request headers")?;
17820    let method = headers
17821        .get(":method")
17822        .and_then(Value::as_str)
17823        .unwrap_or("GET");
17824    let path = headers.get(":path").and_then(Value::as_str).unwrap_or("/");
17825    let mut builder = Request::builder()
17826        .method(Method::from_bytes(method.as_bytes()).map_err(|error| {
17827            SidecarError::InvalidState(format!("invalid HTTP/2 method {method:?}: {error}"))
17828        })?)
17829        .uri(path.parse::<Uri>().map_err(|error| {
17830            SidecarError::InvalidState(format!("invalid HTTP/2 path {path:?}: {error}"))
17831        })?);
17832    {
17833        let header_map = builder.headers_mut().expect("request header map");
17834        for (name, value) in &headers {
17835            if name.starts_with(':') {
17836                continue;
17837            }
17838            apply_http2_header_values(header_map, name, value)?;
17839        }
17840    }
17841    builder
17842        .body(())
17843        .map_err(|error| SidecarError::InvalidState(format!("invalid HTTP/2 request: {error}")))
17844}
17845
17846fn build_http2_response(headers_json: &str) -> Result<Response<()>, SidecarError> {
17847    let headers = parse_http2_headers_json(headers_json, "HTTP/2 response headers")?;
17848    let status = headers
17849        .get(":status")
17850        .and_then(Value::as_u64)
17851        .or_else(|| {
17852            headers
17853                .get(":status")
17854                .and_then(Value::as_str)
17855                .and_then(|value| value.parse::<u16>().ok().map(u64::from))
17856        })
17857        .unwrap_or(200);
17858    let mut builder = Response::builder().status(status as u16);
17859    {
17860        let header_map = builder.headers_mut().expect("response header map");
17861        for (name, value) in &headers {
17862            if name.starts_with(':') {
17863                continue;
17864            }
17865            apply_http2_header_values(header_map, name, value)?;
17866        }
17867    }
17868    builder.body(()).map_err(|error| {
17869        SidecarError::InvalidState(format!("invalid HTTP/2 response headers: {error}"))
17870    })
17871}
17872
17873fn serialize_http2_headers_map(
17874    pseudo: BTreeMap<String, Value>,
17875    headers: &HeaderMap,
17876) -> Result<String, SidecarError> {
17877    let mut serialized = pseudo;
17878    for (name, value) in headers {
17879        let name = name.as_str().to_string();
17880        let value = Value::String(
17881            value
17882                .to_str()
17883                .map_err(|error| {
17884                    SidecarError::Execution(format!("invalid HTTP/2 header value: {error}"))
17885                })?
17886                .to_owned(),
17887        );
17888        match serialized.get_mut(&name) {
17889            Some(Value::Array(values)) => values.push(value),
17890            Some(existing) => {
17891                let first = existing.clone();
17892                *existing = Value::Array(vec![first, value]);
17893            }
17894            None => {
17895                serialized.insert(name, value);
17896            }
17897        }
17898    }
17899    serde_json::to_string(&serialized)
17900        .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
17901}
17902
17903fn serialize_http2_request_headers(
17904    request: &Request<h2::RecvStream>,
17905) -> Result<String, SidecarError> {
17906    let mut pseudo = BTreeMap::new();
17907    pseudo.insert(
17908        String::from(":method"),
17909        Value::String(request.method().as_str().to_string()),
17910    );
17911    pseudo.insert(
17912        String::from(":path"),
17913        Value::String(
17914            request
17915                .uri()
17916                .path_and_query()
17917                .map(|value| value.as_str().to_string())
17918                .unwrap_or_else(|| String::from("/")),
17919        ),
17920    );
17921    serialize_http2_headers_map(pseudo, request.headers())
17922}
17923
17924fn serialize_http2_response_headers(
17925    response: &Response<h2::RecvStream>,
17926) -> Result<String, SidecarError> {
17927    let mut pseudo = BTreeMap::new();
17928    pseudo.insert(
17929        String::from(":status"),
17930        Value::Number(serde_json::Number::from(response.status().as_u16())),
17931    );
17932    serialize_http2_headers_map(pseudo, response.headers())
17933}
17934
17935fn remove_http2_session_resources(
17936    shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17937    session_id: u64,
17938) {
17939    if let Ok(mut state) = shared.lock() {
17940        state.sessions.remove(&session_id);
17941        state.session_events.remove(&session_id);
17942        let stream_ids = state
17943            .streams
17944            .iter()
17945            .filter_map(|(stream_id, stream)| {
17946                (stream.session_id == session_id).then_some(*stream_id)
17947            })
17948            .collect::<Vec<_>>();
17949        for stream_id in stream_ids {
17950            state.streams.remove(&stream_id);
17951        }
17952    }
17953}
17954
17955fn spawn_http2_client_session(
17956    shared: Arc<Mutex<crate::state::Http2SharedState>>,
17957    session_id: u64,
17958    remote_addr: SocketAddr,
17959    tls: Option<JavascriptTlsBridgeOptions>,
17960    snapshot: Arc<Mutex<Http2SessionSnapshot>>,
17961    mut command_rx: UnboundedReceiver<Http2SessionCommand>,
17962) {
17963    thread::spawn(move || {
17964        let runtime = match TokioRuntimeBuilder::new_current_thread()
17965            .enable_all()
17966            .build()
17967        {
17968            Ok(runtime) => runtime,
17969            Err(error) => {
17970                push_http2_session_event(
17971                    &shared,
17972                    session_id,
17973                    Http2BridgeEvent {
17974                        kind: String::from("sessionError"),
17975                        id: session_id,
17976                        data: Some(http2_error_payload(error.to_string())),
17977                        ..Http2BridgeEvent::default()
17978                    },
17979                );
17980                remove_http2_session_resources(&shared, session_id);
17981                return;
17982            }
17983        };
17984
17985        runtime.block_on(async move {
17986            let stream = match tokio::net::TcpStream::connect(remote_addr).await {
17987                Ok(stream) => stream,
17988                Err(error) => {
17989                    push_http2_session_event(
17990                        &shared,
17991                        session_id,
17992                        Http2BridgeEvent {
17993                            kind: String::from("sessionError"),
17994                            id: session_id,
17995                            data: Some(http2_error_payload(error.to_string())),
17996                            ..Http2BridgeEvent::default()
17997                        },
17998                    );
17999                    remove_http2_session_resources(&shared, session_id);
18000                    return;
18001                }
18002            };
18003
18004            let local_addr = match stream.local_addr() {
18005                Ok(addr) => addr,
18006                Err(error) => {
18007                    push_http2_session_event(
18008                        &shared,
18009                        session_id,
18010                        Http2BridgeEvent {
18011                            kind: String::from("sessionError"),
18012                            id: session_id,
18013                            data: Some(http2_error_payload(error.to_string())),
18014                            ..Http2BridgeEvent::default()
18015                        },
18016                    );
18017                    remove_http2_session_resources(&shared, session_id);
18018                    return;
18019                }
18020            };
18021
18022            {
18023                let mut snapshot_guard = snapshot.lock().expect("http2 snapshot lock");
18024                snapshot_guard.socket = http2_socket_snapshot(local_addr, remote_addr);
18025                if let Some(options) = tls.as_ref() {
18026                    snapshot_guard.encrypted = true;
18027                    snapshot_guard.alpn_protocol = Some(String::from("h2"));
18028                    snapshot_guard.socket.encrypted = true;
18029                    snapshot_guard.socket.servername = options.servername.clone();
18030                    snapshot_guard.socket.alpn_protocol = Some(String::from("h2"));
18031                }
18032                snapshot_guard.state = http2_runtime_snapshot();
18033            }
18034            if let Ok(snapshot_json) =
18035                http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())
18036            {
18037                push_http2_session_event(
18038                    &shared,
18039                    session_id,
18040                    Http2BridgeEvent {
18041                        kind: String::from("sessionConnect"),
18042                        id: session_id,
18043                        data: Some(snapshot_json),
18044                        ..Http2BridgeEvent::default()
18045                    },
18046                );
18047            }
18048
18049            let io: Pin<Box<dyn Http2AsyncIo>> = if let Some(options) = tls.as_ref() {
18050                let server_name = match ServerName::try_from(
18051                    options
18052                        .servername
18053                        .clone()
18054                        .unwrap_or_else(|| String::from("localhost")),
18055                ) {
18056                    Ok(server_name) => server_name,
18057                    Err(_) => {
18058                        push_http2_session_event(
18059                            &shared,
18060                            session_id,
18061                            Http2BridgeEvent {
18062                                kind: String::from("sessionError"),
18063                                id: session_id,
18064                                data: Some(http2_error_payload("invalid TLS servername")),
18065                                ..Http2BridgeEvent::default()
18066                            },
18067                        );
18068                        remove_http2_session_resources(&shared, session_id);
18069                        return;
18070                    }
18071                };
18072                let connector = match build_client_tls_config(options) {
18073                    Ok(config) => TlsConnector::from(Arc::new(config)),
18074                    Err(error) => {
18075                        push_http2_session_event(
18076                            &shared,
18077                            session_id,
18078                            Http2BridgeEvent {
18079                                kind: String::from("sessionError"),
18080                                id: session_id,
18081                                data: Some(http2_error_payload(error.to_string())),
18082                                ..Http2BridgeEvent::default()
18083                            },
18084                        );
18085                        remove_http2_session_resources(&shared, session_id);
18086                        return;
18087                    }
18088                };
18089                match connector.connect(server_name, stream).await {
18090                    Ok(tls_stream) => Box::pin(tls_stream),
18091                    Err(error) => {
18092                        push_http2_session_event(
18093                            &shared,
18094                            session_id,
18095                            Http2BridgeEvent {
18096                                kind: String::from("sessionError"),
18097                                id: session_id,
18098                                data: Some(http2_error_payload(error.to_string())),
18099                                ..Http2BridgeEvent::default()
18100                            },
18101                        );
18102                        remove_http2_session_resources(&shared, session_id);
18103                        return;
18104                    }
18105                }
18106            } else {
18107                Box::pin(stream)
18108            };
18109
18110            let (mut sender, connection) = match client::handshake(io).await {
18111                Ok(parts) => parts,
18112                Err(error) => {
18113                    push_http2_session_event(
18114                        &shared,
18115                        session_id,
18116                        Http2BridgeEvent {
18117                            kind: String::from("sessionError"),
18118                            id: session_id,
18119                            data: Some(http2_error_payload(error.to_string())),
18120                            ..Http2BridgeEvent::default()
18121                        },
18122                    );
18123                    remove_http2_session_resources(&shared, session_id);
18124                    return;
18125                }
18126            };
18127
18128            let (status_tx, mut status_rx) = unbounded_channel::<Result<(), String>>();
18129            tokio::spawn(async move {
18130                let _ = status_tx.send(connection.await.map_err(|error| error.to_string()));
18131            });
18132
18133            let streams: Arc<Mutex<BTreeMap<u64, ClientHttp2StreamState>>> =
18134                Arc::new(Mutex::new(BTreeMap::new()));
18135
18136            loop {
18137                tokio::select! {
18138                    Some(result) = status_rx.recv() => {
18139                        if let Err(message) = result {
18140                            push_http2_session_event(
18141                                &shared,
18142                                session_id,
18143                                Http2BridgeEvent {
18144                                    kind: String::from("sessionError"),
18145                                    id: session_id,
18146                                    data: Some(http2_error_payload(message)),
18147                                    ..Http2BridgeEvent::default()
18148                                },
18149                            );
18150                        }
18151                        push_http2_session_event(
18152                            &shared,
18153                            session_id,
18154                            Http2BridgeEvent {
18155                                kind: String::from("sessionClose"),
18156                                id: session_id,
18157                                ..Http2BridgeEvent::default()
18158                            },
18159                        );
18160                        remove_http2_session_resources(&shared, session_id);
18161                        break;
18162                    }
18163                    Some(command) = command_rx.recv() => {
18164                        match command {
18165                            Http2SessionCommand::Request { headers_json, options_json, respond_to } => {
18166                                let request = match build_http2_request(&headers_json) {
18167                                    Ok(request) => request,
18168                                    Err(error) => {
18169                                        let _ = respond_to.send(Err(error.to_string()));
18170                                        continue;
18171                                    }
18172                                };
18173                                let options: JavascriptHttp2RequestOptions =
18174                                    serde_json::from_str(&options_json).unwrap_or_default();
18175                                let stream_id = {
18176                                    let mut state = shared.lock().expect("http2 shared state");
18177                                    let stream_id = next_http2_stream_id(&mut state);
18178                                    state.streams.insert(
18179                                        stream_id,
18180                                        ActiveHttp2Stream {
18181                                            session_id,
18182                                            paused: Arc::new(AtomicBool::new(false)),
18183                                        },
18184                                    );
18185                                    stream_id
18186                                };
18187                                match sender.send_request(request, options.end_stream) {
18188                                    Ok((response_future, send_stream)) => {
18189                                        if !options.end_stream {
18190                                            streams
18191                                                .lock()
18192                                                .expect("http2 client streams")
18193                                                .insert(stream_id, ClientHttp2StreamState { send_stream: Some(send_stream) });
18194                                        }
18195                                        let shared_clone = Arc::clone(&shared);
18196                                        let snapshot_clone = Arc::clone(&snapshot);
18197                                        tokio::spawn(async move {
18198                                            match response_future.await {
18199                                                Ok(response) => {
18200                                                    if let Ok(headers_json) = serialize_http2_response_headers(&response) {
18201                                                        push_http2_session_event(
18202                                                            &shared_clone,
18203                                                            session_id,
18204                                                            Http2BridgeEvent {
18205                                                                kind: String::from("clientResponseHeaders"),
18206                                                                id: stream_id,
18207                                                                data: Some(headers_json),
18208                                                                ..Http2BridgeEvent::default()
18209                                                            },
18210                                                        );
18211                                                    }
18212                                                    let mut body = response.into_body();
18213                                                    while let Some(chunk) = body.data().await {
18214                                                        match chunk {
18215                                                            Ok(bytes) => {
18216                                                                let paused = {
18217                                                                    let state = shared_clone.lock().expect("http2 shared state");
18218                                                                    state.streams.get(&stream_id).map(|stream| Arc::clone(&stream.paused))
18219                                                                };
18220                                                                if let Some(paused) = paused {
18221                                                                    while paused.load(Ordering::SeqCst) {
18222                                                                        tokio::time::sleep(HTTP2_POLL_DELAY).await;
18223                                                                    }
18224                                                                }
18225                                                                let _ = body.flow_control().release_capacity(bytes.len());
18226                                                                push_http2_session_event(
18227                                                                    &shared_clone,
18228                                                                    session_id,
18229                                                                    Http2BridgeEvent {
18230                                                                        kind: String::from("clientData"),
18231                                                                        id: stream_id,
18232                                                                        data: Some(base64::engine::general_purpose::STANDARD.encode(bytes)),
18233                                                                        ..Http2BridgeEvent::default()
18234                                                                    },
18235                                                                );
18236                                                            }
18237                                                            Err(error) => {
18238                                                                push_http2_session_event(
18239                                                                    &shared_clone,
18240                                                                    session_id,
18241                                                                    Http2BridgeEvent {
18242                                                                        kind: String::from("clientError"),
18243                                                                        id: stream_id,
18244                                                                        data: Some(http2_error_payload(error.to_string())),
18245                                                                        ..Http2BridgeEvent::default()
18246                                                                    },
18247                                                                );
18248                                                                break;
18249                                                            }
18250                                                        }
18251                                                    }
18252                                                    {
18253                                                        let mut snapshot = snapshot_clone.lock().expect("http2 snapshot lock");
18254                                                        snapshot.state.next_stream_id =
18255                                                            snapshot.state.next_stream_id.saturating_add(2);
18256                                                    }
18257                                                    push_http2_session_event(
18258                                                        &shared_clone,
18259                                                        session_id,
18260                                                        Http2BridgeEvent {
18261                                                            kind: String::from("clientEnd"),
18262                                                            id: stream_id,
18263                                                            ..Http2BridgeEvent::default()
18264                                                        },
18265                                                    );
18266                                                    push_http2_session_event(
18267                                                        &shared_clone,
18268                                                        session_id,
18269                                                        Http2BridgeEvent {
18270                                                            kind: String::from("clientClose"),
18271                                                            id: stream_id,
18272                                                            extra_number: Some(0),
18273                                                            ..Http2BridgeEvent::default()
18274                                                        },
18275                                                    );
18276                                                    if let Ok(mut state) = shared_clone.lock() {
18277                                                        state.streams.remove(&stream_id);
18278                                                    }
18279                                                }
18280                                                Err(error) => {
18281                                                    push_http2_session_event(
18282                                                        &shared_clone,
18283                                                        session_id,
18284                                                        Http2BridgeEvent {
18285                                                            kind: String::from("clientError"),
18286                                                            id: stream_id,
18287                                                            data: Some(http2_error_payload(error.to_string())),
18288                                                            ..Http2BridgeEvent::default()
18289                                                        },
18290                                                    );
18291                                                    push_http2_session_event(
18292                                                        &shared_clone,
18293                                                        session_id,
18294                                                        Http2BridgeEvent {
18295                                                            kind: String::from("clientClose"),
18296                                                            id: stream_id,
18297                                                            extra_number: Some(u32::from(Reason::INTERNAL_ERROR) as u64),
18298                                                            ..Http2BridgeEvent::default()
18299                                                        },
18300                                                    );
18301                                                    if let Ok(mut state) = shared_clone.lock() {
18302                                                        state.streams.remove(&stream_id);
18303                                                    }
18304                                                }
18305                                            }
18306                                        });
18307                                        let _ = respond_to.send(Ok(json!(stream_id)));
18308                                    }
18309                                    Err(error) => {
18310                                        if let Ok(mut state) = shared.lock() {
18311                                            state.streams.remove(&stream_id);
18312                                        }
18313                                        let _ = respond_to.send(Err(error.to_string()));
18314                                    }
18315                                }
18316                            }
18317                            Http2SessionCommand::Settings { settings_json, respond_to } => {
18318                                let settings = serde_json::from_str::<BTreeMap<String, Value>>(&settings_json)
18319                                    .unwrap_or_default();
18320                                {
18321                                    let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18322                                    snapshot.local_settings = http2_settings_from_value(&settings);
18323                                }
18324                                if let Ok(headers_json) = serde_json::to_string(&settings) {
18325                                    push_http2_session_event(
18326                                        &shared,
18327                                        session_id,
18328                                        Http2BridgeEvent {
18329                                            kind: String::from("sessionLocalSettings"),
18330                                            id: session_id,
18331                                            data: Some(headers_json.clone()),
18332                                            ..Http2BridgeEvent::default()
18333                                        },
18334                                    );
18335                                    push_http2_session_event(
18336                                        &shared,
18337                                        session_id,
18338                                        Http2BridgeEvent {
18339                                            kind: String::from("sessionSettingsAck"),
18340                                            id: session_id,
18341                                            ..Http2BridgeEvent::default()
18342                                        },
18343                                    );
18344                                }
18345                                let _ = respond_to.send(Ok(Value::Null));
18346                            }
18347                            Http2SessionCommand::SetLocalWindowSize { size, respond_to } => {
18348                                {
18349                                    let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18350                                    snapshot.state.local_window_size = size;
18351                                    snapshot.state.effective_local_window_size = size;
18352                                }
18353                                let value = snapshot
18354                                    .lock()
18355                                    .ok()
18356                                    .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok())
18357                                    .map(Value::String)
18358                                    .unwrap_or(Value::Null);
18359                                let _ = respond_to.send(Ok(value));
18360                            }
18361                            Http2SessionCommand::Goaway { error_code, last_stream_id, opaque_data, respond_to } => {
18362                                push_http2_session_event(
18363                                    &shared,
18364                                    session_id,
18365                                    Http2BridgeEvent {
18366                                        kind: String::from("sessionGoaway"),
18367                                        id: session_id,
18368                                        data: opaque_data.map(|value| {
18369                                            base64::engine::general_purpose::STANDARD.encode(value)
18370                                        }),
18371                                        extra_number: Some(error_code as u64),
18372                                        flags: Some(last_stream_id as u64),
18373                                        ..Http2BridgeEvent::default()
18374                                    },
18375                                );
18376                                let _ = respond_to.send(Ok(Value::Null));
18377                            }
18378                            Http2SessionCommand::Close { respond_to, .. } => {
18379                                let _ = respond_to.send(Ok(Value::Null));
18380                                push_http2_session_event(
18381                                    &shared,
18382                                    session_id,
18383                                    Http2BridgeEvent {
18384                                        kind: String::from("sessionClose"),
18385                                        id: session_id,
18386                                        ..Http2BridgeEvent::default()
18387                                    },
18388                                );
18389                                remove_http2_session_resources(&shared, session_id);
18390                                break;
18391                            }
18392                            Http2SessionCommand::StreamWrite { stream_id, chunk, end_stream, respond_to } => {
18393                                let result = streams
18394                                    .lock()
18395                                    .expect("http2 client streams")
18396                                    .get_mut(&stream_id)
18397                                    .and_then(|stream| stream.send_stream.as_mut())
18398                                    .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 client stream {stream_id}")))
18399                                    .and_then(|stream| stream.send_data(Bytes::from(chunk), end_stream).map_err(|error| SidecarError::Execution(error.to_string())));
18400                                match result {
18401                                    Ok(()) => {
18402                                        if end_stream {
18403                                            streams.lock().expect("http2 client streams").remove(&stream_id);
18404                                        }
18405                                        let _ = respond_to.send(Ok(Value::Bool(true)));
18406                                    }
18407                                    Err(error) => {
18408                                        let _ = respond_to.send(Err(error.to_string()));
18409                                    }
18410                                }
18411                            }
18412                            Http2SessionCommand::StreamClose { stream_id, error_code, respond_to } => {
18413                                let mut streams = streams.lock().expect("http2 client streams");
18414                                let Some(mut state) = streams.remove(&stream_id) else {
18415                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 client stream {stream_id}")));
18416                                    continue;
18417                                };
18418                                if let Some(stream) = state.send_stream.as_mut() {
18419                                    stream.send_reset(http2_reason(error_code));
18420                                }
18421                                if let Ok(mut state) = shared.lock() {
18422                                    state.streams.remove(&stream_id);
18423                                }
18424                                push_http2_session_event(
18425                                    &shared,
18426                                    session_id,
18427                                    Http2BridgeEvent {
18428                                        kind: String::from("clientClose"),
18429                                        id: stream_id,
18430                                        extra_number: Some(u32::from(http2_reason(error_code)) as u64),
18431                                        ..Http2BridgeEvent::default()
18432                                    },
18433                                );
18434                                let _ = respond_to.send(Ok(Value::Null));
18435                            }
18436                            Http2SessionCommand::StreamRespond { respond_to, .. }
18437                            | Http2SessionCommand::StreamPush { respond_to, .. }
18438                            | Http2SessionCommand::StreamRespondWithFile { respond_to, .. } => {
18439                                let _ = respond_to.send(Err(String::from("HTTP/2 client streams cannot send server responses")));
18440                            }
18441                        }
18442                    }
18443                    else => break,
18444                }
18445            }
18446        });
18447    });
18448}
18449
18450fn spawn_http2_server_session(
18451    shared: Arc<Mutex<crate::state::Http2SharedState>>,
18452    server_id: u64,
18453    session_id: u64,
18454    stream: TcpStream,
18455    tls: Option<JavascriptTlsBridgeOptions>,
18456    snapshot: Arc<Mutex<Http2SessionSnapshot>>,
18457    mut command_rx: UnboundedReceiver<Http2SessionCommand>,
18458) {
18459    thread::spawn(move || {
18460        let runtime = match TokioRuntimeBuilder::new_current_thread()
18461            .enable_all()
18462            .build()
18463        {
18464            Ok(runtime) => runtime,
18465            Err(error) => {
18466                push_http2_server_event(
18467                    &shared,
18468                    server_id,
18469                    Http2BridgeEvent {
18470                        kind: String::from("serverStreamError"),
18471                        id: session_id,
18472                        data: Some(http2_error_payload(error.to_string())),
18473                        ..Http2BridgeEvent::default()
18474                    },
18475                );
18476                remove_http2_session_resources(&shared, session_id);
18477                return;
18478            }
18479        };
18480
18481        runtime.block_on(async move {
18482            if let Err(error) = stream.set_nonblocking(true) {
18483                push_http2_server_event(
18484                    &shared,
18485                    server_id,
18486                    Http2BridgeEvent {
18487                        kind: String::from("serverStreamError"),
18488                        id: session_id,
18489                        data: Some(http2_error_payload(error.to_string())),
18490                        ..Http2BridgeEvent::default()
18491                    },
18492                );
18493                remove_http2_session_resources(&shared, session_id);
18494                return;
18495            }
18496            let stream = match tokio::net::TcpStream::from_std(stream) {
18497                Ok(stream) => stream,
18498                Err(error) => {
18499                    push_http2_server_event(
18500                        &shared,
18501                        server_id,
18502                        Http2BridgeEvent {
18503                            kind: String::from("serverStreamError"),
18504                            id: session_id,
18505                            data: Some(http2_error_payload(error.to_string())),
18506                            ..Http2BridgeEvent::default()
18507                        },
18508                    );
18509                    remove_http2_session_resources(&shared, session_id);
18510                    return;
18511                }
18512            };
18513            let local_addr = match stream.local_addr() {
18514                Ok(addr) => addr,
18515                Err(error) => {
18516                    push_http2_server_event(
18517                        &shared,
18518                        server_id,
18519                        Http2BridgeEvent {
18520                            kind: String::from("serverStreamError"),
18521                            id: session_id,
18522                            data: Some(http2_error_payload(error.to_string())),
18523                            ..Http2BridgeEvent::default()
18524                        },
18525                    );
18526                    remove_http2_session_resources(&shared, session_id);
18527                    return;
18528                }
18529            };
18530            let remote_addr = match stream.peer_addr() {
18531                Ok(addr) => addr,
18532                Err(error) => {
18533                    push_http2_server_event(
18534                        &shared,
18535                        server_id,
18536                        Http2BridgeEvent {
18537                            kind: String::from("serverStreamError"),
18538                            id: session_id,
18539                            data: Some(http2_error_payload(error.to_string())),
18540                            ..Http2BridgeEvent::default()
18541                        },
18542                    );
18543                    remove_http2_session_resources(&shared, session_id);
18544                    return;
18545                }
18546            };
18547            {
18548                let mut snapshot_guard = snapshot.lock().expect("http2 snapshot lock");
18549                snapshot_guard.socket = http2_socket_snapshot(local_addr, remote_addr);
18550                if tls.is_some() {
18551                    snapshot_guard.encrypted = true;
18552                    snapshot_guard.alpn_protocol = Some(String::from("h2"));
18553                    snapshot_guard.socket.encrypted = true;
18554                    snapshot_guard.socket.alpn_protocol = Some(String::from("h2"));
18555                }
18556                snapshot_guard.state = http2_runtime_snapshot();
18557            }
18558            if let Ok(snapshot_json) =
18559                http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())
18560            {
18561                push_http2_server_event(
18562                    &shared,
18563                    server_id,
18564                    Http2BridgeEvent {
18565                        kind: String::from(if tls.is_some() {
18566                            "serverSecureConnection"
18567                        } else {
18568                            "serverConnection"
18569                        }),
18570                        id: server_id,
18571                        data: Some(serde_json::to_string(&http2_socket_snapshot(local_addr, remote_addr)).unwrap_or_default()),
18572                        ..Http2BridgeEvent::default()
18573                    },
18574                );
18575                push_http2_server_event(
18576                    &shared,
18577                    server_id,
18578                    Http2BridgeEvent {
18579                        kind: String::from("serverSession"),
18580                        id: server_id,
18581                        data: Some(snapshot_json),
18582                        extra_number: Some(session_id),
18583                        ..Http2BridgeEvent::default()
18584                    },
18585                );
18586            }
18587
18588            let io: Pin<Box<dyn Http2AsyncIo>> = if let Some(options) = tls.as_ref() {
18589                let acceptor = match build_server_tls_config(options) {
18590                    Ok(config) => TlsAcceptor::from(Arc::new(config)),
18591                    Err(error) => {
18592                        push_http2_server_event(
18593                            &shared,
18594                            server_id,
18595                            Http2BridgeEvent {
18596                                kind: String::from("serverStreamError"),
18597                                id: session_id,
18598                                data: Some(http2_error_payload(error.to_string())),
18599                                ..Http2BridgeEvent::default()
18600                            },
18601                        );
18602                        remove_http2_session_resources(&shared, session_id);
18603                        return;
18604                    }
18605                };
18606                match acceptor.accept(stream).await {
18607                    Ok(tls_stream) => Box::pin(tls_stream),
18608                    Err(error) => {
18609                        push_http2_server_event(
18610                            &shared,
18611                            server_id,
18612                            Http2BridgeEvent {
18613                                kind: String::from("serverStreamError"),
18614                                id: session_id,
18615                                data: Some(http2_error_payload(error.to_string())),
18616                                ..Http2BridgeEvent::default()
18617                            },
18618                        );
18619                        remove_http2_session_resources(&shared, session_id);
18620                        return;
18621                    }
18622                }
18623            } else {
18624                Box::pin(stream)
18625            };
18626
18627            let mut connection = match server::handshake(io).await {
18628                Ok(connection) => connection,
18629                Err(error) => {
18630                    push_http2_server_event(
18631                        &shared,
18632                        server_id,
18633                        Http2BridgeEvent {
18634                            kind: String::from("serverStreamError"),
18635                            id: session_id,
18636                            data: Some(http2_error_payload(error.to_string())),
18637                            ..Http2BridgeEvent::default()
18638                        },
18639                    );
18640                    remove_http2_session_resources(&shared, session_id);
18641                    return;
18642                }
18643            };
18644
18645            let streams: Arc<Mutex<BTreeMap<u64, ServerHttp2StreamState>>> =
18646                Arc::new(Mutex::new(BTreeMap::new()));
18647
18648            loop {
18649                tokio::select! {
18650                    incoming = connection.accept() => {
18651                        match incoming {
18652                            Some(Ok((request, respond))) => {
18653                                let headers_json = match serialize_http2_request_headers(&request) {
18654                                    Ok(headers) => headers,
18655                                    Err(error) => {
18656                                        push_http2_server_event(
18657                                            &shared,
18658                                            server_id,
18659                                            Http2BridgeEvent {
18660                                                kind: String::from("serverStreamError"),
18661                                                id: server_id,
18662                                                data: Some(http2_error_payload(error.to_string())),
18663                                                ..Http2BridgeEvent::default()
18664                                            },
18665                                        );
18666                                        continue;
18667                                    }
18668                                };
18669                                let stream_id = {
18670                                    let mut state = shared.lock().expect("http2 shared state");
18671                                    let stream_id = next_http2_stream_id(&mut state);
18672                                    state.streams.insert(
18673                                        stream_id,
18674                                        ActiveHttp2Stream {
18675                                            session_id,
18676                                            paused: Arc::new(AtomicBool::new(false)),
18677                                        },
18678                                    );
18679                                    stream_id
18680                                };
18681                                streams.lock().expect("http2 server streams").insert(
18682                                    stream_id,
18683                                    ServerHttp2StreamState {
18684                                        send_response: Some(ServerHttp2Responder::Regular(respond)),
18685                                        send_stream: None,
18686                                    },
18687                                );
18688                                let snapshot_json = snapshot
18689                                    .lock()
18690                                    .ok()
18691                                    .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok());
18692                                push_http2_server_event(
18693                                    &shared,
18694                                    server_id,
18695                                    Http2BridgeEvent {
18696                                        kind: String::from("serverStream"),
18697                                        id: server_id,
18698                                        data: Some(stream_id.to_string()),
18699                                        extra: snapshot_json,
18700                                        extra_number: Some(session_id),
18701                                        extra_headers: Some(headers_json),
18702                                        flags: Some(0),
18703                                    },
18704                                );
18705                                let shared_clone = Arc::clone(&shared);
18706                                tokio::spawn(async move {
18707                                    let mut body = request.into_body();
18708                                    while let Some(chunk) = body.data().await {
18709                                        match chunk {
18710                                            Ok(bytes) => {
18711                                                let paused = {
18712                                                    let state = shared_clone.lock().expect("http2 shared state");
18713                                                    state.streams.get(&stream_id).map(|stream| Arc::clone(&stream.paused))
18714                                                };
18715                                                if let Some(paused) = paused {
18716                                                    while paused.load(Ordering::SeqCst) {
18717                                                        tokio::time::sleep(HTTP2_POLL_DELAY).await;
18718                                                    }
18719                                                }
18720                                                let _ = body.flow_control().release_capacity(bytes.len());
18721                                                push_http2_server_event(
18722                                                    &shared_clone,
18723                                                    server_id,
18724                                                    Http2BridgeEvent {
18725                                                        kind: String::from("serverStreamData"),
18726                                                        id: stream_id,
18727                                                        data: Some(base64::engine::general_purpose::STANDARD.encode(bytes)),
18728                                                        ..Http2BridgeEvent::default()
18729                                                    },
18730                                                );
18731                                            }
18732                                            Err(error) => {
18733                                                push_http2_server_event(
18734                                                    &shared_clone,
18735                                                    server_id,
18736                                                    Http2BridgeEvent {
18737                                                        kind: String::from("serverStreamError"),
18738                                                        id: stream_id,
18739                                                        data: Some(http2_error_payload(error.to_string())),
18740                                                        ..Http2BridgeEvent::default()
18741                                                    },
18742                                                );
18743                                                break;
18744                                            }
18745                                        }
18746                                    }
18747                                    push_http2_server_event(
18748                                        &shared_clone,
18749                                        server_id,
18750                                        Http2BridgeEvent {
18751                                            kind: String::from("serverStreamEnd"),
18752                                            id: stream_id,
18753                                            ..Http2BridgeEvent::default()
18754                                        },
18755                                    );
18756                                });
18757                            }
18758                            Some(Err(error)) => {
18759                                push_http2_server_event(
18760                                    &shared,
18761                                    server_id,
18762                                    Http2BridgeEvent {
18763                                        kind: String::from("serverStreamError"),
18764                                        id: server_id,
18765                                        data: Some(http2_error_payload(error.to_string())),
18766                                        ..Http2BridgeEvent::default()
18767                                    },
18768                                );
18769                                break;
18770                            }
18771                            None => {
18772                                push_http2_server_event(
18773                                    &shared,
18774                                    server_id,
18775                                    Http2BridgeEvent {
18776                                        kind: String::from("sessionClose"),
18777                                        id: session_id,
18778                                        ..Http2BridgeEvent::default()
18779                                    },
18780                                );
18781                                remove_http2_session_resources(&shared, session_id);
18782                                break;
18783                            }
18784                        }
18785                    }
18786                    Some(command) = command_rx.recv() => {
18787                        match command {
18788                            Http2SessionCommand::Settings { settings_json, respond_to } => {
18789                                let settings = serde_json::from_str::<BTreeMap<String, Value>>(&settings_json)
18790                                    .unwrap_or_default();
18791                                if let Some(initial_window_size) = settings
18792                                    .get("initialWindowSize")
18793                                    .and_then(Value::as_u64)
18794                                {
18795                                    let _ = connection.set_initial_window_size(initial_window_size as u32);
18796                                }
18797                                {
18798                                    let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18799                                    snapshot.local_settings = http2_settings_from_value(&settings);
18800                                }
18801                                if let Ok(headers_json) = serde_json::to_string(&settings) {
18802                                    push_http2_session_event(
18803                                        &shared,
18804                                        session_id,
18805                                        Http2BridgeEvent {
18806                                            kind: String::from("sessionLocalSettings"),
18807                                            id: session_id,
18808                                            data: Some(headers_json),
18809                                            ..Http2BridgeEvent::default()
18810                                        },
18811                                    );
18812                                }
18813                                let _ = respond_to.send(Ok(Value::Null));
18814                            }
18815                            Http2SessionCommand::SetLocalWindowSize { size, respond_to } => {
18816                                connection.set_target_window_size(size);
18817                                {
18818                                    let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18819                                    snapshot.state.local_window_size = size;
18820                                    snapshot.state.effective_local_window_size = size;
18821                                }
18822                                let value = snapshot
18823                                    .lock()
18824                                    .ok()
18825                                    .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok())
18826                                    .map(Value::String)
18827                                    .unwrap_or(Value::Null);
18828                                let _ = respond_to.send(Ok(value));
18829                            }
18830                            Http2SessionCommand::Goaway { error_code, last_stream_id, opaque_data, respond_to } => {
18831                                connection.abrupt_shutdown(http2_reason(Some(error_code)));
18832                                push_http2_session_event(
18833                                    &shared,
18834                                    session_id,
18835                                    Http2BridgeEvent {
18836                                        kind: String::from("sessionGoaway"),
18837                                        id: session_id,
18838                                        data: opaque_data.map(|value| {
18839                                            base64::engine::general_purpose::STANDARD.encode(value)
18840                                        }),
18841                                        extra_number: Some(error_code as u64),
18842                                        flags: Some(last_stream_id as u64),
18843                                        ..Http2BridgeEvent::default()
18844                                    },
18845                                );
18846                                let _ = respond_to.send(Ok(Value::Null));
18847                            }
18848                            Http2SessionCommand::Close { abrupt, respond_to } => {
18849                                if abrupt {
18850                                    connection.abrupt_shutdown(Reason::NO_ERROR);
18851                                } else {
18852                                    connection.graceful_shutdown();
18853                                }
18854                                let _ = respond_to.send(Ok(Value::Null));
18855                                push_http2_session_event(
18856                                    &shared,
18857                                    session_id,
18858                                    Http2BridgeEvent {
18859                                        kind: String::from("sessionClose"),
18860                                        id: session_id,
18861                                        ..Http2BridgeEvent::default()
18862                                    },
18863                                );
18864                                remove_http2_session_resources(&shared, session_id);
18865                                break;
18866                            }
18867                            Http2SessionCommand::StreamRespond { stream_id, headers_json, respond_to } => {
18868                                let response = match build_http2_response(&headers_json) {
18869                                    Ok(response) => response,
18870                                    Err(error) => {
18871                                        let _ = respond_to.send(Err(error.to_string()));
18872                                        continue;
18873                                    }
18874                                };
18875                                let mut streams = streams.lock().expect("http2 server streams");
18876                                let Some(state) = streams.get_mut(&stream_id) else {
18877                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
18878                                    continue;
18879                                };
18880                                let Some(send_response) = state.send_response.as_mut() else {
18881                                    let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} already responded")));
18882                                    continue;
18883                                };
18884                                match match send_response {
18885                                    ServerHttp2Responder::Regular(send_response) => {
18886                                        send_response.send_response(response, false)
18887                                    }
18888                                    ServerHttp2Responder::Pushed(send_response) => {
18889                                        send_response.send_response(response, false)
18890                                    }
18891                                } {
18892                                    Ok(send_stream) => {
18893                                        state.send_stream = Some(send_stream);
18894                                        state.send_response = None;
18895                                        let _ = respond_to.send(Ok(Value::Null));
18896                                    }
18897                                    Err(error) => {
18898                                        let _ = respond_to.send(Err(error.to_string()));
18899                                    }
18900                                }
18901                            }
18902                            Http2SessionCommand::StreamPush { stream_id, headers_json, respond_to } => {
18903                                let request = match build_http2_request(&headers_json) {
18904                                    Ok(request) => request,
18905                                    Err(error) => {
18906                                        let _ = respond_to.send(Err(error.to_string()));
18907                                        continue;
18908                                    }
18909                                };
18910                                let mut streams_guard = streams.lock().expect("http2 server streams");
18911                                let Some(state) = streams_guard.get_mut(&stream_id) else {
18912                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
18913                                    continue;
18914                                };
18915                                let Some(send_response) = state.send_response.as_mut() else {
18916                                    let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} cannot push after responding")));
18917                                    continue;
18918                                };
18919                                let ServerHttp2Responder::Regular(send_response) = send_response else {
18920                                    let _ = respond_to.send(Err(format!("HTTP/2 pushed stream {stream_id} cannot create nested push promises")));
18921                                    continue;
18922                                };
18923                                match send_response.push_request(request) {
18924                                    Ok(pushed) => {
18925                                        let pushed_stream_id = {
18926                                            let mut state = shared.lock().expect("http2 shared state");
18927                                            let pushed_stream_id = next_http2_stream_id(&mut state);
18928                                            state.streams.insert(
18929                                                pushed_stream_id,
18930                                                ActiveHttp2Stream {
18931                                                    session_id,
18932                                                    paused: Arc::new(AtomicBool::new(false)),
18933                                                },
18934                                            );
18935                                            pushed_stream_id
18936                                        };
18937                                        streams_guard.insert(
18938                                            pushed_stream_id,
18939                                            ServerHttp2StreamState {
18940                                                send_response: Some(ServerHttp2Responder::Pushed(pushed)),
18941                                                send_stream: None,
18942                                            },
18943                                        );
18944                                        let _ = respond_to.send(Ok(json!({
18945                                            "streamId": pushed_stream_id,
18946                                            "headers": headers_json,
18947                                        }).to_string().into()));
18948                                    }
18949                                    Err(error) => {
18950                                        let _ = respond_to.send(Err(error.to_string()));
18951                                    }
18952                                }
18953                            }
18954                            Http2SessionCommand::StreamWrite { stream_id, chunk, end_stream, respond_to } => {
18955                                let mut streams = streams.lock().expect("http2 server streams");
18956                                let Some(state) = streams.get_mut(&stream_id) else {
18957                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
18958                                    continue;
18959                                };
18960                                let Some(send_stream) = state.send_stream.as_mut() else {
18961                                    let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} has not sent response headers")));
18962                                    continue;
18963                                };
18964                                match send_stream.send_data(Bytes::from(chunk), end_stream) {
18965                                    Ok(()) => {
18966                                        if end_stream {
18967                                            streams.remove(&stream_id);
18968                                            if let Ok(mut state) = shared.lock() {
18969                                                state.streams.remove(&stream_id);
18970                                            }
18971                                            push_http2_server_event(
18972                                                &shared,
18973                                                server_id,
18974                                                Http2BridgeEvent {
18975                                                    kind: String::from("serverStreamClose"),
18976                                                    id: stream_id,
18977                                                    extra_number: Some(0),
18978                                                    ..Http2BridgeEvent::default()
18979                                                },
18980                                            );
18981                                        }
18982                                        let _ = respond_to.send(Ok(Value::Bool(true)));
18983                                    }
18984                                    Err(error) => {
18985                                        let _ = respond_to.send(Err(error.to_string()));
18986                                    }
18987                                }
18988                            }
18989                            Http2SessionCommand::StreamClose { stream_id, error_code, respond_to } => {
18990                                let mut streams_guard = streams.lock().expect("http2 server streams");
18991                                let Some(mut state) = streams_guard.remove(&stream_id) else {
18992                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
18993                                    continue;
18994                                };
18995                                let reason = http2_reason(error_code);
18996                                if let Some(send_stream) = state.send_stream.as_mut() {
18997                                    send_stream.send_reset(reason);
18998                                }
18999                                if let Some(send_response) = state.send_response.as_mut() {
19000                                    match send_response {
19001                                        ServerHttp2Responder::Regular(send_response) => {
19002                                            send_response.send_reset(reason)
19003                                        }
19004                                        ServerHttp2Responder::Pushed(send_response) => {
19005                                            send_response.send_reset(reason)
19006                                        }
19007                                    }
19008                                }
19009                                if let Ok(mut shared_guard) = shared.lock() {
19010                                    shared_guard.streams.remove(&stream_id);
19011                                }
19012                                push_http2_server_event(
19013                                    &shared,
19014                                    server_id,
19015                                    Http2BridgeEvent {
19016                                        kind: String::from("serverStreamClose"),
19017                                        id: stream_id,
19018                                        extra_number: Some(u32::from(reason) as u64),
19019                                        ..Http2BridgeEvent::default()
19020                                    },
19021                                );
19022                                let _ = respond_to.send(Ok(Value::Null));
19023                            }
19024                            Http2SessionCommand::StreamRespondWithFile { stream_id, body, headers_json, options_json, respond_to } => {
19025                                let options: JavascriptHttp2FileResponseOptions =
19026                                    serde_json::from_str(&options_json).unwrap_or_default();
19027                                let response = match build_http2_response(&headers_json) {
19028                                    Ok(response) => response,
19029                                    Err(error) => {
19030                                        let _ = respond_to.send(Err(error.to_string()));
19031                                        continue;
19032                                    }
19033                                };
19034                                let offset = usize::try_from(options.offset.unwrap_or_default()).unwrap_or(0);
19035                                let body = if offset >= body.len() {
19036                                    Vec::new()
19037                                } else {
19038                                    let body = &body[offset..];
19039                                    match options.length {
19040                                        Some(length) if length >= 0 => {
19041                                            body[..body.len().min(length as usize)].to_vec()
19042                                        }
19043                                        _ => body.to_vec(),
19044                                    }
19045                                };
19046                                let mut streams_guard = streams.lock().expect("http2 server streams");
19047                                let Some(state) = streams_guard.get_mut(&stream_id) else {
19048                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
19049                                    continue;
19050                                };
19051                                let Some(send_response) = state.send_response.as_mut() else {
19052                                    let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} already responded")));
19053                                    continue;
19054                                };
19055                                match match send_response {
19056                                    ServerHttp2Responder::Regular(send_response) => {
19057                                        send_response.send_response(response, body.is_empty())
19058                                    }
19059                                    ServerHttp2Responder::Pushed(send_response) => {
19060                                        send_response.send_response(response, body.is_empty())
19061                                    }
19062                                } {
19063                                    Ok(mut send_stream) => {
19064                                        state.send_response = None;
19065                                        if body.is_empty() {
19066                                            streams_guard.remove(&stream_id);
19067                                            if let Ok(mut shared_guard) = shared.lock() {
19068                                                shared_guard.streams.remove(&stream_id);
19069                                            }
19070                                        } else {
19071                                            if let Err(error) = send_stream.send_data(Bytes::from(body), true) {
19072                                                let _ = respond_to.send(Err(error.to_string()));
19073                                                continue;
19074                                            }
19075                                            streams_guard.remove(&stream_id);
19076                                            if let Ok(mut shared_guard) = shared.lock() {
19077                                                shared_guard.streams.remove(&stream_id);
19078                                            }
19079                                        }
19080                                        push_http2_server_event(
19081                                            &shared,
19082                                            server_id,
19083                                            Http2BridgeEvent {
19084                                                kind: String::from("serverStreamClose"),
19085                                                id: stream_id,
19086                                                extra_number: Some(0),
19087                                                ..Http2BridgeEvent::default()
19088                                            },
19089                                        );
19090                                        let _ = respond_to.send(Ok(Value::Null));
19091                                    }
19092                                    Err(error) => {
19093                                        let _ = respond_to.send(Err(error.to_string()));
19094                                    }
19095                                }
19096                            }
19097                            Http2SessionCommand::Request { respond_to, .. } => {
19098                                let _ = respond_to.send(Err(String::from("HTTP/2 server sessions cannot initiate client requests")));
19099                            }
19100                        }
19101                    }
19102                    else => break,
19103                }
19104            }
19105        });
19106    });
19107}
19108
19109fn spawn_http2_server_accept_loop(
19110    shared: Arc<Mutex<crate::state::Http2SharedState>>,
19111    server_id: u64,
19112    listener: TcpListener,
19113) {
19114    thread::spawn(move || {
19115        let listener = listener;
19116        loop {
19117            let closed = shared
19118                .lock()
19119                .ok()
19120                .and_then(|state| {
19121                    state
19122                        .servers
19123                        .get(&server_id)
19124                        .map(|server| server.closed.load(Ordering::SeqCst))
19125                })
19126                .unwrap_or(true);
19127            if closed {
19128                break;
19129            }
19130            match listener.accept() {
19131                Ok((stream, _)) => {
19132                    let (command_tx, command_rx) = unbounded_channel();
19133                    let (guest_local_addr, secure, tls) = {
19134                        let state = shared.lock().expect("http2 shared state");
19135                        let server = state.servers.get(&server_id).expect("http2 server state");
19136                        (server.guest_local_addr, server.secure, server.tls.clone())
19137                    };
19138                    let (local_addr, remote_addr) = match (stream.local_addr(), stream.peer_addr())
19139                    {
19140                        (Ok(local_addr), Ok(remote_addr)) => (local_addr, remote_addr),
19141                        _ => continue,
19142                    };
19143                    let session_snapshot = Arc::new(Mutex::new(Http2SessionSnapshot {
19144                        encrypted: secure,
19145                        alpn_protocol: Some(if secure {
19146                            String::from("h2")
19147                        } else {
19148                            String::from("h2c")
19149                        }),
19150                        local_settings: BTreeMap::new(),
19151                        remote_settings: BTreeMap::new(),
19152                        state: http2_runtime_snapshot(),
19153                        socket: Http2SocketSnapshot {
19154                            local_address: Some(guest_local_addr.ip().to_string()),
19155                            local_port: Some(guest_local_addr.port()),
19156                            local_family: Some(socket_addr_family(&guest_local_addr).to_string()),
19157                            remote_address: Some(remote_addr.ip().to_string()),
19158                            remote_port: Some(remote_addr.port()),
19159                            remote_family: Some(socket_addr_family(&remote_addr).to_string()),
19160                            ..http2_socket_snapshot(local_addr, remote_addr)
19161                        },
19162                        ..Http2SessionSnapshot::default()
19163                    }));
19164                    let session_id = {
19165                        let mut state = shared.lock().expect("http2 shared state");
19166                        let session_id = next_http2_session_id(&mut state);
19167                        state
19168                            .sessions
19169                            .insert(session_id, ActiveHttp2Session { command_tx });
19170                        session_id
19171                    };
19172                    spawn_http2_server_session(
19173                        Arc::clone(&shared),
19174                        server_id,
19175                        session_id,
19176                        stream,
19177                        tls,
19178                        session_snapshot,
19179                        command_rx,
19180                    );
19181                }
19182                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
19183                    thread::sleep(HTTP2_POLL_DELAY);
19184                }
19185                Err(error) => {
19186                    push_http2_server_event(
19187                        &shared,
19188                        server_id,
19189                        Http2BridgeEvent {
19190                            kind: String::from("serverStreamError"),
19191                            id: server_id,
19192                            data: Some(http2_error_payload(error.to_string())),
19193                            ..Http2BridgeEvent::default()
19194                        },
19195                    );
19196                    thread::sleep(HTTP2_POLL_DELAY);
19197                }
19198            }
19199        }
19200    });
19201}
19202
19203fn send_http2_command(
19204    session: &ActiveHttp2Session,
19205    command: impl FnOnce(Sender<Result<Value, String>>) -> Http2SessionCommand,
19206) -> Result<Value, SidecarError> {
19207    let (respond_to, response_rx) = mpsc::channel();
19208    session.command_tx.send(command(respond_to)).map_err(|_| {
19209        SidecarError::InvalidState(String::from("HTTP/2 session command channel closed"))
19210    })?;
19211    response_rx
19212        .recv_timeout(Duration::from_secs(30))
19213        .map_err(|_| {
19214            SidecarError::Execution(String::from("timed out waiting for HTTP/2 session command"))
19215        })?
19216        .map_err(SidecarError::Execution)
19217}
19218
19219fn parse_http2_server_listen_payload(
19220    request: &JavascriptSyncRpcRequest,
19221) -> Result<JavascriptHttp2ServerListenRequest, SidecarError> {
19222    let payload_json =
19223        javascript_sync_rpc_arg_str(&request.args, 0, "net.http2_server_listen payload")?;
19224    serde_json::from_str(payload_json).map_err(|error| {
19225        SidecarError::InvalidState(format!(
19226            "net.http2_server_listen payload must be valid JSON: {error}"
19227        ))
19228    })
19229}
19230
19231fn parse_http2_connect_payload(
19232    request: &JavascriptSyncRpcRequest,
19233) -> Result<JavascriptHttp2SessionConnectRequest, SidecarError> {
19234    let payload_json =
19235        javascript_sync_rpc_arg_str(&request.args, 0, "net.http2_session_connect payload")?;
19236    serde_json::from_str(payload_json).map_err(|error| {
19237        SidecarError::InvalidState(format!(
19238            "net.http2_session_connect payload must be valid JSON: {error}"
19239        ))
19240    })
19241}
19242
19243fn http2_session_for_id(
19244    process: &ActiveProcess,
19245    session_id: u64,
19246) -> Result<ActiveHttp2Session, SidecarError> {
19247    let shared = process
19248        .http2
19249        .shared
19250        .lock()
19251        .map_err(|_| SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned")))?;
19252    shared
19253        .sessions
19254        .get(&session_id)
19255        .cloned()
19256        .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 session {session_id}")))
19257}
19258
19259fn http2_stream_for_id(
19260    process: &ActiveProcess,
19261    stream_id: u64,
19262) -> Result<ActiveHttp2Stream, SidecarError> {
19263    let shared = process
19264        .http2
19265        .shared
19266        .lock()
19267        .map_err(|_| SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned")))?;
19268    shared
19269        .streams
19270        .get(&stream_id)
19271        .cloned()
19272        .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 stream {stream_id}")))
19273}
19274
19275fn service_javascript_http2_sync_rpc<B>(
19276    request: JavascriptHttp2SyncRpcServiceRequest<'_, B>,
19277) -> Result<Value, SidecarError>
19278where
19279    B: NativeSidecarBridge + Send + 'static,
19280    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19281{
19282    let JavascriptHttp2SyncRpcServiceRequest {
19283        bridge,
19284        kernel,
19285        vm_id,
19286        dns,
19287        socket_paths,
19288        process,
19289        sync_request: request,
19290        resource_limits,
19291        network_counts,
19292    } = request;
19293    match request.method.as_str() {
19294        "net.http2_server_listen" => {
19295            check_network_resource_limit(
19296                resource_limits.max_sockets,
19297                network_counts.sockets,
19298                1,
19299                "socket",
19300            )?;
19301            let payload = parse_http2_server_listen_payload(request)?;
19302            let (family, bind_host, guest_host) =
19303                normalize_tcp_listen_host(payload.host.as_deref())?;
19304            let requested_port = payload.port.unwrap_or(0);
19305            bridge.require_network_access(
19306                vm_id,
19307                NetworkOperation::Listen,
19308                format_tcp_resource(bind_host, requested_port),
19309            )?;
19310            let port = allocate_guest_listen_port(
19311                requested_port,
19312                family,
19313                &socket_paths.used_tcp_guest_ports,
19314                socket_paths.listen_policy,
19315            )?;
19316            let mut listener =
19317                ActiveTcpListener::bind(bind_host, guest_host, port, payload.backlog)?;
19318            let guest_local_addr = listener.guest_local_addr();
19319            let closed = Arc::new(AtomicBool::new(false));
19320            {
19321                let mut state = process.http2.shared.lock().map_err(|_| {
19322                    SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19323                })?;
19324                state.servers.insert(
19325                    payload.server_id,
19326                    ActiveHttp2Server {
19327                        actual_local_addr: listener.local_addr(),
19328                        guest_local_addr,
19329                        secure: payload.secure,
19330                        tls: payload.tls.clone().map(|mut tls| {
19331                            tls.is_server = payload.secure;
19332                            if payload.secure && tls.alpn_protocols.is_none() {
19333                                tls.alpn_protocols = Some(vec![String::from("h2")]);
19334                            }
19335                            tls
19336                        }),
19337                        closed: Arc::clone(&closed),
19338                    },
19339                );
19340                state.server_events.entry(payload.server_id).or_default();
19341            }
19342            spawn_http2_server_accept_loop(
19343                Arc::clone(&process.http2.shared),
19344                payload.server_id,
19345                listener.listener.take().ok_or_else(|| {
19346                    SidecarError::InvalidState(String::from(
19347                        "HTTP/2 listener missing host TCP socket",
19348                    ))
19349                })?,
19350            );
19351            javascript_net_json_string(
19352                json!({
19353                    "address": {
19354                        "address": guest_local_addr.ip().to_string(),
19355                        "family": socket_addr_family(&guest_local_addr),
19356                        "port": guest_local_addr.port(),
19357                    }
19358                }),
19359                "net.http2_server_listen",
19360            )
19361        }
19362        "net.http2_server_poll" => {
19363            let server_id =
19364                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_poll server id")?;
19365            let wait_ms = javascript_sync_rpc_arg_u64_optional(
19366                &request.args,
19367                1,
19368                "net.http2_server_poll wait ms",
19369            )?
19370            .unwrap_or_default();
19371            match wait_for_http2_event(&process.http2.shared, server_id, true, wait_ms) {
19372                Some(event) => http2_event_value(&event),
19373                None => Ok(Value::Null),
19374            }
19375        }
19376        "net.http2_server_wait" => {
19377            let server_id =
19378                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_wait server id")?;
19379            dispatch_http2_wait_loop(process, server_id, true)
19380        }
19381        "net.http2_server_close" => {
19382            let server_id =
19383                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_close server id")?;
19384            let server = {
19385                let mut state = process.http2.shared.lock().map_err(|_| {
19386                    SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19387                })?;
19388                state.servers.remove(&server_id)
19389            }
19390            .ok_or_else(|| {
19391                SidecarError::InvalidState(format!("unknown HTTP/2 server {server_id}"))
19392            })?;
19393            server.closed.store(true, Ordering::SeqCst);
19394            push_http2_server_event(
19395                &process.http2.shared,
19396                server_id,
19397                Http2BridgeEvent {
19398                    kind: String::from("serverClose"),
19399                    id: server_id,
19400                    ..Http2BridgeEvent::default()
19401                },
19402            );
19403            Ok(Value::Null)
19404        }
19405        "net.http2_server_respond" => {
19406            let server_id = javascript_sync_rpc_arg_u64(
19407                &request.args,
19408                0,
19409                "net.http2_server_respond server id",
19410            )?;
19411            let request_id = javascript_sync_rpc_arg_u64(
19412                &request.args,
19413                1,
19414                "net.http2_server_respond request id",
19415            )?;
19416            let response_json =
19417                javascript_sync_rpc_arg_str(&request.args, 2, "net.http2_server_respond payload")?;
19418            ensure_vm_fetch_response_within_limit(
19419                response_json,
19420                "net.http2_server_respond",
19421                VM_FETCH_BUFFER_LIMIT_BYTES,
19422            )?;
19423            serde_json::from_str::<Value>(response_json).map_err(|error| {
19424                SidecarError::Execution(format!(
19425                    "net.http2_server_respond payload must be valid JSON: {error}"
19426                ))
19427            })?;
19428            let Some(pending) = process
19429                .pending_http_requests
19430                .get_mut(&(server_id, request_id))
19431            else {
19432                return Err(SidecarError::InvalidState(format!(
19433                    "unknown pending HTTP/2 request {request_id} for server {server_id}"
19434                )));
19435            };
19436            *pending = Some(response_json.to_owned());
19437            Ok(Value::Bool(true))
19438        }
19439        "net.http2_session_connect" => {
19440            check_network_resource_limit(
19441                resource_limits.max_sockets,
19442                network_counts.sockets,
19443                1,
19444                "socket",
19445            )?;
19446            check_network_resource_limit(
19447                resource_limits.max_connections,
19448                network_counts.connections,
19449                1,
19450                "connection",
19451            )?;
19452            let payload = parse_http2_connect_payload(request)?;
19453            let authority = payload.authority.clone().unwrap_or_else(|| {
19454                format!(
19455                    "{}://{}:{}",
19456                    payload.protocol.as_deref().unwrap_or("http"),
19457                    payload.host.as_deref().unwrap_or("localhost"),
19458                    payload.port.unwrap_or(80)
19459                )
19460            });
19461            let url = Url::parse(&authority).map_err(|error| {
19462                SidecarError::InvalidState(format!(
19463                    "invalid HTTP/2 authority {authority:?}: {error}"
19464                ))
19465            })?;
19466            let secure = url.scheme() == "https" || payload.protocol.as_deref() == Some("https:");
19467            let host = payload
19468                .host
19469                .as_deref()
19470                .or_else(|| url.host_str())
19471                .unwrap_or("localhost");
19472            let port = payload.port.or_else(|| url.port()).unwrap_or(80);
19473            bridge.require_network_access(
19474                vm_id,
19475                NetworkOperation::Http,
19476                format_tcp_resource(host, port),
19477            )?;
19478            let resolved = {
19479                let shared = process.http2.shared.lock().map_err(|_| {
19480                    SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19481                })?;
19482                shared
19483                    .servers
19484                    .values()
19485                    .find(|server| {
19486                        is_loopback_request_host(host) && server.guest_local_addr.port() == port
19487                    })
19488                    .map(|server| ResolvedTcpConnectAddr {
19489                        actual_addr: server.actual_local_addr,
19490                        guest_remote_addr: server.guest_local_addr,
19491                        use_kernel_loopback: false,
19492                    })
19493            };
19494            let resolved = match resolved {
19495                Some(resolved) => resolved,
19496                None => {
19497                    resolve_tcp_connect_addr(bridge, kernel, vm_id, dns, host, port, socket_paths)?
19498                }
19499            };
19500            let (command_tx, command_rx) = unbounded_channel();
19501            let snapshot = Arc::new(Mutex::new(Http2SessionSnapshot {
19502                encrypted: secure,
19503                alpn_protocol: Some(String::from(if secure { "h2" } else { "h2c" })),
19504                local_settings: http2_settings_from_value(&payload.settings),
19505                remote_settings: BTreeMap::new(),
19506                state: http2_runtime_snapshot(),
19507                socket: Http2SocketSnapshot {
19508                    encrypted: secure,
19509                    remote_address: Some(resolved.guest_remote_addr.ip().to_string()),
19510                    remote_port: Some(resolved.guest_remote_addr.port()),
19511                    remote_family: Some(
19512                        socket_addr_family(&resolved.guest_remote_addr).to_string(),
19513                    ),
19514                    servername: if secure {
19515                        payload
19516                            .tls
19517                            .as_ref()
19518                            .and_then(|tls| tls.servername.clone())
19519                            .or_else(|| Some(host.to_string()))
19520                    } else {
19521                        None
19522                    },
19523                    alpn_protocol: Some(String::from(if secure { "h2" } else { "h2c" })),
19524                    ..Http2SocketSnapshot::default()
19525                },
19526                ..Http2SessionSnapshot::default()
19527            }));
19528            let session_id = {
19529                let mut state = process.http2.shared.lock().map_err(|_| {
19530                    SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19531                })?;
19532                let session_id = next_http2_session_id(&mut state);
19533                state
19534                    .sessions
19535                    .insert(session_id, ActiveHttp2Session { command_tx });
19536                state.session_events.entry(session_id).or_default();
19537                session_id
19538            };
19539            spawn_http2_client_session(
19540                Arc::clone(&process.http2.shared),
19541                session_id,
19542                resolved.actual_addr,
19543                if secure {
19544                    Some(payload.tls.unwrap_or(JavascriptTlsBridgeOptions {
19545                        is_server: false,
19546                        servername: Some(host.to_string()),
19547                        alpn_protocols: Some(vec![String::from("h2")]),
19548                        ..JavascriptTlsBridgeOptions::default()
19549                    }))
19550                } else {
19551                    None
19552                },
19553                Arc::clone(&snapshot),
19554                command_rx,
19555            );
19556            let snapshot_json =
19557                http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())?;
19558            javascript_net_json_string(
19559                json!({
19560                    "sessionId": session_id,
19561                    "state": snapshot_json,
19562                }),
19563                "net.http2_session_connect",
19564            )
19565        }
19566        "net.http2_session_request" => {
19567            let session_id = javascript_sync_rpc_arg_u64(
19568                &request.args,
19569                0,
19570                "net.http2_session_request session id",
19571            )?;
19572            let headers_json =
19573                javascript_sync_rpc_arg_str(&request.args, 1, "net.http2_session_request headers")?;
19574            let options_json =
19575                javascript_sync_rpc_arg_str(&request.args, 2, "net.http2_session_request options")?;
19576            let session = http2_session_for_id(process, session_id)?;
19577            send_http2_command(&session, |respond_to| Http2SessionCommand::Request {
19578                headers_json: headers_json.to_owned(),
19579                options_json: options_json.to_owned(),
19580                respond_to,
19581            })
19582        }
19583        "net.http2_session_settings" => {
19584            let session_id = javascript_sync_rpc_arg_u64(
19585                &request.args,
19586                0,
19587                "net.http2_session_settings session id",
19588            )?;
19589            let settings_json = javascript_sync_rpc_arg_str(
19590                &request.args,
19591                1,
19592                "net.http2_session_settings settings",
19593            )?;
19594            let session = http2_session_for_id(process, session_id)?;
19595            send_http2_command(&session, |respond_to| Http2SessionCommand::Settings {
19596                settings_json: settings_json.to_owned(),
19597                respond_to,
19598            })
19599        }
19600        "net.http2_session_set_local_window_size" => {
19601            let session_id = javascript_sync_rpc_arg_u64(
19602                &request.args,
19603                0,
19604                "net.http2_session_set_local_window_size session id",
19605            )?;
19606            let window_size = javascript_sync_rpc_arg_u64(
19607                &request.args,
19608                1,
19609                "net.http2_session_set_local_window_size window size",
19610            )?;
19611            let session = http2_session_for_id(process, session_id)?;
19612            send_http2_command(&session, |respond_to| {
19613                Http2SessionCommand::SetLocalWindowSize {
19614                    size: window_size as u32,
19615                    respond_to,
19616                }
19617            })
19618        }
19619        "net.http2_session_goaway" => {
19620            let session_id = javascript_sync_rpc_arg_u64(
19621                &request.args,
19622                0,
19623                "net.http2_session_goaway session id",
19624            )?;
19625            let error_code = javascript_sync_rpc_arg_u64(
19626                &request.args,
19627                1,
19628                "net.http2_session_goaway error code",
19629            )?;
19630            let last_stream_id = javascript_sync_rpc_arg_u64(
19631                &request.args,
19632                2,
19633                "net.http2_session_goaway last stream id",
19634            )?;
19635            let opaque_data = request
19636                .args
19637                .get(3)
19638                .and_then(Value::as_str)
19639                .map(|value| {
19640                    base64::engine::general_purpose::STANDARD
19641                        .decode(value)
19642                        .map_err(|error| {
19643                            SidecarError::InvalidState(format!("invalid GOAWAY payload: {error}"))
19644                        })
19645                })
19646                .transpose()?;
19647            let session = http2_session_for_id(process, session_id)?;
19648            send_http2_command(&session, |respond_to| Http2SessionCommand::Goaway {
19649                error_code: error_code as u32,
19650                last_stream_id: last_stream_id as u32,
19651                opaque_data,
19652                respond_to,
19653            })
19654        }
19655        "net.http2_session_close" | "net.http2_session_destroy" => {
19656            let session_id = javascript_sync_rpc_arg_u64(
19657                &request.args,
19658                0,
19659                "net.http2_session_close session id",
19660            )?;
19661            let session = http2_session_for_id(process, session_id)?;
19662            send_http2_command(&session, |respond_to| Http2SessionCommand::Close {
19663                abrupt: request.method == "net.http2_session_destroy",
19664                respond_to,
19665            })
19666        }
19667        "net.http2_session_poll" => {
19668            let session_id =
19669                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_session_poll session id")?;
19670            let wait_ms = javascript_sync_rpc_arg_u64_optional(
19671                &request.args,
19672                1,
19673                "net.http2_session_poll wait ms",
19674            )?
19675            .unwrap_or_default();
19676            match wait_for_http2_event(&process.http2.shared, session_id, false, wait_ms) {
19677                Some(event) => http2_event_value(&event),
19678                None => Ok(Value::Null),
19679            }
19680        }
19681        "net.http2_session_wait" => {
19682            let session_id =
19683                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_session_wait session id")?;
19684            dispatch_http2_wait_loop(process, session_id, false)
19685        }
19686        "net.http2_stream_respond" => {
19687            let stream_id = javascript_sync_rpc_arg_u64(
19688                &request.args,
19689                0,
19690                "net.http2_stream_respond stream id",
19691            )?;
19692            let headers_json =
19693                javascript_sync_rpc_arg_str(&request.args, 1, "net.http2_stream_respond headers")?;
19694            let stream = http2_stream_for_id(process, stream_id)?;
19695            let session = http2_session_for_id(process, stream.session_id)?;
19696            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamRespond {
19697                stream_id,
19698                headers_json: headers_json.to_owned(),
19699                respond_to,
19700            })
19701        }
19702        "net.http2_stream_push_stream" => {
19703            let stream_id = javascript_sync_rpc_arg_u64(
19704                &request.args,
19705                0,
19706                "net.http2_stream_push_stream stream id",
19707            )?;
19708            let headers_json = javascript_sync_rpc_arg_str(
19709                &request.args,
19710                1,
19711                "net.http2_stream_push_stream headers",
19712            )?;
19713            let _options_json = javascript_sync_rpc_arg_str(
19714                &request.args,
19715                2,
19716                "net.http2_stream_push_stream options",
19717            )?;
19718            let stream = http2_stream_for_id(process, stream_id)?;
19719            let session = http2_session_for_id(process, stream.session_id)?;
19720            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamPush {
19721                stream_id,
19722                headers_json: headers_json.to_owned(),
19723                respond_to,
19724            })
19725        }
19726        "net.http2_stream_write" => {
19727            let stream_id =
19728                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_write stream id")?;
19729            let chunk =
19730                javascript_sync_rpc_base64_arg(&request.args, 1, "net.http2_stream_write data")?;
19731            let stream = http2_stream_for_id(process, stream_id)?;
19732            let session = http2_session_for_id(process, stream.session_id)?;
19733            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamWrite {
19734                stream_id,
19735                chunk,
19736                end_stream: false,
19737                respond_to,
19738            })
19739        }
19740        "net.http2_stream_end" => {
19741            let stream_id =
19742                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_end stream id")?;
19743            let chunk = request
19744                .args
19745                .get(1)
19746                .and_then(Value::as_str)
19747                .map(|value| {
19748                    base64::engine::general_purpose::STANDARD
19749                        .decode(value)
19750                        .map_err(|error| {
19751                            SidecarError::InvalidState(format!(
19752                                "invalid HTTP/2 stream payload: {error}"
19753                            ))
19754                        })
19755                })
19756                .transpose()?
19757                .unwrap_or_default();
19758            let stream = http2_stream_for_id(process, stream_id)?;
19759            let session = http2_session_for_id(process, stream.session_id)?;
19760            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamWrite {
19761                stream_id,
19762                chunk,
19763                end_stream: true,
19764                respond_to,
19765            })
19766        }
19767        "net.http2_stream_close" => {
19768            let stream_id =
19769                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_close stream id")?;
19770            let code = javascript_sync_rpc_arg_u64_optional(
19771                &request.args,
19772                1,
19773                "net.http2_stream_close error code",
19774            )?
19775            .map(|value| value as u32);
19776            let stream = http2_stream_for_id(process, stream_id)?;
19777            let session = http2_session_for_id(process, stream.session_id)?;
19778            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamClose {
19779                stream_id,
19780                error_code: code,
19781                respond_to,
19782            })
19783        }
19784        "net.http2_stream_pause" => {
19785            let stream_id =
19786                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_pause stream id")?;
19787            let stream = http2_stream_for_id(process, stream_id)?;
19788            stream.paused.store(true, Ordering::SeqCst);
19789            Ok(Value::Null)
19790        }
19791        "net.http2_stream_resume" => {
19792            let stream_id =
19793                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_resume stream id")?;
19794            let stream = http2_stream_for_id(process, stream_id)?;
19795            stream.paused.store(false, Ordering::SeqCst);
19796            Ok(Value::Null)
19797        }
19798        "net.http2_stream_respond_with_file" => {
19799            let stream_id = javascript_sync_rpc_arg_u64(
19800                &request.args,
19801                0,
19802                "net.http2_stream_respond_with_file stream id",
19803            )?;
19804            let path = javascript_sync_rpc_arg_str(
19805                &request.args,
19806                1,
19807                "net.http2_stream_respond_with_file path",
19808            )?;
19809            let headers_json = javascript_sync_rpc_arg_str(
19810                &request.args,
19811                2,
19812                "net.http2_stream_respond_with_file headers",
19813            )?;
19814            let options_json = javascript_sync_rpc_arg_str(
19815                &request.args,
19816                3,
19817                "net.http2_stream_respond_with_file options",
19818            )?;
19819            let stream = http2_stream_for_id(process, stream_id)?;
19820            let session = http2_session_for_id(process, stream.session_id)?;
19821            let guest_path = resolve_http2_file_response_guest_path(process, path);
19822            let body = kernel.read_file(&guest_path).map_err(kernel_error)?;
19823            send_http2_command(&session, |respond_to| {
19824                Http2SessionCommand::StreamRespondWithFile {
19825                    stream_id,
19826                    body,
19827                    headers_json: headers_json.to_owned(),
19828                    options_json: options_json.to_owned(),
19829                    respond_to,
19830                }
19831            })
19832        }
19833        other => Err(SidecarError::InvalidState(format!(
19834            "unsupported JavaScript HTTP/2 sync RPC method {other}"
19835        ))),
19836    }
19837}
19838
19839const JAVASCRIPT_NET_POLL_MAX_WAIT: Duration = Duration::from_millis(50);
19840const EXITED_PROCESS_SNAPSHOT_RETENTION: Duration = Duration::from_secs(2);
19841
19842fn resolve_http2_file_response_guest_path(process: &ActiveProcess, path: &str) -> String {
19843    if Path::new(path).is_absolute() {
19844        normalize_path(path)
19845    } else {
19846        normalize_path(&format!("{}/{}", process.guest_cwd, path))
19847    }
19848}
19849
19850pub(crate) fn clamp_javascript_net_poll_wait(wait_ms: u64) -> Duration {
19851    // WASM net.poll runs on the sidecar's sync-RPC main thread. Guest-controlled waits
19852    // must stay bounded so one VM cannot stall dispose/shutdown or unrelated VM work.
19853    if wait_ms == 0 {
19854        Duration::ZERO
19855    } else {
19856        Duration::from_millis(wait_ms).min(JAVASCRIPT_NET_POLL_MAX_WAIT)
19857    }
19858}
19859
19860pub(crate) fn service_javascript_net_sync_rpc<B>(
19861    request: JavascriptNetSyncRpcServiceRequest<'_, B>,
19862) -> Result<Value, SidecarError>
19863where
19864    B: NativeSidecarBridge + Send + 'static,
19865    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19866{
19867    let JavascriptNetSyncRpcServiceRequest {
19868        bridge,
19869        vm_id,
19870        dns,
19871        socket_paths,
19872        kernel,
19873        process,
19874        sync_request: request,
19875        resource_limits,
19876        network_counts,
19877    } = request;
19878    match request.method.as_str() {
19879        "net.http_listen" => {
19880            check_network_resource_limit(
19881                resource_limits.max_sockets,
19882                network_counts.sockets,
19883                1,
19884                "socket",
19885            )?;
19886            let payload_json =
19887                javascript_sync_rpc_arg_str(&request.args, 0, "net.http_listen payload")?;
19888            let payload: JavascriptHttpListenRequest =
19889                serde_json::from_str(payload_json).map_err(|error| {
19890                    SidecarError::InvalidState(format!(
19891                        "net.http_listen payload must be valid JSON: {error}"
19892                    ))
19893                })?;
19894            let (family, bind_host, guest_host) =
19895                normalize_tcp_listen_host(payload.hostname.as_deref())?;
19896            let requested_port = payload.port.unwrap_or(0);
19897            bridge.require_network_access(
19898                vm_id,
19899                NetworkOperation::Listen,
19900                format_tcp_resource(bind_host, requested_port),
19901            )?;
19902            let port = allocate_guest_listen_port(
19903                requested_port,
19904                family,
19905                &socket_paths.used_tcp_guest_ports,
19906                socket_paths.listen_policy,
19907            )?;
19908            let mut listener = ActiveTcpListener::bind(
19909                bind_host,
19910                guest_host,
19911                port,
19912                Some(DEFAULT_JAVASCRIPT_NET_BACKLOG),
19913            )?;
19914            let guest_local_addr = listener.guest_local_addr();
19915            process.http_servers.insert(
19916                payload.server_id,
19917                ActiveHttpServer {
19918                    listener: listener.listener.take().ok_or_else(|| {
19919                        SidecarError::InvalidState(String::from(
19920                            "HTTP listener missing host TCP socket",
19921                        ))
19922                    })?,
19923                    guest_local_addr,
19924                    next_request_id: 0,
19925                },
19926            );
19927            serde_json::to_string(&json!({
19928                "address": {
19929                    "address": guest_local_addr.ip().to_string(),
19930                    "family": socket_addr_family(&guest_local_addr),
19931                    "port": guest_local_addr.port(),
19932                }
19933            }))
19934            .map(Value::String)
19935            .map_err(|error| {
19936                SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}"))
19937            })
19938        }
19939        "net.http_close" => {
19940            let server_id =
19941                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_close server id")?;
19942            let server = process.http_servers.remove(&server_id).ok_or_else(|| {
19943                SidecarError::InvalidState(format!("unknown HTTP server {server_id}"))
19944            })?;
19945            drop(server.listener);
19946            process
19947                .pending_http_requests
19948                .retain(|(pending_server_id, _), _| *pending_server_id != server_id);
19949            Ok(Value::Null)
19950        }
19951        "net.http_wait" => {
19952            let server_id =
19953                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_wait server id")?;
19954            dispatch_http_wait_loop(process, server_id)
19955        }
19956        "net.http_respond" => {
19957            let server_id =
19958                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_respond server id")?;
19959            let request_id =
19960                javascript_sync_rpc_arg_u64(&request.args, 1, "net.http_respond request id")?;
19961            let response_json =
19962                javascript_sync_rpc_arg_str(&request.args, 2, "net.http_respond payload")?;
19963            ensure_vm_fetch_response_within_limit(
19964                response_json,
19965                "net.http_respond",
19966                VM_FETCH_BUFFER_LIMIT_BYTES,
19967            )?;
19968            serde_json::from_str::<Value>(response_json).map_err(|error| {
19969                SidecarError::Execution(format!(
19970                    "net.http_respond payload must be valid JSON: {error}"
19971                ))
19972            })?;
19973            let Some(pending) = process
19974                .pending_http_requests
19975                .get_mut(&(server_id, request_id))
19976            else {
19977                return Err(SidecarError::InvalidState(format!(
19978                    "unknown pending HTTP request {request_id} for server {server_id}"
19979                )));
19980            };
19981            *pending = Some(response_json.to_owned());
19982            Ok(Value::Null)
19983        }
19984        "net.reserve_tcp_port" => {
19985            let payload = request
19986                .args
19987                .first()
19988                .cloned()
19989                .ok_or_else(|| {
19990                    SidecarError::InvalidState(String::from(
19991                        "net.reserve_tcp_port requires a request payload",
19992                    ))
19993                })
19994                .and_then(|value| {
19995                    serde_json::from_value::<JavascriptNetReserveTcpPortRequest>(value).map_err(
19996                        |error| {
19997                            SidecarError::InvalidState(format!(
19998                                "invalid net.reserve_tcp_port payload: {error}"
19999                            ))
20000                        },
20001                    )
20002                })?;
20003            let (family, _bind_host, guest_host) =
20004                normalize_tcp_listen_host(payload.host.as_deref())?;
20005            let requested_port = payload.port.unwrap_or(0);
20006            let port = allocate_guest_listen_port(
20007                requested_port,
20008                family,
20009                &socket_paths.used_tcp_guest_ports,
20010                socket_paths.listen_policy,
20011            )?;
20012            let reservation_id = process.allocate_tcp_port_reservation_id();
20013            process
20014                .tcp_port_reservations
20015                .insert(reservation_id.clone(), (family, port));
20016            Ok(json!({
20017                "reservationId": reservation_id,
20018                "localAddress": guest_host,
20019                "localPort": port,
20020                "family": match family {
20021                    JavascriptSocketFamily::Ipv4 => "IPv4",
20022                    JavascriptSocketFamily::Ipv6 => "IPv6",
20023                },
20024            }))
20025        }
20026        "net.release_tcp_port" => {
20027            let reservation_id =
20028                javascript_sync_rpc_arg_str(&request.args, 0, "net.release_tcp_port reservation")?;
20029            process.tcp_port_reservations.remove(reservation_id);
20030            Ok(Value::Null)
20031        }
20032        "net.connect" => {
20033            check_network_resource_limit(
20034                resource_limits.max_sockets,
20035                network_counts.sockets,
20036                1,
20037                "socket",
20038            )?;
20039            check_network_resource_limit(
20040                resource_limits.max_connections,
20041                network_counts.connections,
20042                1,
20043                "connection",
20044            )?;
20045            let payload = request
20046                .args
20047                .first()
20048                .cloned()
20049                .ok_or_else(|| {
20050                    SidecarError::InvalidState(String::from(
20051                        "net.connect requires a request payload",
20052                    ))
20053                })
20054                .and_then(|value| {
20055                    serde_json::from_value::<JavascriptNetConnectRequest>(value).map_err(|error| {
20056                        SidecarError::InvalidState(format!("invalid net.connect payload: {error}"))
20057                    })
20058                })?;
20059            if let Some(path) = payload.path.as_deref() {
20060                let guest_path = normalize_path(path);
20061                let host_path = resolve_guest_socket_host_path(socket_paths, &guest_path);
20062                let socket = ActiveUnixSocket::connect(&host_path, &guest_path)?;
20063                let socket_id = process.allocate_unix_socket_id();
20064                process.unix_sockets.insert(socket_id.clone(), socket);
20065                Ok(json!({
20066                    "socketId": socket_id,
20067                    "remotePath": guest_path,
20068                }))
20069            } else {
20070                let port = payload.port.ok_or_else(|| {
20071                    SidecarError::InvalidState(String::from(
20072                        "net.connect requires either a path or port",
20073                    ))
20074                })?;
20075                let host = payload.host.as_deref().unwrap_or("localhost");
20076                let local_reservation = payload.local_reservation.as_deref().and_then(|id| {
20077                    process
20078                        .tcp_port_reservations
20079                        .remove(id)
20080                        .map(|reservation| (id.to_owned(), reservation))
20081                });
20082                bridge.require_network_access(
20083                    vm_id,
20084                    NetworkOperation::Http,
20085                    format_tcp_resource(host, port),
20086                )?;
20087                if is_loopback_socket_host(host) {
20088                    let families = [JavascriptSocketFamily::Ipv4, JavascriptSocketFamily::Ipv6];
20089                    if let Some((family, target)) = families.iter().find_map(|family| {
20090                        socket_paths
20091                            .http_loopback_target(*family, port)
20092                            .map(|target| (*family, target))
20093                    }) {
20094                        if let Some((reservation_id, reservation)) = local_reservation {
20095                            process
20096                                .tcp_port_reservations
20097                                .insert(reservation_id, reservation);
20098                        }
20099                        let remote_address = match family {
20100                            JavascriptSocketFamily::Ipv4 => "127.0.0.1",
20101                            JavascriptSocketFamily::Ipv6 => "::1",
20102                        };
20103                        return Ok(json!({
20104                            "loopbackHttpTarget": {
20105                                "processId": target.process_id.clone(),
20106                                "serverId": target.server_id,
20107                                "host": remote_address,
20108                                "port": port,
20109                            },
20110                            "localAddress": match family {
20111                                JavascriptSocketFamily::Ipv4 => "127.0.0.1",
20112                                JavascriptSocketFamily::Ipv6 => "::1",
20113                            },
20114                            "localPort": payload.local_port.unwrap_or(0),
20115                            "remoteAddress": remote_address,
20116                            "remotePort": port,
20117                            "remoteFamily": match family {
20118                                JavascriptSocketFamily::Ipv4 => "IPv4",
20119                                JavascriptSocketFamily::Ipv6 => "IPv6",
20120                            },
20121                        }));
20122                    }
20123                }
20124                let connect_result = ActiveTcpSocket::connect(ActiveTcpConnectRequest {
20125                    bridge,
20126                    kernel,
20127                    kernel_pid: process.kernel_pid,
20128                    vm_id,
20129                    dns,
20130                    host,
20131                    port,
20132                    local_address: payload.local_address.as_deref(),
20133                    local_port: payload.local_port,
20134                    local_reservation: local_reservation
20135                        .as_ref()
20136                        .map(|(_, reservation)| *reservation),
20137                    context: socket_paths,
20138                });
20139                if let Err(error) = connect_result {
20140                    if let Some((reservation_id, reservation)) = local_reservation {
20141                        process
20142                            .tcp_port_reservations
20143                            .insert(reservation_id, reservation);
20144                    }
20145                    return Err(error);
20146                }
20147                let socket = connect_result?;
20148                let socket_id = process.allocate_tcp_socket_id();
20149                let local_addr = socket.guest_local_addr;
20150                let remote_addr = socket.guest_remote_addr;
20151                process.tcp_sockets.insert(socket_id.clone(), socket);
20152                Ok(json!({
20153                    "socketId": socket_id,
20154                    "localAddress": local_addr.ip().to_string(),
20155                    "localPort": local_addr.port(),
20156                    "remoteAddress": remote_addr.ip().to_string(),
20157                    "remotePort": remote_addr.port(),
20158                    "remoteFamily": socket_addr_family(&remote_addr),
20159                }))
20160            }
20161        }
20162        "net.listen" => {
20163            check_network_resource_limit(
20164                resource_limits.max_sockets,
20165                network_counts.sockets,
20166                1,
20167                "socket",
20168            )?;
20169            let payload = request
20170                .args
20171                .first()
20172                .cloned()
20173                .ok_or_else(|| {
20174                    SidecarError::InvalidState(String::from(
20175                        "net.listen requires a request payload",
20176                    ))
20177                })
20178                .and_then(|value| match value {
20179                    Value::String(json) => {
20180                        serde_json::from_str::<JavascriptNetListenRequest>(&json).map_err(|error| {
20181                            SidecarError::InvalidState(format!(
20182                                "invalid net.listen payload: {error}"
20183                            ))
20184                        })
20185                    }
20186                    other => serde_json::from_value::<JavascriptNetListenRequest>(other).map_err(
20187                        |error| {
20188                            SidecarError::InvalidState(format!(
20189                                "invalid net.listen payload: {error}"
20190                            ))
20191                        },
20192                    ),
20193                })?;
20194            if let Some(path) = payload.path.as_deref() {
20195                let guest_path = normalize_path(path);
20196                if kernel.exists(&guest_path).map_err(kernel_error)? {
20197                    return Err(sidecar_net_error(std::io::Error::from_raw_os_error(
20198                        libc::EADDRINUSE,
20199                    )));
20200                }
20201
20202                let host_path = resolve_guest_socket_host_path(socket_paths, &guest_path);
20203                let on_host_mount =
20204                    host_mount_path_for_guest_path_from_mounts(&socket_paths.mounts, &guest_path)
20205                        .is_some();
20206                let listener = ActiveUnixListener::bind(&host_path, &guest_path, payload.backlog)?;
20207                if !on_host_mount {
20208                    ensure_kernel_parent_directories(kernel, &guest_path)?;
20209                    kernel
20210                        .write_file(&guest_path, Vec::new())
20211                        .map_err(kernel_error)?;
20212                }
20213                let listener_id = process.allocate_unix_listener_id();
20214                process.unix_listeners.insert(listener_id.clone(), listener);
20215                Ok(json!({
20216                    "serverId": listener_id,
20217                    "path": guest_path,
20218                }))
20219            } else {
20220                let (family, bind_host, guest_host) =
20221                    normalize_tcp_listen_host(payload.host.as_deref())?;
20222                let requested_port = payload.port.unwrap_or(0);
20223                bridge.require_network_access(
20224                    vm_id,
20225                    NetworkOperation::Listen,
20226                    format_tcp_resource(bind_host, requested_port),
20227                )?;
20228                let local_reservation = payload.local_reservation.as_deref().and_then(|id| {
20229                    process
20230                        .tcp_port_reservations
20231                        .remove(id)
20232                        .map(|reservation| (id.to_owned(), reservation))
20233                });
20234                let port = if requested_port != 0
20235                    && local_reservation
20236                        .as_ref()
20237                        .map(|(_, reservation)| *reservation)
20238                        == Some((family, requested_port))
20239                {
20240                    requested_port
20241                } else {
20242                    allocate_guest_listen_port(
20243                        requested_port,
20244                        family,
20245                        &socket_paths.used_tcp_guest_ports,
20246                        socket_paths.listen_policy,
20247                    )?
20248                };
20249                let listener_result = ActiveTcpListener::bind_kernel(
20250                    kernel,
20251                    process.kernel_pid,
20252                    guest_host,
20253                    port,
20254                    payload.backlog,
20255                );
20256                if let Err(error) = listener_result {
20257                    if let Some((reservation_id, reservation)) = local_reservation {
20258                        process
20259                            .tcp_port_reservations
20260                            .insert(reservation_id, reservation);
20261                    }
20262                    return Err(error);
20263                }
20264                let listener = listener_result?;
20265                let listener_id = process.allocate_tcp_listener_id();
20266                let local_addr = listener.guest_local_addr();
20267                process.tcp_listeners.insert(listener_id.clone(), listener);
20268                Ok(json!({
20269                    "serverId": listener_id,
20270                    "localAddress": local_addr.ip().to_string(),
20271                    "localPort": local_addr.port(),
20272                    "family": socket_addr_family(&local_addr),
20273                }))
20274            }
20275        }
20276        "net.poll" => {
20277            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.poll socket id")?;
20278            let wait_ms =
20279                javascript_sync_rpc_arg_u64_optional(&request.args, 1, "net.poll wait ms")?
20280                    .unwrap_or_default();
20281            let wait = clamp_javascript_net_poll_wait(wait_ms);
20282            let event = if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20283                socket.poll(kernel, process.kernel_pid, wait)?
20284            } else if let Some(socket) = process.unix_sockets.get_mut(socket_id) {
20285                socket.poll(wait)?
20286            } else {
20287                return Err(SidecarError::InvalidState(format!(
20288                    "unknown net socket {socket_id}"
20289                )));
20290            };
20291
20292            match event {
20293                Some(JavascriptTcpSocketEvent::Data(chunk)) => Ok(json!({
20294                    "type": "data",
20295                    "data": javascript_sync_rpc_bytes_value(&chunk),
20296                })),
20297                Some(JavascriptTcpSocketEvent::End) => Ok(json!({
20298                    "type": "end",
20299                })),
20300                Some(JavascriptTcpSocketEvent::Error { code, message }) => Ok(json!({
20301                    "type": "error",
20302                    "code": code,
20303                    "message": message,
20304                })),
20305                Some(JavascriptTcpSocketEvent::Close { had_error }) => {
20306                    if let Some(socket) = process.tcp_sockets.remove(socket_id) {
20307                        if let Some(listener_id) = socket.listener_id.as_deref() {
20308                            if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20309                                listener.release_connection(socket_id);
20310                            }
20311                        }
20312                    } else if let Some(socket) = process.unix_sockets.remove(socket_id) {
20313                        if let Some(listener_id) = socket.listener_id.as_deref() {
20314                            if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20315                                listener.release_connection(socket_id);
20316                            }
20317                        }
20318                    }
20319                    Ok(json!({
20320                        "type": "close",
20321                        "hadError": had_error,
20322                    }))
20323                }
20324                None => Ok(Value::Null),
20325            }
20326        }
20327        "net.socket_wait_connect" => {
20328            let socket_id =
20329                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_wait_connect socket id")?;
20330            if let Some(socket) = process.tcp_sockets.get(socket_id) {
20331                javascript_net_json_string(socket.socket_info(), "net.socket_wait_connect")
20332            } else {
20333                let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
20334                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20335                })?;
20336                javascript_net_json_string(socket.socket_info(), "net.socket_wait_connect")
20337            }
20338        }
20339        "net.socket_read" => {
20340            let socket_id =
20341                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_read socket id")?;
20342            if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20343                javascript_net_read_value(socket.poll(
20344                    kernel,
20345                    process.kernel_pid,
20346                    Duration::ZERO,
20347                )?)
20348            } else {
20349                let socket = process.unix_sockets.get_mut(socket_id).ok_or_else(|| {
20350                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20351                })?;
20352                javascript_net_read_value(socket.poll(Duration::ZERO)?)
20353            }
20354        }
20355        "net.socket_set_no_delay" => {
20356            let socket_id =
20357                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_set_no_delay socket id")?;
20358            let enable =
20359                javascript_sync_rpc_arg_bool(&request.args, 1, "net.socket_set_no_delay enabled")?;
20360            if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20361                socket.set_no_delay(enable)?;
20362            } else if !process.unix_sockets.contains_key(socket_id) {
20363                return Err(SidecarError::InvalidState(format!(
20364                    "unknown net socket {socket_id}"
20365                )));
20366            }
20367            Ok(Value::Null)
20368        }
20369        "net.socket_set_keep_alive" => {
20370            let socket_id = javascript_sync_rpc_arg_str(
20371                &request.args,
20372                0,
20373                "net.socket_set_keep_alive socket id",
20374            )?;
20375            let enable = javascript_sync_rpc_arg_bool(
20376                &request.args,
20377                1,
20378                "net.socket_set_keep_alive enabled",
20379            )?;
20380            let initial_delay_secs = javascript_sync_rpc_arg_u64_optional(
20381                &request.args,
20382                2,
20383                "net.socket_set_keep_alive initial delay seconds",
20384            )?;
20385            if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20386                socket.set_keep_alive(enable, initial_delay_secs)?;
20387            } else if !process.unix_sockets.contains_key(socket_id) {
20388                return Err(SidecarError::InvalidState(format!(
20389                    "unknown net socket {socket_id}"
20390                )));
20391            }
20392            Ok(Value::Null)
20393        }
20394        "net.socket_upgrade_tls" => {
20395            let socket_id =
20396                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_upgrade_tls socket id")?;
20397            let options_json =
20398                javascript_sync_rpc_arg_str(&request.args, 1, "net.socket_upgrade_tls options")?;
20399            let options: JavascriptTlsBridgeOptions =
20400                serde_json::from_str(options_json).map_err(|error| {
20401                    SidecarError::InvalidState(format!(
20402                        "net.socket_upgrade_tls options must be valid JSON: {error}"
20403                    ))
20404                })?;
20405            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20406                SidecarError::InvalidState(format!(
20407                    "unknown TCP socket {socket_id} for TLS upgrade"
20408                ))
20409            })?;
20410            socket.upgrade_tls(vm_id, kernel, options)?;
20411            Ok(Value::Null)
20412        }
20413        "net.socket_get_tls_client_hello" => {
20414            let socket_id = javascript_sync_rpc_arg_str(
20415                &request.args,
20416                0,
20417                "net.socket_get_tls_client_hello socket id",
20418            )?;
20419            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20420                SidecarError::InvalidState(format!(
20421                    "unknown TCP socket {socket_id} for TLS client hello query"
20422                ))
20423            })?;
20424            socket.tls_client_hello_json(vm_id, kernel)
20425        }
20426        "net.socket_tls_query" => {
20427            let socket_id =
20428                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_tls_query socket id")?;
20429            let query =
20430                javascript_sync_rpc_arg_str(&request.args, 1, "net.socket_tls_query query")?;
20431            let detailed = request
20432                .args
20433                .get(2)
20434                .and_then(Value::as_bool)
20435                .unwrap_or(false);
20436            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20437                SidecarError::InvalidState(format!("unknown TCP socket {socket_id} for TLS query"))
20438            })?;
20439            socket.tls_query(query, detailed)
20440        }
20441        "net.server_poll" => {
20442            let listener_id =
20443                javascript_sync_rpc_arg_str(&request.args, 0, "net.server_poll listener id")?;
20444            let wait_ms =
20445                javascript_sync_rpc_arg_u64_optional(&request.args, 1, "net.server_poll wait ms")?
20446                    .unwrap_or_default();
20447            let tcp_event = if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20448                Some(listener.poll(kernel, process.kernel_pid, Duration::from_millis(wait_ms))?)
20449            } else {
20450                None
20451            };
20452
20453            if let Some(event) = tcp_event {
20454                return match event {
20455                    Some(JavascriptTcpListenerEvent::Connection(pending)) => {
20456                        let PendingTcpSocket {
20457                            stream,
20458                            kernel_socket_id,
20459                            preallocated,
20460                            guest_local_addr,
20461                            guest_remote_addr,
20462                        } = pending;
20463                        if !preallocated {
20464                            if let Err(error) = check_network_resource_limit(
20465                                resource_limits.max_sockets,
20466                                network_counts.sockets,
20467                                1,
20468                                "socket",
20469                            )
20470                            .and_then(|()| {
20471                                check_network_resource_limit(
20472                                    resource_limits.max_connections,
20473                                    network_counts.connections,
20474                                    1,
20475                                    "connection",
20476                                )
20477                            }) {
20478                                if let Some(stream) = stream {
20479                                    let _ = stream.shutdown(Shutdown::Both);
20480                                }
20481                                return Ok(json!({
20482                                    "type": "error",
20483                                    "code": "EAGAIN",
20484                                    "message": error.to_string(),
20485                                }));
20486                            }
20487                        }
20488                        let socket = if let Some(stream) = stream {
20489                            ActiveTcpSocket::from_stream(
20490                                stream,
20491                                Some(listener_id.to_string()),
20492                                guest_local_addr,
20493                                guest_remote_addr,
20494                            )?
20495                        } else {
20496                            ActiveTcpSocket::from_kernel(
20497                                kernel_socket_id.ok_or_else(|| {
20498                                    SidecarError::InvalidState(String::from(
20499                                        "kernel TCP accept missing socket id",
20500                                    ))
20501                                })?,
20502                                Some(listener_id.to_string()),
20503                                guest_local_addr,
20504                                guest_remote_addr,
20505                            )
20506                        };
20507                        let socket_id = process.allocate_tcp_socket_id();
20508                        if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20509                            listener.register_connection(&socket_id);
20510                        }
20511                        process.tcp_sockets.insert(socket_id.clone(), socket);
20512                        Ok(json!({
20513                            "type": "connection",
20514                            "socketId": socket_id,
20515                            "localAddress": guest_local_addr.ip().to_string(),
20516                            "localPort": guest_local_addr.port(),
20517                            "remoteAddress": guest_remote_addr.ip().to_string(),
20518                            "remotePort": guest_remote_addr.port(),
20519                            "remoteFamily": socket_addr_family(&guest_remote_addr),
20520                        }))
20521                    }
20522                    Some(JavascriptTcpListenerEvent::Error { code, message }) => Ok(json!({
20523                        "type": "error",
20524                        "code": code,
20525                        "message": message,
20526                    })),
20527                    None => Ok(Value::Null),
20528                };
20529            }
20530
20531            let event = {
20532                let listener = process.unix_listeners.get_mut(listener_id).ok_or_else(|| {
20533                    SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20534                })?;
20535                listener.poll(Duration::from_millis(wait_ms))?
20536            };
20537
20538            match event {
20539                Some(JavascriptUnixListenerEvent::Connection(pending)) => {
20540                    if let Err(error) = check_network_resource_limit(
20541                        resource_limits.max_sockets,
20542                        network_counts.sockets,
20543                        1,
20544                        "socket",
20545                    )
20546                    .and_then(|()| {
20547                        check_network_resource_limit(
20548                            resource_limits.max_connections,
20549                            network_counts.connections,
20550                            1,
20551                            "connection",
20552                        )
20553                    }) {
20554                        let _ = pending.stream.shutdown(Shutdown::Both);
20555                        return Ok(json!({
20556                            "type": "error",
20557                            "code": "EAGAIN",
20558                            "message": error.to_string(),
20559                        }));
20560                    }
20561                    let socket = ActiveUnixSocket::from_stream(
20562                        pending.stream,
20563                        Some(listener_id.to_string()),
20564                        pending.local_path.clone(),
20565                        pending.remote_path.clone(),
20566                    )?;
20567                    let socket_id = process.allocate_unix_socket_id();
20568                    if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20569                        listener.register_connection(&socket_id);
20570                    }
20571                    process.unix_sockets.insert(socket_id.clone(), socket);
20572                    Ok(json!({
20573                        "type": "connection",
20574                        "socketId": socket_id,
20575                        "localPath": pending.local_path,
20576                        "remotePath": pending.remote_path,
20577                    }))
20578                }
20579                Some(JavascriptUnixListenerEvent::Error { code, message }) => Ok(json!({
20580                    "type": "error",
20581                    "code": code,
20582                    "message": message,
20583                })),
20584                None => Ok(Value::Null),
20585            }
20586        }
20587        "net.server_accept" => {
20588            let listener_id =
20589                javascript_sync_rpc_arg_str(&request.args, 0, "net.server_accept listener id")?;
20590            if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20591                return match listener.poll(kernel, process.kernel_pid, Duration::ZERO)? {
20592                    Some(JavascriptTcpListenerEvent::Connection(pending)) => {
20593                        let PendingTcpSocket {
20594                            stream,
20595                            kernel_socket_id,
20596                            preallocated,
20597                            guest_local_addr,
20598                            guest_remote_addr,
20599                        } = pending;
20600                        if !preallocated {
20601                            check_network_resource_limit(
20602                                resource_limits.max_sockets,
20603                                network_counts.sockets,
20604                                1,
20605                                "socket",
20606                            )?;
20607                            check_network_resource_limit(
20608                                resource_limits.max_connections,
20609                                network_counts.connections,
20610                                1,
20611                                "connection",
20612                            )?;
20613                        }
20614                        let info = json!({
20615                            "localAddress": guest_local_addr.ip().to_string(),
20616                            "localPort": guest_local_addr.port(),
20617                            "localFamily": socket_addr_family(&guest_local_addr),
20618                            "remoteAddress": guest_remote_addr.ip().to_string(),
20619                            "remotePort": guest_remote_addr.port(),
20620                            "remoteFamily": socket_addr_family(&guest_remote_addr),
20621                        });
20622                        let socket = if let Some(stream) = stream {
20623                            ActiveTcpSocket::from_stream(
20624                                stream,
20625                                Some(listener_id.to_string()),
20626                                guest_local_addr,
20627                                guest_remote_addr,
20628                            )?
20629                        } else {
20630                            ActiveTcpSocket::from_kernel(
20631                                kernel_socket_id.ok_or_else(|| {
20632                                    SidecarError::InvalidState(String::from(
20633                                        "kernel TCP accept missing socket id",
20634                                    ))
20635                                })?,
20636                                Some(listener_id.to_string()),
20637                                guest_local_addr,
20638                                guest_remote_addr,
20639                            )
20640                        };
20641                        let socket_id = process.allocate_tcp_socket_id();
20642                        if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20643                            listener.register_connection(&socket_id);
20644                        }
20645                        process.tcp_sockets.insert(socket_id.clone(), socket);
20646                        javascript_net_json_string(
20647                            json!({
20648                                "socketId": socket_id,
20649                                "info": info,
20650                            }),
20651                            "net.server_accept",
20652                        )
20653                    }
20654                    Some(JavascriptTcpListenerEvent::Error { code, message }) => {
20655                        let detail = code.unwrap_or_else(|| String::from("server accept"));
20656                        Err(SidecarError::Execution(format!("{detail}: {message}")))
20657                    }
20658                    None => Ok(javascript_net_timeout_value()),
20659                };
20660            }
20661
20662            let listener = process.unix_listeners.get_mut(listener_id).ok_or_else(|| {
20663                SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20664            })?;
20665            match listener.poll(Duration::ZERO)? {
20666                Some(JavascriptUnixListenerEvent::Connection(pending)) => {
20667                    check_network_resource_limit(
20668                        resource_limits.max_sockets,
20669                        network_counts.sockets,
20670                        1,
20671                        "socket",
20672                    )?;
20673                    check_network_resource_limit(
20674                        resource_limits.max_connections,
20675                        network_counts.connections,
20676                        1,
20677                        "connection",
20678                    )?;
20679                    let info = json!({
20680                        "localPath": pending.local_path.clone(),
20681                        "remotePath": pending.remote_path.clone(),
20682                    });
20683                    let socket = ActiveUnixSocket::from_stream(
20684                        pending.stream,
20685                        Some(listener_id.to_string()),
20686                        pending.local_path,
20687                        pending.remote_path,
20688                    )?;
20689                    let socket_id = process.allocate_unix_socket_id();
20690                    if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20691                        listener.register_connection(&socket_id);
20692                    }
20693                    process.unix_sockets.insert(socket_id.clone(), socket);
20694                    javascript_net_json_string(
20695                        json!({
20696                            "socketId": socket_id,
20697                            "info": info,
20698                        }),
20699                        "net.server_accept",
20700                    )
20701                }
20702                Some(JavascriptUnixListenerEvent::Error { code, message }) => {
20703                    let detail = code.unwrap_or_else(|| String::from("server accept"));
20704                    Err(SidecarError::Execution(format!("{detail}: {message}")))
20705                }
20706                None => Ok(javascript_net_timeout_value()),
20707            }
20708        }
20709        "net.server_connections" => {
20710            let listener_id = javascript_sync_rpc_arg_str(
20711                &request.args,
20712                0,
20713                "net.server_connections listener id",
20714            )?;
20715            if let Some(listener) = process.tcp_listeners.get(listener_id) {
20716                Ok(json!(listener.active_connection_count()))
20717            } else {
20718                let listener = process.unix_listeners.get(listener_id).ok_or_else(|| {
20719                    SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20720                })?;
20721                Ok(json!(listener.active_connection_count()))
20722            }
20723        }
20724        "net.upgrade_socket_write" => {
20725            let socket_id = javascript_sync_rpc_arg_str(
20726                &request.args,
20727                0,
20728                "net.upgrade_socket_write socket id",
20729            )?;
20730            let chunk =
20731                javascript_sync_rpc_base64_arg(&request.args, 1, "net.upgrade_socket_write chunk")?;
20732            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20733                SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
20734            })?;
20735            socket
20736                .write_all(kernel, process.kernel_pid, &chunk)
20737                .map(|written| json!(written))
20738        }
20739        "net.upgrade_socket_end" => {
20740            let socket_id =
20741                javascript_sync_rpc_arg_str(&request.args, 0, "net.upgrade_socket_end socket id")?;
20742            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20743                SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
20744            })?;
20745            socket.shutdown_write(kernel, process.kernel_pid)?;
20746            Ok(Value::Null)
20747        }
20748        "net.upgrade_socket_destroy" => {
20749            let socket_id = javascript_sync_rpc_arg_str(
20750                &request.args,
20751                0,
20752                "net.upgrade_socket_destroy socket id",
20753            )?;
20754            let socket = process.tcp_sockets.remove(socket_id).ok_or_else(|| {
20755                SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
20756            })?;
20757            if let Some(listener_id) = socket.listener_id.as_deref() {
20758                if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20759                    listener.release_connection(socket_id);
20760                }
20761            }
20762            let _ = socket.close(kernel, process.kernel_pid);
20763            Ok(Value::Null)
20764        }
20765        "net.write" => {
20766            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.write socket id")?;
20767            let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "net.write chunk")?;
20768            if let Some(socket) = process.tcp_sockets.get(socket_id) {
20769                socket
20770                    .write_all(kernel, process.kernel_pid, &chunk)
20771                    .map(|written| json!(written))
20772            } else {
20773                let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
20774                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20775                })?;
20776                socket.write_all(&chunk).map(|written| json!(written))
20777            }
20778        }
20779        "net.shutdown" => {
20780            let socket_id =
20781                javascript_sync_rpc_arg_str(&request.args, 0, "net.shutdown socket id")?;
20782            if let Some(socket) = process.tcp_sockets.get(socket_id) {
20783                socket.shutdown_write(kernel, process.kernel_pid)?;
20784            } else {
20785                let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
20786                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20787                })?;
20788                socket.shutdown_write()?;
20789            }
20790            Ok(Value::Null)
20791        }
20792        "net.destroy" => {
20793            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.destroy socket id")?;
20794            if let Some(socket) = process.tcp_sockets.remove(socket_id) {
20795                if let Some(listener_id) = socket.listener_id.as_deref() {
20796                    if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20797                        listener.release_connection(socket_id);
20798                    }
20799                }
20800                let _ = socket.close(kernel, process.kernel_pid);
20801                Ok(Value::Null)
20802            } else {
20803                let socket = process.unix_sockets.remove(socket_id).ok_or_else(|| {
20804                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20805                })?;
20806                if let Some(listener_id) = socket.listener_id.as_deref() {
20807                    if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20808                        listener.release_connection(socket_id);
20809                    }
20810                }
20811                let _ = socket.close();
20812                Ok(Value::Null)
20813            }
20814        }
20815        "net.server_close" => {
20816            let listener_id =
20817                javascript_sync_rpc_arg_str(&request.args, 0, "net.server_close listener id")?;
20818            if let Some(listener) = process.tcp_listeners.remove(listener_id) {
20819                listener.close(kernel, process.kernel_pid)?;
20820                Ok(Value::Null)
20821            } else {
20822                let listener = process.unix_listeners.remove(listener_id).ok_or_else(|| {
20823                    SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20824                })?;
20825                listener.close()?;
20826                Ok(Value::Null)
20827            }
20828        }
20829        "tls.get_ciphers" => javascript_net_json_string(
20830            Value::Array(
20831                tls_provider()
20832                    .cipher_suites
20833                    .iter()
20834                    .filter_map(|suite| {
20835                        suite
20836                            .suite()
20837                            .as_str()
20838                            .map(|value| Value::String(value.to_owned()))
20839                    })
20840                    .collect(),
20841            ),
20842            "tls.get_ciphers",
20843        ),
20844        _ => Err(SidecarError::InvalidState(format!(
20845            "unsupported JavaScript net sync RPC method {}",
20846            request.method
20847        ))),
20848    }
20849}
20850
20851fn signal_name_for_stream_event(signal: i32) -> Option<&'static str> {
20852    match signal {
20853        libc::SIGHUP => Some("SIGHUP"),
20854        libc::SIGINT => Some("SIGINT"),
20855        libc::SIGUSR1 => Some("SIGUSR1"),
20856        libc::SIGALRM => Some("SIGALRM"),
20857        libc::SIGCONT => Some("SIGCONT"),
20858        libc::SIGTERM => Some("SIGTERM"),
20859        libc::SIGCHLD => Some("SIGCHLD"),
20860        libc::SIGWINCH => Some("SIGWINCH"),
20861        _ => None,
20862    }
20863}
20864
20865pub(crate) fn canonical_signal_name(signal: i32) -> Option<&'static str> {
20866    match signal {
20867        1 => Some("SIGHUP"),
20868        2 => Some("SIGINT"),
20869        3 => Some("SIGQUIT"),
20870        4 => Some("SIGILL"),
20871        5 => Some("SIGTRAP"),
20872        6 => Some("SIGABRT"),
20873        7 => Some("SIGBUS"),
20874        8 => Some("SIGFPE"),
20875        9 => Some("SIGKILL"),
20876        10 => Some("SIGUSR1"),
20877        11 => Some("SIGSEGV"),
20878        12 => Some("SIGUSR2"),
20879        13 => Some("SIGPIPE"),
20880        14 => Some("SIGALRM"),
20881        15 => Some("SIGTERM"),
20882        17 => Some("SIGCHLD"),
20883        18 => Some("SIGCONT"),
20884        19 => Some("SIGSTOP"),
20885        20 => Some("SIGTSTP"),
20886        21 => Some("SIGTTIN"),
20887        22 => Some("SIGTTOU"),
20888        23 => Some("SIGURG"),
20889        24 => Some("SIGXCPU"),
20890        25 => Some("SIGXFSZ"),
20891        26 => Some("SIGVTALRM"),
20892        27 => Some("SIGPROF"),
20893        28 => Some("SIGWINCH"),
20894        29 => Some("SIGIO"),
20895        30 => Some("SIGPWR"),
20896        31 => Some("SIGSYS"),
20897        _ => None,
20898    }
20899}
20900
20901fn dispatch_v8_process_signal(process: &ActiveProcess, signal: i32) -> Result<bool, SidecarError> {
20902    let Some(signal_name) = signal_name_for_stream_event(signal) else {
20903        return Ok(false);
20904    };
20905    process.execution.send_javascript_stream_event(
20906        "signal",
20907        json!({
20908            "signal": signal_name,
20909            "number": signal,
20910            "action": "default",
20911        }),
20912    )?;
20913    Ok(true)
20914}
20915
20916fn dispatch_v8_session_signal_async(session: V8SessionHandle, signal: i32) {
20917    let Some(signal_name) = signal_name_for_stream_event(signal).map(str::to_owned) else {
20918        return;
20919    };
20920    thread::spawn(move || {
20921        thread::sleep(Duration::from_millis(1));
20922        let payload = v8_runtime::json_to_cbor_payload(&json!({
20923            "signal": signal_name,
20924            "number": signal,
20925            "action": "default",
20926        }))
20927        .unwrap_or_default();
20928        let _ = session.send_stream_event("signal", payload);
20929    });
20930}
20931
20932pub(crate) fn parse_signal(signal: &str) -> Result<i32, SidecarError> {
20933    let trimmed = signal.trim();
20934    if trimmed.is_empty() {
20935        return Err(SidecarError::InvalidState(String::from(
20936            "kill_process requires a non-empty signal",
20937        )));
20938    }
20939
20940    if let Ok(value) = trimmed.parse::<i32>() {
20941        return match value {
20942            0..=31 => Ok(value),
20943            _ => Err(SidecarError::InvalidState(format!(
20944                "unsupported kill_process signal {signal}"
20945            ))),
20946        };
20947    }
20948
20949    let upper = trimmed.to_ascii_uppercase();
20950    let normalized = upper.strip_prefix("SIG").unwrap_or(&upper);
20951
20952    signal_number_from_name(normalized).ok_or_else(|| {
20953        SidecarError::InvalidState(format!("unsupported kill_process signal {signal}"))
20954    })
20955}
20956
20957fn signal_number_from_name(signal: &str) -> Option<i32> {
20958    match signal {
20959        "0" => Some(0),
20960        "HUP" => Some(1),
20961        "INT" => Some(2),
20962        "QUIT" => Some(3),
20963        "ILL" => Some(4),
20964        "TRAP" => Some(5),
20965        "ABRT" | "IOT" => Some(6),
20966        "BUS" => Some(7),
20967        "FPE" => Some(8),
20968        "KILL" => Some(9),
20969        "USR1" => Some(10),
20970        "SEGV" => Some(11),
20971        "USR2" => Some(12),
20972        "PIPE" => Some(13),
20973        "ALRM" => Some(14),
20974        "TERM" => Some(15),
20975        "STKFLT" => Some(16),
20976        "CHLD" => Some(17),
20977        "CONT" => Some(18),
20978        "STOP" => Some(19),
20979        "TSTP" => Some(20),
20980        "TTIN" => Some(21),
20981        "TTOU" => Some(22),
20982        "URG" => Some(23),
20983        "XCPU" => Some(24),
20984        "XFSZ" => Some(25),
20985        "VTALRM" => Some(26),
20986        "PROF" => Some(27),
20987        "WINCH" => Some(28),
20988        "IO" | "POLL" => Some(29),
20989        "PWR" => Some(30),
20990        "SYS" => Some(31),
20991        _ => None,
20992    }
20993}
20994
20995pub(crate) fn runtime_child_is_alive(child_pid: u32) -> Result<bool, SidecarError> {
20996    Ok(runtime_child_exit_status(child_pid)?.is_none())
20997}
20998
20999#[cfg(not(target_os = "macos"))]
21000fn runtime_child_exit_status(child_pid: u32) -> Result<Option<i32>, SidecarError> {
21001    if child_pid == 0 {
21002        return Ok(Some(0));
21003    }
21004
21005    let wait_flags = WaitPidFlag::WNOHANG
21006        | WaitPidFlag::WNOWAIT
21007        | WaitPidFlag::WEXITED
21008        | WaitPidFlag::WUNTRACED
21009        | WaitPidFlag::WCONTINUED;
21010    match wait_on_child(WaitId::Pid(Pid::from_raw(child_pid as i32)), wait_flags) {
21011        Ok(WaitStatus::StillAlive)
21012        | Ok(WaitStatus::Stopped(_, _))
21013        | Ok(WaitStatus::Continued(_)) => Ok(None),
21014        Ok(WaitStatus::Exited(_, status)) => Ok(Some(status)),
21015        Ok(WaitStatus::Signaled(_, signal, _)) => Ok(Some(128 + signal as i32)),
21016        #[cfg(any(target_os = "linux", target_os = "android"))]
21017        Ok(WaitStatus::PtraceEvent(_, _, _) | WaitStatus::PtraceSyscall(_)) => Ok(None),
21018        Err(nix::errno::Errno::ECHILD) => Ok(Some(0)),
21019        Err(error) => Err(SidecarError::Execution(format!(
21020            "failed to inspect guest runtime process {child_pid}: {error}"
21021        ))),
21022    }
21023}
21024
21025// macOS nix exposes no `waitid`/`WNOWAIT`, so we poll with `waitpid(WNOHANG)`.
21026// NOTE: unlike Linux's `waitid(WNOWAIT)`, `waitpid` REAPS an exited child rather
21027// than leaving it waitable. That is correct for this poll (the sidecar is the
21028// reaping parent), but a second status query after exit returns ECHILD → treated
21029// as "exited(0)" below.
21030#[cfg(target_os = "macos")]
21031fn runtime_child_exit_status(child_pid: u32) -> Result<Option<i32>, SidecarError> {
21032    if child_pid == 0 {
21033        return Ok(Some(0));
21034    }
21035
21036    match waitpid(Pid::from_raw(child_pid as i32), Some(WaitPidFlag::WNOHANG)) {
21037        Ok(WaitStatus::StillAlive)
21038        | Ok(WaitStatus::Stopped(_, _))
21039        | Ok(WaitStatus::Continued(_)) => Ok(None),
21040        Ok(WaitStatus::Exited(_, status)) => Ok(Some(status)),
21041        Ok(WaitStatus::Signaled(_, signal, _)) => Ok(Some(128 + signal as i32)),
21042        Err(nix::errno::Errno::ECHILD) => Ok(Some(0)),
21043        Err(error) => Err(SidecarError::Execution(format!(
21044            "failed to inspect guest runtime process {child_pid}: {error}"
21045        ))),
21046    }
21047}
21048
21049pub(crate) fn signal_runtime_process(child_pid: u32, signal: i32) -> Result<(), SidecarError> {
21050    if child_pid == 0 {
21051        return Ok(());
21052    }
21053
21054    if !runtime_child_is_alive(child_pid)? {
21055        return Ok(());
21056    }
21057
21058    if signal == 0 {
21059        return Ok(());
21060    }
21061
21062    let parsed = Signal::try_from(signal).map_err(|_| {
21063        SidecarError::InvalidState(format!("unsupported kill_process signal {signal}"))
21064    })?;
21065    let result = send_signal(Pid::from_raw(child_pid as i32), Some(parsed));
21066
21067    match result {
21068        Ok(()) => Ok(()),
21069        Err(nix::errno::Errno::ESRCH) => Ok(()),
21070        Err(error) => Err(SidecarError::Execution(format!(
21071            "failed to signal guest runtime process {child_pid}: {error}"
21072        ))),
21073    }
21074}
21075
21076pub(crate) fn error_code(error: &SidecarError) -> &'static str {
21077    match error {
21078        SidecarError::InvalidState(_) => "invalid_state",
21079        SidecarError::ProtocolVersionMismatch(_) => "protocol_version_mismatch",
21080        SidecarError::BridgeVersionMismatch(_) => "bridge_version_mismatch",
21081        SidecarError::Conflict(_) => "conflict",
21082        SidecarError::Unauthorized(_) => "unauthorized",
21083        SidecarError::Unsupported(_) => "unsupported",
21084        SidecarError::FrameTooLarge(_) => "frame_too_large",
21085        SidecarError::Kernel(_) => "kernel_error",
21086        SidecarError::Plugin(_) => "plugin_error",
21087        SidecarError::Execution(_) => "execution_error",
21088        SidecarError::Bridge(_) => "bridge_error",
21089        SidecarError::Io(_) => "io_error",
21090    }
21091}
21092
21093fn guest_errno_code(message: &str) -> Option<&str> {
21094    const TRUSTED_PREFIXES: &[&str] = &[
21095        "ERR_AGENTOS_NODE_SYNC_RPC",
21096        "ERR_AGENTOS_PYTHON_VFS_RPC",
21097        "ERR_AGENTOS_BRIDGE",
21098    ];
21099
21100    let mut segments = message.split(':').map(str::trim);
21101    let first = segments.next()?;
21102    if is_guest_errno_segment(first) {
21103        return Some(first);
21104    }
21105
21106    if TRUSTED_PREFIXES.contains(&first) {
21107        let second = segments.next()?;
21108        if is_guest_errno_segment(second) {
21109            return Some(second);
21110        }
21111    }
21112
21113    None
21114}
21115
21116fn is_guest_errno_segment(segment: &str) -> bool {
21117    segment.len() >= 2
21118        && segment.starts_with('E')
21119        && !segment.starts_with("ERR_")
21120        && segment[1..]
21121            .bytes()
21122            .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_')
21123}
21124
21125pub(crate) fn javascript_sync_rpc_error_code(error: &SidecarError) -> String {
21126    let message = error.to_string();
21127    if let Some(code) = guest_errno_code(&message) {
21128        return code.to_owned();
21129    }
21130    if message.starts_with("ERR_NATIVE_BINARY_NOT_SUPPORTED:") {
21131        return String::from("ERR_NATIVE_BINARY_NOT_SUPPORTED");
21132    }
21133
21134    let lower = message.to_ascii_lowercase();
21135    if lower.contains("no such file or directory")
21136        || lower.contains("entry not found")
21137        || lower.contains("not found")
21138    {
21139        return String::from("ENOENT");
21140    }
21141    if lower.contains("permission denied") {
21142        return String::from("EACCES");
21143    }
21144    if lower.contains("already exists")
21145        || lower.contains("already registered")
21146        || lower.contains("file exists")
21147    {
21148        return String::from("EEXIST");
21149    }
21150    if lower.contains("invalid argument") {
21151        return String::from("EINVAL");
21152    }
21153
21154    String::from("ERR_AGENTOS_NODE_SYNC_RPC")
21155}
21156
21157pub(crate) fn ignore_stale_javascript_sync_rpc_response(
21158    error: SidecarError,
21159) -> Result<(), SidecarError> {
21160    match error {
21161        SidecarError::Execution(message)
21162            if message.ends_with("is no longer pending")
21163                && message.starts_with("sync RPC request ") =>
21164        {
21165            Ok(())
21166        }
21167        SidecarError::Execution(message) => {
21168            let lower = message.to_ascii_lowercase();
21169            if lower.contains("sync rpc response")
21170                && (lower.contains("broken pipe") || lower.contains("channel closed unexpectedly"))
21171            {
21172                Ok(())
21173            } else {
21174                Err(SidecarError::Execution(message))
21175            }
21176        }
21177        other => Err(other),
21178    }
21179}
21180
21181#[cfg(test)]
21182mod error_code_tests {
21183    use super::{guest_errno_code, javascript_sync_rpc_error_code, SidecarError};
21184
21185    #[test]
21186    fn guest_errno_code_rejects_guest_controlled_errno_segments() {
21187        assert_eq!(guest_errno_code("user said 'EACCES: denied'"), None);
21188        assert_eq!(
21189            guest_errno_code("prefix: user said 'EPERM': more text"),
21190            None
21191        );
21192        assert_eq!(guest_errno_code("ERR_AGENTOS_FAKE: EACCES: denied"), None);
21193    }
21194
21195    #[test]
21196    fn guest_errno_code_accepts_trusted_secure_exec_prefixes() {
21197        assert_eq!(
21198            guest_errno_code("ERR_AGENTOS_NODE_SYNC_RPC: EACCES: permission denied on /foo"),
21199            Some("EACCES")
21200        );
21201        assert_eq!(
21202            guest_errno_code("ERR_AGENTOS_PYTHON_VFS_RPC: ENOENT: missing file"),
21203            Some("ENOENT")
21204        );
21205        assert_eq!(guest_errno_code("EEXIST: already exists"), Some("EEXIST"));
21206    }
21207
21208    #[test]
21209    fn javascript_sync_rpc_error_code_ignores_spoofed_errnos() {
21210        let error = SidecarError::Execution(String::from("user said 'EACCES: denied'"));
21211        assert_eq!(
21212            javascript_sync_rpc_error_code(&error),
21213            "ERR_AGENTOS_NODE_SYNC_RPC"
21214        );
21215    }
21216
21217    #[test]
21218    fn javascript_sync_rpc_error_code_preserves_real_sidecar_errnos() {
21219        let error = SidecarError::Execution(String::from(
21220            "ERR_AGENTOS_NODE_SYNC_RPC: EACCES: permission denied on /foo",
21221        ));
21222        assert_eq!(javascript_sync_rpc_error_code(&error), "EACCES");
21223    }
21224
21225    #[test]
21226    fn javascript_sync_rpc_error_code_maps_file_exists_messages() {
21227        let error = SidecarError::Io(String::from(
21228            "failed to create mapped guest directory /.next/server: File exists (os error 17)",
21229        ));
21230        assert_eq!(javascript_sync_rpc_error_code(&error), "EEXIST");
21231    }
21232
21233    #[test]
21234    fn javascript_sync_rpc_error_code_preserves_native_binary_rejections() {
21235        let error = SidecarError::Execution(String::from(
21236            "ERR_NATIVE_BINARY_NOT_SUPPORTED: refused to execute native ELF guest binary at /tmp/fake-rg inside the VM",
21237        ));
21238        assert_eq!(
21239            javascript_sync_rpc_error_code(&error),
21240            "ERR_NATIVE_BINARY_NOT_SUPPORTED"
21241        );
21242    }
21243}
21244#[cfg(test)]
21245mod ssrf_egress_classifier_tests {
21246    // F-005/006/007 (sec-sidecar T1/T7/T11): the egress classifier must treat the
21247    // unspecified address (0.0.0.0 / ::), CGNAT (100.64.0.0/10), IPv6 spellings of
21248    // restricted IPv4 targets (::a.b.c.d), and reserved/multicast (240/4, 224/4) as
21249    // restricted. 0.0.0.0 routes to 127.0.0.1 on connect(), so leaving it
21250    // unclassified let a guest bypass the loopback port-ownership gate.
21251    //
21252    // These are bounded SAFEGUARD tests: they exercise the classifier and the DNS
21253    // egress filter directly (no network I/O, no Node), so they run fast and
21254    // deterministically. See FAILURES.md#F-005, #F-006, #F-007.
21255    use super::{
21256        filter_dns_safe_ip_addrs, is_loopback_ip, restricted_non_loopback_ip_range, SidecarError,
21257    };
21258    use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
21259
21260    fn assert_restricted(ip: IpAddr, expected_label: &str) {
21261        let classification = restricted_non_loopback_ip_range(ip);
21262        assert!(
21263            classification.is_some(),
21264            "{ip} must be classified as a restricted egress target"
21265        );
21266        let (_cidr, label) = classification.unwrap();
21267        assert_eq!(
21268            label, expected_label,
21269            "{ip} should be labelled {expected_label}, got {label}"
21270        );
21271    }
21272
21273    fn assert_dns_denied(ip: IpAddr, label: &str) {
21274        match filter_dns_safe_ip_addrs(vec![ip], "attacker.example") {
21275            Err(SidecarError::Execution(message)) => assert!(
21276                message.starts_with("EACCES:"),
21277                "{label}: egress filter must deny with EACCES, got: {message}"
21278            ),
21279            other => panic!("{label}: expected EACCES denial, got {other:?}"),
21280        }
21281    }
21282
21283    // F-005 (sec-sidecar T1).
21284    #[test]
21285    fn classifier_denies_unspecified_and_cgnat_targets() {
21286        // 0.0.0.0 (IPv4 unspecified) -> would route to host loopback.
21287        assert_restricted(IpAddr::V4(Ipv4Addr::UNSPECIFIED), "unspecified");
21288        // :: (IPv6 unspecified).
21289        assert_restricted(IpAddr::V6(Ipv6Addr::UNSPECIFIED), "unspecified");
21290
21291        // CGNAT 100.64.0.0/10 spans 100.64.x.x .. 100.127.x.x.
21292        assert_restricted(
21293            IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1)),
21294            "carrier-grade-nat",
21295        );
21296        assert_restricted(
21297            IpAddr::V4(Ipv4Addr::new(100, 127, 255, 254)),
21298            "carrier-grade-nat",
21299        );
21300
21301        // Guard against over-blocking: addresses just outside 100.64/10 stay allowed.
21302        assert!(
21303            restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(100, 63, 255, 255)))
21304                .is_none(),
21305            "100.63.255.255 is outside CGNAT and must remain allowed"
21306        );
21307        assert!(
21308            restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(100, 128, 0, 0))).is_none(),
21309            "100.128.0.0 is outside CGNAT and must remain allowed"
21310        );
21311
21312        // The DNS egress filter must also deny these via EACCES.
21313        assert_dns_denied(IpAddr::V4(Ipv4Addr::UNSPECIFIED), "0.0.0.0 (unspecified)");
21314        assert_dns_denied(IpAddr::V6(Ipv6Addr::UNSPECIFIED), ":: (unspecified)");
21315        assert_dns_denied(
21316            IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1)),
21317            "100.64.0.1 (CGNAT)",
21318        );
21319    }
21320
21321    // F-006 (sec-sidecar T7).
21322    #[test]
21323    fn classifier_denies_ipv6_spelled_metadata_addresses() {
21324        // The IPv4-mapped form (::ffff:169.254.169.254) was already handled; the
21325        // IPv4-compatible form (::169.254.169.254) is the gap this fixes.
21326        let mapped = "::ffff:169.254.169.254".parse::<Ipv6Addr>().unwrap();
21327        assert_restricted(IpAddr::V6(mapped), "link-local");
21328
21329        let compat = "::169.254.169.254".parse::<Ipv6Addr>().unwrap();
21330        assert_restricted(IpAddr::V6(compat), "link-local");
21331
21332        // Other IPv4-compatible private/CGNAT spellings must also be canonicalized.
21333        assert_restricted(
21334            IpAddr::V6("::10.0.0.1".parse::<Ipv6Addr>().unwrap()),
21335            "private",
21336        );
21337        assert_restricted(
21338            IpAddr::V6("::100.64.0.1".parse::<Ipv6Addr>().unwrap()),
21339            "carrier-grade-nat",
21340        );
21341
21342        // Guard against over-blocking: the IPv6 unspecified/loopback addresses
21343        // are not IPv4-compatible host targets, and a public IPv4-compatible
21344        // address must remain allowed.
21345        assert_eq!(
21346            restricted_non_loopback_ip_range(IpAddr::V6(Ipv6Addr::UNSPECIFIED)),
21347            Some(("::/128", "unspecified")),
21348            ":: must classify as unspecified, not via the IPv4-compat path"
21349        );
21350        assert!(
21351            restricted_non_loopback_ip_range(IpAddr::V6(Ipv6Addr::LOCALHOST)).is_none()
21352                || is_loopback_ip(IpAddr::V6(Ipv6Addr::LOCALHOST)),
21353            "::1 must not be classified as a restricted IPv4-compatible target"
21354        );
21355        assert!(
21356            restricted_non_loopback_ip_range(IpAddr::V6("::8.8.8.8".parse::<Ipv6Addr>().unwrap()))
21357                .is_none(),
21358            "::8.8.8.8 (public IPv4-compatible) must remain allowed"
21359        );
21360
21361        // The DNS egress filter must deny the IPv4-compat metadata spelling.
21362        assert_dns_denied(
21363            IpAddr::V6("::169.254.169.254".parse::<Ipv6Addr>().unwrap()),
21364            "::169.254.169.254 (IPv4-compat metadata)",
21365        );
21366    }
21367
21368    // F-007 (sec-sidecar T11).
21369    #[test]
21370    fn classifier_denies_reserved_and_multicast_targets() {
21371        // 224.0.0.0/4 (multicast) and 240.0.0.0/4 (reserved / future use) are not
21372        // legitimate unicast egress targets; a guest connect to them must be
21373        // classified as restricted and denied.
21374        assert_restricted(IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)), "multicast");
21375        assert_restricted(IpAddr::V4(Ipv4Addr::new(239, 255, 255, 255)), "multicast");
21376        assert_restricted(IpAddr::V4(Ipv4Addr::new(240, 0, 0, 1)), "reserved");
21377        // 255.255.255.255 (limited broadcast) falls in 240.0.0.0/4.
21378        assert_restricted(IpAddr::V4(Ipv4Addr::BROADCAST), "reserved");
21379
21380        // IPv4-compatible IPv6 spellings must canonicalize and be denied too.
21381        assert_restricted(
21382            IpAddr::V6("::224.0.0.1".parse::<Ipv6Addr>().unwrap()),
21383            "multicast",
21384        );
21385        assert_restricted(
21386            IpAddr::V6("::240.0.0.1".parse::<Ipv6Addr>().unwrap()),
21387            "reserved",
21388        );
21389
21390        // Guard against over-blocking: addresses just outside 224/4 stay allowed.
21391        assert!(
21392            restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(223, 255, 255, 255)))
21393                .is_none(),
21394            "223.255.255.255 is outside 224/4 and must remain allowed"
21395        );
21396
21397        // The DNS egress filter must also deny these via EACCES.
21398        assert_dns_denied(
21399            IpAddr::V4(Ipv4Addr::new(240, 0, 0, 1)),
21400            "240.0.0.1 (reserved)",
21401        );
21402        assert_dns_denied(
21403            IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)),
21404            "224.0.0.1 (multicast)",
21405        );
21406    }
21407}
21408
21409/// Adversarial coverage for the DNS-rebinding gap (VECTORS.md D.3) on the
21410/// Python/Pyodide `httpRequestSync` outbound HTTP path. The egress range guard
21411/// (`filter_dns_safe_ip_addrs`) runs at resolution time, but `ureq` performs its
21412/// own DNS resolution for the TCP/TLS connect, so a rebinding DNS server could
21413/// previously make the second lookup land on a private/link-local/metadata IP
21414/// the first check rejected. The fix pins `ureq`'s resolver to the vetted
21415/// address set; these tests prove the connect is pinned and refuses any other
21416/// host or an empty (fully-rejected) address set.
21417#[cfg(test)]
21418mod dns_rebinding_pin_tests {
21419    use super::{issue_outbound_http_request, split_netloc, JavascriptHttpRequestOptions};
21420    use std::collections::BTreeMap;
21421    use std::io::{Read, Write};
21422    use std::net::{IpAddr, Ipv4Addr, TcpListener};
21423    use std::thread;
21424    use url::Url;
21425
21426    fn empty_headers() -> super::HttpHeaderCollection {
21427        super::parse_http_header_collection(&BTreeMap::new(), "test headers")
21428            .expect("empty header collection")
21429    }
21430
21431    fn options() -> JavascriptHttpRequestOptions {
21432        JavascriptHttpRequestOptions {
21433            method: Some(String::from("GET")),
21434            headers: BTreeMap::new(),
21435            body: None,
21436            reject_unauthorized: None,
21437        }
21438    }
21439
21440    #[test]
21441    fn split_netloc_handles_hostnames_and_bracketed_ipv6() {
21442        assert_eq!(
21443            split_netloc("attacker.example:80"),
21444            Some(("attacker.example", 80))
21445        );
21446        assert_eq!(split_netloc("[::1]:443"), Some(("::1", 443)));
21447        assert_eq!(split_netloc("10.0.0.1:8080"), Some(("10.0.0.1", 8080)));
21448        assert_eq!(split_netloc("no-port"), None);
21449        assert_eq!(split_netloc("host:notaport"), None);
21450    }
21451
21452    /// A loopback HTTP server stands in for the egress-vetted target. The
21453    /// request URL uses a *different* hostname (`attacker.example`) whose real
21454    /// DNS would resolve elsewhere; pinning forces the connect onto the vetted
21455    /// IP only. If the resolver were unpinned, the request would fail to reach
21456    /// this server (and on a real host could land on a private/metadata IP).
21457    #[test]
21458    fn outbound_http_connect_is_pinned_to_vetted_ip() {
21459        let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).expect("bind loopback server");
21460        let port = listener.local_addr().expect("local addr").port();
21461        let server = thread::spawn(move || {
21462            let (mut stream, _) = listener.accept().expect("accept");
21463            let mut buf = [0u8; 1024];
21464            let _ = stream.read(&mut buf);
21465            stream
21466                .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi")
21467                .expect("write response");
21468            let _ = stream.flush();
21469        });
21470
21471        let url = Url::parse(&format!("http://attacker.example:{port}/")).expect("url");
21472        let pinned = vec![IpAddr::V4(Ipv4Addr::LOCALHOST)];
21473        let result = issue_outbound_http_request(&url, &options(), &empty_headers(), &pinned)
21474            .expect("pinned request should reach the vetted loopback target");
21475        let payload = result.as_str().expect("string payload");
21476        assert!(
21477            payload.contains("\"status\":200"),
21478            "expected 200 from pinned target, got: {payload}"
21479        );
21480        server.join().expect("server thread");
21481    }
21482
21483    /// With no vetted address (every resolved IP was rejected by the range
21484    /// guard, or the literal IP was a blocked range), the pinned resolver must
21485    /// refuse rather than fall back to the host resolver.
21486    #[test]
21487    fn outbound_http_refuses_when_no_vetted_address() {
21488        let url = Url::parse("https://attacker.example/").expect("url");
21489        let error = issue_outbound_http_request(&url, &options(), &empty_headers(), &[])
21490            .expect_err("empty pinned set must be refused");
21491        let message = error.to_string();
21492        assert!(
21493            message.contains("EACCES") || message.contains("ERR_HTTP_REQUEST_FAILED"),
21494            "expected an egress refusal, got: {message}"
21495        );
21496    }
21497}