Skip to main content

secure_exec_sidecar/
execution.rs

1//! Process execution, networking, and runtime event handling extracted from service.rs.
2
3use secure_exec_vm_config as vm_config;
4
5use crate::filesystem::{
6    handle_python_vfs_rpc_request as filesystem_handle_python_vfs_rpc_request,
7    service_javascript_fs_sync_rpc, service_javascript_module_sync_rpc,
8};
9use crate::protocol::{
10    BoundUdpSnapshotResponse, CloseStdinRequest, EventFrame, EventPayload, ExecuteRequest,
11    FindBoundUdpRequest, FindListenerRequest, GetProcessSnapshotRequest, GetSignalStateRequest,
12    GetZombieTimerCountRequest, GuestRuntimeKind, JavascriptChildProcessSpawnOptions,
13    JavascriptChildProcessSpawnRequest, JavascriptDgramBindRequest,
14    JavascriptDgramCreateSocketRequest, JavascriptDgramSendRequest, JavascriptDnsLookupRequest,
15    JavascriptDnsResolveRequest, JavascriptNetConnectRequest, JavascriptNetListenRequest,
16    JavascriptNetReserveTcpPortRequest, KillProcessRequest, ListenerSnapshotResponse,
17    OwnershipScope, ProcessExitedEvent, ProcessKilledResponse, ProcessOutputEvent,
18    ProcessSnapshotEntry, ProcessSnapshotResponse, ProcessSnapshotStatus, ProcessStartedResponse,
19    RequestFrame, ResponseFrame, ResponsePayload, SidecarRequestPayload, SignalDispositionAction,
20    SignalHandlerRegistration, SignalStateResponse, SocketStateEntry, StdinClosedResponse,
21    StdinWrittenResponse, StreamChannel, VmFetchRequest, VmFetchResponse, WasmPermissionTier,
22    WriteStdinRequest, ZombieTimerCountResponse,
23};
24use crate::service::{
25    audit_fields, dirname, emit_security_audit_event, emit_structured_event, javascript_error,
26    kernel_error, log_stale_process_event, normalize_host_path, normalize_path,
27    parse_javascript_child_process_spawn_request, path_is_within_root,
28    process_event_queue_overflow_error, python_error, wasm_error, MAX_PROCESS_EVENT_QUEUE,
29};
30use crate::state::{
31    ActiveCipherSession, ActiveDhSession, ActiveDiffieHellmanSession, ActiveEcdhSession,
32    ActiveExecution, ActiveExecutionEvent, ActiveHttp2Server, ActiveHttp2Session,
33    ActiveHttp2Stream, ActiveHttpServer, ActiveMappedHostFd, ActiveProcess, ActiveSqliteDatabase,
34    ActiveSqliteStatement, ActiveTcpListener, ActiveTcpSocket, ActiveTlsState, ActiveTlsStream,
35    ActiveUdpSocket, ActiveUnixListener, ActiveUnixSocket, BridgeError, ExitedProcessSnapshot,
36    Http2BridgeEvent, Http2RuntimeSnapshot, Http2SessionCommand, Http2SessionSnapshot,
37    Http2SocketSnapshot, JavascriptHttpLoopbackTarget, JavascriptSocketFamily,
38    JavascriptSocketPathContext, JavascriptTcpListenerEvent, JavascriptTcpSocketEvent,
39    JavascriptTlsBridgeOptions, JavascriptTlsClientHello, JavascriptTlsDataValue,
40    JavascriptTlsMaterial, JavascriptUdpFamily, JavascriptUdpSocketEvent,
41    JavascriptUnixListenerEvent, NetworkResourceCounts, PendingTcpSocket, PendingUnixSocket,
42    ProcNetEntry, ProcessEventEnvelope, ResolvedChildProcessExecution, ResolvedTcpConnectAddr,
43    SharedBridge, SharedSidecarRequestClient, SidecarKernel, SocketQueryKind, ToolExecution,
44    VmDnsConfig, VmListenPolicy, VmState, DEFAULT_JAVASCRIPT_NET_BACKLOG, EXECUTION_DRIVER_NAME,
45    EXECUTION_SANDBOX_ROOT_ENV, JAVASCRIPT_COMMAND, LOOPBACK_EXEMPT_PORTS_ENV,
46    MAPPED_HOST_FD_START, PYTHON_COMMAND, TOOL_DRIVER_NAME,
47    VM_LISTEN_ALLOW_PRIVILEGED_METADATA_KEY, WASM_COMMAND, WASM_STDIO_SYNC_RPC_ENV,
48};
49use crate::tools::{
50    format_tool_failure_output, is_tool_command, normalized_tool_command_name,
51    resolve_tool_command, ToolCommandResolution,
52};
53use crate::wire::{ProtocolFrame as WireProtocolFrame, WireFrameCodec, DEFAULT_MAX_FRAME_BYTES};
54use crate::{DispatchResult, NativeSidecar, NativeSidecarBridge, SidecarError};
55
56use base64::Engine;
57use bytes::Bytes;
58use h2::{client, server, Reason};
59use hickory_resolver::proto::rr::{RData, Record, RecordType};
60use hmac::{Hmac, Mac};
61use http::{HeaderMap, HeaderName, HeaderValue, Method, Request, Response, Uri};
62use md5::Md5;
63use nix::libc;
64use nix::sys::signal::{kill as send_signal, Signal};
65use nix::sys::wait::WaitStatus;
66#[cfg(not(target_os = "macos"))]
67use nix::sys::wait::{waitid as wait_on_child, Id as WaitId, WaitPidFlag};
68#[cfg(target_os = "macos")]
69use nix::sys::wait::{waitpid, WaitPidFlag};
70use nix::unistd::Pid;
71use openssl::bn::{BigNum, BigNumContext};
72use openssl::derive::Deriver;
73use openssl::dh::Dh;
74use openssl::ec::{EcGroup, EcKey, EcPoint, PointConversionForm};
75use openssl::hash::MessageDigest;
76use openssl::nid::Nid;
77use openssl::pkey::{Id as PKeyId, PKey, Params, Private, Public};
78use openssl::rand::rand_bytes;
79use openssl::rsa::{Padding, Rsa};
80use openssl::sign::{Signer, Verifier};
81use openssl::symm::{Cipher, Crypter, Mode};
82use pbkdf2::pbkdf2_hmac;
83use rusqlite::types::ValueRef as SqliteValueRef;
84use rusqlite::{
85    Connection as SqliteConnection, OpenFlags as SqliteOpenFlags, Statement as SqliteStatement,
86};
87use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
88use rustls::crypto::aws_lc_rs;
89use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName};
90use rustls::{
91    ClientConfig, ClientConnection, DigitallySignedStruct, RootCertStore, ServerConfig,
92    ServerConnection, SignatureScheme,
93};
94use scrypt::{scrypt, Params as ScryptParams};
95use secure_exec_bridge::LifecycleState;
96use secure_exec_execution::wasm::WasmExecutionError;
97use secure_exec_execution::{
98    javascript::handle_internal_bridge_call_from_host_context, v8_host::V8SessionHandle,
99    v8_runtime, CreateJavascriptContextRequest, CreatePythonContextRequest,
100    CreateWasmContextRequest, GuestModuleReader, GuestRuntimeConfig, JavascriptExecutionEvent,
101    JavascriptExecutionLimits, JavascriptSyncRpcRequest, ModuleFsReader,
102    NodeSignalDispositionAction, NodeSignalHandlerRegistration, PythonExecutionEvent,
103    PythonExecutionLimits, PythonVfsRpcMethod, PythonVfsRpcRequest, PythonVfsRpcResponsePayload,
104    StartJavascriptExecutionRequest, StartPythonExecutionRequest, StartWasmExecutionRequest,
105    WasmExecutionEvent, WasmExecutionLimits, WasmPermissionTier as ExecutionWasmPermissionTier,
106};
107use secure_exec_kernel::dns::{
108    DnsLookupPolicy, DnsRecordResolution, DnsResolutionSource as KernelDnsResolutionSource,
109};
110use secure_exec_kernel::kernel::{KernelProcessHandle, SpawnOptions, VirtualProcessOptions};
111use secure_exec_kernel::permissions::NetworkOperation;
112use secure_exec_kernel::poll::{PollEvents, PollFd, PollTargetEntry, POLLERR, POLLHUP, POLLIN};
113use secure_exec_kernel::process_table::{ProcessStatus, WaitPidFlags, SIGKILL, SIGTERM};
114use secure_exec_kernel::pty::LineDisciplineConfig;
115use secure_exec_kernel::resource_accounting::ResourceLimits;
116use secure_exec_kernel::root_fs::RootFilesystemMode;
117use secure_exec_kernel::socket_table::{
118    InetSocketAddress, SocketDomain, SocketId, SocketShutdown as KernelSocketShutdown, SocketSpec,
119    SocketState, SocketType,
120};
121use serde::{Deserialize, Serialize};
122use serde_json::{json, Map, Value};
123use sha1::Sha1;
124use sha2::{digest::Digest, Sha256, Sha512};
125use socket2::{SockRef, TcpKeepalive};
126use std::collections::VecDeque;
127use std::collections::{BTreeMap, BTreeSet};
128use std::fmt;
129use std::fs;
130use std::io::{Cursor, Read, Write};
131use std::net::{
132    IpAddr, Ipv4Addr, Ipv6Addr, Shutdown, SocketAddr, TcpListener, TcpStream, ToSocketAddrs,
133    UdpSocket,
134};
135use std::os::unix::fs::{MetadataExt, PermissionsExt};
136use std::os::unix::net::{SocketAddr as UnixSocketAddr, UnixListener, UnixStream};
137use std::path::{Path, PathBuf};
138use std::pin::Pin;
139use std::sync::atomic::{AtomicBool, Ordering};
140use std::sync::mpsc::{self, RecvTimeoutError, Sender};
141use std::sync::{Arc, Mutex, OnceLock, Weak};
142use std::thread;
143use std::time::{Duration, Instant};
144use tokio::io::{AsyncRead, AsyncWrite};
145use tokio::runtime::Builder as TokioRuntimeBuilder;
146use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver};
147use tokio_rustls::{TlsAcceptor, TlsConnector};
148use url::Url;
149
150const DEFAULT_KERNEL_STDIN_READ_MAX_BYTES: usize = 64 * 1024;
151const DEFAULT_KERNEL_STDIN_READ_TIMEOUT_MS: u64 = 100;
152const JAVASCRIPT_NET_TIMEOUT_SENTINEL: &str = "__secure_exec_net_timeout__";
153const PYTHON_PYODIDE_GUEST_ROOT: &str = "/__agentos_pyodide";
154const PYTHON_PYODIDE_CACHE_GUEST_ROOT: &str = "/__agentos_pyodide_cache";
155const TCP_SOCKET_POLL_TIMEOUT: Duration = Duration::from_millis(100);
156const TLS_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(5);
157const HTTP_LOOPBACK_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
158pub(crate) const MAX_PER_PROCESS_STATE_HANDLES: usize = 1024;
159const VM_FETCH_BUFFER_LIMIT_BYTES: usize = DEFAULT_MAX_FRAME_BYTES;
160const DEFAULT_SCRYPT_COST: u64 = 16_384;
161const DEFAULT_SCRYPT_BLOCK_SIZE: u32 = 8;
162const DEFAULT_SCRYPT_PARALLELIZATION: u32 = 1;
163const SQLITE_JS_SAFE_INTEGER_MAX: i64 = 9_007_199_254_740_991;
164const HTTP_LOOPBACK_REQUEST_TIMEOUT_MS_ENV: &str =
165    "SECURE_EXEC_TEST_HTTP_LOOPBACK_REQUEST_TIMEOUT_MS";
166
167trait Http2AsyncIo: AsyncRead + AsyncWrite + Unpin + Send {}
168
169impl<T> Http2AsyncIo for T where T: AsyncRead + AsyncWrite + Unpin + Send {}
170
171fn http_loopback_request_timeout() -> Duration {
172    static TIMEOUT: OnceLock<Duration> = OnceLock::new();
173    *TIMEOUT.get_or_init(|| {
174        std::env::var(HTTP_LOOPBACK_REQUEST_TIMEOUT_MS_ENV)
175            .ok()
176            .and_then(|value| value.parse::<u64>().ok())
177            .map(Duration::from_millis)
178            .unwrap_or(HTTP_LOOPBACK_REQUEST_TIMEOUT)
179    })
180}
181
182const DEFAULT_ALLOWED_NODE_BUILTINS: &[&str] = &[
183    "assert",
184    "buffer",
185    "console",
186    "child_process",
187    "crypto",
188    "dns",
189    "events",
190    "fs",
191    "http",
192    "http2",
193    "https",
194    "module",
195    "os",
196    "path",
197    "perf_hooks",
198    "querystring",
199    "sqlite",
200    "stream",
201    "string_decoder",
202    "timers",
203    "tls",
204    "tty",
205    "url",
206    "util",
207    "zlib",
208];
209
210#[derive(Debug, Clone, Copy, PartialEq, Eq)]
211enum JavascriptCryptoDigestAlgorithm {
212    Md5,
213    Sha1,
214    Sha256,
215    Sha512,
216}
217
218#[derive(Debug, Default, Deserialize)]
219#[serde(default, rename_all = "camelCase")]
220struct JavascriptScryptOptions {
221    #[serde(alias = "N")]
222    cost: Option<u64>,
223    #[serde(alias = "r")]
224    block_size: Option<u32>,
225    #[serde(alias = "p")]
226    parallelization: Option<u32>,
227}
228
229#[derive(Debug, Deserialize)]
230#[serde(rename_all = "camelCase")]
231struct JavascriptHttpListenRequest {
232    server_id: u64,
233    #[serde(default)]
234    port: Option<u16>,
235    #[serde(default)]
236    hostname: Option<String>,
237}
238
239#[derive(Debug, Default, Deserialize)]
240#[serde(default, rename_all = "camelCase")]
241struct JavascriptHttpRequestOptions {
242    method: Option<String>,
243    headers: BTreeMap<String, Value>,
244    body: Option<String>,
245    reject_unauthorized: Option<bool>,
246}
247
248#[derive(Debug, Default, Deserialize)]
249#[serde(default, rename_all = "camelCase")]
250struct JavascriptHttp2ServerListenRequest {
251    server_id: u64,
252    secure: bool,
253    port: Option<u16>,
254    host: Option<String>,
255    backlog: Option<u32>,
256    timeout: Option<u64>,
257    settings: BTreeMap<String, Value>,
258    tls: Option<JavascriptTlsBridgeOptions>,
259}
260
261#[derive(Debug, Default, Deserialize)]
262#[serde(default, rename_all = "camelCase")]
263struct JavascriptHttp2SessionConnectRequest {
264    authority: Option<String>,
265    protocol: Option<String>,
266    host: Option<String>,
267    port: Option<u16>,
268    settings: BTreeMap<String, Value>,
269    tls: Option<JavascriptTlsBridgeOptions>,
270}
271
272#[derive(Debug, Default, Deserialize)]
273#[serde(default, rename_all = "camelCase")]
274struct JavascriptHttp2RequestOptions {
275    end_stream: bool,
276}
277
278#[derive(Debug, Default, Deserialize)]
279#[serde(default, rename_all = "camelCase")]
280struct JavascriptHttp2FileResponseOptions {
281    offset: Option<u64>,
282    length: Option<i64>,
283}
284
285#[derive(Debug, Clone)]
286struct HttpHeaderCollection {
287    normalized: BTreeMap<String, Vec<String>>,
288    raw_pairs: Vec<(String, String)>,
289}
290
291#[derive(Debug)]
292struct InsecureTlsVerifier {
293    supported_schemes: Vec<SignatureScheme>,
294}
295
296impl ServerCertVerifier for InsecureTlsVerifier {
297    fn verify_server_cert(
298        &self,
299        _end_entity: &CertificateDer<'_>,
300        _intermediates: &[CertificateDer<'_>],
301        _server_name: &ServerName<'_>,
302        _ocsp_response: &[u8],
303        _now: rustls::pki_types::UnixTime,
304    ) -> Result<ServerCertVerified, rustls::Error> {
305        Ok(ServerCertVerified::assertion())
306    }
307
308    fn verify_tls12_signature(
309        &self,
310        _message: &[u8],
311        _cert: &CertificateDer<'_>,
312        _dss: &DigitallySignedStruct,
313    ) -> Result<HandshakeSignatureValid, rustls::Error> {
314        Ok(HandshakeSignatureValid::assertion())
315    }
316
317    fn verify_tls13_signature(
318        &self,
319        _message: &[u8],
320        _cert: &CertificateDer<'_>,
321        _dss: &DigitallySignedStruct,
322    ) -> Result<HandshakeSignatureValid, rustls::Error> {
323        Ok(HandshakeSignatureValid::assertion())
324    }
325
326    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
327        self.supported_schemes.clone()
328    }
329}
330
331impl ActiveProcess {
332    pub(crate) fn new(
333        kernel_pid: u32,
334        kernel_handle: KernelProcessHandle,
335        runtime: GuestRuntimeKind,
336        execution: ActiveExecution,
337    ) -> Self {
338        Self {
339            kernel_pid,
340            kernel_handle,
341            kernel_stdin_writer_fd: None,
342            runtime,
343            detached: false,
344            execution,
345            guest_cwd: String::from("/"),
346            env: BTreeMap::new(),
347            host_cwd: PathBuf::from("/"),
348            mapped_host_fds: BTreeMap::new(),
349            next_mapped_host_fd: MAPPED_HOST_FD_START,
350            pending_execution_events: VecDeque::new(),
351            pending_self_signal_exit: None,
352            child_processes: BTreeMap::new(),
353            next_child_process_id: 0,
354            http_servers: BTreeMap::new(),
355            pending_http_requests: BTreeMap::new(),
356            http2: Default::default(),
357            tcp_listeners: BTreeMap::new(),
358            next_tcp_listener_id: 0,
359            tcp_sockets: BTreeMap::new(),
360            next_tcp_socket_id: 0,
361            tcp_port_reservations: BTreeMap::new(),
362            next_tcp_port_reservation_id: 0,
363            unix_listeners: BTreeMap::new(),
364            next_unix_listener_id: 0,
365            unix_sockets: BTreeMap::new(),
366            next_unix_socket_id: 0,
367            udp_sockets: BTreeMap::new(),
368            next_udp_socket_id: 0,
369            cipher_sessions: BTreeMap::new(),
370            next_cipher_session_id: 0,
371            diffie_hellman_sessions: BTreeMap::new(),
372            next_diffie_hellman_session_id: 0,
373            sqlite_databases: BTreeMap::new(),
374            next_sqlite_database_id: 0,
375            sqlite_statements: BTreeMap::new(),
376            next_sqlite_statement_id: 0,
377            module_resolution_cache: secure_exec_execution::LocalModuleResolutionCache::default(),
378        }
379    }
380
381    pub(crate) fn queue_pending_execution_event(
382        &mut self,
383        event: ActiveExecutionEvent,
384    ) -> Result<(), SidecarError> {
385        if self.pending_execution_events.len() >= MAX_PROCESS_EVENT_QUEUE {
386            return Err(process_event_queue_overflow_error());
387        }
388        self.pending_execution_events.push_back(event);
389        Ok(())
390    }
391
392    pub(crate) fn with_host_cwd(mut self, host_cwd: PathBuf) -> Self {
393        self.host_cwd = host_cwd;
394        self
395    }
396
397    pub(crate) fn with_guest_cwd(mut self, guest_cwd: String) -> Self {
398        self.guest_cwd = guest_cwd;
399        self
400    }
401
402    pub(crate) fn with_env(mut self, env: BTreeMap<String, String>) -> Self {
403        self.env = env;
404        self
405    }
406
407    pub(crate) fn with_kernel_stdin_writer_fd(mut self, fd: u32) -> Self {
408        self.kernel_stdin_writer_fd = Some(fd);
409        self
410    }
411
412    pub(crate) fn with_detached(mut self, detached: bool) -> Self {
413        self.detached = detached;
414        self
415    }
416
417    pub(crate) fn allocate_mapped_host_fd(&mut self, fd: ActiveMappedHostFd) -> u32 {
418        let handle = self.next_mapped_host_fd;
419        self.next_mapped_host_fd = self
420            .next_mapped_host_fd
421            .checked_add(1)
422            .unwrap_or(MAPPED_HOST_FD_START);
423        self.mapped_host_fds.insert(handle, fd);
424        handle
425    }
426
427    pub(crate) fn mapped_host_fd(&self, fd: u32) -> Option<&ActiveMappedHostFd> {
428        self.mapped_host_fds.get(&fd)
429    }
430
431    pub(crate) fn mapped_host_fd_mut(&mut self, fd: u32) -> Option<&mut ActiveMappedHostFd> {
432        self.mapped_host_fds.get_mut(&fd)
433    }
434
435    pub(crate) fn close_mapped_host_fd(&mut self, fd: u32) -> bool {
436        self.mapped_host_fds.remove(&fd).is_some()
437    }
438
439    pub(crate) fn allocate_child_process_id(&mut self) -> String {
440        self.next_child_process_id += 1;
441        format!("child-{}", self.next_child_process_id)
442    }
443
444    fn allocate_tcp_listener_id(&mut self) -> String {
445        self.next_tcp_listener_id += 1;
446        format!("listener-{}", self.next_tcp_listener_id)
447    }
448
449    fn allocate_tcp_socket_id(&mut self) -> String {
450        self.next_tcp_socket_id += 1;
451        format!("socket-{}", self.next_tcp_socket_id)
452    }
453
454    fn allocate_tcp_port_reservation_id(&mut self) -> String {
455        self.next_tcp_port_reservation_id += 1;
456        format!("tcp-port-reservation-{}", self.next_tcp_port_reservation_id)
457    }
458
459    fn allocate_unix_listener_id(&mut self) -> String {
460        self.next_unix_listener_id += 1;
461        format!("unix-listener-{}", self.next_unix_listener_id)
462    }
463
464    fn allocate_unix_socket_id(&mut self) -> String {
465        self.next_unix_socket_id += 1;
466        format!("unix-socket-{}", self.next_unix_socket_id)
467    }
468
469    fn allocate_udp_socket_id(&mut self) -> String {
470        self.next_udp_socket_id += 1;
471        format!("udp-socket-{}", self.next_udp_socket_id)
472    }
473
474    pub(crate) fn network_resource_counts(&self) -> NetworkResourceCounts {
475        let mut counts = NetworkResourceCounts {
476            sockets: self.http_servers.len()
477                + self.tcp_listeners.len()
478                + self.tcp_sockets.len()
479                + self.unix_listeners.len()
480                + self.unix_sockets.len()
481                + self.udp_sockets.len(),
482            connections: self.tcp_sockets.len() + self.unix_sockets.len(),
483        };
484        if let Ok(http2) = self.http2.shared.lock() {
485            counts.sockets += http2.servers.len() + http2.sessions.len();
486            counts.connections += http2.sessions.len();
487        }
488
489        for child in self.child_processes.values() {
490            let child_counts = child.network_resource_counts();
491            counts.sockets += child_counts.sockets;
492            counts.connections += child_counts.connections;
493        }
494
495        counts
496    }
497
498    fn sidecar_only_network_resource_counts(&self) -> NetworkResourceCounts {
499        let mut counts = NetworkResourceCounts {
500            sockets: self.http_servers.len()
501                + self
502                    .tcp_listeners
503                    .values()
504                    .filter(|listener| listener.kernel_socket_id.is_none())
505                    .count()
506                + self
507                    .tcp_sockets
508                    .values()
509                    .filter(|socket| socket.kernel_socket_id.is_none())
510                    .count()
511                + self.unix_listeners.len()
512                + self.unix_sockets.len()
513                + self
514                    .udp_sockets
515                    .values()
516                    .filter(|socket| socket.kernel_socket_id.is_none())
517                    .count(),
518            connections: self
519                .tcp_sockets
520                .values()
521                .filter(|socket| socket.kernel_socket_id.is_none())
522                .count()
523                + self.unix_sockets.len(),
524        };
525        if let Ok(http2) = self.http2.shared.lock() {
526            counts.sockets += http2.servers.len() + http2.sessions.len();
527            counts.connections += http2.sessions.len();
528        }
529
530        for child in self.child_processes.values() {
531            let child_counts = child.sidecar_only_network_resource_counts();
532            counts.sockets += child_counts.sockets;
533            counts.connections += child_counts.connections;
534        }
535
536        counts
537    }
538}
539
540fn poll_tool_process_event(
541    execution: &ToolExecution,
542) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
543    let event = execution
544        .pending_events
545        .lock()
546        .unwrap_or_else(|poisoned| poisoned.into_inner())
547        .pop_front();
548    if event.is_some() {
549        return Ok(event);
550    }
551    if execution.events_overflowed.load(Ordering::Relaxed) {
552        return Err(process_event_queue_overflow_error());
553    }
554    Ok(None)
555}
556
557fn descendant_pending_execution_event_capacity(
558    root: &ActiveProcess,
559    child_path: &[&str],
560) -> Option<usize> {
561    let mut child = root;
562    for child_process_id in child_path {
563        child = child.child_processes.get(*child_process_id)?;
564    }
565    Some(MAX_PROCESS_EVENT_QUEUE.saturating_sub(child.pending_execution_events.len()))
566}
567
568fn poll_child_execution_after_exit(
569    child: &mut ActiveProcess,
570    wait: Duration,
571) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
572    match child.execution.poll_event_blocking(wait) {
573        Ok(event) => Ok(event),
574        Err(SidecarError::Execution(message))
575            if child.runtime == GuestRuntimeKind::WebAssembly
576                && message == WasmExecutionError::EventChannelClosed.to_string() =>
577        {
578            Ok(None)
579        }
580        Err(error) => Err(error),
581    }
582}
583
584fn closed_javascript_event_channel(message: &str) -> bool {
585    message == "guest JavaScript event channel closed unexpectedly"
586}
587
588fn closed_python_event_channel(message: &str) -> bool {
589    message == "guest Python event channel closed unexpectedly"
590}
591
592fn closed_wasm_event_channel(message: &str) -> bool {
593    message == WasmExecutionError::EventChannelClosed.to_string()
594}
595
596fn missing_vm_error(vm_id: &str) -> SidecarError {
597    SidecarError::InvalidState(format!("VM {vm_id} is no longer active"))
598}
599
600fn missing_process_error(vm_id: &str, process_id: &str) -> SidecarError {
601    SidecarError::InvalidState(format!(
602        "VM {vm_id} no longer has active process {process_id}"
603    ))
604}
605
606fn is_broken_pipe_error(error: &SidecarError) -> bool {
607    matches!(error, SidecarError::Execution(message) if message.contains("Broken pipe") || message.contains("os error 32") || message.contains("EPIPE"))
608}
609
610fn javascript_child_process_gone_error(process_id: &str, child_path: &[&str]) -> SidecarError {
611    let child_label = if child_path.is_empty() {
612        process_id.to_owned()
613    } else {
614        format!("{process_id}/{}", child_path.join("/"))
615    };
616    SidecarError::Execution(format!(
617        "ECHILD: child_process {child_label} is no longer available"
618    ))
619}
620
621fn is_javascript_child_process_gone_error(error: &SidecarError) -> bool {
622    matches!(
623        error,
624        SidecarError::Execution(message) if guest_errno_code(message) == Some("ECHILD")
625    )
626}
627
628fn loopback_tls_transport_registry(
629) -> &'static Mutex<BTreeMap<String, Weak<crate::state::LoopbackTlsTransportPair>>> {
630    static REGISTRY: OnceLock<
631        Mutex<BTreeMap<String, Weak<crate::state::LoopbackTlsTransportPair>>>,
632    > = OnceLock::new();
633    REGISTRY.get_or_init(|| Mutex::new(BTreeMap::new()))
634}
635
636#[cfg(test)]
637#[allow(dead_code)]
638pub(crate) fn loopback_tls_registry_len() -> usize {
639    loopback_tls_transport_registry()
640        .lock()
641        .expect("loopback TLS transport registry lock poisoned")
642        .len()
643}
644
645#[cfg(test)]
646pub(crate) fn loopback_tls_registry_contains(key: &str) -> bool {
647    loopback_tls_transport_registry()
648        .lock()
649        .expect("loopback TLS transport registry lock poisoned")
650        .contains_key(key)
651}
652
653fn loopback_tls_transport_key(
654    vm_id: &str,
655    socket_id: SocketId,
656    peer_socket_id: SocketId,
657) -> String {
658    let (lower, higher) = if socket_id <= peer_socket_id {
659        (socket_id, peer_socket_id)
660    } else {
661        (peer_socket_id, socket_id)
662    };
663    format!("{vm_id}:{lower}:{higher}")
664}
665
666fn loopback_tls_endpoint(
667    vm_id: &str,
668    socket_id: SocketId,
669    peer_socket_id: SocketId,
670) -> Result<crate::state::LoopbackTlsEndpoint, SidecarError> {
671    let key = loopback_tls_transport_key(vm_id, socket_id, peer_socket_id);
672    let registry = loopback_tls_transport_registry();
673    let mut transports = registry.lock().map_err(|_| {
674        SidecarError::InvalidState(String::from(
675            "loopback TLS transport registry lock poisoned",
676        ))
677    })?;
678    transports.retain(|_, pair| pair.strong_count() > 0);
679    let pair = transports
680        .get(&key)
681        .and_then(Weak::upgrade)
682        .unwrap_or_else(|| {
683            let pair = Arc::new(crate::state::LoopbackTlsTransportPair {
684                state: Mutex::new(crate::state::LoopbackTlsTransportPairState::default()),
685                ready: std::sync::Condvar::new(),
686            });
687            transports.insert(key.clone(), Arc::downgrade(&pair));
688            pair
689        });
690    Ok(crate::state::LoopbackTlsEndpoint {
691        pair,
692        is_lower_socket: socket_id <= peer_socket_id,
693        registry_key: Some(key),
694    })
695}
696
697impl crate::state::LoopbackTlsEndpoint {
698    fn shutdown_write(&self) -> Result<(), SidecarError> {
699        let mut state = self.pair.state.lock().map_err(|_| {
700            SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
701        })?;
702        if self.is_lower_socket {
703            state.lower_write_closed = true;
704        } else {
705            state.higher_write_closed = true;
706        }
707        self.pair.ready.notify_all();
708        Ok(())
709    }
710
711    fn close_endpoint(&self) -> Result<(), SidecarError> {
712        let mut state = self.pair.state.lock().map_err(|_| {
713            SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
714        })?;
715        if self.is_lower_socket {
716            state.lower_write_closed = true;
717            state.lower_closed = true;
718        } else {
719            state.higher_write_closed = true;
720            state.higher_closed = true;
721        }
722        self.pair.ready.notify_all();
723        Ok(())
724    }
725}
726
727fn parse_tls_client_hello_from_bytes(
728    buffer: &[u8],
729) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
730    if buffer.is_empty() {
731        return Ok(None);
732    }
733
734    let mut acceptor = rustls::server::Acceptor::default();
735    let mut cursor = Cursor::new(buffer);
736    acceptor.read_tls(&mut cursor).map_err(sidecar_net_error)?;
737    let Some(accepted) = acceptor.accept().map_err(|(error, _)| {
738        SidecarError::Execution(format!("failed to parse TLS client hello: {error}"))
739    })?
740    else {
741        return Ok(None);
742    };
743    let client_hello = accepted.client_hello();
744    let alpn_protocols = client_hello.alpn().map(|protocols| {
745        protocols
746            .filter_map(|protocol| String::from_utf8(protocol.to_vec()).ok())
747            .collect::<Vec<_>>()
748    });
749    Ok(Some(JavascriptTlsClientHello {
750        servername: client_hello.server_name().map(str::to_owned),
751        alpn_protocols,
752    }))
753}
754
755fn peek_loopback_tls_client_hello(
756    vm_id: &str,
757    socket_id: SocketId,
758    peer_socket_id: SocketId,
759) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
760    let key = loopback_tls_transport_key(vm_id, socket_id, peer_socket_id);
761    let registry = loopback_tls_transport_registry();
762    let pair = registry
763        .lock()
764        .map_err(|_| {
765            SidecarError::InvalidState(String::from(
766                "loopback TLS transport registry lock poisoned",
767            ))
768        })?
769        .get(&key)
770        .and_then(Weak::upgrade);
771    let Some(pair) = pair else {
772        return Ok(None);
773    };
774    let is_lower_socket = socket_id <= peer_socket_id;
775    let state = pair.state.lock().map_err(|_| {
776        SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
777    })?;
778    let buffered = if is_lower_socket {
779        state.higher_to_lower.iter().copied().collect::<Vec<_>>()
780    } else {
781        state.lower_to_higher.iter().copied().collect::<Vec<_>>()
782    };
783    drop(state);
784    parse_tls_client_hello_from_bytes(&buffered)
785}
786
787fn wait_for_loopback_peer_socket_id(
788    kernel: &SidecarKernel,
789    socket_id: SocketId,
790) -> Option<SocketId> {
791    for _ in 0..50 {
792        if let Some(peer_socket_id) = kernel
793            .socket_get(socket_id)
794            .and_then(|record| record.peer_socket_id())
795        {
796            return Some(peer_socket_id);
797        }
798        std::thread::sleep(Duration::from_millis(10));
799    }
800    None
801}
802
803impl Drop for crate::state::LoopbackTlsEndpoint {
804    fn drop(&mut self) {
805        let _ = self.close_endpoint();
806
807        // Eagerly prune this endpoint's registry entry once we are the last owner
808        // of the shared transport pair. Without this, the `Weak` entry survives
809        // until the next `loopback_tls_endpoint()` call runs its lazy `retain()`,
810        // so dead entries accumulate under intermittent use. We must NOT remove
811        // the entry while a peer endpoint still shares the pair, otherwise a later
812        // connection for the same socket pair would fail to find it and build a
813        // mismatched fresh pair.
814        let Some(key) = self.registry_key.take() else {
815            return;
816        };
817        let Ok(mut transports) = loopback_tls_transport_registry().lock() else {
818            // Lock poisoned: leave the entry for the lazy `retain()` to reclaim.
819            return;
820        };
821        let should_remove = match transports.get(&key) {
822            // Only prune when the registered entry still points at *our* pair and
823            // `self` is its last strong owner. During `Drop` `self.pair` is still
824            // alive, so a strong count of 1 (after dropping the temporary upgrade)
825            // means no other endpoint references it.
826            Some(weak) => match weak.upgrade() {
827                Some(existing) => {
828                    let same_pair = Arc::ptr_eq(&existing, &self.pair);
829                    drop(existing);
830                    same_pair && Arc::strong_count(&self.pair) <= 1
831                }
832                None => true,
833            },
834            None => false,
835        };
836        if should_remove {
837            transports.remove(&key);
838        }
839    }
840}
841
842impl Read for crate::state::LoopbackTlsEndpoint {
843    fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
844        let mut state = self
845            .pair
846            .state
847            .lock()
848            .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
849
850        loop {
851            let (peer_write_closed, peer_closed) = if self.is_lower_socket {
852                (state.higher_write_closed, state.higher_closed)
853            } else {
854                (state.lower_write_closed, state.lower_closed)
855            };
856
857            let incoming = if self.is_lower_socket {
858                &mut state.higher_to_lower
859            } else {
860                &mut state.lower_to_higher
861            };
862
863            if !incoming.is_empty() {
864                let mut count = 0;
865                while count < buffer.len() {
866                    let Some(byte) = incoming.pop_front() else {
867                        break;
868                    };
869                    buffer[count] = byte;
870                    count += 1;
871                }
872                return Ok(count);
873            }
874
875            if peer_write_closed || peer_closed {
876                return Ok(0);
877            }
878
879            let (next_state, wait_result) = self
880                .pair
881                .ready
882                .wait_timeout(state, TCP_SOCKET_POLL_TIMEOUT)
883                .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
884            state = next_state;
885            if wait_result.timed_out() {
886                return Err(std::io::Error::new(
887                    std::io::ErrorKind::WouldBlock,
888                    "loopback TLS transport read timed out",
889                ));
890            }
891        }
892    }
893}
894
895impl Write for crate::state::LoopbackTlsEndpoint {
896    fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
897        let mut state = self
898            .pair
899            .state
900            .lock()
901            .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
902
903        let peer_closed = if self.is_lower_socket {
904            state.higher_closed
905        } else {
906            state.lower_closed
907        };
908        let outgoing = if self.is_lower_socket {
909            &mut state.lower_to_higher
910        } else {
911            &mut state.higher_to_lower
912        };
913        if peer_closed {
914            return Err(std::io::Error::new(
915                std::io::ErrorKind::BrokenPipe,
916                "loopback TLS peer is closed",
917            ));
918        }
919
920        outgoing.extend(buffer.iter().copied());
921        self.pair.ready.notify_all();
922        Ok(buffer.len())
923    }
924
925    fn flush(&mut self) -> std::io::Result<()> {
926        Ok(())
927    }
928}
929
930#[cfg(test)]
931mod loopback_tls_registry_tests {
932    use super::{
933        loopback_tls_endpoint, loopback_tls_registry_contains, loopback_tls_transport_key,
934    };
935
936    // Each test uses a unique vm_id so the process-global registry stays
937    // partitioned across concurrently running tests.
938
939    #[test]
940    fn dropping_endpoint_removes_its_registry_entry() {
941        let vm_id = "loopback-tls-drop-removes-entry";
942        let key = loopback_tls_transport_key(vm_id, 1, 2);
943
944        let endpoint = loopback_tls_endpoint(vm_id, 1, 2).expect("create endpoint");
945        assert!(
946            loopback_tls_registry_contains(&key),
947            "registry should contain the key while the endpoint is alive"
948        );
949
950        drop(endpoint);
951        assert!(
952            !loopback_tls_registry_contains(&key),
953            "registry entry must be pruned in the endpoint's Drop, not left for the lazy retain()"
954        );
955    }
956
957    #[test]
958    fn registry_entry_survives_until_last_peer_endpoint_drops() {
959        let vm_id = "loopback-tls-shared-pair";
960        let key = loopback_tls_transport_key(vm_id, 3, 4);
961
962        // Both peers of a loopback connection share the same transport pair.
963        let lower = loopback_tls_endpoint(vm_id, 3, 4).expect("create lower endpoint");
964        let higher = loopback_tls_endpoint(vm_id, 4, 3).expect("create higher endpoint");
965        assert!(loopback_tls_registry_contains(&key));
966
967        // Dropping one peer must keep the entry, since the other peer still owns
968        // the shared pair and a later connection must be able to find it.
969        drop(lower);
970        assert!(
971            loopback_tls_registry_contains(&key),
972            "entry must survive while a peer endpoint still shares the pair"
973        );
974
975        drop(higher);
976        assert!(
977            !loopback_tls_registry_contains(&key),
978            "entry must be pruned once the last peer endpoint drops"
979        );
980    }
981}
982
983// TCP types moved to crate::state
984
985struct ActiveTcpConnectRequest<'a, B> {
986    bridge: &'a SharedBridge<B>,
987    kernel: &'a mut SidecarKernel,
988    kernel_pid: u32,
989    vm_id: &'a str,
990    dns: &'a VmDnsConfig,
991    host: &'a str,
992    port: u16,
993    local_address: Option<&'a str>,
994    local_port: Option<u16>,
995    local_reservation: Option<(JavascriptSocketFamily, u16)>,
996    context: &'a JavascriptSocketPathContext,
997}
998
999struct ActiveUdpSendToRequest<'a, B> {
1000    bridge: &'a SharedBridge<B>,
1001    kernel: &'a mut SidecarKernel,
1002    kernel_pid: u32,
1003    vm_id: &'a str,
1004    dns: &'a VmDnsConfig,
1005    host: &'a str,
1006    port: u16,
1007    context: &'a JavascriptSocketPathContext,
1008    contents: &'a [u8],
1009}
1010
1011struct UdpRemoteAddrRequest<'a, B> {
1012    bridge: &'a SharedBridge<B>,
1013    kernel: &'a SidecarKernel,
1014    vm_id: &'a str,
1015    dns: &'a VmDnsConfig,
1016    host: &'a str,
1017    port: u16,
1018    family: JavascriptUdpFamily,
1019    context: &'a JavascriptSocketPathContext,
1020}
1021
1022pub(crate) struct JavascriptSyncRpcServiceRequest<'a, B> {
1023    pub(crate) bridge: &'a SharedBridge<B>,
1024    pub(crate) vm_id: &'a str,
1025    pub(crate) dns: &'a VmDnsConfig,
1026    pub(crate) socket_paths: &'a JavascriptSocketPathContext,
1027    pub(crate) kernel: &'a mut SidecarKernel,
1028    pub(crate) process: &'a mut ActiveProcess,
1029    pub(crate) sync_request: &'a JavascriptSyncRpcRequest,
1030    pub(crate) resource_limits: &'a ResourceLimits,
1031    pub(crate) network_counts: NetworkResourceCounts,
1032}
1033
1034pub(crate) struct JavascriptNetSyncRpcServiceRequest<'a, B> {
1035    pub(crate) bridge: &'a SharedBridge<B>,
1036    pub(crate) vm_id: &'a str,
1037    pub(crate) dns: &'a VmDnsConfig,
1038    pub(crate) socket_paths: &'a JavascriptSocketPathContext,
1039    pub(crate) kernel: &'a mut SidecarKernel,
1040    pub(crate) process: &'a mut ActiveProcess,
1041    pub(crate) sync_request: &'a JavascriptSyncRpcRequest,
1042    pub(crate) resource_limits: &'a ResourceLimits,
1043    pub(crate) network_counts: NetworkResourceCounts,
1044}
1045
1046struct LoopbackHttpResponseWaitRequest<'a, B> {
1047    bridge: &'a SharedBridge<B>,
1048    vm_id: &'a str,
1049    dns: &'a VmDnsConfig,
1050    socket_paths: &'a JavascriptSocketPathContext,
1051    kernel: &'a mut SidecarKernel,
1052    process: &'a mut ActiveProcess,
1053    resource_limits: &'a ResourceLimits,
1054    request_key: (u64, u64),
1055}
1056
1057pub(crate) struct LoopbackHttpDispatchRequest<'a, B> {
1058    pub(crate) bridge: &'a SharedBridge<B>,
1059    pub(crate) vm_id: &'a str,
1060    pub(crate) dns: &'a VmDnsConfig,
1061    pub(crate) socket_paths: &'a JavascriptSocketPathContext,
1062    pub(crate) kernel: &'a mut SidecarKernel,
1063    pub(crate) process: &'a mut ActiveProcess,
1064    pub(crate) resource_limits: &'a ResourceLimits,
1065    pub(crate) server_id: u64,
1066    pub(crate) request_json: &'a str,
1067}
1068
1069struct JavascriptDgramSyncRpcServiceRequest<'a, B> {
1070    bridge: &'a SharedBridge<B>,
1071    kernel: &'a mut SidecarKernel,
1072    vm_id: &'a str,
1073    dns: &'a VmDnsConfig,
1074    socket_paths: &'a JavascriptSocketPathContext,
1075    process: &'a mut ActiveProcess,
1076    sync_request: &'a JavascriptSyncRpcRequest,
1077    resource_limits: &'a ResourceLimits,
1078    network_counts: NetworkResourceCounts,
1079}
1080
1081struct JavascriptHttp2SyncRpcServiceRequest<'a, B> {
1082    bridge: &'a SharedBridge<B>,
1083    kernel: &'a mut SidecarKernel,
1084    vm_id: &'a str,
1085    dns: &'a VmDnsConfig,
1086    socket_paths: &'a JavascriptSocketPathContext,
1087    process: &'a mut ActiveProcess,
1088    sync_request: &'a JavascriptSyncRpcRequest,
1089    resource_limits: &'a ResourceLimits,
1090    network_counts: NetworkResourceCounts,
1091}
1092
1093impl ActiveTcpSocket {
1094    fn connect<B>(request: ActiveTcpConnectRequest<'_, B>) -> Result<Self, SidecarError>
1095    where
1096        B: NativeSidecarBridge + Send + 'static,
1097        BridgeError<B>: fmt::Debug + Send + Sync + 'static,
1098    {
1099        let ActiveTcpConnectRequest {
1100            bridge,
1101            kernel,
1102            kernel_pid,
1103            vm_id,
1104            dns,
1105            host,
1106            port,
1107            local_address,
1108            local_port,
1109            local_reservation,
1110            context,
1111        } = request;
1112        let resolved = resolve_tcp_connect_addr(bridge, kernel, vm_id, dns, host, port, context)?;
1113        if resolved.use_kernel_loopback {
1114            let family = JavascriptSocketFamily::from_ip(resolved.guest_remote_addr.ip());
1115            let requested_local_port = local_port.unwrap_or(0);
1116            let local_port = if requested_local_port != 0
1117                && local_reservation == Some((family, requested_local_port))
1118            {
1119                requested_local_port
1120            } else {
1121                allocate_guest_listen_port(
1122                    requested_local_port,
1123                    family,
1124                    &context.used_tcp_guest_ports,
1125                    context.listen_policy,
1126                )?
1127            };
1128            let local_ip = match (family, local_address) {
1129                (JavascriptSocketFamily::Ipv4, Some("0.0.0.0")) => {
1130                    IpAddr::V4(Ipv4Addr::UNSPECIFIED)
1131                }
1132                (JavascriptSocketFamily::Ipv4, Some("127.0.0.1") | Some("localhost") | None) => {
1133                    IpAddr::V4(Ipv4Addr::LOCALHOST)
1134                }
1135                (JavascriptSocketFamily::Ipv6, Some("::")) => IpAddr::V6(Ipv6Addr::UNSPECIFIED),
1136                (JavascriptSocketFamily::Ipv6, Some("::1") | Some("localhost") | None) => {
1137                    IpAddr::V6(Ipv6Addr::LOCALHOST)
1138                }
1139                (JavascriptSocketFamily::Ipv4, Some(other)) => {
1140                    return Err(SidecarError::Execution(format!(
1141                        "EACCES: TCP sockets must bind to loopback or unspecified addresses, got {other}"
1142                    )));
1143                }
1144                (JavascriptSocketFamily::Ipv6, Some(other)) => {
1145                    return Err(SidecarError::Execution(format!(
1146                        "EACCES: TCP sockets must bind to loopback or unspecified addresses, got {other}"
1147                    )));
1148                }
1149            };
1150            let local_addr = SocketAddr::new(local_ip, local_port);
1151            let spec = match family {
1152                JavascriptSocketFamily::Ipv4 => SocketSpec::tcp(),
1153                JavascriptSocketFamily::Ipv6 => {
1154                    SocketSpec::new(SocketDomain::Inet6, SocketType::Stream)
1155                }
1156            };
1157            let socket_id = kernel
1158                .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
1159                .map_err(kernel_error)?;
1160            kernel
1161                .socket_bind_inet(
1162                    EXECUTION_DRIVER_NAME,
1163                    kernel_pid,
1164                    socket_id,
1165                    InetSocketAddress::new(local_ip.to_string(), local_port),
1166                )
1167                .map_err(kernel_error)?;
1168            kernel
1169                .socket_connect_inet_loopback(
1170                    EXECUTION_DRIVER_NAME,
1171                    kernel_pid,
1172                    socket_id,
1173                    InetSocketAddress::new(
1174                        resolved.guest_remote_addr.ip().to_string(),
1175                        resolved.guest_remote_addr.port(),
1176                    ),
1177                )
1178                .map_err(kernel_error)?;
1179            return Ok(Self::from_kernel(
1180                socket_id,
1181                None,
1182                local_addr,
1183                resolved.guest_remote_addr,
1184            ));
1185        }
1186
1187        let stream = TcpStream::connect_timeout(&resolved.actual_addr, Duration::from_secs(30))
1188            .map_err(sidecar_net_error)?;
1189        let guest_local_addr = stream.local_addr().map_err(sidecar_net_error)?;
1190        Self::from_stream(stream, None, guest_local_addr, resolved.guest_remote_addr)
1191    }
1192
1193    fn from_stream(
1194        stream: TcpStream,
1195        listener_id: Option<String>,
1196        guest_local_addr: SocketAddr,
1197        guest_remote_addr: SocketAddr,
1198    ) -> Result<Self, SidecarError> {
1199        let read_stream = stream.try_clone().map_err(sidecar_net_error)?;
1200        read_stream
1201            .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
1202            .map_err(sidecar_net_error)?;
1203        let stream = Arc::new(Mutex::new(stream));
1204        let pending_read_stream = Arc::new(Mutex::new(Some(read_stream)));
1205        let (sender, events) = mpsc::channel();
1206        let tls_mode = Arc::new(AtomicBool::new(false));
1207        let tls_stream = Arc::new(Mutex::new(None));
1208        let tls_state = Arc::new(Mutex::new(None));
1209        let saw_local_shutdown = Arc::new(AtomicBool::new(false));
1210        let saw_remote_end = Arc::new(AtomicBool::new(false));
1211        let close_notified = Arc::new(AtomicBool::new(false));
1212
1213        Ok(Self {
1214            stream: Some(stream),
1215            pending_read_stream: Some(pending_read_stream),
1216            events: Some(events),
1217            event_sender: Some(sender),
1218            kernel_socket_id: None,
1219            no_delay: false,
1220            keep_alive: false,
1221            keep_alive_initial_delay_secs: None,
1222            guest_local_addr,
1223            guest_remote_addr,
1224            listener_id,
1225            tls_mode,
1226            tls_stream,
1227            tls_state,
1228            saw_local_shutdown,
1229            saw_remote_end,
1230            close_notified,
1231        })
1232    }
1233
1234    fn from_kernel(
1235        socket_id: SocketId,
1236        listener_id: Option<String>,
1237        guest_local_addr: SocketAddr,
1238        guest_remote_addr: SocketAddr,
1239    ) -> Self {
1240        let (sender, events) = mpsc::channel();
1241        Self {
1242            stream: None,
1243            pending_read_stream: None,
1244            events: Some(events),
1245            event_sender: Some(sender),
1246            kernel_socket_id: Some(socket_id),
1247            no_delay: false,
1248            keep_alive: false,
1249            keep_alive_initial_delay_secs: None,
1250            guest_local_addr,
1251            guest_remote_addr,
1252            listener_id,
1253            tls_mode: Arc::new(AtomicBool::new(false)),
1254            tls_stream: Arc::new(Mutex::new(None)),
1255            tls_state: Arc::new(Mutex::new(None)),
1256            saw_local_shutdown: Arc::new(AtomicBool::new(false)),
1257            saw_remote_end: Arc::new(AtomicBool::new(false)),
1258            close_notified: Arc::new(AtomicBool::new(false)),
1259        }
1260    }
1261
1262    fn poll(
1263        &mut self,
1264        kernel: &mut SidecarKernel,
1265        kernel_pid: u32,
1266        wait: Duration,
1267    ) -> Result<Option<JavascriptTcpSocketEvent>, SidecarError> {
1268        if self.tls_mode.load(Ordering::SeqCst) {
1269            self.ensure_tcp_reader()?;
1270            return match self
1271                .events
1272                .as_ref()
1273                .ok_or_else(|| {
1274                    SidecarError::InvalidState(String::from("TCP socket event channel missing"))
1275                })?
1276                .recv_timeout(wait)
1277            {
1278                Ok(event) => Ok(Some(event)),
1279                Err(RecvTimeoutError::Timeout) => Ok(None),
1280                Err(RecvTimeoutError::Disconnected) => Ok(None),
1281            };
1282        }
1283
1284        if let Some(socket_id) = self.kernel_socket_id {
1285            let result = kernel
1286                .poll_targets(
1287                    EXECUTION_DRIVER_NAME,
1288                    kernel_pid,
1289                    vec![PollTargetEntry::socket(
1290                        socket_id,
1291                        POLLIN | POLLHUP | POLLERR,
1292                    )],
1293                    i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
1294                )
1295                .map_err(kernel_error)?;
1296            let revents = result
1297                .targets
1298                .first()
1299                .map(|entry| entry.revents)
1300                .unwrap_or_else(PollEvents::empty);
1301            if revents.is_empty() {
1302                return Ok(None);
1303            }
1304            if revents.intersects(POLLIN) {
1305                return match kernel.socket_read(
1306                    EXECUTION_DRIVER_NAME,
1307                    kernel_pid,
1308                    socket_id,
1309                    64 * 1024,
1310                ) {
1311                    Ok(Some(bytes)) if !bytes.is_empty() => {
1312                        Ok(Some(JavascriptTcpSocketEvent::Data(bytes)))
1313                    }
1314                    Ok(Some(_)) => Ok(Some(JavascriptTcpSocketEvent::Data(Vec::new()))),
1315                    Ok(None) => Ok(Some(JavascriptTcpSocketEvent::End)),
1316                    Err(error) if error.code() == "EAGAIN" => Ok(None),
1317                    Err(error) => Ok(Some(JavascriptTcpSocketEvent::Error {
1318                        code: Some(error.code().to_string()),
1319                        message: error.to_string(),
1320                    })),
1321                };
1322            }
1323            if revents.intersects(POLLHUP) {
1324                return Ok(Some(JavascriptTcpSocketEvent::End));
1325            }
1326            if revents.intersects(POLLERR) {
1327                return Ok(Some(JavascriptTcpSocketEvent::Error {
1328                    code: Some(String::from("EPIPE")),
1329                    message: String::from("kernel TCP socket reported POLLERR"),
1330                }));
1331            }
1332            return Ok(None);
1333        }
1334
1335        self.ensure_tcp_reader()?;
1336        match self
1337            .events
1338            .as_ref()
1339            .ok_or_else(|| {
1340                SidecarError::InvalidState(String::from("TCP socket event channel missing"))
1341            })?
1342            .recv_timeout(wait)
1343        {
1344            Ok(event) => Ok(Some(event)),
1345            Err(RecvTimeoutError::Timeout) => Ok(None),
1346            Err(RecvTimeoutError::Disconnected) => Ok(None),
1347        }
1348    }
1349
1350    fn ensure_tcp_reader(&self) -> Result<(), SidecarError> {
1351        if self.kernel_socket_id.is_some() {
1352            return Ok(());
1353        }
1354        if self.tls_mode.load(Ordering::SeqCst) {
1355            return Ok(());
1356        }
1357        let read_stream = self
1358            .pending_read_stream
1359            .as_ref()
1360            .ok_or_else(|| {
1361                SidecarError::InvalidState(String::from("TCP socket reader handle missing"))
1362            })?
1363            .lock()
1364            .map_err(|_| {
1365                SidecarError::InvalidState(String::from("TCP socket reader lock poisoned"))
1366            })?
1367            .take();
1368        if let Some(read_stream) = read_stream {
1369            spawn_tcp_socket_reader(
1370                read_stream,
1371                self.event_sender
1372                    .as_ref()
1373                    .ok_or_else(|| {
1374                        SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1375                    })?
1376                    .clone(),
1377                Arc::clone(&self.tls_mode),
1378                Arc::clone(&self.saw_local_shutdown),
1379                Arc::clone(&self.saw_remote_end),
1380                Arc::clone(&self.close_notified),
1381            );
1382        }
1383        Ok(())
1384    }
1385
1386    fn socket_info(&self) -> Value {
1387        json!({
1388            "localAddress": self.guest_local_addr.ip().to_string(),
1389            "localPort": self.guest_local_addr.port(),
1390            "localFamily": socket_addr_family(&self.guest_local_addr),
1391            "remoteAddress": self.guest_remote_addr.ip().to_string(),
1392            "remotePort": self.guest_remote_addr.port(),
1393            "remoteFamily": socket_addr_family(&self.guest_remote_addr),
1394        })
1395    }
1396
1397    fn set_no_delay(&mut self, enable: bool) -> Result<(), SidecarError> {
1398        self.no_delay = enable;
1399        if self.kernel_socket_id.is_some() {
1400            return Ok(());
1401        }
1402        let stream = self
1403            .stream
1404            .as_ref()
1405            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1406            .lock()
1407            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1408        stream.set_nodelay(enable).map_err(sidecar_net_error)
1409    }
1410
1411    fn set_keep_alive(
1412        &mut self,
1413        enable: bool,
1414        initial_delay_secs: Option<u64>,
1415    ) -> Result<(), SidecarError> {
1416        self.keep_alive = enable;
1417        self.keep_alive_initial_delay_secs = initial_delay_secs;
1418        if self.kernel_socket_id.is_some() {
1419            return Ok(());
1420        }
1421        let stream = self
1422            .stream
1423            .as_ref()
1424            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1425            .lock()
1426            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1427        let socket = SockRef::from(&*stream);
1428        socket.set_keepalive(enable).map_err(sidecar_net_error)?;
1429        if enable {
1430            if let Some(delay_secs) = initial_delay_secs.filter(|delay_secs| *delay_secs > 0) {
1431                socket
1432                    .set_tcp_keepalive(
1433                        &TcpKeepalive::new().with_time(Duration::from_secs(delay_secs)),
1434                    )
1435                    .map_err(sidecar_net_error)?;
1436            }
1437        }
1438        Ok(())
1439    }
1440
1441    fn upgrade_tls(
1442        &self,
1443        vm_id: &str,
1444        kernel: &SidecarKernel,
1445        options: JavascriptTlsBridgeOptions,
1446    ) -> Result<(), SidecarError> {
1447        if self.tls_mode.load(Ordering::SeqCst) {
1448            return Ok(());
1449        }
1450
1451        let client_hello = if options.is_server {
1452            self.peek_tls_client_hello(vm_id, kernel)?
1453        } else {
1454            None
1455        };
1456
1457        let tls_stream = if let Some(socket_id) = self.kernel_socket_id {
1458            let peer_socket_id = wait_for_loopback_peer_socket_id(kernel, socket_id)
1459                .ok_or_else(|| {
1460                    SidecarError::Execution(format!(
1461                        "ERR_NOT_IMPLEMENTED: kernel-backed loopback socket {socket_id} has no peer for TLS upgrade"
1462                    ))
1463                })?;
1464            let endpoint = loopback_tls_endpoint(vm_id, socket_id, peer_socket_id)?;
1465            if options.is_server {
1466                ActiveTlsStream::LoopbackServer(build_server_loopback_tls_stream(
1467                    endpoint, &options,
1468                )?)
1469            } else {
1470                ActiveTlsStream::LoopbackClient(build_client_loopback_tls_stream(
1471                    endpoint, &options,
1472                )?)
1473            }
1474        } else {
1475            self.pending_read_stream
1476                .as_ref()
1477                .ok_or_else(|| {
1478                    SidecarError::InvalidState(String::from("TCP socket reader handle missing"))
1479                })?
1480                .lock()
1481                .map_err(|_| {
1482                    SidecarError::InvalidState(String::from("TCP socket reader lock poisoned"))
1483                })?
1484                .take();
1485            let stream = self
1486                .stream
1487                .as_ref()
1488                .ok_or_else(|| {
1489                    SidecarError::InvalidState(String::from("TCP socket stream missing"))
1490                })?
1491                .lock()
1492                .map_err(|_| {
1493                    SidecarError::InvalidState(String::from("TCP socket lock poisoned"))
1494                })?;
1495            let cloned = stream.try_clone().map_err(sidecar_net_error)?;
1496            drop(stream);
1497
1498            if options.is_server {
1499                ActiveTlsStream::Server(build_server_tls_stream(cloned, &options)?)
1500            } else {
1501                ActiveTlsStream::Client(build_client_tls_stream(cloned, &options)?)
1502            }
1503        };
1504
1505        let tls_state = ActiveTlsState {
1506            client_hello,
1507            local_certificates: tls_local_certificates(&options)?,
1508            session_reused: false,
1509        };
1510
1511        self.tls_mode.store(true, Ordering::SeqCst);
1512        {
1513            let mut state = self
1514                .tls_state
1515                .lock()
1516                .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?;
1517            *state = Some(tls_state);
1518        }
1519        {
1520            let mut stream = self.tls_stream.lock().map_err(|_| {
1521                SidecarError::InvalidState(String::from("TLS stream lock poisoned"))
1522            })?;
1523            *stream = Some(tls_stream);
1524        }
1525
1526        spawn_tls_socket_reader(
1527            Arc::clone(&self.tls_stream),
1528            self.event_sender
1529                .as_ref()
1530                .ok_or_else(|| {
1531                    SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1532                })?
1533                .clone(),
1534            Arc::clone(&self.saw_local_shutdown),
1535            Arc::clone(&self.saw_remote_end),
1536            Arc::clone(&self.close_notified),
1537        );
1538        Ok(())
1539    }
1540
1541    fn peek_tls_client_hello(
1542        &self,
1543        vm_id: &str,
1544        kernel: &SidecarKernel,
1545    ) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
1546        if let Some(socket_id) = self.kernel_socket_id {
1547            let Some(peer_socket_id) = kernel
1548                .socket_get(socket_id)
1549                .and_then(|record| record.peer_socket_id())
1550            else {
1551                return Ok(None);
1552            };
1553            return peek_loopback_tls_client_hello(vm_id, socket_id, peer_socket_id);
1554        }
1555
1556        let stream = self
1557            .stream
1558            .as_ref()
1559            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1560            .lock()
1561            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1562        let mut buffer = vec![0_u8; 16 * 1024];
1563        let bytes = match stream.peek(&mut buffer) {
1564            Ok(0) => return Ok(None),
1565            Ok(bytes) => bytes,
1566            Err(error)
1567                if matches!(
1568                    error.kind(),
1569                    std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
1570                ) =>
1571            {
1572                return Ok(None);
1573            }
1574            Err(error) => return Err(sidecar_net_error(error)),
1575        };
1576        parse_tls_client_hello_from_bytes(&buffer[..bytes])
1577    }
1578
1579    fn tls_client_hello_json(
1580        &self,
1581        vm_id: &str,
1582        kernel: &SidecarKernel,
1583    ) -> Result<Value, SidecarError> {
1584        if let Some(client_hello) = self
1585            .tls_state
1586            .lock()
1587            .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?
1588            .as_ref()
1589            .and_then(|state| state.client_hello.clone())
1590        {
1591            return javascript_net_json_string(
1592                serde_json::to_value(client_hello).map_err(|error| {
1593                    SidecarError::InvalidState(format!(
1594                        "failed to serialize TLS client hello: {error}"
1595                    ))
1596                })?,
1597                "net.socket_get_tls_client_hello",
1598            );
1599        }
1600
1601        javascript_net_json_string(
1602            serde_json::to_value(
1603                self.peek_tls_client_hello(vm_id, kernel)?
1604                    .unwrap_or_default(),
1605            )
1606            .map_err(|error| {
1607                SidecarError::InvalidState(format!("failed to serialize TLS client hello: {error}"))
1608            })?,
1609            "net.socket_get_tls_client_hello",
1610        )
1611    }
1612
1613    fn tls_query(&self, query: &str, detailed: bool) -> Result<Value, SidecarError> {
1614        let state = self
1615            .tls_state
1616            .lock()
1617            .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?
1618            .clone();
1619        let mut tls_stream = self
1620            .tls_stream
1621            .lock()
1622            .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?;
1623        let Some(stream) = tls_stream.as_mut() else {
1624            return javascript_net_json_string(
1625                tls_bridge_undefined_value(),
1626                "net.socket_tls_query",
1627            );
1628        };
1629
1630        let payload = match query {
1631            "getSession" => tls_bridge_undefined_value(),
1632            "isSessionReused" => Value::Bool(
1633                state
1634                    .as_ref()
1635                    .is_some_and(|tls_state| tls_state.session_reused),
1636            ),
1637            "getPeerCertificate" => {
1638                let certificate = stream
1639                    .peer_certificates()
1640                    .and_then(|certificates| certificates.first())
1641                    .map(|certificate| {
1642                        tls_certificate_bridge_value(certificate.as_ref(), detailed)
1643                    });
1644                certificate.unwrap_or_else(tls_bridge_undefined_value)
1645            }
1646            "getCertificate" => state
1647                .as_ref()
1648                .and_then(|tls_state| tls_state.local_certificates.first())
1649                .map(|certificate| tls_certificate_bridge_value(certificate, detailed))
1650                .unwrap_or_else(tls_bridge_undefined_value),
1651            "getProtocol" => stream
1652                .protocol_version()
1653                .map(tls_protocol_name)
1654                .map(Value::String)
1655                .unwrap_or(Value::Null),
1656            "getCipher" => stream
1657                .negotiated_cipher_suite()
1658                .map(tls_cipher_bridge_value)
1659                .unwrap_or_else(tls_bridge_undefined_value),
1660            other => {
1661                return Err(SidecarError::InvalidState(format!(
1662                    "unsupported TLS query {other}"
1663                )));
1664            }
1665        };
1666        javascript_net_json_string(payload, "net.socket_tls_query")
1667    }
1668
1669    fn write_all(
1670        &self,
1671        kernel: &mut SidecarKernel,
1672        kernel_pid: u32,
1673        contents: &[u8],
1674    ) -> Result<usize, SidecarError> {
1675        if self.tls_mode.load(Ordering::SeqCst) {
1676            let mut tls_stream = self.tls_stream.lock().map_err(|_| {
1677                SidecarError::InvalidState(String::from("TLS stream lock poisoned"))
1678            })?;
1679            let stream = tls_stream.as_mut().ok_or_else(|| {
1680                SidecarError::InvalidState(String::from("TLS stream missing for upgraded socket"))
1681            })?;
1682            stream.write_all(contents)?;
1683            return Ok(contents.len());
1684        }
1685        if let Some(socket_id) = self.kernel_socket_id {
1686            return kernel
1687                .socket_write(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, contents)
1688                .map_err(kernel_error);
1689        }
1690
1691        let mut stream = self
1692            .stream
1693            .as_ref()
1694            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1695            .lock()
1696            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1697        stream.write_all(contents).map_err(sidecar_net_error)?;
1698        Ok(contents.len())
1699    }
1700
1701    fn shutdown_write(
1702        &self,
1703        kernel: &mut SidecarKernel,
1704        kernel_pid: u32,
1705    ) -> Result<(), SidecarError> {
1706        if self.tls_mode.load(Ordering::SeqCst) {
1707            if let Some(stream) = self
1708                .tls_stream
1709                .lock()
1710                .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?
1711                .as_mut()
1712            {
1713                let _ = stream.send_close_notify();
1714                let _ = stream.shutdown_write();
1715            }
1716            if self.kernel_socket_id.is_some() {
1717                self.saw_local_shutdown.store(true, Ordering::SeqCst);
1718                return Ok(());
1719            }
1720        }
1721        if let Some(socket_id) = self.kernel_socket_id {
1722            return kernel
1723                .socket_shutdown(
1724                    EXECUTION_DRIVER_NAME,
1725                    kernel_pid,
1726                    socket_id,
1727                    KernelSocketShutdown::Write,
1728                )
1729                .map_err(kernel_error);
1730        }
1731        let stream = self
1732            .stream
1733            .as_ref()
1734            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1735            .lock()
1736            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1737        self.saw_local_shutdown.store(true, Ordering::SeqCst);
1738        match stream.shutdown(Shutdown::Write) {
1739            Ok(()) => {}
1740            Err(error) if error.kind() == std::io::ErrorKind::NotConnected => {}
1741            Err(error) => return Err(sidecar_net_error(error)),
1742        }
1743        if self.saw_remote_end.load(Ordering::SeqCst)
1744            && !self.close_notified.swap(true, Ordering::SeqCst)
1745        {
1746            let _ = self
1747                .event_sender
1748                .as_ref()
1749                .ok_or_else(|| {
1750                    SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1751                })?
1752                .send(JavascriptTcpSocketEvent::Close { had_error: false });
1753        }
1754        Ok(())
1755    }
1756
1757    fn close(&self, kernel: &mut SidecarKernel, kernel_pid: u32) -> Result<(), SidecarError> {
1758        if self.tls_mode.load(Ordering::SeqCst) {
1759            if let Some(stream) = self
1760                .tls_stream
1761                .lock()
1762                .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?
1763                .as_mut()
1764            {
1765                let _ = stream.send_close_notify();
1766                let _ = stream.close();
1767            }
1768            if self.kernel_socket_id.is_some() {
1769                return Ok(());
1770            }
1771        }
1772        if let Some(socket_id) = self.kernel_socket_id {
1773            return kernel
1774                .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id)
1775                .map_err(kernel_error);
1776        }
1777        let stream = self
1778            .stream
1779            .as_ref()
1780            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1781            .lock()
1782            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1783        stream.shutdown(Shutdown::Both).map_err(sidecar_net_error)
1784    }
1785}
1786
1787impl ActiveTlsStream {
1788    fn write_all(&mut self, contents: &[u8]) -> Result<(), SidecarError> {
1789        match self {
1790            Self::Client(stream) => {
1791                stream.write_all(contents).map_err(sidecar_net_error)?;
1792                stream.flush().map_err(sidecar_net_error)
1793            }
1794            Self::Server(stream) => {
1795                stream.write_all(contents).map_err(sidecar_net_error)?;
1796                stream.flush().map_err(sidecar_net_error)
1797            }
1798            Self::LoopbackClient(stream) => {
1799                stream.write_all(contents).map_err(sidecar_net_error)?;
1800                stream.flush().map_err(sidecar_net_error)
1801            }
1802            Self::LoopbackServer(stream) => {
1803                stream.write_all(contents).map_err(sidecar_net_error)?;
1804                stream.flush().map_err(sidecar_net_error)
1805            }
1806        }
1807    }
1808
1809    fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
1810        match self {
1811            Self::Client(stream) => stream.read(buffer),
1812            Self::Server(stream) => stream.read(buffer),
1813            Self::LoopbackClient(stream) => stream.read(buffer),
1814            Self::LoopbackServer(stream) => stream.read(buffer),
1815        }
1816    }
1817
1818    fn send_close_notify(&mut self) -> Result<(), SidecarError> {
1819        match self {
1820            Self::Client(stream) => {
1821                stream.conn.send_close_notify();
1822                let _ = stream.conn.complete_io(&mut stream.sock);
1823            }
1824            Self::Server(stream) => {
1825                stream.conn.send_close_notify();
1826                let _ = stream.conn.complete_io(&mut stream.sock);
1827            }
1828            Self::LoopbackClient(stream) => {
1829                stream.conn.send_close_notify();
1830                let _ = stream.conn.complete_io(&mut stream.sock);
1831            }
1832            Self::LoopbackServer(stream) => {
1833                stream.conn.send_close_notify();
1834                let _ = stream.conn.complete_io(&mut stream.sock);
1835            }
1836        }
1837        Ok(())
1838    }
1839
1840    fn shutdown_write(&mut self) -> Result<(), SidecarError> {
1841        match self {
1842            Self::Client(stream) => stream
1843                .sock
1844                .shutdown(Shutdown::Write)
1845                .map_err(sidecar_net_error),
1846            Self::Server(stream) => stream
1847                .sock
1848                .shutdown(Shutdown::Write)
1849                .map_err(sidecar_net_error),
1850            Self::LoopbackClient(stream) => stream.sock.shutdown_write(),
1851            Self::LoopbackServer(stream) => stream.sock.shutdown_write(),
1852        }
1853    }
1854
1855    fn close(&mut self) -> Result<(), SidecarError> {
1856        match self {
1857            Self::Client(stream) => stream
1858                .sock
1859                .shutdown(Shutdown::Both)
1860                .map_err(sidecar_net_error),
1861            Self::Server(stream) => stream
1862                .sock
1863                .shutdown(Shutdown::Both)
1864                .map_err(sidecar_net_error),
1865            Self::LoopbackClient(stream) => stream.sock.close_endpoint(),
1866            Self::LoopbackServer(stream) => stream.sock.close_endpoint(),
1867        }
1868    }
1869
1870    fn peer_certificates(&self) -> Option<&[CertificateDer<'static>]> {
1871        match self {
1872            Self::Client(stream) => stream.conn.peer_certificates(),
1873            Self::Server(stream) => stream.conn.peer_certificates(),
1874            Self::LoopbackClient(stream) => stream.conn.peer_certificates(),
1875            Self::LoopbackServer(stream) => stream.conn.peer_certificates(),
1876        }
1877    }
1878
1879    fn negotiated_cipher_suite(&self) -> Option<rustls::SupportedCipherSuite> {
1880        match self {
1881            Self::Client(stream) => stream.conn.negotiated_cipher_suite(),
1882            Self::Server(stream) => stream.conn.negotiated_cipher_suite(),
1883            Self::LoopbackClient(stream) => stream.conn.negotiated_cipher_suite(),
1884            Self::LoopbackServer(stream) => stream.conn.negotiated_cipher_suite(),
1885        }
1886    }
1887
1888    fn protocol_version(&self) -> Option<rustls::ProtocolVersion> {
1889        match self {
1890            Self::Client(stream) => stream.conn.protocol_version(),
1891            Self::Server(stream) => stream.conn.protocol_version(),
1892            Self::LoopbackClient(stream) => stream.conn.protocol_version(),
1893            Self::LoopbackServer(stream) => stream.conn.protocol_version(),
1894        }
1895    }
1896}
1897
1898// ActiveTcpListener moved to crate::state
1899
1900// Unix socket types moved to crate::state
1901
1902impl ActiveUnixSocket {
1903    fn connect(host_path: &Path, guest_path: &str) -> Result<Self, SidecarError> {
1904        let stream = UnixStream::connect(host_path).map_err(sidecar_net_error)?;
1905        Self::from_stream(stream, None, None, Some(guest_path.to_owned()))
1906    }
1907
1908    fn from_stream(
1909        stream: UnixStream,
1910        listener_id: Option<String>,
1911        local_path: Option<String>,
1912        remote_path: Option<String>,
1913    ) -> Result<Self, SidecarError> {
1914        let read_stream = stream.try_clone().map_err(sidecar_net_error)?;
1915        let stream = Arc::new(Mutex::new(stream));
1916        let (sender, events) = mpsc::channel();
1917        let saw_local_shutdown = Arc::new(AtomicBool::new(false));
1918        let saw_remote_end = Arc::new(AtomicBool::new(false));
1919        let close_notified = Arc::new(AtomicBool::new(false));
1920        spawn_unix_socket_reader(
1921            read_stream,
1922            sender.clone(),
1923            Arc::clone(&saw_local_shutdown),
1924            Arc::clone(&saw_remote_end),
1925            Arc::clone(&close_notified),
1926        );
1927
1928        Ok(Self {
1929            stream,
1930            events,
1931            event_sender: sender,
1932            listener_id,
1933            local_path,
1934            remote_path,
1935            saw_local_shutdown,
1936            saw_remote_end,
1937            close_notified,
1938        })
1939    }
1940
1941    fn poll(&mut self, wait: Duration) -> Result<Option<JavascriptTcpSocketEvent>, SidecarError> {
1942        match self.events.recv_timeout(wait) {
1943            Ok(event) => Ok(Some(event)),
1944            Err(RecvTimeoutError::Timeout) => Ok(None),
1945            Err(RecvTimeoutError::Disconnected) => Ok(None),
1946        }
1947    }
1948
1949    fn socket_info(&self) -> Value {
1950        json!({
1951            "localPath": self.local_path.clone(),
1952            "remotePath": self.remote_path.clone(),
1953        })
1954    }
1955
1956    fn write_all(&self, contents: &[u8]) -> Result<usize, SidecarError> {
1957        let mut stream = self
1958            .stream
1959            .lock()
1960            .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
1961        stream.write_all(contents).map_err(sidecar_net_error)?;
1962        Ok(contents.len())
1963    }
1964
1965    fn shutdown_write(&self) -> Result<(), SidecarError> {
1966        let stream = self
1967            .stream
1968            .lock()
1969            .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
1970        self.saw_local_shutdown.store(true, Ordering::SeqCst);
1971        stream
1972            .shutdown(Shutdown::Write)
1973            .map_err(sidecar_net_error)?;
1974        if self.saw_remote_end.load(Ordering::SeqCst)
1975            && !self.close_notified.swap(true, Ordering::SeqCst)
1976        {
1977            let _ = self
1978                .event_sender
1979                .send(JavascriptTcpSocketEvent::Close { had_error: false });
1980        }
1981        Ok(())
1982    }
1983
1984    fn close(&self) -> Result<(), SidecarError> {
1985        let stream = self
1986            .stream
1987            .lock()
1988            .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
1989        stream.shutdown(Shutdown::Both).map_err(sidecar_net_error)
1990    }
1991}
1992
1993// ActiveUnixListener moved to crate::state
1994
1995impl ActiveUnixListener {
1996    fn bind(
1997        host_path: &Path,
1998        guest_path: &str,
1999        backlog: Option<u32>,
2000    ) -> Result<Self, SidecarError> {
2001        if let Some(parent) = host_path.parent() {
2002            fs::create_dir_all(parent).map_err(sidecar_net_error)?;
2003        }
2004        let listener = UnixListener::bind(host_path).map_err(sidecar_net_error)?;
2005        listener.set_nonblocking(true).map_err(sidecar_net_error)?;
2006        Ok(Self {
2007            listener,
2008            path: guest_path.to_owned(),
2009            backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
2010                .expect("default backlog fits within usize"),
2011            active_connection_ids: BTreeSet::new(),
2012        })
2013    }
2014
2015    fn path(&self) -> &str {
2016        &self.path
2017    }
2018
2019    fn poll(
2020        &mut self,
2021        wait: Duration,
2022    ) -> Result<Option<JavascriptUnixListenerEvent>, SidecarError> {
2023        let deadline = Instant::now() + wait;
2024        loop {
2025            match self.listener.accept() {
2026                Ok((stream, remote_addr)) => {
2027                    if self.active_connection_ids.len() >= self.backlog {
2028                        let _ = stream.shutdown(Shutdown::Both);
2029                        if wait.is_zero() || Instant::now() >= deadline {
2030                            return Ok(None);
2031                        }
2032                        continue;
2033                    }
2034
2035                    let local_path = Some(self.path.clone());
2036                    let remote_path = unix_socket_path(&remote_addr);
2037                    return Ok(Some(JavascriptUnixListenerEvent::Connection(
2038                        PendingUnixSocket {
2039                            stream,
2040                            local_path,
2041                            remote_path,
2042                        },
2043                    )));
2044                }
2045                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
2046                    if wait.is_zero() || Instant::now() >= deadline {
2047                        return Ok(None);
2048                    }
2049                    thread::sleep(Duration::from_millis(10));
2050                }
2051                Err(error) => {
2052                    return Ok(Some(JavascriptUnixListenerEvent::Error {
2053                        code: io_error_code(&error),
2054                        message: error.to_string(),
2055                    }));
2056                }
2057            }
2058        }
2059    }
2060
2061    fn close(&self) -> Result<(), SidecarError> {
2062        Ok(())
2063    }
2064
2065    fn active_connection_count(&self) -> usize {
2066        self.active_connection_ids.len()
2067    }
2068
2069    fn register_connection(&mut self, socket_id: &str) {
2070        self.active_connection_ids.insert(socket_id.to_string());
2071    }
2072
2073    fn release_connection(&mut self, socket_id: &str) {
2074        self.active_connection_ids.remove(socket_id);
2075    }
2076}
2077
2078impl ActiveTcpListener {
2079    fn bind(
2080        bind_host: &str,
2081        guest_host: &str,
2082        guest_port: u16,
2083        backlog: Option<u32>,
2084    ) -> Result<Self, SidecarError> {
2085        let bind_addr = resolve_tcp_bind_addr(bind_host, 0)?;
2086        let guest_addr = resolve_tcp_bind_addr(guest_host, guest_port)?;
2087        let listener = TcpListener::bind(bind_addr).map_err(sidecar_net_error)?;
2088        listener.set_nonblocking(true).map_err(sidecar_net_error)?;
2089        let local_addr = listener.local_addr().map_err(sidecar_net_error)?;
2090        Ok(Self {
2091            listener: Some(listener),
2092            kernel_socket_id: None,
2093            local_addr: Some(local_addr),
2094            guest_local_addr: guest_addr,
2095            backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
2096                .expect("default backlog fits within usize"),
2097            active_connection_ids: BTreeSet::new(),
2098        })
2099    }
2100
2101    fn bind_kernel(
2102        kernel: &mut SidecarKernel,
2103        kernel_pid: u32,
2104        guest_host: &str,
2105        guest_port: u16,
2106        backlog: Option<u32>,
2107    ) -> Result<Self, SidecarError> {
2108        let guest_addr = resolve_tcp_bind_addr(guest_host, guest_port)?;
2109        let spec = match guest_addr {
2110            SocketAddr::V4(_) => SocketSpec::tcp(),
2111            SocketAddr::V6(_) => SocketSpec::new(SocketDomain::Inet6, SocketType::Stream),
2112        };
2113        let socket_id = kernel
2114            .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
2115            .map_err(kernel_error)?;
2116        kernel
2117            .socket_bind_inet(
2118                EXECUTION_DRIVER_NAME,
2119                kernel_pid,
2120                socket_id,
2121                InetSocketAddress::new(guest_addr.ip().to_string(), guest_addr.port()),
2122            )
2123            .map_err(kernel_error)?;
2124        kernel
2125            .socket_listen(
2126                EXECUTION_DRIVER_NAME,
2127                kernel_pid,
2128                socket_id,
2129                usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
2130                    .expect("default backlog fits within usize"),
2131            )
2132            .map_err(kernel_error)?;
2133        Ok(Self {
2134            listener: None,
2135            kernel_socket_id: Some(socket_id),
2136            local_addr: Some(guest_addr),
2137            guest_local_addr: guest_addr,
2138            backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
2139                .expect("default backlog fits within usize"),
2140            active_connection_ids: BTreeSet::new(),
2141        })
2142    }
2143
2144    pub(crate) fn local_addr(&self) -> SocketAddr {
2145        self.local_addr.unwrap_or(self.guest_local_addr)
2146    }
2147
2148    fn guest_local_addr(&self) -> SocketAddr {
2149        self.guest_local_addr
2150    }
2151
2152    fn poll(
2153        &mut self,
2154        kernel: &mut SidecarKernel,
2155        kernel_pid: u32,
2156        wait: Duration,
2157    ) -> Result<Option<JavascriptTcpListenerEvent>, SidecarError> {
2158        if let Some(socket_id) = self.kernel_socket_id {
2159            let result = kernel
2160                .poll_targets(
2161                    EXECUTION_DRIVER_NAME,
2162                    kernel_pid,
2163                    vec![PollTargetEntry::socket(socket_id, POLLIN)],
2164                    i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
2165                )
2166                .map_err(kernel_error)?;
2167            let revents = result
2168                .targets
2169                .first()
2170                .map(|entry| entry.revents)
2171                .unwrap_or_else(PollEvents::empty);
2172            if revents.is_empty() {
2173                return Ok(None);
2174            }
2175            let accepted_socket_id =
2176                match kernel.socket_accept(EXECUTION_DRIVER_NAME, kernel_pid, socket_id) {
2177                    Ok(accepted_socket_id) => accepted_socket_id,
2178                    Err(error) if error.code() == "EAGAIN" => return Ok(None),
2179                    Err(error) => {
2180                        return Ok(Some(JavascriptTcpListenerEvent::Error {
2181                            code: Some(error.code().to_string()),
2182                            message: error.to_string(),
2183                        }));
2184                    }
2185                };
2186            let accepted = kernel.socket_get(accepted_socket_id).ok_or_else(|| {
2187                SidecarError::InvalidState(format!(
2188                    "accepted kernel TCP socket {accepted_socket_id} is missing"
2189                ))
2190            })?;
2191            let local_addr = accepted.local_address().ok_or_else(|| {
2192                SidecarError::InvalidState(format!(
2193                    "accepted kernel TCP socket {accepted_socket_id} missing local address"
2194                ))
2195            })?;
2196            let remote_addr = accepted.peer_address().ok_or_else(|| {
2197                SidecarError::InvalidState(format!(
2198                    "accepted kernel TCP socket {accepted_socket_id} missing peer address"
2199                ))
2200            })?;
2201            return Ok(Some(JavascriptTcpListenerEvent::Connection(
2202                PendingTcpSocket {
2203                    stream: None,
2204                    kernel_socket_id: Some(accepted_socket_id),
2205                    preallocated: true,
2206                    guest_local_addr: resolve_tcp_bind_addr(local_addr.host(), local_addr.port())?,
2207                    guest_remote_addr: resolve_tcp_bind_addr(
2208                        remote_addr.host(),
2209                        remote_addr.port(),
2210                    )?,
2211                },
2212            )));
2213        }
2214
2215        let deadline = Instant::now() + wait;
2216        loop {
2217            match self
2218                .listener
2219                .as_ref()
2220                .ok_or_else(|| {
2221                    SidecarError::InvalidState(String::from("TCP listener socket missing"))
2222                })?
2223                .accept()
2224            {
2225                Ok((stream, remote_addr)) => {
2226                    if self.active_connection_ids.len() >= self.backlog {
2227                        let _ = stream.shutdown(Shutdown::Both);
2228                        if wait.is_zero() || Instant::now() >= deadline {
2229                            return Ok(None);
2230                        }
2231                        continue;
2232                    }
2233                    return Ok(Some(JavascriptTcpListenerEvent::Connection(
2234                        PendingTcpSocket {
2235                            stream: Some(stream),
2236                            kernel_socket_id: None,
2237                            preallocated: false,
2238                            guest_local_addr: self.guest_local_addr,
2239                            guest_remote_addr: SocketAddr::new(
2240                                remote_addr.ip(),
2241                                remote_addr.port(),
2242                            ),
2243                        },
2244                    )));
2245                }
2246                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
2247                    if wait.is_zero() || Instant::now() >= deadline {
2248                        return Ok(None);
2249                    }
2250                    thread::sleep(Duration::from_millis(10));
2251                }
2252                Err(error) => {
2253                    return Ok(Some(JavascriptTcpListenerEvent::Error {
2254                        code: io_error_code(&error),
2255                        message: error.to_string(),
2256                    }));
2257                }
2258            }
2259        }
2260    }
2261
2262    fn close(&self, kernel: &mut SidecarKernel, kernel_pid: u32) -> Result<(), SidecarError> {
2263        if let Some(socket_id) = self.kernel_socket_id {
2264            kernel
2265                .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id)
2266                .map_err(kernel_error)?;
2267        }
2268        Ok(())
2269    }
2270
2271    fn active_connection_count(&self) -> usize {
2272        self.active_connection_ids.len()
2273    }
2274
2275    fn register_connection(&mut self, socket_id: &str) {
2276        self.active_connection_ids.insert(socket_id.to_string());
2277    }
2278
2279    fn release_connection(&mut self, socket_id: &str) {
2280        self.active_connection_ids.remove(socket_id);
2281    }
2282}
2283
2284// UDP types moved to crate::state
2285
2286impl ActiveUdpSocket {
2287    fn new(
2288        kernel: &mut SidecarKernel,
2289        kernel_pid: u32,
2290        family: JavascriptUdpFamily,
2291    ) -> Result<Self, SidecarError> {
2292        let spec = match family {
2293            JavascriptUdpFamily::Ipv4 => SocketSpec::udp(),
2294            JavascriptUdpFamily::Ipv6 => SocketSpec::new(SocketDomain::Inet6, SocketType::Datagram),
2295        };
2296        let socket_id = kernel
2297            .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
2298            .map_err(kernel_error)?;
2299        Ok(Self {
2300            family,
2301            socket: None,
2302            kernel_socket_id: Some(socket_id),
2303            guest_local_addr: None,
2304            recv_buffer_size: 0,
2305            send_buffer_size: 0,
2306        })
2307    }
2308
2309    fn local_addr(&self) -> Option<SocketAddr> {
2310        self.guest_local_addr
2311    }
2312
2313    fn socket(&self) -> Result<&UdpSocket, SidecarError> {
2314        self.socket
2315            .as_ref()
2316            .ok_or_else(|| SidecarError::Execution(String::from("EBADF: bad file descriptor")))
2317    }
2318
2319    fn bind(
2320        &mut self,
2321        kernel: &mut SidecarKernel,
2322        kernel_pid: u32,
2323        host: Option<&str>,
2324        port: u16,
2325        context: &JavascriptSocketPathContext,
2326    ) -> Result<SocketAddr, SidecarError> {
2327        if self.socket.is_some() || self.guest_local_addr.is_some() {
2328            return Err(SidecarError::Execution(String::from(
2329                "EINVAL: secure-exec dgram socket is already bound",
2330            )));
2331        }
2332
2333        let (bind_host, guest_host, guest_family) = normalize_udp_bind_host(host, self.family)?;
2334        let guest_port = allocate_guest_listen_port(
2335            port,
2336            guest_family,
2337            &context.used_udp_guest_ports,
2338            context.listen_policy,
2339        )?;
2340        let local_addr = resolve_udp_bind_addr(guest_host, guest_port, self.family)?;
2341        if let Some(socket_id) = self.kernel_socket_id {
2342            kernel
2343                .socket_bind_inet(
2344                    EXECUTION_DRIVER_NAME,
2345                    kernel_pid,
2346                    socket_id,
2347                    InetSocketAddress::new(local_addr.ip().to_string(), local_addr.port()),
2348                )
2349                .map_err(kernel_error)?;
2350        } else {
2351            let bind_addr = resolve_udp_bind_addr(bind_host, 0, self.family)?;
2352            let socket = UdpSocket::bind(bind_addr).map_err(sidecar_net_error)?;
2353            socket.set_nonblocking(true).map_err(sidecar_net_error)?;
2354            self.socket = Some(socket);
2355        }
2356        self.guest_local_addr = Some(local_addr);
2357        Ok(local_addr)
2358    }
2359
2360    fn ensure_bound_for_send(
2361        &mut self,
2362        kernel: &mut SidecarKernel,
2363        kernel_pid: u32,
2364        context: &JavascriptSocketPathContext,
2365    ) -> Result<SocketAddr, SidecarError> {
2366        if let Some(local_addr) = self.local_addr() {
2367            return Ok(local_addr);
2368        }
2369
2370        self.bind(kernel, kernel_pid, None, 0, context)
2371    }
2372
2373    fn send_to<B>(
2374        &mut self,
2375        request: ActiveUdpSendToRequest<'_, B>,
2376    ) -> Result<(usize, SocketAddr), SidecarError>
2377    where
2378        B: NativeSidecarBridge + Send + 'static,
2379        BridgeError<B>: fmt::Debug + Send + Sync + 'static,
2380    {
2381        let ActiveUdpSendToRequest {
2382            bridge,
2383            kernel,
2384            kernel_pid,
2385            vm_id,
2386            dns,
2387            host,
2388            port,
2389            context,
2390            contents,
2391        } = request;
2392        let remote_addr = resolve_udp_addr(UdpRemoteAddrRequest {
2393            bridge,
2394            kernel,
2395            vm_id,
2396            dns,
2397            host,
2398            port,
2399            family: self.family,
2400            context,
2401        })?;
2402        let local_addr = self.ensure_bound_for_send(kernel, kernel_pid, context)?;
2403        let written = if let Some(socket_id) = self.kernel_socket_id {
2404            if is_loopback_ip(remote_addr.ip()) && remote_addr.port() == port {
2405                kernel
2406                    .socket_send_to_inet_loopback(
2407                        EXECUTION_DRIVER_NAME,
2408                        kernel_pid,
2409                        socket_id,
2410                        InetSocketAddress::new(remote_addr.ip().to_string(), remote_addr.port()),
2411                        contents,
2412                    )
2413                    .map_err(kernel_error)?
2414            } else {
2415                return Err(SidecarError::Execution(String::from(
2416                    "ERR_NOT_IMPLEMENTED: external UDP datagrams are not yet supported by the kernel-backed V8 bridge",
2417                )));
2418            }
2419        } else {
2420            let socket = self.socket.as_ref().ok_or_else(|| {
2421                SidecarError::InvalidState(String::from("UDP socket is not initialized"))
2422            })?;
2423            socket
2424                .send_to(contents, remote_addr)
2425                .map_err(sidecar_net_error)?
2426        };
2427        Ok((written, local_addr))
2428    }
2429
2430    fn poll(
2431        &self,
2432        kernel: &mut SidecarKernel,
2433        kernel_pid: u32,
2434        wait: Duration,
2435    ) -> Result<Option<JavascriptUdpSocketEvent>, SidecarError> {
2436        if let Some(socket_id) = self.kernel_socket_id {
2437            let result = kernel
2438                .poll_targets(
2439                    EXECUTION_DRIVER_NAME,
2440                    kernel_pid,
2441                    vec![PollTargetEntry::socket(socket_id, POLLIN)],
2442                    i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
2443                )
2444                .map_err(kernel_error)?;
2445            let revents = result
2446                .targets
2447                .first()
2448                .map(|entry| entry.revents)
2449                .unwrap_or_else(PollEvents::empty);
2450            if revents.is_empty() {
2451                return Ok(None);
2452            }
2453            return match kernel.socket_recv_datagram(
2454                EXECUTION_DRIVER_NAME,
2455                kernel_pid,
2456                socket_id,
2457                64 * 1024,
2458            ) {
2459                Ok(Some(datagram)) => {
2460                    let (source_address, payload) = datagram.into_parts();
2461                    let remote_addr = source_address
2462                        .map(|source| {
2463                            resolve_udp_bind_addr(source.host(), source.port(), self.family)
2464                        })
2465                        .transpose()?
2466                        .unwrap_or_else(|| match self.family {
2467                            JavascriptUdpFamily::Ipv4 => {
2468                                SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)
2469                            }
2470                            JavascriptUdpFamily::Ipv6 => {
2471                                SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0)
2472                            }
2473                        });
2474                    Ok(Some(JavascriptUdpSocketEvent::Message {
2475                        data: payload,
2476                        remote_addr,
2477                    }))
2478                }
2479                Ok(None) => Ok(None),
2480                Err(error) if error.code() == "EAGAIN" => Ok(None),
2481                Err(error) => Ok(Some(JavascriptUdpSocketEvent::Error {
2482                    code: Some(error.code().to_string()),
2483                    message: error.to_string(),
2484                })),
2485            };
2486        }
2487        let socket = self.socket()?;
2488        let deadline = Instant::now() + wait;
2489        let mut buffer = vec![0_u8; 64 * 1024];
2490
2491        loop {
2492            match socket.recv_from(&mut buffer) {
2493                Ok((bytes_read, remote_addr)) => {
2494                    return Ok(Some(JavascriptUdpSocketEvent::Message {
2495                        data: buffer[..bytes_read].to_vec(),
2496                        remote_addr,
2497                    }));
2498                }
2499                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
2500                    if wait.is_zero() || Instant::now() >= deadline {
2501                        return Ok(None);
2502                    }
2503                    thread::sleep(Duration::from_millis(10));
2504                }
2505                Err(error) => {
2506                    return Ok(Some(JavascriptUdpSocketEvent::Error {
2507                        code: io_error_code(&error),
2508                        message: error.to_string(),
2509                    }));
2510                }
2511            }
2512        }
2513    }
2514
2515    fn close(&mut self, kernel: &mut SidecarKernel, kernel_pid: u32) {
2516        if let Some(socket_id) = self.kernel_socket_id {
2517            let _ = kernel.socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id);
2518        }
2519        self.socket.take();
2520        self.guest_local_addr = None;
2521    }
2522
2523    fn set_buffer_size(&mut self, which: &str, size: usize) -> Result<(), SidecarError> {
2524        match which {
2525            "recv" => self.recv_buffer_size = size,
2526            "send" => self.send_buffer_size = size,
2527            other => {
2528                return Err(SidecarError::InvalidState(format!(
2529                    "unsupported UDP buffer size kind {other}"
2530                )));
2531            }
2532        }
2533        if self.kernel_socket_id.is_some() {
2534            return Ok(());
2535        }
2536        let socket = self.socket()?;
2537        let socket = SockRef::from(socket);
2538        match which {
2539            "recv" => socket.set_recv_buffer_size(size).map_err(sidecar_net_error),
2540            "send" => socket.set_send_buffer_size(size).map_err(sidecar_net_error),
2541            other => Err(SidecarError::InvalidState(format!(
2542                "unsupported UDP buffer size kind {other}"
2543            ))),
2544        }
2545    }
2546
2547    fn get_buffer_size(&self, which: &str) -> Result<usize, SidecarError> {
2548        if self.kernel_socket_id.is_some() {
2549            return Ok(match which {
2550                "recv" => self.recv_buffer_size,
2551                "send" => self.send_buffer_size,
2552                other => {
2553                    return Err(SidecarError::InvalidState(format!(
2554                        "unsupported UDP buffer size kind {other}"
2555                    )));
2556                }
2557            });
2558        }
2559        let socket = self.socket()?;
2560        let socket = SockRef::from(socket);
2561        match which {
2562            "recv" => socket.recv_buffer_size().map_err(sidecar_net_error),
2563            "send" => socket.send_buffer_size().map_err(sidecar_net_error),
2564            other => Err(SidecarError::InvalidState(format!(
2565                "unsupported UDP buffer size kind {other}"
2566            ))),
2567        }
2568    }
2569}
2570
2571// ActiveExecution, ActiveExecutionEvent, SocketQueryKind moved to crate::state
2572
2573impl ActiveExecution {
2574    pub(crate) fn uses_shared_v8_runtime(&self) -> bool {
2575        match self {
2576            Self::Javascript(execution) => execution.uses_shared_v8_runtime(),
2577            Self::Python(execution) => execution.uses_shared_v8_runtime(),
2578            Self::Wasm(execution) => execution.uses_shared_v8_runtime(),
2579            Self::Tool(_) => false,
2580        }
2581    }
2582
2583    pub(crate) fn child_pid(&self) -> u32 {
2584        match self {
2585            Self::Javascript(execution) => execution.child_pid(),
2586            Self::Python(execution) => execution.child_pid(),
2587            Self::Wasm(execution) => execution.child_pid(),
2588            Self::Tool(_) => 0,
2589        }
2590    }
2591
2592    pub(crate) fn write_stdin(&mut self, chunk: &[u8]) -> Result<(), SidecarError> {
2593        match self {
2594            Self::Javascript(execution) => execution
2595                .write_stdin(chunk)
2596                .map_err(|error| SidecarError::Execution(error.to_string())),
2597            Self::Python(execution) => execution
2598                .write_stdin(chunk)
2599                .map_err(|error| SidecarError::Execution(error.to_string())),
2600            Self::Wasm(execution) => execution
2601                .write_stdin(chunk)
2602                .map_err(|error| SidecarError::Execution(error.to_string())),
2603            Self::Tool(_) => Ok(()),
2604        }
2605    }
2606
2607    pub(crate) fn close_stdin(&mut self) -> Result<(), SidecarError> {
2608        match self {
2609            Self::Javascript(execution) => execution
2610                .close_stdin()
2611                .map_err(|error| SidecarError::Execution(error.to_string())),
2612            Self::Python(execution) => execution
2613                .close_stdin()
2614                .map_err(|error| SidecarError::Execution(error.to_string())),
2615            Self::Wasm(execution) => execution
2616                .close_stdin()
2617                .map_err(|error| SidecarError::Execution(error.to_string())),
2618            Self::Tool(_) => Ok(()),
2619        }
2620    }
2621
2622    pub(crate) fn respond_python_vfs_rpc_success(
2623        &mut self,
2624        id: u64,
2625        payload: PythonVfsRpcResponsePayload,
2626    ) -> Result<(), SidecarError> {
2627        match self {
2628            Self::Python(execution) => execution
2629                .respond_vfs_rpc_success(id, payload)
2630                .map_err(|error| SidecarError::Execution(error.to_string())),
2631            _ => Err(SidecarError::InvalidState(String::from(
2632                "only Python executions can service Python VFS RPC responses",
2633            ))),
2634        }
2635    }
2636
2637    pub(crate) fn respond_python_vfs_rpc_error(
2638        &mut self,
2639        id: u64,
2640        code: impl Into<String>,
2641        message: impl Into<String>,
2642    ) -> Result<(), SidecarError> {
2643        match self {
2644            Self::Python(execution) => execution
2645                .respond_vfs_rpc_error(id, code, message)
2646                .map_err(|error| SidecarError::Execution(error.to_string())),
2647            _ => Err(SidecarError::InvalidState(String::from(
2648                "only Python executions can service Python VFS RPC responses",
2649            ))),
2650        }
2651    }
2652
2653    pub(crate) fn send_javascript_stream_event(
2654        &self,
2655        event_type: &str,
2656        payload: Value,
2657    ) -> Result<(), SidecarError> {
2658        match self {
2659            Self::Javascript(execution) => execution
2660                .send_stream_event(event_type, payload)
2661                .map_err(|error| SidecarError::Execution(error.to_string())),
2662            Self::Wasm(execution) => execution
2663                .send_stream_event(event_type, payload)
2664                .map_err(|error| SidecarError::Execution(error.to_string())),
2665            _ => Err(SidecarError::InvalidState(String::from(
2666                "only embedded V8 executions can receive JavaScript stream events",
2667            ))),
2668        }
2669    }
2670
2671    pub(crate) fn javascript_v8_session_handle(&self) -> Option<V8SessionHandle> {
2672        match self {
2673            Self::Javascript(execution) => Some(execution.v8_session_handle()),
2674            Self::Wasm(execution) => Some(execution.v8_session_handle()),
2675            _ => None,
2676        }
2677    }
2678
2679    pub(crate) fn terminate(&mut self) -> Result<(), SidecarError> {
2680        match self {
2681            Self::Javascript(execution) => execution
2682                .terminate()
2683                .map_err(|error| SidecarError::Execution(error.to_string())),
2684            Self::Python(execution) => execution
2685                .kill()
2686                .map_err(|error| SidecarError::Execution(error.to_string())),
2687            Self::Wasm(execution) => execution
2688                .terminate()
2689                .map_err(|error| SidecarError::Execution(error.to_string())),
2690            Self::Tool(_) => Ok(()),
2691        }
2692    }
2693
2694    pub(crate) fn respond_javascript_sync_rpc_success(
2695        &mut self,
2696        id: u64,
2697        result: Value,
2698    ) -> Result<(), SidecarError> {
2699        match self {
2700            Self::Javascript(execution) => execution
2701                .respond_sync_rpc_success(id, result)
2702                .map_err(|error| SidecarError::Execution(error.to_string())),
2703            Self::Python(execution) => execution
2704                .respond_javascript_sync_rpc_success(id, result)
2705                .map_err(|error| SidecarError::Execution(error.to_string())),
2706            Self::Wasm(execution) => execution
2707                .respond_sync_rpc_success(id, result)
2708                .map_err(|error| SidecarError::Execution(error.to_string())),
2709            _ => Err(SidecarError::InvalidState(String::from(
2710                "only JavaScript, Python, and WebAssembly executions can service JavaScript sync RPC responses",
2711            ))),
2712        }
2713    }
2714
2715    pub(crate) fn respond_javascript_sync_rpc_error(
2716        &mut self,
2717        id: u64,
2718        code: impl Into<String>,
2719        message: impl Into<String>,
2720    ) -> Result<(), SidecarError> {
2721        match self {
2722            Self::Javascript(execution) => execution
2723                .respond_sync_rpc_error(id, code, message)
2724                .map_err(|error| SidecarError::Execution(error.to_string())),
2725            Self::Python(execution) => execution
2726                .respond_javascript_sync_rpc_error(id, code, message)
2727                .map_err(|error| SidecarError::Execution(error.to_string())),
2728            Self::Wasm(execution) => execution
2729                .respond_sync_rpc_error(id, code, message)
2730                .map_err(|error| SidecarError::Execution(error.to_string())),
2731            _ => Err(SidecarError::InvalidState(String::from(
2732                "only JavaScript, Python, and WebAssembly executions can service JavaScript sync RPC responses",
2733            ))),
2734        }
2735    }
2736
2737    pub(crate) async fn poll_event(
2738        &mut self,
2739        timeout: Duration,
2740    ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
2741        match self {
2742            Self::Javascript(execution) => execution
2743                .poll_event(timeout)
2744                .await
2745                .map(|event| {
2746                    event.map(|event| match event {
2747                        JavascriptExecutionEvent::Stdout(chunk) => {
2748                            ActiveExecutionEvent::Stdout(chunk)
2749                        }
2750                        JavascriptExecutionEvent::Stderr(chunk) => {
2751                            ActiveExecutionEvent::Stderr(chunk)
2752                        }
2753                        JavascriptExecutionEvent::SyncRpcRequest(request) => {
2754                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2755                        }
2756                        JavascriptExecutionEvent::SignalState {
2757                            signal,
2758                            registration,
2759                        } => ActiveExecutionEvent::SignalState {
2760                            signal,
2761                            registration: map_node_signal_registration(registration),
2762                        },
2763                        JavascriptExecutionEvent::Exited(code) => {
2764                            ActiveExecutionEvent::Exited(code)
2765                        }
2766                    })
2767                })
2768                .map_err(|error| SidecarError::Execution(error.to_string())),
2769            Self::Python(execution) => execution
2770                .poll_event(timeout)
2771                .await
2772                .map(|event| {
2773                    event.map(|event| match event {
2774                        PythonExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2775                        PythonExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2776                        PythonExecutionEvent::JavascriptSyncRpcRequest(request) => {
2777                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2778                        }
2779                        PythonExecutionEvent::VfsRpcRequest(request) => {
2780                            ActiveExecutionEvent::PythonVfsRpcRequest(request)
2781                        }
2782                        PythonExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2783                    })
2784                })
2785                .map_err(|error| SidecarError::Execution(error.to_string())),
2786            Self::Wasm(execution) => execution
2787                .poll_event(timeout)
2788                .await
2789                .map(|event| {
2790                    event.map(|event| match event {
2791                        WasmExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2792                        WasmExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2793                        WasmExecutionEvent::SyncRpcRequest(request) => {
2794                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2795                        }
2796                        WasmExecutionEvent::SignalState {
2797                            signal,
2798                            registration,
2799                        } => ActiveExecutionEvent::SignalState {
2800                            signal,
2801                            registration: map_wasm_signal_registration(registration),
2802                        },
2803                        WasmExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2804                    })
2805                })
2806                .map_err(|error| SidecarError::Execution(error.to_string())),
2807            Self::Tool(execution) => {
2808                let _ = timeout;
2809                poll_tool_process_event(execution)
2810            }
2811        }
2812    }
2813
2814    pub(crate) fn poll_event_blocking(
2815        &mut self,
2816        timeout: Duration,
2817    ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
2818        match self {
2819            Self::Javascript(execution) => execution
2820                .poll_event_blocking(timeout)
2821                .map(|event| {
2822                    event.map(|event| match event {
2823                        JavascriptExecutionEvent::Stdout(chunk) => {
2824                            ActiveExecutionEvent::Stdout(chunk)
2825                        }
2826                        JavascriptExecutionEvent::Stderr(chunk) => {
2827                            ActiveExecutionEvent::Stderr(chunk)
2828                        }
2829                        JavascriptExecutionEvent::SyncRpcRequest(request) => {
2830                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2831                        }
2832                        JavascriptExecutionEvent::SignalState {
2833                            signal,
2834                            registration,
2835                        } => ActiveExecutionEvent::SignalState {
2836                            signal,
2837                            registration: map_node_signal_registration(registration),
2838                        },
2839                        JavascriptExecutionEvent::Exited(code) => {
2840                            ActiveExecutionEvent::Exited(code)
2841                        }
2842                    })
2843                })
2844                .map_err(|error| SidecarError::Execution(error.to_string())),
2845            Self::Python(execution) => execution
2846                .poll_event_blocking(timeout)
2847                .map(|event| {
2848                    event.map(|event| match event {
2849                        PythonExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2850                        PythonExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2851                        PythonExecutionEvent::JavascriptSyncRpcRequest(request) => {
2852                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2853                        }
2854                        PythonExecutionEvent::VfsRpcRequest(request) => {
2855                            ActiveExecutionEvent::PythonVfsRpcRequest(request)
2856                        }
2857                        PythonExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2858                    })
2859                })
2860                .map_err(|error| SidecarError::Execution(error.to_string())),
2861            Self::Wasm(execution) => execution
2862                .poll_event_blocking(timeout)
2863                .map(|event| {
2864                    event.map(|event| match event {
2865                        WasmExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2866                        WasmExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2867                        WasmExecutionEvent::SyncRpcRequest(request) => {
2868                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2869                        }
2870                        WasmExecutionEvent::SignalState {
2871                            signal,
2872                            registration,
2873                        } => ActiveExecutionEvent::SignalState {
2874                            signal,
2875                            registration: map_wasm_signal_registration(registration),
2876                        },
2877                        WasmExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2878                    })
2879                })
2880                .map_err(|error| SidecarError::Execution(error.to_string())),
2881            Self::Tool(execution) => {
2882                let _ = timeout;
2883                poll_tool_process_event(execution)
2884            }
2885        }
2886    }
2887}
2888
2889struct ToolProcessEventRequest {
2890    sidecar_requests: SharedSidecarRequestClient,
2891    connection_id: String,
2892    session_id: String,
2893    vm_id: String,
2894    tool_resolution: ToolCommandResolution,
2895    cancelled: Arc<AtomicBool>,
2896    pending_events: Arc<Mutex<VecDeque<ActiveExecutionEvent>>>,
2897    events_overflowed: Arc<AtomicBool>,
2898}
2899
2900pub(crate) fn send_tool_process_event(
2901    pending_events: &Arc<Mutex<VecDeque<ActiveExecutionEvent>>>,
2902    events_overflowed: &AtomicBool,
2903    event: ActiveExecutionEvent,
2904) -> bool {
2905    let mut pending_events = pending_events
2906        .lock()
2907        .unwrap_or_else(|poisoned| poisoned.into_inner());
2908    if pending_events.len() >= MAX_PROCESS_EVENT_QUEUE {
2909        events_overflowed.store(true, Ordering::Relaxed);
2910        return false;
2911    }
2912    pending_events.push_back(event);
2913    true
2914}
2915
2916fn spawn_tool_process_events(request: ToolProcessEventRequest) {
2917    let ToolProcessEventRequest {
2918        sidecar_requests,
2919        connection_id,
2920        session_id,
2921        vm_id,
2922        tool_resolution,
2923        cancelled,
2924        pending_events,
2925        events_overflowed,
2926    } = request;
2927    std::thread::spawn(move || match tool_resolution {
2928        ToolCommandResolution::Failure(message) => {
2929            if !send_tool_process_event(
2930                &pending_events,
2931                &events_overflowed,
2932                ActiveExecutionEvent::Stderr(format_tool_failure_output(&message)),
2933            ) {
2934                return;
2935            }
2936            let _ = send_tool_process_event(
2937                &pending_events,
2938                &events_overflowed,
2939                ActiveExecutionEvent::Exited(1),
2940            );
2941        }
2942        ToolCommandResolution::Invoke { request, timeout } => {
2943            let response = sidecar_requests.invoke(
2944                OwnershipScope::vm(connection_id.clone(), session_id.clone(), vm_id.clone()),
2945                SidecarRequestPayload::HostCallback(request.clone()),
2946                timeout,
2947            );
2948            if cancelled.load(Ordering::Relaxed) {
2949                return;
2950            }
2951
2952            match response {
2953                Ok(crate::protocol::SidecarResponsePayload::HostCallbackResult(result)) => {
2954                    if let Some(value) = result.result {
2955                        let value: serde_json::Value = serde_json::from_str(&value)
2956                            .unwrap_or(serde_json::Value::String(value));
2957                        let stdout = serde_json::to_vec(&json!({
2958                            "ok": true,
2959                            "result": value,
2960                        }))
2961                        .unwrap_or_else(|error| {
2962                            format_tool_failure_output(&format!(
2963                                "failed to serialize tool result: {error}"
2964                            ))
2965                        });
2966                        if !send_tool_process_event(
2967                            &pending_events,
2968                            &events_overflowed,
2969                            ActiveExecutionEvent::Stdout(stdout),
2970                        ) {
2971                            return;
2972                        }
2973                        let _ = send_tool_process_event(
2974                            &pending_events,
2975                            &events_overflowed,
2976                            ActiveExecutionEvent::Exited(0),
2977                        );
2978                    } else {
2979                        let message = result
2980                            .error
2981                            .unwrap_or_else(|| String::from("tool invocation returned no result"));
2982                        if !send_tool_process_event(
2983                            &pending_events,
2984                            &events_overflowed,
2985                            ActiveExecutionEvent::Stderr(format_tool_failure_output(&message)),
2986                        ) {
2987                            return;
2988                        }
2989                        let _ = send_tool_process_event(
2990                            &pending_events,
2991                            &events_overflowed,
2992                            ActiveExecutionEvent::Exited(1),
2993                        );
2994                    }
2995                }
2996                Ok(_) => {
2997                    if !send_tool_process_event(
2998                        &pending_events,
2999                        &events_overflowed,
3000                        ActiveExecutionEvent::Stderr(format_tool_failure_output(
3001                            "unexpected sidecar tool response",
3002                        )),
3003                    ) {
3004                        return;
3005                    }
3006                    let _ = send_tool_process_event(
3007                        &pending_events,
3008                        &events_overflowed,
3009                        ActiveExecutionEvent::Exited(1),
3010                    );
3011                }
3012                Err(error) => {
3013                    if !send_tool_process_event(
3014                        &pending_events,
3015                        &events_overflowed,
3016                        ActiveExecutionEvent::Stderr(format_tool_failure_output(
3017                            &error.to_string(),
3018                        )),
3019                    ) {
3020                        return;
3021                    }
3022                    let _ = send_tool_process_event(
3023                        &pending_events,
3024                        &events_overflowed,
3025                        ActiveExecutionEvent::Exited(1),
3026                    );
3027                }
3028            }
3029        }
3030    });
3031}
3032
3033impl<B> NativeSidecar<B>
3034where
3035    B: NativeSidecarBridge + Send + 'static,
3036    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
3037{
3038    pub(crate) async fn execute(
3039        &mut self,
3040        request: &RequestFrame,
3041        payload: ExecuteRequest,
3042    ) -> Result<DispatchResult, SidecarError> {
3043        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3044        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3045
3046        let vm = self
3047            .vms
3048            .get_mut(&vm_id)
3049            .ok_or_else(|| missing_vm_error(&vm_id))?;
3050        if vm.active_processes.contains_key(&payload.process_id) {
3051            return Err(SidecarError::InvalidState(format!(
3052                "VM {vm_id} already has an active process with id {}",
3053                payload.process_id
3054            )));
3055        }
3056
3057        if let Some(command) = payload.command.as_deref() {
3058            if let Some(tool_resolution) =
3059                resolve_tool_command(vm, command, &payload.args, payload.cwd.as_deref())?
3060            {
3061                let guest_cwd = payload
3062                    .cwd
3063                    .as_deref()
3064                    .map(normalize_path)
3065                    .unwrap_or_else(|| vm.guest_cwd.clone());
3066                let kernel_handle = vm
3067                    .kernel
3068                    .create_virtual_process(
3069                        EXECUTION_DRIVER_NAME,
3070                        TOOL_DRIVER_NAME,
3071                        command,
3072                        std::iter::once(command.to_owned())
3073                            .chain(payload.args.iter().cloned())
3074                            .collect(),
3075                        VirtualProcessOptions {
3076                            env: vm.guest_env.clone(),
3077                            cwd: Some(guest_cwd.clone()),
3078                            ..VirtualProcessOptions::default()
3079                        },
3080                    )
3081                    .map_err(kernel_error)?;
3082                let kernel_pid = kernel_handle.pid();
3083                let tool_execution = ToolExecution::default();
3084                let cancelled = tool_execution.cancelled.clone();
3085                let pending_events = tool_execution.pending_events.clone();
3086                let events_overflowed = tool_execution.events_overflowed.clone();
3087                vm.active_processes.insert(
3088                    payload.process_id.clone(),
3089                    ActiveProcess::new(
3090                        kernel_pid,
3091                        kernel_handle,
3092                        GuestRuntimeKind::JavaScript,
3093                        ActiveExecution::Tool(tool_execution),
3094                    )
3095                    .with_guest_cwd(guest_cwd.clone())
3096                    .with_host_cwd(resolve_vm_guest_path_to_host(vm, &guest_cwd)),
3097                );
3098                self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy)?;
3099                spawn_tool_process_events(ToolProcessEventRequest {
3100                    sidecar_requests: self.sidecar_requests.clone(),
3101                    connection_id: connection_id.clone(),
3102                    session_id: session_id.clone(),
3103                    vm_id: vm_id.clone(),
3104                    tool_resolution,
3105                    cancelled,
3106                    pending_events,
3107                    events_overflowed,
3108                });
3109
3110                return Ok(DispatchResult {
3111                    response: self.respond(
3112                        request,
3113                        ResponsePayload::ProcessStarted(ProcessStartedResponse {
3114                            process_id: payload.process_id,
3115                            pid: Some(kernel_pid),
3116                        }),
3117                    ),
3118                    events: Vec::new(),
3119                });
3120            }
3121        }
3122
3123        let resolved = resolve_execute_request(vm, &payload)?;
3124        let mut env = resolved.env.clone();
3125        let sandbox_root = normalize_host_path(&vm.cwd);
3126        env.insert(
3127            String::from(EXECUTION_SANDBOX_ROOT_ENV),
3128            sandbox_root.to_string_lossy().into_owned(),
3129        );
3130        if resolved.runtime == GuestRuntimeKind::JavaScript {
3131            env.insert(
3132                String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
3133                String::from("1"),
3134            );
3135        } else if resolved.runtime == GuestRuntimeKind::WebAssembly {
3136            env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
3137        }
3138        let argv = std::iter::once(resolved.entrypoint.clone())
3139            .chain(resolved.execution_args.iter().cloned())
3140            .collect::<Vec<_>>();
3141        let kernel_handle = vm
3142            .kernel
3143            .spawn_process(
3144                &resolved.command,
3145                argv,
3146                SpawnOptions {
3147                    requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
3148                    cwd: Some(resolved.guest_cwd.clone()),
3149                    ..SpawnOptions::default()
3150                },
3151            )
3152            .map_err(kernel_error)?;
3153        let kernel_pid = kernel_handle.pid();
3154
3155        let (execution, process_env) = match resolved.runtime {
3156            GuestRuntimeKind::JavaScript => {
3157                let inline_code = load_javascript_entrypoint_source(
3158                    vm,
3159                    &resolved.host_cwd,
3160                    &resolved.entrypoint,
3161                    &env,
3162                );
3163                prepare_javascript_shadow(vm, &resolved)?;
3164
3165                let context =
3166                    self.javascript_engine
3167                        .create_context(CreateJavascriptContextRequest {
3168                            vm_id: vm_id.clone(),
3169                            bootstrap_module: None,
3170                            compile_cache_root: Some(self.cache_root.join("node-compile-cache")),
3171                        });
3172                let built_reader = build_module_reader(vm, &resolved);
3173                let guest_reader = built_reader.clone().map(|reader| {
3174                    Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader))
3175                        as Box<dyn GuestModuleReader>
3176                });
3177                let module_reader =
3178                    built_reader.map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
3179                let execution = self
3180                    .javascript_engine
3181                    .start_execution_with_module_reader(
3182                        StartJavascriptExecutionRequest {
3183                            guest_runtime: guest_runtime_identity(vm, None, None),
3184                            vm_id: vm_id.clone(),
3185                            context_id: context.context_id,
3186                            argv: std::iter::once(resolved.entrypoint.clone())
3187                                .chain(resolved.execution_args.iter().cloned())
3188                                .collect(),
3189                            env: env.clone(),
3190                            cwd: resolved.host_cwd.clone(),
3191                            limits: javascript_execution_limits(vm),
3192                            inline_code,
3193                        },
3194                        module_reader,
3195                        guest_reader,
3196                    )
3197                    .map_err(javascript_error)?;
3198                (ActiveExecution::Javascript(execution), env.clone())
3199            }
3200            GuestRuntimeKind::Python => {
3201                let python_file_path = python_file_entrypoint(&resolved.entrypoint);
3202                let pyodide_dist_path = self
3203                    .python_engine
3204                    .bundled_pyodide_dist_path_for_vm(&vm_id)
3205                    .map_err(python_error)?;
3206                let pyodide_cache_path = pyodide_dist_path
3207                    .parent()
3208                    .and_then(Path::parent)
3209                    .unwrap_or(pyodide_dist_path.as_path())
3210                    .join("pyodide-package-cache");
3211                add_runtime_guest_path_mapping(
3212                    &mut env,
3213                    PYTHON_PYODIDE_GUEST_ROOT,
3214                    &pyodide_dist_path,
3215                );
3216                add_runtime_guest_path_mapping(
3217                    &mut env,
3218                    PYTHON_PYODIDE_CACHE_GUEST_ROOT,
3219                    &pyodide_cache_path,
3220                );
3221                add_runtime_host_access_path(
3222                    &mut env,
3223                    "AGENTOS_EXTRA_FS_READ_PATHS",
3224                    &pyodide_dist_path,
3225                    true,
3226                );
3227                add_runtime_host_access_path(
3228                    &mut env,
3229                    "AGENTOS_EXTRA_FS_READ_PATHS",
3230                    &pyodide_cache_path,
3231                    true,
3232                );
3233                add_runtime_host_access_path(
3234                    &mut env,
3235                    "AGENTOS_EXTRA_FS_WRITE_PATHS",
3236                    &pyodide_cache_path,
3237                    false,
3238                );
3239                let context = self
3240                    .python_engine
3241                    .create_context(CreatePythonContextRequest {
3242                        vm_id: vm_id.clone(),
3243                        pyodide_dist_path,
3244                    });
3245                let execution = self
3246                    .python_engine
3247                    .start_execution(StartPythonExecutionRequest {
3248                        vm_id: vm_id.clone(),
3249                        context_id: context.context_id,
3250                        code: resolved.entrypoint.clone(),
3251                        file_path: python_file_path,
3252                        env: env.clone(),
3253                        cwd: resolved.host_cwd.clone(),
3254                        limits: python_execution_limits(vm),
3255                        guest_runtime: guest_runtime_identity(vm, None, None),
3256                    })
3257                    .map_err(python_error)?;
3258                (ActiveExecution::Python(execution), env.clone())
3259            }
3260            GuestRuntimeKind::WebAssembly => {
3261                let wasm_limits = wasm_execution_limits(vm);
3262                let wasm_guest_runtime =
3263                    guest_runtime_identity(vm, Some(u64::from(kernel_pid)), Some(0));
3264                let wasm_permission_tier = resolved.wasm_permission_tier.unwrap_or_else(|| {
3265                    resolve_wasm_permission_tier(
3266                        vm,
3267                        Some(&resolved.command),
3268                        None,
3269                        &resolved.entrypoint,
3270                    )
3271                });
3272                let context = self.wasm_engine.create_context(CreateWasmContextRequest {
3273                    vm_id: vm_id.clone(),
3274                    module_path: Some(resolved.entrypoint.clone()),
3275                });
3276                let execution = self
3277                    .wasm_engine
3278                    .start_execution(StartWasmExecutionRequest {
3279                        vm_id: vm_id.clone(),
3280                        context_id: context.context_id,
3281                        argv: resolved.process_args.clone(),
3282                        env: env.clone(),
3283                        cwd: resolved.host_cwd.clone(),
3284                        permission_tier: execution_wasm_permission_tier(wasm_permission_tier),
3285                        limits: wasm_limits,
3286                        guest_runtime: wasm_guest_runtime,
3287                    })
3288                    .map_err(wasm_error)?;
3289                (ActiveExecution::Wasm(Box::new(execution)), env)
3290            }
3291        };
3292        let child_pid = execution.child_pid();
3293        let kernel_stdin_writer_fd = install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?;
3294        vm.active_processes.insert(
3295            payload.process_id.clone(),
3296            ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
3297                .with_kernel_stdin_writer_fd(kernel_stdin_writer_fd)
3298                .with_guest_cwd(resolved.guest_cwd.clone())
3299                .with_env(process_env)
3300                .with_host_cwd(resolved.host_cwd.clone()),
3301        );
3302        self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy)?;
3303
3304        Ok(DispatchResult {
3305            response: self.respond(
3306                request,
3307                ResponsePayload::ProcessStarted(ProcessStartedResponse {
3308                    process_id: payload.process_id,
3309                    pid: Some(if child_pid == 0 {
3310                        kernel_pid
3311                    } else {
3312                        child_pid
3313                    }),
3314                }),
3315            ),
3316            events: Vec::new(),
3317        })
3318    }
3319
3320    pub(crate) async fn write_stdin(
3321        &mut self,
3322        request: &RequestFrame,
3323        payload: WriteStdinRequest,
3324    ) -> Result<DispatchResult, SidecarError> {
3325        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3326        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3327
3328        let vm = self
3329            .vms
3330            .get_mut(&vm_id)
3331            .ok_or_else(|| missing_vm_error(&vm_id))?;
3332        let process = vm
3333            .active_processes
3334            .get_mut(&payload.process_id)
3335            .ok_or_else(|| {
3336                SidecarError::InvalidState(format!(
3337                    "VM {vm_id} has no active process {}",
3338                    payload.process_id
3339                ))
3340            })?;
3341        process.execution.write_stdin(&payload.chunk)?;
3342        write_kernel_process_stdin(&mut vm.kernel, process, &payload.chunk)?;
3343
3344        Ok(DispatchResult {
3345            response: self.respond(
3346                request,
3347                ResponsePayload::StdinWritten(StdinWrittenResponse {
3348                    process_id: payload.process_id,
3349                    accepted_bytes: payload.chunk.len() as u64,
3350                }),
3351            ),
3352            events: Vec::new(),
3353        })
3354    }
3355
3356    pub(crate) async fn close_stdin(
3357        &mut self,
3358        request: &RequestFrame,
3359        payload: CloseStdinRequest,
3360    ) -> Result<DispatchResult, SidecarError> {
3361        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3362        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3363
3364        let vm = self
3365            .vms
3366            .get_mut(&vm_id)
3367            .ok_or_else(|| missing_vm_error(&vm_id))?;
3368        let process = vm
3369            .active_processes
3370            .get_mut(&payload.process_id)
3371            .ok_or_else(|| {
3372                SidecarError::InvalidState(format!(
3373                    "VM {vm_id} has no active process {}",
3374                    payload.process_id
3375                ))
3376            })?;
3377        process.execution.close_stdin()?;
3378        close_kernel_process_stdin(&mut vm.kernel, process)?;
3379
3380        Ok(DispatchResult {
3381            response: self.respond(
3382                request,
3383                ResponsePayload::StdinClosed(StdinClosedResponse {
3384                    process_id: payload.process_id,
3385                }),
3386            ),
3387            events: Vec::new(),
3388        })
3389    }
3390
3391    pub(crate) async fn kill_process(
3392        &mut self,
3393        request: &RequestFrame,
3394        payload: KillProcessRequest,
3395    ) -> Result<DispatchResult, SidecarError> {
3396        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3397        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3398        self.kill_process_internal(&vm_id, &payload.process_id, &payload.signal)?;
3399
3400        Ok(DispatchResult {
3401            response: self.respond(
3402                request,
3403                ResponsePayload::ProcessKilled(ProcessKilledResponse {
3404                    process_id: payload.process_id,
3405                }),
3406            ),
3407            events: Vec::new(),
3408        })
3409    }
3410
3411    pub(crate) async fn find_listener(
3412        &mut self,
3413        request: &RequestFrame,
3414        payload: FindListenerRequest,
3415    ) -> Result<DispatchResult, SidecarError> {
3416        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3417        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3418        require_vm_inspection_permission(
3419            &self.bridge,
3420            &vm_id,
3421            "network.inspect",
3422            "network",
3423            &socket_query_resource(SocketQueryKind::TcpListener, &payload),
3424        )?;
3425
3426        let listener =
3427            find_socket_state_entry(self.vms.get(&vm_id), SocketQueryKind::TcpListener, &payload)?;
3428
3429        Ok(DispatchResult {
3430            response: self.respond(
3431                request,
3432                ResponsePayload::ListenerSnapshot(ListenerSnapshotResponse { listener }),
3433            ),
3434            events: Vec::new(),
3435        })
3436    }
3437
3438    pub(crate) async fn get_process_snapshot(
3439        &mut self,
3440        request: &RequestFrame,
3441        _payload: GetProcessSnapshotRequest,
3442    ) -> Result<DispatchResult, SidecarError> {
3443        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3444        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3445        require_vm_inspection_permission(
3446            &self.bridge,
3447            &vm_id,
3448            "process.inspect",
3449            "process",
3450            "process://snapshot",
3451        )?;
3452
3453        let processes = self
3454            .vms
3455            .get_mut(&vm_id)
3456            .map(|vm| {
3457                prune_exited_process_snapshots(vm);
3458                snapshot_vm_processes(vm)
3459            })
3460            .unwrap_or_default();
3461
3462        Ok(DispatchResult {
3463            response: self.respond(
3464                request,
3465                ResponsePayload::ProcessSnapshot(ProcessSnapshotResponse { processes }),
3466            ),
3467            events: Vec::new(),
3468        })
3469    }
3470
3471    pub(crate) async fn find_bound_udp(
3472        &mut self,
3473        request: &RequestFrame,
3474        payload: FindBoundUdpRequest,
3475    ) -> Result<DispatchResult, SidecarError> {
3476        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3477        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3478
3479        let lookup_request = FindListenerRequest {
3480            host: payload.host,
3481            port: payload.port,
3482            path: None,
3483        };
3484        require_vm_inspection_permission(
3485            &self.bridge,
3486            &vm_id,
3487            "network.inspect",
3488            "network",
3489            &socket_query_resource(SocketQueryKind::UdpBound, &lookup_request),
3490        )?;
3491        let socket = find_socket_state_entry(
3492            self.vms.get(&vm_id),
3493            SocketQueryKind::UdpBound,
3494            &lookup_request,
3495        )?;
3496
3497        Ok(DispatchResult {
3498            response: self.respond(
3499                request,
3500                ResponsePayload::BoundUdpSnapshot(BoundUdpSnapshotResponse { socket }),
3501            ),
3502            events: Vec::new(),
3503        })
3504    }
3505
3506    pub(crate) async fn vm_fetch(
3507        &mut self,
3508        request: &RequestFrame,
3509        payload: VmFetchRequest,
3510    ) -> Result<DispatchResult, SidecarError> {
3511        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3512        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3513
3514        let vm = self
3515            .vms
3516            .get_mut(&vm_id)
3517            .ok_or_else(|| SidecarError::InvalidState(String::from("unknown sidecar VM")))?;
3518        let target_path = if payload.path.starts_with('/') {
3519            payload.path.clone()
3520        } else {
3521            format!("/{}", payload.path)
3522        };
3523        let request_url = Url::parse(&format!("http://127.0.0.1:{}{target_path}", payload.port))
3524            .map_err(|error| {
3525                SidecarError::InvalidState(format!(
3526                    "invalid vm.fetch target {target_path:?}: {error}"
3527                ))
3528            })?;
3529        let header_values: BTreeMap<String, Value> = serde_json::from_str(&payload.headers_json)
3530            .map_err(|error| {
3531                SidecarError::InvalidState(format!(
3532                    "vm.fetch headers_json must be valid JSON: {error}"
3533                ))
3534            })?;
3535        let options = JavascriptHttpRequestOptions {
3536            method: Some(payload.method),
3537            headers: header_values,
3538            body: payload.body,
3539            reject_unauthorized: None,
3540        };
3541        let headers = parse_http_header_collection(&options.headers, "vm.fetch headers")?;
3542        let target_process_id = find_kernel_http_listener_process(vm, payload.port);
3543        if let Some(target_process_id) = target_process_id {
3544            let max_fetch_response_bytes = vm.limits.http.max_fetch_response_bytes;
3545            let response_json = match dispatch_kernel_http_fetch(
3546                &self.bridge,
3547                &vm_id,
3548                vm,
3549                &target_process_id,
3550                payload.port,
3551                &target_path,
3552                &options,
3553                &headers,
3554                max_fetch_response_bytes,
3555            ) {
3556                Ok(response_json) => response_json,
3557                Err(error) => {
3558                    if let Some(exit_code) = kernel_http_fetch_target_exit_code(&error) {
3559                        let _ = vm;
3560                        self.finish_active_process_exit(&vm_id, &target_process_id, exit_code)?;
3561                    }
3562                    return Err(error);
3563                }
3564            };
3565            let response = self.respond(
3566                request,
3567                ResponsePayload::VmFetchResult(VmFetchResponse { response_json }),
3568            );
3569            ensure_vm_fetch_response_frame_within_limit(&response, self.config.max_frame_bytes)?;
3570
3571            return Ok(DispatchResult {
3572                response,
3573                events: Vec::new(),
3574            });
3575        }
3576
3577        let Some((target_process_id, server_id)) =
3578            vm.active_processes
3579                .iter()
3580                .find_map(|(process_id, process)| {
3581                    process
3582                        .http_servers
3583                        .iter()
3584                        .find(|(_, server)| server.guest_local_addr.port() == payload.port)
3585                        .map(|(server_id, _)| (process_id.clone(), *server_id))
3586                })
3587        else {
3588            return Err(SidecarError::Execution(format!(
3589                "vm.fetch could not find a guest HTTP listener on port {}",
3590                payload.port
3591            )));
3592        };
3593        let socket_paths = build_javascript_socket_path_context(vm)?;
3594        let resource_limits = vm.kernel.resource_limits().clone();
3595        let process = vm
3596            .active_processes
3597            .get_mut(&target_process_id)
3598            .ok_or_else(|| {
3599                SidecarError::InvalidState(format!(
3600                    "vm.fetch target process disappeared: {target_process_id}"
3601                ))
3602            })?;
3603        let request_json = serialize_http_loopback_request(&request_url, &options, &headers)?;
3604        let response_json = dispatch_loopback_http_request(LoopbackHttpDispatchRequest {
3605            bridge: &self.bridge,
3606            vm_id: &vm_id,
3607            dns: &vm.dns,
3608            socket_paths: &socket_paths,
3609            kernel: &mut vm.kernel,
3610            process,
3611            resource_limits: &resource_limits,
3612            server_id,
3613            request_json: &request_json,
3614        })?;
3615
3616        let response = self.respond(
3617            request,
3618            ResponsePayload::VmFetchResult(VmFetchResponse { response_json }),
3619        );
3620        ensure_vm_fetch_response_frame_within_limit(&response, self.config.max_frame_bytes)?;
3621
3622        Ok(DispatchResult {
3623            response,
3624            events: Vec::new(),
3625        })
3626    }
3627
3628    pub(crate) async fn get_signal_state(
3629        &mut self,
3630        request: &RequestFrame,
3631        payload: GetSignalStateRequest,
3632    ) -> Result<DispatchResult, SidecarError> {
3633        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3634        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3635
3636        let handlers = self
3637            .vms
3638            .get(&vm_id)
3639            .and_then(|vm| vm.signal_states.get(&payload.process_id))
3640            .cloned()
3641            .unwrap_or_default();
3642
3643        Ok(DispatchResult {
3644            response: self.respond(
3645                request,
3646                ResponsePayload::SignalState(SignalStateResponse {
3647                    process_id: payload.process_id,
3648                    handlers: handlers.into_iter().collect(),
3649                }),
3650            ),
3651            events: Vec::new(),
3652        })
3653    }
3654
3655    pub(crate) async fn get_zombie_timer_count(
3656        &mut self,
3657        request: &RequestFrame,
3658        _payload: GetZombieTimerCountRequest,
3659    ) -> Result<DispatchResult, SidecarError> {
3660        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3661        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3662
3663        let count = self
3664            .vms
3665            .get(&vm_id)
3666            .map(|vm| vm.kernel.zombie_timer_count() as u64)
3667            .unwrap_or_default();
3668
3669        Ok(DispatchResult {
3670            response: self.respond(
3671                request,
3672                ResponsePayload::ZombieTimerCount(ZombieTimerCountResponse { count }),
3673            ),
3674            events: Vec::new(),
3675        })
3676    }
3677
3678    pub(crate) fn kill_process_internal(
3679        &mut self,
3680        vm_id: &str,
3681        process_id: &str,
3682        signal: &str,
3683    ) -> Result<(), SidecarError> {
3684        let signal_name = signal.to_owned();
3685        let signal = parse_signal(signal)?;
3686        let vm = self
3687            .vms
3688            .get_mut(vm_id)
3689            .ok_or_else(|| SidecarError::InvalidState(format!("unknown sidecar VM {vm_id}")))?;
3690        let process = vm.active_processes.get_mut(process_id).ok_or_else(|| {
3691            SidecarError::InvalidState(format!("VM {vm_id} has no active process {process_id}"))
3692        })?;
3693        let kernel_pid = process.kernel_pid;
3694
3695        enum KillBehavior {
3696            Tool,
3697            SharedV8StateOnly,
3698            SharedV8Continue,
3699            SharedV8Terminate,
3700            SharedV8DispatchOrTerminate,
3701            Noop,
3702            HostPid(u32),
3703        }
3704
3705        let behavior = match &process.execution {
3706            ActiveExecution::Tool(_) => KillBehavior::Tool,
3707            ActiveExecution::Javascript(execution)
3708                if execution.uses_shared_v8_runtime() && matches!(signal, 0 | libc::SIGSTOP) =>
3709            {
3710                KillBehavior::SharedV8StateOnly
3711            }
3712            ActiveExecution::Javascript(execution)
3713                if execution.uses_shared_v8_runtime() && signal == libc::SIGCONT =>
3714            {
3715                KillBehavior::SharedV8Continue
3716            }
3717            ActiveExecution::Wasm(execution)
3718                if execution.uses_shared_v8_runtime()
3719                    && matches!(signal, 0 | libc::SIGSTOP | libc::SIGCONT) =>
3720            {
3721                KillBehavior::SharedV8StateOnly
3722            }
3723            ActiveExecution::Python(execution)
3724                if execution.uses_shared_v8_runtime()
3725                    && matches!(signal, 0 | libc::SIGSTOP | libc::SIGCONT) =>
3726            {
3727                KillBehavior::SharedV8StateOnly
3728            }
3729            ActiveExecution::Javascript(execution)
3730                if execution.uses_shared_v8_runtime() && signal == SIGKILL =>
3731            {
3732                KillBehavior::SharedV8Terminate
3733            }
3734            ActiveExecution::Wasm(execution)
3735                if execution.uses_shared_v8_runtime() && signal == SIGKILL =>
3736            {
3737                KillBehavior::SharedV8Terminate
3738            }
3739            ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime() => {
3740                KillBehavior::SharedV8DispatchOrTerminate
3741            }
3742            ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime() => {
3743                KillBehavior::SharedV8Terminate
3744            }
3745            ActiveExecution::Python(execution) if execution.uses_shared_v8_runtime() => {
3746                KillBehavior::SharedV8Terminate
3747            }
3748            ActiveExecution::Javascript(execution) if execution.child_pid() == 0 => {
3749                KillBehavior::Noop
3750            }
3751            _ => KillBehavior::HostPid(process.execution.child_pid()),
3752        };
3753
3754        match behavior {
3755            KillBehavior::Tool => {
3756                let ActiveExecution::Tool(execution) = &process.execution else {
3757                    unreachable!("kill behavior must match tool execution");
3758                };
3759                if signal != 0 {
3760                    execution.cancelled.store(true, Ordering::Relaxed);
3761                    process.queue_pending_execution_event(ActiveExecutionEvent::Exited(
3762                        128 + signal,
3763                    ))?;
3764                }
3765            }
3766            KillBehavior::SharedV8StateOnly => {
3767                if matches!(signal, libc::SIGSTOP | libc::SIGCONT) {
3768                    vm.kernel
3769                        .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal)
3770                        .map_err(kernel_error)?;
3771                }
3772            }
3773            KillBehavior::SharedV8Continue => {
3774                vm.kernel
3775                    .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal)
3776                    .map_err(kernel_error)?;
3777                if signal != 0 && !dispatch_v8_process_signal(process, signal)? {
3778                    process.execution.terminate()?;
3779                }
3780            }
3781            KillBehavior::SharedV8Terminate => {
3782                if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
3783                    close_kernel_process_stdin(&mut vm.kernel, process)?;
3784                }
3785                process.execution.terminate()?;
3786                let needs_synthetic_exit = matches!(process.execution, ActiveExecution::Wasm(_))
3787                    || (signal == SIGKILL
3788                        && matches!(process.execution, ActiveExecution::Javascript(_)));
3789                if signal != 0 && needs_synthetic_exit {
3790                    process.queue_pending_execution_event(ActiveExecutionEvent::Exited(
3791                        128 + signal,
3792                    ))?;
3793                }
3794            }
3795            KillBehavior::SharedV8DispatchOrTerminate => {
3796                if signal != 0 && !dispatch_v8_process_signal(process, signal)? {
3797                    process.execution.terminate()?;
3798                }
3799            }
3800            KillBehavior::Noop => {}
3801            KillBehavior::HostPid(pid) => {
3802                if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
3803                    close_kernel_process_stdin(&mut vm.kernel, process)?;
3804                }
3805                signal_runtime_process(pid, signal)?;
3806            }
3807        }
3808        emit_security_audit_event(
3809            &self.bridge,
3810            vm_id,
3811            "security.process.kill",
3812            audit_fields([
3813                (String::from("source"), String::from("control_plane")),
3814                (String::from("source_pid"), String::from("0")),
3815                (String::from("target_pid"), process.kernel_pid.to_string()),
3816                (String::from("process_id"), process_id.to_owned()),
3817                (String::from("signal"), signal_name),
3818                (
3819                    String::from("host_pid"),
3820                    process.execution.child_pid().to_string(),
3821                ),
3822            ]),
3823        );
3824        Ok(())
3825    }
3826
3827    pub async fn pump_process_events(
3828        &mut self,
3829        ownership: &OwnershipScope,
3830    ) -> Result<bool, SidecarError> {
3831        let mut emitted_any = false;
3832
3833        let mut queued_envelopes = Vec::new();
3834        {
3835            let pending_capacity = self.pending_process_event_capacity();
3836            let receiver = self.process_event_receiver.as_mut().ok_or_else(|| {
3837                SidecarError::InvalidState(String::from("process event receiver unavailable"))
3838            })?;
3839            loop {
3840                if queued_envelopes.len() >= pending_capacity {
3841                    if receiver.is_empty() {
3842                        break;
3843                    }
3844                    return Err(process_event_queue_overflow_error());
3845                }
3846                match receiver.try_recv() {
3847                    Ok(envelope) => {
3848                        queued_envelopes.push(envelope);
3849                        emitted_any = true;
3850                    }
3851                    Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
3852                    Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
3853                }
3854            }
3855        }
3856        for envelope in queued_envelopes {
3857            self.queue_pending_process_event(envelope)?;
3858        }
3859
3860        let vm_ids = self.vm_ids_for_scope(ownership)?;
3861        for vm_id in vm_ids {
3862            while let Some(vm) = self.vms.get(&vm_id) {
3863                let connection_id = vm.connection_id.clone();
3864                let session_id = vm.session_id.clone();
3865                let process_ids = self
3866                    .vms
3867                    .get(&vm_id)
3868                    .map(|vm| vm.active_processes.keys().cloned().collect::<Vec<_>>())
3869                    .unwrap_or_default();
3870                let mut emitted_this_pass = false;
3871
3872                for process_id in process_ids {
3873                    if self
3874                        .vms
3875                        .get(&vm_id)
3876                        .is_some_and(|vm| vm.detached_child_processes.contains(&process_id))
3877                    {
3878                        continue;
3879                    }
3880                    enum ProcessPollResult {
3881                        Event(Box<Option<ActiveExecutionEvent>>),
3882                        RecoverClosedChannel,
3883                    }
3884                    let poll_result = {
3885                        let Some(vm) = self.vms.get_mut(&vm_id) else {
3886                            continue;
3887                        };
3888                        let Some(process) = vm.active_processes.get_mut(&process_id) else {
3889                            continue;
3890                        };
3891                        if let Some(event) = process.pending_execution_events.pop_front() {
3892                            ProcessPollResult::Event(Box::new(Some(event)))
3893                        } else {
3894                            match process.execution.poll_event(Duration::ZERO).await {
3895                                Ok(event) => ProcessPollResult::Event(Box::new(event)),
3896                                Err(SidecarError::Execution(message))
3897                                    if (process.runtime == GuestRuntimeKind::JavaScript
3898                                        && closed_javascript_event_channel(&message))
3899                                        || (process.runtime == GuestRuntimeKind::Python
3900                                            && closed_python_event_channel(&message))
3901                                        || (process.runtime == GuestRuntimeKind::WebAssembly
3902                                            && closed_wasm_event_channel(&message)) =>
3903                                {
3904                                    ProcessPollResult::RecoverClosedChannel
3905                                }
3906                                Err(other) => return Err(other),
3907                            }
3908                        }
3909                    };
3910                    let event = match poll_result {
3911                        ProcessPollResult::Event(event) => *event,
3912                        ProcessPollResult::RecoverClosedChannel => {
3913                            self.recover_closed_root_runtime_process_event(&vm_id, &process_id)?
3914                        }
3915                    };
3916
3917                    let Some(event) = event else {
3918                        continue;
3919                    };
3920
3921                    if Self::internal_execution_event(&event) {
3922                        // These events are sidecar work items, not client-facing
3923                        // process events. Handle them immediately so a sibling
3924                        // process can service sync RPCs while another request
3925                        // waits on VM-local networking.
3926                        self.handle_execution_event(&vm_id, &process_id, event)?;
3927                    } else {
3928                        self.queue_pending_process_event(ProcessEventEnvelope {
3929                            connection_id: connection_id.clone(),
3930                            session_id: session_id.clone(),
3931                            vm_id: vm_id.clone(),
3932                            process_id: process_id.clone(),
3933                            event,
3934                        })?;
3935                    }
3936                    emitted_any = true;
3937                    emitted_this_pass = true;
3938                }
3939
3940                if !emitted_this_pass {
3941                    break;
3942                }
3943            }
3944
3945            if self.pump_detached_child_process_events(&vm_id)? {
3946                emitted_any = true;
3947            }
3948        }
3949
3950        Ok(emitted_any)
3951    }
3952
3953    fn internal_execution_event(event: &ActiveExecutionEvent) -> bool {
3954        matches!(
3955            event,
3956            ActiveExecutionEvent::JavascriptSyncRpcRequest(_)
3957                | ActiveExecutionEvent::PythonVfsRpcRequest(_)
3958                | ActiveExecutionEvent::SignalState { .. }
3959        )
3960    }
3961
3962    fn recover_closed_root_runtime_process_event(
3963        &mut self,
3964        vm_id: &str,
3965        process_id: &str,
3966    ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
3967        let Some(vm) = self.vms.get_mut(vm_id) else {
3968            return Ok(None);
3969        };
3970        let Some(process) = vm.active_processes.get(process_id) else {
3971            return Ok(None);
3972        };
3973        if process.execution.uses_shared_v8_runtime() {
3974            return Ok(None);
3975        }
3976        if process.runtime != GuestRuntimeKind::JavaScript
3977            && process.runtime != GuestRuntimeKind::Python
3978            && process.runtime != GuestRuntimeKind::WebAssembly
3979        {
3980            return Ok(None);
3981        }
3982        let runtime_child_pid = process.execution.child_pid();
3983        if runtime_child_pid == 0 {
3984            return Ok(None);
3985        }
3986        if let Some(status) = runtime_child_exit_status(runtime_child_pid)? {
3987            return Ok(Some(ActiveExecutionEvent::Exited(status)));
3988        }
3989        if runtime_child_is_alive(runtime_child_pid)? {
3990            return Ok(None);
3991        }
3992        Ok(Some(ActiveExecutionEvent::Exited(0)))
3993    }
3994
3995    fn active_process_by_path<'a>(
3996        process: &'a ActiveProcess,
3997        child_path: &[&str],
3998    ) -> Option<&'a ActiveProcess> {
3999        let mut current = process;
4000        for child_id in child_path {
4001            current = current.child_processes.get(*child_id)?;
4002        }
4003        Some(current)
4004    }
4005
4006    fn active_process_by_path_mut<'a>(
4007        process: &'a mut ActiveProcess,
4008        child_path: &[&str],
4009    ) -> Option<&'a mut ActiveProcess> {
4010        let mut current = process;
4011        for child_id in child_path {
4012            current = current.child_processes.get_mut(*child_id)?;
4013        }
4014        Some(current)
4015    }
4016
4017    fn active_process_by_owned_path_mut<'a>(
4018        process: &'a mut ActiveProcess,
4019        child_path: &[String],
4020    ) -> Option<&'a mut ActiveProcess> {
4021        let mut current = process;
4022        for child_id in child_path {
4023            current = current.child_processes.get_mut(child_id)?;
4024        }
4025        Some(current)
4026    }
4027
4028    fn active_process_path_by_kernel_pid(
4029        process: &ActiveProcess,
4030        kernel_pid: u32,
4031    ) -> Option<Vec<String>> {
4032        if process.kernel_pid == kernel_pid {
4033            return Some(Vec::new());
4034        }
4035
4036        for (child_id, child) in &process.child_processes {
4037            let Some(mut path) = Self::active_process_path_by_kernel_pid(child, kernel_pid) else {
4038                continue;
4039            };
4040            path.insert(0, child_id.clone());
4041            return Some(path);
4042        }
4043
4044        None
4045    }
4046
4047    fn descendant_parent_process<'a>(
4048        vm: &'a VmState,
4049        process_id: &str,
4050        child_path: &[&str],
4051    ) -> Option<&'a ActiveProcess> {
4052        let root = vm.active_processes.get(process_id)?;
4053        Self::active_process_by_path(root, child_path)
4054    }
4055
4056    fn descendant_parent_process_mut<'a>(
4057        vm: &'a mut VmState,
4058        process_id: &str,
4059        child_path: &[&str],
4060    ) -> Option<&'a mut ActiveProcess> {
4061        let root = vm.active_processes.get_mut(process_id)?;
4062        Self::active_process_by_path_mut(root, child_path)
4063    }
4064
4065    fn child_process_path_label(process_id: &str, child_path: &[&str]) -> String {
4066        if child_path.is_empty() {
4067            process_id.to_owned()
4068        } else {
4069            format!("{process_id}/{}", child_path.join("/"))
4070        }
4071    }
4072
4073    fn adopt_detached_child_processes(
4074        current_process_id: &str,
4075        process: &mut ActiveProcess,
4076    ) -> Vec<(String, ActiveProcess)> {
4077        let mut adopted = Vec::new();
4078        let child_ids = process.child_processes.keys().cloned().collect::<Vec<_>>();
4079        for child_id in child_ids {
4080            let child_process_id = format!("{current_process_id}/{child_id}");
4081            let Some(mut child) = process.child_processes.remove(&child_id) else {
4082                continue;
4083            };
4084            if child.detached {
4085                adopted.push((child_process_id, child));
4086                continue;
4087            }
4088
4089            adopted.extend(Self::adopt_detached_child_processes(
4090                &child_process_id,
4091                &mut child,
4092            ));
4093            process.child_processes.insert(child_id, child);
4094        }
4095        adopted
4096    }
4097
4098    fn child_process_signal_key<'a>(process_id: &'a str, child_path: &[&'a str]) -> &'a str {
4099        child_path.last().copied().unwrap_or(process_id)
4100    }
4101
4102    fn resolve_detached_child_process_path(
4103        vm: &VmState,
4104        detached_process_id: &str,
4105    ) -> Option<(String, Vec<String>)> {
4106        let root_process_id = vm
4107            .active_processes
4108            .keys()
4109            .filter(|candidate| {
4110                detached_process_id == candidate.as_str()
4111                    || detached_process_id
4112                        .strip_prefix(candidate.as_str())
4113                        .is_some_and(|remainder| remainder.starts_with('/'))
4114            })
4115            .max_by_key(|candidate| candidate.len())?
4116            .clone();
4117
4118        let remainder = detached_process_id
4119            .strip_prefix(root_process_id.as_str())
4120            .unwrap_or_default();
4121        if remainder.is_empty() {
4122            return Some((root_process_id, Vec::new()));
4123        }
4124
4125        Some((
4126            root_process_id,
4127            remainder
4128                .trim_start_matches('/')
4129                .split('/')
4130                .map(str::to_owned)
4131                .collect(),
4132        ))
4133    }
4134
4135    fn pump_detached_child_process_events(&mut self, vm_id: &str) -> Result<bool, SidecarError> {
4136        let detached_process_ids = self
4137            .vms
4138            .get(vm_id)
4139            .map(|vm| {
4140                vm.detached_child_processes
4141                    .iter()
4142                    .cloned()
4143                    .collect::<Vec<_>>()
4144            })
4145            .unwrap_or_default();
4146        let mut emitted_any = false;
4147        for detached_process_id in detached_process_ids {
4148            let Some((root_process_id, child_path)) = self
4149                .vms
4150                .get(vm_id)
4151                .and_then(|vm| Self::resolve_detached_child_process_path(vm, &detached_process_id))
4152            else {
4153                if let Some(vm) = self.vms.get_mut(vm_id) {
4154                    vm.detached_child_processes.remove(&detached_process_id);
4155                }
4156                continue;
4157            };
4158            if child_path.is_empty() {
4159                loop {
4160                    enum ProcessPollResult {
4161                        Event(Box<Option<ActiveExecutionEvent>>),
4162                        RecoverClosedChannel,
4163                    }
4164                    let poll_result = {
4165                        let Some(vm) = self.vms.get_mut(vm_id) else {
4166                            break;
4167                        };
4168                        let Some(process) = vm.active_processes.get_mut(&root_process_id) else {
4169                            break;
4170                        };
4171                        if let Some(event) = process.pending_execution_events.pop_front() {
4172                            ProcessPollResult::Event(Box::new(Some(event)))
4173                        } else {
4174                            match process.execution.poll_event_blocking(Duration::ZERO) {
4175                                Ok(event) => ProcessPollResult::Event(Box::new(event)),
4176                                Err(SidecarError::Execution(message))
4177                                    if (process.runtime == GuestRuntimeKind::JavaScript
4178                                        && closed_javascript_event_channel(&message))
4179                                        || (process.runtime == GuestRuntimeKind::Python
4180                                            && closed_python_event_channel(&message))
4181                                        || (process.runtime == GuestRuntimeKind::WebAssembly
4182                                            && closed_wasm_event_channel(&message)) =>
4183                                {
4184                                    ProcessPollResult::RecoverClosedChannel
4185                                }
4186                                Err(error) => return Err(error),
4187                            }
4188                        }
4189                    };
4190                    let event = match poll_result {
4191                        ProcessPollResult::Event(event) => *event,
4192                        ProcessPollResult::RecoverClosedChannel => {
4193                            self.recover_closed_root_runtime_process_event(vm_id, &root_process_id)?
4194                        }
4195                    };
4196                    let Some(event) = event else {
4197                        break;
4198                    };
4199                    let Some((connection_id, session_id)) = self
4200                        .vms
4201                        .get(vm_id)
4202                        .map(|vm| (vm.connection_id.clone(), vm.session_id.clone()))
4203                    else {
4204                        break;
4205                    };
4206                    match event {
4207                        ActiveExecutionEvent::Stdout(chunk) => {
4208                            self.queue_pending_process_event(ProcessEventEnvelope {
4209                                connection_id,
4210                                session_id,
4211                                vm_id: vm_id.to_owned(),
4212                                process_id: detached_process_id.clone(),
4213                                event: ActiveExecutionEvent::Stdout(chunk),
4214                            })?;
4215                            emitted_any = true;
4216                        }
4217                        ActiveExecutionEvent::Stderr(chunk) => {
4218                            self.queue_pending_process_event(ProcessEventEnvelope {
4219                                connection_id,
4220                                session_id,
4221                                vm_id: vm_id.to_owned(),
4222                                process_id: detached_process_id.clone(),
4223                                event: ActiveExecutionEvent::Stderr(chunk),
4224                            })?;
4225                            emitted_any = true;
4226                        }
4227                        ActiveExecutionEvent::Exited(exit_code) => {
4228                            if let Some(vm) = self.vms.get_mut(vm_id) {
4229                                vm.detached_child_processes.remove(&detached_process_id);
4230                            }
4231                            self.queue_pending_process_event(ProcessEventEnvelope {
4232                                connection_id,
4233                                session_id,
4234                                vm_id: vm_id.to_owned(),
4235                                process_id: detached_process_id.clone(),
4236                                event: ActiveExecutionEvent::Exited(exit_code),
4237                            })?;
4238                            emitted_any = true;
4239                            break;
4240                        }
4241                        ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
4242                            self.handle_javascript_sync_rpc_request(
4243                                vm_id,
4244                                &root_process_id,
4245                                request,
4246                            )?;
4247                        }
4248                        ActiveExecutionEvent::PythonVfsRpcRequest(request) => {
4249                            self.handle_python_vfs_rpc_request(vm_id, &root_process_id, *request)?;
4250                        }
4251                        ActiveExecutionEvent::SignalState {
4252                            signal,
4253                            registration,
4254                        } => {
4255                            if let Some(vm) = self.vms.get_mut(vm_id) {
4256                                vm.signal_states
4257                                    .entry(root_process_id.clone())
4258                                    .or_default()
4259                                    .insert(signal, registration);
4260                            }
4261                        }
4262                    }
4263                }
4264                continue;
4265            }
4266
4267            let parent_path = child_path[..child_path.len() - 1]
4268                .iter()
4269                .map(String::as_str)
4270                .collect::<Vec<_>>();
4271            let child_process_id = child_path.last().expect("child path cannot be empty");
4272
4273            loop {
4274                let event = match self.poll_descendant_javascript_child_process(
4275                    vm_id,
4276                    &root_process_id,
4277                    &parent_path,
4278                    child_process_id,
4279                    0,
4280                ) {
4281                    Ok(event) => event,
4282                    Err(SidecarError::InvalidState(message))
4283                        if message.contains("unknown child process")
4284                            || message.contains("unknown child process path") =>
4285                    {
4286                        if let Some(vm) = self.vms.get_mut(vm_id) {
4287                            vm.detached_child_processes.remove(&detached_process_id);
4288                        }
4289                        break;
4290                    }
4291                    Err(error) if is_javascript_child_process_gone_error(&error) => {
4292                        if let Some(vm) = self.vms.get_mut(vm_id) {
4293                            vm.detached_child_processes.remove(&detached_process_id);
4294                        }
4295                        break;
4296                    }
4297                    Err(error) => return Err(error),
4298                };
4299
4300                let Some(event_type) = event.get("type").and_then(Value::as_str) else {
4301                    break;
4302                };
4303                let Some((connection_id, session_id)) = self
4304                    .vms
4305                    .get(vm_id)
4306                    .map(|vm| (vm.connection_id.clone(), vm.session_id.clone()))
4307                else {
4308                    break;
4309                };
4310
4311                let envelope = match event_type {
4312                    "stdout" => Some(ProcessEventEnvelope {
4313                        connection_id: connection_id.clone(),
4314                        session_id: session_id.clone(),
4315                        vm_id: vm_id.to_owned(),
4316                        process_id: detached_process_id.clone(),
4317                        event: ActiveExecutionEvent::Stdout(javascript_sync_rpc_bytes_arg(
4318                            &[event.get("data").cloned().unwrap_or(Value::Null)],
4319                            0,
4320                            "detached child_process stdout",
4321                        )?),
4322                    }),
4323                    "stderr" => Some(ProcessEventEnvelope {
4324                        connection_id: connection_id.clone(),
4325                        session_id: session_id.clone(),
4326                        vm_id: vm_id.to_owned(),
4327                        process_id: detached_process_id.clone(),
4328                        event: ActiveExecutionEvent::Stderr(javascript_sync_rpc_bytes_arg(
4329                            &[event.get("data").cloned().unwrap_or(Value::Null)],
4330                            0,
4331                            "detached child_process stderr",
4332                        )?),
4333                    }),
4334                    "exit" => {
4335                        if let Some(vm) = self.vms.get_mut(vm_id) {
4336                            vm.detached_child_processes.remove(&detached_process_id);
4337                        }
4338                        Some(ProcessEventEnvelope {
4339                            connection_id,
4340                            session_id,
4341                            vm_id: vm_id.to_owned(),
4342                            process_id: detached_process_id.clone(),
4343                            event: ActiveExecutionEvent::Exited(
4344                                event
4345                                    .get("exitCode")
4346                                    .and_then(Value::as_i64)
4347                                    .map(|value| value as i32)
4348                                    .unwrap_or(1),
4349                            ),
4350                        })
4351                    }
4352                    _ => None,
4353                };
4354
4355                let Some(envelope) = envelope else {
4356                    break;
4357                };
4358                self.queue_pending_process_event(envelope)?;
4359                emitted_any = true;
4360
4361                if event_type == "exit" {
4362                    break;
4363                }
4364            }
4365        }
4366
4367        Ok(emitted_any)
4368    }
4369    pub(crate) fn drain_queued_descendant_javascript_child_process_events(
4370        &mut self,
4371        vm_id: &str,
4372        process_id: &str,
4373        child_path: &[&str],
4374    ) -> Result<(), SidecarError> {
4375        if child_path.is_empty() {
4376            return Ok(());
4377        }
4378        let target_process_id = Self::child_process_path_label(process_id, child_path);
4379        let mut child_capacity = self
4380            .vms
4381            .get(vm_id)
4382            .and_then(|vm| vm.active_processes.get(process_id))
4383            .and_then(|root| descendant_pending_execution_event_capacity(root, child_path));
4384
4385        let mut deferred = VecDeque::new();
4386        while let Some(envelope) = self.pending_process_events.pop_front() {
4387            if envelope.vm_id == vm_id && envelope.process_id == target_process_id {
4388                if matches!(child_capacity, Some(0)) {
4389                    self.pending_process_events.push_front(envelope);
4390                    while let Some(deferred_envelope) = deferred.pop_back() {
4391                        self.pending_process_events.push_front(deferred_envelope);
4392                    }
4393                    return Err(process_event_queue_overflow_error());
4394                }
4395                if let Some(vm) = self.vms.get_mut(vm_id) {
4396                    if let Some(root) = vm.active_processes.get_mut(process_id) {
4397                        if let Some(child) = Self::active_process_by_path_mut(root, child_path) {
4398                            child.queue_pending_execution_event(envelope.event)?;
4399                            child_capacity = child_capacity.map(|capacity| capacity - 1);
4400                            continue;
4401                        }
4402                    }
4403                }
4404            }
4405            deferred.push_back(envelope);
4406        }
4407        self.pending_process_events = deferred;
4408
4409        let mut queued = Vec::new();
4410        {
4411            let transfer_capacity = self
4412                .pending_process_event_capacity()
4413                .min(child_capacity.unwrap_or(usize::MAX));
4414            let receiver = self.process_event_receiver.as_mut().ok_or_else(|| {
4415                SidecarError::InvalidState(String::from("process event receiver unavailable"))
4416            })?;
4417            loop {
4418                if queued.len() >= transfer_capacity {
4419                    if receiver.is_empty() {
4420                        break;
4421                    }
4422                    return Err(process_event_queue_overflow_error());
4423                }
4424                match receiver.try_recv() {
4425                    Ok(envelope) => queued.push(envelope),
4426                    Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
4427                    Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
4428                }
4429            }
4430        }
4431        for envelope in queued {
4432            if envelope.vm_id == vm_id && envelope.process_id == target_process_id {
4433                if let Some(vm) = self.vms.get_mut(vm_id) {
4434                    if let Some(root) = vm.active_processes.get_mut(process_id) {
4435                        if let Some(child) = Self::active_process_by_path_mut(root, child_path) {
4436                            child.queue_pending_execution_event(envelope.event)?;
4437                            continue;
4438                        }
4439                    }
4440                }
4441            }
4442            self.queue_pending_process_event(envelope)?;
4443        }
4444
4445        Ok(())
4446    }
4447
4448    pub(crate) fn handle_execution_event(
4449        &mut self,
4450        vm_id: &str,
4451        process_id: &str,
4452        event: ActiveExecutionEvent,
4453    ) -> Result<Option<EventFrame>, SidecarError> {
4454        let Some(vm) = self.vms.get(vm_id) else {
4455            log_stale_process_event(&self.bridge, vm_id, process_id, "execution event dispatch");
4456            return Ok(None);
4457        };
4458        if !vm.active_processes.contains_key(process_id) {
4459            log_stale_process_event(&self.bridge, vm_id, process_id, "execution event dispatch");
4460            return Ok(None);
4461        }
4462        let (connection_id, session_id) = { (vm.connection_id.clone(), vm.session_id.clone()) };
4463        let ownership = OwnershipScope::vm(&connection_id, &session_id, vm_id);
4464
4465        if self.capture_extension_process_output_event(vm_id, process_id, &event) {
4466            return Ok(None);
4467        }
4468
4469        match event {
4470            ActiveExecutionEvent::Stdout(chunk) => Ok(Some(EventFrame::new(
4471                ownership,
4472                EventPayload::ProcessOutput(ProcessOutputEvent {
4473                    process_id: process_id.to_owned(),
4474                    channel: StreamChannel::Stdout,
4475                    chunk,
4476                }),
4477            ))),
4478            ActiveExecutionEvent::Stderr(chunk) => Ok(Some(EventFrame::new(
4479                ownership,
4480                EventPayload::ProcessOutput(ProcessOutputEvent {
4481                    process_id: process_id.to_owned(),
4482                    channel: StreamChannel::Stderr,
4483                    chunk,
4484                }),
4485            ))),
4486            ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
4487                self.handle_javascript_sync_rpc_request(vm_id, process_id, request)?;
4488                Ok(None)
4489            }
4490            ActiveExecutionEvent::PythonVfsRpcRequest(request) => {
4491                self.handle_python_vfs_rpc_request(vm_id, process_id, *request)?;
4492                Ok(None)
4493            }
4494            ActiveExecutionEvent::SignalState {
4495                signal,
4496                registration,
4497            } => {
4498                let Some(vm) = self.vms.get_mut(vm_id) else {
4499                    return Ok(None);
4500                };
4501                if !vm.active_processes.contains_key(process_id) {
4502                    return Ok(None);
4503                }
4504                vm.signal_states
4505                    .entry(process_id.to_owned())
4506                    .or_default()
4507                    .insert(signal, registration);
4508                Ok(None)
4509            }
4510            ActiveExecutionEvent::Exited(exit_code) => {
4511                let became_idle = self
4512                    .finish_active_process_exit(vm_id, process_id, exit_code)?
4513                    .unwrap_or(false);
4514
4515                if became_idle {
4516                    self.bridge.emit_lifecycle(vm_id, LifecycleState::Ready)?;
4517                }
4518
4519                Ok(Some(EventFrame::new(
4520                    ownership,
4521                    EventPayload::ProcessExited(ProcessExitedEvent {
4522                        process_id: process_id.to_owned(),
4523                        exit_code,
4524                    }),
4525                )))
4526            }
4527        }
4528    }
4529
4530    pub(crate) fn finish_active_process_exit(
4531        &mut self,
4532        vm_id: &str,
4533        process_id: &str,
4534        exit_code: i32,
4535    ) -> Result<Option<bool>, SidecarError> {
4536        let Some(vm) = self.vms.get_mut(vm_id) else {
4537            log_stale_process_event(&self.bridge, vm_id, process_id, "process exit cleanup");
4538            return Ok(None);
4539        };
4540        if !vm.active_processes.contains_key(process_id) {
4541            log_stale_process_event(&self.bridge, vm_id, process_id, "process exit cleanup");
4542            return Ok(None);
4543        }
4544
4545        prune_exited_process_snapshots(vm);
4546        let process_table = vm.kernel.list_processes();
4547        let Some(mut process) = vm.active_processes.remove(process_id) else {
4548            return Ok(None);
4549        };
4550        if let Some(info) = process_table.get(&process.kernel_pid) {
4551            vm.exited_process_snapshots
4552                .push_back(ExitedProcessSnapshot {
4553                    captured_at: Instant::now(),
4554                    process: build_process_snapshot_entry(
4555                        process_id,
4556                        &process,
4557                        info,
4558                        Some(exit_code),
4559                    ),
4560                });
4561        }
4562        let detached_children = Self::adopt_detached_child_processes(process_id, &mut process);
4563        sync_process_host_writes_to_kernel(vm, &process)?;
4564        terminate_child_process_tree(&mut vm.kernel, &mut process);
4565        process.kernel_handle.finish(exit_code);
4566        let _ = vm.kernel.wait_and_reap(process.kernel_pid);
4567        vm.signal_states.remove(process_id);
4568        for (detached_process_id, detached_child) in detached_children {
4569            vm.detached_child_processes
4570                .insert(detached_process_id.clone());
4571            vm.active_processes
4572                .insert(detached_process_id, detached_child);
4573        }
4574        let became_idle = vm.active_processes.is_empty();
4575        self.prune_extension_process_resource(process_id);
4576
4577        Ok(Some(became_idle))
4578    }
4579
4580    pub(crate) fn drain_process_events_blocking_with_limit(
4581        &mut self,
4582        vm_id: &str,
4583        process_id: &str,
4584        max_events: usize,
4585    ) -> Result<Vec<ActiveExecutionEvent>, SidecarError> {
4586        let mut events = Vec::new();
4587        if max_events == 0 {
4588            return Ok(events);
4589        }
4590        let mut deadline = Instant::now() + Duration::from_millis(150);
4591
4592        loop {
4593            if events.len() >= max_events {
4594                break;
4595            }
4596            let event = {
4597                let Some(vm) = self.vms.get_mut(vm_id) else {
4598                    break;
4599                };
4600                let Some(process) = vm.active_processes.get_mut(process_id) else {
4601                    break;
4602                };
4603                if let Some(event) = process.pending_execution_events.pop_front() {
4604                    Some(event)
4605                } else {
4606                    match process.execution.poll_event_blocking(Duration::ZERO) {
4607                        Ok(event) => event,
4608                        Err(SidecarError::Execution(_)) => None,
4609                        Err(other) => return Err(other),
4610                    }
4611                }
4612            };
4613
4614            let Some(event) = event else {
4615                if Instant::now() >= deadline {
4616                    break;
4617                }
4618                let blocking_wait = deadline.saturating_duration_since(Instant::now());
4619                if blocking_wait.is_zero() {
4620                    break;
4621                }
4622                if events.len() >= max_events {
4623                    break;
4624                }
4625                let delayed_event = {
4626                    let Some(vm) = self.vms.get_mut(vm_id) else {
4627                        break;
4628                    };
4629                    let Some(process) = vm.active_processes.get_mut(process_id) else {
4630                        break;
4631                    };
4632                    if let Some(event) = process.pending_execution_events.pop_front() {
4633                        Some(event)
4634                    } else {
4635                        match process.execution.poll_event_blocking(blocking_wait) {
4636                            Ok(event) => event,
4637                            Err(SidecarError::Execution(_)) => None,
4638                            Err(other) => return Err(other),
4639                        }
4640                    }
4641                };
4642                let Some(event) = delayed_event else {
4643                    break;
4644                };
4645                events.push(event);
4646                deadline = Instant::now() + Duration::from_millis(150);
4647                continue;
4648            };
4649            events.push(event);
4650            deadline = Instant::now() + Duration::from_millis(150);
4651        }
4652
4653        Ok(events)
4654    }
4655
4656    pub(crate) fn handle_python_vfs_rpc_request(
4657        &mut self,
4658        vm_id: &str,
4659        process_id: &str,
4660        request: PythonVfsRpcRequest,
4661    ) -> Result<(), SidecarError> {
4662        match request.method {
4663            PythonVfsRpcMethod::Read
4664            | PythonVfsRpcMethod::Write
4665            | PythonVfsRpcMethod::Stat
4666            | PythonVfsRpcMethod::ReadDir
4667            | PythonVfsRpcMethod::Mkdir => {
4668                filesystem_handle_python_vfs_rpc_request(self, vm_id, process_id, request)
4669            }
4670            PythonVfsRpcMethod::HttpRequest => {
4671                self.handle_python_http_rpc_request(vm_id, process_id, request)
4672            }
4673            PythonVfsRpcMethod::DnsLookup => {
4674                self.handle_python_dns_rpc_request(vm_id, process_id, request)
4675            }
4676            PythonVfsRpcMethod::SubprocessRun => {
4677                self.handle_python_subprocess_rpc_request(vm_id, process_id, request)
4678            }
4679        }
4680    }
4681
4682    fn handle_python_http_rpc_request(
4683        &mut self,
4684        vm_id: &str,
4685        process_id: &str,
4686        request: PythonVfsRpcRequest,
4687    ) -> Result<(), SidecarError> {
4688        let Some(vm) = self.vms.get(vm_id) else {
4689            return Ok(());
4690        };
4691        if !vm.active_processes.contains_key(process_id) {
4692            return Ok(());
4693        }
4694        let response = (|| {
4695            let url_text = request.url.as_deref().ok_or_else(|| {
4696                SidecarError::InvalidState(String::from("python httpRequest requires a url"))
4697            })?;
4698            let url = Url::parse(url_text)
4699                .map_err(|error| SidecarError::Execution(format!("ERR_INVALID_URL: {error}")))?;
4700            let host = url.host_str().ok_or_else(|| {
4701                SidecarError::Execution(String::from("ERR_INVALID_URL: missing host"))
4702            })?;
4703            let port = url.port_or_known_default().ok_or_else(|| {
4704                SidecarError::Execution(String::from("ERR_INVALID_URL: missing port"))
4705            })?;
4706            self.bridge.require_network_access(
4707                vm_id,
4708                NetworkOperation::Http,
4709                format_tcp_resource(host, port),
4710            )?;
4711            // Pin the outbound connection to the IP addresses that pass the
4712            // egress range guard at resolution time. A literal IP is validated
4713            // directly; a hostname is resolved once here and the resulting
4714            // address set is pinned into the HTTP client's resolver below so a
4715            // rebinding DNS server cannot make the second (TLS/TCP) lookup land
4716            // on a private/link-local/metadata IP that this check rejected.
4717            let pinned_addresses = if let Ok(literal_ip) = host.parse::<IpAddr>() {
4718                filter_dns_safe_ip_addrs(vec![literal_ip], host)?
4719            } else {
4720                filter_dns_safe_ip_addrs(
4721                    resolve_dns_ip_addrs(
4722                        &self.bridge,
4723                        &vm.kernel,
4724                        vm_id,
4725                        &vm.dns,
4726                        host,
4727                        DnsLookupPolicy::SkipPermissions,
4728                    )?,
4729                    host,
4730                )?
4731            };
4732            let mut headers = BTreeMap::new();
4733            for (name, value) in &request.headers {
4734                headers.insert(name.clone(), Value::String(value.clone()));
4735            }
4736            let options = JavascriptHttpRequestOptions {
4737                method: Some(
4738                    request
4739                        .http_method
4740                        .clone()
4741                        .unwrap_or_else(|| String::from("GET")),
4742                ),
4743                headers,
4744                body: request.body_base64.as_deref().map(|body| {
4745                    String::from_utf8(
4746                        base64::engine::general_purpose::STANDARD
4747                            .decode(body)
4748                            .unwrap_or_default(),
4749                    )
4750                    .unwrap_or_default()
4751                }),
4752                reject_unauthorized: None,
4753            };
4754            let headers =
4755                parse_http_header_collection(&options.headers, "python httpRequest headers")?;
4756            let response =
4757                issue_outbound_http_request(&url, &options, &headers, &pinned_addresses)?;
4758            let payload_json = response.as_str().ok_or_else(|| {
4759                SidecarError::Execution(String::from(
4760                    "python httpRequest returned a non-string response payload",
4761                ))
4762            })?;
4763            let payload: Value = serde_json::from_str(payload_json).map_err(|error| {
4764                SidecarError::Execution(format!(
4765                    "python httpRequest response must be valid JSON: {error}"
4766                ))
4767            })?;
4768            let header_map = payload
4769                .get("headers")
4770                .and_then(Value::as_array)
4771                .map(|entries| {
4772                    let mut normalized = BTreeMap::<String, Vec<String>>::new();
4773                    for entry in entries {
4774                        let Some(pair) = entry.as_array() else {
4775                            continue;
4776                        };
4777                        let Some(name) = pair.first().and_then(Value::as_str) else {
4778                            continue;
4779                        };
4780                        let Some(value) = pair.get(1).and_then(Value::as_str) else {
4781                            continue;
4782                        };
4783                        normalized
4784                            .entry(name.to_owned())
4785                            .or_default()
4786                            .push(value.to_owned());
4787                    }
4788                    normalized
4789                })
4790                .unwrap_or_default();
4791            Ok(PythonVfsRpcResponsePayload::Http {
4792                status: payload
4793                    .get("status")
4794                    .and_then(Value::as_u64)
4795                    .map(|value| value as u16)
4796                    .unwrap_or_default(),
4797                reason: payload
4798                    .get("statusText")
4799                    .and_then(Value::as_str)
4800                    .unwrap_or_default()
4801                    .to_owned(),
4802                url: payload
4803                    .get("url")
4804                    .and_then(Value::as_str)
4805                    .unwrap_or(url_text)
4806                    .to_owned(),
4807                headers: header_map,
4808                body_base64: payload
4809                    .get("body")
4810                    .and_then(Value::as_str)
4811                    .unwrap_or_default()
4812                    .to_owned(),
4813            })
4814        })();
4815
4816        self.respond_python_rpc(vm_id, process_id, request.id, response)
4817    }
4818
4819    fn handle_python_dns_rpc_request(
4820        &mut self,
4821        vm_id: &str,
4822        process_id: &str,
4823        request: PythonVfsRpcRequest,
4824    ) -> Result<(), SidecarError> {
4825        let Some(vm) = self.vms.get(vm_id) else {
4826            return Ok(());
4827        };
4828        if !vm.active_processes.contains_key(process_id) {
4829            return Ok(());
4830        }
4831        let response = (|| {
4832            let hostname = request.hostname.as_deref().ok_or_else(|| {
4833                SidecarError::InvalidState(String::from("python dnsLookup requires a hostname"))
4834            })?;
4835            let mut addresses = filter_dns_safe_ip_addrs(
4836                resolve_dns_ip_addrs(
4837                    &self.bridge,
4838                    &vm.kernel,
4839                    vm_id,
4840                    &vm.dns,
4841                    hostname,
4842                    DnsLookupPolicy::CheckPermissions,
4843                )?,
4844                hostname,
4845            )?;
4846            if let Some(family) = request.family {
4847                addresses.retain(|address| {
4848                    matches!((family, address), (4, IpAddr::V4(_)) | (6, IpAddr::V6(_)))
4849                });
4850            }
4851            Ok(PythonVfsRpcResponsePayload::DnsLookup {
4852                addresses: addresses
4853                    .into_iter()
4854                    .map(|address| address.to_string())
4855                    .collect(),
4856            })
4857        })();
4858
4859        self.respond_python_rpc(vm_id, process_id, request.id, response)
4860    }
4861
4862    fn handle_python_subprocess_rpc_request(
4863        &mut self,
4864        vm_id: &str,
4865        process_id: &str,
4866        request: PythonVfsRpcRequest,
4867    ) -> Result<(), SidecarError> {
4868        let command = request.command.clone().ok_or_else(|| {
4869            SidecarError::InvalidState(String::from("python subprocessRun requires a command"))
4870        })?;
4871        let (internal_bootstrap_env, cwd) = {
4872            let Some(vm) = self.vms.get(vm_id) else {
4873                return Ok(());
4874            };
4875            let Some(process) = vm.active_processes.get(process_id) else {
4876                return Ok(());
4877            };
4878            let virtual_home = guest_virtual_home(vm);
4879            let cwd = request.cwd.clone().or_else(|| {
4880                guest_runtime_path_for_host_path(
4881                    &vm.guest_env,
4882                    &virtual_home,
4883                    &vm.host_cwd,
4884                    &process.host_cwd.to_string_lossy(),
4885                )
4886            });
4887            (
4888                sanitize_javascript_child_process_internal_bootstrap_env(&vm.guest_env),
4889                cwd,
4890            )
4891        };
4892        let response = self
4893            .spawn_javascript_child_process_sync(
4894                vm_id,
4895                process_id,
4896                JavascriptChildProcessSpawnRequest {
4897                    command,
4898                    args: request.args.clone(),
4899                    options: JavascriptChildProcessSpawnOptions {
4900                        cwd,
4901                        env: request.env.clone(),
4902                        input: None,
4903                        internal_bootstrap_env,
4904                        shell: request.shell,
4905                        detached: false,
4906                        stdio: vec![
4907                            String::from("pipe"),
4908                            String::from("pipe"),
4909                            String::from("pipe"),
4910                        ],
4911                        timeout: None,
4912                        kill_signal: None,
4913                    },
4914                },
4915                request.max_buffer,
4916            )
4917            .map(|payload| PythonVfsRpcResponsePayload::SubprocessRun {
4918                exit_code: payload
4919                    .get("code")
4920                    .and_then(Value::as_i64)
4921                    .map(|value| value as i32)
4922                    .unwrap_or(1),
4923                stdout: payload
4924                    .get("stdout")
4925                    .and_then(Value::as_str)
4926                    .unwrap_or_default()
4927                    .to_owned(),
4928                stderr: payload
4929                    .get("stderr")
4930                    .and_then(Value::as_str)
4931                    .unwrap_or_default()
4932                    .to_owned(),
4933                max_buffer_exceeded: payload
4934                    .get("maxBufferExceeded")
4935                    .and_then(Value::as_bool)
4936                    .unwrap_or(false),
4937            });
4938
4939        self.respond_python_rpc(vm_id, process_id, request.id, response)
4940    }
4941
4942    fn respond_python_rpc(
4943        &mut self,
4944        vm_id: &str,
4945        process_id: &str,
4946        request_id: u64,
4947        response: Result<PythonVfsRpcResponsePayload, SidecarError>,
4948    ) -> Result<(), SidecarError> {
4949        let Some(vm) = self.vms.get_mut(vm_id) else {
4950            return Ok(());
4951        };
4952        let Some(process) = vm.active_processes.get_mut(process_id) else {
4953            return Ok(());
4954        };
4955        let result = match response {
4956            Ok(payload) => process
4957                .execution
4958                .respond_python_vfs_rpc_success(request_id, payload),
4959            Err(error) => process.execution.respond_python_vfs_rpc_error(
4960                request_id,
4961                "ERR_AGENTOS_PYTHON_VFS_RPC",
4962                error.to_string(),
4963            ),
4964        };
4965        match result {
4966            Ok(()) => Ok(()),
4967            Err(error) if is_broken_pipe_error(&error) => Ok(()),
4968            Err(error) => Err(error),
4969        }
4970    }
4971
4972    pub(crate) fn resolve_javascript_child_process_execution(
4973        &self,
4974        vm: &VmState,
4975        parent_env: &BTreeMap<String, String>,
4976        parent_guest_cwd: &str,
4977        parent_host_cwd: &Path,
4978        request: &JavascriptChildProcessSpawnRequest,
4979    ) -> Result<ResolvedChildProcessExecution, SidecarError> {
4980        let mut runtime_env = parent_env.clone();
4981        runtime_env.extend(request.options.internal_bootstrap_env.clone());
4982        let (guest_cwd, host_cwd_override) = request
4983            .options
4984            .cwd
4985            .as_deref()
4986            .map(|cwd| {
4987                let normalized_parent_host_cwd = normalize_host_path(parent_host_cwd);
4988                let requested_host_cwd = normalize_host_path(Path::new(cwd));
4989                if path_is_within_root(&requested_host_cwd, &normalized_parent_host_cwd) {
4990                    let relative = requested_host_cwd
4991                        .strip_prefix(&normalized_parent_host_cwd)
4992                        .unwrap_or_else(|_| Path::new(""));
4993                    let relative = relative.to_string_lossy().replace('\\', "/");
4994                    let guest_cwd = if relative.is_empty() {
4995                        parent_guest_cwd.to_owned()
4996                    } else {
4997                        normalize_path(&format!("{parent_guest_cwd}/{relative}"))
4998                    };
4999                    (guest_cwd, Some(requested_host_cwd))
5000                } else if Path::new(cwd).is_relative() {
5001                    (
5002                        normalize_path(&format!("{parent_guest_cwd}/{cwd}")),
5003                        Some(normalize_host_path(&parent_host_cwd.join(cwd))),
5004                    )
5005                } else {
5006                    (normalize_path(cwd), None)
5007                }
5008            })
5009            .unwrap_or_else(|| (parent_guest_cwd.to_owned(), None));
5010        let inherited_host_cwd = (host_cwd_override.is_none() && guest_cwd == parent_guest_cwd)
5011            .then(|| normalize_host_path(parent_host_cwd));
5012        let host_cwd = host_cwd_override
5013            .or(inherited_host_cwd)
5014            .or_else(|| {
5015                host_runtime_path_for_guest_path_with_env(
5016                    vm,
5017                    &runtime_env,
5018                    &guest_cwd,
5019                    parent_host_cwd,
5020                )
5021            })
5022            .unwrap_or_else(|| {
5023                let candidate = PathBuf::from(&guest_cwd);
5024                if guest_cwd == parent_guest_cwd {
5025                    normalize_host_path(parent_host_cwd)
5026                } else if candidate.is_absolute() {
5027                    shadow_path_for_guest(vm, &guest_cwd)
5028                } else {
5029                    vm.host_cwd.clone()
5030                }
5031            });
5032        let mut env = parent_env.clone();
5033        env.extend(request.options.env.clone());
5034        // Child JavaScript executions must resolve their own entrypoint/eval state.
5035        // Reusing the parent's values makes the sidecar load the wrong source file.
5036        env.remove("AGENTOS_GUEST_ENTRYPOINT");
5037        env.remove("AGENTOS_NODE_EVAL");
5038
5039        let (command, process_args) = if request.options.shell {
5040            let tokens = tokenize_shell_free_command(&request.command);
5041            let requires_shell = command_requires_shell(&request.command)
5042                || tokens.first().is_some_and(|command| {
5043                    is_posix_shell_builtin(command) || shell_first_token_requires_shell(command)
5044                });
5045            if requires_shell {
5046                if !vm.command_guest_paths.contains_key("sh") {
5047                    return Err(SidecarError::InvalidState(format!(
5048                        "shell-mode child_process command requires /bin/sh, which is not \
5049                         installed in this VM (install a software package that provides sh, \
5050                         for example @secure-exec/coreutils): {}",
5051                        request.command
5052                    )));
5053                }
5054                (
5055                    String::from("sh"),
5056                    vec![String::from("-c"), request.command.clone()],
5057                )
5058            } else {
5059                let Some((command, args)) = tokens.split_first() else {
5060                    return Err(SidecarError::InvalidState(String::from(
5061                        "child_process shell command must not be empty",
5062                    )));
5063                };
5064                (command.clone(), args.to_vec())
5065            }
5066        } else {
5067            (request.command.clone(), request.args.clone())
5068        };
5069        let process_args = apply_shell_cwd_prefix(&command, process_args, &guest_cwd);
5070        if is_tool_command(vm, &command) {
5071            let command = normalized_tool_command_name(&command).unwrap_or(command);
5072            return Ok(ResolvedChildProcessExecution {
5073                command: command.clone(),
5074                process_args: std::iter::once(command.clone())
5075                    .chain(process_args.iter().cloned())
5076                    .collect(),
5077                runtime: GuestRuntimeKind::JavaScript,
5078                entrypoint: command,
5079                execution_args: process_args,
5080                env,
5081                guest_cwd,
5082                host_cwd,
5083                wasm_permission_tier: None,
5084                tool_command: true,
5085            });
5086        }
5087
5088        if is_path_like_specifier(&command)
5089            && matches!(
5090                Path::new(&command).extension().and_then(|ext| ext.to_str()),
5091                Some("js" | "mjs" | "cjs" | "ts" | "mts" | "cts")
5092            )
5093        {
5094            let guest_entrypoint = if command.starts_with('/') {
5095                normalize_path(&command)
5096            } else if command.starts_with("file:") {
5097                normalize_path(command.trim_start_matches("file:"))
5098            } else {
5099                normalize_path(&format!("{guest_cwd}/{command}"))
5100            };
5101            let host_entrypoint = if command.starts_with("./") || command.starts_with("../") {
5102                normalize_host_path(&host_cwd.join(&command))
5103            } else {
5104                host_runtime_path_for_guest_path_with_env(
5105                    vm,
5106                    &runtime_env,
5107                    &guest_entrypoint,
5108                    parent_host_cwd,
5109                )
5110                .unwrap_or_else(|| {
5111                    let candidate = PathBuf::from(&guest_entrypoint);
5112                    if candidate.is_absolute() {
5113                        candidate
5114                    } else {
5115                        host_cwd.join(&guest_entrypoint)
5116                    }
5117                })
5118            };
5119            env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint);
5120            let guest_entrypoint = env.get("AGENTOS_GUEST_ENTRYPOINT").cloned();
5121            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
5122
5123            return Ok(ResolvedChildProcessExecution {
5124                command: command.clone(),
5125                process_args: std::iter::once(command)
5126                    .chain(process_args.iter().cloned())
5127                    .collect(),
5128                runtime: GuestRuntimeKind::JavaScript,
5129                entrypoint: host_entrypoint.to_string_lossy().into_owned(),
5130                execution_args: process_args,
5131                env,
5132                guest_cwd,
5133                host_cwd,
5134                wasm_permission_tier: None,
5135                tool_command: false,
5136            });
5137        }
5138
5139        if is_node_runtime_command(&command) {
5140            if let Some(cli) = resolve_host_node_cli_entrypoint(&command) {
5141                env.insert(
5142                    String::from("AGENTOS_NODE_EVAL"),
5143                    build_host_node_cli_eval(&cli),
5144                );
5145                prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
5146                add_runtime_guest_path_mapping(&mut env, &cli.guest_root, &cli.package_root);
5147                add_runtime_host_access_path(
5148                    &mut env,
5149                    "AGENTOS_EXTRA_FS_READ_PATHS",
5150                    &cli.package_root,
5151                    true,
5152                );
5153
5154                return Ok(ResolvedChildProcessExecution {
5155                    command: command.clone(),
5156                    process_args: std::iter::once(command.clone())
5157                        .chain(process_args.iter().cloned())
5158                        .collect(),
5159                    runtime: GuestRuntimeKind::JavaScript,
5160                    entrypoint: String::from("-e"),
5161                    execution_args: std::iter::once(cli.guest_entrypoint.clone())
5162                        .chain(process_args.iter().cloned())
5163                        .collect(),
5164                    env,
5165                    guest_cwd,
5166                    host_cwd,
5167                    wasm_permission_tier: None,
5168                    tool_command: false,
5169                });
5170            }
5171
5172            if process_args.is_empty() {
5173                env.insert(String::from("AGENTOS_NODE_EVAL"), String::new());
5174                prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
5175
5176                return Ok(ResolvedChildProcessExecution {
5177                    command: command.clone(),
5178                    process_args: vec![command.clone()],
5179                    runtime: GuestRuntimeKind::JavaScript,
5180                    entrypoint: String::from("-e"),
5181                    execution_args: Vec::new(),
5182                    env,
5183                    guest_cwd,
5184                    host_cwd,
5185                    wasm_permission_tier: None,
5186                    tool_command: false,
5187                });
5188            }
5189
5190            if let Some((entrypoint, execution_args)) =
5191                resolve_special_node_cli_invocation(&process_args, &mut env)
5192            {
5193                prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
5194
5195                return Ok(ResolvedChildProcessExecution {
5196                    command: command.clone(),
5197                    process_args: std::iter::once(command.clone())
5198                        .chain(process_args.iter().cloned())
5199                        .collect(),
5200                    runtime: GuestRuntimeKind::JavaScript,
5201                    entrypoint,
5202                    execution_args,
5203                    env,
5204                    guest_cwd,
5205                    host_cwd,
5206                    wasm_permission_tier: None,
5207                    tool_command: false,
5208                });
5209            }
5210
5211            let Some(entrypoint_specifier) = process_args.first() else {
5212                return Err(SidecarError::InvalidState(format!(
5213                    "{command} child_process spawn requires an entrypoint"
5214                )));
5215            };
5216
5217            let (entrypoint, execution_args) = if is_path_like_specifier(entrypoint_specifier) {
5218                let guest_entrypoint = if entrypoint_specifier.starts_with('/') {
5219                    normalize_path(entrypoint_specifier)
5220                } else if entrypoint_specifier.starts_with("file:") {
5221                    normalize_path(entrypoint_specifier.trim_start_matches("file:"))
5222                } else {
5223                    normalize_path(&format!("{guest_cwd}/{entrypoint_specifier}"))
5224                };
5225                let host_entrypoint = if entrypoint_specifier.starts_with("./")
5226                    || entrypoint_specifier.starts_with("../")
5227                {
5228                    normalize_host_path(&host_cwd.join(entrypoint_specifier))
5229                } else {
5230                    host_runtime_path_for_guest_path_with_env(
5231                        vm,
5232                        &runtime_env,
5233                        &guest_entrypoint,
5234                        parent_host_cwd,
5235                    )
5236                    .unwrap_or_else(|| {
5237                        let candidate = PathBuf::from(&guest_entrypoint);
5238                        if candidate.is_absolute() {
5239                            candidate
5240                        } else {
5241                            host_cwd.join(&guest_entrypoint)
5242                        }
5243                    })
5244                };
5245                env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint);
5246                (
5247                    host_entrypoint.to_string_lossy().into_owned(),
5248                    process_args.iter().skip(1).cloned().collect(),
5249                )
5250            } else {
5251                (
5252                    entrypoint_specifier.clone(),
5253                    process_args.iter().skip(1).cloned().collect(),
5254                )
5255            };
5256            let guest_entrypoint = env.get("AGENTOS_GUEST_ENTRYPOINT").cloned();
5257            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
5258
5259            return Ok(ResolvedChildProcessExecution {
5260                command: command.clone(),
5261                process_args: std::iter::once(command)
5262                    .chain(process_args.iter().cloned())
5263                    .collect(),
5264                runtime: GuestRuntimeKind::JavaScript,
5265                entrypoint,
5266                execution_args,
5267                env,
5268                guest_cwd,
5269                host_cwd,
5270                wasm_permission_tier: None,
5271                tool_command: false,
5272            });
5273        }
5274
5275        if command == PYTHON_COMMAND {
5276            return Err(SidecarError::InvalidState(String::from(
5277                "nested python child_process execution is not supported yet",
5278            )));
5279        }
5280
5281        let guest_entrypoint = resolve_guest_command_entrypoint(
5282            vm,
5283            &guest_cwd,
5284            &command,
5285            env.get("PATH").map(String::as_str),
5286        )
5287        .ok_or_else(|| SidecarError::InvalidState(format!("command not found: {command}")))?;
5288        let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint);
5289        let wasm_permission_tier = vm.command_permissions.get(&command).copied().or_else(|| {
5290            Path::new(&guest_entrypoint)
5291                .file_name()
5292                .and_then(|name| name.to_str())
5293                .and_then(|name| vm.command_permissions.get(name).copied())
5294        });
5295        if let Some((javascript_guest_entrypoint, javascript_host_entrypoint)) =
5296            resolve_javascript_command_entrypoint(vm, &guest_entrypoint, &host_entrypoint)
5297        {
5298            prepare_guest_runtime_env(
5299                vm,
5300                &mut env,
5301                &guest_cwd,
5302                &host_cwd,
5303                Some(javascript_guest_entrypoint),
5304            )?;
5305
5306            return Ok(ResolvedChildProcessExecution {
5307                command: command.clone(),
5308                process_args: std::iter::once(command)
5309                    .chain(process_args.iter().cloned())
5310                    .collect(),
5311                runtime: GuestRuntimeKind::JavaScript,
5312                entrypoint: javascript_host_entrypoint.to_string_lossy().into_owned(),
5313                execution_args: process_args,
5314                env,
5315                guest_cwd,
5316                host_cwd,
5317                wasm_permission_tier: None,
5318                tool_command: false,
5319            });
5320        }
5321        prepare_guest_runtime_env(
5322            vm,
5323            &mut env,
5324            &guest_cwd,
5325            &host_cwd,
5326            Some(guest_entrypoint.clone()),
5327        )?;
5328
5329        Ok(ResolvedChildProcessExecution {
5330            command: command.clone(),
5331            process_args: std::iter::once(command)
5332                .chain(process_args.iter().cloned())
5333                .collect(),
5334            runtime: GuestRuntimeKind::WebAssembly,
5335            entrypoint: host_entrypoint.to_string_lossy().into_owned(),
5336            execution_args: process_args,
5337            env,
5338            guest_cwd,
5339            host_cwd,
5340            wasm_permission_tier,
5341            tool_command: false,
5342        })
5343    }
5344
5345    pub(crate) fn spawn_javascript_child_process(
5346        &mut self,
5347        vm_id: &str,
5348        process_id: &str,
5349        request: JavascriptChildProcessSpawnRequest,
5350    ) -> Result<Value, SidecarError> {
5351        let resolved = {
5352            let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?;
5353            let parent = vm
5354                .active_processes
5355                .get(process_id)
5356                .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5357            self.resolve_javascript_child_process_execution(
5358                vm,
5359                &parent.env,
5360                &parent.guest_cwd,
5361                &parent.host_cwd,
5362                &request,
5363            )?
5364        };
5365        let (parent_kernel_pid, child_process_id) = {
5366            let vm = self
5367                .vms
5368                .get_mut(vm_id)
5369                .ok_or_else(|| missing_vm_error(vm_id))?;
5370            let process = vm
5371                .active_processes
5372                .get_mut(process_id)
5373                .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5374            (process.kernel_pid, process.allocate_child_process_id())
5375        };
5376        let sidecar_requests = self.sidecar_requests.clone();
5377        let vm = self
5378            .vms
5379            .get_mut(vm_id)
5380            .ok_or_else(|| missing_vm_error(vm_id))?;
5381        let (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd) = if resolved
5382            .tool_command
5383        {
5384            let tool_resolution = resolve_tool_command(
5385                vm,
5386                &resolved.command,
5387                &resolved.execution_args,
5388                Some(&resolved.guest_cwd),
5389            )?
5390            .ok_or_else(|| {
5391                SidecarError::InvalidState(format!(
5392                    "tool command no longer resolves: {}",
5393                    resolved.command
5394                ))
5395            })?;
5396            let kernel_handle = vm
5397                .kernel
5398                .create_virtual_process(
5399                    EXECUTION_DRIVER_NAME,
5400                    TOOL_DRIVER_NAME,
5401                    &resolved.command,
5402                    resolved.process_args.clone(),
5403                    VirtualProcessOptions {
5404                        parent_pid: Some(parent_kernel_pid),
5405                        env: resolved.env.clone(),
5406                        cwd: Some(resolved.guest_cwd.clone()),
5407                    },
5408                )
5409                .map_err(kernel_error)?;
5410            let kernel_pid = kernel_handle.pid();
5411            let tool_execution = ToolExecution::default();
5412            let cancelled = tool_execution.cancelled.clone();
5413            let pending_events = tool_execution.pending_events.clone();
5414            let events_overflowed = tool_execution.events_overflowed.clone();
5415            spawn_tool_process_events(ToolProcessEventRequest {
5416                sidecar_requests: sidecar_requests.clone(),
5417                connection_id: vm.connection_id.clone(),
5418                session_id: vm.session_id.clone(),
5419                vm_id: vm_id.to_owned(),
5420                tool_resolution,
5421                cancelled,
5422                pending_events,
5423                events_overflowed,
5424            });
5425            (
5426                kernel_pid,
5427                kernel_handle,
5428                ActiveExecution::Tool(tool_execution),
5429                None,
5430            )
5431        } else {
5432            let kernel_command = match resolved.runtime {
5433                GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND,
5434                GuestRuntimeKind::WebAssembly => WASM_COMMAND,
5435                GuestRuntimeKind::Python => {
5436                    unreachable!("python child_process execution is rejected")
5437                }
5438            };
5439            let kernel_handle = vm
5440                .kernel
5441                .spawn_process(
5442                    kernel_command,
5443                    resolved.process_args.clone(),
5444                    SpawnOptions {
5445                        requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
5446                        parent_pid: Some(parent_kernel_pid),
5447                        env: resolved.env.clone(),
5448                        cwd: Some(resolved.guest_cwd.clone()),
5449                    },
5450                )
5451                .map_err(kernel_error)?;
5452            let kernel_pid = kernel_handle.pid();
5453            if request.options.detached {
5454                vm.kernel
5455                    .setsid(EXECUTION_DRIVER_NAME, kernel_pid)
5456                    .map_err(kernel_error)?;
5457            }
5458            let mut execution_env = resolved.env.clone();
5459            execution_env.insert(
5460                String::from(EXECUTION_SANDBOX_ROOT_ENV),
5461                normalize_host_path(&vm.cwd).to_string_lossy().into_owned(),
5462            );
5463
5464            let execution = match resolved.runtime {
5465                GuestRuntimeKind::JavaScript => {
5466                    execution_env.extend(sanitize_javascript_child_process_internal_bootstrap_env(
5467                        &request.options.internal_bootstrap_env,
5468                    ));
5469                    execution_env.insert(
5470                        String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
5471                        String::from("1"),
5472                    );
5473                    let context =
5474                        self.javascript_engine
5475                            .create_context(CreateJavascriptContextRequest {
5476                                vm_id: vm_id.to_owned(),
5477                                bootstrap_module: None,
5478                                compile_cache_root: Some(
5479                                    self.cache_root.join("node-compile-cache"),
5480                                ),
5481                            });
5482                    let inline_code = load_javascript_entrypoint_source(
5483                        vm,
5484                        &resolved.host_cwd,
5485                        &resolved.entrypoint,
5486                        &execution_env,
5487                    );
5488                    prepare_javascript_shadow(vm, &resolved)?;
5489
5490                    let built_reader = build_module_reader(vm, &resolved);
5491                    let guest_reader = built_reader.clone().map(|reader| {
5492                        Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader))
5493                            as Box<dyn GuestModuleReader>
5494                    });
5495                    let module_reader = built_reader
5496                        .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
5497                    let execution = self
5498                        .javascript_engine
5499                        .start_execution_with_module_reader(
5500                            StartJavascriptExecutionRequest {
5501                                guest_runtime: guest_runtime_identity(
5502                                    vm,
5503                                    Some(u64::from(kernel_pid)),
5504                                    Some(u64::from(parent_kernel_pid)),
5505                                ),
5506                                vm_id: vm_id.to_owned(),
5507                                context_id: context.context_id,
5508                                argv: std::iter::once(resolved.entrypoint.clone())
5509                                    .chain(resolved.execution_args.clone())
5510                                    .collect(),
5511                                env: execution_env,
5512                                cwd: resolved.host_cwd.clone(),
5513                                limits: javascript_execution_limits(vm),
5514                                inline_code,
5515                            },
5516                            module_reader,
5517                            guest_reader,
5518                        )
5519                        .map_err(javascript_error)?;
5520                    ActiveExecution::Javascript(execution)
5521                }
5522                GuestRuntimeKind::WebAssembly => {
5523                    execution_env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
5524                    let wasm_limits = wasm_execution_limits(vm);
5525                    let wasm_guest_runtime = guest_runtime_identity(
5526                        vm,
5527                        Some(u64::from(kernel_pid)),
5528                        Some(u64::from(parent_kernel_pid)),
5529                    );
5530                    let context = self.wasm_engine.create_context(CreateWasmContextRequest {
5531                        vm_id: vm_id.to_owned(),
5532                        module_path: Some(resolved.entrypoint.clone()),
5533                    });
5534                    let execution = self
5535                        .wasm_engine
5536                        .start_execution(StartWasmExecutionRequest {
5537                            vm_id: vm_id.to_owned(),
5538                            context_id: context.context_id,
5539                            argv: resolved.process_args.clone(),
5540                            env: execution_env,
5541                            cwd: resolved.host_cwd.clone(),
5542                            permission_tier: execution_wasm_permission_tier(
5543                                resolved
5544                                    .wasm_permission_tier
5545                                    .unwrap_or(WasmPermissionTier::Full),
5546                            ),
5547                            limits: wasm_limits,
5548                            guest_runtime: wasm_guest_runtime,
5549                        })
5550                        .map_err(wasm_error)?;
5551                    ActiveExecution::Wasm(Box::new(execution))
5552                }
5553                GuestRuntimeKind::Python => {
5554                    unreachable!("python child_process execution is rejected")
5555                }
5556            };
5557            let kernel_stdin_writer_fd = match javascript_child_process_stdin_mode(&request) {
5558                "pipe" => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5559                "ignore" => {
5560                    vm.kernel
5561                        .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, 0)
5562                        .map_err(kernel_error)?;
5563                    None
5564                }
5565                "inherit" => None,
5566                _ => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5567            };
5568            (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd)
5569        };
5570
5571        let process = vm
5572            .active_processes
5573            .get_mut(process_id)
5574            .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5575        process.child_processes.insert(
5576            child_process_id.clone(),
5577            ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
5578                .with_detached(request.options.detached)
5579                .with_guest_cwd(resolved.guest_cwd.clone())
5580                .with_env(resolved.env.clone())
5581                .with_host_cwd(resolved.host_cwd.clone()),
5582        );
5583        if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd {
5584            process
5585                .child_processes
5586                .get_mut(&child_process_id)
5587                .ok_or_else(|| {
5588                    SidecarError::InvalidState(format!(
5589                        "child process {child_process_id} disappeared during spawn"
5590                    ))
5591                })?
5592                .kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd);
5593        }
5594        Ok(json!({
5595            "childId": child_process_id,
5596            "pid": kernel_pid,
5597            "command": resolved.command,
5598            "args": resolved.process_args,
5599        }))
5600    }
5601
5602    pub(crate) fn spawn_javascript_child_process_sync(
5603        &mut self,
5604        vm_id: &str,
5605        process_id: &str,
5606        request: JavascriptChildProcessSpawnRequest,
5607        max_buffer: Option<usize>,
5608    ) -> Result<Value, SidecarError> {
5609        let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?;
5610        let timeout_deadline = request
5611            .options
5612            .timeout
5613            .map(|timeout_ms| Instant::now() + Duration::from_millis(timeout_ms));
5614        let timeout_signal = request
5615            .options
5616            .kill_signal
5617            .clone()
5618            .unwrap_or_else(|| String::from("SIGTERM"));
5619        let spawned = self.spawn_javascript_child_process(vm_id, process_id, request)?;
5620        let child_process_id = spawned
5621            .get("childId")
5622            .and_then(Value::as_str)
5623            .ok_or_else(|| {
5624                SidecarError::InvalidState(String::from(
5625                    "child_process.spawn_sync response is missing childId",
5626                ))
5627            })?
5628            .to_owned();
5629
5630        if let Some(input) = sync_input.as_deref() {
5631            self.write_javascript_child_process_stdin(vm_id, process_id, &child_process_id, input)?;
5632        }
5633        self.close_javascript_child_process_stdin(vm_id, process_id, &child_process_id)?;
5634
5635        let max_buffer = max_buffer.unwrap_or(1024 * 1024);
5636        let mut stdout = Vec::new();
5637        let mut stderr = Vec::new();
5638        let mut max_buffer_exceeded = false;
5639        let mut kill_sent = false;
5640        let mut timed_out = false;
5641
5642        let exit_code = loop {
5643            let wait_ms = if let Some(deadline) = timeout_deadline {
5644                let now = Instant::now();
5645                if now >= deadline {
5646                    if !kill_sent {
5647                        timed_out = true;
5648                        self.kill_javascript_child_process(
5649                            vm_id,
5650                            process_id,
5651                            &child_process_id,
5652                            &timeout_signal,
5653                        )?;
5654                        kill_sent = true;
5655                    }
5656                    0
5657                } else {
5658                    u64::try_from(deadline.saturating_duration_since(now).as_millis().min(50))
5659                        .unwrap_or(50)
5660                }
5661            } else {
5662                50
5663            };
5664            let event =
5665                self.poll_javascript_child_process(vm_id, process_id, &child_process_id, wait_ms)?;
5666            if event.is_null() {
5667                continue;
5668            }
5669
5670            match event.get("type").and_then(Value::as_str) {
5671                Some("stdout") => {
5672                    let chunk = javascript_sync_rpc_bytes_arg(
5673                        &[event.get("data").cloned().unwrap_or(Value::Null)],
5674                        0,
5675                        "child_process.spawn_sync stdout",
5676                    )?;
5677                    stdout.extend_from_slice(&chunk);
5678                    if stdout.len() > max_buffer && !kill_sent {
5679                        max_buffer_exceeded = true;
5680                        self.kill_javascript_child_process(
5681                            vm_id,
5682                            process_id,
5683                            &child_process_id,
5684                            "SIGTERM",
5685                        )?;
5686                        kill_sent = true;
5687                    }
5688                }
5689                Some("stderr") => {
5690                    let chunk = javascript_sync_rpc_bytes_arg(
5691                        &[event.get("data").cloned().unwrap_or(Value::Null)],
5692                        0,
5693                        "child_process.spawn_sync stderr",
5694                    )?;
5695                    stderr.extend_from_slice(&chunk);
5696                    if stderr.len() > max_buffer && !kill_sent {
5697                        max_buffer_exceeded = true;
5698                        self.kill_javascript_child_process(
5699                            vm_id,
5700                            process_id,
5701                            &child_process_id,
5702                            "SIGTERM",
5703                        )?;
5704                        kill_sent = true;
5705                    }
5706                }
5707                Some("exit") => {
5708                    break event
5709                        .get("exitCode")
5710                        .and_then(Value::as_i64)
5711                        .map(|value| value as i32)
5712                        .unwrap_or(1);
5713                }
5714                _ => {}
5715            }
5716        };
5717
5718        Ok(json!({
5719            "stdout": String::from_utf8_lossy(&stdout),
5720            "stderr": String::from_utf8_lossy(&stderr),
5721            "code": exit_code,
5722            "signal": if timed_out { Value::String(timeout_signal) } else { Value::Null },
5723            "timedOut": timed_out,
5724            "maxBufferExceeded": max_buffer_exceeded,
5725        }))
5726    }
5727
5728    fn spawn_descendant_javascript_child_process(
5729        &mut self,
5730        vm_id: &str,
5731        process_id: &str,
5732        current_process_path: &[&str],
5733        request: JavascriptChildProcessSpawnRequest,
5734    ) -> Result<Value, SidecarError> {
5735        let current_process_label =
5736            Self::child_process_path_label(process_id, current_process_path);
5737        let (resolved, parent_kernel_pid) = {
5738            let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?;
5739            let root = vm
5740                .active_processes
5741                .get(process_id)
5742                .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5743            let parent =
5744                Self::active_process_by_path(root, current_process_path).ok_or_else(|| {
5745                    SidecarError::InvalidState(format!(
5746                        "unknown child process path {current_process_label} during nested spawn"
5747                    ))
5748                })?;
5749            (
5750                self.resolve_javascript_child_process_execution(
5751                    vm,
5752                    &parent.env,
5753                    &parent.guest_cwd,
5754                    &parent.host_cwd,
5755                    &request,
5756                )?,
5757                parent.kernel_pid,
5758            )
5759        };
5760
5761        let sidecar_requests = self.sidecar_requests.clone();
5762        let vm = self
5763            .vms
5764            .get_mut(vm_id)
5765            .ok_or_else(|| missing_vm_error(vm_id))?;
5766        let child_process_id = {
5767            let root = vm
5768                .active_processes
5769                .get_mut(process_id)
5770                .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5771            let parent =
5772                Self::active_process_by_path_mut(root, current_process_path).ok_or_else(|| {
5773                    SidecarError::InvalidState(format!(
5774                        "unknown child process path {current_process_label} during nested spawn"
5775                    ))
5776                })?;
5777            parent.allocate_child_process_id()
5778        };
5779        let mut child_path = current_process_path.to_vec();
5780        child_path.push(child_process_id.as_str());
5781        let (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd) = if resolved
5782            .tool_command
5783        {
5784            let tool_resolution = resolve_tool_command(
5785                vm,
5786                &resolved.command,
5787                &resolved.execution_args,
5788                Some(&resolved.guest_cwd),
5789            )?
5790            .ok_or_else(|| {
5791                SidecarError::InvalidState(format!(
5792                    "tool command no longer resolves: {}",
5793                    resolved.command
5794                ))
5795            })?;
5796            let kernel_handle = vm
5797                .kernel
5798                .create_virtual_process(
5799                    EXECUTION_DRIVER_NAME,
5800                    TOOL_DRIVER_NAME,
5801                    &resolved.command,
5802                    resolved.process_args.clone(),
5803                    VirtualProcessOptions {
5804                        parent_pid: Some(parent_kernel_pid),
5805                        env: resolved.env.clone(),
5806                        cwd: Some(resolved.guest_cwd.clone()),
5807                    },
5808                )
5809                .map_err(kernel_error)?;
5810            let kernel_pid = kernel_handle.pid();
5811            let tool_execution = ToolExecution::default();
5812            let cancelled = tool_execution.cancelled.clone();
5813            let pending_events = tool_execution.pending_events.clone();
5814            let events_overflowed = tool_execution.events_overflowed.clone();
5815            spawn_tool_process_events(ToolProcessEventRequest {
5816                sidecar_requests: sidecar_requests.clone(),
5817                connection_id: vm.connection_id.clone(),
5818                session_id: vm.session_id.clone(),
5819                vm_id: vm_id.to_owned(),
5820                tool_resolution,
5821                cancelled,
5822                pending_events,
5823                events_overflowed,
5824            });
5825            (
5826                kernel_pid,
5827                kernel_handle,
5828                ActiveExecution::Tool(tool_execution),
5829                None,
5830            )
5831        } else {
5832            let kernel_command = match resolved.runtime {
5833                GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND,
5834                GuestRuntimeKind::WebAssembly => WASM_COMMAND,
5835                GuestRuntimeKind::Python => {
5836                    unreachable!("python child_process execution is rejected")
5837                }
5838            };
5839            let kernel_handle = vm
5840                .kernel
5841                .spawn_process(
5842                    kernel_command,
5843                    resolved.process_args.clone(),
5844                    SpawnOptions {
5845                        requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
5846                        parent_pid: Some(parent_kernel_pid),
5847                        env: resolved.env.clone(),
5848                        cwd: Some(resolved.guest_cwd.clone()),
5849                    },
5850                )
5851                .map_err(kernel_error)?;
5852            let kernel_pid = kernel_handle.pid();
5853            if request.options.detached {
5854                vm.kernel
5855                    .setsid(EXECUTION_DRIVER_NAME, kernel_pid)
5856                    .map_err(kernel_error)?;
5857            }
5858            let mut execution_env = resolved.env.clone();
5859            execution_env.insert(
5860                String::from(EXECUTION_SANDBOX_ROOT_ENV),
5861                normalize_host_path(&vm.cwd).to_string_lossy().into_owned(),
5862            );
5863            let execution = match resolved.runtime {
5864                GuestRuntimeKind::JavaScript => {
5865                    execution_env.extend(sanitize_javascript_child_process_internal_bootstrap_env(
5866                        &request.options.internal_bootstrap_env,
5867                    ));
5868                    execution_env.insert(
5869                        String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
5870                        String::from("1"),
5871                    );
5872                    let context =
5873                        self.javascript_engine
5874                            .create_context(CreateJavascriptContextRequest {
5875                                vm_id: vm_id.to_owned(),
5876                                bootstrap_module: None,
5877                                compile_cache_root: Some(
5878                                    self.cache_root.join("node-compile-cache"),
5879                                ),
5880                            });
5881                    let inline_code = load_javascript_entrypoint_source(
5882                        vm,
5883                        &resolved.host_cwd,
5884                        &resolved.entrypoint,
5885                        &execution_env,
5886                    );
5887                    prepare_javascript_shadow(vm, &resolved)?;
5888
5889                    let built_reader = build_module_reader(vm, &resolved);
5890                    let guest_reader = built_reader.clone().map(|reader| {
5891                        Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader))
5892                            as Box<dyn GuestModuleReader>
5893                    });
5894                    let module_reader = built_reader
5895                        .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
5896                    let execution = self
5897                        .javascript_engine
5898                        .start_execution_with_module_reader(
5899                            StartJavascriptExecutionRequest {
5900                                guest_runtime: guest_runtime_identity(
5901                                    vm,
5902                                    Some(u64::from(kernel_pid)),
5903                                    Some(u64::from(parent_kernel_pid)),
5904                                ),
5905                                vm_id: vm_id.to_owned(),
5906                                context_id: context.context_id,
5907                                argv: std::iter::once(resolved.entrypoint.clone())
5908                                    .chain(resolved.execution_args.clone())
5909                                    .collect(),
5910                                env: execution_env,
5911                                cwd: resolved.host_cwd.clone(),
5912                                limits: javascript_execution_limits(vm),
5913                                inline_code,
5914                            },
5915                            module_reader,
5916                            guest_reader,
5917                        )
5918                        .map_err(javascript_error)?;
5919                    ActiveExecution::Javascript(execution)
5920                }
5921                GuestRuntimeKind::WebAssembly => {
5922                    execution_env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
5923                    let wasm_limits = wasm_execution_limits(vm);
5924                    let wasm_guest_runtime = guest_runtime_identity(
5925                        vm,
5926                        Some(u64::from(kernel_pid)),
5927                        Some(u64::from(parent_kernel_pid)),
5928                    );
5929                    let context = self.wasm_engine.create_context(CreateWasmContextRequest {
5930                        vm_id: vm_id.to_owned(),
5931                        module_path: Some(resolved.entrypoint.clone()),
5932                    });
5933                    let execution = self
5934                        .wasm_engine
5935                        .start_execution(StartWasmExecutionRequest {
5936                            vm_id: vm_id.to_owned(),
5937                            context_id: context.context_id,
5938                            argv: resolved.process_args.clone(),
5939                            env: execution_env,
5940                            cwd: resolved.host_cwd.clone(),
5941                            permission_tier: execution_wasm_permission_tier(
5942                                resolved
5943                                    .wasm_permission_tier
5944                                    .unwrap_or(WasmPermissionTier::Full),
5945                            ),
5946                            limits: wasm_limits,
5947                            guest_runtime: wasm_guest_runtime,
5948                        })
5949                        .map_err(wasm_error)?;
5950                    ActiveExecution::Wasm(Box::new(execution))
5951                }
5952                GuestRuntimeKind::Python => {
5953                    unreachable!("python child_process execution is rejected")
5954                }
5955            };
5956            let kernel_stdin_writer_fd = match javascript_child_process_stdin_mode(&request) {
5957                "pipe" => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5958                "ignore" => {
5959                    vm.kernel
5960                        .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, 0)
5961                        .map_err(kernel_error)?;
5962                    None
5963                }
5964                "inherit" => None,
5965                _ => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5966            };
5967            (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd)
5968        };
5969
5970        let root = vm
5971            .active_processes
5972            .get_mut(process_id)
5973            .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5974        let parent =
5975            Self::active_process_by_path_mut(root, current_process_path).ok_or_else(|| {
5976                SidecarError::InvalidState(format!(
5977                    "unknown child process path {current_process_label} during nested spawn"
5978                ))
5979            })?;
5980        parent.child_processes.insert(
5981            child_process_id.clone(),
5982            ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
5983                .with_detached(request.options.detached)
5984                .with_guest_cwd(resolved.guest_cwd.clone())
5985                .with_env(resolved.env.clone())
5986                .with_host_cwd(resolved.host_cwd.clone()),
5987        );
5988        if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd {
5989            parent
5990                .child_processes
5991                .get_mut(&child_process_id)
5992                .ok_or_else(|| {
5993                    SidecarError::InvalidState(format!(
5994                        "child process {child_process_id} disappeared during nested spawn"
5995                    ))
5996                })?
5997                .kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd);
5998        }
5999        Ok(json!({
6000            "childId": child_process_id,
6001            "pid": kernel_pid,
6002            "command": resolved.command,
6003            "args": resolved.process_args,
6004        }))
6005    }
6006
6007    fn spawn_descendant_javascript_child_process_sync(
6008        &mut self,
6009        vm_id: &str,
6010        process_id: &str,
6011        current_process_path: &[&str],
6012        request: JavascriptChildProcessSpawnRequest,
6013        max_buffer: Option<usize>,
6014    ) -> Result<Value, SidecarError> {
6015        let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?;
6016        let timeout_deadline = request
6017            .options
6018            .timeout
6019            .map(|timeout_ms| Instant::now() + Duration::from_millis(timeout_ms));
6020        let timeout_signal = request
6021            .options
6022            .kill_signal
6023            .clone()
6024            .unwrap_or_else(|| String::from("SIGTERM"));
6025        let spawned = self.spawn_descendant_javascript_child_process(
6026            vm_id,
6027            process_id,
6028            current_process_path,
6029            request,
6030        )?;
6031        let child_process_id = spawned
6032            .get("childId")
6033            .and_then(Value::as_str)
6034            .ok_or_else(|| {
6035                SidecarError::InvalidState(String::from(
6036                    "child_process.spawn_sync response is missing childId",
6037                ))
6038            })?
6039            .to_owned();
6040
6041        if let Some(input) = sync_input.as_deref() {
6042            self.write_descendant_javascript_child_process_stdin(
6043                vm_id,
6044                process_id,
6045                current_process_path,
6046                &child_process_id,
6047                input,
6048            )?;
6049        }
6050        self.close_descendant_javascript_child_process_stdin(
6051            vm_id,
6052            process_id,
6053            current_process_path,
6054            &child_process_id,
6055        )?;
6056
6057        let max_buffer = max_buffer.unwrap_or(1024 * 1024);
6058        let mut stdout = Vec::new();
6059        let mut stderr = Vec::new();
6060        let mut max_buffer_exceeded = false;
6061        let mut kill_sent = false;
6062        let mut timed_out = false;
6063
6064        let exit_code = loop {
6065            let wait_ms = if let Some(deadline) = timeout_deadline {
6066                let now = Instant::now();
6067                if now >= deadline {
6068                    if !kill_sent {
6069                        timed_out = true;
6070                        self.kill_descendant_javascript_child_process(
6071                            vm_id,
6072                            process_id,
6073                            current_process_path,
6074                            &child_process_id,
6075                            &timeout_signal,
6076                        )?;
6077                        kill_sent = true;
6078                    }
6079                    0
6080                } else {
6081                    u64::try_from(deadline.saturating_duration_since(now).as_millis().min(50))
6082                        .unwrap_or(50)
6083                }
6084            } else {
6085                50
6086            };
6087            let event = self.poll_descendant_javascript_child_process(
6088                vm_id,
6089                process_id,
6090                current_process_path,
6091                &child_process_id,
6092                wait_ms,
6093            )?;
6094            if event.is_null() {
6095                continue;
6096            }
6097
6098            match event.get("type").and_then(Value::as_str) {
6099                Some("stdout") => {
6100                    let chunk = javascript_sync_rpc_bytes_arg(
6101                        &[event.get("data").cloned().unwrap_or(Value::Null)],
6102                        0,
6103                        "child_process.spawn_sync stdout",
6104                    )?;
6105                    stdout.extend_from_slice(&chunk);
6106                    if stdout.len() > max_buffer && !kill_sent {
6107                        max_buffer_exceeded = true;
6108                        self.kill_descendant_javascript_child_process(
6109                            vm_id,
6110                            process_id,
6111                            current_process_path,
6112                            &child_process_id,
6113                            "SIGTERM",
6114                        )?;
6115                        kill_sent = true;
6116                    }
6117                }
6118                Some("stderr") => {
6119                    let chunk = javascript_sync_rpc_bytes_arg(
6120                        &[event.get("data").cloned().unwrap_or(Value::Null)],
6121                        0,
6122                        "child_process.spawn_sync stderr",
6123                    )?;
6124                    stderr.extend_from_slice(&chunk);
6125                    if stderr.len() > max_buffer && !kill_sent {
6126                        max_buffer_exceeded = true;
6127                        self.kill_descendant_javascript_child_process(
6128                            vm_id,
6129                            process_id,
6130                            current_process_path,
6131                            &child_process_id,
6132                            "SIGTERM",
6133                        )?;
6134                        kill_sent = true;
6135                    }
6136                }
6137                Some("exit") => {
6138                    break event
6139                        .get("exitCode")
6140                        .and_then(Value::as_i64)
6141                        .map(|value| value as i32)
6142                        .unwrap_or(1);
6143                }
6144                _ => {}
6145            }
6146        };
6147
6148        Ok(json!({
6149            "stdout": String::from_utf8_lossy(&stdout),
6150            "stderr": String::from_utf8_lossy(&stderr),
6151            "code": exit_code,
6152            "signal": if timed_out { Value::String(timeout_signal) } else { Value::Null },
6153            "timedOut": timed_out,
6154            "maxBufferExceeded": max_buffer_exceeded,
6155        }))
6156    }
6157
6158    fn handle_descendant_javascript_child_process_rpc(
6159        &mut self,
6160        vm_id: &str,
6161        process_id: &str,
6162        current_process_path: &[&str],
6163        request: &JavascriptSyncRpcRequest,
6164    ) -> Result<Value, SidecarError> {
6165        match request.method.as_str() {
6166            "child_process.spawn" => {
6167                let Some(vm) = self.vms.get(vm_id) else {
6168                    return Ok(Value::Null);
6169                };
6170                let (payload, _) = parse_javascript_child_process_spawn_request(vm, &request.args)?;
6171                self.spawn_descendant_javascript_child_process(
6172                    vm_id,
6173                    process_id,
6174                    current_process_path,
6175                    payload,
6176                )
6177            }
6178            "child_process.spawn_sync" => {
6179                let Some(vm) = self.vms.get(vm_id) else {
6180                    return Ok(Value::Null);
6181                };
6182                let (payload, max_buffer) =
6183                    parse_javascript_child_process_spawn_request(vm, &request.args)?;
6184                self.spawn_descendant_javascript_child_process_sync(
6185                    vm_id,
6186                    process_id,
6187                    current_process_path,
6188                    payload,
6189                    max_buffer,
6190                )
6191            }
6192            "child_process.poll" => {
6193                let child_process_id =
6194                    javascript_sync_rpc_arg_str(&request.args, 0, "child_process.poll child id")?;
6195                let wait_ms = javascript_sync_rpc_arg_u64_optional(
6196                    &request.args,
6197                    1,
6198                    "child_process.poll wait ms",
6199                )?
6200                .unwrap_or_default();
6201                self.poll_descendant_javascript_child_process(
6202                    vm_id,
6203                    process_id,
6204                    current_process_path,
6205                    child_process_id,
6206                    wait_ms,
6207                )
6208            }
6209            "child_process.write_stdin" => {
6210                let child_process_id = javascript_sync_rpc_arg_str(
6211                    &request.args,
6212                    0,
6213                    "child_process.write_stdin child id",
6214                )?;
6215                let chunk = javascript_sync_rpc_bytes_arg(
6216                    &request.args,
6217                    1,
6218                    "child_process.write_stdin chunk",
6219                )?;
6220                self.write_descendant_javascript_child_process_stdin(
6221                    vm_id,
6222                    process_id,
6223                    current_process_path,
6224                    child_process_id,
6225                    &chunk,
6226                )?;
6227                Ok(Value::Null)
6228            }
6229            "child_process.close_stdin" => {
6230                let child_process_id = javascript_sync_rpc_arg_str(
6231                    &request.args,
6232                    0,
6233                    "child_process.close_stdin child id",
6234                )?;
6235                self.close_descendant_javascript_child_process_stdin(
6236                    vm_id,
6237                    process_id,
6238                    current_process_path,
6239                    child_process_id,
6240                )?;
6241                Ok(Value::Null)
6242            }
6243            "child_process.kill" => {
6244                let child_process_id =
6245                    javascript_sync_rpc_arg_str(&request.args, 0, "child_process.kill child id")?;
6246                let signal =
6247                    javascript_sync_rpc_arg_str(&request.args, 1, "child_process.kill signal")?;
6248                self.kill_descendant_javascript_child_process(
6249                    vm_id,
6250                    process_id,
6251                    current_process_path,
6252                    child_process_id,
6253                    signal,
6254                )?;
6255                Ok(Value::Null)
6256            }
6257            _ => Err(SidecarError::InvalidState(format!(
6258                "unsupported nested child process RPC method {}",
6259                request.method
6260            ))),
6261        }
6262    }
6263
6264    fn poll_descendant_javascript_child_process(
6265        &mut self,
6266        vm_id: &str,
6267        process_id: &str,
6268        current_process_path: &[&str],
6269        child_process_id: &str,
6270        wait_ms: u64,
6271    ) -> Result<Value, SidecarError> {
6272        let mut child_path = current_process_path.to_vec();
6273        child_path.push(child_process_id);
6274        let child_gone_error = || javascript_child_process_gone_error(process_id, &child_path);
6275        let deadline = Instant::now() + Duration::from_millis(wait_ms);
6276        let mut polled_once = false;
6277
6278        loop {
6279            self.drain_queued_descendant_javascript_child_process_events(
6280                vm_id,
6281                process_id,
6282                &child_path,
6283            )?;
6284            enum ChildPollResult {
6285                Event(Box<Option<ActiveExecutionEvent>>),
6286                RecoverRuntimeExit,
6287                Timeout,
6288            }
6289            let wait = if wait_ms == 0 {
6290                Duration::ZERO
6291            } else {
6292                deadline.saturating_duration_since(Instant::now())
6293            };
6294            let poll_result = {
6295                let Some(vm) = self.vms.get_mut(vm_id) else {
6296                    return Ok(Value::Null);
6297                };
6298                let Some(parent) =
6299                    Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6300                else {
6301                    return Err(child_gone_error());
6302                };
6303                let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6304                    return Err(child_gone_error());
6305                };
6306                if let Some(event) = child.pending_execution_events.pop_front() {
6307                    ChildPollResult::Event(Box::new(Some(event)))
6308                } else if polled_once && wait.is_zero() {
6309                    ChildPollResult::Timeout
6310                } else {
6311                    polled_once = true;
6312                    match child.execution.poll_event_blocking(wait) {
6313                        Ok(Some(event)) => ChildPollResult::Event(Box::new(Some(event))),
6314                        Ok(None) => ChildPollResult::RecoverRuntimeExit,
6315                        Err(SidecarError::Execution(message))
6316                            if (child.runtime == GuestRuntimeKind::JavaScript
6317                                && closed_javascript_event_channel(&message))
6318                                || (child.runtime == GuestRuntimeKind::Python
6319                                    && closed_python_event_channel(&message))
6320                                || (child.runtime == GuestRuntimeKind::WebAssembly
6321                                    && closed_wasm_event_channel(&message)) =>
6322                        {
6323                            ChildPollResult::RecoverRuntimeExit
6324                        }
6325                        Err(error) => return Err(error),
6326                    }
6327                }
6328            };
6329            let event = match poll_result {
6330                ChildPollResult::Event(event) => *event,
6331                ChildPollResult::Timeout => return Ok(Value::Null),
6332                ChildPollResult::RecoverRuntimeExit => self
6333                    .recover_descendant_runtime_child_process_event(
6334                        vm_id,
6335                        process_id,
6336                        current_process_path,
6337                        child_process_id,
6338                        wait.as_millis().try_into().unwrap_or(u64::MAX),
6339                    )?,
6340            };
6341
6342            let Some(event) = event else {
6343                return Ok(Value::Null);
6344            };
6345
6346            match event {
6347                ActiveExecutionEvent::Stdout(chunk) => {
6348                    return Ok(json!({
6349                        "type": "stdout",
6350                        "data": javascript_sync_rpc_bytes_value(&chunk),
6351                    }));
6352                }
6353                ActiveExecutionEvent::Stderr(chunk) => {
6354                    return Ok(json!({
6355                        "type": "stderr",
6356                        "data": javascript_sync_rpc_bytes_value(&chunk),
6357                    }));
6358                }
6359                ActiveExecutionEvent::Exited(exit_code) => {
6360                    let had_trailing_events = {
6361                        let Some(vm) = self.vms.get_mut(vm_id) else {
6362                            return Ok(Value::Null);
6363                        };
6364                        let Some(parent) = Self::descendant_parent_process_mut(
6365                            vm,
6366                            process_id,
6367                            current_process_path,
6368                        ) else {
6369                            return Ok(Value::Null);
6370                        };
6371                        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6372                            return Ok(Value::Null);
6373                        };
6374                        let deadline = Instant::now() + Duration::from_millis(150);
6375                        loop {
6376                            let wait = deadline.saturating_duration_since(Instant::now());
6377                            let next = poll_child_execution_after_exit(child, wait)?;
6378                            let Some(next) = next else {
6379                                break;
6380                            };
6381                            if matches!(next, ActiveExecutionEvent::Exited(_)) {
6382                                continue;
6383                            }
6384                            child.queue_pending_execution_event(next)?;
6385                            if Instant::now() >= deadline {
6386                                break;
6387                            }
6388                        }
6389                        if !child.pending_execution_events.is_empty() {
6390                            child.queue_pending_execution_event(ActiveExecutionEvent::Exited(
6391                                exit_code,
6392                            ))?;
6393                            true
6394                        } else {
6395                            false
6396                        }
6397                    };
6398                    if had_trailing_events {
6399                        continue;
6400                    }
6401
6402                    let parent_signal_key =
6403                        Self::child_process_signal_key(process_id, current_process_path);
6404                    let Some(vm) = self.vms.get_mut(vm_id) else {
6405                        return Ok(Value::Null);
6406                    };
6407                    let signal_name = {
6408                        let Some(parent) = Self::descendant_parent_process_mut(
6409                            vm,
6410                            process_id,
6411                            current_process_path,
6412                        ) else {
6413                            return Ok(Value::Null);
6414                        };
6415                        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6416                            return Ok(Value::Null);
6417                        };
6418                        child.pending_self_signal_exit.take().and_then(|signal| {
6419                            if exit_code == 128 + signal {
6420                                canonical_signal_name(signal).map(str::to_owned)
6421                            } else {
6422                                None
6423                            }
6424                        })
6425                    };
6426                    let (parent_runtime_pid, parent_v8_signal_session, should_signal_parent) = {
6427                        let Some(parent) =
6428                            Self::descendant_parent_process(vm, process_id, current_process_path)
6429                        else {
6430                            return Ok(Value::Null);
6431                        };
6432                        (
6433                            parent.execution.child_pid(),
6434                            parent.execution.javascript_v8_session_handle().filter(|_| {
6435                                matches!(
6436                                    &parent.execution,
6437                                    ActiveExecution::Javascript(execution)
6438                                        if execution.uses_shared_v8_runtime()
6439                                )
6440                            }),
6441                            vm.signal_states
6442                                .get(parent_signal_key)
6443                                .and_then(|handlers| handlers.get(&(libc::SIGCHLD as u32)))
6444                                .is_some_and(|registration| {
6445                                    registration.action != SignalDispositionAction::Default
6446                                }),
6447                        )
6448                    };
6449                    let Some(parent) =
6450                        Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6451                    else {
6452                        return Ok(Value::Null);
6453                    };
6454                    let Some(mut child) = parent.child_processes.remove(child_process_id) else {
6455                        return Ok(Value::Null);
6456                    };
6457                    let child_process_label =
6458                        Self::child_process_path_label(process_id, &child_path);
6459                    let detached_children =
6460                        Self::adopt_detached_child_processes(&child_process_label, &mut child);
6461                    sync_process_host_writes_to_kernel(vm, &child)?;
6462                    terminate_child_process_tree(&mut vm.kernel, &mut child);
6463                    child.kernel_handle.finish(exit_code);
6464                    let _ = vm.kernel.wait_and_reap(child.kernel_pid);
6465                    vm.signal_states.remove(child_process_id);
6466                    for (detached_process_id, detached_child) in detached_children {
6467                        vm.detached_child_processes
6468                            .insert(detached_process_id.clone());
6469                        vm.active_processes
6470                            .insert(detached_process_id, detached_child);
6471                    }
6472                    if should_signal_parent {
6473                        if let Some(session) = parent_v8_signal_session {
6474                            dispatch_v8_session_signal_async(session, libc::SIGCHLD);
6475                        } else {
6476                            signal_runtime_process(parent_runtime_pid, libc::SIGCHLD)?;
6477                        }
6478                    }
6479                    let mut payload = Map::new();
6480                    payload.insert(String::from("type"), Value::String(String::from("exit")));
6481                    payload.insert(String::from("exitCode"), Value::from(exit_code));
6482                    if let Some(signal_name) = signal_name {
6483                        payload.insert(String::from("signal"), Value::String(signal_name));
6484                    }
6485                    return Ok(Value::Object(payload));
6486                }
6487                ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
6488                    let mut current_child_path = current_process_path.to_vec();
6489                    current_child_path.push(child_process_id);
6490                    let response = if request.method == "process.signal_state" {
6491                        let (signal, registration) =
6492                            parse_process_signal_state_request(&request.args)?;
6493                        let Some(vm) = self.vms.get_mut(vm_id) else {
6494                            return Ok(Value::Null);
6495                        };
6496                        let signal_key =
6497                            Self::child_process_signal_key(process_id, &current_child_path)
6498                                .to_owned();
6499                        apply_process_signal_state_update(
6500                            &mut vm.signal_states,
6501                            &signal_key,
6502                            signal,
6503                            registration,
6504                        );
6505                        Ok(Value::Null)
6506                    } else if request.method == "process.kill" {
6507                        self.handle_descendant_process_kill_rpc(
6508                            vm_id,
6509                            process_id,
6510                            current_process_path,
6511                            child_process_id,
6512                            &request,
6513                        )
6514                    } else if request.method.starts_with("child_process.") {
6515                        self.handle_descendant_javascript_child_process_rpc(
6516                            vm_id,
6517                            process_id,
6518                            &current_child_path,
6519                            &request,
6520                        )
6521                    } else {
6522                        let Some(vm) = self.vms.get_mut(vm_id) else {
6523                            return Ok(Value::Null);
6524                        };
6525                        let resource_limits = vm.kernel.resource_limits().clone();
6526                        let network_counts = vm_network_resource_counts(vm);
6527                        let socket_paths = build_javascript_socket_path_context(vm)?;
6528                        let Some(root) = vm.active_processes.get_mut(process_id) else {
6529                            return Ok(Value::Null);
6530                        };
6531                        let Some(parent) =
6532                            Self::active_process_by_path_mut(root, current_process_path)
6533                        else {
6534                            return Ok(Value::Null);
6535                        };
6536                        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6537                            return Ok(Value::Null);
6538                        };
6539                        service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
6540                            bridge: &self.bridge,
6541                            vm_id,
6542                            dns: &vm.dns,
6543                            socket_paths: &socket_paths,
6544                            kernel: &mut vm.kernel,
6545                            process: child,
6546                            sync_request: &request,
6547                            resource_limits: &resource_limits,
6548                            network_counts,
6549                        })
6550                    };
6551
6552                    let Some(vm) = self.vms.get_mut(vm_id) else {
6553                        return Ok(Value::Null);
6554                    };
6555                    let Some(parent) =
6556                        Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6557                    else {
6558                        return Ok(Value::Null);
6559                    };
6560                    let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6561                        return Ok(Value::Null);
6562                    };
6563                    let parent_signal_event = response.as_ref().ok().and_then(|result| {
6564                        let target_path_label =
6565                            Self::child_process_path_label(process_id, current_process_path);
6566                        if request.method != "process.kill"
6567                            || result.get("action").and_then(Value::as_str) != Some("user")
6568                            || result.get("targetProcessPath").and_then(Value::as_str)
6569                                != Some(target_path_label.as_str())
6570                        {
6571                            return None;
6572                        }
6573                        Some(json!({
6574                            "type": "signal",
6575                            "signal": result.get("signal").and_then(Value::as_str).unwrap_or_default(),
6576                            "number": result.get("number").and_then(Value::as_i64).unwrap_or_default(),
6577                        }))
6578                    });
6579                    match response {
6580                        Ok(result) => child
6581                            .execution
6582                            .respond_javascript_sync_rpc_success(request.id, result)
6583                            .or_else(ignore_stale_javascript_sync_rpc_response)?,
6584                        Err(error) => child
6585                            .execution
6586                            .respond_javascript_sync_rpc_error(
6587                                request.id,
6588                                javascript_sync_rpc_error_code(&error),
6589                                error.to_string(),
6590                            )
6591                            .or_else(ignore_stale_javascript_sync_rpc_response)?,
6592                    }
6593                    if let Some(event) = parent_signal_event {
6594                        return Ok(event);
6595                    }
6596                }
6597                ActiveExecutionEvent::PythonVfsRpcRequest(_) => {
6598                    return Err(SidecarError::InvalidState(String::from(
6599                        "nested Python child_process execution is not supported yet",
6600                    )));
6601                }
6602                ActiveExecutionEvent::SignalState {
6603                    signal,
6604                    registration,
6605                } => {
6606                    let Some(vm) = self.vms.get_mut(vm_id) else {
6607                        return Ok(Value::Null);
6608                    };
6609                    let signal_key =
6610                        Self::child_process_signal_key(process_id, &child_path).to_owned();
6611                    apply_process_signal_state_update(
6612                        &mut vm.signal_states,
6613                        &signal_key,
6614                        signal,
6615                        registration.clone(),
6616                    );
6617                    return Ok(json!({
6618                        "type": "signal_state",
6619                        "signal": signal,
6620                        "registration": registration,
6621                    }));
6622                }
6623            }
6624        }
6625    }
6626
6627    fn recover_descendant_runtime_child_process_event(
6628        &mut self,
6629        vm_id: &str,
6630        process_id: &str,
6631        current_process_path: &[&str],
6632        child_process_id: &str,
6633        wait_ms: u64,
6634    ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
6635        let (
6636            parent_kernel_pid,
6637            child_kernel_pid,
6638            child_runtime_pid,
6639            child_runtime,
6640            child_shared_runtime,
6641        ) = {
6642            let mut child_path = current_process_path.to_vec();
6643            child_path.push(child_process_id);
6644            let Some(vm) = self.vms.get_mut(vm_id) else {
6645                return Ok(None);
6646            };
6647            let Some(parent) =
6648                Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6649            else {
6650                return Err(javascript_child_process_gone_error(process_id, &child_path));
6651            };
6652            let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6653                return Err(javascript_child_process_gone_error(process_id, &child_path));
6654            };
6655            (
6656                parent.kernel_pid,
6657                child.kernel_pid,
6658                child.execution.child_pid(),
6659                child.runtime.clone(),
6660                child.execution.uses_shared_v8_runtime(),
6661            )
6662        };
6663        if child_runtime != GuestRuntimeKind::JavaScript
6664            && child_runtime != GuestRuntimeKind::Python
6665            && child_runtime != GuestRuntimeKind::WebAssembly
6666        {
6667            return Ok(None);
6668        }
6669        let wait_deadline = Instant::now() + Duration::from_millis(wait_ms.min(25));
6670        loop {
6671            let Some(vm) = self.vms.get_mut(vm_id) else {
6672                return Ok(None);
6673            };
6674            if let Some(process_info) = vm.kernel.list_processes().get(&child_kernel_pid) {
6675                if process_info.status == ProcessStatus::Exited {
6676                    return Ok(Some(ActiveExecutionEvent::Exited(
6677                        process_info.exit_code.unwrap_or(0),
6678                    )));
6679                }
6680            }
6681            if let Some(wait_result) = vm
6682                .kernel
6683                .waitpid_with_options(
6684                    EXECUTION_DRIVER_NAME,
6685                    parent_kernel_pid,
6686                    child_kernel_pid as i32,
6687                    WaitPidFlags::WNOHANG,
6688                )
6689                .map_err(kernel_error)?
6690            {
6691                return Ok(Some(ActiveExecutionEvent::Exited(wait_result.status)));
6692            }
6693
6694            if !child_shared_runtime && child_runtime_pid != 0 {
6695                if let Some(status) = runtime_child_exit_status(child_runtime_pid)? {
6696                    return Ok(Some(ActiveExecutionEvent::Exited(status)));
6697                }
6698                if !runtime_child_is_alive(child_runtime_pid)? {
6699                    return Ok(Some(ActiveExecutionEvent::Exited(0)));
6700                }
6701            }
6702            if Instant::now() >= wait_deadline {
6703                return Ok(None);
6704            }
6705            std::thread::sleep(Duration::from_millis(5));
6706        }
6707    }
6708
6709    fn write_descendant_javascript_child_process_stdin(
6710        &mut self,
6711        vm_id: &str,
6712        process_id: &str,
6713        current_process_path: &[&str],
6714        child_process_id: &str,
6715        chunk: &[u8],
6716    ) -> Result<(), SidecarError> {
6717        let mut child_path = current_process_path.to_vec();
6718        child_path.push(child_process_id);
6719        let Some(vm) = self.vms.get_mut(vm_id) else {
6720            return Err(javascript_child_process_gone_error(process_id, &child_path));
6721        };
6722        let Some(root) = vm.active_processes.get_mut(process_id) else {
6723            return Err(javascript_child_process_gone_error(process_id, &child_path));
6724        };
6725        let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
6726            return Err(javascript_child_process_gone_error(process_id, &child_path));
6727        };
6728        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6729            return Err(javascript_child_process_gone_error(process_id, &child_path));
6730        };
6731        if let Err(error) = child.execution.write_stdin(chunk) {
6732            if is_broken_pipe_error(&error) {
6733                return Ok(());
6734            }
6735            return Err(error);
6736        }
6737        write_kernel_process_stdin(&mut vm.kernel, child, chunk)
6738    }
6739
6740    fn close_descendant_javascript_child_process_stdin(
6741        &mut self,
6742        vm_id: &str,
6743        process_id: &str,
6744        current_process_path: &[&str],
6745        child_process_id: &str,
6746    ) -> Result<(), SidecarError> {
6747        let mut child_path = current_process_path.to_vec();
6748        child_path.push(child_process_id);
6749        let Some(vm) = self.vms.get_mut(vm_id) else {
6750            return Err(javascript_child_process_gone_error(process_id, &child_path));
6751        };
6752        let Some(root) = vm.active_processes.get_mut(process_id) else {
6753            return Err(javascript_child_process_gone_error(process_id, &child_path));
6754        };
6755        let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
6756            return Err(javascript_child_process_gone_error(process_id, &child_path));
6757        };
6758        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6759            return Err(javascript_child_process_gone_error(process_id, &child_path));
6760        };
6761        child.execution.close_stdin()?;
6762        close_kernel_process_stdin(&mut vm.kernel, child)
6763    }
6764
6765    fn kill_descendant_javascript_child_process(
6766        &mut self,
6767        vm_id: &str,
6768        process_id: &str,
6769        current_process_path: &[&str],
6770        child_process_id: &str,
6771        signal: &str,
6772    ) -> Result<(), SidecarError> {
6773        let signal_name = signal.to_owned();
6774        let signal = parse_signal(signal)?;
6775        let Some(vm) = self.vms.get_mut(vm_id) else {
6776            return Ok(());
6777        };
6778        let Some(root) = vm.active_processes.get_mut(process_id) else {
6779            return Ok(());
6780        };
6781        let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
6782            return Ok(());
6783        };
6784        let source_pid = parent.kernel_pid;
6785        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6786            return Ok(());
6787        };
6788        terminate_tracked_child_process_for_signal(&mut vm.kernel, child, signal)?;
6789        let child_process_label = if current_process_path.is_empty() {
6790            child_process_id.to_owned()
6791        } else {
6792            format!("{}/{}", current_process_path.join("/"), child_process_id)
6793        };
6794        emit_security_audit_event(
6795            &self.bridge,
6796            vm_id,
6797            "security.process.kill",
6798            audit_fields([
6799                (String::from("source"), String::from("guest_child_process")),
6800                (String::from("source_pid"), source_pid.to_string()),
6801                (String::from("target_pid"), child.kernel_pid.to_string()),
6802                (String::from("process_id"), process_id.to_owned()),
6803                (String::from("child_process_id"), child_process_label),
6804                (String::from("signal"), signal_name),
6805            ]),
6806        );
6807        Ok(())
6808    }
6809
6810    fn handle_descendant_process_kill_rpc(
6811        &mut self,
6812        vm_id: &str,
6813        process_id: &str,
6814        current_process_path: &[&str],
6815        child_process_id: &str,
6816        request: &JavascriptSyncRpcRequest,
6817    ) -> Result<Value, SidecarError> {
6818        let target_pid = javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?;
6819        let signal_name = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?;
6820        let signal = parse_signal(signal_name)?;
6821
6822        let mut source_path = current_process_path.to_vec();
6823        source_path.push(child_process_id);
6824
6825        if signal != 0 && target_pid < 0 {
6826            let pgid = target_pid.unsigned_abs();
6827            let caller_kernel_pid = {
6828                let Some(vm) = self.vms.get(vm_id) else {
6829                    return Err(SidecarError::InvalidState(String::from(
6830                        "ESRCH: unknown VM during process.kill",
6831                    )));
6832                };
6833                let Some(root) = vm.active_processes.get(process_id) else {
6834                    return Err(SidecarError::InvalidState(format!(
6835                        "ESRCH: unknown process {process_id} during process.kill",
6836                    )));
6837                };
6838                let Some(source) = Self::active_process_by_path(root, &source_path) else {
6839                    return Err(SidecarError::InvalidState(format!(
6840                        "ESRCH: unknown child process {child_process_id} during process.kill",
6841                    )));
6842                };
6843                source.kernel_pid
6844            };
6845            let caller_is_member =
6846                self.signal_vm_process_group(vm_id, caller_kernel_pid, pgid, signal_name)?;
6847            if !caller_is_member {
6848                return Ok(Value::Null);
6849            }
6850            let Some(vm) = self.vms.get_mut(vm_id) else {
6851                return Ok(Value::Null);
6852            };
6853            let Some(root) = vm.active_processes.get_mut(process_id) else {
6854                return Ok(Value::Null);
6855            };
6856            let Some(source) = Self::active_process_by_path_mut(root, &source_path) else {
6857                return Ok(Value::Null);
6858            };
6859            source.pending_self_signal_exit = None;
6860            if !matches!(
6861                canonical_signal_name(signal),
6862                Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
6863            ) {
6864                source.pending_self_signal_exit = Some(signal);
6865            }
6866            return Ok(json!({
6867                "self": true,
6868                "action": "default",
6869            }));
6870        }
6871
6872        let Some(vm) = self.vms.get_mut(vm_id) else {
6873            return Err(SidecarError::InvalidState(String::from(
6874                "ESRCH: unknown VM during process.kill",
6875            )));
6876        };
6877
6878        if signal == 0 {
6879            vm.kernel
6880                .signal_process(EXECUTION_DRIVER_NAME, target_pid, signal)
6881                .map_err(kernel_error)?;
6882            return Ok(Value::Null);
6883        }
6884
6885        let target_kernel_pid = u32::try_from(target_pid).map_err(|_| {
6886            SidecarError::InvalidState(format!("EINVAL: invalid process pid {target_pid}"))
6887        })?;
6888        let (source_pid, located_target_path) = {
6889            let Some(root) = vm.active_processes.get(process_id) else {
6890                return Err(SidecarError::InvalidState(format!(
6891                    "ESRCH: unknown process {process_id} during process.kill",
6892                )));
6893            };
6894            let Some(source) = Self::active_process_by_path(root, &source_path) else {
6895                return Err(SidecarError::InvalidState(format!(
6896                    "ESRCH: unknown child process {child_process_id} during process.kill",
6897                )));
6898            };
6899            vm.kernel
6900                .signal_process(EXECUTION_DRIVER_NAME, target_pid, 0)
6901                .map_err(kernel_error)?;
6902            (
6903                source.kernel_pid,
6904                Self::active_process_path_by_kernel_pid(root, target_kernel_pid),
6905            )
6906        };
6907        let Some(target_path) = located_target_path else {
6908            // The target is alive but not part of this root's process tree.
6909            // Resolve it VM-wide so cross-tree pids and untracked kernel
6910            // processes still receive the signal.
6911            self.signal_vm_kernel_pid(vm_id, target_kernel_pid, signal_name)?;
6912            return Ok(Value::Null);
6913        };
6914        let Some(vm) = self.vms.get_mut(vm_id) else {
6915            return Err(SidecarError::InvalidState(String::from(
6916                "ESRCH: unknown VM during process.kill",
6917            )));
6918        };
6919
6920        if source_pid == target_kernel_pid {
6921            let Some(root) = vm.active_processes.get_mut(process_id) else {
6922                return Ok(Value::Null);
6923            };
6924            let Some(source) = Self::active_process_by_path_mut(root, &source_path) else {
6925                return Ok(Value::Null);
6926            };
6927            source.pending_self_signal_exit = None;
6928            if !matches!(
6929                canonical_signal_name(signal),
6930                Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
6931            ) {
6932                source.pending_self_signal_exit = Some(signal);
6933            }
6934            return Ok(json!({
6935                "self": true,
6936                "action": "default",
6937            }));
6938        }
6939
6940        let signal_key = target_path.last().map(String::as_str).unwrap_or(process_id);
6941        let registration = vm
6942            .signal_states
6943            .get(signal_key)
6944            .and_then(|handlers| handlers.get(&(signal as u32)))
6945            .cloned();
6946
6947        let action = match registration
6948            .as_ref()
6949            .map(|registration| &registration.action)
6950        {
6951            Some(SignalDispositionAction::Ignore) => "ignore",
6952            Some(SignalDispositionAction::User) => {
6953                let Some(root) = vm.active_processes.get_mut(process_id) else {
6954                    return Ok(Value::Null);
6955                };
6956                let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path)
6957                else {
6958                    return Err(SidecarError::InvalidState(format!(
6959                        "ESRCH: unknown process pid {target_pid}"
6960                    )));
6961                };
6962                if let Some(session) = target.execution.javascript_v8_session_handle().filter(
6963                    |_| matches!(&target.execution, ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime())
6964                        || matches!(&target.execution, ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime()),
6965                ) {
6966                    dispatch_v8_session_signal_async(session, signal);
6967                } else if !dispatch_v8_process_signal(target, signal)? {
6968                    return Err(SidecarError::InvalidState(format!(
6969                        "unsupported guest signal delivery for pid {target_pid}"
6970                    )));
6971                }
6972                "user"
6973            }
6974            Some(SignalDispositionAction::Default) | None
6975                if matches!(
6976                    canonical_signal_name(signal),
6977                    Some("SIGWINCH" | "SIGCHLD" | "SIGURG")
6978                ) =>
6979            {
6980                "ignore"
6981            }
6982            Some(SignalDispositionAction::Default) | None => {
6983                let Some(root) = vm.active_processes.get_mut(process_id) else {
6984                    return Ok(Value::Null);
6985                };
6986                let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path)
6987                else {
6988                    return Err(SidecarError::InvalidState(format!(
6989                        "ESRCH: unknown process pid {target_pid}"
6990                    )));
6991                };
6992                apply_active_process_default_signal(&mut vm.kernel, target, signal)?;
6993                "default"
6994            }
6995        };
6996
6997        let target_path_label = Self::child_process_path_label(
6998            process_id,
6999            &target_path.iter().map(String::as_str).collect::<Vec<_>>(),
7000        );
7001        emit_security_audit_event(
7002            &self.bridge,
7003            vm_id,
7004            "security.process.kill",
7005            audit_fields([
7006                (String::from("source"), String::from("guest_process")),
7007                (String::from("source_pid"), source_pid.to_string()),
7008                (String::from("target_pid"), target_pid.to_string()),
7009                (String::from("process_id"), process_id.to_owned()),
7010                (
7011                    String::from("target_process_path"),
7012                    target_path_label.clone(),
7013                ),
7014                (String::from("signal"), signal_name.to_owned()),
7015            ]),
7016        );
7017
7018        Ok(json!({
7019            "self": false,
7020            "action": action,
7021            "signal": signal_name,
7022            "number": signal,
7023            "targetProcessPath": target_path_label,
7024        }))
7025    }
7026
7027    pub(crate) fn poll_javascript_child_process(
7028        &mut self,
7029        vm_id: &str,
7030        process_id: &str,
7031        child_process_id: &str,
7032        wait_ms: u64,
7033    ) -> Result<Value, SidecarError> {
7034        self.poll_descendant_javascript_child_process(
7035            vm_id,
7036            process_id,
7037            &[],
7038            child_process_id,
7039            wait_ms,
7040        )
7041    }
7042
7043    pub(crate) fn write_javascript_child_process_stdin(
7044        &mut self,
7045        vm_id: &str,
7046        process_id: &str,
7047        child_process_id: &str,
7048        chunk: &[u8],
7049    ) -> Result<(), SidecarError> {
7050        let Some(vm) = self.vms.get_mut(vm_id) else {
7051            return Err(javascript_child_process_gone_error(
7052                process_id,
7053                &[child_process_id],
7054            ));
7055        };
7056        let Some(child) = vm
7057            .active_processes
7058            .get_mut(process_id)
7059            .ok_or_else(|| missing_process_error(vm_id, process_id))?
7060            .child_processes
7061            .get_mut(child_process_id)
7062        else {
7063            return Err(javascript_child_process_gone_error(
7064                process_id,
7065                &[child_process_id],
7066            ));
7067        };
7068        if let Err(error) = child.execution.write_stdin(chunk) {
7069            if is_broken_pipe_error(&error) {
7070                return Ok(());
7071            }
7072            return Err(error);
7073        }
7074        write_kernel_process_stdin(&mut vm.kernel, child, chunk)
7075    }
7076
7077    pub(crate) fn close_javascript_child_process_stdin(
7078        &mut self,
7079        vm_id: &str,
7080        process_id: &str,
7081        child_process_id: &str,
7082    ) -> Result<(), SidecarError> {
7083        let Some(vm) = self.vms.get_mut(vm_id) else {
7084            return Err(javascript_child_process_gone_error(
7085                process_id,
7086                &[child_process_id],
7087            ));
7088        };
7089        let Some(child) = vm
7090            .active_processes
7091            .get_mut(process_id)
7092            .ok_or_else(|| missing_process_error(vm_id, process_id))?
7093            .child_processes
7094            .get_mut(child_process_id)
7095        else {
7096            return Err(javascript_child_process_gone_error(
7097                process_id,
7098                &[child_process_id],
7099            ));
7100        };
7101        child.execution.close_stdin()?;
7102        close_kernel_process_stdin(&mut vm.kernel, child)
7103    }
7104
7105    pub(crate) fn kill_javascript_child_process(
7106        &mut self,
7107        vm_id: &str,
7108        process_id: &str,
7109        child_process_id: &str,
7110        signal: &str,
7111    ) -> Result<(), SidecarError> {
7112        let signal_name = signal.to_owned();
7113        let signal = parse_signal(signal)?;
7114        let Some(vm) = self.vms.get_mut(vm_id) else {
7115            return Ok(());
7116        };
7117        let process = vm
7118            .active_processes
7119            .get_mut(process_id)
7120            .ok_or_else(|| missing_process_error(vm_id, process_id))?;
7121        let source_pid = process.kernel_pid;
7122        let child = process
7123            .child_processes
7124            .get_mut(child_process_id)
7125            .ok_or_else(|| {
7126                SidecarError::InvalidState(format!(
7127                    "unknown child process {child_process_id} during kill"
7128                ))
7129            })?;
7130        terminate_tracked_child_process_for_signal(&mut vm.kernel, child, signal)?;
7131        emit_security_audit_event(
7132            &self.bridge,
7133            vm_id,
7134            "security.process.kill",
7135            audit_fields([
7136                (String::from("source"), String::from("guest_child_process")),
7137                (String::from("source_pid"), source_pid.to_string()),
7138                (String::from("target_pid"), child.kernel_pid.to_string()),
7139                (String::from("process_id"), process_id.to_owned()),
7140                (
7141                    String::from("child_process_id"),
7142                    child_process_id.to_owned(),
7143                ),
7144                (String::from("signal"), signal_name),
7145            ]),
7146        );
7147        Ok(())
7148    }
7149
7150    /// Delivers a signal to one kernel pid inside a VM, resolving the target
7151    /// through the active-process tree first so tracked sidecar executions get
7152    /// the same termination handling as a direct `child_process.kill`.
7153    /// Untracked kernel processes (for example WASM subprocess trees) receive
7154    /// the signal through the kernel process table directly.
7155    pub(crate) fn signal_vm_kernel_pid(
7156        &mut self,
7157        vm_id: &str,
7158        target_kernel_pid: u32,
7159        signal_name: &str,
7160    ) -> Result<(), SidecarError> {
7161        let signal = parse_signal(signal_name)?;
7162        let located = {
7163            let Some(vm) = self.vms.get(vm_id) else {
7164                return Err(SidecarError::InvalidState(String::from(
7165                    "ESRCH: unknown VM during process.kill",
7166                )));
7167            };
7168            let alive = vm
7169                .kernel
7170                .list_processes()
7171                .get(&target_kernel_pid)
7172                .is_some_and(|info| info.status != ProcessStatus::Exited);
7173            if !alive {
7174                return Err(SidecarError::InvalidState(format!(
7175                    "ESRCH: no such process {target_kernel_pid}"
7176                )));
7177            }
7178            vm.active_processes.iter().find_map(|(process_id, root)| {
7179                Self::active_process_path_by_kernel_pid(root, target_kernel_pid)
7180                    .map(|path| (process_id.clone(), path))
7181            })
7182        };
7183
7184        match located {
7185            Some((process_id, path)) if path.is_empty() => {
7186                self.kill_process_internal(vm_id, &process_id, signal_name)
7187            }
7188            Some((process_id, path)) => {
7189                let Some(vm) = self.vms.get_mut(vm_id) else {
7190                    return Ok(());
7191                };
7192                let Some(root) = vm.active_processes.get_mut(&process_id) else {
7193                    return Ok(());
7194                };
7195                let Some(target) = Self::active_process_by_owned_path_mut(root, &path) else {
7196                    return Err(SidecarError::InvalidState(format!(
7197                        "ESRCH: no such process {target_kernel_pid}"
7198                    )));
7199                };
7200                terminate_tracked_child_process_for_signal(&mut vm.kernel, target, signal)?;
7201                emit_security_audit_event(
7202                    &self.bridge,
7203                    vm_id,
7204                    "security.process.kill",
7205                    audit_fields([
7206                        (String::from("source"), String::from("guest_process")),
7207                        (String::from("target_pid"), target_kernel_pid.to_string()),
7208                        (String::from("process_id"), process_id),
7209                        (String::from("signal"), signal_name.to_owned()),
7210                    ]),
7211                );
7212                Ok(())
7213            }
7214            None => {
7215                let Some(vm) = self.vms.get_mut(vm_id) else {
7216                    return Ok(());
7217                };
7218                let target_pid = i32::try_from(target_kernel_pid).map_err(|_| {
7219                    SidecarError::InvalidState(format!(
7220                        "EINVAL: invalid process pid {target_kernel_pid}"
7221                    ))
7222                })?;
7223                vm.kernel
7224                    .signal_process(EXECUTION_DRIVER_NAME, target_pid, signal)
7225                    .map_err(kernel_error)?;
7226                emit_security_audit_event(
7227                    &self.bridge,
7228                    vm_id,
7229                    "security.process.kill",
7230                    audit_fields([
7231                        (String::from("source"), String::from("guest_process")),
7232                        (String::from("target_pid"), target_kernel_pid.to_string()),
7233                        (String::from("signal"), signal_name.to_owned()),
7234                    ]),
7235                );
7236                Ok(())
7237            }
7238        }
7239    }
7240
7241    /// Delivers a signal to every live member of a VM process group, matching
7242    /// Linux `kill(-pgid, sig)` semantics. Returns whether the caller itself
7243    /// is a member of the group so entry points can apply self-signal
7244    /// delivery; the caller is intentionally skipped here.
7245    pub(crate) fn signal_vm_process_group(
7246        &mut self,
7247        vm_id: &str,
7248        caller_kernel_pid: u32,
7249        pgid: u32,
7250        signal_name: &str,
7251    ) -> Result<bool, SidecarError> {
7252        parse_signal(signal_name)?;
7253        let members = {
7254            let Some(vm) = self.vms.get(vm_id) else {
7255                return Err(SidecarError::InvalidState(String::from(
7256                    "ESRCH: unknown VM during process.kill",
7257                )));
7258            };
7259            vm.kernel
7260                .list_processes()
7261                .into_iter()
7262                .filter(|(_, info)| info.pgid == pgid && info.status != ProcessStatus::Exited)
7263                .map(|(pid, _)| pid)
7264                .collect::<Vec<_>>()
7265        };
7266        if members.is_empty() {
7267            return Err(SidecarError::InvalidState(format!(
7268                "ESRCH: no such process group {pgid}"
7269            )));
7270        }
7271
7272        let mut caller_is_member = false;
7273        for member_pid in members {
7274            if member_pid == caller_kernel_pid {
7275                caller_is_member = true;
7276                continue;
7277            }
7278            match self.signal_vm_kernel_pid(vm_id, member_pid, signal_name) {
7279                Ok(()) => {}
7280                // Group members can exit while the group is being signaled. A
7281                // vanished member is not an error for the group kill overall.
7282                Err(error) if sidecar_error_is_esrch(&error) => {}
7283                Err(error) => return Err(error),
7284            }
7285        }
7286        Ok(caller_is_member)
7287    }
7288}
7289
7290/// Applies a kill signal to a tracked child execution. Shared-runtime
7291/// executions for lethal signals are terminated directly with a synthetic
7292/// signal exit so child polls observe a prompt close; everything else routes
7293/// through the kernel process table.
7294fn terminate_tracked_child_process_for_signal(
7295    kernel: &mut SidecarKernel,
7296    child: &mut ActiveProcess,
7297    signal: i32,
7298) -> Result<(), SidecarError> {
7299    let should_terminate_shared_runtime = child.execution.uses_shared_v8_runtime()
7300        && signal != 0
7301        && !matches!(
7302            signal,
7303            libc::SIGHUP
7304                | libc::SIGINT
7305                | libc::SIGTERM
7306                | libc::SIGCHLD
7307                | libc::SIGWINCH
7308                | libc::SIGSTOP
7309                | libc::SIGCONT
7310        );
7311    if should_terminate_shared_runtime {
7312        child.execution.terminate()?;
7313        child.pending_self_signal_exit = Some(signal);
7314        child.queue_pending_execution_event(ActiveExecutionEvent::Exited(128 + signal))?;
7315    } else {
7316        kernel
7317            .kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, signal)
7318            .map_err(kernel_error)?;
7319    }
7320    Ok(())
7321}
7322
7323fn sidecar_error_is_esrch(error: &SidecarError) -> bool {
7324    error.to_string().contains("ESRCH")
7325}
7326
7327fn apply_active_process_default_signal(
7328    kernel: &mut SidecarKernel,
7329    process: &mut ActiveProcess,
7330    signal: i32,
7331) -> Result<(), SidecarError> {
7332    if matches!(signal, libc::SIGSTOP | libc::SIGCONT) {
7333        return kernel
7334            .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal)
7335            .map_err(kernel_error);
7336    }
7337
7338    if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
7339        close_kernel_process_stdin(kernel, process)?;
7340    }
7341
7342    if process.execution.uses_shared_v8_runtime() {
7343        process.execution.terminate()?;
7344        if signal != 0 && matches!(process.execution, ActiveExecution::Wasm(_)) {
7345            process.queue_pending_execution_event(ActiveExecutionEvent::Exited(128 + signal))?;
7346        }
7347        return Ok(());
7348    }
7349
7350    kernel
7351        .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal)
7352        .map_err(kernel_error)
7353}
7354
7355fn map_wasm_signal_registration(
7356    registration: secure_exec_execution::wasm::WasmSignalHandlerRegistration,
7357) -> SignalHandlerRegistration {
7358    SignalHandlerRegistration {
7359        action: match registration.action {
7360            secure_exec_execution::wasm::WasmSignalDispositionAction::Default => {
7361                crate::protocol::SignalDispositionAction::Default
7362            }
7363            secure_exec_execution::wasm::WasmSignalDispositionAction::Ignore => {
7364                crate::protocol::SignalDispositionAction::Ignore
7365            }
7366            secure_exec_execution::wasm::WasmSignalDispositionAction::User => {
7367                crate::protocol::SignalDispositionAction::User
7368            }
7369        },
7370        mask: registration.mask,
7371        flags: registration.flags,
7372    }
7373}
7374
7375fn parse_process_signal_state_request(
7376    args: &[Value],
7377) -> Result<(u32, SignalHandlerRegistration), SidecarError> {
7378    let signal = javascript_sync_rpc_arg_u32(args, 0, "process.signal_state signal")?;
7379    let action = javascript_sync_rpc_arg_str(args, 1, "process.signal_state action")?;
7380    let mask_json = javascript_sync_rpc_arg_str(args, 2, "process.signal_state mask")?;
7381    let flags = javascript_sync_rpc_arg_u32(args, 3, "process.signal_state flags")?;
7382    let mask: Vec<u32> = serde_json::from_str(mask_json).map_err(|error| {
7383        SidecarError::InvalidState(format!(
7384            "process.signal_state mask must be valid JSON: {error}"
7385        ))
7386    })?;
7387    let action = match action.trim().to_ascii_lowercase().as_str() {
7388        "default" => SignalDispositionAction::Default,
7389        "ignore" => SignalDispositionAction::Ignore,
7390        "user" => SignalDispositionAction::User,
7391        other => {
7392            return Err(SidecarError::InvalidState(format!(
7393                "unsupported process.signal_state action {other}"
7394            )));
7395        }
7396    };
7397
7398    Ok((
7399        signal,
7400        SignalHandlerRegistration {
7401            action,
7402            mask,
7403            flags,
7404        },
7405    ))
7406}
7407
7408fn apply_process_signal_state_update(
7409    signal_states: &mut BTreeMap<String, BTreeMap<u32, SignalHandlerRegistration>>,
7410    process_id: &str,
7411    signal: u32,
7412    registration: SignalHandlerRegistration,
7413) {
7414    if registration.action == SignalDispositionAction::Default
7415        && registration.mask.is_empty()
7416        && registration.flags == 0
7417    {
7418        let remove_process_entry = signal_states
7419            .get_mut(process_id)
7420            .map(|handlers| {
7421                handlers.remove(&signal);
7422                handlers.is_empty()
7423            })
7424            .unwrap_or(false);
7425        if remove_process_entry {
7426            signal_states.remove(process_id);
7427        }
7428        return;
7429    }
7430
7431    signal_states
7432        .entry(process_id.to_owned())
7433        .or_default()
7434        .insert(signal, registration);
7435}
7436
7437fn map_node_signal_registration(
7438    registration: NodeSignalHandlerRegistration,
7439) -> SignalHandlerRegistration {
7440    SignalHandlerRegistration {
7441        action: match registration.action {
7442            NodeSignalDispositionAction::Default => SignalDispositionAction::Default,
7443            NodeSignalDispositionAction::Ignore => SignalDispositionAction::Ignore,
7444            NodeSignalDispositionAction::User => SignalDispositionAction::User,
7445        },
7446        mask: registration.mask,
7447        flags: registration.flags,
7448    }
7449}
7450
7451fn javascript_child_process_sync_input_bytes(
7452    value: Option<&Value>,
7453) -> Result<Option<Vec<u8>>, SidecarError> {
7454    let Some(value) = value else {
7455        return Ok(None);
7456    };
7457
7458    match value {
7459        Value::Null => Ok(None),
7460        Value::String(text) => Ok(Some(text.as_bytes().to_vec())),
7461        other => javascript_sync_rpc_bytes_arg(
7462            std::slice::from_ref(other),
7463            0,
7464            "child_process.spawn_sync input",
7465        )
7466        .map(Some),
7467    }
7468}
7469
7470// bridge_permissions moved to crate::bridge
7471
7472// reconcile_mounts, resolve_cwd moved to crate::vm
7473
7474fn resolve_execute_request(
7475    vm: &VmState,
7476    payload: &ExecuteRequest,
7477) -> Result<ResolvedChildProcessExecution, SidecarError> {
7478    let payload_env: BTreeMap<String, String> = payload
7479        .env
7480        .iter()
7481        .map(|(k, v)| (k.clone(), v.clone()))
7482        .collect();
7483    if let Some(command) = payload.command.as_deref() {
7484        return resolve_command_execution(
7485            vm,
7486            command,
7487            &payload.args,
7488            &payload_env,
7489            payload.cwd.as_deref(),
7490            payload.wasm_permission_tier,
7491        );
7492    }
7493
7494    let runtime = payload.runtime.clone().ok_or_else(|| {
7495        SidecarError::InvalidState(String::from("execute requires either command or runtime"))
7496    })?;
7497    let entrypoint = payload.entrypoint.clone().ok_or_else(|| {
7498        SidecarError::InvalidState(String::from(
7499            "execute requires either command or entrypoint",
7500        ))
7501    })?;
7502    let (guest_cwd, host_cwd, allow_host_path_overrides) =
7503        resolve_execution_cwds(vm, payload.cwd.as_deref());
7504    let mut env = vm.guest_env.clone();
7505    env.extend(payload_env.clone());
7506
7507    let requested_host_entrypoint = resolve_host_entrypoint_within_vm_host_cwd(vm, &entrypoint);
7508    if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
7509        let requested_cwd = payload.cwd.as_deref().unwrap_or(guest_cwd.as_str());
7510        return Err(SidecarError::InvalidState(format!(
7511            "execution cwd {requested_cwd} is outside sandbox root {}",
7512            vm.host_cwd.to_string_lossy()
7513        )));
7514    }
7515    let host_entrypoint_override = allow_host_path_overrides
7516        .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, &entrypoint))
7517        .flatten();
7518
7519    let guest_entrypoint = host_entrypoint_override
7520        .as_ref()
7521        .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
7522        .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, &entrypoint));
7523    prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
7524
7525    Ok(ResolvedChildProcessExecution {
7526        command: match runtime {
7527            GuestRuntimeKind::JavaScript => String::from(JAVASCRIPT_COMMAND),
7528            GuestRuntimeKind::Python => String::from(PYTHON_COMMAND),
7529            GuestRuntimeKind::WebAssembly => String::from(WASM_COMMAND),
7530        },
7531        process_args: std::iter::once(entrypoint.clone())
7532            .chain(payload.args.iter().cloned())
7533            .collect(),
7534        runtime,
7535        entrypoint: host_entrypoint_override
7536            .map(|(_, host_entrypoint)| host_entrypoint)
7537            .unwrap_or(entrypoint),
7538        execution_args: payload.args.clone(),
7539        env,
7540        guest_cwd,
7541        host_cwd,
7542        wasm_permission_tier: payload.wasm_permission_tier,
7543        tool_command: false,
7544    })
7545}
7546
7547fn resolve_command_execution(
7548    vm: &VmState,
7549    command: &str,
7550    args: &[String],
7551    extra_env: &BTreeMap<String, String>,
7552    cwd: Option<&str>,
7553    explicit_wasm_permission_tier: Option<WasmPermissionTier>,
7554) -> Result<ResolvedChildProcessExecution, SidecarError> {
7555    let (guest_cwd, host_cwd, allow_host_path_overrides) = resolve_execution_cwds(vm, cwd);
7556    let mut env = vm.guest_env.clone();
7557    env.extend(extra_env.clone());
7558    let args = apply_shell_cwd_prefix(command, args.to_vec(), &guest_cwd);
7559
7560    if is_tool_command(vm, command) {
7561        let command = normalized_tool_command_name(command).unwrap_or_else(|| command.to_owned());
7562        return Ok(ResolvedChildProcessExecution {
7563            command: command.clone(),
7564            process_args: std::iter::once(command.clone())
7565                .chain(args.iter().cloned())
7566                .collect(),
7567            runtime: GuestRuntimeKind::JavaScript,
7568            entrypoint: command,
7569            execution_args: args,
7570            env,
7571            guest_cwd,
7572            host_cwd,
7573            wasm_permission_tier: None,
7574            tool_command: true,
7575        });
7576    }
7577
7578    if is_node_runtime_command(command) {
7579        if let Some(cli) = resolve_host_node_cli_entrypoint(command) {
7580            env.insert(
7581                String::from("AGENTOS_NODE_EVAL"),
7582                build_host_node_cli_eval(&cli),
7583            );
7584            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
7585            add_runtime_guest_path_mapping(&mut env, &cli.guest_root, &cli.package_root);
7586            add_runtime_host_access_path(
7587                &mut env,
7588                "AGENTOS_EXTRA_FS_READ_PATHS",
7589                &cli.package_root,
7590                true,
7591            );
7592
7593            return Ok(ResolvedChildProcessExecution {
7594                command: String::from(JAVASCRIPT_COMMAND),
7595                process_args: std::iter::once(command.to_owned())
7596                    .chain(args.iter().cloned())
7597                    .collect(),
7598                runtime: GuestRuntimeKind::JavaScript,
7599                entrypoint: String::from("-e"),
7600                execution_args: std::iter::once(cli.guest_entrypoint.clone())
7601                    .chain(args.iter().cloned())
7602                    .collect(),
7603                env,
7604                guest_cwd,
7605                host_cwd,
7606                wasm_permission_tier: None,
7607                tool_command: false,
7608            });
7609        }
7610
7611        if args.is_empty() {
7612            env.insert(String::from("AGENTOS_NODE_EVAL"), String::new());
7613            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
7614
7615            return Ok(ResolvedChildProcessExecution {
7616                command: String::from(JAVASCRIPT_COMMAND),
7617                process_args: vec![command.to_owned()],
7618                runtime: GuestRuntimeKind::JavaScript,
7619                entrypoint: String::from("-e"),
7620                execution_args: Vec::new(),
7621                env,
7622                guest_cwd,
7623                host_cwd,
7624                wasm_permission_tier: None,
7625                tool_command: false,
7626            });
7627        }
7628
7629        if let Some((entrypoint, execution_args)) =
7630            resolve_special_node_cli_invocation(&args, &mut env)
7631        {
7632            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
7633
7634            return Ok(ResolvedChildProcessExecution {
7635                command: String::from(JAVASCRIPT_COMMAND),
7636                process_args: std::iter::once(command.to_owned())
7637                    .chain(args.iter().cloned())
7638                    .collect(),
7639                runtime: GuestRuntimeKind::JavaScript,
7640                entrypoint,
7641                execution_args,
7642                env,
7643                guest_cwd,
7644                host_cwd,
7645                wasm_permission_tier: None,
7646                tool_command: false,
7647            });
7648        }
7649
7650        let Some(entrypoint_specifier) = args.first() else {
7651            return Err(SidecarError::InvalidState(format!(
7652                "{command} execution requires an entrypoint"
7653            )));
7654        };
7655
7656        let (entrypoint, execution_args, guest_entrypoint) = {
7657            let requested_host_entrypoint =
7658                resolve_host_entrypoint_within_vm_host_cwd(vm, entrypoint_specifier);
7659            if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
7660                let requested_cwd = cwd.unwrap_or(guest_cwd.as_str());
7661                return Err(SidecarError::InvalidState(format!(
7662                    "execution cwd {requested_cwd} is outside sandbox root {}",
7663                    vm.host_cwd.to_string_lossy()
7664                )));
7665            }
7666            let host_entrypoint_override = allow_host_path_overrides
7667                .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, entrypoint_specifier))
7668                .flatten();
7669            let guest_entrypoint = host_entrypoint_override
7670                .as_ref()
7671                .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
7672                .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, entrypoint_specifier));
7673            let entrypoint = host_entrypoint_override.map_or_else(
7674                || {
7675                    guest_entrypoint.as_ref().map_or_else(
7676                        || entrypoint_specifier.clone(),
7677                        |guest_entrypoint| {
7678                            resolve_vm_guest_path_to_host(vm, guest_entrypoint)
7679                                .to_string_lossy()
7680                                .into_owned()
7681                        },
7682                    )
7683                },
7684                |(_, host_entrypoint)| host_entrypoint,
7685            );
7686            (
7687                entrypoint,
7688                args.iter().skip(1).cloned().collect(),
7689                guest_entrypoint,
7690            )
7691        };
7692
7693        prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
7694
7695        return Ok(ResolvedChildProcessExecution {
7696            command: String::from(JAVASCRIPT_COMMAND),
7697            process_args: std::iter::once(command.to_owned())
7698                .chain(args.iter().cloned())
7699                .collect(),
7700            runtime: GuestRuntimeKind::JavaScript,
7701            entrypoint,
7702            execution_args,
7703            env,
7704            guest_cwd,
7705            host_cwd,
7706            wasm_permission_tier: None,
7707            tool_command: false,
7708        });
7709    }
7710
7711    if command.ends_with(".js") || command.ends_with(".mjs") || command.ends_with(".cjs") {
7712        let requested_host_entrypoint = resolve_host_entrypoint_within_vm_host_cwd(vm, command);
7713        if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
7714            let requested_cwd = cwd.unwrap_or(guest_cwd.as_str());
7715            return Err(SidecarError::InvalidState(format!(
7716                "execution cwd {requested_cwd} is outside sandbox root {}",
7717                vm.host_cwd.to_string_lossy()
7718            )));
7719        }
7720        let host_entrypoint_override = allow_host_path_overrides
7721            .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, command))
7722            .flatten();
7723        let guest_entrypoint = host_entrypoint_override
7724            .as_ref()
7725            .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
7726            .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, command));
7727        let entrypoint = host_entrypoint_override.map_or_else(
7728            || {
7729                guest_entrypoint.as_ref().map_or_else(
7730                    || command.to_owned(),
7731                    |guest_entrypoint| {
7732                        resolve_vm_guest_path_to_host(vm, guest_entrypoint)
7733                            .to_string_lossy()
7734                            .into_owned()
7735                    },
7736                )
7737            },
7738            |(_, host_entrypoint)| host_entrypoint,
7739        );
7740        prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
7741
7742        return Ok(ResolvedChildProcessExecution {
7743            command: String::from(JAVASCRIPT_COMMAND),
7744            process_args: std::iter::once(command.to_owned())
7745                .chain(args.iter().cloned())
7746                .collect(),
7747            runtime: GuestRuntimeKind::JavaScript,
7748            entrypoint,
7749            execution_args: args.to_vec(),
7750            env,
7751            guest_cwd,
7752            host_cwd,
7753            wasm_permission_tier: None,
7754            tool_command: false,
7755        });
7756    }
7757
7758    let guest_entrypoint = resolve_guest_command_entrypoint(
7759        vm,
7760        &guest_cwd,
7761        command,
7762        env.get("PATH").map(String::as_str),
7763    )
7764    .ok_or_else(|| {
7765        SidecarError::InvalidState(format!(
7766            "command not found on native sidecar path: {command}"
7767        ))
7768    })?;
7769    let wasm_permission_tier = explicit_wasm_permission_tier
7770        .or_else(|| vm.command_permissions.get(command).copied())
7771        .or_else(|| {
7772            Path::new(&guest_entrypoint)
7773                .file_name()
7774                .and_then(|name| name.to_str())
7775                .and_then(|name| vm.command_permissions.get(name).copied())
7776        });
7777
7778    let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint);
7779    if let Some((javascript_guest_entrypoint, javascript_host_entrypoint)) =
7780        resolve_javascript_command_entrypoint(vm, &guest_entrypoint, &host_entrypoint)
7781    {
7782        prepare_guest_runtime_env(
7783            vm,
7784            &mut env,
7785            &guest_cwd,
7786            &host_cwd,
7787            Some(javascript_guest_entrypoint),
7788        )?;
7789
7790        return Ok(ResolvedChildProcessExecution {
7791            command: command.to_owned(),
7792            process_args: std::iter::once(command.to_owned())
7793                .chain(args.iter().cloned())
7794                .collect(),
7795            runtime: GuestRuntimeKind::JavaScript,
7796            entrypoint: javascript_host_entrypoint.to_string_lossy().into_owned(),
7797            execution_args: args.to_vec(),
7798            env,
7799            guest_cwd,
7800            host_cwd,
7801            wasm_permission_tier: None,
7802            tool_command: false,
7803        });
7804    }
7805    prepare_guest_runtime_env(
7806        vm,
7807        &mut env,
7808        &guest_cwd,
7809        &host_cwd,
7810        Some(guest_entrypoint.clone()),
7811    )?;
7812
7813    Ok(ResolvedChildProcessExecution {
7814        command: command.to_owned(),
7815        process_args: std::iter::once(command.to_owned())
7816            .chain(args.iter().cloned())
7817            .collect(),
7818        runtime: GuestRuntimeKind::WebAssembly,
7819        entrypoint: host_entrypoint.to_string_lossy().into_owned(),
7820        execution_args: args.to_vec(),
7821        env,
7822        guest_cwd,
7823        host_cwd,
7824        wasm_permission_tier,
7825        tool_command: false,
7826    })
7827}
7828
7829const MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH: usize = 4;
7830
7831fn resolve_javascript_command_entrypoint(
7832    vm: &VmState,
7833    guest_entrypoint: &str,
7834    host_entrypoint: &Path,
7835) -> Option<(String, PathBuf)> {
7836    resolve_javascript_command_entrypoint_inner(
7837        vm,
7838        guest_entrypoint,
7839        host_entrypoint,
7840        MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH,
7841    )
7842}
7843
7844fn resolve_javascript_command_entrypoint_inner(
7845    vm: &VmState,
7846    guest_entrypoint: &str,
7847    host_entrypoint: &Path,
7848    redirects_remaining: usize,
7849) -> Option<(String, PathBuf)> {
7850    if redirects_remaining > 0 {
7851        let symlink_target = fs::symlink_metadata(host_entrypoint)
7852            .ok()
7853            .filter(|metadata| metadata.file_type().is_symlink())
7854            .and_then(|_| fs::read_link(host_entrypoint).ok());
7855        if let Some(symlink_target) = symlink_target {
7856            let guest_parent = Path::new(guest_entrypoint)
7857                .parent()
7858                .and_then(|path| path.to_str())
7859                .unwrap_or("/");
7860            let symlink_guest_entrypoint = if symlink_target.is_absolute() {
7861                normalize_path(&symlink_target.to_string_lossy())
7862            } else {
7863                normalize_path(&format!(
7864                    "{guest_parent}/{}",
7865                    symlink_target.to_string_lossy().replace('\\', "/")
7866                ))
7867            };
7868            let symlink_host_entrypoint =
7869                resolve_vm_guest_path_to_host(vm, &symlink_guest_entrypoint);
7870            return resolve_javascript_command_entrypoint_inner(
7871                vm,
7872                &symlink_guest_entrypoint,
7873                &symlink_host_entrypoint,
7874                redirects_remaining - 1,
7875            );
7876        }
7877    }
7878
7879    let script = load_executable_script_preview(host_entrypoint)?;
7880    let interpreter = parse_script_interpreter_name(&script);
7881
7882    if interpreter.is_none() && is_probable_javascript_entrypoint(host_entrypoint, &script) {
7883        return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf()));
7884    }
7885
7886    let interpreter = interpreter?;
7887    if interpreter == "node" {
7888        return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf()));
7889    }
7890
7891    if redirects_remaining == 0 || !matches!(interpreter.as_str(), "sh" | "bash" | "dash") {
7892        return None;
7893    }
7894
7895    let shim_target = parse_node_shell_shim_target(&script)?;
7896    let guest_parent = Path::new(guest_entrypoint)
7897        .parent()
7898        .and_then(|path| path.to_str())
7899        .unwrap_or("/");
7900    let shim_guest_entrypoint = normalize_path(&format!("{guest_parent}/{shim_target}"));
7901    let shim_host_entrypoint = resolve_vm_guest_path_to_host(vm, &shim_guest_entrypoint);
7902    resolve_javascript_command_entrypoint_inner(
7903        vm,
7904        &shim_guest_entrypoint,
7905        &shim_host_entrypoint,
7906        redirects_remaining - 1,
7907    )
7908}
7909
7910fn load_executable_script_preview(path: &Path) -> Option<String> {
7911    let bytes = fs::read(path).ok()?;
7912    let preview_len = bytes.len().min(16 * 1024);
7913    Some(String::from_utf8_lossy(&bytes[..preview_len]).into_owned())
7914}
7915
7916fn parse_script_interpreter_name(script: &str) -> Option<String> {
7917    let shebang = script.lines().next()?.strip_prefix("#!")?.trim();
7918    let mut tokens = shebang.split_whitespace();
7919    let command = tokens.next()?;
7920    let command_name = Path::new(command).file_name()?.to_str()?;
7921    if command_name == "env" {
7922        for token in tokens {
7923            if token.starts_with('-') {
7924                continue;
7925            }
7926            return Path::new(token)
7927                .file_name()
7928                .and_then(|name| name.to_str())
7929                .map(ToOwned::to_owned);
7930        }
7931        return None;
7932    }
7933
7934    Some(command_name.to_owned())
7935}
7936
7937fn parse_node_shell_shim_target(script: &str) -> Option<String> {
7938    for line in script.lines() {
7939        let trimmed = line.trim();
7940        if !trimmed.starts_with("exec ") {
7941            continue;
7942        }
7943
7944        let mut remaining = trimmed;
7945        while let Some(start) = remaining.find("\"$basedir/") {
7946            let after_prefix = &remaining[start + "\"$basedir/".len()..];
7947            let end = after_prefix.find('"')?;
7948            let candidate = &after_prefix[..end];
7949            remaining = &after_prefix[end + 1..];
7950
7951            if candidate.is_empty() || candidate == "node" || candidate.ends_with("/node") {
7952                continue;
7953            }
7954
7955            return Some(candidate.to_owned());
7956        }
7957    }
7958
7959    None
7960}
7961
7962fn is_probable_javascript_entrypoint(path: &Path, script: &str) -> bool {
7963    let extension = path
7964        .extension()
7965        .and_then(|value| value.to_str())
7966        .unwrap_or_default();
7967    if matches!(extension, "js" | "cjs" | "mjs") {
7968        return true;
7969    }
7970
7971    if !path
7972        .components()
7973        .any(|component| component.as_os_str() == "node_modules")
7974    {
7975        return false;
7976    }
7977
7978    let preview = script.trim_start_matches('\u{feff}').trim_start();
7979    !preview.is_empty()
7980        && !preview.starts_with("#!")
7981        && (preview.starts_with("\"use strict\"")
7982            || preview.starts_with("'use strict'")
7983            || preview.starts_with("import ")
7984            || preview.starts_with("export ")
7985            || preview.starts_with("const ")
7986            || preview.starts_with("let ")
7987            || preview.starts_with("var ")
7988            || preview.starts_with("Object.defineProperty(exports")
7989            || preview.starts_with("module.exports")
7990            || preview.starts_with("require("))
7991}
7992
7993fn resolve_guest_execution_cwd(vm: &VmState, value: Option<&str>) -> String {
7994    value
7995        .map(normalize_path)
7996        .unwrap_or_else(|| vm.guest_cwd.clone())
7997}
7998
7999fn resolve_execution_cwds(vm: &VmState, value: Option<&str>) -> (String, PathBuf, bool) {
8000    if let Some(raw_cwd) = value {
8001        let normalized_vm_host_cwd = normalize_host_path(&vm.host_cwd);
8002        let requested_host_cwd = normalize_host_path(Path::new(raw_cwd));
8003        if path_is_within_root(&requested_host_cwd, &normalized_vm_host_cwd) {
8004            let relative = requested_host_cwd
8005                .strip_prefix(&normalized_vm_host_cwd)
8006                .unwrap_or_else(|_| Path::new(""));
8007            let relative = relative.to_string_lossy().replace('\\', "/");
8008            let guest_cwd = if relative.is_empty() {
8009                String::from("/")
8010            } else {
8011                normalize_path(&format!("/{relative}"))
8012            };
8013            return (guest_cwd, requested_host_cwd, true);
8014        }
8015    }
8016
8017    let guest_cwd = resolve_guest_execution_cwd(vm, value);
8018    let host_cwd = if value.is_none() {
8019        vm.host_cwd.clone()
8020    } else {
8021        resolve_vm_guest_path_to_host(vm, &guest_cwd)
8022    };
8023    (guest_cwd, host_cwd, value.is_none())
8024}
8025
8026fn resolve_vm_guest_path_to_host(vm: &VmState, guest_path: &str) -> PathBuf {
8027    host_mount_path_for_guest_path(vm, guest_path)
8028        .unwrap_or_else(|| shadow_path_for_guest(vm, guest_path))
8029}
8030
8031fn shadow_path_for_guest(vm: &VmState, guest_path: &str) -> PathBuf {
8032    let normalized = normalize_path(guest_path);
8033    let relative = normalized.trim_start_matches('/');
8034    if relative.is_empty() {
8035        return vm.cwd.clone();
8036    }
8037    vm.cwd.join(relative)
8038}
8039
8040fn apply_shell_cwd_prefix(command: &str, mut args: Vec<String>, guest_cwd: &str) -> Vec<String> {
8041    if guest_cwd == "/" || !is_shell_command(command) {
8042        return args;
8043    }
8044
8045    let Some(flag) = args.first() else {
8046        return args;
8047    };
8048    if !matches!(flag.as_str(), "-c" | "-lc") || args.len() < 2 {
8049        return args;
8050    }
8051
8052    let command_text = args[1].clone();
8053    let quoted_cwd = shell_single_quote(guest_cwd);
8054    args[1] = format!("cd {quoted_cwd} && {command_text}");
8055    args
8056}
8057
8058fn is_shell_command(command: &str) -> bool {
8059    Path::new(command)
8060        .file_name()
8061        .and_then(|name| name.to_str())
8062        .unwrap_or(command)
8063        .trim_end_matches(".exe")
8064        .eq("sh")
8065        || Path::new(command)
8066            .file_name()
8067            .and_then(|name| name.to_str())
8068            .unwrap_or(command)
8069            .trim_end_matches(".exe")
8070            .eq("bash")
8071}
8072
8073fn shell_single_quote(value: &str) -> String {
8074    if value.is_empty() {
8075        return String::from("''");
8076    }
8077    format!("'{}'", value.replace('\'', "'\"'\"'"))
8078}
8079
8080pub(crate) fn sync_active_process_host_writes_to_kernel(
8081    vm: &mut VmState,
8082) -> Result<(), SidecarError> {
8083    if vm.root_filesystem_mode != RootFilesystemMode::ReadOnly {
8084        let shadow_root = vm.cwd.clone();
8085        sync_host_directory_tree_to_kernel(vm, &shadow_root, "/")?;
8086    }
8087
8088    let normalized_vm_root = normalize_host_path(&vm.cwd);
8089    let extra_roots = collect_active_process_host_sync_roots(vm, &normalized_vm_root);
8090    for (host_cwd, guest_cwd) in extra_roots {
8091        sync_host_directory_tree_to_kernel(vm, &host_cwd, &guest_cwd)?;
8092    }
8093
8094    Ok(())
8095}
8096
8097fn collect_active_process_host_sync_roots(
8098    vm: &VmState,
8099    normalized_vm_root: &Path,
8100) -> Vec<(PathBuf, String)> {
8101    let mut roots = Vec::new();
8102    let mut seen = BTreeSet::new();
8103
8104    for process in vm.active_processes.values() {
8105        collect_process_host_sync_roots(process, normalized_vm_root, &mut seen, &mut roots);
8106    }
8107
8108    roots
8109}
8110
8111fn collect_process_host_sync_roots(
8112    process: &ActiveProcess,
8113    normalized_vm_root: &Path,
8114    seen: &mut BTreeSet<(PathBuf, String)>,
8115    roots: &mut Vec<(PathBuf, String)>,
8116) {
8117    let normalized_host_cwd = normalize_host_path(&process.host_cwd);
8118    if !path_is_within_root(&normalized_host_cwd, normalized_vm_root) {
8119        let guest_cwd = normalize_path(&process.guest_cwd);
8120        if seen.insert((normalized_host_cwd.clone(), guest_cwd.clone())) {
8121            roots.push((normalized_host_cwd, guest_cwd));
8122        }
8123    }
8124
8125    for child in process.child_processes.values() {
8126        collect_process_host_sync_roots(child, normalized_vm_root, seen, roots);
8127    }
8128}
8129
8130fn sync_process_host_writes_to_kernel(
8131    vm: &mut VmState,
8132    process: &ActiveProcess,
8133) -> Result<(), SidecarError> {
8134    if vm.root_filesystem_mode != RootFilesystemMode::ReadOnly {
8135        let shadow_root = vm.cwd.clone();
8136        sync_host_directory_tree_to_kernel(vm, &shadow_root, "/")?;
8137    }
8138
8139    if !path_is_within_root(
8140        &normalize_host_path(&process.host_cwd),
8141        &normalize_host_path(&vm.cwd),
8142    ) {
8143        sync_host_directory_tree_to_kernel(vm, &process.host_cwd, &process.guest_cwd)?;
8144    }
8145
8146    Ok(())
8147}
8148
8149fn sync_host_directory_tree_to_kernel(
8150    vm: &mut VmState,
8151    host_root: &Path,
8152    guest_root: &str,
8153) -> Result<(), SidecarError> {
8154    let normalized_host_root = normalize_host_path(host_root);
8155    let normalized_guest_root = normalize_path(guest_root);
8156    let mut synced_file_times = BTreeMap::new();
8157    sync_host_directory_tree_to_kernel_inner(
8158        vm,
8159        &normalized_host_root,
8160        &normalized_host_root,
8161        &normalized_guest_root,
8162        &mut synced_file_times,
8163    )
8164}
8165
8166fn sync_host_directory_tree_to_kernel_inner(
8167    vm: &mut VmState,
8168    host_root: &Path,
8169    current_host_dir: &Path,
8170    guest_root: &str,
8171    synced_file_times: &mut BTreeMap<(u64, u64), (u64, u64)>,
8172) -> Result<(), SidecarError> {
8173    let entries = match fs::read_dir(current_host_dir) {
8174        Ok(entries) => entries,
8175        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
8176        Err(error) => {
8177            return Err(SidecarError::Io(format!(
8178                "failed to read host shadow directory {}: {error}",
8179                current_host_dir.display()
8180            )));
8181        }
8182    };
8183
8184    for entry in entries {
8185        let entry = entry.map_err(|error| {
8186            SidecarError::Io(format!(
8187                "failed to read host shadow entry in {}: {error}",
8188                current_host_dir.display()
8189            ))
8190        })?;
8191        let host_path = entry.path();
8192        let file_type = entry.file_type().map_err(|error| {
8193            SidecarError::Io(format!(
8194                "failed to stat host shadow entry {}: {error}",
8195                host_path.display()
8196            ))
8197        })?;
8198        let relative_path = host_path
8199            .strip_prefix(host_root)
8200            .map_err(|error| {
8201                SidecarError::InvalidState(format!(
8202                    "failed to relativize host shadow path {} against {}: {error}",
8203                    host_path.display(),
8204                    host_root.display()
8205                ))
8206            })?
8207            .to_string_lossy()
8208            .replace('\\', "/");
8209        let guest_path = if guest_root == "/" {
8210            normalize_path(&format!("/{relative_path}"))
8211        } else {
8212            normalize_path(&format!(
8213                "{}/{}",
8214                guest_root.trim_end_matches('/'),
8215                relative_path
8216            ))
8217        };
8218
8219        if should_skip_shadow_sync_path(vm, &guest_path) {
8220            continue;
8221        }
8222
8223        if file_type.is_dir() {
8224            let metadata = entry.metadata().map_err(|error| {
8225                SidecarError::Io(format!(
8226                    "failed to read host shadow metadata {}: {error}",
8227                    host_path.display()
8228                ))
8229            })?;
8230            if !is_shadow_bootstrap_dir(&guest_path)
8231                && !vm.kernel.exists(&guest_path).unwrap_or(false)
8232            {
8233                vm.kernel.mkdir(&guest_path, true).map_err(|error| {
8234                    SidecarError::InvalidState(format!(
8235                        "failed to sync host shadow directory {} to guest {}: {}",
8236                        host_path.display(),
8237                        guest_path,
8238                        kernel_error(error)
8239                    ))
8240                })?;
8241                vm.kernel
8242                    .chmod(&guest_path, host_shadow_mode(&metadata))
8243                    .map_err(|error| {
8244                        SidecarError::InvalidState(format!(
8245                            "failed to sync host shadow directory mode {} to guest {}: {}",
8246                            host_path.display(),
8247                            guest_path,
8248                            kernel_error(error)
8249                        ))
8250                    })?;
8251            }
8252            sync_host_directory_tree_to_kernel_inner(
8253                vm,
8254                host_root,
8255                &host_path,
8256                guest_root,
8257                synced_file_times,
8258            )?;
8259            continue;
8260        }
8261
8262        if file_type.is_file() {
8263            let metadata = entry.metadata().map_err(|error| {
8264                SidecarError::Io(format!(
8265                    "failed to read host shadow metadata {}: {error}",
8266                    host_path.display()
8267                ))
8268            })?;
8269            let timestamp_key = (metadata.dev(), metadata.ino());
8270            let (atime_ms, mtime_ms) =
8271                *synced_file_times.entry(timestamp_key).or_insert_with(|| {
8272                    (
8273                        metadata_time_ms(metadata.atime(), metadata.atime_nsec()),
8274                        metadata_time_ms(metadata.mtime(), metadata.mtime_nsec()),
8275                    )
8276                });
8277            let desired_mode = host_shadow_mode(&metadata);
8278            // Fast path: skip the expensive re-read + re-write when the kernel already
8279            // holds a copy of this shadow file that matches on size, mode, and mtime.
8280            //
8281            // Every read-side fs op (exists/stat/readFile/...) triggers a full
8282            // shadow-tree reconciliation walk. Without this skip the walk re-reads every
8283            // file's bytes from the host and re-writes them into the kernel VFS on every
8284            // op -- O(whole tree) per op, and super-linear as the VM's shadow grows,
8285            // which is a dominant source of session-creation/runtime latency on
8286            // populated VMs.
8287            //
8288            // This is a (size, mode, mtime) quick-check, the same heuristic rsync uses
8289            // by default. It needs no separate cache to invalidate -- it compares against
8290            // the kernel's own stat, so a kernel reset (e.g. a layer swap) or any host
8291            // change that moves size/mode/mtime forces a resync. Limitation: mtime is
8292            // compared at the millisecond granularity the kernel stores (utimes truncates
8293            // to ms), so a host-side rewrite that preserves byte length AND mode AND lands
8294            // in the same wall-clock millisecond can be skipped and leave stale bytes.
8295            // That window is sub-millisecond same-length edits; if it ever matters here,
8296            // upgrade this to a content digest (or full-precision mtime) for files whose
8297            // mtime is within the last few ms of `now`.
8298            if let Ok(existing) = vm.kernel.lstat(&guest_path) {
8299                if !existing.is_directory
8300                    && !existing.is_symbolic_link
8301                    && existing.size == metadata.len()
8302                    && (existing.mode & 0o7777) == (desired_mode & 0o7777)
8303                    && existing.mtime_ms == mtime_ms
8304                {
8305                    continue;
8306                }
8307            }
8308            let bytes = read_host_shadow_file(&host_path, desired_mode).map_err(|error| {
8309                SidecarError::Io(format!(
8310                    "failed to read host shadow file {}: {error}",
8311                    host_path.display()
8312                ))
8313            })?;
8314            vm.kernel.write_file(&guest_path, bytes).map_err(|error| {
8315                SidecarError::InvalidState(format!(
8316                    "failed to sync host shadow file {} to guest {}: {}",
8317                    host_path.display(),
8318                    guest_path,
8319                    kernel_error(error)
8320                ))
8321            })?;
8322            vm.kernel
8323                .chmod(&guest_path, desired_mode)
8324                .map_err(|error| {
8325                    SidecarError::InvalidState(format!(
8326                        "failed to sync host shadow file mode {} to guest {}: {}",
8327                        host_path.display(),
8328                        guest_path,
8329                        kernel_error(error)
8330                    ))
8331                })?;
8332            vm.kernel
8333                .utimes(&guest_path, atime_ms, mtime_ms)
8334                .map_err(|error| {
8335                    SidecarError::InvalidState(format!(
8336                        "failed to sync host shadow file times {} to guest {}: {}",
8337                        host_path.display(),
8338                        guest_path,
8339                        kernel_error(error)
8340                    ))
8341                })?;
8342            continue;
8343        }
8344
8345        if file_type.is_symlink() {
8346            let target = match fs::read_link(&host_path) {
8347                Ok(target) => target,
8348                Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
8349                Err(error) => {
8350                    return Err(SidecarError::Io(format!(
8351                        "failed to read host shadow symlink {}: {error}",
8352                        host_path.display()
8353                    )));
8354                }
8355            };
8356            replace_kernel_symlink(vm, &guest_path, &target.to_string_lossy())?;
8357        }
8358    }
8359
8360    Ok(())
8361}
8362
8363fn replace_kernel_symlink(
8364    vm: &mut VmState,
8365    guest_path: &str,
8366    target: &str,
8367) -> Result<(), SidecarError> {
8368    if vm.kernel.symlink(target, guest_path).is_ok() {
8369        return Ok(());
8370    }
8371
8372    if let Ok(existing_target) = vm.kernel.read_link(guest_path) {
8373        if existing_target == target {
8374            return Ok(());
8375        }
8376    }
8377
8378    let _ = vm.kernel.remove_file(guest_path);
8379    let _ = vm.kernel.remove_dir(guest_path);
8380    vm.kernel
8381        .symlink(target, guest_path)
8382        .map_err(kernel_error)?;
8383    Ok(())
8384}
8385
8386fn host_shadow_mode(metadata: &fs::Metadata) -> u32 {
8387    metadata.permissions().mode() & 0o7777
8388}
8389
8390/// Reads a shadow-root file back into the kernel even when guest-visible mode
8391/// bits make it unreadable for the host user. The sidecar is the kernel for
8392/// this tree, so guest permission bits (for example a 0o200 write-only file
8393/// produced by `chmod` plus a shell append redirect) must not break the
8394/// exit-time shadow sync. The original mode is restored after the read.
8395fn read_host_shadow_file(host_path: &Path, mode: u32) -> std::io::Result<Vec<u8>> {
8396    match fs::read(host_path) {
8397        Ok(bytes) => Ok(bytes),
8398        Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => {
8399            fs::set_permissions(host_path, fs::Permissions::from_mode(mode | 0o400))?;
8400            let result = fs::read(host_path);
8401            fs::set_permissions(host_path, fs::Permissions::from_mode(mode))?;
8402            result
8403        }
8404        Err(error) => Err(error),
8405    }
8406}
8407
8408fn metadata_time_ms(seconds: i64, nanos: i64) -> u64 {
8409    let seconds = seconds.max(0) as u64;
8410    let nanos = nanos.max(0) as u64;
8411    seconds
8412        .saturating_mul(1_000)
8413        .saturating_add(nanos / 1_000_000)
8414}
8415
8416fn is_shadow_bootstrap_dir(path: &str) -> bool {
8417    matches!(
8418        path,
8419        "/dev"
8420            | "/proc"
8421            | "/tmp"
8422            | "/bin"
8423            | "/lib"
8424            | "/sbin"
8425            | "/boot"
8426            | "/etc"
8427            | "/root"
8428            | "/run"
8429            | "/srv"
8430            | "/sys"
8431            | "/opt"
8432            | "/mnt"
8433            | "/media"
8434            | "/home"
8435            | "/home/agentos"
8436            | "/usr"
8437            | "/usr/bin"
8438            | "/usr/games"
8439            | "/usr/include"
8440            | "/usr/lib"
8441            | "/usr/libexec"
8442            | "/usr/man"
8443            | "/usr/local"
8444            | "/usr/local/bin"
8445            | "/usr/sbin"
8446            | "/usr/share"
8447            | "/usr/share/man"
8448            | "/var"
8449            | "/var/cache"
8450            | "/var/empty"
8451            | "/var/lib"
8452            | "/var/lock"
8453            | "/var/log"
8454            | "/var/run"
8455            | "/var/spool"
8456            | "/var/tmp"
8457            | "/etc/agentos"
8458            | "/workspace"
8459    )
8460}
8461
8462#[cfg(test)]
8463mod shadow_sync_tests {
8464    use super::{is_protected_agentos_shadow_sync_path, is_shadow_bootstrap_dir};
8465
8466    #[test]
8467    fn shadow_bootstrap_sync_skips_virtual_home_tree() {
8468        assert!(is_shadow_bootstrap_dir("/home"));
8469        assert!(is_shadow_bootstrap_dir("/home/agentos"));
8470    }
8471
8472    #[test]
8473    fn protected_agentos_paths_are_not_shadow_synced() {
8474        assert!(is_protected_agentos_shadow_sync_path("/etc/agentos"));
8475        assert!(is_protected_agentos_shadow_sync_path(
8476            "/etc/agentos/instructions.md"
8477        ));
8478        assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos-copy"));
8479        assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos.md"));
8480    }
8481}
8482
8483fn is_kernel_owned_shadow_sync_path(path: &str) -> bool {
8484    matches!(path, "/dev" | "/proc" | "/sys")
8485        || path.starts_with("/dev/")
8486        || path.starts_with("/proc/")
8487        || path.starts_with("/sys/")
8488}
8489
8490pub(crate) fn is_protected_agentos_shadow_sync_path(path: &str) -> bool {
8491    path == "/etc/agentos" || path.starts_with("/etc/agentos/")
8492}
8493
8494fn should_skip_shadow_sync_path(vm: &VmState, guest_path: &str) -> bool {
8495    is_kernel_owned_shadow_sync_path(guest_path)
8496        || is_protected_agentos_shadow_sync_path(guest_path)
8497        || host_mount_path_for_guest_path_from_mounts(&vm.configuration.mounts, guest_path)
8498            .is_some()
8499}
8500
8501fn resolve_path_like_guest_specifier(cwd: &str, specifier: &str) -> String {
8502    if specifier.starts_with("file://") {
8503        normalize_path(specifier.trim_start_matches("file://"))
8504    } else if specifier.starts_with("file:") {
8505        normalize_path(specifier.trim_start_matches("file:"))
8506    } else if specifier.starts_with('/') {
8507        normalize_path(specifier)
8508    } else {
8509        normalize_path(&format!("{cwd}/{specifier}"))
8510    }
8511}
8512
8513fn guest_entrypoint_for_specifier(cwd: &str, specifier: &str) -> Option<String> {
8514    is_path_like_specifier(specifier).then(|| resolve_path_like_guest_specifier(cwd, specifier))
8515}
8516
8517fn is_node_runtime_command(command: &str) -> bool {
8518    matches!(command, "node" | "npm" | "npx")
8519        || Path::new(command)
8520            .file_name()
8521            .and_then(|name| name.to_str())
8522            .is_some_and(|name| matches!(name, "node" | "npm" | "npx"))
8523}
8524
8525fn resolve_special_node_cli_invocation(
8526    args: &[String],
8527    env: &mut BTreeMap<String, String>,
8528) -> Option<(String, Vec<String>)> {
8529    let first = args.first()?;
8530    match first.as_str() {
8531        "-e" | "--eval" => {
8532            env.insert(
8533                String::from("AGENTOS_NODE_EVAL"),
8534                args.get(1).cloned().unwrap_or_default(),
8535            );
8536            Some((first.clone(), args.iter().skip(2).cloned().collect()))
8537        }
8538        "-v" | "--version" => {
8539            env.insert(
8540                String::from("AGENTOS_NODE_EVAL"),
8541                String::from("console.log(process.version);"),
8542            );
8543            Some((String::from("-e"), args.to_vec()))
8544        }
8545        _ => None,
8546    }
8547}
8548
8549fn node_runtime_command_name(command: &str) -> Option<&str> {
8550    let name = Path::new(command)
8551        .file_name()
8552        .and_then(|name| name.to_str())?;
8553    matches!(name, "node" | "npm" | "npx").then_some(name)
8554}
8555
8556struct ResolvedHostNodeCliEntrypoint {
8557    command_name: String,
8558    guest_root: String,
8559    guest_entrypoint: String,
8560    package_root: PathBuf,
8561}
8562
8563fn resolve_host_node_cli_entrypoint(command: &str) -> Option<ResolvedHostNodeCliEntrypoint> {
8564    let command_name = node_runtime_command_name(command)?;
8565    if !matches!(command_name, "npm" | "npx") {
8566        return None;
8567    }
8568
8569    let path = std::env::var_os("PATH")?;
8570    for root in std::env::split_paths(&path) {
8571        let candidate = root.join(command_name);
8572        if !candidate.is_file() {
8573            continue;
8574        }
8575        let entrypoint = candidate.canonicalize().ok().unwrap_or(candidate);
8576        let package_root = entrypoint.parent()?.parent()?.to_path_buf();
8577        let guest_root = format!("/__secure_exec/node-runtime/{command_name}");
8578        let relative_entrypoint = entrypoint.strip_prefix(&package_root).ok()?;
8579        let guest_entrypoint = normalize_path(&format!(
8580            "{guest_root}/{}",
8581            relative_entrypoint.to_string_lossy().replace('\\', "/")
8582        ));
8583        return Some(ResolvedHostNodeCliEntrypoint {
8584            command_name: command_name.to_owned(),
8585            guest_root,
8586            guest_entrypoint,
8587            package_root,
8588        });
8589    }
8590
8591    None
8592}
8593
8594fn build_host_node_cli_eval(cli: &ResolvedHostNodeCliEntrypoint) -> String {
8595    let guest_npm_main = normalize_path(&format!("{}/lib/npm.js", cli.guest_root));
8596    let guest_npm_cli = normalize_path(&format!("{}/bin/npm-cli.js", cli.guest_root));
8597    let guest_package_json = normalize_path(&format!("{}/package.json", cli.guest_root));
8598    let guest_display_module = normalize_path(&format!("{}/lib/utils/display.js", cli.guest_root));
8599    let guest_log_file_module =
8600        normalize_path(&format!("{}/lib/utils/log-file.js", cli.guest_root));
8601    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); } } }";
8602    let display_stub = format!(
8603        "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 }};",
8604        display_module = serde_json::to_string(&guest_display_module)
8605            .unwrap_or_else(|_| format!("\"{guest_display_module}\"")),
8606        log_file_module = serde_json::to_string(&guest_log_file_module)
8607            .unwrap_or_else(|_| format!("\"{guest_log_file_module}\"")),
8608    );
8609    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)); }";
8610    match cli.command_name.as_str() {
8611        "npx" => format!(
8612            "{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); }});",
8613            debug_preamble = debug_preamble,
8614            display_stub = display_stub,
8615            registry_fetch_stub = registry_fetch_stub.replace(
8616                "__AGENTOS_NPM_MAIN__",
8617                &serde_json::to_string(&guest_npm_main)
8618                    .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8619            ),
8620            npm_main = serde_json::to_string(&guest_npm_main)
8621                .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8622            npm_cli = serde_json::to_string(&guest_npm_cli)
8623                .unwrap_or_else(|_| format!("\"{guest_npm_cli}\"")),
8624            package_json = serde_json::to_string(&guest_package_json)
8625                .unwrap_or_else(|_| format!("\"{guest_package_json}\"")),
8626        ),
8627        _ => format!(
8628            "{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); }});",
8629            debug_preamble = debug_preamble,
8630            display_stub = display_stub,
8631            registry_fetch_stub = registry_fetch_stub.replace(
8632                "__AGENTOS_NPM_MAIN__",
8633                &serde_json::to_string(&guest_npm_main)
8634                    .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8635            ),
8636            npm_main = serde_json::to_string(&guest_npm_main)
8637                .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8638            package_json = serde_json::to_string(&guest_package_json)
8639                .unwrap_or_else(|_| format!("\"{guest_package_json}\"")),
8640        ),
8641    }
8642}
8643
8644fn resolve_guest_command_entrypoint(
8645    vm: &VmState,
8646    guest_cwd: &str,
8647    command: &str,
8648    path_env: Option<&str>,
8649) -> Option<String> {
8650    if !is_path_like_specifier(command) {
8651        if let Some(entrypoint) = vm.command_guest_paths.get(command) {
8652            return Some(entrypoint.clone());
8653        }
8654
8655        for search_dir in guest_command_search_dirs(vm, guest_cwd, path_env) {
8656            let candidate = normalize_path(&format!("{search_dir}/{command}"));
8657            if let Some(entrypoint) = resolve_guest_command_path_candidate(vm, &candidate) {
8658                return Some(entrypoint);
8659            }
8660        }
8661
8662        return None;
8663    }
8664
8665    let normalized = resolve_path_like_guest_specifier(guest_cwd, command);
8666    resolve_guest_command_path_candidate(vm, &normalized).or_else(|| {
8667        // Some guest shells materialize PATH lookups into absolute candidate paths.
8668        // If that path points into a searched directory but does not exist, fall
8669        // back to the command basename so the sidecar can remap VM command packages.
8670        let parent_dir = Path::new(&normalized).parent()?.to_str()?;
8671        if !guest_command_search_dirs(vm, guest_cwd, path_env)
8672            .iter()
8673            .any(|search_dir| normalize_path(search_dir) == normalize_path(parent_dir))
8674        {
8675            return None;
8676        }
8677
8678        let file_name = Path::new(&normalized).file_name()?.to_str()?;
8679        vm.command_guest_paths.get(file_name).cloned()
8680    })
8681}
8682
8683fn guest_command_search_dirs(vm: &VmState, guest_cwd: &str, path_env: Option<&str>) -> Vec<String> {
8684    let mut search_dirs = Vec::new();
8685    let mut seen = BTreeSet::new();
8686
8687    if let Some(path) = path_env.or_else(|| vm.guest_env.get("PATH").map(String::as_str)) {
8688        for segment in path.split(':') {
8689            let trimmed = segment.trim();
8690            if trimmed.is_empty() {
8691                continue;
8692            }
8693            let normalized = if trimmed.starts_with('/') {
8694                normalize_path(trimmed)
8695            } else {
8696                normalize_path(&format!("{guest_cwd}/{trimmed}"))
8697            };
8698            if seen.insert(normalized.clone()) {
8699                search_dirs.push(normalized);
8700            }
8701        }
8702    }
8703
8704    for fallback in ["/bin", "/usr/bin", "/usr/local/bin"] {
8705        let normalized = String::from(fallback);
8706        if seen.insert(normalized.clone()) {
8707            search_dirs.push(normalized);
8708        }
8709    }
8710
8711    search_dirs
8712}
8713
8714fn resolve_guest_command_path_candidate(vm: &VmState, candidate: &str) -> Option<String> {
8715    if candidate.starts_with("/bin/")
8716        || candidate.starts_with("/usr/bin/")
8717        || candidate.starts_with("/usr/local/bin/")
8718        || candidate.starts_with("/__secure_exec/commands/")
8719    {
8720        if let Some(file_name) = Path::new(candidate)
8721            .file_name()
8722            .and_then(|name| name.to_str())
8723        {
8724            if let Some(guest_entrypoint) = vm.command_guest_paths.get(file_name) {
8725                return Some(guest_entrypoint.clone());
8726            }
8727        }
8728    }
8729
8730    if vm
8731        .kernel
8732        .exists(candidate)
8733        .ok()
8734        .is_some_and(|exists| exists)
8735    {
8736        return Some(normalize_path(candidate));
8737    }
8738
8739    resolve_vm_guest_path_to_host(vm, candidate)
8740        .is_file()
8741        .then(|| normalize_path(candidate))
8742}
8743
8744fn resolve_host_entrypoint_within_vm_host_cwd(
8745    vm: &VmState,
8746    specifier: &str,
8747) -> Option<(String, String)> {
8748    let candidate = Path::new(specifier);
8749    if !candidate.is_absolute() {
8750        return None;
8751    }
8752
8753    let normalized_entrypoint = normalize_host_path(candidate);
8754    let normalized_host_cwd = normalize_host_path(&vm.host_cwd);
8755    if !path_is_within_root(&normalized_entrypoint, &normalized_host_cwd) {
8756        return None;
8757    }
8758
8759    let relative = normalized_entrypoint
8760        .strip_prefix(&normalized_host_cwd)
8761        .ok()?
8762        .to_string_lossy()
8763        .replace('\\', "/");
8764    let guest_entrypoint = if relative.is_empty() {
8765        String::from("/")
8766    } else {
8767        normalize_path(&format!("/{relative}"))
8768    };
8769    Some((
8770        guest_entrypoint,
8771        normalized_entrypoint.to_string_lossy().into_owned(),
8772    ))
8773}
8774
8775fn prepare_guest_runtime_env(
8776    vm: &VmState,
8777    env: &mut BTreeMap<String, String>,
8778    guest_cwd: &str,
8779    host_cwd: &Path,
8780    guest_entrypoint: Option<String>,
8781) -> Result<(), SidecarError> {
8782    let user = vm.kernel.user_profile();
8783    let path_mappings = runtime_guest_path_mappings(vm);
8784    let read_paths = expand_host_access_paths(
8785        std::iter::once(vm.cwd.clone())
8786            .chain(
8787                path_mappings
8788                    .iter()
8789                    .map(|mapping| PathBuf::from(&mapping.host_path)),
8790            )
8791            .chain(std::iter::once(host_cwd.to_path_buf()))
8792            .collect::<Vec<_>>()
8793            .as_slice(),
8794    );
8795    let write_paths = dedupe_host_paths(
8796        std::iter::once(vm.cwd.clone())
8797            .chain(std::iter::once(host_cwd.to_path_buf()))
8798            .chain(runtime_guest_writable_host_paths(vm))
8799            .collect::<Vec<_>>()
8800            .as_slice(),
8801    );
8802    let allowed_node_builtins = configured_allowed_node_builtins(vm);
8803    let loopback_exempt_ports = configured_loopback_exempt_ports(vm);
8804
8805    env.insert(
8806        String::from("AGENTOS_GUEST_PATH_MAPPINGS"),
8807        serde_json::to_string(&path_mappings).map_err(|error| {
8808            SidecarError::InvalidState(format!("failed to encode guest path mappings: {error}"))
8809        })?,
8810    );
8811    env.entry(String::from(EXECUTION_SANDBOX_ROOT_ENV))
8812        .or_insert_with(|| normalize_host_path(&vm.cwd).to_string_lossy().into_owned());
8813    env.insert(
8814        String::from("AGENTOS_EXTRA_FS_READ_PATHS"),
8815        serde_json::to_string(
8816            &read_paths
8817                .iter()
8818                .map(|path| path.to_string_lossy().into_owned())
8819                .collect::<Vec<_>>(),
8820        )
8821        .map_err(|error| {
8822            SidecarError::InvalidState(format!("failed to encode read paths: {error}"))
8823        })?,
8824    );
8825    env.insert(
8826        String::from("AGENTOS_EXTRA_FS_WRITE_PATHS"),
8827        serde_json::to_string(
8828            &write_paths
8829                .iter()
8830                .map(|path| path.to_string_lossy().into_owned())
8831                .collect::<Vec<_>>(),
8832        )
8833        .map_err(|error| {
8834            SidecarError::InvalidState(format!("failed to encode write paths: {error}"))
8835        })?,
8836    );
8837    env.insert(
8838        String::from("AGENTOS_ALLOWED_NODE_BUILTINS"),
8839        serde_json::to_string(&allowed_node_builtins).map_err(|error| {
8840            SidecarError::InvalidState(format!("failed to encode allowed builtins: {error}"))
8841        })?,
8842    );
8843    // The guest JS host platform drives subtractive global scrubbing in the
8844    // per-execution runtime shim (see prepend_v8_runtime_shim).
8845    env.insert(
8846        String::from("AGENTOS_JS_PLATFORM"),
8847        js_runtime_platform_env(vm).to_owned(),
8848    );
8849    // Module-resolution mode (omitted when full Node resolution / the default).
8850    if let Some(resolution) = js_runtime_module_resolution_env(vm) {
8851        env.insert(
8852            String::from("AGENTOS_JS_MODULE_RESOLUTION"),
8853            resolution.to_owned(),
8854        );
8855    }
8856    // Builtin allow-list gate for the live resolver. Present only when builtins
8857    // should be restricted (non-node platform => deny all; node + explicit
8858    // allow-list => exactly those). Absent => unrestricted (node default).
8859    if let Some(allowlist) = js_runtime_enforced_builtins(vm) {
8860        env.insert(
8861            String::from("AGENTOS_JS_BUILTIN_ALLOWLIST"),
8862            serde_json::to_string(&allowlist).map_err(|error| {
8863                SidecarError::InvalidState(format!(
8864                    "failed to encode jsRuntime builtin allow-list: {error}"
8865                ))
8866            })?,
8867        );
8868    }
8869    // Virtual OS identity (os.cpus/totalmem/freemem/homedir/userInfo/...) now
8870    // rides the typed `guest_runtime` (see `guest_runtime_identity`), exposed to
8871    // the guest as the `__agentOSVirtualOs` structured global by the runtime
8872    // shim — no longer the `AGENTOS_VIRTUAL_OS_*` env vars.
8873    // Virtual process uid/gid now ride the typed `guest_runtime` identity
8874    // (see `guest_runtime_identity`), not the `AGENTOS_VIRTUAL_PROCESS_*` env.
8875    env.entry(String::from("HOME"))
8876        .or_insert_with(|| user.homedir.clone());
8877    env.entry(String::from("USER"))
8878        .or_insert_with(|| user.username.clone());
8879    env.entry(String::from("LOGNAME"))
8880        .or_insert_with(|| user.username.clone());
8881    env.entry(String::from("SHELL"))
8882        .or_insert_with(|| user.shell.clone());
8883    env.entry(String::from("PATH")).or_insert_with(|| {
8884        vm.guest_env
8885            .get("PATH")
8886            .cloned()
8887            .unwrap_or_else(|| crate::vm::DEFAULT_GUEST_PATH_ENV.to_owned())
8888    });
8889    env.entry(String::from("TMPDIR"))
8890        .or_insert_with(|| String::from("/tmp"));
8891    env.insert(String::from("PWD"), guest_cwd.to_owned());
8892    if !loopback_exempt_ports.is_empty() {
8893        env.insert(
8894            String::from(LOOPBACK_EXEMPT_PORTS_ENV),
8895            serde_json::to_string(&loopback_exempt_ports).map_err(|error| {
8896                SidecarError::InvalidState(format!("failed to encode loopback exemptions: {error}"))
8897            })?,
8898        );
8899    }
8900    if let Some(guest_entrypoint) = guest_entrypoint {
8901        env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint);
8902    }
8903    Ok(())
8904}
8905
8906fn virtual_os_cpu_count(resource_limits: &ResourceLimits) -> usize {
8907    resource_limits.virtual_cpu_count.unwrap_or(1).max(1)
8908}
8909
8910fn virtual_os_totalmem_bytes(resource_limits: &ResourceLimits) -> u64 {
8911    resource_limits
8912        .max_wasm_memory_bytes
8913        .unwrap_or(1024 * 1024 * 1024)
8914}
8915
8916fn virtual_os_freemem_bytes(resource_limits: &ResourceLimits) -> u64 {
8917    resource_limits
8918        .max_wasm_memory_bytes
8919        .unwrap_or(512 * 1024 * 1024)
8920}
8921
8922/// Build the typed per-execution JavaScript limits from the per-VM `VmLimits`
8923/// (sourced from `CreateVmConfig` on the BARE wire). These ride the execution
8924/// request, not `AGENTOS_*` env vars — see the env-vs-wire rule in
8925/// `crates/sidecar/CLAUDE.md`.
8926fn javascript_execution_limits(vm: &VmState) -> JavascriptExecutionLimits {
8927    JavascriptExecutionLimits {
8928        v8_heap_limit_mb: vm.limits.js_runtime.v8_heap_limit_mb,
8929        sync_rpc_wait_timeout_ms: vm.limits.js_runtime.sync_rpc_wait_timeout_ms,
8930    }
8931}
8932
8933/// Build the typed per-execution guest-runtime identity (virtual `process.*`)
8934/// from kernel state. Replaces the `AGENTOS_VIRTUAL_PROCESS_{UID,GID,PID,PPID}`
8935/// env round-trip: the runtime shim reads these from `guest_runtime`, not env.
8936/// `uid`/`gid` come from the VM user profile (applied to every guest);
8937/// `pid`/`ppid` are per-process and only set for paths that assigned them.
8938fn guest_runtime_identity(
8939    vm: &VmState,
8940    virtual_pid: Option<u64>,
8941    virtual_ppid: Option<u64>,
8942) -> GuestRuntimeConfig {
8943    let user = vm.kernel.user_profile();
8944    let resource_limits = vm.kernel.resource_limits();
8945    GuestRuntimeConfig {
8946        virtual_uid: Some(u64::from(user.uid)),
8947        virtual_gid: Some(u64::from(user.gid)),
8948        virtual_pid,
8949        virtual_ppid,
8950        virtual_exec_path: None,
8951        os_cpu_count: Some(virtual_os_cpu_count(resource_limits) as u64),
8952        os_totalmem: Some(virtual_os_totalmem_bytes(resource_limits)),
8953        os_freemem: Some(virtual_os_freemem_bytes(resource_limits)),
8954        os_homedir: Some(user.homedir.clone()),
8955        os_hostname: None,
8956        os_shell: Some(user.shell.clone()),
8957        os_user: Some(user.username.clone()),
8958        // Userland bundle to bake into the per-sidecar snapshot, supplied by the
8959        // (trusted) client via jsRuntime.snapshotUserlandCode. The agent-os layer
8960        // sets this to the agent SDK bundle for snapshot-enabled agents; `None`
8961        // keeps the bridge-only snapshot.
8962        snapshot_userland_code: vm
8963            .configuration
8964            .js_runtime
8965            .as_ref()
8966            .and_then(|cfg| cfg.snapshot_userland_code.clone()),
8967    }
8968}
8969
8970/// The guest's virtual home directory, sourced from the VM user profile (the
8971/// same value carried to the guest as `os.homedir()` via `guest_runtime`). Used
8972/// by sidecar-internal `~`-path resolution; falls back to `/root` for a
8973/// non-absolute profile value.
8974fn guest_virtual_home(vm: &VmState) -> String {
8975    let homedir = vm.kernel.user_profile().homedir;
8976    if homedir.starts_with('/') {
8977        homedir
8978    } else {
8979        String::from("/root")
8980    }
8981}
8982
8983/// Build the typed per-execution Python limits from the per-VM `VmLimits`.
8984fn python_execution_limits(vm: &VmState) -> PythonExecutionLimits {
8985    PythonExecutionLimits {
8986        output_buffer_max_bytes: Some(vm.limits.python.output_buffer_max_bytes),
8987        execution_timeout_ms: Some(vm.limits.python.execution_timeout_ms),
8988        max_old_space_mb: Some(vm.limits.python.max_old_space_mb),
8989        vfs_rpc_timeout_ms: Some(vm.limits.python.vfs_rpc_timeout_ms),
8990    }
8991}
8992
8993/// Build the typed per-execution WebAssembly limits from the per-VM kernel
8994/// `ResourceLimits`. Replaces the old `apply_wasm_limit_env` env round-trip;
8995/// notably this is the path that finally enforces the stack cap that the
8996/// `AGENTOS_WASM_MAX_STACK_BYTES` env knob set but no reader consumed.
8997fn wasm_execution_limits(vm: &VmState) -> WasmExecutionLimits {
8998    let resource_limits = vm.kernel.resource_limits();
8999    WasmExecutionLimits {
9000        max_fuel: resource_limits.max_wasm_fuel,
9001        max_memory_bytes: resource_limits.max_wasm_memory_bytes,
9002        max_stack_bytes: resource_limits
9003            .max_wasm_stack_bytes
9004            .map(|value| value as u64),
9005    }
9006}
9007
9008/// The guest JavaScript host platform configured for this VM, defaulting to
9009/// full Node.js emulation when no `jsRuntime` config was supplied at create.
9010fn js_runtime_platform(vm: &VmState) -> vm_config::JsRuntimePlatform {
9011    vm.configuration
9012        .js_runtime
9013        .as_ref()
9014        .map(|cfg| cfg.platform)
9015        .unwrap_or(vm_config::JsRuntimePlatform::Node)
9016}
9017
9018/// Lowercase wire name for the configured platform, mirroring the serde
9019/// representation of `vm_config::JsRuntimePlatform`.
9020fn js_runtime_platform_env(vm: &VmState) -> &'static str {
9021    match js_runtime_platform(vm) {
9022        vm_config::JsRuntimePlatform::Node => "node",
9023        vm_config::JsRuntimePlatform::Browser => "browser",
9024        vm_config::JsRuntimePlatform::Neutral => "neutral",
9025        vm_config::JsRuntimePlatform::Bare => "bare",
9026    }
9027}
9028
9029/// Wire name for the configured module-resolution mode, or `None` when it is the
9030/// full-Node default (which the live resolver also assumes when the env is unset).
9031fn js_runtime_module_resolution_env(vm: &VmState) -> Option<&'static str> {
9032    let resolution = vm
9033        .configuration
9034        .js_runtime
9035        .as_ref()
9036        .map(|cfg| cfg.module_resolution)
9037        .unwrap_or(vm_config::JsModuleResolution::Node);
9038    match resolution {
9039        vm_config::JsModuleResolution::Node => None,
9040        vm_config::JsModuleResolution::Relative => Some("relative"),
9041        vm_config::JsModuleResolution::None => Some("none"),
9042    }
9043}
9044
9045/// The builtin allow-list the live resolver should enforce, or `None` to leave
9046/// builtins unrestricted (full Node default — preserving today's behavior).
9047/// Non-node platforms enforce an empty list (deny all builtins).
9048fn js_runtime_enforced_builtins(vm: &VmState) -> Option<Vec<String>> {
9049    if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node {
9050        return Some(Vec::new());
9051    }
9052    vm.configuration
9053        .js_runtime
9054        .as_ref()
9055        .and_then(|cfg| cfg.allowed_builtins.clone())
9056}
9057
9058fn configured_allowed_node_builtins(vm: &VmState) -> Vec<String> {
9059    // Non-node platforms expose no Node builtin modules at all.
9060    if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node {
9061        return Vec::new();
9062    }
9063    // Under the node platform an explicit allow-list wins — including an explicit
9064    // empty list, which means deny all. Absence falls back to the engine default.
9065    let configured = match vm
9066        .configuration
9067        .js_runtime
9068        .as_ref()
9069        .and_then(|cfg| cfg.allowed_builtins.as_ref())
9070    {
9071        Some(list) => list.clone(),
9072        None => DEFAULT_ALLOWED_NODE_BUILTINS
9073            .iter()
9074            .map(|value| (*value).to_owned())
9075            .collect::<Vec<_>>(),
9076    };
9077    dedupe_strings(&configured)
9078}
9079
9080fn configured_loopback_exempt_ports(vm: &VmState) -> Vec<String> {
9081    if !vm.configuration.loopback_exempt_ports.is_empty() {
9082        return vm
9083            .configuration
9084            .loopback_exempt_ports
9085            .iter()
9086            .map(ToString::to_string)
9087            .collect();
9088    }
9089
9090    vm.create_loopback_exempt_ports
9091        .iter()
9092        .map(ToString::to_string)
9093        .collect()
9094}
9095
9096/// Extract the `hostPath` string from a mount plugin's JSON-encoded config.
9097fn mount_config_host_path(config: &str) -> Option<String> {
9098    serde_json::from_str::<Value>(config)
9099        .ok()?
9100        .get("hostPath")
9101        .and_then(Value::as_str)
9102        .map(str::to_owned)
9103}
9104
9105fn runtime_guest_writable_host_paths(vm: &VmState) -> Vec<PathBuf> {
9106    vm.configuration
9107        .mounts
9108        .iter()
9109        .filter(|mount| !mount.read_only)
9110        .filter_map(|mount| {
9111            ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
9112                .then(|| mount_config_host_path(&mount.plugin.config))
9113                .flatten()
9114                .map(PathBuf::from)
9115        })
9116        .collect()
9117}
9118
9119fn runtime_guest_path_mappings(vm: &VmState) -> Vec<RuntimeGuestPathMapping> {
9120    let mut mappings = vm
9121        .configuration
9122        .mounts
9123        .iter()
9124        .filter_map(|mount| {
9125            ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
9126                .then(|| {
9127                    mount_config_host_path(&mount.plugin.config).map(|host_path| {
9128                        RuntimeGuestPathMapping {
9129                            guest_path: normalize_path(&mount.guest_path),
9130                            host_path,
9131                            read_only: mount.read_only,
9132                        }
9133                    })
9134                })
9135                .flatten()
9136        })
9137        .collect::<Vec<_>>();
9138    let mut command_root_mappings = vm
9139        .command_guest_paths
9140        .values()
9141        .filter_map(|guest_path| {
9142            Path::new(guest_path)
9143                .parent()
9144                .and_then(|parent| parent.to_str())
9145                .map(normalize_path)
9146        })
9147        .collect::<BTreeSet<_>>()
9148        .into_iter()
9149        .map(|guest_path| RuntimeGuestPathMapping {
9150            host_path: resolve_vm_guest_path_to_host(vm, &guest_path)
9151                .to_string_lossy()
9152                .into_owned(),
9153            guest_path,
9154            read_only: false,
9155        })
9156        .collect::<Vec<_>>();
9157    mappings.append(&mut command_root_mappings);
9158    let mut extra_node_modules_roots = mappings
9159        .iter()
9160        .filter(|mapping| mapping.guest_path.starts_with("/root/node_modules/"))
9161        .filter_map(|mapping| {
9162            host_node_modules_root(Path::new(&mapping.host_path)).map(|host_root| {
9163                RuntimeGuestPathMapping {
9164                    guest_path: String::from("/root/node_modules"),
9165                    host_path: host_root.to_string_lossy().into_owned(),
9166                    read_only: mapping.read_only,
9167                }
9168            })
9169        })
9170        .collect::<Vec<_>>();
9171    mappings.append(&mut extra_node_modules_roots);
9172    mappings.push(RuntimeGuestPathMapping {
9173        guest_path: String::from("/"),
9174        host_path: vm.cwd.to_string_lossy().into_owned(),
9175        read_only: false,
9176    });
9177    mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.guest_path.len()));
9178    mappings.dedup_by(|left, right| {
9179        left.guest_path == right.guest_path && left.host_path == right.host_path
9180    });
9181    mappings
9182}
9183
9184/// Build a `Send`-able, read-only VFS module reader over the VM's read-only
9185/// `host_dir`/`module_access` mounts (and the derived `/root/node_modules` root
9186/// for nested mounts). When present, the V8 bridge thread resolves modules
9187/// inline against this reader — concurrently with the service loop — so a large
9188/// cold-start module graph never serializes behind / starves an in-flight ACP
9189/// `session/new` bootstrap on the single service-loop thread. The reader reads
9190/// the same mounted tree the guest sees (anchored `openat2`, escaping-symlink
9191/// refusal), never the host-direct path translator. Returns `None` when the VM
9192/// has no usable read-only mount, so resolution falls back to the service-loop
9193/// kernel reader.
9194fn build_module_reader(
9195    vm: &VmState,
9196    resolved: &ResolvedChildProcessExecution,
9197) -> Option<crate::plugins::host_dir::HostDirModuleReader> {
9198    let mut pairs: Vec<(String, PathBuf)> = vm
9199        .configuration
9200        .mounts
9201        .iter()
9202        .filter(|mount| mount.read_only)
9203        .filter(|mount| (mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
9204        .filter_map(|mount| {
9205            mount_config_host_path(&mount.plugin.config)
9206                .map(|host_path| (normalize_path(&mount.guest_path), PathBuf::from(host_path)))
9207        })
9208        .collect();
9209
9210    let guest_entrypoint = resolved
9211        .env
9212        .get("AGENTOS_GUEST_ENTRYPOINT")
9213        .map(|path| normalize_path(path));
9214    if let Some(guest_entrypoint) = guest_entrypoint.as_deref() {
9215        let entrypoint_in_read_only_mount = pairs.iter().any(|(guest_path, _)| {
9216            guest_entrypoint == guest_path
9217                || guest_entrypoint.starts_with(&format!("{guest_path}/"))
9218        });
9219        if !entrypoint_in_read_only_mount {
9220            return None;
9221        }
9222    }
9223
9224    // Mirror runtime_guest_path_mappings: a mount nested under
9225    // `/root/node_modules/<pkg>` implies a `/root/node_modules` root the resolver
9226    // walks, so expose that root too (e.g. software-package mounts).
9227    let extra_roots: Vec<(String, PathBuf)> = pairs
9228        .iter()
9229        .filter(|(guest_path, _)| guest_path.starts_with("/root/node_modules/"))
9230        .filter_map(|(_, host_path)| {
9231            host_node_modules_root(host_path).map(|root| (String::from("/root/node_modules"), root))
9232        })
9233        .collect();
9234    pairs.extend(extra_roots);
9235
9236    crate::plugins::host_dir::HostDirModuleReader::from_mounts(pairs)
9237}
9238
9239fn host_node_modules_root(path: &Path) -> Option<PathBuf> {
9240    if let Some(root) = path
9241        .ancestors()
9242        .filter(|candidate| {
9243            candidate.file_name().and_then(|name| name.to_str()) == Some("node_modules")
9244        })
9245        .last()
9246        .map(Path::to_path_buf)
9247    {
9248        return Some(root);
9249    }
9250
9251    fs::canonicalize(path)
9252        .ok()?
9253        .ancestors()
9254        .filter(|candidate| {
9255            candidate.file_name().and_then(|name| name.to_str()) == Some("node_modules")
9256        })
9257        .last()
9258        .map(Path::to_path_buf)
9259}
9260
9261#[cfg(test)]
9262mod runtime_guest_path_mapping_tests {
9263    use super::{host_node_modules_root, javascript_sync_rpc_option_bool};
9264    use serde_json::json;
9265    use std::fs;
9266    use std::time::{SystemTime, UNIX_EPOCH};
9267
9268    #[test]
9269    fn host_node_modules_root_prefers_workspace_root_over_pnpm_package_node_modules() {
9270        let unique = SystemTime::now()
9271            .duration_since(UNIX_EPOCH)
9272            .expect("clock should be monotonic")
9273            .as_nanos();
9274        let temp = std::env::temp_dir().join(format!("secure-exec-sidecar-node-modules-{unique}"));
9275        let workspace_node_modules = temp.join("node_modules");
9276        let package_root = workspace_node_modules
9277            .join(".pnpm")
9278            .join("example@1.0.0")
9279            .join("node_modules")
9280            .join("@scope")
9281            .join("pkg");
9282        fs::create_dir_all(&package_root).expect("package root should be created");
9283
9284        let resolved =
9285            host_node_modules_root(&package_root).expect("node_modules root should resolve");
9286
9287        assert_eq!(resolved, workspace_node_modules);
9288
9289        fs::remove_dir_all(&temp).expect("temp tree should be removed");
9290    }
9291
9292    #[test]
9293    fn host_node_modules_root_preserves_symlinked_workspace_node_modules_path() {
9294        let unique = SystemTime::now()
9295            .duration_since(UNIX_EPOCH)
9296            .expect("clock should be monotonic")
9297            .as_nanos();
9298        let temp =
9299            std::env::temp_dir().join(format!("secure-exec-sidecar-node-modules-symlink-{unique}"));
9300        let workspace_node_modules = temp.join("node_modules");
9301        let package_link = workspace_node_modules.join("@scope").join("pkg");
9302        let real_package = temp.join("registry").join("agent").join("pkg");
9303        fs::create_dir_all(package_link.parent().expect("package parent should exist"))
9304            .expect("scoped parent should be created");
9305        fs::create_dir_all(&real_package).expect("real package root should be created");
9306        std::os::unix::fs::symlink(&real_package, &package_link)
9307            .expect("package symlink should be created");
9308
9309        let resolved =
9310            host_node_modules_root(&package_link).expect("node_modules root should resolve");
9311
9312        assert_eq!(resolved, workspace_node_modules);
9313
9314        fs::remove_dir_all(&temp).expect("temp tree should be removed");
9315    }
9316
9317    #[test]
9318    fn javascript_sync_rpc_option_bool_accepts_boolean_recursive_argument() {
9319        assert_eq!(
9320            javascript_sync_rpc_option_bool(&[json!("/workspace"), json!(true)], 1, "recursive"),
9321            Some(true)
9322        );
9323        assert_eq!(
9324            javascript_sync_rpc_option_bool(
9325                &[json!("/workspace"), json!({ "recursive": false })],
9326                1,
9327                "recursive"
9328            ),
9329            Some(false)
9330        );
9331    }
9332}
9333
9334#[cfg(test)]
9335mod kernel_poll_sync_rpc_tests {
9336    use super::{
9337        service_javascript_kernel_poll_sync_rpc, ActiveExecution, ActiveProcess,
9338        JavascriptSyncRpcRequest, KernelPollFdResponse, SidecarKernel, ToolExecution,
9339        EXECUTION_DRIVER_NAME, JAVASCRIPT_COMMAND,
9340    };
9341    use secure_exec_kernel::command_registry::CommandDriver;
9342    use secure_exec_kernel::kernel::{KernelVmConfig, SpawnOptions};
9343    use secure_exec_kernel::mount_table::MountTable;
9344    use secure_exec_kernel::permissions::Permissions;
9345    use secure_exec_kernel::poll::{POLLHUP, POLLIN};
9346    use secure_exec_kernel::vfs::MemoryFileSystem;
9347    use serde_json::{json, Value};
9348    #[test]
9349    fn javascript_kernel_poll_sync_rpc_reports_multiple_kernel_fds() {
9350        let mut config = KernelVmConfig::new("vm-js-kernel-poll");
9351        config.permissions = Permissions::allow_all();
9352        let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config);
9353        kernel
9354            .register_driver(CommandDriver::new(
9355                EXECUTION_DRIVER_NAME,
9356                [JAVASCRIPT_COMMAND],
9357            ))
9358            .expect("register execution driver");
9359
9360        let kernel_handle = kernel
9361            .spawn_process(
9362                JAVASCRIPT_COMMAND,
9363                Vec::new(),
9364                SpawnOptions {
9365                    requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
9366                    ..SpawnOptions::default()
9367                },
9368            )
9369            .expect("spawn javascript kernel process");
9370        let pid = kernel_handle.pid();
9371
9372        let (stdin_read_fd, stdin_write_fd) = kernel
9373            .open_pipe(EXECUTION_DRIVER_NAME, pid)
9374            .expect("open kernel stdin pipe");
9375        kernel
9376            .fd_dup2(EXECUTION_DRIVER_NAME, pid, stdin_read_fd, 0)
9377            .expect("dup stdin pipe onto fd 0");
9378        kernel
9379            .fd_close(EXECUTION_DRIVER_NAME, pid, stdin_read_fd)
9380            .expect("close original stdin read fd");
9381
9382        let process = ActiveProcess::new(
9383            pid,
9384            kernel_handle,
9385            super::GuestRuntimeKind::JavaScript,
9386            ActiveExecution::Tool(ToolExecution::default()),
9387        );
9388
9389        kernel
9390            .fd_write(EXECUTION_DRIVER_NAME, pid, stdin_write_fd, b"poll-ready")
9391            .expect("write kernel stdin payload");
9392        kernel
9393            .fd_close(EXECUTION_DRIVER_NAME, pid, stdin_write_fd)
9394            .expect("close kernel stdin writer");
9395
9396        let response = service_javascript_kernel_poll_sync_rpc(
9397            &mut kernel,
9398            &process,
9399            &JavascriptSyncRpcRequest {
9400                id: 1,
9401                method: String::from("__kernel_poll"),
9402                args: vec![
9403                    json!([
9404                        { "fd": 0, "events": POLLIN.bits() },
9405                        { "fd": 1, "events": POLLIN.bits() }
9406                    ]),
9407                    json!(250),
9408                ],
9409            },
9410        )
9411        .expect("poll kernel fds");
9412
9413        assert_eq!(response["readyCount"], Value::from(1));
9414        let fds: Vec<KernelPollFdResponse> =
9415            serde_json::from_value(response["fds"].clone()).expect("kernel poll fd response");
9416        assert_eq!(
9417            fds,
9418            vec![
9419                KernelPollFdResponse {
9420                    fd: 0,
9421                    events: POLLIN.bits(),
9422                    revents: (POLLIN | POLLHUP).bits(),
9423                },
9424                KernelPollFdResponse {
9425                    fd: 1,
9426                    events: POLLIN.bits(),
9427                    revents: 0,
9428                },
9429            ]
9430        );
9431
9432        process.kernel_handle.finish(0);
9433        kernel.waitpid(pid).expect("wait javascript kernel process");
9434    }
9435}
9436
9437fn dedupe_strings(values: &[String]) -> Vec<String> {
9438    let mut seen = BTreeSet::new();
9439    let mut deduped = Vec::new();
9440    for value in values {
9441        if seen.insert(value.clone()) {
9442            deduped.push(value.clone());
9443        }
9444    }
9445    deduped
9446}
9447
9448fn dedupe_host_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
9449    let mut seen = BTreeSet::new();
9450    let mut deduped = Vec::new();
9451    for path in paths {
9452        let normalized = normalize_host_path(path);
9453        let key = normalized.to_string_lossy().into_owned();
9454        if seen.insert(key) {
9455            deduped.push(normalized);
9456        }
9457    }
9458    deduped
9459}
9460
9461fn expand_host_access_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
9462    let mut expanded = Vec::new();
9463    let mut seen = BTreeSet::new();
9464
9465    let mut add_path = |candidate: PathBuf| {
9466        let normalized = normalize_host_path(&candidate);
9467        let key = normalized.to_string_lossy().into_owned();
9468        if seen.insert(key) {
9469            expanded.push(normalized);
9470        }
9471    };
9472
9473    for host_path in paths {
9474        add_path(host_path.clone());
9475        if let Ok(realpath) = fs::canonicalize(host_path) {
9476            add_path(realpath);
9477        }
9478
9479        if host_path.file_name().and_then(|name| name.to_str()) != Some("node_modules") {
9480            continue;
9481        }
9482
9483        let mut current = host_path.parent();
9484        while let Some(parent) = current {
9485            let candidate = parent.join("node_modules");
9486            if candidate.exists() {
9487                add_path(candidate.clone());
9488                if let Ok(realpath) = fs::canonicalize(&candidate) {
9489                    add_path(realpath);
9490                }
9491            }
9492            current = parent.parent();
9493        }
9494    }
9495
9496    expanded
9497}
9498
9499fn prepare_javascript_shadow(
9500    vm: &mut VmState,
9501    resolved: &ResolvedChildProcessExecution,
9502) -> Result<(), SidecarError> {
9503    let guest_entrypoint = resolved
9504        .env
9505        .get("AGENTOS_GUEST_ENTRYPOINT")
9506        .cloned()
9507        // An absolute `entrypoint` may be a host path that lives inside the VM's
9508        // host cwd (callers can pass a fully-qualified host path). The guest sees
9509        // it at its translated guest path (host_cwd -> guest_cwd), so the shadow
9510        // must be keyed by that guest path rather than the raw host path. Falling
9511        // back to the host path here would materialize the file at the wrong guest
9512        // location and the runtime's `require()` would fail with "Cannot find
9513        // module".
9514        .or_else(|| {
9515            resolve_host_entrypoint_within_vm_host_cwd(vm, &resolved.entrypoint)
9516                .map(|(guest_entrypoint, _)| guest_entrypoint)
9517        })
9518        .or_else(|| {
9519            resolved
9520                .entrypoint
9521                .starts_with('/')
9522                .then(|| normalize_path(&resolved.entrypoint))
9523        });
9524    let Some(guest_entrypoint) = guest_entrypoint else {
9525        return Ok(());
9526    };
9527    if host_mount_path_for_guest_path(vm, &guest_entrypoint).is_some() {
9528        return Ok(());
9529    }
9530    if vm.kernel.lstat(&guest_entrypoint).is_err() {
9531        let host_entrypoint = {
9532            let candidate = Path::new(&resolved.entrypoint);
9533            if candidate.is_absolute() {
9534                candidate.to_path_buf()
9535            } else {
9536                resolved.host_cwd.join(candidate)
9537            }
9538        };
9539        if host_entrypoint.exists() {
9540            materialize_host_path_to_shadow(vm, &guest_entrypoint, &host_entrypoint)?;
9541            // The shadow write only stages the file on the host side; the runtime
9542            // resolves modules against the kernel VFS, so the staged entrypoint
9543            // must be synced into the kernel before execution starts (otherwise
9544            // `require()` reports "Cannot find module").
9545            return sync_shadow_entrypoint_into_kernel(vm, &guest_entrypoint);
9546        }
9547    }
9548    materialize_guest_path_to_shadow(vm, &guest_entrypoint)
9549}
9550
9551/// Sync a freshly-staged shadow entrypoint into the kernel VFS so the runtime's
9552/// kernel-backed module resolver can read it. Mirrors the host->kernel file sync
9553/// used by the broader shadow reconciliation, but scoped to the single
9554/// entrypoint we just materialized.
9555fn sync_shadow_entrypoint_into_kernel(
9556    vm: &mut VmState,
9557    guest_entrypoint: &str,
9558) -> Result<(), SidecarError> {
9559    if vm.kernel.exists(guest_entrypoint).unwrap_or(false) {
9560        return Ok(());
9561    }
9562    let shadow_path = shadow_path_for_guest(vm, guest_entrypoint);
9563    let bytes = match fs::read(&shadow_path) {
9564        Ok(bytes) => bytes,
9565        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
9566        Err(error) => {
9567            return Err(SidecarError::Io(format!(
9568                "failed to read staged shadow entrypoint {}: {error}",
9569                shadow_path.display()
9570            )));
9571        }
9572    };
9573    if let Some(parent) = guest_parent_path(guest_entrypoint) {
9574        if !vm.kernel.exists(&parent).unwrap_or(false) {
9575            vm.kernel.mkdir(&parent, true).map_err(kernel_error)?;
9576        }
9577    }
9578    vm.kernel
9579        .write_file(guest_entrypoint, bytes)
9580        .map_err(kernel_error)?;
9581    Ok(())
9582}
9583
9584fn guest_parent_path(guest_path: &str) -> Option<String> {
9585    let parent = Path::new(guest_path).parent()?;
9586    let parent = parent.to_string_lossy();
9587    if parent.is_empty() || parent == "/" {
9588        None
9589    } else {
9590        Some(parent.into_owned())
9591    }
9592}
9593
9594fn materialize_host_path_to_shadow(
9595    vm: &VmState,
9596    guest_path: &str,
9597    host_path: &Path,
9598) -> Result<(), SidecarError> {
9599    let shadow_path = shadow_path_for_guest(vm, guest_path);
9600    let metadata = fs::symlink_metadata(host_path)
9601        .map_err(|error| SidecarError::Io(format!("failed to stat host entrypoint: {error}")))?;
9602
9603    if metadata.file_type().is_symlink() {
9604        if let Some(parent) = shadow_path.parent() {
9605            fs::create_dir_all(parent).map_err(|error| {
9606                SidecarError::Io(format!("failed to create shadow symlink parent: {error}"))
9607            })?;
9608        }
9609        let _ = fs::remove_file(&shadow_path);
9610        let _ = fs::remove_dir_all(&shadow_path);
9611        let target = fs::read_link(host_path)
9612            .map_err(|error| SidecarError::Io(format!("failed to read host symlink: {error}")))?;
9613        std::os::unix::fs::symlink(&target, &shadow_path)
9614            .map_err(|error| SidecarError::Io(format!("failed to mirror host symlink: {error}")))?;
9615        return Ok(());
9616    }
9617
9618    if metadata.is_dir() {
9619        fs::create_dir_all(&shadow_path).map_err(|error| {
9620            SidecarError::Io(format!("failed to create shadow directory: {error}"))
9621        })?;
9622        fs::set_permissions(
9623            &shadow_path,
9624            fs::Permissions::from_mode(metadata.permissions().mode() & 0o7777),
9625        )
9626        .map_err(|error| {
9627            SidecarError::Io(format!(
9628                "failed to set shadow directory mode on {}: {error}",
9629                shadow_path.display()
9630            ))
9631        })?;
9632        return Ok(());
9633    }
9634
9635    if let Some(parent) = shadow_path.parent() {
9636        fs::create_dir_all(parent).map_err(|error| {
9637            SidecarError::Io(format!("failed to create shadow parent: {error}"))
9638        })?;
9639    }
9640    let bytes = fs::read(host_path)
9641        .map_err(|error| SidecarError::Io(format!("failed to read host entrypoint: {error}")))?;
9642    fs::write(&shadow_path, bytes).map_err(|error| {
9643        SidecarError::Io(format!(
9644            "failed to mirror host file into shadow root: {error}"
9645        ))
9646    })?;
9647    fs::set_permissions(
9648        &shadow_path,
9649        fs::Permissions::from_mode(metadata.permissions().mode() & 0o7777),
9650    )
9651    .map_err(|error| {
9652        SidecarError::Io(format!(
9653            "failed to set shadow file mode on {}: {error}",
9654            shadow_path.display()
9655        ))
9656    })?;
9657    Ok(())
9658}
9659
9660fn materialize_guest_path_to_shadow(
9661    vm: &mut VmState,
9662    guest_path: &str,
9663) -> Result<(), SidecarError> {
9664    let stat = vm.kernel.lstat(guest_path).map_err(kernel_error)?;
9665    let shadow_path = shadow_path_for_guest(vm, guest_path);
9666
9667    if stat.is_symbolic_link {
9668        if let Some(parent) = shadow_path.parent() {
9669            fs::create_dir_all(parent).map_err(|error| {
9670                SidecarError::Io(format!("failed to create shadow symlink parent: {error}"))
9671            })?;
9672        }
9673        let _ = fs::remove_file(&shadow_path);
9674        let _ = fs::remove_dir_all(&shadow_path);
9675        let target = vm.kernel.read_link(guest_path).map_err(kernel_error)?;
9676        std::os::unix::fs::symlink(&target, &shadow_path)
9677            .map_err(|error| SidecarError::Io(format!("failed to mirror symlink: {error}")))?;
9678        return Ok(());
9679    }
9680
9681    if stat.is_directory {
9682        fs::create_dir_all(&shadow_path).map_err(|error| {
9683            SidecarError::Io(format!("failed to create shadow directory: {error}"))
9684        })?;
9685        fs::set_permissions(&shadow_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err(
9686            |error| {
9687                SidecarError::Io(format!(
9688                    "failed to set shadow directory mode on {}: {error}",
9689                    shadow_path.display()
9690                ))
9691            },
9692        )?;
9693        return Ok(());
9694    }
9695
9696    if let Some(parent) = shadow_path.parent() {
9697        fs::create_dir_all(parent).map_err(|error| {
9698            SidecarError::Io(format!("failed to create shadow parent: {error}"))
9699        })?;
9700    }
9701    let bytes = vm.kernel.read_file(guest_path).map_err(kernel_error)?;
9702    fs::write(&shadow_path, bytes).map_err(|error| {
9703        SidecarError::Io(format!(
9704            "failed to mirror guest file into shadow root: {error}"
9705        ))
9706    })?;
9707    fs::set_permissions(&shadow_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err(
9708        |error| {
9709            SidecarError::Io(format!(
9710                "failed to set shadow file mode on {}: {error}",
9711                shadow_path.display()
9712            ))
9713        },
9714    )?;
9715    Ok(())
9716}
9717
9718fn load_javascript_entrypoint_source(
9719    vm: &mut VmState,
9720    host_cwd: &Path,
9721    entrypoint: &str,
9722    env: &BTreeMap<String, String>,
9723) -> Option<String> {
9724    let mut read_guest_file = |path: &str| {
9725        vm.kernel
9726            .read_file(path)
9727            .ok()
9728            .and_then(|bytes| String::from_utf8(bytes).ok())
9729    };
9730
9731    if let Some(source) = env
9732        .get("AGENTOS_GUEST_ENTRYPOINT")
9733        .filter(|path| path.starts_with('/'))
9734        .and_then(|path| read_guest_file(path))
9735    {
9736        return Some(source);
9737    }
9738
9739    if entrypoint.starts_with('/') {
9740        if let Some(source) = read_guest_file(entrypoint) {
9741            return Some(source);
9742        }
9743    }
9744
9745    let host_entrypoint = if Path::new(entrypoint).is_absolute() {
9746        PathBuf::from(entrypoint)
9747    } else {
9748        host_cwd.join(entrypoint)
9749    };
9750    let normalized_entrypoint = normalize_host_path(&host_entrypoint);
9751    let sandbox_root = normalize_host_path(&vm.cwd);
9752    let host_cwd = normalize_host_path(&vm.host_cwd);
9753    if !path_is_within_root(&normalized_entrypoint, &sandbox_root)
9754        && !path_is_within_root(&normalized_entrypoint, &host_cwd)
9755    {
9756        return None;
9757    }
9758
9759    fs::read_to_string(&normalized_entrypoint).ok()
9760}
9761
9762fn emit_dns_resolution_event<B>(
9763    bridge: &SharedBridge<B>,
9764    vm_id: &str,
9765    hostname: &str,
9766    source: KernelDnsResolutionSource,
9767    addresses: &[IpAddr],
9768    dns: &VmDnsConfig,
9769) where
9770    B: NativeSidecarBridge + Send + 'static,
9771    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9772{
9773    let _ = emit_structured_event(
9774        bridge,
9775        vm_id,
9776        "network.dns.resolved",
9777        audit_fields([
9778            ("hostname", hostname.to_owned()),
9779            ("source", source.as_str().to_owned()),
9780            (
9781                "addresses",
9782                addresses
9783                    .iter()
9784                    .map(ToString::to_string)
9785                    .collect::<Vec<_>>()
9786                    .join(","),
9787            ),
9788            ("address_count", addresses.len().to_string()),
9789            ("resolver_count", dns.name_servers.len().to_string()),
9790            (
9791                "resolvers",
9792                dns.name_servers
9793                    .iter()
9794                    .map(ToString::to_string)
9795                    .collect::<Vec<_>>()
9796                    .join(","),
9797            ),
9798        ]),
9799    );
9800}
9801
9802fn emit_dns_record_resolution_event<B>(
9803    bridge: &SharedBridge<B>,
9804    vm_id: &str,
9805    hostname: &str,
9806    resolution: &DnsRecordResolution,
9807    dns: &VmDnsConfig,
9808) where
9809    B: NativeSidecarBridge + Send + 'static,
9810    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9811{
9812    if let Some(addresses) = dns_resolution_ip_addrs(resolution.records()) {
9813        emit_dns_resolution_event(
9814            bridge,
9815            vm_id,
9816            hostname,
9817            resolution.source(),
9818            &addresses,
9819            dns,
9820        );
9821        return;
9822    }
9823
9824    let _ = emit_structured_event(
9825        bridge,
9826        vm_id,
9827        "network.dns.resolved",
9828        audit_fields([
9829            ("hostname", hostname.to_owned()),
9830            ("source", resolution.source().as_str().to_owned()),
9831            (
9832                "addresses",
9833                resolution
9834                    .records()
9835                    .iter()
9836                    .map(summarize_dns_record)
9837                    .collect::<Vec<_>>()
9838                    .join(","),
9839            ),
9840            ("address_count", resolution.records().len().to_string()),
9841            ("resolver_count", dns.name_servers.len().to_string()),
9842            (
9843                "resolvers",
9844                dns.name_servers
9845                    .iter()
9846                    .map(ToString::to_string)
9847                    .collect::<Vec<_>>()
9848                    .join(","),
9849            ),
9850        ]),
9851    );
9852}
9853
9854fn emit_dns_resolution_failure_event<B>(
9855    bridge: &SharedBridge<B>,
9856    vm_id: &str,
9857    hostname: &str,
9858    dns: &VmDnsConfig,
9859    error: &SidecarError,
9860) where
9861    B: NativeSidecarBridge + Send + 'static,
9862    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9863{
9864    let _ = emit_structured_event(
9865        bridge,
9866        vm_id,
9867        "network.dns.resolve_failed",
9868        audit_fields([
9869            ("hostname", hostname.to_owned()),
9870            ("reason", error.to_string()),
9871            ("resolver_count", dns.name_servers.len().to_string()),
9872            (
9873                "resolvers",
9874                dns.name_servers
9875                    .iter()
9876                    .map(ToString::to_string)
9877                    .collect::<Vec<_>>()
9878                    .join(","),
9879            ),
9880        ]),
9881    );
9882}
9883
9884fn parse_dns_record_type(rrtype: &str) -> Result<RecordType, SidecarError> {
9885    match rrtype {
9886        "A" => Ok(RecordType::A),
9887        "AAAA" => Ok(RecordType::AAAA),
9888        "MX" => Ok(RecordType::MX),
9889        "TXT" => Ok(RecordType::TXT),
9890        "SRV" => Ok(RecordType::SRV),
9891        "CNAME" => Ok(RecordType::CNAME),
9892        "PTR" => Ok(RecordType::PTR),
9893        "NS" => Ok(RecordType::NS),
9894        "SOA" => Ok(RecordType::SOA),
9895        "NAPTR" => Ok(RecordType::NAPTR),
9896        "CAA" => Ok(RecordType::CAA),
9897        "ANY" => Ok(RecordType::ANY),
9898        other => Err(SidecarError::Execution(format!(
9899            "ERR_NOT_IMPLEMENTED: dns rrtype {other} is not supported by the secure-exec dns bridge"
9900        ))),
9901    }
9902}
9903
9904fn dns_resolution_to_node_value(
9905    resolution: &DnsRecordResolution,
9906    requested_type: &str,
9907) -> Result<Value, SidecarError> {
9908    let safe_ips = dns_resolution_safe_ip_set(resolution.records(), resolution.hostname())?;
9909    match requested_type {
9910        "A" | "AAAA" => Ok(Value::Array(
9911            resolution
9912                .records()
9913                .iter()
9914                .filter_map(|record| dns_record_ip_string(record, &safe_ips))
9915                .map(Value::String)
9916                .collect(),
9917        )),
9918        "MX" => Ok(Value::Array(
9919            resolution
9920                .records()
9921                .iter()
9922                .filter_map(|record| match record.data() {
9923                    RData::MX(mx) => Some(json!({
9924                        "priority": mx.preference,
9925                        "exchange": normalize_dns_name_for_node(&mx.exchange),
9926                        "type": "MX",
9927                    })),
9928                    _ => None,
9929                })
9930                .collect(),
9931        )),
9932        "TXT" => Ok(Value::Array(
9933            resolution
9934                .records()
9935                .iter()
9936                .filter_map(|record| match record.data() {
9937                    RData::TXT(txt) => Some(Value::Array(
9938                        txt.txt_data
9939                            .iter()
9940                            .map(|entry| Value::String(String::from_utf8_lossy(entry).into_owned()))
9941                            .collect(),
9942                    )),
9943                    _ => None,
9944                })
9945                .collect(),
9946        )),
9947        "SRV" => Ok(Value::Array(
9948            resolution
9949                .records()
9950                .iter()
9951                .filter_map(|record| match record.data() {
9952                    RData::SRV(srv) => Some(json!({
9953                        "priority": srv.priority,
9954                        "weight": srv.weight,
9955                        "port": srv.port,
9956                        "name": normalize_dns_name_for_node(&srv.target),
9957                        "type": "SRV",
9958                    })),
9959                    _ => None,
9960                })
9961                .collect(),
9962        )),
9963        "CNAME" => Ok(Value::Array(
9964            resolution
9965                .records()
9966                .iter()
9967                .filter_map(|record| match record.data() {
9968                    RData::CNAME(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9969                    _ => None,
9970                })
9971                .collect(),
9972        )),
9973        "PTR" => Ok(Value::Array(
9974            resolution
9975                .records()
9976                .iter()
9977                .filter_map(|record| match record.data() {
9978                    RData::PTR(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9979                    _ => None,
9980                })
9981                .collect(),
9982        )),
9983        "NS" => Ok(Value::Array(
9984            resolution
9985                .records()
9986                .iter()
9987                .filter_map(|record| match record.data() {
9988                    RData::NS(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9989                    _ => None,
9990                })
9991                .collect(),
9992        )),
9993        "SOA" => resolution
9994            .records()
9995            .iter()
9996            .find_map(|record| match record.data() {
9997                RData::SOA(soa) => Some(json!({
9998                    "nsname": normalize_dns_name_for_node(&soa.mname),
9999                    "hostmaster": normalize_dns_name_for_node(&soa.rname),
10000                    "serial": soa.serial,
10001                    "refresh": soa.refresh,
10002                    "retry": soa.retry,
10003                    "expire": soa.expire,
10004                    "minttl": soa.minimum,
10005                })),
10006                _ => None,
10007            })
10008            .ok_or_else(|| {
10009                SidecarError::Execution(String::from("failed to resolve DNS SOA record"))
10010            }),
10011        "NAPTR" => Ok(Value::Array(
10012            resolution
10013                .records()
10014                .iter()
10015                .filter_map(|record| match record.data() {
10016                    RData::NAPTR(naptr) => Some(json!({
10017                        "flags": String::from_utf8_lossy(&naptr.flags).into_owned(),
10018                        "service": String::from_utf8_lossy(&naptr.services).into_owned(),
10019                        "regexp": String::from_utf8_lossy(&naptr.regexp).into_owned(),
10020                        "replacement": normalize_dns_name_for_node(&naptr.replacement),
10021                        "order": naptr.order,
10022                        "preference": naptr.preference,
10023                    })),
10024                    _ => None,
10025                })
10026                .collect(),
10027        )),
10028        "CAA" => Ok(Value::Array(
10029            resolution
10030                .records()
10031                .iter()
10032                .filter_map(|record| match record.data() {
10033                    RData::CAA(caa) => {
10034                        let mut value = serde_json::Map::new();
10035                        value.insert(
10036                            "critical".to_owned(),
10037                            Value::from(u8::from(caa.issuer_critical)),
10038                        );
10039                        value.insert("type".to_owned(), Value::String(String::from("CAA")));
10040                        if caa.tag.eq_ignore_ascii_case("iodef") {
10041                            value.insert(
10042                                "iodef".to_owned(),
10043                                Value::String(
10044                                    caa.value_as_iodef()
10045                                        .map(|url| url.to_string())
10046                                        .unwrap_or_else(|_| {
10047                                            String::from_utf8_lossy(&caa.value).into_owned()
10048                                        }),
10049                                ),
10050                            );
10051                        } else if let Ok((issuer, _params)) = caa.value_as_issue() {
10052                            let field = if caa.tag.eq_ignore_ascii_case("issuewild") {
10053                                "issuewild"
10054                            } else {
10055                                "issue"
10056                            };
10057                            value.insert(
10058                                field.to_owned(),
10059                                Value::String(
10060                                    issuer.as_ref().map(ToString::to_string).unwrap_or_else(|| {
10061                                        String::from_utf8_lossy(&caa.value).into_owned()
10062                                    }),
10063                                ),
10064                            );
10065                        } else {
10066                            value.insert(
10067                                caa.tag.to_ascii_lowercase(),
10068                                Value::String(String::from_utf8_lossy(&caa.value).into_owned()),
10069                            );
10070                        }
10071                        Some(Value::Object(value))
10072                    }
10073                    _ => None,
10074                })
10075                .collect(),
10076        )),
10077        "ANY" => Ok(Value::Array(
10078            resolution
10079                .records()
10080                .iter()
10081                .filter_map(|record| dns_any_record_to_value(record, &safe_ips))
10082                .collect(),
10083        )),
10084        other => Err(SidecarError::Execution(format!(
10085            "ERR_NOT_IMPLEMENTED: dns rrtype {other} is not supported by the secure-exec dns bridge"
10086        ))),
10087    }
10088}
10089
10090fn dns_resolution_safe_ip_set(
10091    records: &[Record],
10092    hostname: &str,
10093) -> Result<BTreeSet<IpAddr>, SidecarError> {
10094    let ips = records
10095        .iter()
10096        .filter_map(dns_record_ip_addr)
10097        .collect::<Vec<_>>();
10098    if ips.is_empty() {
10099        return Ok(BTreeSet::new());
10100    }
10101    Ok(filter_dns_safe_ip_addrs(ips, hostname)?
10102        .into_iter()
10103        .collect())
10104}
10105
10106fn dns_resolution_ip_addrs(records: &[Record]) -> Option<Vec<IpAddr>> {
10107    let ips = records
10108        .iter()
10109        .filter_map(dns_record_ip_addr)
10110        .collect::<Vec<_>>();
10111    if ips.is_empty() {
10112        return None;
10113    }
10114    Some(ips)
10115}
10116
10117fn dns_record_ip_addr(record: &Record) -> Option<IpAddr> {
10118    match record.data() {
10119        RData::A(address) => Some(IpAddr::V4(**address)),
10120        RData::AAAA(address) => Some(IpAddr::V6(**address)),
10121        _ => None,
10122    }
10123}
10124
10125fn dns_record_ip_string(record: &Record, safe_ips: &BTreeSet<IpAddr>) -> Option<String> {
10126    let ip = dns_record_ip_addr(record)?;
10127    safe_ips.contains(&ip).then(|| ip.to_string())
10128}
10129
10130fn dns_any_record_to_value(record: &Record, safe_ips: &BTreeSet<IpAddr>) -> Option<Value> {
10131    let value = match record.data() {
10132        RData::A(_) | RData::AAAA(_) => json!({
10133            "address": dns_record_ip_string(record, safe_ips)?,
10134            "ttl": record.ttl(),
10135            "type": record.record_type().to_string(),
10136        }),
10137        RData::MX(mx) => json!({
10138            "exchange": normalize_dns_name_for_node(&mx.exchange),
10139            "priority": mx.preference,
10140            "type": "MX",
10141        }),
10142        RData::TXT(txt) => json!({
10143            "entries": txt
10144                .txt_data
10145                .iter()
10146                .map(|entry| String::from_utf8_lossy(entry).into_owned())
10147                .collect::<Vec<_>>(),
10148            "type": "TXT",
10149        }),
10150        RData::SRV(srv) => json!({
10151            "name": normalize_dns_name_for_node(&srv.target),
10152            "port": srv.port,
10153            "priority": srv.priority,
10154            "weight": srv.weight,
10155            "type": "SRV",
10156        }),
10157        RData::CNAME(name) => json!({
10158            "value": normalize_dns_name_for_node(&name.0),
10159            "type": "CNAME",
10160        }),
10161        RData::PTR(name) => json!({
10162            "value": normalize_dns_name_for_node(&name.0),
10163            "type": "PTR",
10164        }),
10165        RData::NS(name) => json!({
10166            "value": normalize_dns_name_for_node(&name.0),
10167            "type": "NS",
10168        }),
10169        RData::SOA(soa) => json!({
10170            "nsname": normalize_dns_name_for_node(&soa.mname),
10171            "hostmaster": normalize_dns_name_for_node(&soa.rname),
10172            "serial": soa.serial,
10173            "refresh": soa.refresh,
10174            "retry": soa.retry,
10175            "expire": soa.expire,
10176            "minttl": soa.minimum,
10177            "type": "SOA",
10178        }),
10179        RData::NAPTR(naptr) => json!({
10180            "flags": String::from_utf8_lossy(&naptr.flags).into_owned(),
10181            "service": String::from_utf8_lossy(&naptr.services).into_owned(),
10182            "regexp": String::from_utf8_lossy(&naptr.regexp).into_owned(),
10183            "replacement": normalize_dns_name_for_node(&naptr.replacement),
10184            "order": naptr.order,
10185            "preference": naptr.preference,
10186            "type": "NAPTR",
10187        }),
10188        RData::CAA(caa) => {
10189            let mut value = serde_json::Map::new();
10190            value.insert(
10191                "critical".to_owned(),
10192                Value::from(u8::from(caa.issuer_critical)),
10193            );
10194            value.insert("type".to_owned(), Value::String(String::from("CAA")));
10195            if caa.tag.eq_ignore_ascii_case("iodef") {
10196                value.insert(
10197                    "iodef".to_owned(),
10198                    Value::String(
10199                        caa.value_as_iodef()
10200                            .map(|url| url.to_string())
10201                            .unwrap_or_else(|_| String::from_utf8_lossy(&caa.value).into_owned()),
10202                    ),
10203                );
10204            } else if let Ok((issuer, _params)) = caa.value_as_issue() {
10205                let field = if caa.tag.eq_ignore_ascii_case("issuewild") {
10206                    "issuewild"
10207                } else {
10208                    "issue"
10209                };
10210                value.insert(
10211                    field.to_owned(),
10212                    Value::String(
10213                        issuer
10214                            .as_ref()
10215                            .map(ToString::to_string)
10216                            .unwrap_or_else(|| String::from_utf8_lossy(&caa.value).into_owned()),
10217                    ),
10218                );
10219            }
10220            Value::Object(value)
10221        }
10222        _ => return None,
10223    };
10224    Some(value)
10225}
10226
10227fn normalize_dns_name_for_node(name: &impl ToString) -> String {
10228    name.to_string().trim_end_matches('.').to_owned()
10229}
10230
10231fn summarize_dns_record(record: &Record) -> String {
10232    match record.data() {
10233        RData::A(_) | RData::AAAA(_) => record.data().to_string(),
10234        _ => format!("{} {}", record.record_type(), record.data()),
10235    }
10236}
10237
10238// build_root_filesystem, convert_root_lower_descriptor, convert_root_filesystem_entry,
10239// root_snapshot_entry moved to crate::bootstrap
10240
10241// apply_root_filesystem_entry, ensure_parent_directories moved to crate::bootstrap
10242
10243// ProcNetEntry moved to crate::state
10244
10245fn find_socket_state_entry(
10246    vm: Option<&VmState>,
10247    kind: SocketQueryKind,
10248    request: &FindListenerRequest,
10249) -> Result<Option<SocketStateEntry>, SidecarError> {
10250    let vm = vm.ok_or_else(|| SidecarError::InvalidState(String::from("unknown sidecar VM")))?;
10251
10252    for (process_id, process) in &vm.active_processes {
10253        if let Some(path) = request.path.as_deref() {
10254            if matches!(kind, SocketQueryKind::TcpListener) {
10255                for listener in process.unix_listeners.values() {
10256                    if listener.path() != path {
10257                        continue;
10258                    }
10259                    return Ok(Some(SocketStateEntry {
10260                        process_id: process_id.to_owned(),
10261                        host: None,
10262                        port: None,
10263                        path: Some(path.to_owned()),
10264                    }));
10265                }
10266            }
10267        }
10268
10269        if request.path.is_none() {
10270            if let Some(entry) =
10271                find_kernel_socket_state_entry(&vm.kernel, process_id, process, kind, request)?
10272            {
10273                return Ok(Some(entry));
10274            }
10275
10276            match kind {
10277                SocketQueryKind::TcpListener => {
10278                    for server in process.http_servers.values() {
10279                        let local_addr = server.guest_local_addr;
10280                        let local_host = local_addr.ip().to_string();
10281                        if !socket_host_matches(request.host.as_deref(), &local_host) {
10282                            continue;
10283                        }
10284                        if let Some(port) = request.port {
10285                            if local_addr.port() != port {
10286                                continue;
10287                            }
10288                        }
10289                        return Ok(Some(SocketStateEntry {
10290                            process_id: process_id.to_owned(),
10291                            host: Some(local_host),
10292                            port: Some(local_addr.port()),
10293                            path: None,
10294                        }));
10295                    }
10296
10297                    for listener in process.tcp_listeners.values() {
10298                        if listener.kernel_socket_id.is_some() {
10299                            continue;
10300                        }
10301                        let local_addr = listener.guest_local_addr();
10302                        let local_host = local_addr.ip().to_string();
10303                        if !socket_host_matches(request.host.as_deref(), &local_host) {
10304                            continue;
10305                        }
10306                        if let Some(port) = request.port {
10307                            if local_addr.port() != port {
10308                                continue;
10309                            }
10310                        }
10311                        return Ok(Some(SocketStateEntry {
10312                            process_id: process_id.to_owned(),
10313                            host: Some(local_host),
10314                            port: Some(local_addr.port()),
10315                            path: None,
10316                        }));
10317                    }
10318                }
10319                SocketQueryKind::UdpBound => {
10320                    for socket in process.udp_sockets.values() {
10321                        if socket.kernel_socket_id.is_some() {
10322                            continue;
10323                        }
10324                        let Some(local_addr) = socket.local_addr() else {
10325                            continue;
10326                        };
10327                        let local_host = local_addr.ip().to_string();
10328                        if !socket_host_matches(request.host.as_deref(), &local_host) {
10329                            continue;
10330                        }
10331                        if let Some(port) = request.port {
10332                            if local_addr.port() != port {
10333                                continue;
10334                            }
10335                        }
10336                        return Ok(Some(SocketStateEntry {
10337                            process_id: process_id.to_owned(),
10338                            host: Some(local_host),
10339                            port: Some(local_addr.port()),
10340                            path: None,
10341                        }));
10342                    }
10343                }
10344            }
10345        }
10346
10347        let child_pid = process.execution.child_pid();
10348        let inodes = socket_inodes_for_pid(child_pid)?;
10349        if inodes.is_empty() {
10350            continue;
10351        }
10352
10353        if let Some(path) = request.path.as_deref() {
10354            if let Some(listener) = find_unix_socket_for_pid(child_pid, &inodes, path, process_id)?
10355            {
10356                return Ok(Some(listener));
10357            }
10358            continue;
10359        }
10360
10361        let table_paths = match kind {
10362            SocketQueryKind::TcpListener => [
10363                format!("/proc/{child_pid}/net/tcp"),
10364                format!("/proc/{child_pid}/net/tcp6"),
10365            ],
10366            SocketQueryKind::UdpBound => [
10367                format!("/proc/{child_pid}/net/udp"),
10368                format!("/proc/{child_pid}/net/udp6"),
10369            ],
10370        };
10371        for table_path in table_paths {
10372            if let Some(entry) = find_inet_socket_for_pid(
10373                &table_path,
10374                &inodes,
10375                kind,
10376                request.host.as_deref(),
10377                request.port,
10378                process_id,
10379            )? {
10380                return Ok(Some(entry));
10381            }
10382        }
10383    }
10384
10385    Ok(None)
10386}
10387
10388fn require_vm_inspection_permission<B>(
10389    bridge: &SharedBridge<B>,
10390    vm_id: &str,
10391    capability: &str,
10392    domain: &str,
10393    resource: &str,
10394) -> Result<(), SidecarError>
10395where
10396    B: NativeSidecarBridge + Send + 'static,
10397    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
10398{
10399    let decision = bridge.static_permission_decision(vm_id, capability, domain, Some(resource));
10400    if decision.as_ref().is_some_and(|decision| decision.allow) {
10401        return Ok(());
10402    }
10403
10404    let reason = decision
10405        .and_then(|decision| decision.reason)
10406        .unwrap_or_else(|| format!("{capability} permission required"));
10407    Err(SidecarError::Execution(format!(
10408        "EACCES: permission denied, {resource}: {reason}"
10409    )))
10410}
10411
10412fn socket_query_resource(kind: SocketQueryKind, request: &FindListenerRequest) -> String {
10413    if let Some(path) = request.path.as_deref() {
10414        return format!("unix://{path}");
10415    }
10416
10417    let host = request.host.as_deref().unwrap_or("*");
10418    let port = request
10419        .port
10420        .map_or_else(|| String::from("*"), |port| port.to_string());
10421    match kind {
10422        SocketQueryKind::TcpListener => format!("tcp://{host}:{port}"),
10423        SocketQueryKind::UdpBound => format!("udp://{host}:{port}"),
10424    }
10425}
10426
10427fn snapshot_vm_processes(vm: &VmState) -> Vec<ProcessSnapshotEntry> {
10428    let process_table = vm.kernel.list_processes();
10429    snapshot_vm_processes_inner(vm, &process_table)
10430}
10431
10432fn snapshot_vm_processes_inner(
10433    vm: &VmState,
10434    process_table: &BTreeMap<u32, secure_exec_kernel::process_table::ProcessInfo>,
10435) -> Vec<ProcessSnapshotEntry> {
10436    let mut entries = Vec::new();
10437
10438    for (process_id, process) in &vm.active_processes {
10439        collect_process_snapshot_entries(process_id, process, process_table, &mut entries);
10440    }
10441
10442    for exited in &vm.exited_process_snapshots {
10443        entries.push(exited.process.clone());
10444    }
10445
10446    entries
10447}
10448
10449fn prune_exited_process_snapshots(vm: &mut VmState) {
10450    let cutoff = Instant::now() - EXITED_PROCESS_SNAPSHOT_RETENTION;
10451    while vm
10452        .exited_process_snapshots
10453        .front()
10454        .is_some_and(|snapshot| snapshot.captured_at < cutoff)
10455    {
10456        vm.exited_process_snapshots.pop_front();
10457    }
10458}
10459
10460fn build_process_snapshot_entry(
10461    process_id: &str,
10462    process: &ActiveProcess,
10463    info: &secure_exec_kernel::process_table::ProcessInfo,
10464    exit_code: Option<i32>,
10465) -> ProcessSnapshotEntry {
10466    ProcessSnapshotEntry {
10467        process_id: process_id.to_owned(),
10468        pid: info.pid,
10469        ppid: info.ppid,
10470        pgid: info.pgid,
10471        sid: info.sid,
10472        driver: info.driver.clone(),
10473        command: info.command.clone(),
10474        args: Vec::new(),
10475        cwd: process.guest_cwd.clone(),
10476        status: if exit_code.is_some() {
10477            ProcessSnapshotStatus::Exited
10478        } else {
10479            match info.status {
10480                ProcessStatus::Running => ProcessSnapshotStatus::Running,
10481                ProcessStatus::Stopped => ProcessSnapshotStatus::Stopped,
10482                ProcessStatus::Exited => ProcessSnapshotStatus::Exited,
10483            }
10484        },
10485        exit_code: exit_code.or(info.exit_code),
10486    }
10487}
10488
10489fn collect_process_snapshot_entries(
10490    process_id: &str,
10491    process: &ActiveProcess,
10492    process_table: &BTreeMap<u32, secure_exec_kernel::process_table::ProcessInfo>,
10493    entries: &mut Vec<ProcessSnapshotEntry>,
10494) {
10495    if let Some(info) = process_table.get(&process.kernel_pid) {
10496        entries.push(build_process_snapshot_entry(
10497            process_id, process, info, None,
10498        ));
10499    }
10500
10501    for (child_id, child) in &process.child_processes {
10502        let child_process_id = format!("{process_id}/{child_id}");
10503        collect_process_snapshot_entries(&child_process_id, child, process_table, entries);
10504    }
10505}
10506
10507fn find_kernel_socket_state_entry(
10508    kernel: &SidecarKernel,
10509    process_id: &str,
10510    process: &ActiveProcess,
10511    kind: SocketQueryKind,
10512    request: &FindListenerRequest,
10513) -> Result<Option<SocketStateEntry>, SidecarError> {
10514    let entry = match kind {
10515        SocketQueryKind::TcpListener => process
10516            .tcp_listeners
10517            .values()
10518            .filter_map(|listener| listener.kernel_socket_id)
10519            .find_map(|socket_id| {
10520                kernel_socket_state_entry(kernel, process_id, socket_id, kind, request)
10521            }),
10522        SocketQueryKind::UdpBound => process
10523            .udp_sockets
10524            .values()
10525            .filter_map(|socket| socket.kernel_socket_id)
10526            .find_map(|socket_id| {
10527                kernel_socket_state_entry(kernel, process_id, socket_id, kind, request)
10528            }),
10529    };
10530
10531    if entry.is_some() {
10532        return Ok(entry);
10533    }
10534
10535    for child in process.child_processes.values() {
10536        if let Some(entry) =
10537            find_kernel_socket_state_entry(kernel, process_id, child, kind, request)?
10538        {
10539            return Ok(Some(entry));
10540        }
10541    }
10542
10543    Ok(None)
10544}
10545
10546fn kernel_socket_state_entry(
10547    kernel: &SidecarKernel,
10548    process_id: &str,
10549    socket_id: SocketId,
10550    kind: SocketQueryKind,
10551    request: &FindListenerRequest,
10552) -> Option<SocketStateEntry> {
10553    let record = kernel.socket_get(socket_id)?;
10554    let local_address = record.local_address()?;
10555    match kind {
10556        SocketQueryKind::TcpListener if record.state() == SocketState::Listening => {}
10557        SocketQueryKind::TcpListener => return None,
10558        SocketQueryKind::UdpBound => {}
10559    }
10560
10561    if !socket_host_matches(request.host.as_deref(), local_address.host()) {
10562        return None;
10563    }
10564    if request
10565        .port
10566        .is_some_and(|port| local_address.port() != port)
10567    {
10568        return None;
10569    }
10570
10571    Some(SocketStateEntry {
10572        process_id: process_id.to_owned(),
10573        host: Some(local_address.host().to_owned()),
10574        port: Some(local_address.port()),
10575        path: None,
10576    })
10577}
10578
10579fn socket_inodes_for_pid(pid: u32) -> Result<BTreeSet<u64>, SidecarError> {
10580    let fd_dir = PathBuf::from(format!("/proc/{pid}/fd"));
10581    let entries = match fs::read_dir(&fd_dir) {
10582        Ok(entries) => entries,
10583        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(BTreeSet::new()),
10584        Err(error) => {
10585            return Err(SidecarError::Io(format!(
10586                "failed to read socket descriptors for process {pid}: {error}"
10587            )));
10588        }
10589    };
10590
10591    let mut inodes = BTreeSet::new();
10592    for entry in entries {
10593        let entry = entry.map_err(|error| {
10594            SidecarError::Io(format!(
10595                "failed to inspect fd entry for process {pid}: {error}"
10596            ))
10597        })?;
10598        let target = match fs::read_link(entry.path()) {
10599            Ok(target) => target,
10600            Err(_) => continue,
10601        };
10602        if let Some(inode) = parse_socket_inode(&target) {
10603            inodes.insert(inode);
10604        }
10605    }
10606
10607    Ok(inodes)
10608}
10609
10610fn parse_socket_inode(target: &Path) -> Option<u64> {
10611    let value = target.to_string_lossy();
10612    let trimmed = value.strip_prefix("socket:[")?.strip_suffix(']')?;
10613    trimmed.parse().ok()
10614}
10615
10616fn unix_socket_path(addr: &UnixSocketAddr) -> Option<String> {
10617    addr.as_pathname()
10618        .map(|path| path.to_string_lossy().into_owned())
10619}
10620
10621fn find_unix_socket_for_pid(
10622    pid: u32,
10623    inodes: &BTreeSet<u64>,
10624    path: &str,
10625    process_id: &str,
10626) -> Result<Option<SocketStateEntry>, SidecarError> {
10627    let table_path = format!("/proc/{pid}/net/unix");
10628    let contents = match fs::read_to_string(&table_path) {
10629        Ok(contents) => contents,
10630        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
10631        Err(error) => {
10632            return Err(SidecarError::Io(format!(
10633                "failed to inspect unix sockets for process {pid}: {error}"
10634            )));
10635        }
10636    };
10637
10638    for line in contents.lines().skip(1) {
10639        let columns = line.split_whitespace().collect::<Vec<_>>();
10640        if columns.len() < 8 {
10641            continue;
10642        }
10643        let Ok(inode) = columns[6].parse::<u64>() else {
10644            continue;
10645        };
10646        if !inodes.contains(&inode) || columns[7] != path {
10647            continue;
10648        }
10649        return Ok(Some(SocketStateEntry {
10650            process_id: process_id.to_owned(),
10651            host: None,
10652            port: None,
10653            path: Some(path.to_owned()),
10654        }));
10655    }
10656
10657    Ok(None)
10658}
10659
10660fn find_inet_socket_for_pid(
10661    table_path: &str,
10662    inodes: &BTreeSet<u64>,
10663    kind: SocketQueryKind,
10664    requested_host: Option<&str>,
10665    requested_port: Option<u16>,
10666    process_id: &str,
10667) -> Result<Option<SocketStateEntry>, SidecarError> {
10668    for entry in parse_proc_net_entries(table_path)? {
10669        if !inodes.contains(&entry.inode) {
10670            continue;
10671        }
10672        if matches!(kind, SocketQueryKind::TcpListener) && entry.state != "0A" {
10673            continue;
10674        }
10675        if !socket_host_matches(requested_host, &entry.local_host) {
10676            continue;
10677        }
10678        if let Some(port) = requested_port {
10679            if entry.local_port != port {
10680                continue;
10681            }
10682        }
10683        return Ok(Some(SocketStateEntry {
10684            process_id: process_id.to_owned(),
10685            host: Some(entry.local_host),
10686            port: Some(entry.local_port),
10687            path: None,
10688        }));
10689    }
10690
10691    Ok(None)
10692}
10693
10694fn is_unspecified_socket_host(host: &str) -> bool {
10695    host == "0.0.0.0" || host == "::"
10696}
10697
10698fn is_loopback_socket_host(host: &str) -> bool {
10699    host == "127.0.0.1" || host == "::1" || host.eq_ignore_ascii_case("localhost")
10700}
10701
10702pub(crate) fn vm_network_resource_counts(vm: &VmState) -> NetworkResourceCounts {
10703    let snapshot = vm.kernel.resource_snapshot();
10704    let mut counts = NetworkResourceCounts {
10705        sockets: snapshot.sockets,
10706        connections: snapshot.socket_connections,
10707    };
10708    for process in vm.active_processes.values() {
10709        let process_counts = process.sidecar_only_network_resource_counts();
10710        counts.sockets += process_counts.sockets;
10711        counts.connections += process_counts.connections;
10712    }
10713    counts
10714}
10715
10716#[allow(clippy::too_many_arguments)]
10717fn collect_javascript_socket_port_state(
10718    kernel: &SidecarKernel,
10719    process_id: &str,
10720    process: &ActiveProcess,
10721    tcp_guest_to_host: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10722    http_loopback_targets: &mut BTreeMap<
10723        (JavascriptSocketFamily, u16),
10724        JavascriptHttpLoopbackTarget,
10725    >,
10726    udp_guest_to_host: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10727    udp_host_to_guest: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10728    used_tcp_ports: &mut BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10729    used_udp_ports: &mut BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10730) {
10731    for (family, port) in process.tcp_port_reservations.values() {
10732        used_tcp_ports.entry(*family).or_default().insert(*port);
10733    }
10734
10735    let mut record_tcp_listener = |guest_addr: SocketAddr, host_port: u16| {
10736        let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10737        used_tcp_ports
10738            .entry(family)
10739            .or_default()
10740            .insert(guest_addr.port());
10741        // VM-local loopback connects should also resolve listeners bound to
10742        // unspecified guest addresses like 0.0.0.0/::.
10743        tcp_guest_to_host.insert((family, guest_addr.port()), host_port);
10744    };
10745
10746    for listener in process.tcp_listeners.values() {
10747        let local_addr = listener
10748            .kernel_socket_id
10749            .and_then(|socket_id| kernel.socket_get(socket_id))
10750            .and_then(|record| record.local_address().cloned())
10751            .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
10752            .unwrap_or_else(|| listener.guest_local_addr());
10753        record_tcp_listener(local_addr, local_addr.port());
10754    }
10755
10756    for (server_id, server) in &process.http_servers {
10757        let host_port = match server.listener.local_addr() {
10758            Ok(addr) => addr.port(),
10759            Err(_) => continue,
10760        };
10761        record_tcp_listener(server.guest_local_addr, host_port);
10762        let family = JavascriptSocketFamily::from_ip(server.guest_local_addr.ip());
10763        http_loopback_targets.insert(
10764            (family, server.guest_local_addr.port()),
10765            JavascriptHttpLoopbackTarget {
10766                process_id: process_id.to_owned(),
10767                server_id: *server_id,
10768            },
10769        );
10770    }
10771
10772    if let Ok(http2) = process.http2.shared.lock() {
10773        for server in http2.servers.values() {
10774            record_tcp_listener(server.guest_local_addr, server.actual_local_addr.port());
10775        }
10776    }
10777
10778    for socket in process.tcp_sockets.values() {
10779        let guest_addr = socket
10780            .kernel_socket_id
10781            .and_then(|socket_id| kernel.socket_get(socket_id))
10782            .and_then(|record| record.local_address().cloned())
10783            .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
10784            .unwrap_or(socket.guest_local_addr);
10785        let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10786        used_tcp_ports
10787            .entry(family)
10788            .or_default()
10789            .insert(guest_addr.port());
10790    }
10791
10792    for socket in process.udp_sockets.values() {
10793        let guest_addr = socket
10794            .kernel_socket_id
10795            .and_then(|socket_id| kernel.socket_get(socket_id))
10796            .and_then(|record| record.local_address().cloned())
10797            .and_then(|address| {
10798                resolve_udp_bind_addr(address.host(), address.port(), socket.family).ok()
10799            })
10800            .or_else(|| socket.local_addr());
10801        let Some(guest_addr) = guest_addr else {
10802            continue;
10803        };
10804        let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10805        used_udp_ports
10806            .entry(family)
10807            .or_default()
10808            .insert(guest_addr.port());
10809        if let Some(host_addr) = socket
10810            .socket
10811            .as_ref()
10812            .and_then(|socket| socket.local_addr().ok())
10813        {
10814            if is_loopback_ip(guest_addr.ip()) {
10815                udp_guest_to_host.insert((family, guest_addr.port()), host_addr.port());
10816                udp_host_to_guest.insert((family, host_addr.port()), guest_addr.port());
10817            }
10818        } else if socket.kernel_socket_id.is_some() && is_loopback_ip(guest_addr.ip()) {
10819            udp_guest_to_host.insert((family, guest_addr.port()), guest_addr.port());
10820            udp_host_to_guest.insert((family, guest_addr.port()), guest_addr.port());
10821        }
10822    }
10823
10824    for (child_process_id, child) in &process.child_processes {
10825        let child_id = format!("{process_id}/{child_process_id}");
10826        collect_javascript_socket_port_state(
10827            kernel,
10828            &child_id,
10829            child,
10830            tcp_guest_to_host,
10831            http_loopback_targets,
10832            udp_guest_to_host,
10833            udp_host_to_guest,
10834            used_tcp_ports,
10835            used_udp_ports,
10836        );
10837    }
10838}
10839
10840pub(crate) fn build_javascript_socket_path_context(
10841    vm: &VmState,
10842) -> Result<JavascriptSocketPathContext, SidecarError> {
10843    let mut loopback_exempt_ports = vm.create_loopback_exempt_ports.clone();
10844    loopback_exempt_ports.extend(vm.configuration.loopback_exempt_ports.iter().copied());
10845    let mut tcp_loopback_guest_to_host_ports = BTreeMap::new();
10846    let mut http_loopback_targets = BTreeMap::new();
10847    let mut udp_loopback_guest_to_host_ports = BTreeMap::new();
10848    let mut udp_loopback_host_to_guest_ports = BTreeMap::new();
10849    let mut used_tcp_guest_ports = BTreeMap::new();
10850    let mut used_udp_guest_ports = BTreeMap::new();
10851    for (process_id, process) in &vm.active_processes {
10852        collect_javascript_socket_port_state(
10853            &vm.kernel,
10854            process_id,
10855            process,
10856            &mut tcp_loopback_guest_to_host_ports,
10857            &mut http_loopback_targets,
10858            &mut udp_loopback_guest_to_host_ports,
10859            &mut udp_loopback_host_to_guest_ports,
10860            &mut used_tcp_guest_ports,
10861            &mut used_udp_guest_ports,
10862        );
10863    }
10864    Ok(JavascriptSocketPathContext {
10865        sandbox_root: vm.cwd.clone(),
10866        mounts: vm.configuration.mounts.clone(),
10867        listen_policy: vm.listen_policy,
10868        loopback_exempt_ports,
10869        tcp_loopback_guest_to_host_ports,
10870        http_loopback_targets,
10871        udp_loopback_guest_to_host_ports,
10872        udp_loopback_host_to_guest_ports,
10873        used_tcp_guest_ports,
10874        used_udp_guest_ports,
10875    })
10876}
10877
10878fn check_network_resource_limit(
10879    limit: Option<usize>,
10880    current: usize,
10881    additional: usize,
10882    label: &str,
10883) -> Result<(), SidecarError> {
10884    if let Some(limit) = limit {
10885        if current.saturating_add(additional) > limit {
10886            return Err(SidecarError::Execution(format!(
10887                "EAGAIN: maximum {label} count reached"
10888            )));
10889        }
10890    }
10891    Ok(())
10892}
10893
10894fn normalize_tcp_listen_host(
10895    host: Option<&str>,
10896) -> Result<(JavascriptSocketFamily, &'static str, &'static str), SidecarError> {
10897    match host.unwrap_or("127.0.0.1") {
10898        "127.0.0.1" | "localhost" => Ok((JavascriptSocketFamily::Ipv4, "127.0.0.1", "127.0.0.1")),
10899        "::1" => Ok((JavascriptSocketFamily::Ipv6, "::1", "::1")),
10900        "0.0.0.0" => Ok((JavascriptSocketFamily::Ipv4, "127.0.0.1", "0.0.0.0")),
10901        "::" => Ok((JavascriptSocketFamily::Ipv6, "::1", "::")),
10902        other => Err(SidecarError::Execution(format!(
10903            "EACCES: TCP listeners must bind to loopback or unspecified addresses, got {other}"
10904        ))),
10905    }
10906}
10907
10908fn normalize_udp_bind_host(
10909    host: Option<&str>,
10910    family: JavascriptUdpFamily,
10911) -> Result<(&'static str, &'static str, JavascriptSocketFamily), SidecarError> {
10912    match (family, host) {
10913        (JavascriptUdpFamily::Ipv4, None) | (JavascriptUdpFamily::Ipv4, Some("0.0.0.0")) => {
10914            Ok(("127.0.0.1", "0.0.0.0", JavascriptSocketFamily::Ipv4))
10915        }
10916        (JavascriptUdpFamily::Ipv4, Some("127.0.0.1"))
10917        | (JavascriptUdpFamily::Ipv4, Some("localhost")) => {
10918            Ok(("127.0.0.1", "127.0.0.1", JavascriptSocketFamily::Ipv4))
10919        }
10920        (JavascriptUdpFamily::Ipv6, None) | (JavascriptUdpFamily::Ipv6, Some("::")) => {
10921            Ok(("::1", "::", JavascriptSocketFamily::Ipv6))
10922        }
10923        (JavascriptUdpFamily::Ipv6, Some("::1"))
10924        | (JavascriptUdpFamily::Ipv6, Some("localhost")) => {
10925            Ok(("::1", "::1", JavascriptSocketFamily::Ipv6))
10926        }
10927        (JavascriptUdpFamily::Ipv4, Some(other)) => Err(SidecarError::Execution(format!(
10928            "EACCES: udp4 sockets must bind to 127.0.0.1 or 0.0.0.0, got {other}"
10929        ))),
10930        (JavascriptUdpFamily::Ipv6, Some(other)) => Err(SidecarError::Execution(format!(
10931            "EACCES: udp6 sockets must bind to ::1 or ::, got {other}"
10932        ))),
10933    }
10934}
10935
10936fn allocate_guest_listen_port(
10937    requested_port: u16,
10938    family: JavascriptSocketFamily,
10939    used_ports: &BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10940    policy: VmListenPolicy,
10941) -> Result<u16, SidecarError> {
10942    let is_allowed = |port: u16| {
10943        port >= policy.port_min
10944            && port <= policy.port_max
10945            && (policy.allow_privileged || port >= 1024)
10946    };
10947    let used = used_ports.get(&family);
10948
10949    if requested_port != 0 {
10950        if !is_allowed(requested_port) {
10951            let reason = if requested_port < 1024 && !policy.allow_privileged {
10952                format!(
10953                    "EACCES: privileged listen port {requested_port} requires {}=true",
10954                    VM_LISTEN_ALLOW_PRIVILEGED_METADATA_KEY
10955                )
10956            } else {
10957                format!(
10958                    "EACCES: listen port {requested_port} is outside the allowed range {}-{}",
10959                    policy.port_min, policy.port_max
10960                )
10961            };
10962            return Err(SidecarError::Execution(reason));
10963        }
10964        if used.is_some_and(|ports| ports.contains(&requested_port)) {
10965            return Err(sidecar_net_error(std::io::Error::from_raw_os_error(
10966                libc::EADDRINUSE,
10967            )));
10968        }
10969        return Ok(requested_port);
10970    }
10971
10972    let allocation_start = policy
10973        .port_min
10974        .max(if policy.allow_privileged { 1 } else { 1024 });
10975    for candidate in allocation_start..=policy.port_max {
10976        if used.is_some_and(|ports| ports.contains(&candidate)) {
10977            continue;
10978        }
10979        return Ok(candidate);
10980    }
10981
10982    Err(sidecar_net_error(std::io::Error::from_raw_os_error(
10983        libc::EADDRINUSE,
10984    )))
10985}
10986
10987fn socket_host_matches(requested: Option<&str>, actual: &str) -> bool {
10988    match requested {
10989        None => true,
10990        Some(requested) if requested == actual => true,
10991        Some(requested)
10992            if is_unspecified_socket_host(requested) && is_unspecified_socket_host(actual) =>
10993        {
10994            true
10995        }
10996        Some(requested) if is_unspecified_socket_host(requested) => is_loopback_socket_host(actual),
10997        Some(requested) if requested.eq_ignore_ascii_case("localhost") => {
10998            is_loopback_socket_host(actual)
10999        }
11000        _ => false,
11001    }
11002}
11003
11004fn parse_proc_net_entries(table_path: &str) -> Result<Vec<ProcNetEntry>, SidecarError> {
11005    let contents = match fs::read_to_string(table_path) {
11006        Ok(contents) => contents,
11007        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
11008        Err(error) => {
11009            return Err(SidecarError::Io(format!(
11010                "failed to inspect socket table {table_path}: {error}"
11011            )));
11012        }
11013    };
11014
11015    let mut entries = Vec::new();
11016    for line in contents.lines().skip(1) {
11017        let columns = line.split_whitespace().collect::<Vec<_>>();
11018        if columns.len() < 10 {
11019            continue;
11020        }
11021        let Some((host, port)) = parse_proc_ip_port(columns[1]) else {
11022            continue;
11023        };
11024        let Ok(inode) = columns[9].parse::<u64>() else {
11025            continue;
11026        };
11027        entries.push(ProcNetEntry {
11028            local_host: host,
11029            local_port: port,
11030            state: columns[3].to_owned(),
11031            inode,
11032        });
11033    }
11034
11035    Ok(entries)
11036}
11037
11038fn parse_proc_ip_port(value: &str) -> Option<(String, u16)> {
11039    let (raw_ip, raw_port) = value.split_once(':')?;
11040    let port = u16::from_str_radix(raw_port, 16).ok()?;
11041    let host = match raw_ip.len() {
11042        8 => {
11043            let raw = u32::from_str_radix(raw_ip, 16).ok()?;
11044            Ipv4Addr::from(raw.to_le_bytes()).to_string()
11045        }
11046        32 => {
11047            let mut bytes = [0_u8; 16];
11048            for (index, chunk) in raw_ip.as_bytes().chunks(8).enumerate() {
11049                let word = u32::from_str_radix(std::str::from_utf8(chunk).ok()?, 16).ok()?;
11050                bytes[index * 4..(index + 1) * 4].copy_from_slice(&word.to_le_bytes());
11051            }
11052            Ipv6Addr::from(bytes).to_string()
11053        }
11054        _ => return None,
11055    };
11056    Some((host, port))
11057}
11058
11059fn python_file_entrypoint(entrypoint: &str) -> Option<PathBuf> {
11060    let path = Path::new(entrypoint);
11061    (path.extension().and_then(|extension| extension.to_str()) == Some("py"))
11062        .then(|| path.to_path_buf())
11063}
11064
11065fn add_runtime_guest_path_mapping(
11066    env: &mut BTreeMap<String, String>,
11067    guest_path: &str,
11068    host_path: &Path,
11069) {
11070    let mut mappings = env
11071        .get("AGENTOS_GUEST_PATH_MAPPINGS")
11072        .and_then(|value| serde_json::from_str::<Vec<Value>>(value).ok())
11073        .unwrap_or_default();
11074    mappings.retain(|mapping| {
11075        mapping
11076            .get("guestPath")
11077            .and_then(Value::as_str)
11078            .map(|existing| normalize_path(existing) != normalize_path(guest_path))
11079            .unwrap_or(true)
11080    });
11081    mappings.push(json!({
11082        "guestPath": normalize_path(guest_path),
11083        "hostPath": host_path.display().to_string(),
11084    }));
11085    if let Ok(serialized) = serde_json::to_string(&mappings) {
11086        env.insert(String::from("AGENTOS_GUEST_PATH_MAPPINGS"), serialized);
11087    }
11088}
11089
11090fn add_runtime_host_access_path(
11091    env: &mut BTreeMap<String, String>,
11092    key: &str,
11093    host_path: &Path,
11094    expand: bool,
11095) {
11096    let existing = env
11097        .get(key)
11098        .and_then(|value| serde_json::from_str::<Vec<String>>(value).ok())
11099        .unwrap_or_default()
11100        .into_iter()
11101        .map(PathBuf::from)
11102        .collect::<Vec<_>>();
11103    let mut paths = existing;
11104    paths.push(host_path.to_path_buf());
11105    let normalized = if expand {
11106        expand_host_access_paths(&paths)
11107    } else {
11108        dedupe_host_paths(&paths)
11109    };
11110    let serialized = normalized
11111        .iter()
11112        .map(|path| path.to_string_lossy().into_owned())
11113        .collect::<Vec<_>>();
11114    if let Ok(serialized) = serde_json::to_string(&serialized) {
11115        env.insert(key.to_owned(), serialized);
11116    }
11117}
11118
11119// discover_command_guest_paths moved to crate::bootstrap
11120
11121fn is_path_like_specifier(specifier: &str) -> bool {
11122    specifier.starts_with('/')
11123        || specifier.starts_with("./")
11124        || specifier.starts_with("../")
11125        || specifier.starts_with("file:")
11126}
11127
11128fn execution_wasm_permission_tier(tier: WasmPermissionTier) -> ExecutionWasmPermissionTier {
11129    match tier {
11130        WasmPermissionTier::Full => ExecutionWasmPermissionTier::Full,
11131        WasmPermissionTier::ReadWrite => ExecutionWasmPermissionTier::ReadWrite,
11132        WasmPermissionTier::ReadOnly => ExecutionWasmPermissionTier::ReadOnly,
11133        WasmPermissionTier::Isolated => ExecutionWasmPermissionTier::Isolated,
11134    }
11135}
11136
11137fn resolve_wasm_permission_tier(
11138    vm: &VmState,
11139    command_name: Option<&str>,
11140    explicit_tier: Option<WasmPermissionTier>,
11141    entrypoint: &str,
11142) -> WasmPermissionTier {
11143    explicit_tier
11144        .or_else(|| command_name.and_then(|command| vm.command_permissions.get(command).copied()))
11145        .or_else(|| {
11146            Path::new(entrypoint)
11147                .file_name()
11148                .and_then(|name| name.to_str())
11149                .and_then(|command| vm.command_permissions.get(command).copied())
11150        })
11151        .unwrap_or(WasmPermissionTier::Full)
11152}
11153
11154fn tokenize_shell_free_command(command: &str) -> Vec<String> {
11155    command
11156        .split_whitespace()
11157        .filter(|segment| !segment.is_empty())
11158        .map(str::to_owned)
11159        .collect()
11160}
11161
11162fn is_posix_shell_builtin(command: &str) -> bool {
11163    matches!(
11164        command,
11165        "." | ":"
11166            | "break"
11167            | "cd"
11168            | "continue"
11169            | "eval"
11170            | "exec"
11171            | "exit"
11172            | "export"
11173            | "readonly"
11174            | "return"
11175            | "set"
11176            | "shift"
11177            | "times"
11178            | "trap"
11179            | "umask"
11180            | "unset"
11181    )
11182}
11183
11184/// Single-token checks for shell-mode commands whose first word forces a real
11185/// shell even when the command string has no shell metacharacters. This is not
11186/// a parser: env-assignment prefixes (`FOO=bar cmd`) and shell reserved words
11187/// have no meaning outside `sh`, so whitespace-tokenizing them would silently
11188/// run the wrong program.
11189fn shell_first_token_requires_shell(token: &str) -> bool {
11190    token.contains('=') || is_shell_reserved_word(token)
11191}
11192
11193fn is_shell_reserved_word(token: &str) -> bool {
11194    matches!(
11195        token,
11196        "if" | "then"
11197            | "elif"
11198            | "else"
11199            | "fi"
11200            | "for"
11201            | "in"
11202            | "do"
11203            | "done"
11204            | "while"
11205            | "until"
11206            | "case"
11207            | "esac"
11208            | "{"
11209            | "}"
11210            | "!"
11211    )
11212}
11213
11214fn command_requires_shell(command: &str) -> bool {
11215    command.chars().any(|ch| {
11216        matches!(
11217            ch,
11218            '|' | '&'
11219                | ';'
11220                | '<'
11221                | '>'
11222                | '('
11223                | ')'
11224                | '$'
11225                | '`'
11226                | '*'
11227                | '?'
11228                | '['
11229                | ']'
11230                | '{'
11231                | '}'
11232                | '~'
11233                | '\''
11234                | '"'
11235                | '\\'
11236                | '\n'
11237        )
11238    })
11239}
11240
11241fn host_mount_path_for_guest_path(vm: &VmState, guest_path: &str) -> Option<PathBuf> {
11242    let normalized = normalize_path(guest_path);
11243
11244    let mut mounts = vm
11245        .configuration
11246        .mounts
11247        .iter()
11248        .filter_map(|mount| {
11249            ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
11250                .then(|| {
11251                    mount_config_host_path(&mount.plugin.config)
11252                        .map(|host_path| (mount.guest_path.as_str(), host_path))
11253                })
11254                .flatten()
11255        })
11256        .collect::<Vec<_>>();
11257    mounts.sort_by_key(|mount| std::cmp::Reverse(mount.0.len()));
11258
11259    for (guest_root, host_root) in mounts {
11260        if normalized != guest_root && !normalized.starts_with(&format!("{guest_root}/")) {
11261            continue;
11262        }
11263
11264        let suffix = normalized
11265            .strip_prefix(guest_root)
11266            .unwrap_or_default()
11267            .trim_start_matches('/');
11268        let mut path = PathBuf::from(host_root);
11269        if !suffix.is_empty() {
11270            path.push(suffix);
11271        }
11272        return Some(path);
11273    }
11274
11275    None
11276}
11277
11278fn host_runtime_path_for_guest_path_with_env(
11279    vm: &VmState,
11280    runtime_env: &BTreeMap<String, String>,
11281    guest_path: &str,
11282    default_host_cwd: &Path,
11283) -> Option<PathBuf> {
11284    if let Some(path) = host_mount_path_for_guest_path(vm, guest_path) {
11285        return Some(path);
11286    }
11287    if let Some(path) = host_path_from_runtime_guest_mappings(runtime_env, guest_path) {
11288        return Some(path);
11289    }
11290
11291    let normalized = normalize_path(guest_path);
11292    let virtual_home = guest_virtual_home(vm);
11293
11294    if normalized == virtual_home || normalized.starts_with(&format!("{virtual_home}/")) {
11295        let suffix = normalized
11296            .strip_prefix(&virtual_home)
11297            .unwrap_or_default()
11298            .trim_start_matches('/');
11299        let mut host_path = default_host_cwd.to_path_buf();
11300        if !suffix.is_empty() {
11301            host_path.push(suffix);
11302        }
11303        return Some(host_path);
11304    }
11305
11306    None
11307}
11308
11309#[derive(Deserialize, Serialize)]
11310struct RuntimeGuestPathMapping {
11311    #[serde(rename = "guestPath")]
11312    guest_path: String,
11313    #[serde(rename = "hostPath")]
11314    host_path: String,
11315    #[serde(rename = "readOnly", default)]
11316    read_only: bool,
11317}
11318
11319pub(crate) fn host_path_from_runtime_guest_mappings(
11320    runtime_env: &BTreeMap<String, String>,
11321    guest_path: &str,
11322) -> Option<PathBuf> {
11323    let mappings = runtime_env
11324        .get("AGENTOS_GUEST_PATH_MAPPINGS")
11325        .and_then(|value| serde_json::from_str::<Vec<RuntimeGuestPathMapping>>(value).ok())?;
11326    let normalized = normalize_path(guest_path);
11327
11328    let mut sorted_mappings = mappings
11329        .into_iter()
11330        .filter_map(|mapping| {
11331            (!mapping.guest_path.is_empty() && !mapping.host_path.is_empty()).then_some((
11332                normalize_path(&mapping.guest_path),
11333                PathBuf::from(mapping.host_path),
11334            ))
11335        })
11336        .collect::<Vec<_>>();
11337    sorted_mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.0.len()));
11338
11339    for (guest_root, mut host_root) in sorted_mappings {
11340        if guest_root != "/"
11341            && normalized != guest_root
11342            && !normalized.starts_with(&format!("{guest_root}/"))
11343        {
11344            continue;
11345        }
11346        if guest_root == "/" && !normalized.starts_with('/') {
11347            continue;
11348        }
11349
11350        if host_root.is_relative() {
11351            host_root = std::env::current_dir().ok()?.join(host_root);
11352        }
11353
11354        let suffix = if guest_root == "/" {
11355            normalized.trim_start_matches('/')
11356        } else {
11357            normalized
11358                .strip_prefix(&guest_root)
11359                .unwrap_or_default()
11360                .trim_start_matches('/')
11361        };
11362        if !suffix.is_empty() {
11363            host_root.push(suffix);
11364        }
11365        return Some(host_root);
11366    }
11367
11368    None
11369}
11370
11371fn guest_runtime_path_for_host_path(
11372    runtime_env: &BTreeMap<String, String>,
11373    virtual_home: &str,
11374    cwd: &Path,
11375    host_path: &str,
11376) -> Option<String> {
11377    let resolved = if host_path.starts_with("file://") {
11378        PathBuf::from(host_path.trim_start_matches("file://"))
11379    } else if host_path.starts_with("file:") {
11380        PathBuf::from(host_path.trim_start_matches("file:"))
11381    } else {
11382        let candidate = PathBuf::from(host_path);
11383        if candidate.is_absolute() {
11384            candidate
11385        } else if host_path.starts_with("./") || host_path.starts_with("../") {
11386            cwd.join(candidate)
11387        } else {
11388            return None;
11389        }
11390    };
11391    let normalized = normalize_host_path(&resolved);
11392
11393    if let Some(path) = guest_path_from_runtime_host_mappings(runtime_env, &normalized) {
11394        return Some(path);
11395    }
11396
11397    let normalized_cwd = normalize_host_path(cwd);
11398    if !path_is_within_root(&normalized, &normalized_cwd) {
11399        return None;
11400    }
11401
11402    let virtual_home = if virtual_home.starts_with('/') {
11403        virtual_home.to_string()
11404    } else {
11405        String::from("/root")
11406    };
11407    let suffix = normalized
11408        .strip_prefix(&normalized_cwd)
11409        .ok()?
11410        .to_string_lossy()
11411        .replace('\\', "/")
11412        .trim_start_matches('/')
11413        .to_owned();
11414
11415    Some(if suffix.is_empty() {
11416        virtual_home
11417    } else {
11418        normalize_path(&format!("{virtual_home}/{suffix}"))
11419    })
11420}
11421
11422fn guest_path_from_runtime_host_mappings(
11423    runtime_env: &BTreeMap<String, String>,
11424    host_path: &Path,
11425) -> Option<String> {
11426    let mappings = runtime_env
11427        .get("AGENTOS_GUEST_PATH_MAPPINGS")
11428        .and_then(|value| serde_json::from_str::<Vec<RuntimeGuestPathMapping>>(value).ok())?;
11429    let normalized = normalize_host_path(host_path);
11430
11431    let mut sorted_mappings = mappings
11432        .into_iter()
11433        .filter_map(|mapping| {
11434            (!mapping.guest_path.is_empty() && !mapping.host_path.is_empty()).then_some((
11435                normalize_path(&mapping.guest_path),
11436                normalize_host_path(Path::new(&mapping.host_path)),
11437            ))
11438        })
11439        .collect::<Vec<_>>();
11440    sorted_mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.1.as_os_str().len()));
11441
11442    for (guest_root, host_root) in sorted_mappings {
11443        if !path_is_within_root(&normalized, &host_root) {
11444            continue;
11445        }
11446        let suffix = normalized
11447            .strip_prefix(&host_root)
11448            .ok()?
11449            .to_string_lossy()
11450            .replace('\\', "/")
11451            .trim_start_matches('/')
11452            .to_owned();
11453
11454        return Some(if suffix.is_empty() {
11455            guest_root
11456        } else if guest_root == "/" {
11457            normalize_path(&format!("/{suffix}"))
11458        } else {
11459            normalize_path(&format!("{guest_root}/{suffix}"))
11460        });
11461    }
11462
11463    None
11464}
11465
11466fn host_mount_path_for_guest_path_from_mounts(
11467    mounts: &[crate::protocol::MountDescriptor],
11468    guest_path: &str,
11469) -> Option<PathBuf> {
11470    let normalized = normalize_path(guest_path);
11471
11472    let mut host_mounts = mounts
11473        .iter()
11474        .filter_map(|mount| {
11475            ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
11476                .then(|| {
11477                    mount_config_host_path(&mount.plugin.config)
11478                        .map(|host_path| (mount.guest_path.as_str(), host_path))
11479                })
11480                .flatten()
11481        })
11482        .collect::<Vec<_>>();
11483    host_mounts.sort_by_key(|mount| std::cmp::Reverse(mount.0.len()));
11484
11485    for (guest_root, host_root) in host_mounts {
11486        if normalized != guest_root && !normalized.starts_with(&format!("{guest_root}/")) {
11487            continue;
11488        }
11489
11490        let suffix = normalized
11491            .strip_prefix(guest_root)
11492            .unwrap_or_default()
11493            .trim_start_matches('/');
11494        let mut path = PathBuf::from(host_root);
11495        if !suffix.is_empty() {
11496            path.push(suffix);
11497        }
11498        return Some(path);
11499    }
11500
11501    None
11502}
11503
11504#[cfg(test)]
11505mod host_mount_path_for_guest_path_from_mounts_tests {
11506    use super::host_mount_path_for_guest_path_from_mounts;
11507    use crate::protocol::{MountDescriptor, MountPluginDescriptor};
11508    use serde_json::json;
11509    use std::path::PathBuf;
11510
11511    #[test]
11512    fn resolves_module_access_mount_paths() {
11513        let mounts = vec![MountDescriptor {
11514            guest_path: String::from("/root/node_modules"),
11515            read_only: true,
11516            plugin: MountPluginDescriptor {
11517                id: String::from("module_access"),
11518                config: json!({
11519                    "hostPath": "/tmp/workspace/node_modules",
11520                })
11521                .to_string(),
11522            },
11523        }];
11524
11525        let resolved =
11526            host_mount_path_for_guest_path_from_mounts(&mounts, "/root/node_modules/pkg/index.js")
11527                .expect("module_access mount should resolve");
11528
11529        assert_eq!(
11530            resolved,
11531            PathBuf::from("/tmp/workspace/node_modules/pkg/index.js")
11532        );
11533    }
11534}
11535
11536fn resolve_guest_socket_host_path(
11537    context: &JavascriptSocketPathContext,
11538    guest_path: &str,
11539) -> PathBuf {
11540    if let Some(path) = host_mount_path_for_guest_path_from_mounts(&context.mounts, guest_path) {
11541        return path;
11542    }
11543
11544    let normalized = normalize_path(guest_path);
11545    let mut host_path = context.sandbox_root.clone();
11546    let suffix = normalized.trim_start_matches('/');
11547    if !suffix.is_empty() {
11548        host_path.push(suffix);
11549    }
11550    host_path
11551}
11552
11553fn ensure_kernel_parent_directories(
11554    kernel: &mut SidecarKernel,
11555    path: &str,
11556) -> Result<(), SidecarError> {
11557    let parent = dirname(path);
11558    if parent != "/" && !kernel.exists(&parent).map_err(kernel_error)? {
11559        kernel.mkdir(&parent, true).map_err(kernel_error)?;
11560    }
11561    Ok(())
11562}
11563
11564// JavascriptChildProcessSpawnOptions, JavascriptChildProcessSpawnRequest moved to crate::protocol
11565// ResolvedChildProcessExecution moved to crate::state
11566
11567pub(crate) fn sanitize_javascript_child_process_internal_bootstrap_env(
11568    env: &BTreeMap<String, String>,
11569) -> BTreeMap<String, String> {
11570    const ALLOWED_KEYS: &[&str] = &[
11571        "AGENTOS_ALLOWED_NODE_BUILTINS",
11572        "AGENTOS_GUEST_PATH_MAPPINGS",
11573        "AGENTOS_LOOPBACK_EXEMPT_PORTS",
11574        "AGENTOS_VIRTUAL_PROCESS_EXEC_PATH",
11575        "AGENTOS_VIRTUAL_PROCESS_UID",
11576        "AGENTOS_VIRTUAL_PROCESS_GID",
11577        "AGENTOS_VIRTUAL_PROCESS_VERSION",
11578    ];
11579
11580    env.iter()
11581        .filter(|(key, _)| {
11582            ALLOWED_KEYS.contains(&key.as_str()) || key.starts_with("AGENTOS_VIRTUAL_OS_")
11583        })
11584        .map(|(key, value)| (key.clone(), value.clone()))
11585        .collect()
11586}
11587
11588// Network request types moved to crate::protocol
11589
11590// VmDnsConfig, DnsResolutionSource moved to crate::state
11591
11592fn resolve_tcp_bind_addr(host: &str, port: u16) -> Result<SocketAddr, SidecarError> {
11593    (host, port)
11594        .to_socket_addrs()
11595        .map_err(sidecar_net_error)?
11596        .next()
11597        .ok_or_else(|| {
11598            SidecarError::Execution(format!("failed to resolve TCP bind address {host}:{port}"))
11599        })
11600}
11601
11602pub(crate) fn format_dns_resource(hostname: &str) -> String {
11603    format!("dns://{hostname}")
11604}
11605
11606pub(crate) fn format_tcp_resource(host: &str, port: u16) -> String {
11607    format!("tcp://{host}:{port}")
11608}
11609
11610fn is_loopback_ip(ip: IpAddr) -> bool {
11611    match ip {
11612        IpAddr::V4(ip) => ip.is_loopback(),
11613        IpAddr::V6(ip) => {
11614            ip.is_loopback()
11615                || ip
11616                    .to_ipv4_mapped()
11617                    .is_some_and(|mapped| mapped.is_loopback())
11618        }
11619    }
11620}
11621
11622fn loopback_cidr(ip: IpAddr) -> &'static str {
11623    match ip {
11624        IpAddr::V4(ip) if ip.is_loopback() => "127.0.0.0/8",
11625        IpAddr::V6(ip)
11626            if ip
11627                .to_ipv4_mapped()
11628                .is_some_and(|mapped| mapped.is_loopback()) =>
11629        {
11630            "127.0.0.0/8"
11631        }
11632        IpAddr::V6(_) => "::1/128",
11633        IpAddr::V4(_) => "127.0.0.0/8",
11634    }
11635}
11636
11637/// Returns the embedded IPv4 address of an IPv4-compatible IPv6 address
11638/// (`::a.b.c.d`): the first six 16-bit segments are zero and the final 32 bits
11639/// hold the IPv4 address. The all-zero (`::`) and loopback (`::1`) addresses are
11640/// deliberately excluded so they are handled by the unspecified/loopback paths
11641/// rather than treated as IPv4-compatible.
11642fn ipv4_compatible_embedded(ip: Ipv6Addr) -> Option<Ipv4Addr> {
11643    let segments = ip.segments();
11644    if segments[0..6].iter().any(|&s| s != 0) {
11645        return None;
11646    }
11647    let embedded = (u32::from(segments[6]) << 16) | u32::from(segments[7]);
11648    // Skip :: (0.0.0.0) and ::1 (0.0.0.1) — these are the IPv6 unspecified /
11649    // loopback addresses, not IPv4-compatible representations of an IPv4 host.
11650    if embedded == 0 || embedded == 1 {
11651        return None;
11652    }
11653    Some(Ipv4Addr::from(embedded))
11654}
11655
11656fn restricted_non_loopback_ip_range(ip: IpAddr) -> Option<(&'static str, &'static str)> {
11657    match ip {
11658        IpAddr::V4(ip) => {
11659            if ip.is_unspecified() {
11660                // 0.0.0.0 is unspecified; the host stack routes a connect() to
11661                // it back to 127.0.0.1, so it must not bypass the loopback gate.
11662                return Some(("0.0.0.0/32", "unspecified"));
11663            }
11664            let [first, second, ..] = ip.octets();
11665            match (first, second) {
11666                (10, _) => Some(("10.0.0.0/8", "private")),
11667                (100, 64..=127) => Some(("100.64.0.0/10", "carrier-grade-nat")),
11668                (172, 16..=31) => Some(("172.16.0.0/12", "private")),
11669                (192, 168) => Some(("192.168.0.0/16", "private")),
11670                (169, 254) => Some(("169.254.0.0/16", "link-local")),
11671                // 224.0.0.0/4 is the IPv4 multicast range and 240.0.0.0/4 is
11672                // reserved/future-use (255.255.255.255 broadcast falls in it).
11673                // Neither is a legitimate unicast egress target, so a guest
11674                // connect to them must be denied rather than attempted.
11675                (224..=239, _) => Some(("224.0.0.0/4", "multicast")),
11676                (240..=255, _) => Some(("240.0.0.0/4", "reserved")),
11677                _ => None,
11678            }
11679        }
11680        IpAddr::V6(ip) => {
11681            if let Some(mapped) = ip.to_ipv4_mapped() {
11682                return restricted_non_loopback_ip_range(IpAddr::V4(mapped));
11683            }
11684            // IPv4-compatible IPv6 (::a.b.c.d): the first six segments are zero
11685            // and the last two carry an embedded IPv4 address. `to_ipv4_mapped`
11686            // returns None for this form, so without canonicalizing it here a
11687            // guest could spell a restricted IPv4 target (e.g. cloud-metadata
11688            // ::169.254.169.254) and bypass the IPv4 classifier. `::`/`::1` are
11689            // excluded so they fall through to the unspecified/loopback paths.
11690            if let Some(compat) = ipv4_compatible_embedded(ip) {
11691                return restricted_non_loopback_ip_range(IpAddr::V4(compat));
11692            }
11693
11694            if ip.is_unspecified() {
11695                // :: is the IPv6 unspecified address; same routing hazard as
11696                // 0.0.0.0, so deny it rather than letting it reach the host.
11697                return Some(("::/128", "unspecified"));
11698            }
11699
11700            let segments = ip.segments();
11701            if (segments[0] & 0xfe00) == 0xfc00 {
11702                return Some(("fc00::/7", "unique-local"));
11703            }
11704            if (segments[0] & 0xffc0) == 0xfe80 {
11705                return Some(("fe80::/10", "link-local"));
11706            }
11707            None
11708        }
11709    }
11710}
11711
11712fn blocked_dns_resolution_error(
11713    resource: &str,
11714    ip: IpAddr,
11715    cidr: &str,
11716    label: &str,
11717) -> SidecarError {
11718    SidecarError::Execution(format!(
11719        "EACCES: blocked outbound network access to {resource}: {ip} is within restricted {label} range {cidr}"
11720    ))
11721}
11722
11723fn blocked_loopback_connect_error(resource: &str, ip: IpAddr, port: u16) -> SidecarError {
11724    SidecarError::Execution(format!(
11725        "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}",
11726        loopback_cidr(ip)
11727    ))
11728}
11729
11730fn filter_dns_safe_ip_addrs(
11731    addresses: Vec<IpAddr>,
11732    hostname: &str,
11733) -> Result<Vec<IpAddr>, SidecarError> {
11734    let resource = format_dns_resource(hostname);
11735    let mut allowed = Vec::new();
11736    let mut blocked = None;
11737
11738    for ip in addresses {
11739        if let Some((cidr, label)) = restricted_non_loopback_ip_range(ip) {
11740            blocked.get_or_insert((ip, cidr, label));
11741            continue;
11742        }
11743        allowed.push(ip);
11744    }
11745
11746    if allowed.is_empty() {
11747        let (ip, cidr, label) = blocked.expect("blocked DNS results should capture a reason");
11748        return Err(blocked_dns_resolution_error(&resource, ip, cidr, label));
11749    }
11750
11751    Ok(allowed)
11752}
11753
11754fn loopback_connect_allowed(context: &JavascriptSocketPathContext, port: u16) -> bool {
11755    context.loopback_port_allowed(port)
11756}
11757
11758fn filter_tcp_connect_ip_addrs(
11759    addresses: Vec<IpAddr>,
11760    host: &str,
11761    port: u16,
11762    context: &JavascriptSocketPathContext,
11763) -> Result<Vec<IpAddr>, SidecarError> {
11764    let resource = format_tcp_resource(host, port);
11765    let mut allowed = Vec::new();
11766    let mut blocked = None;
11767
11768    for ip in addresses {
11769        if let Some((cidr, label)) = restricted_non_loopback_ip_range(ip) {
11770            blocked.get_or_insert_with(|| blocked_dns_resolution_error(&resource, ip, cidr, label));
11771            continue;
11772        }
11773        if is_loopback_ip(ip) && !loopback_connect_allowed(context, port) {
11774            blocked.get_or_insert_with(|| blocked_loopback_connect_error(&resource, ip, port));
11775            continue;
11776        }
11777        allowed.push(ip);
11778    }
11779
11780    if allowed.is_empty() {
11781        return Err(blocked.expect("blocked TCP connect results should capture a reason"));
11782    }
11783
11784    Ok(allowed)
11785}
11786
11787fn resolve_tcp_connect_addr<B>(
11788    bridge: &SharedBridge<B>,
11789    kernel: &SidecarKernel,
11790    vm_id: &str,
11791    dns: &VmDnsConfig,
11792    host: &str,
11793    port: u16,
11794    context: &JavascriptSocketPathContext,
11795) -> Result<ResolvedTcpConnectAddr, SidecarError>
11796where
11797    B: NativeSidecarBridge + Send + 'static,
11798    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11799{
11800    let allowed = filter_tcp_connect_ip_addrs(
11801        resolve_dns_ip_addrs(
11802            bridge,
11803            kernel,
11804            vm_id,
11805            dns,
11806            host,
11807            DnsLookupPolicy::SkipPermissions,
11808        )?,
11809        host,
11810        port,
11811        context,
11812    )?;
11813    let ip = allowed
11814        .iter()
11815        .copied()
11816        .find(|candidate| {
11817            let family = JavascriptSocketFamily::from_ip(*candidate);
11818            context.translate_tcp_loopback_port(family, port).is_some()
11819        })
11820        // We do not implement Happy Eyeballs yet, so prefer IPv4 over a
11821        // verbatim IPv6-first DNS answer for general outbound TCP connects.
11822        .or_else(|| allowed.iter().copied().find(IpAddr::is_ipv4))
11823        .or_else(|| allowed.first().copied())
11824        .ok_or_else(|| {
11825            SidecarError::Execution(format!("failed to resolve TCP address {host}:{port}"))
11826        })?;
11827    let family = JavascriptSocketFamily::from_ip(ip);
11828    let translated_loopback_port = context.translate_tcp_loopback_port(family, port);
11829    let use_kernel_loopback = is_loopback_ip(ip) && translated_loopback_port == Some(port);
11830    let actual_port = if is_loopback_ip(ip) {
11831        translated_loopback_port.unwrap_or(port)
11832    } else {
11833        port
11834    };
11835    Ok(ResolvedTcpConnectAddr {
11836        actual_addr: SocketAddr::new(ip, actual_port),
11837        guest_remote_addr: SocketAddr::new(ip, port),
11838        use_kernel_loopback,
11839    })
11840}
11841
11842fn resolve_dns_ip_addrs<B>(
11843    bridge: &SharedBridge<B>,
11844    kernel: &SidecarKernel,
11845    vm_id: &str,
11846    dns: &VmDnsConfig,
11847    hostname: &str,
11848    policy: DnsLookupPolicy,
11849) -> Result<Vec<IpAddr>, SidecarError>
11850where
11851    B: NativeSidecarBridge + Send + 'static,
11852    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11853{
11854    let resolution = match kernel.resolve_dns(hostname, policy) {
11855        Ok(resolution) => resolution,
11856        Err(error) => {
11857            let sidecar_error = kernel_error(error.clone());
11858            if error.code() != "EACCES" {
11859                emit_dns_resolution_failure_event(bridge, vm_id, hostname, dns, &sidecar_error);
11860            }
11861            return Err(sidecar_error);
11862        }
11863    };
11864    emit_dns_resolution_event(
11865        bridge,
11866        vm_id,
11867        hostname,
11868        resolution.source(),
11869        resolution.addresses(),
11870        dns,
11871    );
11872    Ok(resolution.addresses().to_vec())
11873}
11874
11875fn resolve_dns_records<B>(
11876    bridge: &SharedBridge<B>,
11877    kernel: &SidecarKernel,
11878    vm_id: &str,
11879    dns: &VmDnsConfig,
11880    hostname: &str,
11881    record_type: RecordType,
11882    policy: DnsLookupPolicy,
11883) -> Result<DnsRecordResolution, SidecarError>
11884where
11885    B: NativeSidecarBridge + Send + 'static,
11886    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11887{
11888    let resolution = match kernel.resolve_dns_records(hostname, record_type, policy) {
11889        Ok(resolution) => resolution,
11890        Err(error) => {
11891            let sidecar_error = kernel_error(error.clone());
11892            if error.code() != "EACCES" {
11893                emit_dns_resolution_failure_event(bridge, vm_id, hostname, dns, &sidecar_error);
11894            }
11895            return Err(sidecar_error);
11896        }
11897    };
11898    emit_dns_record_resolution_event(bridge, vm_id, hostname, &resolution, dns);
11899    Ok(resolution)
11900}
11901
11902fn filter_dns_ip_addrs(
11903    addresses: Vec<IpAddr>,
11904    family: Option<u8>,
11905) -> Result<Vec<IpAddr>, SidecarError> {
11906    let filtered: Vec<_> = match family.unwrap_or(0) {
11907        0 => addresses,
11908        4 => addresses
11909            .into_iter()
11910            .filter(|ip| matches!(ip, IpAddr::V4(_)))
11911            .collect(),
11912        6 => addresses
11913            .into_iter()
11914            .filter(|ip| matches!(ip, IpAddr::V6(_)))
11915            .collect(),
11916        other => {
11917            return Err(SidecarError::InvalidState(format!(
11918                "unsupported dns family {other}"
11919            )));
11920        }
11921    };
11922
11923    if filtered.is_empty() {
11924        return Err(SidecarError::Execution(String::from(
11925            "failed to resolve DNS address for requested family",
11926        )));
11927    }
11928
11929    Ok(filtered)
11930}
11931
11932fn resolve_udp_bind_addr(
11933    host: &str,
11934    port: u16,
11935    family: JavascriptUdpFamily,
11936) -> Result<SocketAddr, SidecarError> {
11937    (host, port)
11938        .to_socket_addrs()
11939        .map_err(sidecar_net_error)?
11940        .find(|addr| family.matches_addr(addr))
11941        .ok_or_else(|| {
11942            SidecarError::Execution(format!(
11943                "failed to resolve {} UDP bind address {host}:{port}",
11944                family.socket_type()
11945            ))
11946        })
11947}
11948
11949fn resolve_udp_addr<B>(request: UdpRemoteAddrRequest<'_, B>) -> Result<SocketAddr, SidecarError>
11950where
11951    B: NativeSidecarBridge + Send + 'static,
11952    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11953{
11954    let UdpRemoteAddrRequest {
11955        bridge,
11956        kernel,
11957        vm_id,
11958        dns,
11959        host,
11960        port,
11961        family,
11962        context,
11963    } = request;
11964    resolve_dns_ip_addrs(
11965        bridge,
11966        kernel,
11967        vm_id,
11968        dns,
11969        host,
11970        DnsLookupPolicy::SkipPermissions,
11971    )?
11972    .into_iter()
11973    .map(|ip| {
11974        let family_key = JavascriptSocketFamily::from_ip(ip);
11975        let actual_port = if is_loopback_ip(ip) {
11976            context
11977                .translate_udp_loopback_port(family_key, port)
11978                .unwrap_or(port)
11979        } else {
11980            port
11981        };
11982        SocketAddr::new(ip, actual_port)
11983    })
11984    .find(|addr| family.matches_addr(addr))
11985    .ok_or_else(|| {
11986        SidecarError::Execution(format!(
11987            "failed to resolve {} UDP address {host}:{port}",
11988            family.socket_type()
11989        ))
11990    })
11991}
11992
11993fn socket_addr_family(addr: &SocketAddr) -> &'static str {
11994    match addr {
11995        SocketAddr::V4(_) => "IPv4",
11996        SocketAddr::V6(_) => "IPv6",
11997    }
11998}
11999
12000fn javascript_net_timeout_value() -> Value {
12001    Value::String(String::from(JAVASCRIPT_NET_TIMEOUT_SENTINEL))
12002}
12003
12004fn javascript_net_json_string(value: Value, label: &str) -> Result<Value, SidecarError> {
12005    serde_json::to_string(&value)
12006        .map(Value::String)
12007        .map_err(|error| {
12008            SidecarError::InvalidState(format!("failed to serialize {label} payload: {error}"))
12009        })
12010}
12011
12012fn javascript_net_read_value(
12013    event: Option<JavascriptTcpSocketEvent>,
12014) -> Result<Value, SidecarError> {
12015    match event {
12016        Some(JavascriptTcpSocketEvent::Data(chunk)) => Ok(Value::String(
12017            base64::engine::general_purpose::STANDARD.encode(chunk),
12018        )),
12019        Some(JavascriptTcpSocketEvent::End | JavascriptTcpSocketEvent::Close { .. }) => {
12020            Ok(Value::Null)
12021        }
12022        Some(JavascriptTcpSocketEvent::Error { code, message }) => {
12023            let detail = code.unwrap_or_else(|| String::from("socket read"));
12024            Err(SidecarError::Execution(format!("{detail}: {message}")))
12025        }
12026        None => Ok(javascript_net_timeout_value()),
12027    }
12028}
12029
12030fn io_error_code(error: &std::io::Error) -> Option<String> {
12031    match error.raw_os_error() {
12032        Some(libc::EADDRINUSE) => Some(String::from("EADDRINUSE")),
12033        Some(libc::EADDRNOTAVAIL) => Some(String::from("EADDRNOTAVAIL")),
12034        Some(libc::ECONNREFUSED) => Some(String::from("ECONNREFUSED")),
12035        Some(libc::ECONNRESET) => Some(String::from("ECONNRESET")),
12036        Some(libc::EINVAL) => Some(String::from("EINVAL")),
12037        Some(libc::EPIPE) => Some(String::from("EPIPE")),
12038        Some(libc::ETIMEDOUT) => Some(String::from("ETIMEDOUT")),
12039        Some(libc::EHOSTUNREACH) => Some(String::from("EHOSTUNREACH")),
12040        Some(libc::ENETUNREACH) => Some(String::from("ENETUNREACH")),
12041        _ => None,
12042    }
12043}
12044
12045fn sidecar_net_error(error: std::io::Error) -> SidecarError {
12046    let message = match io_error_code(&error) {
12047        Some(code) => format!("{code}: {error}"),
12048        None => error.to_string(),
12049    };
12050    SidecarError::Execution(message)
12051}
12052
12053fn tls_provider() -> Arc<rustls::crypto::CryptoProvider> {
12054    Arc::new(aws_lc_rs::default_provider())
12055}
12056
12057fn tls_local_certificates(
12058    options: &JavascriptTlsBridgeOptions,
12059) -> Result<Vec<Vec<u8>>, SidecarError> {
12060    let Some(certificates) = options.cert.as_ref() else {
12061        return Ok(Vec::new());
12062    };
12063    tls_material_entries(certificates)
12064}
12065
12066fn tls_material_entries(material: &JavascriptTlsMaterial) -> Result<Vec<Vec<u8>>, SidecarError> {
12067    match material {
12068        JavascriptTlsMaterial::Single(entry) => tls_data_value(entry).map(|value| vec![value]),
12069        JavascriptTlsMaterial::Many(entries) => entries.iter().map(tls_data_value).collect(),
12070    }
12071}
12072
12073fn tls_data_value(value: &JavascriptTlsDataValue) -> Result<Vec<u8>, SidecarError> {
12074    match value {
12075        JavascriptTlsDataValue::Buffer { data } => base64::engine::general_purpose::STANDARD
12076            .decode(data)
12077            .map_err(|error| {
12078                SidecarError::InvalidState(format!("TLS material contains invalid base64: {error}"))
12079            }),
12080        JavascriptTlsDataValue::String { data } => Ok(data.as_bytes().to_vec()),
12081    }
12082}
12083
12084fn tls_certificates_from_material(
12085    material: &JavascriptTlsMaterial,
12086) -> Result<Vec<CertificateDer<'static>>, SidecarError> {
12087    let mut certificates = Vec::new();
12088    for entry in tls_material_entries(material)? {
12089        let mut reader = std::io::BufReader::new(Cursor::new(entry.clone()));
12090        let parsed = rustls_pemfile::certs(&mut reader)
12091            .collect::<Result<Vec<_>, _>>()
12092            .map_err(sidecar_net_error)?;
12093        if parsed.is_empty() {
12094            certificates.push(CertificateDer::from(entry));
12095        } else {
12096            certificates.extend(parsed);
12097        }
12098    }
12099    if certificates.is_empty() {
12100        return Err(SidecarError::InvalidState(String::from(
12101            "TLS certificate material did not contain any certificates",
12102        )));
12103    }
12104    Ok(certificates)
12105}
12106
12107fn tls_private_key_from_material(
12108    material: &JavascriptTlsMaterial,
12109) -> Result<PrivateKeyDer<'static>, SidecarError> {
12110    for entry in tls_material_entries(material)? {
12111        let mut reader = std::io::BufReader::new(Cursor::new(entry));
12112        if let Some(key) = rustls_pemfile::private_key(&mut reader).map_err(sidecar_net_error)? {
12113            return Ok(key);
12114        }
12115    }
12116    Err(SidecarError::InvalidState(String::from(
12117        "TLS private key material did not contain a supported key",
12118    )))
12119}
12120
12121fn tls_root_store(options: &JavascriptTlsBridgeOptions) -> Result<RootCertStore, SidecarError> {
12122    let mut roots = RootCertStore::empty();
12123    if let Some(ca) = options.ca.as_ref() {
12124        for certificate in tls_certificates_from_material(ca)? {
12125            roots.add(certificate).map_err(|error| {
12126                SidecarError::InvalidState(format!("failed to add TLS CA certificate: {error}"))
12127            })?;
12128        }
12129        return Ok(roots);
12130    }
12131
12132    for certificate in rustls_native_certs::load_native_certs().certs {
12133        roots.add(certificate).map_err(|error| {
12134            SidecarError::InvalidState(format!(
12135                "failed to add native TLS certificate to root store: {error}"
12136            ))
12137        })?;
12138    }
12139    Ok(roots)
12140}
12141
12142fn build_client_tls_stream(
12143    stream: TcpStream,
12144    options: &JavascriptTlsBridgeOptions,
12145) -> Result<rustls::StreamOwned<ClientConnection, TcpStream>, SidecarError> {
12146    let config = build_client_tls_config(options)?;
12147    let server_name = options
12148        .servername
12149        .clone()
12150        .unwrap_or_else(|| String::from("localhost"));
12151    let server_name = ServerName::try_from(server_name)
12152        .map_err(|_| SidecarError::InvalidState(String::from("invalid TLS servername")))?;
12153    stream
12154        .set_read_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
12155        .map_err(sidecar_net_error)?;
12156    stream
12157        .set_write_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
12158        .map_err(sidecar_net_error)?;
12159    let mut tls_stream = rustls::StreamOwned::new(
12160        ClientConnection::new(Arc::new(config), server_name).map_err(|error| {
12161            SidecarError::Execution(format!("failed to start TLS client: {error}"))
12162        })?,
12163        stream,
12164    );
12165    while tls_stream.conn.is_handshaking() {
12166        tls_stream
12167            .conn
12168            .complete_io(&mut tls_stream.sock)
12169            .map_err(sidecar_net_error)?;
12170    }
12171    tls_stream
12172        .sock
12173        .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
12174        .map_err(sidecar_net_error)?;
12175    tls_stream
12176        .sock
12177        .set_write_timeout(None)
12178        .map_err(sidecar_net_error)?;
12179    Ok(tls_stream)
12180}
12181
12182fn build_client_loopback_tls_stream(
12183    transport: crate::state::LoopbackTlsEndpoint,
12184    options: &JavascriptTlsBridgeOptions,
12185) -> Result<rustls::StreamOwned<ClientConnection, crate::state::LoopbackTlsEndpoint>, SidecarError>
12186{
12187    let config = build_client_tls_config(options)?;
12188    let server_name = options
12189        .servername
12190        .clone()
12191        .unwrap_or_else(|| String::from("localhost"));
12192    let server_name = ServerName::try_from(server_name)
12193        .map_err(|_| SidecarError::InvalidState(String::from("invalid TLS servername")))?;
12194    let mut tls_stream = rustls::StreamOwned::new(
12195        ClientConnection::new(Arc::new(config), server_name).map_err(|error| {
12196            SidecarError::Execution(format!("failed to start TLS client: {error}"))
12197        })?,
12198        transport,
12199    );
12200    match tls_stream.conn.complete_io(&mut tls_stream.sock) {
12201        Ok(_) => {}
12202        Err(error)
12203            if matches!(
12204                error.kind(),
12205                std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
12206            ) => {}
12207        Err(error) => return Err(sidecar_net_error(error)),
12208    }
12209    Ok(tls_stream)
12210}
12211
12212fn build_client_tls_config(
12213    options: &JavascriptTlsBridgeOptions,
12214) -> Result<ClientConfig, SidecarError> {
12215    let provider = tls_provider();
12216    let builder = ClientConfig::builder_with_provider(provider.clone())
12217        .with_safe_default_protocol_versions()
12218        .map_err(|error| {
12219            SidecarError::InvalidState(format!("invalid TLS protocol config: {error}"))
12220        })?;
12221
12222    let mut config = if options.reject_unauthorized == Some(false) {
12223        let verifier = Arc::new(InsecureTlsVerifier {
12224            supported_schemes: provider
12225                .signature_verification_algorithms
12226                .supported_schemes(),
12227        });
12228        builder
12229            .dangerous()
12230            .with_custom_certificate_verifier(verifier)
12231            .with_no_client_auth()
12232    } else {
12233        builder
12234            .with_root_certificates(tls_root_store(options)?)
12235            .with_no_client_auth()
12236    };
12237
12238    if let Some(protocols) = options.alpn_protocols.as_ref() {
12239        config.alpn_protocols = protocols
12240            .iter()
12241            .map(|protocol| protocol.as_bytes().to_vec())
12242            .collect();
12243    }
12244    Ok(config)
12245}
12246
12247fn build_server_tls_stream(
12248    stream: TcpStream,
12249    options: &JavascriptTlsBridgeOptions,
12250) -> Result<rustls::StreamOwned<ServerConnection, TcpStream>, SidecarError> {
12251    let config = build_server_tls_config(options)?;
12252    stream
12253        .set_read_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
12254        .map_err(sidecar_net_error)?;
12255    stream
12256        .set_write_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
12257        .map_err(sidecar_net_error)?;
12258    let mut tls_stream = rustls::StreamOwned::new(
12259        ServerConnection::new(Arc::new(config)).map_err(|error| {
12260            SidecarError::Execution(format!("failed to start TLS server: {error}"))
12261        })?,
12262        stream,
12263    );
12264    while tls_stream.conn.is_handshaking() {
12265        tls_stream
12266            .conn
12267            .complete_io(&mut tls_stream.sock)
12268            .map_err(sidecar_net_error)?;
12269    }
12270    tls_stream
12271        .sock
12272        .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
12273        .map_err(sidecar_net_error)?;
12274    tls_stream
12275        .sock
12276        .set_write_timeout(None)
12277        .map_err(sidecar_net_error)?;
12278    Ok(tls_stream)
12279}
12280
12281fn build_server_loopback_tls_stream(
12282    transport: crate::state::LoopbackTlsEndpoint,
12283    options: &JavascriptTlsBridgeOptions,
12284) -> Result<rustls::StreamOwned<ServerConnection, crate::state::LoopbackTlsEndpoint>, SidecarError>
12285{
12286    let config = build_server_tls_config(options)?;
12287    Ok(rustls::StreamOwned::new(
12288        ServerConnection::new(Arc::new(config)).map_err(|error| {
12289            SidecarError::Execution(format!("failed to start TLS server: {error}"))
12290        })?,
12291        transport,
12292    ))
12293}
12294
12295fn build_server_tls_config(
12296    options: &JavascriptTlsBridgeOptions,
12297) -> Result<ServerConfig, SidecarError> {
12298    let certificates = tls_certificates_from_material(options.cert.as_ref().ok_or_else(|| {
12299        SidecarError::InvalidState(String::from("TLS server upgrade requires a certificate"))
12300    })?)?;
12301    let key = tls_private_key_from_material(options.key.as_ref().ok_or_else(|| {
12302        SidecarError::InvalidState(String::from("TLS server upgrade requires a private key"))
12303    })?)?;
12304
12305    let mut config = ServerConfig::builder_with_provider(tls_provider())
12306        .with_safe_default_protocol_versions()
12307        .map_err(|error| {
12308            SidecarError::InvalidState(format!("invalid TLS protocol config: {error}"))
12309        })?
12310        .with_no_client_auth()
12311        .with_single_cert(certificates, key)
12312        .map_err(|error| {
12313            SidecarError::InvalidState(format!("invalid TLS server config: {error}"))
12314        })?;
12315
12316    if let Some(protocols) = options.alpn_protocols.as_ref() {
12317        config.alpn_protocols = protocols
12318            .iter()
12319            .map(|protocol| protocol.as_bytes().to_vec())
12320            .collect();
12321    }
12322    Ok(config)
12323}
12324
12325fn tls_protocol_name(version: rustls::ProtocolVersion) -> String {
12326    match version {
12327        rustls::ProtocolVersion::TLSv1_2 => String::from("TLSv1.2"),
12328        rustls::ProtocolVersion::TLSv1_3 => String::from("TLSv1.3"),
12329        other => other
12330            .as_str()
12331            .map(str::to_owned)
12332            .unwrap_or_else(|| format!("{other:?}")),
12333    }
12334}
12335
12336fn tls_cipher_bridge_value(suite: rustls::SupportedCipherSuite) -> Value {
12337    tls_bridge_object(vec![
12338        (
12339            "name",
12340            suite
12341                .suite()
12342                .as_str()
12343                .map(|value| Value::String(value.to_owned()))
12344                .unwrap_or(Value::Null),
12345        ),
12346        (
12347            "standardName",
12348            suite
12349                .suite()
12350                .as_str()
12351                .map(|value| Value::String(value.to_owned()))
12352                .unwrap_or(Value::Null),
12353        ),
12354        (
12355            "version",
12356            Value::String(if suite.tls13().is_some() {
12357                String::from("TLSv1.3")
12358            } else {
12359                String::from("TLSv1.2")
12360            }),
12361        ),
12362    ])
12363}
12364
12365fn tls_certificate_bridge_value(certificate: &[u8], detailed: bool) -> Value {
12366    let mut fields = vec![("raw", tls_bridge_buffer_value(certificate))];
12367    if detailed {
12368        fields.push(("issuerCertificate", tls_bridge_undefined_value()));
12369    }
12370    tls_bridge_object(fields)
12371}
12372
12373fn tls_bridge_buffer_value(bytes: &[u8]) -> Value {
12374    json!({
12375        "type": "buffer",
12376        "data": base64::engine::general_purpose::STANDARD.encode(bytes),
12377    })
12378}
12379
12380fn tls_bridge_object(entries: Vec<(&str, Value)>) -> Value {
12381    let value = entries
12382        .into_iter()
12383        .map(|(key, value)| (key.to_owned(), value))
12384        .collect::<serde_json::Map<String, Value>>();
12385    json!({
12386        "type": "object",
12387        "id": 1,
12388        "value": value,
12389    })
12390}
12391
12392fn tls_bridge_undefined_value() -> Value {
12393    json!({
12394        "type": "undefined",
12395    })
12396}
12397
12398fn spawn_tcp_socket_reader(
12399    stream: TcpStream,
12400    sender: Sender<JavascriptTcpSocketEvent>,
12401    tls_mode: Arc<AtomicBool>,
12402    saw_local_shutdown: Arc<AtomicBool>,
12403    saw_remote_end: Arc<AtomicBool>,
12404    close_notified: Arc<AtomicBool>,
12405) {
12406    thread::spawn(move || {
12407        let mut stream = stream;
12408        let mut buffer = vec![0_u8; 64 * 1024];
12409        loop {
12410            if tls_mode.load(Ordering::SeqCst) {
12411                break;
12412            }
12413            match stream.read(&mut buffer) {
12414                Ok(0) => {
12415                    saw_remote_end.store(true, Ordering::SeqCst);
12416                    let _ = sender.send(JavascriptTcpSocketEvent::End);
12417                    if saw_local_shutdown.load(Ordering::SeqCst)
12418                        && !close_notified.swap(true, Ordering::SeqCst)
12419                    {
12420                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12421                    }
12422                    break;
12423                }
12424                Ok(bytes_read) => {
12425                    if sender
12426                        .send(JavascriptTcpSocketEvent::Data(
12427                            buffer[..bytes_read].to_vec(),
12428                        ))
12429                        .is_err()
12430                    {
12431                        break;
12432                    }
12433                }
12434                Err(error)
12435                    if matches!(
12436                        error.kind(),
12437                        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
12438                    ) =>
12439                {
12440                    continue;
12441                }
12442                Err(error) => {
12443                    let code = io_error_code(&error);
12444                    let _ = sender.send(JavascriptTcpSocketEvent::Error {
12445                        code,
12446                        message: error.to_string(),
12447                    });
12448                    if !close_notified.swap(true, Ordering::SeqCst) {
12449                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12450                    }
12451                    break;
12452                }
12453            }
12454        }
12455    });
12456}
12457
12458fn spawn_tls_socket_reader(
12459    tls_stream: Arc<Mutex<Option<ActiveTlsStream>>>,
12460    sender: Sender<JavascriptTcpSocketEvent>,
12461    saw_local_shutdown: Arc<AtomicBool>,
12462    saw_remote_end: Arc<AtomicBool>,
12463    close_notified: Arc<AtomicBool>,
12464) {
12465    thread::spawn(move || {
12466        let mut buffer = vec![0_u8; 64 * 1024];
12467        loop {
12468            let read_result = {
12469                let mut guard = match tls_stream.lock() {
12470                    Ok(guard) => guard,
12471                    Err(_) => return,
12472                };
12473                let Some(stream) = guard.as_mut() else {
12474                    return;
12475                };
12476                stream.read(&mut buffer)
12477            };
12478
12479            match read_result {
12480                Ok(0) => {
12481                    saw_remote_end.store(true, Ordering::SeqCst);
12482                    let _ = sender.send(JavascriptTcpSocketEvent::End);
12483                    if saw_local_shutdown.load(Ordering::SeqCst)
12484                        && !close_notified.swap(true, Ordering::SeqCst)
12485                    {
12486                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12487                    }
12488                    break;
12489                }
12490                Ok(bytes_read) => {
12491                    if sender
12492                        .send(JavascriptTcpSocketEvent::Data(
12493                            buffer[..bytes_read].to_vec(),
12494                        ))
12495                        .is_err()
12496                    {
12497                        break;
12498                    }
12499                }
12500                Err(error)
12501                    if matches!(
12502                        error.kind(),
12503                        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
12504                    ) =>
12505                {
12506                    // The TLS reader and writer share one rustls stream mutex. Yield after
12507                    // timed-out reads so request writes can acquire the lock promptly.
12508                    std::thread::sleep(Duration::from_millis(1));
12509                    continue;
12510                }
12511                Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => {
12512                    saw_remote_end.store(true, Ordering::SeqCst);
12513                    let _ = sender.send(JavascriptTcpSocketEvent::End);
12514                    if saw_local_shutdown.load(Ordering::SeqCst)
12515                        && !close_notified.swap(true, Ordering::SeqCst)
12516                    {
12517                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12518                    }
12519                    break;
12520                }
12521                Err(error) => {
12522                    let code = io_error_code(&error);
12523                    let _ = sender.send(JavascriptTcpSocketEvent::Error {
12524                        code,
12525                        message: error.to_string(),
12526                    });
12527                    if !close_notified.swap(true, Ordering::SeqCst) {
12528                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12529                    }
12530                    break;
12531                }
12532            }
12533        }
12534    });
12535}
12536
12537fn spawn_unix_socket_reader(
12538    stream: UnixStream,
12539    sender: Sender<JavascriptTcpSocketEvent>,
12540    saw_local_shutdown: Arc<AtomicBool>,
12541    saw_remote_end: Arc<AtomicBool>,
12542    close_notified: Arc<AtomicBool>,
12543) {
12544    thread::spawn(move || {
12545        let mut stream = stream;
12546        let mut buffer = vec![0_u8; 64 * 1024];
12547        loop {
12548            match stream.read(&mut buffer) {
12549                Ok(0) => {
12550                    saw_remote_end.store(true, Ordering::SeqCst);
12551                    let _ = sender.send(JavascriptTcpSocketEvent::End);
12552                    if saw_local_shutdown.load(Ordering::SeqCst)
12553                        && !close_notified.swap(true, Ordering::SeqCst)
12554                    {
12555                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12556                    }
12557                    break;
12558                }
12559                Ok(bytes_read) => {
12560                    if sender
12561                        .send(JavascriptTcpSocketEvent::Data(
12562                            buffer[..bytes_read].to_vec(),
12563                        ))
12564                        .is_err()
12565                    {
12566                        break;
12567                    }
12568                }
12569                Err(error) => {
12570                    let code = io_error_code(&error);
12571                    let _ = sender.send(JavascriptTcpSocketEvent::Error {
12572                        code,
12573                        message: error.to_string(),
12574                    });
12575                    if !close_notified.swap(true, Ordering::SeqCst) {
12576                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12577                    }
12578                    break;
12579                }
12580            }
12581        }
12582    });
12583}
12584
12585fn terminate_child_process_tree(kernel: &mut SidecarKernel, process: &mut ActiveProcess) {
12586    let sqlite_database_ids = process.sqlite_databases.keys().copied().collect::<Vec<_>>();
12587    for database_id in sqlite_database_ids {
12588        let _ = close_sqlite_database(kernel, process, database_id);
12589    }
12590    process.sqlite_statements.clear();
12591    process.http_servers.clear();
12592    process.pending_http_requests.clear();
12593    if let Ok(mut http2) = process.http2.shared.lock() {
12594        let sessions = http2.sessions.values().cloned().collect::<Vec<_>>();
12595        http2.server_events.clear();
12596        http2.session_events.clear();
12597        http2.streams.clear();
12598        http2.servers.clear();
12599        http2.sessions.clear();
12600        drop(http2);
12601        for session in sessions {
12602            let (respond_to, _rx) = mpsc::channel();
12603            let _ = session.command_tx.send(Http2SessionCommand::Close {
12604                abrupt: true,
12605                respond_to,
12606            });
12607        }
12608    }
12609
12610    let listener_ids = process.tcp_listeners.keys().cloned().collect::<Vec<_>>();
12611    for listener_id in listener_ids {
12612        if let Some(listener) = process.tcp_listeners.remove(&listener_id) {
12613            let _ = listener.close(kernel, process.kernel_pid);
12614        }
12615    }
12616
12617    let sockets = process.tcp_sockets.keys().cloned().collect::<Vec<_>>();
12618    for socket_id in sockets {
12619        if let Some(socket) = process.tcp_sockets.remove(&socket_id) {
12620            let _ = socket.close(kernel, process.kernel_pid);
12621        }
12622    }
12623
12624    let unix_listener_ids = process.unix_listeners.keys().cloned().collect::<Vec<_>>();
12625    for listener_id in unix_listener_ids {
12626        if let Some(listener) = process.unix_listeners.remove(&listener_id) {
12627            let _ = listener.close();
12628        }
12629    }
12630
12631    let unix_sockets = process.unix_sockets.keys().cloned().collect::<Vec<_>>();
12632    for socket_id in unix_sockets {
12633        if let Some(socket) = process.unix_sockets.remove(&socket_id) {
12634            let _ = socket.close();
12635        }
12636    }
12637
12638    let udp_socket_ids = process.udp_sockets.keys().cloned().collect::<Vec<_>>();
12639    for socket_id in udp_socket_ids {
12640        if let Some(mut socket) = process.udp_sockets.remove(&socket_id) {
12641            socket.close(kernel, process.kernel_pid);
12642        }
12643    }
12644
12645    let child_ids = process.child_processes.keys().cloned().collect::<Vec<_>>();
12646    for child_id in child_ids {
12647        let Some(mut child) = process.child_processes.remove(&child_id) else {
12648            continue;
12649        };
12650        terminate_child_process_tree(kernel, &mut child);
12651        let _ = kernel.kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, SIGTERM);
12652        let _ = signal_runtime_process(child.execution.child_pid(), SIGTERM);
12653        child.kernel_handle.finish(0);
12654        let _ = kernel.wait_and_reap(child.kernel_pid);
12655    }
12656}
12657
12658fn service_javascript_sqlite_sync_rpc(
12659    kernel: &mut SidecarKernel,
12660    process: &mut ActiveProcess,
12661    request: &JavascriptSyncRpcRequest,
12662) -> Result<Value, SidecarError> {
12663    match request.method.as_str() {
12664        "sqlite.constants" => Ok(json!({})),
12665        "sqlite.open" => sqlite_open_database(kernel, process, request),
12666        "sqlite.close" => {
12667            let database_id =
12668                javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.close database id")?;
12669            close_sqlite_database(kernel, process, database_id)?;
12670            Ok(Value::Null)
12671        }
12672        "sqlite.exec" => sqlite_exec_database(kernel, process, request),
12673        "sqlite.query" => sqlite_query_database(process, request),
12674        "sqlite.prepare" => sqlite_prepare_statement(process, request),
12675        "sqlite.location" => {
12676            let database_id =
12677                javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.location database id")?;
12678            let database = sqlite_database(process, database_id)?;
12679            Ok(database
12680                .vm_path
12681                .as_ref()
12682                .map(|path| Value::String(path.clone()))
12683                .unwrap_or(Value::Null))
12684        }
12685        "sqlite.checkpoint" => {
12686            let database_id =
12687                javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.checkpoint database id")?;
12688            let kernel_pid = process.kernel_pid;
12689            let database = sqlite_database_mut(process, database_id)?;
12690            sqlite_sync_database(kernel, kernel_pid, database)?;
12691            Ok(Value::Null)
12692        }
12693        "sqlite.statement.run" => sqlite_run_statement(kernel, process, request),
12694        "sqlite.statement.get" => sqlite_get_statement(process, request),
12695        "sqlite.statement.all" | "sqlite.statement.iterate" => {
12696            sqlite_all_statement(process, request)
12697        }
12698        "sqlite.statement.columns" => sqlite_statement_columns(process, request),
12699        "sqlite.statement.setReturnArrays" => {
12700            let statement_id = javascript_sync_rpc_arg_u64(
12701                &request.args,
12702                0,
12703                "sqlite.statement.setReturnArrays statement id",
12704            )?;
12705            let enabled = javascript_sync_rpc_arg_bool(
12706                &request.args,
12707                1,
12708                "sqlite.statement.setReturnArrays enabled",
12709            )?;
12710            sqlite_statement_mut(process, statement_id)?.return_arrays = enabled;
12711            Ok(Value::Null)
12712        }
12713        "sqlite.statement.setReadBigInts" => {
12714            let statement_id = javascript_sync_rpc_arg_u64(
12715                &request.args,
12716                0,
12717                "sqlite.statement.setReadBigInts statement id",
12718            )?;
12719            let enabled = javascript_sync_rpc_arg_bool(
12720                &request.args,
12721                1,
12722                "sqlite.statement.setReadBigInts enabled",
12723            )?;
12724            sqlite_statement_mut(process, statement_id)?.read_bigints = enabled;
12725            Ok(Value::Null)
12726        }
12727        "sqlite.statement.setAllowBareNamedParameters" => {
12728            let statement_id = javascript_sync_rpc_arg_u64(
12729                &request.args,
12730                0,
12731                "sqlite.statement.setAllowBareNamedParameters statement id",
12732            )?;
12733            let enabled = javascript_sync_rpc_arg_bool(
12734                &request.args,
12735                1,
12736                "sqlite.statement.setAllowBareNamedParameters enabled",
12737            )?;
12738            sqlite_statement_mut(process, statement_id)?.allow_bare_named_parameters = enabled;
12739            Ok(Value::Null)
12740        }
12741        "sqlite.statement.setAllowUnknownNamedParameters" => {
12742            let statement_id = javascript_sync_rpc_arg_u64(
12743                &request.args,
12744                0,
12745                "sqlite.statement.setAllowUnknownNamedParameters statement id",
12746            )?;
12747            let enabled = javascript_sync_rpc_arg_bool(
12748                &request.args,
12749                1,
12750                "sqlite.statement.setAllowUnknownNamedParameters enabled",
12751            )?;
12752            sqlite_statement_mut(process, statement_id)?.allow_unknown_named_parameters = enabled;
12753            Ok(Value::Null)
12754        }
12755        "sqlite.statement.finalize" => {
12756            let statement_id = javascript_sync_rpc_arg_u64(
12757                &request.args,
12758                0,
12759                "sqlite.statement.finalize statement id",
12760            )?;
12761            process
12762                .sqlite_statements
12763                .remove(&statement_id)
12764                .ok_or_else(|| {
12765                    SidecarError::InvalidState(format!(
12766                        "sqlite statement handle not found: {statement_id}"
12767                    ))
12768                })?;
12769            Ok(Value::Null)
12770        }
12771        other => Err(SidecarError::InvalidState(format!(
12772            "unsupported JavaScript sqlite sync RPC method {other}"
12773        ))),
12774    }
12775}
12776
12777fn sqlite_open_database(
12778    kernel: &mut SidecarKernel,
12779    process: &mut ActiveProcess,
12780    request: &JavascriptSyncRpcRequest,
12781) -> Result<Value, SidecarError> {
12782    ensure_per_process_state_handle_capacity(process.sqlite_databases.len(), "sqlite database")?;
12783    let path = request.args.first().and_then(Value::as_str);
12784    let vm_path = path.filter(|value| !value.is_empty() && *value != ":memory:");
12785    let options = request.args.get(1);
12786    let read_only = sqlite_option_bool(options, "readOnly").unwrap_or(false);
12787    let create = sqlite_option_bool(options, "create").unwrap_or(!read_only);
12788    let timeout_ms = sqlite_option_u64(options, "timeout");
12789
12790    process.next_sqlite_database_id += 1;
12791    let database_id = process.next_sqlite_database_id;
12792
12793    let host_path = if vm_path.is_some() {
12794        Some(
12795            std::env::temp_dir()
12796                .join(format!(
12797                    "secure-exec-sidecar-sqlite-{}-{database_id}",
12798                    process.kernel_pid
12799                ))
12800                .join("database.sqlite"),
12801        )
12802    } else {
12803        None
12804    };
12805
12806    if let Some(host_path) = host_path.as_ref() {
12807        if let Some(parent) = host_path.parent() {
12808            fs::create_dir_all(parent).map_err(|error| {
12809                SidecarError::Io(format!(
12810                    "failed to prepare sqlite temp directory {}: {error}",
12811                    parent.display()
12812                ))
12813            })?;
12814        }
12815    }
12816
12817    if let (Some(vm_path), Some(host_path)) = (vm_path, host_path.as_ref()) {
12818        if kernel
12819            .exists_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, vm_path)
12820            .map_err(kernel_error)?
12821        {
12822            let contents = kernel
12823                .read_file_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, vm_path)
12824                .map_err(kernel_error)?;
12825            fs::write(host_path, contents).map_err(|error| {
12826                SidecarError::Io(format!(
12827                    "failed to materialize sqlite database {}: {error}",
12828                    host_path.display()
12829                ))
12830            })?;
12831        } else if read_only && !create {
12832            return Err(SidecarError::InvalidState(format!(
12833                "sqlite database does not exist: {vm_path}"
12834            )));
12835        }
12836    }
12837
12838    let target = host_path
12839        .as_ref()
12840        .map(|path| path.to_string_lossy().into_owned())
12841        .unwrap_or_else(|| String::from(":memory:"));
12842    let mut flags = if read_only {
12843        SqliteOpenFlags::SQLITE_OPEN_READ_ONLY
12844    } else {
12845        SqliteOpenFlags::SQLITE_OPEN_READ_WRITE
12846    };
12847    if create && !read_only {
12848        flags |= SqliteOpenFlags::SQLITE_OPEN_CREATE;
12849    }
12850
12851    let connection = SqliteConnection::open_with_flags(&target, flags).map_err(|error| {
12852        SidecarError::InvalidState(format!(
12853            "sqlite database open failed for {}: {error}",
12854            vm_path.unwrap_or(":memory:")
12855        ))
12856    })?;
12857    if let Some(timeout_ms) = timeout_ms {
12858        connection
12859            .busy_timeout(Duration::from_millis(timeout_ms))
12860            .map_err(sqlite_error)?;
12861    }
12862    if host_path.is_some() && !read_only {
12863        let _ = connection.pragma_update(None, "journal_mode", "WAL");
12864    }
12865
12866    process.sqlite_databases.insert(
12867        database_id,
12868        ActiveSqliteDatabase {
12869            connection,
12870            host_path,
12871            vm_path: vm_path.map(String::from),
12872            dirty: false,
12873            transaction_depth: 0,
12874            read_only,
12875        },
12876    );
12877
12878    Ok(json!(database_id))
12879}
12880
12881fn sqlite_exec_database(
12882    kernel: &mut SidecarKernel,
12883    process: &mut ActiveProcess,
12884    request: &JavascriptSyncRpcRequest,
12885) -> Result<Value, SidecarError> {
12886    let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.exec database id")?;
12887    let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.exec sql")?;
12888    let kernel_pid = process.kernel_pid;
12889    let database = sqlite_database_mut(process, database_id)?;
12890    let before = database.connection.total_changes();
12891    database
12892        .connection
12893        .execute_batch(sql)
12894        .map_err(sqlite_error)?;
12895    mark_sqlite_mutation(database, sql);
12896    sqlite_sync_database(kernel, kernel_pid, database)?;
12897    Ok(json!(database
12898        .connection
12899        .total_changes()
12900        .saturating_sub(before)))
12901}
12902
12903fn sqlite_query_database(
12904    process: &mut ActiveProcess,
12905    request: &JavascriptSyncRpcRequest,
12906) -> Result<Value, SidecarError> {
12907    let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.query database id")?;
12908    let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.query sql")?;
12909    let params = request.args.get(2);
12910    let options = request.args.get(3);
12911    let return_arrays = sqlite_option_bool(options, "returnArrays").unwrap_or(false);
12912    let read_bigints = sqlite_option_bool(options, "readBigInts").unwrap_or(false);
12913    let database = sqlite_database_mut(process, database_id)?;
12914    sqlite_query_rows(
12915        &mut database.connection,
12916        sql,
12917        params,
12918        return_arrays,
12919        read_bigints,
12920        true,
12921        false,
12922    )
12923}
12924
12925fn sqlite_prepare_statement(
12926    process: &mut ActiveProcess,
12927    request: &JavascriptSyncRpcRequest,
12928) -> Result<Value, SidecarError> {
12929    ensure_per_process_state_handle_capacity(process.sqlite_statements.len(), "sqlite statement")?;
12930    let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.prepare database id")?;
12931    let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.prepare sql")?;
12932    let _ = sqlite_database(process, database_id)?;
12933    process.next_sqlite_statement_id += 1;
12934    let statement_id = process.next_sqlite_statement_id;
12935    process.sqlite_statements.insert(
12936        statement_id,
12937        ActiveSqliteStatement {
12938            database_id,
12939            sql: sql.to_owned(),
12940            return_arrays: false,
12941            read_bigints: false,
12942            allow_bare_named_parameters: false,
12943            allow_unknown_named_parameters: false,
12944        },
12945    );
12946    Ok(json!(statement_id))
12947}
12948
12949fn sqlite_run_statement(
12950    kernel: &mut SidecarKernel,
12951    process: &mut ActiveProcess,
12952    request: &JavascriptSyncRpcRequest,
12953) -> Result<Value, SidecarError> {
12954    let statement_id =
12955        javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.run statement id")?;
12956    let params = request.args.get(1);
12957    let statement_state = sqlite_statement(process, statement_id)?.clone();
12958    let kernel_pid = process.kernel_pid;
12959    let database = sqlite_database_mut(process, statement_state.database_id)?;
12960    let before = database.connection.total_changes();
12961    {
12962        let mut statement = database
12963            .connection
12964            .prepare(&statement_state.sql)
12965            .map_err(sqlite_error)?;
12966        bind_sqlite_parameters(
12967            &mut statement,
12968            params,
12969            statement_state.allow_bare_named_parameters,
12970            statement_state.allow_unknown_named_parameters,
12971        )?;
12972        statement.raw_execute().map_err(sqlite_error)?;
12973    }
12974    let changes = database.connection.total_changes().saturating_sub(before);
12975    let last_insert_rowid = database.connection.last_insert_rowid();
12976    mark_sqlite_mutation(database, &statement_state.sql);
12977    sqlite_sync_database(kernel, kernel_pid, database)?;
12978    let result = json!({
12979        "changes": changes,
12980        "lastInsertRowid": encode_sqlite_integer(last_insert_rowid, true),
12981    });
12982    Ok(result)
12983}
12984
12985fn sqlite_get_statement(
12986    process: &mut ActiveProcess,
12987    request: &JavascriptSyncRpcRequest,
12988) -> Result<Value, SidecarError> {
12989    let statement_id =
12990        javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.get statement id")?;
12991    let params = request.args.get(1);
12992    let statement_state = sqlite_statement(process, statement_id)?.clone();
12993    let database = sqlite_database_mut(process, statement_state.database_id)?;
12994    let rows = sqlite_query_rows(
12995        &mut database.connection,
12996        &statement_state.sql,
12997        params,
12998        statement_state.return_arrays,
12999        statement_state.read_bigints,
13000        statement_state.allow_bare_named_parameters,
13001        statement_state.allow_unknown_named_parameters,
13002    )?;
13003    Ok(rows
13004        .as_array()
13005        .and_then(|rows| rows.first().cloned())
13006        .unwrap_or(Value::Null))
13007}
13008
13009fn sqlite_all_statement(
13010    process: &mut ActiveProcess,
13011    request: &JavascriptSyncRpcRequest,
13012) -> Result<Value, SidecarError> {
13013    let statement_id =
13014        javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.all statement id")?;
13015    let params = request.args.get(1);
13016    let statement_state = sqlite_statement(process, statement_id)?.clone();
13017    let database = sqlite_database_mut(process, statement_state.database_id)?;
13018    sqlite_query_rows(
13019        &mut database.connection,
13020        &statement_state.sql,
13021        params,
13022        statement_state.return_arrays,
13023        statement_state.read_bigints,
13024        statement_state.allow_bare_named_parameters,
13025        statement_state.allow_unknown_named_parameters,
13026    )
13027}
13028
13029fn sqlite_statement_columns(
13030    process: &mut ActiveProcess,
13031    request: &JavascriptSyncRpcRequest,
13032) -> Result<Value, SidecarError> {
13033    let statement_id =
13034        javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.columns statement id")?;
13035    let statement_state = sqlite_statement(process, statement_id)?.clone();
13036    let database = sqlite_database_mut(process, statement_state.database_id)?;
13037    let statement = database
13038        .connection
13039        .prepare(&statement_state.sql)
13040        .map_err(sqlite_error)?;
13041    Ok(Value::Array(
13042        statement
13043            .column_names()
13044            .iter()
13045            .map(|name| json!({ "name": name }))
13046            .collect(),
13047    ))
13048}
13049
13050fn sqlite_query_rows(
13051    connection: &mut SqliteConnection,
13052    sql: &str,
13053    params: Option<&Value>,
13054    return_arrays: bool,
13055    read_bigints: bool,
13056    allow_bare_named_parameters: bool,
13057    allow_unknown_named_parameters: bool,
13058) -> Result<Value, SidecarError> {
13059    let mut statement = connection.prepare(sql).map_err(sqlite_error)?;
13060    let column_names = statement
13061        .column_names()
13062        .iter()
13063        .map(|name| (*name).to_owned())
13064        .collect::<Vec<_>>();
13065    let column_count = statement.column_count();
13066    bind_sqlite_parameters(
13067        &mut statement,
13068        params,
13069        allow_bare_named_parameters,
13070        allow_unknown_named_parameters,
13071    )?;
13072    let mut rows = statement.raw_query();
13073    let mut encoded_rows = Vec::new();
13074    while let Some(row) = rows.next().map_err(sqlite_error)? {
13075        encoded_rows.push(encode_sqlite_row(
13076            row,
13077            &column_names,
13078            column_count,
13079            return_arrays,
13080            read_bigints,
13081        )?);
13082    }
13083    Ok(Value::Array(encoded_rows))
13084}
13085
13086fn encode_sqlite_row(
13087    row: &rusqlite::Row<'_>,
13088    column_names: &[String],
13089    column_count: usize,
13090    return_arrays: bool,
13091    read_bigints: bool,
13092) -> Result<Value, SidecarError> {
13093    if return_arrays {
13094        let mut values = Vec::with_capacity(column_count);
13095        for index in 0..column_count {
13096            values.push(encode_sqlite_value_ref(
13097                row.get_ref(index).map_err(sqlite_error)?,
13098                read_bigints,
13099            )?);
13100        }
13101        return Ok(Value::Array(values));
13102    }
13103
13104    let mut object = Map::with_capacity(column_count);
13105    for (index, name) in column_names.iter().enumerate() {
13106        object.insert(
13107            name.clone(),
13108            encode_sqlite_value_ref(row.get_ref(index).map_err(sqlite_error)?, read_bigints)?,
13109        );
13110    }
13111    Ok(Value::Object(object))
13112}
13113
13114fn encode_sqlite_value_ref(
13115    value: SqliteValueRef<'_>,
13116    read_bigints: bool,
13117) -> Result<Value, SidecarError> {
13118    Ok(match value {
13119        SqliteValueRef::Null => Value::Null,
13120        SqliteValueRef::Integer(number) => encode_sqlite_integer(number, read_bigints),
13121        SqliteValueRef::Real(number) => json!(number),
13122        SqliteValueRef::Text(text) => Value::String(String::from_utf8_lossy(text).into_owned()),
13123        SqliteValueRef::Blob(bytes) => json!({
13124            "__agentosSqliteType": "uint8array",
13125            "value": base64::engine::general_purpose::STANDARD.encode(bytes),
13126        }),
13127    })
13128}
13129
13130fn encode_sqlite_integer(number: i64, read_bigints: bool) -> Value {
13131    if read_bigints || number.abs() > SQLITE_JS_SAFE_INTEGER_MAX {
13132        json!({
13133            "__agentosSqliteType": "bigint",
13134            "value": number.to_string(),
13135        })
13136    } else {
13137        json!(number)
13138    }
13139}
13140
13141fn bind_sqlite_parameters(
13142    statement: &mut SqliteStatement<'_>,
13143    params: Option<&Value>,
13144    allow_bare_named_parameters: bool,
13145    allow_unknown_named_parameters: bool,
13146) -> Result<(), SidecarError> {
13147    let Some(params) = params else {
13148        return Ok(());
13149    };
13150    match params {
13151        Value::Null => Ok(()),
13152        Value::Array(values) => {
13153            for (index, value) in values.iter().enumerate() {
13154                statement
13155                    .raw_bind_parameter(index + 1, decode_sqlite_parameter(value)?)
13156                    .map_err(sqlite_error)?;
13157            }
13158            Ok(())
13159        }
13160        Value::Object(map)
13161            if map
13162                .get("__agentosSqliteType")
13163                .and_then(Value::as_str)
13164                .is_none() =>
13165        {
13166            for (key, value) in map {
13167                let index =
13168                    resolve_sqlite_parameter_index(statement, key, allow_bare_named_parameters)?;
13169                let Some(index) = index else {
13170                    if allow_unknown_named_parameters {
13171                        continue;
13172                    }
13173                    return Err(SidecarError::InvalidState(format!(
13174                        "sqlite named parameter not found: {key}"
13175                    )));
13176                };
13177                statement
13178                    .raw_bind_parameter(index, decode_sqlite_parameter(value)?)
13179                    .map_err(sqlite_error)?;
13180            }
13181            Ok(())
13182        }
13183        other => statement
13184            .raw_bind_parameter(1, decode_sqlite_parameter(other)?)
13185            .map_err(sqlite_error),
13186    }
13187}
13188
13189fn resolve_sqlite_parameter_index(
13190    statement: &mut SqliteStatement<'_>,
13191    key: &str,
13192    allow_bare_named_parameters: bool,
13193) -> Result<Option<usize>, SidecarError> {
13194    let mut candidates = vec![key.to_owned()];
13195    if allow_bare_named_parameters
13196        && !key.starts_with(':')
13197        && !key.starts_with('@')
13198        && !key.starts_with('$')
13199    {
13200        candidates.push(format!(":{key}"));
13201        candidates.push(format!("@{key}"));
13202        candidates.push(format!("${key}"));
13203    }
13204    for candidate in candidates {
13205        if let Some(index) = statement
13206            .parameter_index(&candidate)
13207            .map_err(sqlite_error)?
13208        {
13209            return Ok(Some(index));
13210        }
13211    }
13212    Ok(None)
13213}
13214
13215fn decode_sqlite_parameter(value: &Value) -> Result<rusqlite::types::Value, SidecarError> {
13216    Ok(match value {
13217        Value::Null => rusqlite::types::Value::Null,
13218        Value::Bool(value) => rusqlite::types::Value::Integer(i64::from(*value)),
13219        Value::Number(value) => match (value.as_i64(), value.as_f64()) {
13220            (Some(integer), _) => rusqlite::types::Value::Integer(integer),
13221            (_, Some(real)) => rusqlite::types::Value::Real(real),
13222            _ => {
13223                return Err(SidecarError::InvalidState(String::from(
13224                    "sqlite parameter number is not representable",
13225                )));
13226            }
13227        },
13228        Value::String(value) => rusqlite::types::Value::Text(value.clone()),
13229        Value::Array(_) => {
13230            return Err(SidecarError::InvalidState(String::from(
13231                "sqlite parameters do not support nested arrays",
13232            )));
13233        }
13234        Value::Object(map) => match map.get("__agentosSqliteType").and_then(Value::as_str) {
13235            Some("bigint") => rusqlite::types::Value::Integer(
13236                map.get("value")
13237                    .and_then(Value::as_str)
13238                    .ok_or_else(|| {
13239                        SidecarError::InvalidState(String::from(
13240                            "sqlite bigint parameter missing string value",
13241                        ))
13242                    })?
13243                    .parse::<i64>()
13244                    .map_err(|error| {
13245                        SidecarError::InvalidState(format!(
13246                            "sqlite bigint parameter is not a signed 64-bit integer: {error}"
13247                        ))
13248                    })?,
13249            ),
13250            Some("uint8array") => rusqlite::types::Value::Blob(
13251                base64::engine::general_purpose::STANDARD
13252                    .decode(map.get("value").and_then(Value::as_str).ok_or_else(|| {
13253                        SidecarError::InvalidState(String::from(
13254                            "sqlite blob parameter missing base64 value",
13255                        ))
13256                    })?)
13257                    .map_err(|error| {
13258                        SidecarError::InvalidState(format!(
13259                            "sqlite blob parameter contains invalid base64: {error}"
13260                        ))
13261                    })?,
13262            ),
13263            Some(other) => {
13264                return Err(SidecarError::InvalidState(format!(
13265                    "unsupported sqlite tagged parameter type {other}"
13266                )));
13267            }
13268            None => {
13269                return Err(SidecarError::InvalidState(String::from(
13270                    "sqlite named parameter objects must be passed as the top-level params object",
13271                )));
13272            }
13273        },
13274    })
13275}
13276
13277fn close_sqlite_database(
13278    kernel: &mut SidecarKernel,
13279    process: &mut ActiveProcess,
13280    database_id: u64,
13281) -> Result<(), SidecarError> {
13282    let mut database = process
13283        .sqlite_databases
13284        .remove(&database_id)
13285        .ok_or_else(|| {
13286            SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
13287        })?;
13288    process
13289        .sqlite_statements
13290        .retain(|_, statement| statement.database_id != database_id);
13291    sqlite_sync_database(kernel, process.kernel_pid, &mut database)?;
13292    let host_path = database.host_path.clone();
13293    drop(database);
13294    cleanup_sqlite_host_artifacts(host_path.as_deref())?;
13295    Ok(())
13296}
13297
13298fn ensure_per_process_state_handle_capacity(len: usize, label: &str) -> Result<(), SidecarError> {
13299    if len >= MAX_PER_PROCESS_STATE_HANDLES {
13300        return Err(SidecarError::InvalidState(format!(
13301            "{label} handle limit exceeded: limit is {MAX_PER_PROCESS_STATE_HANDLES}"
13302        )));
13303    }
13304    Ok(())
13305}
13306
13307fn sqlite_sync_database(
13308    kernel: &mut SidecarKernel,
13309    kernel_pid: u32,
13310    database: &mut ActiveSqliteDatabase,
13311) -> Result<(), SidecarError> {
13312    if !database.dirty
13313        || database.transaction_depth > 0
13314        || database.read_only
13315        || database.host_path.is_none()
13316        || database.vm_path.is_none()
13317    {
13318        return Ok(());
13319    }
13320
13321    let _ = database
13322        .connection
13323        .execute_batch("PRAGMA wal_checkpoint(TRUNCATE)");
13324    let host_path = database.host_path.as_ref().expect("sqlite host path");
13325    if !host_path.exists() {
13326        return Ok(());
13327    }
13328    ensure_vm_parent_dir(
13329        kernel,
13330        kernel_pid,
13331        database.vm_path.as_deref().expect("sqlite vm path"),
13332    )?;
13333    let contents = fs::read(host_path).map_err(|error| {
13334        SidecarError::Io(format!(
13335            "failed to read sqlite temp database {}: {error}",
13336            host_path.display()
13337        ))
13338    })?;
13339    kernel
13340        .write_file_for_process(
13341            EXECUTION_DRIVER_NAME,
13342            kernel_pid,
13343            database.vm_path.as_deref().expect("sqlite vm path"),
13344            contents,
13345            None,
13346        )
13347        .map_err(kernel_error)?;
13348    database.dirty = false;
13349    Ok(())
13350}
13351
13352fn cleanup_sqlite_host_artifacts(host_path: Option<&Path>) -> Result<(), SidecarError> {
13353    let Some(host_path) = host_path else {
13354        return Ok(());
13355    };
13356    let parent = host_path.parent().map(PathBuf::from);
13357    for suffix in ["", "-wal", "-shm"] {
13358        let path = PathBuf::from(format!("{}{}", host_path.display(), suffix));
13359        if path.exists() {
13360            fs::remove_file(&path).map_err(|error| {
13361                SidecarError::Io(format!(
13362                    "failed to remove sqlite temp artifact {}: {error}",
13363                    path.display()
13364                ))
13365            })?;
13366        }
13367    }
13368    if let Some(parent) = parent {
13369        let _ = fs::remove_dir_all(parent);
13370    }
13371    Ok(())
13372}
13373
13374fn ensure_vm_parent_dir(
13375    kernel: &mut SidecarKernel,
13376    kernel_pid: u32,
13377    path: &str,
13378) -> Result<(), SidecarError> {
13379    let parent = dirname(path);
13380    if parent == "/" || parent == "." {
13381        return Ok(());
13382    }
13383    let mut current = String::new();
13384    for segment in parent.split('/').filter(|segment| !segment.is_empty()) {
13385        current.push('/');
13386        current.push_str(segment);
13387        if !kernel
13388            .exists_for_process(EXECUTION_DRIVER_NAME, kernel_pid, &current)
13389            .map_err(kernel_error)?
13390        {
13391            kernel
13392                .mkdir_for_process(EXECUTION_DRIVER_NAME, kernel_pid, &current, false, None)
13393                .map_err(kernel_error)?;
13394        }
13395    }
13396    Ok(())
13397}
13398
13399fn sqlite_database(
13400    process: &ActiveProcess,
13401    database_id: u64,
13402) -> Result<&ActiveSqliteDatabase, SidecarError> {
13403    process.sqlite_databases.get(&database_id).ok_or_else(|| {
13404        SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
13405    })
13406}
13407
13408fn sqlite_database_mut(
13409    process: &mut ActiveProcess,
13410    database_id: u64,
13411) -> Result<&mut ActiveSqliteDatabase, SidecarError> {
13412    process
13413        .sqlite_databases
13414        .get_mut(&database_id)
13415        .ok_or_else(|| {
13416            SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
13417        })
13418}
13419
13420fn sqlite_statement(
13421    process: &ActiveProcess,
13422    statement_id: u64,
13423) -> Result<&ActiveSqliteStatement, SidecarError> {
13424    process.sqlite_statements.get(&statement_id).ok_or_else(|| {
13425        SidecarError::InvalidState(format!("sqlite statement handle not found: {statement_id}"))
13426    })
13427}
13428
13429fn sqlite_statement_mut(
13430    process: &mut ActiveProcess,
13431    statement_id: u64,
13432) -> Result<&mut ActiveSqliteStatement, SidecarError> {
13433    process
13434        .sqlite_statements
13435        .get_mut(&statement_id)
13436        .ok_or_else(|| {
13437            SidecarError::InvalidState(format!("sqlite statement handle not found: {statement_id}"))
13438        })
13439}
13440
13441fn mark_sqlite_mutation(database: &mut ActiveSqliteDatabase, sql: &str) {
13442    let normalized = sql.trim_start().to_ascii_lowercase();
13443    if normalized.starts_with("begin") || normalized.starts_with("savepoint") {
13444        database.dirty = true;
13445        database.transaction_depth += 1;
13446        return;
13447    }
13448    if normalized.starts_with("commit") || normalized.starts_with("release savepoint") {
13449        database.dirty = true;
13450        database.transaction_depth = database.transaction_depth.saturating_sub(1);
13451        return;
13452    }
13453    if normalized.starts_with("rollback") && !normalized.starts_with("rollback to") {
13454        database.dirty = true;
13455        database.transaction_depth = database.transaction_depth.saturating_sub(1);
13456        return;
13457    }
13458    if normalized.starts_with("insert")
13459        || normalized.starts_with("update")
13460        || normalized.starts_with("delete")
13461        || normalized.starts_with("replace")
13462        || normalized.starts_with("create")
13463        || normalized.starts_with("alter")
13464        || normalized.starts_with("drop")
13465        || normalized.starts_with("vacuum")
13466        || normalized.starts_with("reindex")
13467        || normalized.starts_with("analyze")
13468        || normalized.starts_with("attach")
13469        || normalized.starts_with("detach")
13470        || normalized.starts_with("pragma")
13471    {
13472        database.dirty = true;
13473    }
13474}
13475
13476fn sqlite_option_bool(options: Option<&Value>, key: &str) -> Option<bool> {
13477    options
13478        .and_then(|value| value.get(key))
13479        .and_then(Value::as_bool)
13480}
13481
13482fn sqlite_option_u64(options: Option<&Value>, key: &str) -> Option<u64> {
13483    options
13484        .and_then(|value| value.get(key))
13485        .and_then(Value::as_u64)
13486}
13487
13488fn sqlite_error(error: rusqlite::Error) -> SidecarError {
13489    SidecarError::InvalidState(format!("sqlite error: {error}"))
13490}
13491
13492pub(crate) fn javascript_sync_rpc_arg_str<'a>(
13493    args: &'a [Value],
13494    index: usize,
13495    label: &str,
13496) -> Result<&'a str, SidecarError> {
13497    args.get(index)
13498        .and_then(Value::as_str)
13499        .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a string argument")))
13500}
13501
13502pub(crate) fn javascript_sync_rpc_arg_bool(
13503    args: &[Value],
13504    index: usize,
13505    label: &str,
13506) -> Result<bool, SidecarError> {
13507    args.get(index)
13508        .and_then(Value::as_bool)
13509        .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a boolean argument")))
13510}
13511
13512pub(crate) fn javascript_sync_rpc_encoding(args: &[Value]) -> Option<String> {
13513    args.get(1).and_then(|value| {
13514        value.as_str().map(str::to_owned).or_else(|| {
13515            value
13516                .get("encoding")
13517                .and_then(Value::as_str)
13518                .map(str::to_owned)
13519        })
13520    })
13521}
13522
13523pub(crate) fn javascript_sync_rpc_option_bool(
13524    args: &[Value],
13525    index: usize,
13526    key: &str,
13527) -> Option<bool> {
13528    let value = args.get(index)?;
13529    if key == "recursive" {
13530        if let Some(boolean) = value.as_bool() {
13531            return Some(boolean);
13532        }
13533    }
13534    value.get(key).and_then(Value::as_bool)
13535}
13536
13537pub(crate) fn javascript_sync_rpc_option_u32(
13538    args: &[Value],
13539    index: usize,
13540    key: &str,
13541) -> Result<Option<u32>, SidecarError> {
13542    let Some(value) = args.get(index).and_then(|value| {
13543        if value.is_object() {
13544            value.get(key)
13545        } else if key == "mode" && value.is_number() {
13546            Some(value)
13547        } else {
13548            None
13549        }
13550    }) else {
13551        return Ok(None);
13552    };
13553    if value.is_null() {
13554        return Ok(None);
13555    }
13556
13557    let numeric = value
13558        .as_u64()
13559        .or_else(|| {
13560            value
13561                .as_f64()
13562                .filter(|number| number.is_finite() && *number >= 0.0)
13563                .map(|number| number as u64)
13564        })
13565        .ok_or_else(|| SidecarError::InvalidState(format!("{key} must be numeric")))?;
13566
13567    u32::try_from(numeric)
13568        .map(Some)
13569        .map_err(|_| SidecarError::InvalidState(format!("{key} must fit within u32")))
13570}
13571
13572pub(crate) fn javascript_sync_rpc_arg_u32(
13573    args: &[Value],
13574    index: usize,
13575    label: &str,
13576) -> Result<u32, SidecarError> {
13577    let value = javascript_sync_rpc_arg_u64(args, index, label)?;
13578    u32::try_from(value)
13579        .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")))
13580}
13581
13582pub(crate) fn javascript_sync_rpc_arg_i32(
13583    args: &[Value],
13584    index: usize,
13585    label: &str,
13586) -> Result<i32, SidecarError> {
13587    let Some(value) = args.get(index) else {
13588        return Err(SidecarError::InvalidState(format!("{label} is required")));
13589    };
13590
13591    let numeric = value
13592        .as_i64()
13593        .or_else(|| {
13594            value
13595                .as_f64()
13596                .filter(|number| number.is_finite())
13597                .map(|number| number as i64)
13598        })
13599        .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a numeric argument")))?;
13600
13601    i32::try_from(numeric)
13602        .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within i32")))
13603}
13604
13605pub(crate) fn javascript_sync_rpc_arg_u32_optional(
13606    args: &[Value],
13607    index: usize,
13608    label: &str,
13609) -> Result<Option<u32>, SidecarError> {
13610    javascript_sync_rpc_arg_u64_optional(args, index, label)?
13611        .map(|value| {
13612            u32::try_from(value)
13613                .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")))
13614        })
13615        .transpose()
13616}
13617
13618pub(crate) fn javascript_sync_rpc_arg_u64(
13619    args: &[Value],
13620    index: usize,
13621    label: &str,
13622) -> Result<u64, SidecarError> {
13623    let Some(value) = args.get(index) else {
13624        return Err(SidecarError::InvalidState(format!("{label} is required")));
13625    };
13626
13627    value
13628        .as_u64()
13629        .or_else(|| {
13630            value
13631                .as_f64()
13632                .filter(|number| number.is_finite() && *number >= 0.0)
13633                .map(|number| number as u64)
13634        })
13635        .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a numeric argument")))
13636}
13637
13638pub(crate) fn javascript_sync_rpc_arg_u64_optional(
13639    args: &[Value],
13640    index: usize,
13641    label: &str,
13642) -> Result<Option<u64>, SidecarError> {
13643    let Some(value) = args.get(index) else {
13644        return Ok(None);
13645    };
13646    if value.is_null() {
13647        return Ok(None);
13648    }
13649    javascript_sync_rpc_arg_u64(args, index, label).map(Some)
13650}
13651
13652pub(crate) fn javascript_sync_rpc_bytes_arg(
13653    args: &[Value],
13654    index: usize,
13655    label: &str,
13656) -> Result<Vec<u8>, SidecarError> {
13657    let Some(value) = args.get(index) else {
13658        return Err(SidecarError::InvalidState(format!("{label} is required")));
13659    };
13660
13661    if let Some(text) = value.as_str() {
13662        return Ok(text.as_bytes().to_vec());
13663    }
13664
13665    let Some(base64_value) = value
13666        .get("__agentOSType")
13667        .and_then(Value::as_str)
13668        .filter(|kind| *kind == "bytes")
13669        .and_then(|_| value.get("base64"))
13670        .and_then(Value::as_str)
13671    else {
13672        return Err(SidecarError::InvalidState(format!(
13673            "{label} must be a string or encoded bytes payload"
13674        )));
13675    };
13676
13677    base64::engine::general_purpose::STANDARD
13678        .decode(base64_value)
13679        .map_err(|error| {
13680            SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
13681        })
13682}
13683
13684pub(crate) fn javascript_sync_rpc_bytes_value(bytes: &[u8]) -> Value {
13685    json!({
13686        "__agentOSType": "bytes",
13687        "base64": base64::engine::general_purpose::STANDARD.encode(bytes),
13688    })
13689}
13690
13691#[derive(Debug, Deserialize)]
13692struct KernelPollFdRequest {
13693    fd: u32,
13694    events: u16,
13695}
13696
13697#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
13698struct KernelPollFdResponse {
13699    fd: u32,
13700    events: u16,
13701    revents: u16,
13702}
13703
13704fn javascript_sync_rpc_base64_arg(
13705    args: &[Value],
13706    index: usize,
13707    label: &str,
13708) -> Result<Vec<u8>, SidecarError> {
13709    let value = javascript_sync_rpc_arg_str(args, index, label)?;
13710    base64::engine::general_purpose::STANDARD
13711        .decode(value)
13712        .map_err(|error| {
13713            SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
13714        })
13715}
13716
13717// ── Sync-RPC round-trip counting (opt-in via AGENTOS_SYNC_RPC_TRACE=1) ──
13718// Each guest fs/module/net sync RPC funnels through service_javascript_sync_rpc,
13719// so this is the one place to measure the kernel-VFS "syscall storm" that makes
13720// metadata-heavy phases (resourceLoader.reload, createAgentSession) 40-90x slower
13721// in the VM than on bare node. Emits a perf log line every 200 calls with the
13722// running per-method breakdown.
13723static SYNC_RPC_STATS: std::sync::OnceLock<
13724    std::sync::Mutex<std::collections::BTreeMap<String, u64>>,
13725> = std::sync::OnceLock::new();
13726
13727fn sync_rpc_trace_enabled() -> bool {
13728    std::env::var("AGENTOS_SYNC_RPC_TRACE").as_deref() == Ok("1")
13729}
13730
13731fn record_sync_rpc(method: &str) {
13732    let stats =
13733        SYNC_RPC_STATS.get_or_init(|| std::sync::Mutex::new(std::collections::BTreeMap::new()));
13734    let Ok(mut map) = stats.lock() else {
13735        return;
13736    };
13737    *map.entry(method.to_string()).or_insert(0) += 1;
13738    let total: u64 = map.values().sum();
13739    if total == 1 || total.is_multiple_of(50) {
13740        let mut top: Vec<(&String, &u64)> = map.iter().collect();
13741        top.sort_by(|a, b| b.1.cmp(a.1));
13742        let breakdown = top
13743            .iter()
13744            .take(8)
13745            .map(|(m, c)| format!("{m}={c}"))
13746            .collect::<Vec<_>>()
13747            .join(" ");
13748        tracing::info!(target: "secure_exec_sidecar::perf", total, %breakdown, "sync_rpc count");
13749    }
13750}
13751
13752pub(crate) fn service_javascript_sync_rpc<B>(
13753    request: JavascriptSyncRpcServiceRequest<'_, B>,
13754) -> Result<Value, SidecarError>
13755where
13756    B: NativeSidecarBridge + Send + 'static,
13757    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
13758{
13759    if sync_rpc_trace_enabled() {
13760        record_sync_rpc(request.sync_request.method.as_str());
13761    }
13762    let JavascriptSyncRpcServiceRequest {
13763        bridge,
13764        vm_id,
13765        dns,
13766        socket_paths,
13767        kernel,
13768        process,
13769        sync_request: request,
13770        resource_limits,
13771        network_counts,
13772    } = request;
13773    match request.method.as_str() {
13774        // Module resolution / loading / format detection read the kernel VFS so
13775        // the resolver sees exactly what the guest and `kernel.readFile()` see.
13776        "_resolveModule"
13777        | "_resolveModuleSync"
13778        | "__resolve_module"
13779        | "_batchResolveModules"
13780        | "__batch_resolve_modules"
13781        | "_loadFile"
13782        | "_loadFileSync"
13783        | "__load_file"
13784        | "_moduleFormat"
13785        | "__module_format" => service_javascript_module_sync_rpc(kernel, process, request),
13786        // Polyfills are static guest expressions, not VFS reads.
13787        "_loadPolyfill" | "__load_polyfill" => {
13788            service_javascript_internal_bridge_sync_rpc(process, request)
13789        }
13790        "__kernel_stdin_read" => match &process.execution {
13791            ActiveExecution::Javascript(execution) => execution
13792                .read_kernel_stdin_sync_rpc(request)
13793                .map_err(|error| SidecarError::Execution(error.to_string())),
13794            ActiveExecution::Python(_) | ActiveExecution::Wasm(_) | ActiveExecution::Tool(_) => {
13795                service_javascript_kernel_stdin_sync_rpc(kernel, process, request)
13796            }
13797        },
13798        "__kernel_stdio_write" => {
13799            service_javascript_kernel_stdio_write_sync_rpc(kernel, process, request)
13800        }
13801        "__kernel_poll" => service_javascript_kernel_poll_sync_rpc(kernel, process, request),
13802        "__pty_set_raw_mode" => {
13803            service_javascript_pty_set_raw_mode_sync_rpc(kernel, process, request)
13804        }
13805        "crypto.hashDigest"
13806        | "crypto.hmacDigest"
13807        | "crypto.pbkdf2"
13808        | "crypto.scrypt"
13809        | "crypto.cipheriv"
13810        | "crypto.decipheriv"
13811        | "crypto.cipherivCreate"
13812        | "crypto.cipherivUpdate"
13813        | "crypto.cipherivFinal"
13814        | "crypto.sign"
13815        | "crypto.verify"
13816        | "crypto.asymmetricOp"
13817        | "crypto.createKeyObject"
13818        | "crypto.generateKeyPairSync"
13819        | "crypto.generateKeySync"
13820        | "crypto.generatePrimeSync"
13821        | "crypto.diffieHellman"
13822        | "crypto.diffieHellmanGroup"
13823        | "crypto.diffieHellmanSessionCreate"
13824        | "crypto.diffieHellmanSessionCall"
13825        | "crypto.diffieHellmanSessionDestroy"
13826        | "crypto.subtle" => service_javascript_crypto_sync_rpc(process, request),
13827        "dns.lookup" | "dns.resolve" | "dns.resolve4" | "dns.resolve6" => {
13828            service_javascript_dns_sync_rpc(bridge, kernel, vm_id, dns, request)
13829        }
13830        "net.http_listen" | "net.http_close" | "net.http_wait" | "net.http_respond" => {
13831            service_javascript_net_sync_rpc(JavascriptNetSyncRpcServiceRequest {
13832                bridge,
13833                vm_id,
13834                dns,
13835                socket_paths,
13836                kernel,
13837                process,
13838                sync_request: request,
13839                resource_limits,
13840                network_counts,
13841            })
13842        }
13843        "net.http2_server_listen"
13844        | "net.http2_server_poll"
13845        | "net.http2_server_close"
13846        | "net.http2_server_respond"
13847        | "net.http2_server_wait"
13848        | "net.http2_session_connect"
13849        | "net.http2_session_request"
13850        | "net.http2_session_settings"
13851        | "net.http2_session_set_local_window_size"
13852        | "net.http2_session_goaway"
13853        | "net.http2_session_close"
13854        | "net.http2_session_destroy"
13855        | "net.http2_session_poll"
13856        | "net.http2_session_wait"
13857        | "net.http2_stream_respond"
13858        | "net.http2_stream_push_stream"
13859        | "net.http2_stream_write"
13860        | "net.http2_stream_end"
13861        | "net.http2_stream_close"
13862        | "net.http2_stream_pause"
13863        | "net.http2_stream_resume"
13864        | "net.http2_stream_respond_with_file" => {
13865            service_javascript_http2_sync_rpc(JavascriptHttp2SyncRpcServiceRequest {
13866                bridge,
13867                kernel,
13868                vm_id,
13869                dns,
13870                socket_paths,
13871                process,
13872                sync_request: request,
13873                resource_limits,
13874                network_counts,
13875            })
13876        }
13877        "net.connect"
13878        | "net.reserve_tcp_port"
13879        | "net.release_tcp_port"
13880        | "net.listen"
13881        | "net.poll"
13882        | "net.socket_wait_connect"
13883        | "net.socket_read"
13884        | "net.socket_set_no_delay"
13885        | "net.socket_set_keep_alive"
13886        | "net.socket_upgrade_tls"
13887        | "net.socket_get_tls_client_hello"
13888        | "net.socket_tls_query"
13889        | "net.server_poll"
13890        | "net.server_accept"
13891        | "net.server_connections"
13892        | "net.upgrade_socket_write"
13893        | "net.upgrade_socket_end"
13894        | "net.upgrade_socket_destroy"
13895        | "net.write"
13896        | "net.shutdown"
13897        | "net.destroy"
13898        | "net.server_close"
13899        | "tls.get_ciphers" => {
13900            service_javascript_net_sync_rpc(JavascriptNetSyncRpcServiceRequest {
13901                bridge,
13902                vm_id,
13903                dns,
13904                socket_paths,
13905                kernel,
13906                process,
13907                sync_request: request,
13908                resource_limits,
13909                network_counts,
13910            })
13911        }
13912        "dgram.createSocket"
13913        | "dgram.bind"
13914        | "dgram.send"
13915        | "dgram.poll"
13916        | "dgram.close"
13917        | "dgram.address"
13918        | "dgram.setBufferSize"
13919        | "dgram.getBufferSize" => {
13920            service_javascript_dgram_sync_rpc(JavascriptDgramSyncRpcServiceRequest {
13921                bridge,
13922                kernel,
13923                vm_id,
13924                dns,
13925                socket_paths,
13926                process,
13927                sync_request: request,
13928                resource_limits,
13929                network_counts,
13930            })
13931        }
13932        "sqlite.constants"
13933        | "sqlite.open"
13934        | "sqlite.close"
13935        | "sqlite.exec"
13936        | "sqlite.query"
13937        | "sqlite.prepare"
13938        | "sqlite.location"
13939        | "sqlite.checkpoint"
13940        | "sqlite.statement.run"
13941        | "sqlite.statement.get"
13942        | "sqlite.statement.all"
13943        | "sqlite.statement.iterate"
13944        | "sqlite.statement.columns"
13945        | "sqlite.statement.setReturnArrays"
13946        | "sqlite.statement.setReadBigInts"
13947        | "sqlite.statement.setAllowBareNamedParameters"
13948        | "sqlite.statement.setAllowUnknownNamedParameters"
13949        | "sqlite.statement.finalize" => {
13950            service_javascript_sqlite_sync_rpc(kernel, process, request)
13951        }
13952        "process.kill" => {
13953            let target_pid =
13954                javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?;
13955            let signal = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?;
13956            let parsed_signal = parse_signal(signal)?;
13957            if parsed_signal == 0 {
13958                kernel
13959                    .signal_process(EXECUTION_DRIVER_NAME, target_pid, parsed_signal)
13960                    .map_err(kernel_error)?;
13961                return Ok(Value::Null);
13962            }
13963            let process_pid = i32::try_from(process.kernel_pid)
13964                .map_err(|_| SidecarError::InvalidState("process pid exceeds i32".into()))?;
13965            if target_pid != process_pid {
13966                return Err(SidecarError::InvalidState(format!(
13967                    "unknown process pid {target_pid}"
13968                )));
13969            }
13970            process.pending_self_signal_exit = None;
13971            if parsed_signal != 0
13972                && !matches!(
13973                    canonical_signal_name(parsed_signal),
13974                    Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
13975                )
13976            {
13977                process.pending_self_signal_exit = Some(parsed_signal);
13978            }
13979            Ok(json!({
13980                "self": true,
13981                "action": "default",
13982            }))
13983        }
13984        "process.umask" => {
13985            let new_mask = javascript_sync_rpc_arg_u32_optional(&request.args, 0, "process umask")?;
13986            kernel
13987                .umask(EXECUTION_DRIVER_NAME, process.kernel_pid, new_mask)
13988                .map(|mask| json!(mask))
13989                .map_err(kernel_error)
13990        }
13991        "fs.chmodSync" | "fs.promises.chmod" => {
13992            let response =
13993                service_javascript_fs_sync_rpc(kernel, process, process.kernel_pid, request)?;
13994            mirror_process_chmod_to_host(process, request)?;
13995            Ok(response)
13996        }
13997        _ => service_javascript_fs_sync_rpc(kernel, process, process.kernel_pid, request),
13998    }
13999}
14000
14001fn service_javascript_internal_bridge_sync_rpc(
14002    process: &ActiveProcess,
14003    request: &JavascriptSyncRpcRequest,
14004) -> Result<Value, SidecarError> {
14005    // Module resolution / loading / format now reads the kernel VFS via
14006    // `service_javascript_module_sync_rpc`. This host-context path only handles
14007    // polyfills, which are static guest expressions independent of the FS.
14008    let method = match request.method.as_str() {
14009        "_loadPolyfill" | "__load_polyfill" => "_loadPolyfill",
14010        other => {
14011            return Err(SidecarError::InvalidState(format!(
14012                "unsupported JavaScript internal bridge method {other}"
14013            )));
14014        }
14015    };
14016
14017    handle_internal_bridge_call_from_host_context(
14018        &process.host_cwd,
14019        &process.guest_cwd,
14020        &process.env,
14021        method,
14022        &request.args,
14023    )
14024    .ok_or_else(|| {
14025        SidecarError::InvalidState(format!(
14026            "JavaScript internal bridge method {method} returned no value"
14027        ))
14028    })
14029}
14030
14031fn mirror_process_chmod_to_host(
14032    process: &ActiveProcess,
14033    request: &JavascriptSyncRpcRequest,
14034) -> Result<(), SidecarError> {
14035    let guest_path = javascript_sync_rpc_arg_str(&request.args, 0, "filesystem chmod path")?;
14036    let mode = javascript_sync_rpc_arg_u32(&request.args, 1, "filesystem chmod mode")? & 0o7777;
14037    let Some(host_path) = resolve_process_guest_path_to_host(process, guest_path) else {
14038        return Ok(());
14039    };
14040    if !host_path.exists() {
14041        return Ok(());
14042    }
14043    fs::set_permissions(&host_path, fs::Permissions::from_mode(mode)).map_err(|error| {
14044        SidecarError::Io(format!(
14045            "failed to mirror chmod to host path {}: {error}",
14046            host_path.display()
14047        ))
14048    })
14049}
14050
14051fn resolve_process_guest_path_to_host(
14052    process: &ActiveProcess,
14053    guest_path: &str,
14054) -> Option<PathBuf> {
14055    let normalized_guest_path = if guest_path.starts_with('/') {
14056        normalize_path(guest_path)
14057    } else {
14058        normalize_path(&format!(
14059            "{}/{}",
14060            process.guest_cwd.trim_end_matches('/'),
14061            guest_path
14062        ))
14063    };
14064    if let Some(host_path) =
14065        host_path_from_runtime_guest_mappings(&process.env, &normalized_guest_path)
14066    {
14067        return Some(host_path);
14068    }
14069    let normalized_guest_cwd = normalize_path(&process.guest_cwd);
14070    let mut host_root = normalize_host_path(&process.host_cwd);
14071    for _ in normalized_guest_cwd
14072        .trim_start_matches('/')
14073        .split('/')
14074        .filter(|segment| !segment.is_empty())
14075    {
14076        host_root = host_root.parent()?.to_path_buf();
14077    }
14078    if normalized_guest_path == "/" {
14079        Some(host_root)
14080    } else {
14081        Some(host_root.join(normalized_guest_path.trim_start_matches('/')))
14082    }
14083}
14084
14085pub(crate) fn service_javascript_crypto_sync_rpc(
14086    process: &mut ActiveProcess,
14087    request: &JavascriptSyncRpcRequest,
14088) -> Result<Value, SidecarError> {
14089    match request.method.as_str() {
14090        "crypto.hashDigest" => {
14091            let algorithm = javascript_crypto_digest_algorithm(
14092                &request.args,
14093                0,
14094                "crypto.hashDigest algorithm",
14095            )?;
14096            let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.hashDigest data")?;
14097            Ok(Value::String(
14098                base64::engine::general_purpose::STANDARD.encode(algorithm.digest(&data)),
14099            ))
14100        }
14101        "crypto.hmacDigest" => {
14102            let algorithm = javascript_crypto_digest_algorithm(
14103                &request.args,
14104                0,
14105                "crypto.hmacDigest algorithm",
14106            )?;
14107            let key = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.hmacDigest key")?;
14108            let data = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.hmacDigest data")?;
14109            Ok(Value::String(
14110                base64::engine::general_purpose::STANDARD.encode(algorithm.hmac(&key, &data)?),
14111            ))
14112        }
14113        "crypto.pbkdf2" => {
14114            let password =
14115                javascript_sync_rpc_base64_arg(&request.args, 0, "crypto.pbkdf2 password")?;
14116            let salt = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.pbkdf2 salt")?;
14117            let iterations =
14118                javascript_sync_rpc_arg_u32(&request.args, 2, "crypto.pbkdf2 iterations")?;
14119            if iterations == 0 {
14120                return Err(SidecarError::InvalidState(String::from(
14121                    "crypto.pbkdf2 iterations must be greater than zero",
14122                )));
14123            }
14124            let key_len = usize::try_from(javascript_sync_rpc_arg_u64(
14125                &request.args,
14126                3,
14127                "crypto.pbkdf2 key length",
14128            )?)
14129            .map_err(|_| {
14130                SidecarError::InvalidState(String::from(
14131                    "crypto.pbkdf2 key length must fit within usize",
14132                ))
14133            })?;
14134            let algorithm =
14135                javascript_crypto_digest_algorithm(&request.args, 4, "crypto.pbkdf2 digest")?;
14136            let mut output = vec![0u8; key_len];
14137            algorithm.pbkdf2(&password, &salt, iterations, &mut output);
14138            Ok(Value::String(
14139                base64::engine::general_purpose::STANDARD.encode(output),
14140            ))
14141        }
14142        "crypto.scrypt" => {
14143            let password =
14144                javascript_sync_rpc_base64_arg(&request.args, 0, "crypto.scrypt password")?;
14145            let salt = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.scrypt salt")?;
14146            let key_len = usize::try_from(javascript_sync_rpc_arg_u64(
14147                &request.args,
14148                2,
14149                "crypto.scrypt key length",
14150            )?)
14151            .map_err(|_| {
14152                SidecarError::InvalidState(String::from(
14153                    "crypto.scrypt key length must fit within usize",
14154                ))
14155            })?;
14156            let options_json =
14157                javascript_sync_rpc_arg_str(&request.args, 3, "crypto.scrypt options")?;
14158            let options: JavascriptScryptOptions =
14159                serde_json::from_str(options_json).map_err(|error| {
14160                    SidecarError::InvalidState(format!(
14161                        "crypto.scrypt options must be valid JSON: {error}"
14162                    ))
14163                })?;
14164            let cost = options.cost.unwrap_or(DEFAULT_SCRYPT_COST);
14165            if cost == 0 || !cost.is_power_of_two() {
14166                return Err(SidecarError::InvalidState(String::from(
14167                    "crypto.scrypt cost must be a positive power of two",
14168                )));
14169            }
14170            let log_n = u8::try_from(cost.ilog2()).map_err(|_| {
14171                SidecarError::InvalidState(String::from(
14172                    "crypto.scrypt cost exceeds supported parameter range",
14173                ))
14174            })?;
14175            let params = ScryptParams::new(
14176                log_n,
14177                options.block_size.unwrap_or(DEFAULT_SCRYPT_BLOCK_SIZE),
14178                options
14179                    .parallelization
14180                    .unwrap_or(DEFAULT_SCRYPT_PARALLELIZATION),
14181                key_len,
14182            )
14183            .map_err(|error| {
14184                SidecarError::InvalidState(format!("crypto.scrypt options are invalid: {error}"))
14185            })?;
14186            let mut output = vec![0u8; key_len];
14187            scrypt(&password, &salt, &params, &mut output).map_err(|error| {
14188                SidecarError::Execution(format!("crypto.scrypt failed: {error}"))
14189            })?;
14190            Ok(Value::String(
14191                base64::engine::general_purpose::STANDARD.encode(output),
14192            ))
14193        }
14194        "crypto.cipheriv" => service_javascript_crypto_cipheriv_sync_rpc(request),
14195        "crypto.decipheriv" => service_javascript_crypto_decipheriv_sync_rpc(request),
14196        "crypto.cipherivCreate" => {
14197            service_javascript_crypto_cipheriv_create_sync_rpc(process, request)
14198        }
14199        "crypto.cipherivUpdate" => {
14200            service_javascript_crypto_cipheriv_update_sync_rpc(process, request)
14201        }
14202        "crypto.cipherivFinal" => {
14203            service_javascript_crypto_cipheriv_final_sync_rpc(process, request)
14204        }
14205        "crypto.sign" => service_javascript_crypto_sign_sync_rpc(request),
14206        "crypto.verify" => service_javascript_crypto_verify_sync_rpc(request),
14207        "crypto.asymmetricOp" => service_javascript_crypto_asymmetric_op_sync_rpc(request),
14208        "crypto.createKeyObject" => service_javascript_crypto_create_key_object_sync_rpc(request),
14209        "crypto.generateKeyPairSync" => {
14210            service_javascript_crypto_generate_key_pair_sync_rpc(request)
14211        }
14212        "crypto.generateKeySync" => service_javascript_crypto_generate_key_sync_rpc(request),
14213        "crypto.generatePrimeSync" => service_javascript_crypto_generate_prime_sync_rpc(request),
14214        "crypto.diffieHellman" => service_javascript_crypto_diffie_hellman_sync_rpc(request),
14215        "crypto.diffieHellmanGroup" => {
14216            service_javascript_crypto_diffie_hellman_group_sync_rpc(request)
14217        }
14218        "crypto.diffieHellmanSessionCreate" => {
14219            service_javascript_crypto_diffie_hellman_session_create_sync_rpc(process, request)
14220        }
14221        "crypto.diffieHellmanSessionCall" => {
14222            service_javascript_crypto_diffie_hellman_session_call_sync_rpc(process, request)
14223        }
14224        "crypto.diffieHellmanSessionDestroy" => {
14225            service_javascript_crypto_diffie_hellman_session_destroy_sync_rpc(process, request)
14226        }
14227        "crypto.subtle" => service_javascript_crypto_subtle_sync_rpc(request),
14228        _ => Err(SidecarError::InvalidState(format!(
14229            "unsupported JavaScript crypto sync RPC method {}",
14230            request.method
14231        ))),
14232    }
14233}
14234
14235fn javascript_crypto_digest_algorithm(
14236    args: &[Value],
14237    index: usize,
14238    label: &str,
14239) -> Result<JavascriptCryptoDigestAlgorithm, SidecarError> {
14240    JavascriptCryptoDigestAlgorithm::parse(javascript_sync_rpc_arg_str(args, index, label)?)
14241}
14242
14243impl JavascriptCryptoDigestAlgorithm {
14244    fn parse(value: &str) -> Result<Self, SidecarError> {
14245        match value.trim().to_ascii_lowercase().replace('-', "").as_str() {
14246            "md5" => Ok(Self::Md5),
14247            "sha1" => Ok(Self::Sha1),
14248            "sha256" => Ok(Self::Sha256),
14249            "sha512" => Ok(Self::Sha512),
14250            _ => Err(SidecarError::InvalidState(format!(
14251                "unsupported crypto digest algorithm {value}"
14252            ))),
14253        }
14254    }
14255
14256    fn digest(self, data: &[u8]) -> Vec<u8> {
14257        match self {
14258            Self::Md5 => Md5::digest(data).to_vec(),
14259            Self::Sha1 => Sha1::digest(data).to_vec(),
14260            Self::Sha256 => Sha256::digest(data).to_vec(),
14261            Self::Sha512 => Sha512::digest(data).to_vec(),
14262        }
14263    }
14264
14265    fn hmac(self, key: &[u8], data: &[u8]) -> Result<Vec<u8>, SidecarError> {
14266        match self {
14267            Self::Md5 => {
14268                let mut mac = Hmac::<Md5>::new_from_slice(key).map_err(|error| {
14269                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14270                })?;
14271                mac.update(data);
14272                Ok(mac.finalize().into_bytes().to_vec())
14273            }
14274            Self::Sha1 => {
14275                let mut mac = Hmac::<Sha1>::new_from_slice(key).map_err(|error| {
14276                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14277                })?;
14278                mac.update(data);
14279                Ok(mac.finalize().into_bytes().to_vec())
14280            }
14281            Self::Sha256 => {
14282                let mut mac = Hmac::<Sha256>::new_from_slice(key).map_err(|error| {
14283                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14284                })?;
14285                mac.update(data);
14286                Ok(mac.finalize().into_bytes().to_vec())
14287            }
14288            Self::Sha512 => {
14289                let mut mac = Hmac::<Sha512>::new_from_slice(key).map_err(|error| {
14290                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14291                })?;
14292                mac.update(data);
14293                Ok(mac.finalize().into_bytes().to_vec())
14294            }
14295        }
14296    }
14297
14298    fn pbkdf2(self, password: &[u8], salt: &[u8], iterations: u32, output: &mut [u8]) {
14299        match self {
14300            Self::Md5 => pbkdf2_hmac::<Md5>(password, salt, iterations, output),
14301            Self::Sha1 => pbkdf2_hmac::<Sha1>(password, salt, iterations, output),
14302            Self::Sha256 => pbkdf2_hmac::<Sha256>(password, salt, iterations, output),
14303            Self::Sha512 => pbkdf2_hmac::<Sha512>(password, salt, iterations, output),
14304        }
14305    }
14306}
14307
14308#[derive(Debug, Clone)]
14309enum JavascriptCryptoKeyMaterial {
14310    Private(PKey<Private>),
14311    Public(PKey<Public>),
14312    Secret(Vec<u8>),
14313}
14314
14315#[derive(Debug, Clone, Deserialize, Serialize)]
14316struct JavascriptSerializedSandboxKeyObject {
14317    #[serde(rename = "type")]
14318    kind: String,
14319    #[serde(skip_serializing_if = "Option::is_none")]
14320    pem: Option<String>,
14321    #[serde(skip_serializing_if = "Option::is_none")]
14322    raw: Option<String>,
14323    #[serde(skip_serializing_if = "Option::is_none", rename = "asymmetricKeyType")]
14324    asymmetric_key_type: Option<String>,
14325    #[serde(
14326        skip_serializing_if = "Option::is_none",
14327        rename = "asymmetricKeyDetails"
14328    )]
14329    asymmetric_key_details: Option<Map<String, Value>>,
14330    #[serde(skip_serializing_if = "Option::is_none")]
14331    jwk: Option<Value>,
14332}
14333
14334#[derive(Debug, Clone)]
14335struct JavascriptDirectKeyInput {
14336    key: JavascriptCryptoKeyMaterial,
14337    padding: Option<Padding>,
14338}
14339
14340fn service_javascript_crypto_cipheriv_sync_rpc(
14341    request: &JavascriptSyncRpcRequest,
14342) -> Result<Value, SidecarError> {
14343    service_javascript_crypto_cipheriv_inner(request, false)
14344}
14345
14346fn service_javascript_crypto_decipheriv_sync_rpc(
14347    request: &JavascriptSyncRpcRequest,
14348) -> Result<Value, SidecarError> {
14349    service_javascript_crypto_cipheriv_inner(request, true)
14350}
14351
14352fn service_javascript_crypto_cipheriv_create_sync_rpc(
14353    process: &mut ActiveProcess,
14354    request: &JavascriptSyncRpcRequest,
14355) -> Result<Value, SidecarError> {
14356    ensure_per_process_state_handle_capacity(process.cipher_sessions.len(), "cipher session")?;
14357    let mode = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.cipherivCreate mode")?;
14358    let decrypt = mode == "decipher";
14359    let algorithm =
14360        javascript_sync_rpc_arg_str(&request.args, 1, "crypto.cipherivCreate algorithm")?;
14361    let key = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.cipherivCreate key")?;
14362    let iv = javascript_sync_rpc_base64_arg_optional(&request.args, 3, "crypto.cipherivCreate iv")?;
14363    let options =
14364        javascript_sync_rpc_json_arg_optional(&request.args, 4, "crypto.cipherivCreate options")?;
14365    let auth_tag_len = javascript_crypto_requested_aead_tag_len(algorithm, options.as_ref())?;
14366    let context = javascript_crypto_build_cipher_context(
14367        algorithm,
14368        &key,
14369        iv.as_deref(),
14370        decrypt,
14371        options.as_ref(),
14372    )?;
14373    process.next_cipher_session_id += 1;
14374    let session_id = process.next_cipher_session_id;
14375    process.cipher_sessions.insert(
14376        session_id,
14377        ActiveCipherSession {
14378            algorithm: algorithm.to_string(),
14379            auth_tag_len,
14380            context,
14381        },
14382    );
14383    Ok(json!(session_id))
14384}
14385
14386fn service_javascript_crypto_cipheriv_update_sync_rpc(
14387    process: &mut ActiveProcess,
14388    request: &JavascriptSyncRpcRequest,
14389) -> Result<Value, SidecarError> {
14390    let session_id =
14391        javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.cipherivUpdate session id")?;
14392    let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.cipherivUpdate data")?;
14393    let session = process
14394        .cipher_sessions
14395        .get_mut(&session_id)
14396        .ok_or_else(|| {
14397            SidecarError::InvalidState(format!("Cipher session {session_id} not found"))
14398        })?;
14399    let result = javascript_crypto_cipher_update(&mut session.context, &data)?;
14400    Ok(Value::String(
14401        base64::engine::general_purpose::STANDARD.encode(result),
14402    ))
14403}
14404
14405fn service_javascript_crypto_cipheriv_final_sync_rpc(
14406    process: &mut ActiveProcess,
14407    request: &JavascriptSyncRpcRequest,
14408) -> Result<Value, SidecarError> {
14409    let session_id =
14410        javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.cipherivFinal session id")?;
14411    let mut session = process.cipher_sessions.remove(&session_id).ok_or_else(|| {
14412        SidecarError::InvalidState(format!("Cipher session {session_id} not found"))
14413    })?;
14414    let data = javascript_crypto_cipher_finalize(&mut session.context)?;
14415    let mut response = Map::new();
14416    response.insert(
14417        String::from("data"),
14418        Value::String(base64::engine::general_purpose::STANDARD.encode(data)),
14419    );
14420    if javascript_crypto_is_aead(&session.algorithm) {
14421        let mut auth_tag = vec![0_u8; session.auth_tag_len];
14422        session
14423            .context
14424            .get_tag(&mut auth_tag)
14425            .map_err(javascript_crypto_openssl_error)?;
14426        response.insert(
14427            String::from("authTag"),
14428            Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
14429        );
14430    }
14431    Ok(Value::String(serde_json::to_string(&response).map_err(
14432        |error| SidecarError::InvalidState(format!("serialize cipher final response: {error}")),
14433    )?))
14434}
14435
14436fn service_javascript_crypto_sign_sync_rpc(
14437    request: &JavascriptSyncRpcRequest,
14438) -> Result<Value, SidecarError> {
14439    let algorithm = request.args.first().and_then(Value::as_str);
14440    let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.sign data")?;
14441    let key_json = javascript_sync_rpc_arg_str(&request.args, 2, "crypto.sign key")?;
14442    let key_input =
14443        javascript_crypto_parse_direct_key_input(key_json, Some("private"), "crypto.sign key")?;
14444    let private_key = javascript_crypto_expect_private_key(key_input.key, "crypto.sign key")?;
14445    let mut signer = javascript_crypto_new_signer(algorithm, &private_key)?;
14446    if let Some(padding) = key_input.padding {
14447        signer
14448            .set_rsa_padding(padding)
14449            .map_err(javascript_crypto_openssl_error)?;
14450    }
14451    signer
14452        .update(&data)
14453        .map_err(javascript_crypto_openssl_error)?;
14454    Ok(Value::String(
14455        base64::engine::general_purpose::STANDARD.encode(
14456            signer
14457                .sign_to_vec()
14458                .map_err(javascript_crypto_openssl_error)?,
14459        ),
14460    ))
14461}
14462
14463fn service_javascript_crypto_verify_sync_rpc(
14464    request: &JavascriptSyncRpcRequest,
14465) -> Result<Value, SidecarError> {
14466    let algorithm = request.args.first().and_then(Value::as_str);
14467    let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.verify data")?;
14468    let key_json = javascript_sync_rpc_arg_str(&request.args, 2, "crypto.verify key")?;
14469    let signature = javascript_sync_rpc_base64_arg(&request.args, 3, "crypto.verify signature")?;
14470    let key_input =
14471        javascript_crypto_parse_direct_key_input(key_json, Some("public"), "crypto.verify key")?;
14472    let public_key = javascript_crypto_expect_public_key(key_input.key, "crypto.verify key")?;
14473    let mut verifier = javascript_crypto_new_verifier(algorithm, &public_key)?;
14474    if let Some(padding) = key_input.padding {
14475        verifier
14476            .set_rsa_padding(padding)
14477            .map_err(javascript_crypto_openssl_error)?;
14478    }
14479    verifier
14480        .update(&data)
14481        .map_err(javascript_crypto_openssl_error)?;
14482    Ok(json!(verifier
14483        .verify(&signature)
14484        .map_err(javascript_crypto_openssl_error)?))
14485}
14486
14487fn service_javascript_crypto_asymmetric_op_sync_rpc(
14488    request: &JavascriptSyncRpcRequest,
14489) -> Result<Value, SidecarError> {
14490    let operation = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.asymmetricOp operation")?;
14491    let key_json = javascript_sync_rpc_arg_str(&request.args, 1, "crypto.asymmetricOp key")?;
14492    let data = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.asymmetricOp data")?;
14493    let expect_kind = match operation {
14494        "publicEncrypt" | "publicDecrypt" => Some("public"),
14495        "privateEncrypt" | "privateDecrypt" => Some("private"),
14496        other => {
14497            return Err(SidecarError::InvalidState(format!(
14498                "Unsupported asymmetric crypto operation: {other}"
14499            )));
14500        }
14501    };
14502    let key_input =
14503        javascript_crypto_parse_direct_key_input(key_json, expect_kind, "crypto.asymmetricOp key")?;
14504    let padding = key_input.padding.unwrap_or(Padding::PKCS1);
14505    let mut output = vec![0_u8; javascript_crypto_rsa_output_size(&key_input.key)?];
14506    let written = match (operation, key_input.key) {
14507        ("publicEncrypt", JavascriptCryptoKeyMaterial::Public(key))
14508        | ("publicDecrypt", JavascriptCryptoKeyMaterial::Public(key)) => {
14509            let rsa = key.rsa().map_err(javascript_crypto_openssl_error)?;
14510            if operation == "publicEncrypt" {
14511                rsa.public_encrypt(&data, &mut output, padding)
14512                    .map_err(javascript_crypto_openssl_error)?
14513            } else {
14514                rsa.public_decrypt(&data, &mut output, padding)
14515                    .map_err(javascript_crypto_openssl_error)?
14516            }
14517        }
14518        ("privateEncrypt", JavascriptCryptoKeyMaterial::Private(key))
14519        | ("privateDecrypt", JavascriptCryptoKeyMaterial::Private(key)) => {
14520            let rsa = key.rsa().map_err(javascript_crypto_openssl_error)?;
14521            if operation == "privateEncrypt" {
14522                rsa.private_encrypt(&data, &mut output, padding)
14523                    .map_err(javascript_crypto_openssl_error)?
14524            } else {
14525                rsa.private_decrypt(&data, &mut output, padding)
14526                    .map_err(javascript_crypto_openssl_error)?
14527            }
14528        }
14529        _ => {
14530            return Err(SidecarError::InvalidState(format!(
14531                "{operation} requires an RSA {} key",
14532                expect_kind.unwrap_or("asymmetric")
14533            )));
14534        }
14535    };
14536    output.truncate(written);
14537    Ok(Value::String(
14538        base64::engine::general_purpose::STANDARD.encode(output),
14539    ))
14540}
14541
14542fn service_javascript_crypto_create_key_object_sync_rpc(
14543    request: &JavascriptSyncRpcRequest,
14544) -> Result<Value, SidecarError> {
14545    let operation =
14546        javascript_sync_rpc_arg_str(&request.args, 0, "crypto.createKeyObject operation")?;
14547    let key_json = javascript_sync_rpc_arg_str(&request.args, 1, "crypto.createKeyObject key")?;
14548    let expected = match operation {
14549        "createPrivateKey" => Some("private"),
14550        "createPublicKey" => Some("public"),
14551        other => {
14552            return Err(SidecarError::InvalidState(format!(
14553                "Unsupported key creation operation: {other}"
14554            )));
14555        }
14556    };
14557    let key_input =
14558        javascript_crypto_parse_direct_key_input(key_json, expected, "crypto.createKeyObject key")?;
14559    Ok(Value::String(
14560        serde_json::to_string(&javascript_crypto_serialize_sandbox_key_object(
14561            &key_input.key,
14562        )?)
14563        .map_err(|error| {
14564            SidecarError::InvalidState(format!("serialize crypto key object: {error}"))
14565        })?,
14566    ))
14567}
14568
14569fn service_javascript_crypto_generate_key_pair_sync_rpc(
14570    request: &JavascriptSyncRpcRequest,
14571) -> Result<Value, SidecarError> {
14572    let key_type =
14573        javascript_sync_rpc_arg_str(&request.args, 0, "crypto.generateKeyPairSync type")?;
14574    let options = javascript_crypto_parse_serialized_options_arg(
14575        &request.args,
14576        1,
14577        "crypto.generateKeyPairSync options",
14578    )?
14579    .unwrap_or(Value::Object(Map::new()));
14580    let public_encoding = options.get("publicKeyEncoding").cloned();
14581    let private_encoding = options.get("privateKeyEncoding").cloned();
14582
14583    let private_key = match key_type {
14584        "rsa" => {
14585            let bits = options
14586                .get("modulusLength")
14587                .and_then(Value::as_u64)
14588                .unwrap_or(2048) as u32;
14589            let exponent = options
14590                .get("publicExponent")
14591                .map(|value| javascript_crypto_u32_from_bridge_value(value, "rsa publicExponent"))
14592                .transpose()?
14593                .unwrap_or(65_537);
14594            let exponent = BigNum::from_u32(exponent).map_err(javascript_crypto_openssl_error)?;
14595            let rsa =
14596                Rsa::generate_with_e(bits, &exponent).map_err(javascript_crypto_openssl_error)?;
14597            PKey::from_rsa(rsa).map_err(javascript_crypto_openssl_error)?
14598        }
14599        "ec" => {
14600            let named_curve = options
14601                .get("namedCurve")
14602                .and_then(Value::as_str)
14603                .ok_or_else(|| {
14604                    SidecarError::InvalidState(String::from(
14605                        "crypto.generateKeyPairSync ec requires namedCurve",
14606                    ))
14607                })?;
14608            let group = EcGroup::from_curve_name(javascript_crypto_curve_nid(named_curve)?)
14609                .map_err(javascript_crypto_openssl_error)?;
14610            let key = EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?;
14611            PKey::from_ec_key(key).map_err(javascript_crypto_openssl_error)?
14612        }
14613        "ed25519" => PKey::generate_ed25519().map_err(javascript_crypto_openssl_error)?,
14614        "x25519" => PKey::generate_x25519().map_err(javascript_crypto_openssl_error)?,
14615        other => {
14616            return Err(SidecarError::InvalidState(format!(
14617                "unsupported crypto key pair type {other}"
14618            )));
14619        }
14620    };
14621    let public_key = PKey::public_key_from_pem(
14622        &private_key
14623            .public_key_to_pem()
14624            .map_err(javascript_crypto_openssl_error)?,
14625    )
14626    .map_err(javascript_crypto_openssl_error)?;
14627    let response = if public_encoding.is_some() || private_encoding.is_some() {
14628        json!({
14629            "publicKey": javascript_crypto_serialize_encoded_key_value_public(&public_key, public_encoding.as_ref())?,
14630            "privateKey": javascript_crypto_serialize_encoded_key_value_private(&private_key, private_encoding.as_ref())?,
14631        })
14632    } else {
14633        json!({
14634            "publicKey": javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Public(public_key))?,
14635            "privateKey": javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Private(private_key))?,
14636        })
14637    };
14638    Ok(Value::String(serde_json::to_string(&response).map_err(
14639        |error| SidecarError::InvalidState(format!("serialize generated key pair: {error}")),
14640    )?))
14641}
14642
14643fn service_javascript_crypto_generate_key_sync_rpc(
14644    request: &JavascriptSyncRpcRequest,
14645) -> Result<Value, SidecarError> {
14646    let key_type = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.generateKeySync type")?;
14647    let options = javascript_crypto_parse_serialized_options_arg(
14648        &request.args,
14649        1,
14650        "crypto.generateKeySync options",
14651    )?
14652    .unwrap_or(Value::Object(Map::new()));
14653    let bit_length = options
14654        .get("length")
14655        .and_then(Value::as_u64)
14656        .ok_or_else(|| {
14657            SidecarError::InvalidState(String::from(
14658                "crypto.generateKeySync options.length is required",
14659            ))
14660        })? as usize;
14661    let mut raw = vec![0_u8; bit_length.div_ceil(8)];
14662    rand_bytes(&mut raw).map_err(javascript_crypto_openssl_error)?;
14663    let serialized = match key_type {
14664        "hmac" => javascript_crypto_serialize_sandbox_key_object(
14665            &JavascriptCryptoKeyMaterial::Secret(raw),
14666        )?,
14667        "aes" => javascript_crypto_serialize_sandbox_key_object(
14668            &JavascriptCryptoKeyMaterial::Secret(raw),
14669        )?,
14670        other => {
14671            return Err(SidecarError::InvalidState(format!(
14672                "unsupported crypto.generateKeySync type {other}"
14673            )));
14674        }
14675    };
14676    Ok(Value::String(serde_json::to_string(&serialized).map_err(
14677        |error| SidecarError::InvalidState(format!("serialize generated key: {error}")),
14678    )?))
14679}
14680
14681fn service_javascript_crypto_generate_prime_sync_rpc(
14682    request: &JavascriptSyncRpcRequest,
14683) -> Result<Value, SidecarError> {
14684    let bits =
14685        javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.generatePrimeSync size")? as i32;
14686    let options = javascript_crypto_parse_serialized_options_arg(
14687        &request.args,
14688        1,
14689        "crypto.generatePrimeSync options",
14690    )?
14691    .unwrap_or(Value::Object(Map::new()));
14692    let safe = options
14693        .get("safe")
14694        .and_then(Value::as_bool)
14695        .unwrap_or(false);
14696    let add = options
14697        .get("add")
14698        .map(|value| javascript_crypto_bignum_from_bridge_value(value, "prime add"))
14699        .transpose()?;
14700    let rem = options
14701        .get("rem")
14702        .map(|value| javascript_crypto_bignum_from_bridge_value(value, "prime rem"))
14703        .transpose()?;
14704    let mut prime = BigNum::new().map_err(javascript_crypto_openssl_error)?;
14705    prime
14706        .generate_prime(bits, safe, add.as_deref(), rem.as_deref())
14707        .map_err(javascript_crypto_openssl_error)?;
14708    let payload = if options
14709        .get("bigint")
14710        .and_then(Value::as_bool)
14711        .unwrap_or(false)
14712    {
14713        json!({
14714            "__type": "bigint",
14715            "value": prime.to_dec_str().map_err(javascript_crypto_openssl_error)?.to_string(),
14716        })
14717    } else {
14718        json!({
14719            "__type": "buffer",
14720            "value": base64::engine::general_purpose::STANDARD.encode(prime.to_vec()),
14721        })
14722    };
14723    Ok(Value::String(serde_json::to_string(&payload).map_err(
14724        |error| SidecarError::InvalidState(format!("serialize generated prime: {error}")),
14725    )?))
14726}
14727
14728fn service_javascript_crypto_diffie_hellman_sync_rpc(
14729    request: &JavascriptSyncRpcRequest,
14730) -> Result<Value, SidecarError> {
14731    let options = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.diffieHellman options")?;
14732    let parsed: Value = serde_json::from_str(options).map_err(|error| {
14733        SidecarError::InvalidState(format!(
14734            "crypto.diffieHellman options must be valid JSON: {error}"
14735        ))
14736    })?;
14737    let private_key = javascript_crypto_parse_key_material_value(
14738        parsed.get("privateKey").ok_or_else(|| {
14739            SidecarError::InvalidState(String::from("crypto.diffieHellman missing privateKey"))
14740        })?,
14741        Some("private"),
14742        "crypto.diffieHellman privateKey",
14743    )?;
14744    let public_key = javascript_crypto_parse_key_material_value(
14745        parsed.get("publicKey").ok_or_else(|| {
14746            SidecarError::InvalidState(String::from("crypto.diffieHellman missing publicKey"))
14747        })?,
14748        Some("public"),
14749        "crypto.diffieHellman publicKey",
14750    )?;
14751    let private_key =
14752        javascript_crypto_expect_private_key(private_key, "crypto.diffieHellman privateKey")?;
14753    let public_key =
14754        javascript_crypto_expect_public_key(public_key, "crypto.diffieHellman publicKey")?;
14755    let mut deriver = Deriver::new(&private_key).map_err(javascript_crypto_openssl_error)?;
14756    deriver
14757        .set_peer(&public_key)
14758        .map_err(javascript_crypto_openssl_error)?;
14759    let secret = deriver
14760        .derive_to_vec()
14761        .map_err(javascript_crypto_openssl_error)?;
14762    Ok(Value::String(
14763        serde_json::to_string(&json!({
14764            "__type": "buffer",
14765            "value": base64::engine::general_purpose::STANDARD.encode(secret),
14766        }))
14767        .map_err(|error| {
14768            SidecarError::InvalidState(format!("serialize derived secret: {error}"))
14769        })?,
14770    ))
14771}
14772
14773fn service_javascript_crypto_diffie_hellman_group_sync_rpc(
14774    request: &JavascriptSyncRpcRequest,
14775) -> Result<Value, SidecarError> {
14776    let name = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.diffieHellmanGroup name")?;
14777    let params = javascript_crypto_named_dh_group(name)?;
14778    let response = json!({
14779        "prime": {
14780            "__type": "buffer",
14781            "value": base64::engine::general_purpose::STANDARD.encode(params.prime_p().to_vec()),
14782        },
14783        "generator": {
14784            "__type": "buffer",
14785            "value": base64::engine::general_purpose::STANDARD.encode(params.generator().to_vec()),
14786        },
14787    });
14788    Ok(Value::String(serde_json::to_string(&response).map_err(
14789        |error| {
14790            SidecarError::InvalidState(format!("serialize diffieHellmanGroup response: {error}"))
14791        },
14792    )?))
14793}
14794
14795fn service_javascript_crypto_diffie_hellman_session_create_sync_rpc(
14796    process: &mut ActiveProcess,
14797    request: &JavascriptSyncRpcRequest,
14798) -> Result<Value, SidecarError> {
14799    ensure_per_process_state_handle_capacity(
14800        process.diffie_hellman_sessions.len(),
14801        "diffie-hellman session",
14802    )?;
14803    let raw = javascript_sync_rpc_arg_str(
14804        &request.args,
14805        0,
14806        "crypto.diffieHellmanSessionCreate request",
14807    )?;
14808    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14809        SidecarError::InvalidState(format!(
14810            "crypto.diffieHellmanSessionCreate request must be valid JSON: {error}"
14811        ))
14812    })?;
14813    let session = match parsed.get("type").and_then(Value::as_str) {
14814        Some("group") => {
14815            let name = parsed.get("name").and_then(Value::as_str).ok_or_else(|| {
14816                SidecarError::InvalidState(String::from(
14817                    "crypto.diffieHellmanSessionCreate group requires name",
14818                ))
14819            })?;
14820            ActiveDiffieHellmanSession::Dh(ActiveDhSession {
14821                params: javascript_crypto_named_dh_group(name)?,
14822                key_pair: None,
14823            })
14824        }
14825        Some("dh") => {
14826            let args = parsed
14827                .get("args")
14828                .and_then(Value::as_array)
14829                .ok_or_else(|| {
14830                    SidecarError::InvalidState(String::from(
14831                        "crypto.diffieHellmanSessionCreate dh requires args",
14832                    ))
14833                })?;
14834            let params = javascript_crypto_build_dh_params(args)?;
14835            ActiveDiffieHellmanSession::Dh(ActiveDhSession {
14836                params,
14837                key_pair: None,
14838            })
14839        }
14840        Some("ecdh") => {
14841            let curve = parsed.get("name").and_then(Value::as_str).ok_or_else(|| {
14842                SidecarError::InvalidState(String::from(
14843                    "crypto.diffieHellmanSessionCreate ecdh requires name",
14844                ))
14845            })?;
14846            ActiveDiffieHellmanSession::Ecdh(ActiveEcdhSession {
14847                curve: curve.to_string(),
14848                key_pair: None,
14849            })
14850        }
14851        other => {
14852            return Err(SidecarError::InvalidState(format!(
14853                "Unsupported Diffie-Hellman session type: {}",
14854                other.unwrap_or("<missing>")
14855            )));
14856        }
14857    };
14858    process.next_diffie_hellman_session_id += 1;
14859    let session_id = process.next_diffie_hellman_session_id;
14860    process.diffie_hellman_sessions.insert(session_id, session);
14861    Ok(json!(session_id))
14862}
14863
14864fn service_javascript_crypto_diffie_hellman_session_call_sync_rpc(
14865    process: &mut ActiveProcess,
14866    request: &JavascriptSyncRpcRequest,
14867) -> Result<Value, SidecarError> {
14868    let session_id = javascript_sync_rpc_arg_u64(
14869        &request.args,
14870        0,
14871        "crypto.diffieHellmanSessionCall session id",
14872    )?;
14873    let raw =
14874        javascript_sync_rpc_arg_str(&request.args, 1, "crypto.diffieHellmanSessionCall request")?;
14875    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14876        SidecarError::InvalidState(format!(
14877            "crypto.diffieHellmanSessionCall request must be valid JSON: {error}"
14878        ))
14879    })?;
14880    let method = parsed
14881        .get("method")
14882        .and_then(Value::as_str)
14883        .ok_or_else(|| {
14884            SidecarError::InvalidState(String::from(
14885                "crypto.diffieHellmanSessionCall request missing method",
14886            ))
14887        })?;
14888    let args = parsed
14889        .get("args")
14890        .and_then(Value::as_array)
14891        .cloned()
14892        .unwrap_or_default();
14893    let session = process
14894        .diffie_hellman_sessions
14895        .get_mut(&session_id)
14896        .ok_or_else(|| {
14897            SidecarError::InvalidState(format!("Diffie-Hellman session {session_id} not found"))
14898        })?;
14899    let (result, has_result) = match session {
14900        ActiveDiffieHellmanSession::Dh(session) => {
14901            javascript_crypto_call_dh_session(session, method, &args)?
14902        }
14903        ActiveDiffieHellmanSession::Ecdh(session) => {
14904            javascript_crypto_call_ecdh_session(session, method, &args)?
14905        }
14906    };
14907    Ok(Value::String(
14908        serde_json::to_string(&json!({
14909            "result": result,
14910            "hasResult": has_result,
14911        }))
14912        .map_err(|error| {
14913            SidecarError::InvalidState(format!("serialize diffie session result: {error}"))
14914        })?,
14915    ))
14916}
14917
14918fn service_javascript_crypto_diffie_hellman_session_destroy_sync_rpc(
14919    process: &mut ActiveProcess,
14920    request: &JavascriptSyncRpcRequest,
14921) -> Result<Value, SidecarError> {
14922    let session_id = javascript_sync_rpc_arg_u64(
14923        &request.args,
14924        0,
14925        "crypto.diffieHellmanSessionDestroy session id",
14926    )?;
14927    process
14928        .diffie_hellman_sessions
14929        .remove(&session_id)
14930        .ok_or_else(|| {
14931            SidecarError::InvalidState(format!("Diffie-Hellman session {session_id} not found"))
14932        })?;
14933    Ok(Value::Null)
14934}
14935
14936fn service_javascript_crypto_subtle_sync_rpc(
14937    request: &JavascriptSyncRpcRequest,
14938) -> Result<Value, SidecarError> {
14939    let raw = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.subtle request")?;
14940    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14941        SidecarError::InvalidState(format!("crypto.subtle request must be valid JSON: {error}"))
14942    })?;
14943    let op = parsed.get("op").and_then(Value::as_str).ok_or_else(|| {
14944        SidecarError::InvalidState(String::from("crypto.subtle request missing op"))
14945    })?;
14946    match op {
14947        "digest" => {
14948            let algorithm = parsed
14949                .get("algorithm")
14950                .and_then(Value::as_str)
14951                .ok_or_else(|| {
14952                    SidecarError::InvalidState(String::from(
14953                        "crypto.subtle.digest missing algorithm",
14954                    ))
14955                })?;
14956            let data = parsed.get("data").and_then(Value::as_str).ok_or_else(|| {
14957                SidecarError::InvalidState(String::from("crypto.subtle.digest missing data"))
14958            })?;
14959            let bytes = base64::engine::general_purpose::STANDARD
14960                .decode(data)
14961                .map_err(|error| {
14962                    SidecarError::InvalidState(format!("crypto.subtle.digest data base64: {error}"))
14963                })?;
14964            let digest = JavascriptCryptoDigestAlgorithm::parse(algorithm)?.digest(&bytes);
14965            Ok(Value::String(
14966                serde_json::to_string(&json!({
14967                    "data": base64::engine::general_purpose::STANDARD.encode(digest),
14968                }))
14969                .map_err(|error| {
14970                    SidecarError::InvalidState(format!("serialize crypto.subtle digest: {error}"))
14971                })?,
14972            ))
14973        }
14974        "generateKey" => {
14975            let algorithm = parsed.get("algorithm").ok_or_else(|| {
14976                SidecarError::InvalidState(String::from(
14977                    "crypto.subtle.generateKey missing algorithm",
14978                ))
14979            })?;
14980            let name =
14981                javascript_crypto_subtle_algorithm_name(algorithm, "crypto.subtle.generateKey")?;
14982            if !matches!(name, "AES-GCM" | "AES-CBC" | "AES-CTR" | "AES-KW") {
14983                return Err(SidecarError::InvalidState(format!(
14984                    "Unsupported key algorithm: {name}"
14985                )));
14986            }
14987            let length_bits = algorithm
14988                .get("length")
14989                .and_then(Value::as_u64)
14990                .ok_or_else(|| {
14991                    SidecarError::InvalidState(String::from(
14992                        "crypto.subtle.generateKey AES algorithm requires length",
14993                    ))
14994                })?;
14995            if length_bits % 8 != 0 {
14996                return Err(SidecarError::InvalidState(String::from(
14997                    "crypto.subtle.generateKey length must be byte-aligned",
14998                )));
14999            }
15000            let length_bytes = usize::try_from(length_bits / 8).map_err(|_| {
15001                SidecarError::InvalidState(String::from(
15002                    "crypto.subtle.generateKey length is too large",
15003                ))
15004            })?;
15005            let mut raw = vec![0_u8; length_bytes];
15006            rand_bytes(&mut raw).map_err(javascript_crypto_openssl_error)?;
15007            let key = javascript_crypto_serialize_subtle_secret_key(
15008                &raw,
15009                javascript_crypto_normalize_subtle_secret_algorithm(algorithm.clone(), &raw)?,
15010                parsed
15011                    .get("extractable")
15012                    .and_then(Value::as_bool)
15013                    .unwrap_or(false),
15014                parsed.get("usages").cloned().unwrap_or_else(|| json!([])),
15015            )?;
15016            Ok(Value::String(
15017                serde_json::to_string(&json!({ "key": key })).map_err(|error| {
15018                    SidecarError::InvalidState(format!(
15019                        "serialize crypto.subtle generated key: {error}"
15020                    ))
15021                })?,
15022            ))
15023        }
15024        "importKey" => {
15025            let format = parsed
15026                .get("format")
15027                .and_then(Value::as_str)
15028                .ok_or_else(|| {
15029                    SidecarError::InvalidState(String::from(
15030                        "crypto.subtle.importKey missing format",
15031                    ))
15032                })?;
15033            if format != "raw" {
15034                return Err(SidecarError::InvalidState(format!(
15035                    "Unsupported import format: {format}"
15036                )));
15037            }
15038            let key_data = parsed
15039                .get("keyData")
15040                .and_then(Value::as_str)
15041                .ok_or_else(|| {
15042                    SidecarError::InvalidState(String::from(
15043                        "crypto.subtle.importKey missing keyData",
15044                    ))
15045                })?;
15046            let raw = base64::engine::general_purpose::STANDARD
15047                .decode(key_data)
15048                .map_err(|error| {
15049                    SidecarError::InvalidState(format!(
15050                        "crypto.subtle.importKey keyData base64: {error}"
15051                    ))
15052                })?;
15053            let algorithm = parsed.get("algorithm").ok_or_else(|| {
15054                SidecarError::InvalidState(String::from(
15055                    "crypto.subtle.importKey missing algorithm",
15056                ))
15057            })?;
15058            let key = javascript_crypto_serialize_subtle_secret_key(
15059                &raw,
15060                javascript_crypto_normalize_subtle_secret_algorithm(algorithm.clone(), &raw)?,
15061                parsed
15062                    .get("extractable")
15063                    .and_then(Value::as_bool)
15064                    .unwrap_or(false),
15065                parsed.get("usages").cloned().unwrap_or_else(|| json!([])),
15066            )?;
15067            Ok(Value::String(
15068                serde_json::to_string(&json!({ "key": key })).map_err(|error| {
15069                    SidecarError::InvalidState(format!(
15070                        "serialize crypto.subtle imported key: {error}"
15071                    ))
15072                })?,
15073            ))
15074        }
15075        "exportKey" => {
15076            let format = parsed
15077                .get("format")
15078                .and_then(Value::as_str)
15079                .ok_or_else(|| {
15080                    SidecarError::InvalidState(String::from(
15081                        "crypto.subtle.exportKey missing format",
15082                    ))
15083                })?;
15084            if format != "raw" {
15085                return Err(SidecarError::InvalidState(format!(
15086                    "Unsupported export format: {format}"
15087                )));
15088            }
15089            let raw = javascript_crypto_subtle_key_raw(
15090                parsed.get("key").ok_or_else(|| {
15091                    SidecarError::InvalidState(String::from("crypto.subtle.exportKey missing key"))
15092                })?,
15093                "crypto.subtle.exportKey key",
15094            )?;
15095            Ok(Value::String(
15096                serde_json::to_string(&json!({
15097                    "data": base64::engine::general_purpose::STANDARD.encode(raw),
15098                }))
15099                .map_err(|error| {
15100                    SidecarError::InvalidState(format!("serialize crypto.subtle export: {error}"))
15101                })?,
15102            ))
15103        }
15104        "encrypt" | "decrypt" => service_javascript_crypto_subtle_aes_crypt_sync_rpc(op, &parsed),
15105        _ => Err(SidecarError::InvalidState(format!(
15106            "Unsupported subtle operation: {op}"
15107        ))),
15108    }
15109}
15110
15111fn javascript_crypto_subtle_algorithm_name<'a>(
15112    algorithm: &'a Value,
15113    label: &str,
15114) -> Result<&'a str, SidecarError> {
15115    if let Some(name) = algorithm.as_str() {
15116        return Ok(name);
15117    }
15118    algorithm
15119        .get("name")
15120        .and_then(Value::as_str)
15121        .ok_or_else(|| SidecarError::InvalidState(format!("{label} algorithm missing name")))
15122}
15123
15124fn javascript_crypto_normalize_subtle_secret_algorithm(
15125    algorithm: Value,
15126    raw: &[u8],
15127) -> Result<Value, SidecarError> {
15128    let mut object = match algorithm {
15129        Value::String(name) => {
15130            let mut object = Map::new();
15131            object.insert(String::from("name"), Value::String(name));
15132            object
15133        }
15134        Value::Object(object) => object,
15135        _ => {
15136            return Err(SidecarError::InvalidState(String::from(
15137                "crypto.subtle secret algorithm must be a string or object",
15138            )));
15139        }
15140    };
15141    let name = object
15142        .get("name")
15143        .and_then(Value::as_str)
15144        .ok_or_else(|| {
15145            SidecarError::InvalidState(String::from("crypto.subtle secret algorithm missing name"))
15146        })?
15147        .to_string();
15148    if matches!(name.as_str(), "AES-GCM" | "AES-CBC" | "AES-CTR" | "AES-KW")
15149        && !object.contains_key("length")
15150    {
15151        object.insert(String::from("length"), json!(raw.len() * 8));
15152    }
15153    Ok(Value::Object(object))
15154}
15155
15156fn javascript_crypto_serialize_subtle_secret_key(
15157    raw: &[u8],
15158    algorithm: Value,
15159    extractable: bool,
15160    usages: Value,
15161) -> Result<Value, SidecarError> {
15162    let raw_base64 = base64::engine::general_purpose::STANDARD.encode(raw);
15163    let source_key_object_data = javascript_crypto_serialize_sandbox_key_object(
15164        &JavascriptCryptoKeyMaterial::Secret(raw.to_vec()),
15165    )?;
15166    Ok(json!({
15167        "type": "secret",
15168        "algorithm": algorithm,
15169        "extractable": extractable,
15170        "usages": usages,
15171        "_raw": raw_base64,
15172        "_sourceKeyObjectData": source_key_object_data,
15173    }))
15174}
15175
15176fn javascript_crypto_subtle_key_raw(key: &Value, label: &str) -> Result<Vec<u8>, SidecarError> {
15177    let raw = key.get("_raw").and_then(Value::as_str).ok_or_else(|| {
15178        SidecarError::InvalidState(format!("{label} must be a raw secret CryptoKey"))
15179    })?;
15180    base64::engine::general_purpose::STANDARD
15181        .decode(raw)
15182        .map_err(|error| SidecarError::InvalidState(format!("{label} raw base64: {error}")))
15183}
15184
15185fn service_javascript_crypto_subtle_aes_crypt_sync_rpc(
15186    op: &str,
15187    parsed: &Value,
15188) -> Result<Value, SidecarError> {
15189    let algorithm = parsed.get("algorithm").ok_or_else(|| {
15190        SidecarError::InvalidState(format!("crypto.subtle.{op} missing algorithm"))
15191    })?;
15192    let name = javascript_crypto_subtle_algorithm_name(algorithm, &format!("crypto.subtle.{op}"))?;
15193    if name != "AES-GCM" {
15194        return Err(SidecarError::InvalidState(format!(
15195            "Unsupported subtle AES operation algorithm: {name}"
15196        )));
15197    }
15198    let key = javascript_crypto_subtle_key_raw(
15199        parsed
15200            .get("key")
15201            .ok_or_else(|| SidecarError::InvalidState(format!("crypto.subtle.{op} missing key")))?,
15202        &format!("crypto.subtle.{op} key"),
15203    )?;
15204    let iv = algorithm.get("iv").and_then(Value::as_str).ok_or_else(|| {
15205        SidecarError::InvalidState(format!("crypto.subtle.{op} AES-GCM missing iv"))
15206    })?;
15207    let iv = base64::engine::general_purpose::STANDARD
15208        .decode(iv)
15209        .map_err(|error| {
15210            SidecarError::InvalidState(format!("crypto.subtle.{op} iv base64: {error}"))
15211        })?;
15212    let data = parsed
15213        .get("data")
15214        .and_then(Value::as_str)
15215        .ok_or_else(|| SidecarError::InvalidState(format!("crypto.subtle.{op} missing data")))?;
15216    let mut data = base64::engine::general_purpose::STANDARD
15217        .decode(data)
15218        .map_err(|error| {
15219            SidecarError::InvalidState(format!("crypto.subtle.{op} data base64: {error}"))
15220        })?;
15221    let tag_len = javascript_crypto_subtle_aes_gcm_tag_len(algorithm)?;
15222    let mut options = Map::new();
15223    options.insert(String::from("authTagLength"), json!(tag_len));
15224    if let Some(additional_data) = algorithm.get("additionalData").and_then(Value::as_str) {
15225        options.insert(
15226            String::from("aad"),
15227            Value::String(additional_data.to_string()),
15228        );
15229    }
15230    let decrypt = op == "decrypt";
15231    if decrypt {
15232        if data.len() < tag_len {
15233            return Err(SidecarError::InvalidState(String::from(
15234                "crypto.subtle.decrypt AES-GCM data shorter than auth tag",
15235            )));
15236        }
15237        let auth_tag = data.split_off(data.len() - tag_len);
15238        options.insert(
15239            String::from("authTag"),
15240            Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
15241        );
15242    }
15243    let cipher_name = format!("aes-{}-gcm", key.len() * 8);
15244    let mut context = javascript_crypto_build_cipher_context(
15245        &cipher_name,
15246        &key,
15247        Some(&iv),
15248        decrypt,
15249        Some(&Value::Object(options)),
15250    )?;
15251    let mut output = javascript_crypto_cipher_update(&mut context, &data)?;
15252    output.extend(javascript_crypto_cipher_finalize(&mut context)?);
15253    if !decrypt {
15254        let mut auth_tag = vec![0_u8; tag_len];
15255        context
15256            .get_tag(&mut auth_tag)
15257            .map_err(javascript_crypto_openssl_error)?;
15258        output.extend(auth_tag);
15259    }
15260    Ok(Value::String(
15261        serde_json::to_string(&json!({
15262            "data": base64::engine::general_purpose::STANDARD.encode(output),
15263        }))
15264        .map_err(|error| {
15265            SidecarError::InvalidState(format!("serialize crypto.subtle {op}: {error}"))
15266        })?,
15267    ))
15268}
15269
15270fn javascript_crypto_subtle_aes_gcm_tag_len(algorithm: &Value) -> Result<usize, SidecarError> {
15271    let tag_bits = algorithm
15272        .get("tagLength")
15273        .and_then(Value::as_u64)
15274        .unwrap_or(128);
15275    if !tag_bits.is_multiple_of(8) {
15276        return Err(SidecarError::InvalidState(String::from(
15277            "crypto.subtle AES-GCM tagLength must be byte-aligned",
15278        )));
15279    }
15280    usize::try_from(tag_bits / 8).map_err(|_| {
15281        SidecarError::InvalidState(String::from("crypto.subtle AES-GCM tagLength too large"))
15282    })
15283}
15284
15285fn service_javascript_crypto_cipheriv_inner(
15286    request: &JavascriptSyncRpcRequest,
15287    decrypt: bool,
15288) -> Result<Value, SidecarError> {
15289    let label = if decrypt {
15290        "crypto.decipheriv"
15291    } else {
15292        "crypto.cipheriv"
15293    };
15294    let algorithm = javascript_sync_rpc_arg_str(&request.args, 0, &format!("{label} algorithm"))?;
15295    let key = javascript_sync_rpc_base64_arg(&request.args, 1, &format!("{label} key"))?;
15296    let iv = javascript_sync_rpc_base64_arg_optional(&request.args, 2, &format!("{label} iv"))?;
15297    let data = javascript_sync_rpc_base64_arg(&request.args, 3, &format!("{label} data"))?;
15298    let options =
15299        javascript_sync_rpc_json_arg_optional(&request.args, 4, &format!("{label} options"))?;
15300    let auth_tag_len = javascript_crypto_requested_aead_tag_len(algorithm, options.as_ref())?;
15301    let mut context = javascript_crypto_build_cipher_context(
15302        algorithm,
15303        &key,
15304        iv.as_deref(),
15305        decrypt,
15306        options.as_ref(),
15307    )?;
15308    let payload = javascript_crypto_cipher_update(&mut context, &data)?;
15309    let final_bytes = javascript_crypto_cipher_finalize(&mut context)?;
15310    if decrypt {
15311        let mut output = payload;
15312        output.extend(final_bytes);
15313        return Ok(Value::String(
15314            base64::engine::general_purpose::STANDARD.encode(output),
15315        ));
15316    }
15317
15318    let mut response = Map::new();
15319    let mut encrypted = payload;
15320    encrypted.extend(final_bytes);
15321    response.insert(
15322        String::from("data"),
15323        Value::String(base64::engine::general_purpose::STANDARD.encode(encrypted)),
15324    );
15325    if javascript_crypto_is_aead(algorithm) {
15326        let mut auth_tag = vec![0_u8; auth_tag_len];
15327        context
15328            .get_tag(&mut auth_tag)
15329            .map_err(javascript_crypto_openssl_error)?;
15330        response.insert(
15331            String::from("authTag"),
15332            Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
15333        );
15334    }
15335    Ok(Value::String(serde_json::to_string(&response).map_err(
15336        |error| SidecarError::InvalidState(format!("serialize {label} response: {error}")),
15337    )?))
15338}
15339
15340fn javascript_sync_rpc_base64_arg_optional(
15341    args: &[Value],
15342    index: usize,
15343    label: &str,
15344) -> Result<Option<Vec<u8>>, SidecarError> {
15345    if args.get(index).is_none() || args[index].is_null() {
15346        return Ok(None);
15347    }
15348    javascript_sync_rpc_base64_arg(args, index, label).map(Some)
15349}
15350
15351fn javascript_sync_rpc_json_arg_optional(
15352    args: &[Value],
15353    index: usize,
15354    label: &str,
15355) -> Result<Option<Value>, SidecarError> {
15356    if args.get(index).is_none() || args[index].is_null() {
15357        return Ok(None);
15358    }
15359    let raw = javascript_sync_rpc_arg_str(args, index, label)?;
15360    serde_json::from_str(raw)
15361        .map(Some)
15362        .map_err(|error| SidecarError::InvalidState(format!("{label} must be valid JSON: {error}")))
15363}
15364
15365fn javascript_crypto_parse_direct_key_input(
15366    raw: &str,
15367    expected: Option<&str>,
15368    label: &str,
15369) -> Result<JavascriptDirectKeyInput, SidecarError> {
15370    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
15371        SidecarError::InvalidState(format!("{label} must be valid JSON: {error}"))
15372    })?;
15373    let padding = match parsed.as_object().and_then(|value| value.get("padding")) {
15374        Some(value) => javascript_crypto_padding_from_value(value)?,
15375        None => None,
15376    };
15377    Ok(JavascriptDirectKeyInput {
15378        key: javascript_crypto_parse_key_material_value(&parsed, expected, label)?,
15379        padding,
15380    })
15381}
15382
15383fn javascript_crypto_parse_key_material_value(
15384    value: &Value,
15385    expected: Option<&str>,
15386    label: &str,
15387) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15388    if let Some(object) = value.as_object() {
15389        if object.get("__type").and_then(Value::as_str) == Some("keyObject") {
15390            let serialized = object.get("value").ok_or_else(|| {
15391                SidecarError::InvalidState(format!("{label} keyObject is missing a value"))
15392            })?;
15393            return javascript_crypto_parse_serialized_key_object(serialized, expected, label);
15394        }
15395        if object.contains_key("type") && (object.contains_key("pem") || object.contains_key("raw"))
15396        {
15397            return javascript_crypto_parse_serialized_key_object(value, expected, label);
15398        }
15399        if let Some(source) = object.get("key") {
15400            return javascript_crypto_parse_key_source(
15401                source,
15402                object.get("format").and_then(Value::as_str),
15403                object.get("type").and_then(Value::as_str),
15404                expected,
15405                label,
15406            );
15407        }
15408    }
15409    javascript_crypto_parse_key_source(value, None, None, expected, label)
15410}
15411
15412fn javascript_crypto_parse_key_source(
15413    source: &Value,
15414    format: Option<&str>,
15415    kind: Option<&str>,
15416    expected: Option<&str>,
15417    label: &str,
15418) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15419    match source {
15420        Value::String(pem) => javascript_crypto_parse_key_from_pem(pem.as_bytes(), expected, label),
15421        Value::Object(object) if object.get("__type").and_then(Value::as_str) == Some("buffer") => {
15422            let data = javascript_crypto_decode_bridge_buffer(source, label)?;
15423            javascript_crypto_parse_key_from_bytes(&data, format, kind, expected, label)
15424        }
15425        Value::Object(_) => {
15426            if format == Some("jwk") {
15427                return Err(SidecarError::InvalidState(format!(
15428                    "{label} jwk inputs are not supported yet"
15429                )));
15430            }
15431            Err(SidecarError::InvalidState(format!(
15432                "{label} has an unsupported key shape"
15433            )))
15434        }
15435        _ => Err(SidecarError::InvalidState(format!(
15436            "{label} has an unsupported key value"
15437        ))),
15438    }
15439}
15440
15441fn javascript_crypto_parse_key_from_pem(
15442    pem: &[u8],
15443    expected: Option<&str>,
15444    label: &str,
15445) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15446    match expected {
15447        Some("private") => PKey::private_key_from_pem(pem)
15448            .map(JavascriptCryptoKeyMaterial::Private)
15449            .map_err(|error| {
15450                SidecarError::InvalidState(format!("{label} private key is invalid: {error}"))
15451            }),
15452        Some("public") => PKey::public_key_from_pem(pem)
15453            .map(JavascriptCryptoKeyMaterial::Public)
15454            .map_err(|error| {
15455                SidecarError::InvalidState(format!("{label} public key is invalid: {error}"))
15456            }),
15457        _ => PKey::private_key_from_pem(pem)
15458            .map(JavascriptCryptoKeyMaterial::Private)
15459            .or_else(|_| PKey::public_key_from_pem(pem).map(JavascriptCryptoKeyMaterial::Public))
15460            .map_err(|error| {
15461                SidecarError::InvalidState(format!("{label} PEM key is invalid: {error}"))
15462            }),
15463    }
15464}
15465
15466fn javascript_crypto_parse_key_from_bytes(
15467    der: &[u8],
15468    format: Option<&str>,
15469    kind: Option<&str>,
15470    expected: Option<&str>,
15471    label: &str,
15472) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15473    match (format.unwrap_or("der"), kind.or(expected)) {
15474        ("der", Some("pkcs8")) | ("der", Some("private")) => PKey::private_key_from_der(der)
15475            .map(JavascriptCryptoKeyMaterial::Private)
15476            .map_err(|error| {
15477                SidecarError::InvalidState(format!("{label} private key DER is invalid: {error}"))
15478            }),
15479        ("der", Some("spki")) | ("der", Some("public")) => PKey::public_key_from_der(der)
15480            .map(JavascriptCryptoKeyMaterial::Public)
15481            .map_err(|error| {
15482                SidecarError::InvalidState(format!("{label} public key DER is invalid: {error}"))
15483            }),
15484        _ => Err(SidecarError::InvalidState(format!(
15485            "{label} unsupported key bytes format"
15486        ))),
15487    }
15488}
15489
15490fn javascript_crypto_parse_serialized_key_object(
15491    value: &Value,
15492    expected: Option<&str>,
15493    label: &str,
15494) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15495    let serialized: JavascriptSerializedSandboxKeyObject = serde_json::from_value(value.clone())
15496        .map_err(|error| {
15497            SidecarError::InvalidState(format!("{label} keyObject is invalid: {error}"))
15498        })?;
15499    match serialized.kind.as_str() {
15500        "secret" => {
15501            if expected == Some("public") || expected == Some("private") {
15502                return Err(SidecarError::InvalidState(format!(
15503                    "{label} expected an asymmetric key"
15504                )));
15505            }
15506            Ok(JavascriptCryptoKeyMaterial::Secret(
15507                base64::engine::general_purpose::STANDARD
15508                    .decode(serialized.raw.unwrap_or_default())
15509                    .map_err(|error| {
15510                        SidecarError::InvalidState(format!(
15511                            "{label} secret key contains invalid base64: {error}"
15512                        ))
15513                    })?,
15514            ))
15515        }
15516        "private" => {
15517            let pem = serialized.pem.ok_or_else(|| {
15518                SidecarError::InvalidState(format!("{label} private keyObject is missing pem"))
15519            })?;
15520            javascript_crypto_parse_key_from_pem(pem.as_bytes(), Some("private"), label)
15521        }
15522        "public" => {
15523            let pem = serialized.pem.ok_or_else(|| {
15524                SidecarError::InvalidState(format!("{label} public keyObject is missing pem"))
15525            })?;
15526            javascript_crypto_parse_key_from_pem(pem.as_bytes(), Some("public"), label)
15527        }
15528        other => Err(SidecarError::InvalidState(format!(
15529            "{label} has unsupported keyObject type {other}"
15530        ))),
15531    }
15532}
15533
15534fn javascript_crypto_expect_private_key(
15535    key: JavascriptCryptoKeyMaterial,
15536    label: &str,
15537) -> Result<PKey<Private>, SidecarError> {
15538    match key {
15539        JavascriptCryptoKeyMaterial::Private(key) => Ok(key),
15540        _ => Err(SidecarError::InvalidState(format!(
15541            "{label} requires a private key"
15542        ))),
15543    }
15544}
15545
15546fn javascript_crypto_expect_public_key(
15547    key: JavascriptCryptoKeyMaterial,
15548    label: &str,
15549) -> Result<PKey<Public>, SidecarError> {
15550    match key {
15551        JavascriptCryptoKeyMaterial::Public(key) => Ok(key),
15552        JavascriptCryptoKeyMaterial::Private(key) => {
15553            let pem = key
15554                .public_key_to_pem()
15555                .map_err(javascript_crypto_openssl_error)?;
15556            PKey::public_key_from_pem(&pem).map_err(javascript_crypto_openssl_error)
15557        }
15558        _ => Err(SidecarError::InvalidState(format!(
15559            "{label} requires a public key"
15560        ))),
15561    }
15562}
15563
15564fn javascript_crypto_new_signer<'a>(
15565    algorithm: Option<&'a str>,
15566    key: &'a PKey<Private>,
15567) -> Result<Signer<'a>, SidecarError> {
15568    if matches!(key.id(), PKeyId::ED25519 | PKeyId::ED448) || algorithm.is_none() {
15569        return Signer::new_without_digest(key).map_err(javascript_crypto_openssl_error);
15570    }
15571    Signer::new(
15572        javascript_crypto_message_digest_from_name(algorithm.ok_or_else(|| {
15573            SidecarError::InvalidState(String::from("crypto.sign requires a digest algorithm"))
15574        })?)?,
15575        key,
15576    )
15577    .map_err(javascript_crypto_openssl_error)
15578}
15579
15580fn javascript_crypto_new_verifier<'a>(
15581    algorithm: Option<&'a str>,
15582    key: &'a PKey<Public>,
15583) -> Result<Verifier<'a>, SidecarError> {
15584    if matches!(key.id(), PKeyId::ED25519 | PKeyId::ED448) || algorithm.is_none() {
15585        return Verifier::new_without_digest(key).map_err(javascript_crypto_openssl_error);
15586    }
15587    Verifier::new(
15588        javascript_crypto_message_digest_from_name(algorithm.ok_or_else(|| {
15589            SidecarError::InvalidState(String::from("crypto.verify requires a digest algorithm"))
15590        })?)?,
15591        key,
15592    )
15593    .map_err(javascript_crypto_openssl_error)
15594}
15595
15596fn javascript_crypto_message_digest_from_name(name: &str) -> Result<MessageDigest, SidecarError> {
15597    match name.trim().to_ascii_lowercase().replace('-', "").as_str() {
15598        "md5" => Ok(MessageDigest::md5()),
15599        "sha1" => Ok(MessageDigest::sha1()),
15600        "sha256" => Ok(MessageDigest::sha256()),
15601        "sha384" => Ok(MessageDigest::sha384()),
15602        "sha512" => Ok(MessageDigest::sha512()),
15603        other => Err(SidecarError::InvalidState(format!(
15604            "unsupported crypto digest algorithm {other}"
15605        ))),
15606    }
15607}
15608
15609fn javascript_crypto_padding_from_value(value: &Value) -> Result<Option<Padding>, SidecarError> {
15610    let Some(number) = value.as_i64() else {
15611        return Ok(None);
15612    };
15613    let padding = match number {
15614        1 => Padding::PKCS1,
15615        3 => Padding::NONE,
15616        4 => Padding::PKCS1_OAEP,
15617        6 => Padding::PKCS1_PSS,
15618        other => {
15619            return Err(SidecarError::InvalidState(format!(
15620                "unsupported RSA padding constant {other}"
15621            )));
15622        }
15623    };
15624    Ok(Some(padding))
15625}
15626
15627fn javascript_crypto_decode_bridge_buffer(
15628    value: &Value,
15629    label: &str,
15630) -> Result<Vec<u8>, SidecarError> {
15631    let base64_value = value
15632        .as_object()
15633        .filter(|object| object.get("__type").and_then(Value::as_str) == Some("buffer"))
15634        .and_then(|object| object.get("value"))
15635        .and_then(Value::as_str)
15636        .ok_or_else(|| {
15637            SidecarError::InvalidState(format!("{label} must be a serialized bridge buffer"))
15638        })?;
15639    base64::engine::general_purpose::STANDARD
15640        .decode(base64_value)
15641        .map_err(|error| {
15642            SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
15643        })
15644}
15645
15646fn javascript_crypto_serialize_sandbox_key_object(
15647    key: &JavascriptCryptoKeyMaterial,
15648) -> Result<Value, SidecarError> {
15649    let serialized = match key {
15650        JavascriptCryptoKeyMaterial::Private(key) => JavascriptSerializedSandboxKeyObject {
15651            kind: String::from("private"),
15652            pem: Some(
15653                String::from_utf8(
15654                    key.private_key_to_pem_pkcs8()
15655                        .map_err(javascript_crypto_openssl_error)?,
15656                )
15657                .map_err(|error| {
15658                    SidecarError::InvalidState(format!("private key PEM is not utf8: {error}"))
15659                })?,
15660            ),
15661            raw: None,
15662            asymmetric_key_type: javascript_crypto_pkey_type_name(key.id()),
15663            asymmetric_key_details: None,
15664            jwk: None,
15665        },
15666        JavascriptCryptoKeyMaterial::Public(key) => JavascriptSerializedSandboxKeyObject {
15667            kind: String::from("public"),
15668            pem: Some(
15669                String::from_utf8(
15670                    key.public_key_to_pem()
15671                        .map_err(javascript_crypto_openssl_error)?,
15672                )
15673                .map_err(|error| {
15674                    SidecarError::InvalidState(format!("public key PEM is not utf8: {error}"))
15675                })?,
15676            ),
15677            raw: None,
15678            asymmetric_key_type: javascript_crypto_pkey_type_name(key.id()),
15679            asymmetric_key_details: None,
15680            jwk: None,
15681        },
15682        JavascriptCryptoKeyMaterial::Secret(raw) => JavascriptSerializedSandboxKeyObject {
15683            kind: String::from("secret"),
15684            pem: None,
15685            raw: Some(base64::engine::general_purpose::STANDARD.encode(raw)),
15686            asymmetric_key_type: None,
15687            asymmetric_key_details: None,
15688            jwk: None,
15689        },
15690    };
15691    serde_json::to_value(serialized)
15692        .map_err(|error| SidecarError::InvalidState(format!("serialize key object: {error}")))
15693}
15694
15695fn javascript_crypto_pkey_type_name(id: PKeyId) -> Option<String> {
15696    match id {
15697        PKeyId::RSA => Some(String::from("rsa")),
15698        PKeyId::EC => Some(String::from("ec")),
15699        PKeyId::ED25519 => Some(String::from("ed25519")),
15700        PKeyId::ED448 => Some(String::from("ed448")),
15701        PKeyId::X25519 => Some(String::from("x25519")),
15702        PKeyId::X448 => Some(String::from("x448")),
15703        PKeyId::DH => Some(String::from("dh")),
15704        _ => None,
15705    }
15706}
15707
15708fn javascript_crypto_rsa_output_size(
15709    key: &JavascriptCryptoKeyMaterial,
15710) -> Result<usize, SidecarError> {
15711    match key {
15712        JavascriptCryptoKeyMaterial::Private(key) => key
15713            .rsa()
15714            .map(|rsa| rsa.size() as usize)
15715            .map_err(javascript_crypto_openssl_error),
15716        JavascriptCryptoKeyMaterial::Public(key) => key
15717            .rsa()
15718            .map(|rsa| rsa.size() as usize)
15719            .map_err(javascript_crypto_openssl_error),
15720        JavascriptCryptoKeyMaterial::Secret(_) => Err(SidecarError::InvalidState(String::from(
15721            "RSA operations require an asymmetric key",
15722        ))),
15723    }
15724}
15725
15726fn javascript_crypto_parse_serialized_options_arg(
15727    args: &[Value],
15728    index: usize,
15729    label: &str,
15730) -> Result<Option<Value>, SidecarError> {
15731    let Some(raw) = args.get(index).and_then(Value::as_str) else {
15732        return Ok(None);
15733    };
15734    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
15735        SidecarError::InvalidState(format!("{label} must be valid JSON: {error}"))
15736    })?;
15737    if parsed.get("hasOptions").and_then(Value::as_bool) == Some(true) {
15738        Ok(parsed.get("options").cloned())
15739    } else {
15740        Ok(None)
15741    }
15742}
15743
15744fn javascript_crypto_u32_from_bridge_value(
15745    value: &Value,
15746    label: &str,
15747) -> Result<u32, SidecarError> {
15748    if let Some(number) = value.as_u64() {
15749        return u32::try_from(number)
15750            .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")));
15751    }
15752    let bytes = javascript_crypto_decode_bridge_buffer(value, label)?;
15753    if bytes.len() > 4 {
15754        return Err(SidecarError::InvalidState(format!(
15755            "{label} buffer is too large for u32"
15756        )));
15757    }
15758    Ok(bytes
15759        .into_iter()
15760        .fold(0_u32, |acc, byte| (acc << 8) | u32::from(byte)))
15761}
15762
15763fn javascript_crypto_bignum_from_bridge_value(
15764    value: &Value,
15765    label: &str,
15766) -> Result<BigNum, SidecarError> {
15767    if let Some(object) = value.as_object() {
15768        if object.get("__type").and_then(Value::as_str) == Some("bigint") {
15769            let decimal = object.get("value").and_then(Value::as_str).ok_or_else(|| {
15770                SidecarError::InvalidState(format!("{label} bigint is missing a value"))
15771            })?;
15772            return BigNum::from_dec_str(decimal).map_err(javascript_crypto_openssl_error);
15773        }
15774    }
15775    let bytes = javascript_crypto_decode_bridge_buffer(value, label)?;
15776    BigNum::from_slice(&bytes).map_err(javascript_crypto_openssl_error)
15777}
15778
15779fn javascript_crypto_curve_nid(name: &str) -> Result<Nid, SidecarError> {
15780    match name {
15781        "prime256v1" | "P-256" => Ok(Nid::X9_62_PRIME256V1),
15782        "secp384r1" | "P-384" => Ok(Nid::SECP384R1),
15783        "secp521r1" | "P-521" => Ok(Nid::SECP521R1),
15784        "secp256k1" => Ok(Nid::SECP256K1),
15785        other => Err(SidecarError::InvalidState(format!(
15786            "unsupported EC curve {other}"
15787        ))),
15788    }
15789}
15790
15791fn javascript_crypto_named_dh_group(name: &str) -> Result<Dh<Params>, SidecarError> {
15792    match name {
15793        "modp2" => Dh::get_1024_160().map_err(javascript_crypto_openssl_error),
15794        "modp14" | "modp15" | "modp16" | "modp17" | "modp18" => {
15795            Dh::get_2048_256().map_err(javascript_crypto_openssl_error)
15796        }
15797        other => Err(SidecarError::InvalidState(format!(
15798            "unsupported Diffie-Hellman group {other}"
15799        ))),
15800    }
15801}
15802
15803fn javascript_crypto_clone_dh_params(params: &Dh<Params>) -> Result<Dh<Params>, SidecarError> {
15804    Dh::from_pqg(
15805        params
15806            .prime_p()
15807            .to_owned()
15808            .map_err(javascript_crypto_openssl_error)?,
15809        params
15810            .prime_q()
15811            .map(|value| value.to_owned().map_err(javascript_crypto_openssl_error))
15812            .transpose()?,
15813        params
15814            .generator()
15815            .to_owned()
15816            .map_err(javascript_crypto_openssl_error)?,
15817    )
15818    .map_err(javascript_crypto_openssl_error)
15819}
15820
15821fn javascript_crypto_build_dh_params(args: &[Value]) -> Result<Dh<Params>, SidecarError> {
15822    let Some(first) = args.first() else {
15823        return Err(SidecarError::InvalidState(String::from(
15824            "Diffie-Hellman session args are required",
15825        )));
15826    };
15827    if let Some(bits) = first.as_u64() {
15828        let generator = args
15829            .get(1)
15830            .map(|value| javascript_crypto_u32_from_bridge_value(value, "Diffie-Hellman generator"))
15831            .transpose()?
15832            .unwrap_or(2);
15833        return Dh::generate_params(bits as u32, generator)
15834            .map_err(javascript_crypto_openssl_error);
15835    }
15836    let prime = javascript_crypto_bignum_from_bridge_value(first, "Diffie-Hellman prime")?;
15837    let generator = args
15838        .get(1)
15839        .map(|value| javascript_crypto_bignum_from_bridge_value(value, "Diffie-Hellman generator"))
15840        .transpose()?
15841        .unwrap_or(BigNum::from_u32(2).map_err(javascript_crypto_openssl_error)?);
15842    Dh::from_pqg(prime, None, generator).map_err(javascript_crypto_openssl_error)
15843}
15844
15845fn javascript_crypto_call_dh_session(
15846    session: &mut ActiveDhSession,
15847    method: &str,
15848    args: &[Value],
15849) -> Result<(Value, bool), SidecarError> {
15850    match method {
15851        "verifyError" => Ok((Value::Null, false)),
15852        "generateKeys" => {
15853            if session.key_pair.is_none() {
15854                session.key_pair = Some(
15855                    javascript_crypto_clone_dh_params(&session.params)?
15856                        .generate_key()
15857                        .map_err(javascript_crypto_openssl_error)?,
15858                );
15859            }
15860            let public = session
15861                .key_pair
15862                .as_ref()
15863                .expect("dh key pair")
15864                .public_key()
15865                .to_vec();
15866            Ok((javascript_crypto_bridge_buffer_value(&public), true))
15867        }
15868        "computeSecret" => {
15869            if session.key_pair.is_none() {
15870                session.key_pair = Some(
15871                    javascript_crypto_clone_dh_params(&session.params)?
15872                        .generate_key()
15873                        .map_err(javascript_crypto_openssl_error)?,
15874                );
15875            }
15876            let peer = javascript_crypto_bignum_from_bridge_value(
15877                args.first().ok_or_else(|| {
15878                    SidecarError::InvalidState(String::from(
15879                        "computeSecret requires peer public key",
15880                    ))
15881                })?,
15882                "Diffie-Hellman peer public key",
15883            )?;
15884            let secret = session
15885                .key_pair
15886                .as_ref()
15887                .expect("dh key pair")
15888                .compute_key(&peer)
15889                .map_err(javascript_crypto_openssl_error)?;
15890            Ok((javascript_crypto_bridge_buffer_value(&secret), true))
15891        }
15892        "getPrime" => Ok((
15893            javascript_crypto_bridge_buffer_value(&session.params.prime_p().to_vec()),
15894            true,
15895        )),
15896        "getGenerator" => Ok((
15897            javascript_crypto_bridge_buffer_value(&session.params.generator().to_vec()),
15898            true,
15899        )),
15900        "getPublicKey" => {
15901            if session.key_pair.is_none() {
15902                session.key_pair = Some(
15903                    javascript_crypto_clone_dh_params(&session.params)?
15904                        .generate_key()
15905                        .map_err(javascript_crypto_openssl_error)?,
15906                );
15907            }
15908            Ok((
15909                javascript_crypto_bridge_buffer_value(
15910                    &session
15911                        .key_pair
15912                        .as_ref()
15913                        .expect("dh key pair")
15914                        .public_key()
15915                        .to_vec(),
15916                ),
15917                true,
15918            ))
15919        }
15920        "getPrivateKey" => {
15921            if session.key_pair.is_none() {
15922                session.key_pair = Some(
15923                    javascript_crypto_clone_dh_params(&session.params)?
15924                        .generate_key()
15925                        .map_err(javascript_crypto_openssl_error)?,
15926                );
15927            }
15928            Ok((
15929                javascript_crypto_bridge_buffer_value(
15930                    &session
15931                        .key_pair
15932                        .as_ref()
15933                        .expect("dh key pair")
15934                        .private_key()
15935                        .to_vec(),
15936                ),
15937                true,
15938            ))
15939        }
15940        other => Err(SidecarError::InvalidState(format!(
15941            "Unsupported Diffie-Hellman method: {other}"
15942        ))),
15943    }
15944}
15945
15946fn javascript_crypto_call_ecdh_session(
15947    session: &mut ActiveEcdhSession,
15948    method: &str,
15949    args: &[Value],
15950) -> Result<(Value, bool), SidecarError> {
15951    let nid = javascript_crypto_curve_nid(&session.curve)?;
15952    let group = EcGroup::from_curve_name(nid).map_err(javascript_crypto_openssl_error)?;
15953    match method {
15954        "verifyError" => Ok((Value::Null, false)),
15955        "generateKeys" => {
15956            if session.key_pair.is_none() {
15957                session.key_pair =
15958                    Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15959            }
15960            let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
15961            let bytes = session
15962                .key_pair
15963                .as_ref()
15964                .expect("ecdh key pair")
15965                .public_key()
15966                .to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx)
15967                .map_err(javascript_crypto_openssl_error)?;
15968            Ok((javascript_crypto_bridge_buffer_value(&bytes), true))
15969        }
15970        "computeSecret" => {
15971            if session.key_pair.is_none() {
15972                session.key_pair =
15973                    Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15974            }
15975            let peer_bytes = javascript_crypto_decode_bridge_buffer(
15976                args.first().ok_or_else(|| {
15977                    SidecarError::InvalidState(String::from(
15978                        "computeSecret requires peer public key",
15979                    ))
15980                })?,
15981                "ECDH peer public key",
15982            )?;
15983            let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
15984            let peer_point = EcPoint::from_bytes(&group, &peer_bytes, &mut ctx)
15985                .map_err(javascript_crypto_openssl_error)?;
15986            let peer_key = EcKey::from_public_key(&group, &peer_point)
15987                .map_err(javascript_crypto_openssl_error)?;
15988            let private =
15989                PKey::from_ec_key(session.key_pair.as_ref().expect("ecdh key pair").to_owned())
15990                    .map_err(javascript_crypto_openssl_error)?;
15991            let peer = PKey::from_ec_key(peer_key).map_err(javascript_crypto_openssl_error)?;
15992            let mut deriver = Deriver::new(&private).map_err(javascript_crypto_openssl_error)?;
15993            deriver
15994                .set_peer(&peer)
15995                .map_err(javascript_crypto_openssl_error)?;
15996            let secret = deriver
15997                .derive_to_vec()
15998                .map_err(javascript_crypto_openssl_error)?;
15999            Ok((javascript_crypto_bridge_buffer_value(&secret), true))
16000        }
16001        "getPublicKey" => {
16002            if session.key_pair.is_none() {
16003                session.key_pair =
16004                    Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
16005            }
16006            let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
16007            let bytes = session
16008                .key_pair
16009                .as_ref()
16010                .expect("ecdh key pair")
16011                .public_key()
16012                .to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx)
16013                .map_err(javascript_crypto_openssl_error)?;
16014            Ok((javascript_crypto_bridge_buffer_value(&bytes), true))
16015        }
16016        "getPrivateKey" => {
16017            if session.key_pair.is_none() {
16018                session.key_pair =
16019                    Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
16020            }
16021            Ok((
16022                javascript_crypto_bridge_buffer_value(
16023                    &session
16024                        .key_pair
16025                        .as_ref()
16026                        .expect("ecdh key pair")
16027                        .private_key()
16028                        .to_vec(),
16029                ),
16030                true,
16031            ))
16032        }
16033        other => Err(SidecarError::InvalidState(format!(
16034            "Unsupported Diffie-Hellman method: {other}"
16035        ))),
16036    }
16037}
16038
16039fn javascript_crypto_serialize_encoded_key_value_public(
16040    key: &PKey<Public>,
16041    encoding: Option<&Value>,
16042) -> Result<Value, SidecarError> {
16043    if let Some(encoding) = encoding {
16044        let format = encoding
16045            .get("format")
16046            .and_then(Value::as_str)
16047            .unwrap_or("pem");
16048        return Ok(match format {
16049            "der" => json!({
16050                "kind": "buffer",
16051                "value": base64::engine::general_purpose::STANDARD
16052                    .encode(key.public_key_to_der().map_err(javascript_crypto_openssl_error)?),
16053            }),
16054            _ => json!({
16055                "kind": "string",
16056                "value": String::from_utf8(
16057                    key.public_key_to_pem().map_err(javascript_crypto_openssl_error)?,
16058                )
16059                .map_err(|error| SidecarError::InvalidState(format!("public key PEM utf8: {error}")))?,
16060            }),
16061        });
16062    }
16063    javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Public(
16064        key.to_owned(),
16065    ))
16066}
16067
16068fn javascript_crypto_serialize_encoded_key_value_private(
16069    key: &PKey<Private>,
16070    encoding: Option<&Value>,
16071) -> Result<Value, SidecarError> {
16072    if let Some(encoding) = encoding {
16073        let format = encoding
16074            .get("format")
16075            .and_then(Value::as_str)
16076            .unwrap_or("pem");
16077        return Ok(match format {
16078            "der" => json!({
16079                "kind": "buffer",
16080                "value": base64::engine::general_purpose::STANDARD
16081                    .encode(key.private_key_to_der().map_err(javascript_crypto_openssl_error)?),
16082            }),
16083            _ => json!({
16084                "kind": "string",
16085                "value": String::from_utf8(
16086                    key.private_key_to_pem_pkcs8().map_err(javascript_crypto_openssl_error)?,
16087                )
16088                .map_err(|error| SidecarError::InvalidState(format!("private key PEM utf8: {error}")))?,
16089            }),
16090        });
16091    }
16092    javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Private(
16093        key.to_owned(),
16094    ))
16095}
16096
16097fn javascript_crypto_bridge_buffer_value(bytes: &[u8]) -> Value {
16098    json!({
16099        "__type": "buffer",
16100        "value": base64::engine::general_purpose::STANDARD.encode(bytes),
16101    })
16102}
16103
16104fn javascript_crypto_build_cipher_context(
16105    algorithm: &str,
16106    key: &[u8],
16107    iv: Option<&[u8]>,
16108    decrypt: bool,
16109    options: Option<&Value>,
16110) -> Result<Crypter, SidecarError> {
16111    let cipher = javascript_crypto_cipher_from_name(algorithm)?;
16112    let mode = if decrypt {
16113        Mode::Decrypt
16114    } else {
16115        Mode::Encrypt
16116    };
16117    let mut context =
16118        Crypter::new(cipher, mode, key, iv).map_err(javascript_crypto_openssl_error)?;
16119    if let Some(auto_padding) = options
16120        .and_then(|value| value.get("autoPadding"))
16121        .and_then(Value::as_bool)
16122    {
16123        context.pad(auto_padding);
16124    }
16125    if javascript_crypto_is_aead(algorithm) {
16126        if let Some(aad) = options
16127            .and_then(|value| value.get("aad"))
16128            .and_then(Value::as_str)
16129        {
16130            context
16131                .aad_update(
16132                    &base64::engine::general_purpose::STANDARD
16133                        .decode(aad)
16134                        .map_err(|error| {
16135                            SidecarError::InvalidState(format!(
16136                                "cipher aad contains invalid base64: {error}"
16137                            ))
16138                        })?,
16139                )
16140                .map_err(javascript_crypto_openssl_error)?;
16141        }
16142        if decrypt {
16143            if let Some(auth_tag) = options
16144                .and_then(|value| value.get("authTag"))
16145                .and_then(Value::as_str)
16146            {
16147                let decoded = base64::engine::general_purpose::STANDARD
16148                    .decode(auth_tag)
16149                    .map_err(|error| {
16150                        SidecarError::InvalidState(format!(
16151                            "cipher authTag contains invalid base64: {error}"
16152                        ))
16153                    })?;
16154                context
16155                    .set_tag(&decoded)
16156                    .map_err(javascript_crypto_openssl_error)?;
16157            }
16158        }
16159    }
16160    Ok(context)
16161}
16162
16163fn javascript_crypto_requested_aead_tag_len(
16164    algorithm: &str,
16165    options: Option<&Value>,
16166) -> Result<usize, SidecarError> {
16167    if !javascript_crypto_is_aead(algorithm) {
16168        return Ok(0);
16169    }
16170    let requested = options
16171        .and_then(|value| value.get("authTagLength"))
16172        .and_then(Value::as_u64)
16173        .unwrap_or(javascript_crypto_aead_tag_len(algorithm) as u64);
16174    usize::try_from(requested).map_err(|_| {
16175        SidecarError::InvalidState(String::from("cipher authTagLength must fit within usize"))
16176    })
16177}
16178
16179fn javascript_crypto_cipher_update(
16180    context: &mut Crypter,
16181    data: &[u8],
16182) -> Result<Vec<u8>, SidecarError> {
16183    let mut output = vec![0_u8; data.len() + 32];
16184    let written = context
16185        .update(data, &mut output)
16186        .map_err(javascript_crypto_openssl_error)?;
16187    output.truncate(written);
16188    Ok(output)
16189}
16190
16191fn javascript_crypto_cipher_finalize(context: &mut Crypter) -> Result<Vec<u8>, SidecarError> {
16192    let mut output = vec![0_u8; 32];
16193    let written = context
16194        .finalize(&mut output)
16195        .map_err(javascript_crypto_openssl_error)?;
16196    output.truncate(written);
16197    Ok(output)
16198}
16199
16200fn javascript_crypto_cipher_from_name(name: &str) -> Result<Cipher, SidecarError> {
16201    match name.to_ascii_lowercase().as_str() {
16202        "aes-128-cbc" => Ok(Cipher::aes_128_cbc()),
16203        "aes-192-cbc" => Ok(Cipher::aes_192_cbc()),
16204        "aes-256-cbc" => Ok(Cipher::aes_256_cbc()),
16205        "aes-128-ctr" => Ok(Cipher::aes_128_ctr()),
16206        "aes-192-ctr" => Ok(Cipher::aes_192_ctr()),
16207        "aes-256-ctr" => Ok(Cipher::aes_256_ctr()),
16208        "aes-128-gcm" => Ok(Cipher::aes_128_gcm()),
16209        "aes-192-gcm" => Ok(Cipher::aes_192_gcm()),
16210        "aes-256-gcm" => Ok(Cipher::aes_256_gcm()),
16211        other => Err(SidecarError::InvalidState(format!(
16212            "unsupported crypto cipher algorithm {other}"
16213        ))),
16214    }
16215}
16216
16217fn javascript_crypto_is_aead(algorithm: &str) -> bool {
16218    algorithm.to_ascii_lowercase().ends_with("-gcm")
16219}
16220
16221fn javascript_crypto_aead_tag_len(_algorithm: &str) -> usize {
16222    16
16223}
16224
16225fn javascript_crypto_openssl_error(error: openssl::error::ErrorStack) -> SidecarError {
16226    SidecarError::Execution(format!("crypto operation failed: {error}"))
16227}
16228
16229fn service_javascript_kernel_stdin_sync_rpc(
16230    kernel: &mut SidecarKernel,
16231    process: &mut ActiveProcess,
16232    request: &JavascriptSyncRpcRequest,
16233) -> Result<Value, SidecarError> {
16234    let max_bytes =
16235        javascript_sync_rpc_arg_u64_optional(&request.args, 0, "__kernel_stdin_read max bytes")?
16236            .map(|value| value.clamp(1, DEFAULT_KERNEL_STDIN_READ_MAX_BYTES as u64) as usize)
16237            .unwrap_or(DEFAULT_KERNEL_STDIN_READ_MAX_BYTES);
16238    let timeout_ms =
16239        javascript_sync_rpc_arg_u64_optional(&request.args, 1, "__kernel_stdin_read timeout ms")?
16240            .unwrap_or(DEFAULT_KERNEL_STDIN_READ_TIMEOUT_MS);
16241
16242    match kernel
16243        .fd_read_with_timeout_result(
16244            EXECUTION_DRIVER_NAME,
16245            process.kernel_pid,
16246            0,
16247            max_bytes,
16248            Some(Duration::from_millis(timeout_ms)),
16249        )
16250        .map_err(kernel_error)
16251    {
16252        Ok(Some(chunk)) if !chunk.is_empty() => Ok(json!({
16253            "dataBase64": base64::engine::general_purpose::STANDARD.encode(chunk),
16254        })),
16255        Ok(Some(_)) => Ok(Value::Null),
16256        Ok(None) => Ok(json!({
16257            "done": true,
16258        })),
16259        Err(SidecarError::Kernel(error)) if error.starts_with("EAGAIN:") => Ok(Value::Null),
16260        Err(error) => Err(error),
16261    }
16262}
16263
16264fn service_javascript_pty_set_raw_mode_sync_rpc(
16265    kernel: &mut SidecarKernel,
16266    process: &mut ActiveProcess,
16267    request: &JavascriptSyncRpcRequest,
16268) -> Result<Value, SidecarError> {
16269    let enabled = javascript_sync_rpc_arg_bool(&request.args, 0, "__pty_set_raw_mode enabled")?;
16270    kernel
16271        .pty_set_discipline(
16272            EXECUTION_DRIVER_NAME,
16273            process.kernel_pid,
16274            0,
16275            LineDisciplineConfig {
16276                canonical: Some(!enabled),
16277                echo: Some(!enabled),
16278                isig: Some(!enabled),
16279            },
16280        )
16281        .map_err(kernel_error)?;
16282    Ok(Value::Null)
16283}
16284
16285fn service_javascript_kernel_stdio_write_sync_rpc(
16286    kernel: &mut SidecarKernel,
16287    process: &mut ActiveProcess,
16288    request: &JavascriptSyncRpcRequest,
16289) -> Result<Value, SidecarError> {
16290    let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_stdio_write fd")?;
16291    let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "__kernel_stdio_write chunk")?;
16292
16293    let written = match fd {
16294        1 => kernel
16295            .write_process_stdout(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
16296            .map_err(kernel_error)?,
16297        2 => kernel
16298            .write_process_stderr(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
16299            .map_err(kernel_error)?,
16300        other => {
16301            return Err(SidecarError::InvalidState(format!(
16302                "__kernel_stdio_write only supports fd 1/2, got {other}"
16303            )));
16304        }
16305    };
16306
16307    let event = if fd == 1 {
16308        ActiveExecutionEvent::Stdout(chunk)
16309    } else {
16310        ActiveExecutionEvent::Stderr(chunk)
16311    };
16312    process.queue_pending_execution_event(event)?;
16313
16314    Ok(json!(written))
16315}
16316
16317fn service_javascript_kernel_poll_sync_rpc(
16318    kernel: &mut SidecarKernel,
16319    process: &ActiveProcess,
16320    request: &JavascriptSyncRpcRequest,
16321) -> Result<Value, SidecarError> {
16322    let fd_requests: Vec<KernelPollFdRequest> = serde_json::from_value(
16323        request
16324            .args
16325            .first()
16326            .cloned()
16327            .unwrap_or_else(|| Value::Array(Vec::new())),
16328    )
16329    .map_err(|error| {
16330        SidecarError::InvalidState(format!(
16331            "__kernel_poll fd list must be a JSON array of {{ fd, events }} objects: {error}"
16332        ))
16333    })?;
16334    let timeout_ms =
16335        javascript_sync_rpc_arg_u64_optional(&request.args, 1, "__kernel_poll timeout ms")?
16336            .unwrap_or_default();
16337    let timeout_ms = i32::try_from(timeout_ms).map_err(|_| {
16338        SidecarError::InvalidState(String::from("__kernel_poll timeout ms must fit within i32"))
16339    })?;
16340
16341    let poll_fds = fd_requests
16342        .iter()
16343        .map(|entry| PollFd {
16344            fd: entry.fd,
16345            events: PollEvents::from_bits(entry.events),
16346            revents: PollEvents::empty(),
16347        })
16348        .collect::<Vec<_>>();
16349    let result = kernel
16350        .poll_fds(
16351            EXECUTION_DRIVER_NAME,
16352            process.kernel_pid,
16353            poll_fds,
16354            timeout_ms,
16355        )
16356        .map_err(kernel_error)?;
16357
16358    Ok(json!({
16359        "readyCount": result.ready_count,
16360        "fds": result
16361            .fds
16362            .into_iter()
16363            .map(|entry| KernelPollFdResponse {
16364                fd: entry.fd,
16365                events: entry.events.bits(),
16366                revents: entry.revents.bits(),
16367            })
16368            .collect::<Vec<_>>(),
16369    }))
16370}
16371
16372fn install_kernel_stdin_pipe(kernel: &mut SidecarKernel, pid: u32) -> Result<u32, SidecarError> {
16373    let (read_fd, write_fd) = kernel
16374        .open_pipe(EXECUTION_DRIVER_NAME, pid)
16375        .map_err(kernel_error)?;
16376    kernel
16377        .fd_dup2(EXECUTION_DRIVER_NAME, pid, read_fd, 0)
16378        .map_err(kernel_error)?;
16379    kernel
16380        .fd_close(EXECUTION_DRIVER_NAME, pid, read_fd)
16381        .map_err(kernel_error)?;
16382    Ok(write_fd)
16383}
16384
16385fn javascript_child_process_stdin_mode(request: &JavascriptChildProcessSpawnRequest) -> &str {
16386    request
16387        .options
16388        .stdio
16389        .first()
16390        .map(String::as_str)
16391        .unwrap_or("pipe")
16392}
16393
16394pub(crate) fn write_kernel_process_stdin(
16395    kernel: &mut SidecarKernel,
16396    process: &mut ActiveProcess,
16397    chunk: &[u8],
16398) -> Result<(), SidecarError> {
16399    if process.runtime == GuestRuntimeKind::JavaScript {
16400        return Ok(());
16401    }
16402    let Some(writer_fd) = process.kernel_stdin_writer_fd else {
16403        return Ok(());
16404    };
16405    kernel
16406        .fd_write(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd, chunk)
16407        .map(|_| ())
16408        .map_err(kernel_error)
16409}
16410
16411pub(crate) fn close_kernel_process_stdin(
16412    kernel: &mut SidecarKernel,
16413    process: &mut ActiveProcess,
16414) -> Result<(), SidecarError> {
16415    let Some(writer_fd) = process.kernel_stdin_writer_fd.take() else {
16416        return Ok(());
16417    };
16418    kernel
16419        .fd_close(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd)
16420        .map_err(kernel_error)
16421}
16422
16423fn parse_http_header_collection(
16424    headers: &BTreeMap<String, Value>,
16425    label: &str,
16426) -> Result<HttpHeaderCollection, SidecarError> {
16427    let mut normalized = BTreeMap::<String, Vec<String>>::new();
16428    let mut raw_pairs = Vec::new();
16429
16430    for (raw_name, value) in headers {
16431        let normalized_name = raw_name.to_ascii_lowercase();
16432        let values = match value {
16433            Value::String(text) => vec![text.clone()],
16434            Value::Array(values) => values
16435                .iter()
16436                .map(|entry| {
16437                    entry.as_str().map(str::to_owned).ok_or_else(|| {
16438                        SidecarError::InvalidState(format!(
16439                            "{label} header {raw_name} must contain only strings"
16440                        ))
16441                    })
16442                })
16443                .collect::<Result<Vec<_>, _>>()?,
16444            other => {
16445                return Err(SidecarError::InvalidState(format!(
16446                    "{label} header {raw_name} must be a string or string array, received {other}"
16447                )));
16448            }
16449        };
16450        raw_pairs.extend(
16451            values
16452                .iter()
16453                .cloned()
16454                .map(|entry| (raw_name.clone(), entry)),
16455        );
16456        normalized
16457            .entry(normalized_name)
16458            .or_default()
16459            .extend(values);
16460    }
16461
16462    Ok(HttpHeaderCollection {
16463        normalized,
16464        raw_pairs,
16465    })
16466}
16467
16468fn http_headers_json(headers: &HttpHeaderCollection) -> Value {
16469    let map = headers
16470        .normalized
16471        .iter()
16472        .map(|(name, values)| {
16473            let value = if values.len() == 1 {
16474                Value::String(values[0].clone())
16475            } else {
16476                Value::Array(values.iter().cloned().map(Value::String).collect())
16477            };
16478            (name.clone(), value)
16479        })
16480        .collect::<Map<String, Value>>();
16481    Value::Object(map)
16482}
16483
16484fn http_raw_headers_json(headers: &HttpHeaderCollection) -> Value {
16485    Value::Array(
16486        headers
16487            .raw_pairs
16488            .iter()
16489            .flat_map(|(name, value)| [Value::String(name.clone()), Value::String(value.clone())])
16490            .collect(),
16491    )
16492}
16493
16494fn is_loopback_request_host(host: &str) -> bool {
16495    let bare = host
16496        .strip_prefix('[')
16497        .and_then(|value| value.strip_suffix(']'))
16498        .unwrap_or(host);
16499    matches!(bare, "localhost" | "127.0.0.1" | "::1")
16500}
16501
16502fn serialize_http_loopback_request(
16503    url: &Url,
16504    options: &JavascriptHttpRequestOptions,
16505    headers: &HttpHeaderCollection,
16506) -> Result<String, SidecarError> {
16507    let body_base64 = options
16508        .body
16509        .as_ref()
16510        .map(|body| base64::engine::general_purpose::STANDARD.encode(body.as_bytes()));
16511    serde_json::to_string(&json!({
16512        "method": options.method.clone().unwrap_or_else(|| String::from("GET")),
16513        "url": http_request_target(url),
16514        "headers": http_headers_json(headers),
16515        "rawHeaders": http_raw_headers_json(headers),
16516        "bodyBase64": body_base64,
16517    }))
16518    .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
16519}
16520
16521fn http_request_target(url: &Url) -> String {
16522    let path = if url.path().is_empty() {
16523        "/"
16524    } else {
16525        url.path()
16526    };
16527    format!(
16528        "{path}{}",
16529        url.query()
16530            .map(|query| format!("?{query}"))
16531            .unwrap_or_default()
16532    )
16533}
16534
16535fn find_kernel_http_listener_process(vm: &VmState, port: u16) -> Option<String> {
16536    vm.active_processes
16537        .iter()
16538        .find_map(|(process_id, process)| {
16539            process.tcp_listeners.values().find_map(|listener| {
16540                let socket_id = listener.kernel_socket_id?;
16541                let record = vm.kernel.socket_get(socket_id)?;
16542                let local_addr = record
16543                    .local_address()
16544                    .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
16545                    .unwrap_or_else(|| listener.guest_local_addr());
16546                if local_addr.port() == port && is_vm_local_http_listener_addr(local_addr.ip()) {
16547                    Some(process_id.to_owned())
16548                } else {
16549                    None
16550                }
16551            })
16552        })
16553}
16554
16555fn is_vm_local_http_listener_addr(ip: IpAddr) -> bool {
16556    ip.is_loopback() || ip.is_unspecified()
16557}
16558
16559fn serialize_kernel_http_fetch_request(
16560    port: u16,
16561    path: &str,
16562    options: &JavascriptHttpRequestOptions,
16563    headers: &HttpHeaderCollection,
16564) -> Vec<u8> {
16565    let method = options.method.as_deref().unwrap_or("GET");
16566    let mut lines = vec![format!("{method} {path} HTTP/1.1")];
16567    let mut has_host = false;
16568    let mut has_connection = false;
16569    let mut has_content_length = false;
16570    for (name, values) in &headers.normalized {
16571        match name.as_str() {
16572            "host" => has_host = true,
16573            "connection" => has_connection = true,
16574            "content-length" => has_content_length = true,
16575            _ => {}
16576        }
16577        lines.push(format!("{name}: {}", values.join(", ")));
16578    }
16579    if !has_host {
16580        lines.push(format!("Host: 127.0.0.1:{port}"));
16581    }
16582    if !has_connection {
16583        lines.push(String::from("Connection: close"));
16584    }
16585    let body = options.body.as_deref().unwrap_or("").as_bytes();
16586    if !has_content_length && !body.is_empty() {
16587        lines.push(format!("Content-Length: {}", body.len()));
16588    }
16589    lines.push(String::new());
16590    lines.push(String::new());
16591
16592    let mut request = lines.join("\r\n").into_bytes();
16593    request.extend_from_slice(body);
16594    request
16595}
16596
16597fn parse_kernel_http_fetch_response(
16598    buffer: &[u8],
16599    peer_closed: bool,
16600    url: &str,
16601) -> Result<Option<String>, SidecarError> {
16602    let Some(header_end) = find_http_header_end(buffer) else {
16603        return Ok(None);
16604    };
16605    let header_bytes = &buffer[..header_end];
16606    let head = String::from_utf8_lossy(header_bytes);
16607    let mut lines = head.split("\r\n");
16608    let status_line = lines.next().unwrap_or_default();
16609    let mut status_parts = status_line.splitn(3, ' ');
16610    let version = status_parts.next().unwrap_or_default();
16611    if !version.starts_with("HTTP/") {
16612        return Err(SidecarError::Execution(format!(
16613            "invalid vm.fetch HTTP response status line: {status_line}"
16614        )));
16615    }
16616    let status = status_parts
16617        .next()
16618        .ok_or_else(|| {
16619            SidecarError::Execution(format!(
16620                "invalid vm.fetch HTTP response status line: {status_line}"
16621            ))
16622        })?
16623        .parse::<u16>()
16624        .map_err(|error| {
16625            SidecarError::Execution(format!(
16626                "invalid vm.fetch HTTP response status code in {status_line:?}: {error}"
16627            ))
16628        })?;
16629    let status_text = status_parts.next().unwrap_or_default();
16630    let mut headers = Vec::new();
16631    let mut raw_headers = Vec::new();
16632    let mut content_length = None;
16633    let mut transfer_encoding_values = Vec::new();
16634    for line in lines {
16635        if line.is_empty() {
16636            continue;
16637        }
16638        let Some((name, value)) = line.split_once(':') else {
16639            return Err(SidecarError::Execution(format!(
16640                "invalid vm.fetch HTTP response header line: {line}"
16641            )));
16642        };
16643        let value = value.trim().to_owned();
16644        let normalized = name.to_ascii_lowercase();
16645        if normalized == "content-length" {
16646            content_length = Some(value.parse::<usize>().map_err(|error| {
16647                SidecarError::Execution(format!(
16648                    "invalid vm.fetch Content-Length header {value:?}: {error}"
16649                ))
16650            })?);
16651        } else if normalized == "transfer-encoding" {
16652            transfer_encoding_values.push(value.clone());
16653        }
16654        headers.push(json!([normalized, value.clone()]));
16655        raw_headers.push(Value::String(name.to_owned()));
16656        raw_headers.push(Value::String(value));
16657    }
16658
16659    let body_start = header_end + 4;
16660    let transfer_encoding = transfer_encoding_tokens(&transfer_encoding_values);
16661    let is_chunked = transfer_encoding.iter().any(|token| token == "chunked");
16662    let body = if is_chunked {
16663        if content_length.is_some() {
16664            return Err(SidecarError::Execution(String::from(
16665                "vm.fetch HTTP response cannot include both Transfer-Encoding: chunked and Content-Length",
16666            )));
16667        }
16668        if transfer_encoding.len() != 1 {
16669            return Err(SidecarError::Execution(format!(
16670                "unsupported vm.fetch Transfer-Encoding: {}",
16671                transfer_encoding.join(", ")
16672            )));
16673        }
16674        let Some(decoded) = decode_kernel_http_chunked_body(&buffer[body_start..])? else {
16675            return Ok(None);
16676        };
16677        decoded
16678    } else if !transfer_encoding.is_empty() {
16679        return Err(SidecarError::Execution(format!(
16680            "unsupported vm.fetch Transfer-Encoding: {}",
16681            transfer_encoding.join(", ")
16682        )));
16683    } else if let Some(content_length) = content_length {
16684        let body_end = body_start.saturating_add(content_length);
16685        // Reject a response whose declared length exceeds the hard raw-buffer cap
16686        // before waiting for the whole body: such a body can never satisfy the
16687        // limit, and the guest cannot stream it through the bounded kernel socket
16688        // buffer before the request deadline, so the read loop would otherwise
16689        // stall on an incomplete body until it times out. (A smaller-but-still-
16690        // over-a-configured-limit response is delivered in full and rejected by
16691        // the payload-size check, preserving that error's message.)
16692        if body_end > VM_FETCH_BUFFER_LIMIT_BYTES {
16693            return Err(SidecarError::Execution(format!(
16694                "vm.fetch raw response buffer is {body_end} bytes, limit is {VM_FETCH_BUFFER_LIMIT_BYTES}"
16695            )));
16696        }
16697        if buffer.len() < body_end {
16698            return Ok(None);
16699        }
16700        buffer[body_start..body_end].to_vec()
16701    } else if peer_closed {
16702        buffer[body_start..].to_vec()
16703    } else {
16704        return Ok(None);
16705    };
16706
16707    serde_json::to_string(&json!({
16708        "status": status,
16709        "statusText": status_text,
16710        "headers": headers,
16711        "rawHeaders": raw_headers,
16712        "body": base64::engine::general_purpose::STANDARD.encode(&body),
16713        "bodyEncoding": "base64",
16714        "url": url,
16715    }))
16716    .map(Some)
16717    .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
16718}
16719
16720fn find_http_header_end(buffer: &[u8]) -> Option<usize> {
16721    buffer.windows(4).position(|window| window == b"\r\n\r\n")
16722}
16723
16724fn find_crlf(buffer: &[u8], start: usize) -> Option<usize> {
16725    buffer
16726        .get(start..)?
16727        .windows(2)
16728        .position(|window| window == b"\r\n")
16729        .map(|offset| start + offset)
16730}
16731
16732fn transfer_encoding_tokens(values: &[String]) -> Vec<String> {
16733    values
16734        .iter()
16735        .flat_map(|value| value.split(','))
16736        .map(|token| token.trim().to_ascii_lowercase())
16737        .filter(|token| !token.is_empty())
16738        .collect()
16739}
16740
16741fn decode_kernel_http_chunked_body(buffer: &[u8]) -> Result<Option<Vec<u8>>, SidecarError> {
16742    let mut offset = 0;
16743    let mut body = Vec::new();
16744    loop {
16745        let Some(line_end) = find_crlf(buffer, offset) else {
16746            return Ok(None);
16747        };
16748        let size_line = std::str::from_utf8(&buffer[offset..line_end]).map_err(|error| {
16749            SidecarError::Execution(format!(
16750                "invalid vm.fetch chunk size line encoding: {error}"
16751            ))
16752        })?;
16753        let size_part = size_line.split(';').next().unwrap_or_default();
16754        if size_part.is_empty() || !size_part.bytes().all(|byte| byte.is_ascii_hexdigit()) {
16755            return Err(SidecarError::Execution(format!(
16756                "invalid vm.fetch chunk size line: {size_line:?}"
16757            )));
16758        }
16759        let chunk_size = usize::from_str_radix(size_part, 16).map_err(|error| {
16760            SidecarError::Execution(format!(
16761                "invalid vm.fetch chunk size {size_part:?}: {error}"
16762            ))
16763        })?;
16764        let chunk_start = line_end + 2;
16765        let chunk_end = chunk_start
16766            .checked_add(chunk_size)
16767            .ok_or_else(|| SidecarError::Execution(String::from("vm.fetch chunk size overflow")))?;
16768        if chunk_size > 0 {
16769            let chunk_terminator_end = chunk_end.checked_add(2).ok_or_else(|| {
16770                SidecarError::Execution(String::from("vm.fetch chunk terminator overflow"))
16771            })?;
16772            if chunk_terminator_end > buffer.len() {
16773                return Ok(None);
16774            }
16775            if buffer.get(chunk_end..chunk_terminator_end) != Some(b"\r\n") {
16776                return Err(SidecarError::Execution(String::from(
16777                    "invalid vm.fetch chunk terminator",
16778                )));
16779            }
16780            body.extend_from_slice(&buffer[chunk_start..chunk_end]);
16781            offset = chunk_terminator_end;
16782            continue;
16783        }
16784
16785        if buffer.get(chunk_start..chunk_start + 2) == Some(b"\r\n") {
16786            return Ok(Some(body));
16787        }
16788        let Some(trailer_end) = find_http_header_end(&buffer[chunk_start..]) else {
16789            return Ok(None);
16790        };
16791        let trailer_bytes = &buffer[chunk_start..chunk_start + trailer_end];
16792        let trailers = String::from_utf8_lossy(trailer_bytes);
16793        for line in trailers.split("\r\n") {
16794            if line.is_empty() {
16795                continue;
16796            }
16797            if line.starts_with(' ') || line.starts_with('\t') || !line.contains(':') {
16798                return Err(SidecarError::Execution(format!(
16799                    "invalid vm.fetch chunk trailer line: {line}"
16800                )));
16801            }
16802        }
16803        return Ok(Some(body));
16804    }
16805}
16806
16807fn kernel_http_fetch_target_exit_code(error: &SidecarError) -> Option<i32> {
16808    let SidecarError::Execution(message) = error else {
16809        return None;
16810    };
16811    message
16812        .strip_prefix("vm.fetch target exited before responding (exit code ")?
16813        .strip_suffix(')')?
16814        .parse()
16815        .ok()
16816}
16817
16818#[allow(clippy::too_many_arguments)]
16819fn service_host_fetch_target_event<B>(
16820    bridge: &SharedBridge<B>,
16821    vm_id: &str,
16822    dns: &VmDnsConfig,
16823    socket_paths: &JavascriptSocketPathContext,
16824    kernel: &mut SidecarKernel,
16825    process: &mut ActiveProcess,
16826    resource_limits: &ResourceLimits,
16827    wait: Duration,
16828) -> Result<bool, SidecarError>
16829where
16830    B: NativeSidecarBridge + Send + 'static,
16831    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16832{
16833    let Some(event) = process
16834        .execution
16835        .poll_event_blocking(wait)
16836        .map_err(|error| SidecarError::Execution(error.to_string()))?
16837    else {
16838        return Ok(false);
16839    };
16840
16841    match event {
16842        ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
16843            let network_counts = process.network_resource_counts();
16844            let response = service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
16845                bridge,
16846                vm_id,
16847                dns,
16848                socket_paths,
16849                kernel,
16850                process,
16851                sync_request: &request,
16852                resource_limits,
16853                network_counts,
16854            });
16855            match response {
16856                Ok(result) => process
16857                    .execution
16858                    .respond_javascript_sync_rpc_success(request.id, result)
16859                    .or_else(ignore_stale_javascript_sync_rpc_response)?,
16860                Err(error) => process
16861                    .execution
16862                    .respond_javascript_sync_rpc_error(
16863                        request.id,
16864                        javascript_sync_rpc_error_code(&error),
16865                        error.to_string(),
16866                    )
16867                    .or_else(ignore_stale_javascript_sync_rpc_response)?,
16868            }
16869        }
16870        ActiveExecutionEvent::Exited(code) => {
16871            return Err(SidecarError::Execution(format!(
16872                "vm.fetch target exited before responding (exit code {code})"
16873            )));
16874        }
16875        other => {
16876            process.queue_pending_execution_event(other)?;
16877        }
16878    }
16879    Ok(true)
16880}
16881
16882fn drain_host_fetch_target_events<B>(
16883    bridge: &SharedBridge<B>,
16884    vm_id: &str,
16885    vm: &mut VmState,
16886    target_process_id: &str,
16887    socket_paths: &JavascriptSocketPathContext,
16888    resource_limits: &ResourceLimits,
16889) -> Result<(), SidecarError>
16890where
16891    B: NativeSidecarBridge + Send + 'static,
16892    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16893{
16894    for _ in 0..32 {
16895        let dns = vm.dns.clone();
16896        let Some(process) = vm.active_processes.get_mut(target_process_id) else {
16897            break;
16898        };
16899        let serviced = service_host_fetch_target_event(
16900            bridge,
16901            vm_id,
16902            &dns,
16903            socket_paths,
16904            &mut vm.kernel,
16905            process,
16906            resource_limits,
16907            Duration::from_millis(1),
16908        )?;
16909        if !serviced {
16910            break;
16911        }
16912    }
16913    Ok(())
16914}
16915
16916#[allow(clippy::too_many_arguments)]
16917fn dispatch_kernel_http_fetch<B>(
16918    bridge: &SharedBridge<B>,
16919    vm_id: &str,
16920    vm: &mut VmState,
16921    target_process_id: &str,
16922    port: u16,
16923    path: &str,
16924    options: &JavascriptHttpRequestOptions,
16925    headers: &HttpHeaderCollection,
16926    max_fetch_response_bytes: usize,
16927) -> Result<String, SidecarError>
16928where
16929    B: NativeSidecarBridge + Send + 'static,
16930    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16931{
16932    let socket_paths = build_javascript_socket_path_context(vm)?;
16933    let family = JavascriptSocketFamily::Ipv4;
16934    let local_port = allocate_guest_listen_port(
16935        0,
16936        family,
16937        &socket_paths.used_tcp_guest_ports,
16938        socket_paths.listen_policy,
16939    )?;
16940    let resource_limits = vm.kernel.resource_limits().clone();
16941    let network_counts = vm_network_resource_counts(vm);
16942    check_network_resource_limit(
16943        resource_limits.max_sockets,
16944        network_counts.sockets,
16945        2,
16946        "socket",
16947    )?;
16948    check_network_resource_limit(
16949        resource_limits.max_connections,
16950        network_counts.connections,
16951        2,
16952        "connection",
16953    )?;
16954
16955    let kernel_pid = vm
16956        .active_processes
16957        .get(target_process_id)
16958        .ok_or_else(|| {
16959            SidecarError::InvalidState(format!(
16960                "vm.fetch target process disappeared: {target_process_id}"
16961            ))
16962        })?
16963        .kernel_pid;
16964    let socket_id = vm
16965        .kernel
16966        .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, SocketSpec::tcp())
16967        .map_err(kernel_error)?;
16968
16969    let result = dispatch_kernel_http_fetch_with_socket(
16970        bridge,
16971        vm_id,
16972        vm,
16973        target_process_id,
16974        kernel_pid,
16975        socket_id,
16976        local_port,
16977        port,
16978        path,
16979        options,
16980        headers,
16981        &socket_paths,
16982        &resource_limits,
16983        max_fetch_response_bytes,
16984    );
16985    let close_result = vm
16986        .kernel
16987        .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id)
16988        .map_err(kernel_error);
16989    let cleanup_result = if result.is_err() {
16990        drain_host_fetch_target_events(
16991            bridge,
16992            vm_id,
16993            vm,
16994            target_process_id,
16995            &socket_paths,
16996            &resource_limits,
16997        )
16998    } else {
16999        Ok(())
17000    };
17001    match (result, close_result) {
17002        (Ok(response), Ok(())) => cleanup_result.map(|()| response),
17003        (Err(error), _) => Err(error),
17004        (Ok(_), Err(error)) => Err(error),
17005    }
17006}
17007
17008#[allow(clippy::too_many_arguments)]
17009fn dispatch_kernel_http_fetch_with_socket<B>(
17010    bridge: &SharedBridge<B>,
17011    vm_id: &str,
17012    vm: &mut VmState,
17013    target_process_id: &str,
17014    kernel_pid: u32,
17015    socket_id: SocketId,
17016    local_port: u16,
17017    port: u16,
17018    path: &str,
17019    options: &JavascriptHttpRequestOptions,
17020    headers: &HttpHeaderCollection,
17021    socket_paths: &JavascriptSocketPathContext,
17022    resource_limits: &ResourceLimits,
17023    max_fetch_response_bytes: usize,
17024) -> Result<String, SidecarError>
17025where
17026    B: NativeSidecarBridge + Send + 'static,
17027    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17028{
17029    vm.kernel
17030        .socket_bind_inet(
17031            EXECUTION_DRIVER_NAME,
17032            kernel_pid,
17033            socket_id,
17034            InetSocketAddress::new("127.0.0.1", local_port),
17035        )
17036        .map_err(kernel_error)?;
17037    vm.kernel
17038        .socket_connect_inet_loopback(
17039            EXECUTION_DRIVER_NAME,
17040            kernel_pid,
17041            socket_id,
17042            InetSocketAddress::new("127.0.0.1", port),
17043        )
17044        .map_err(kernel_error)?;
17045
17046    let request_bytes = serialize_kernel_http_fetch_request(port, path, options, headers);
17047    vm.kernel
17048        .socket_write(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, &request_bytes)
17049        .map_err(kernel_error)?;
17050
17051    let mut response_buffer = Vec::new();
17052    let mut peer_closed = false;
17053    let url = format!("http://127.0.0.1:{port}{path}");
17054    let deadline = Instant::now() + http_loopback_request_timeout();
17055    loop {
17056        if let Some(response) =
17057            parse_kernel_http_fetch_response(&response_buffer, peer_closed, &url)?
17058        {
17059            ensure_vm_fetch_response_within_limit(&response, "vm.fetch", max_fetch_response_bytes)?;
17060            return Ok(response);
17061        }
17062        if Instant::now() >= deadline {
17063            let preview = String::from_utf8_lossy(&response_buffer);
17064            return Err(SidecarError::Execution(format!(
17065                "vm.fetch timed out waiting for kernel TCP HTTP response ({} buffered bytes: {:?})",
17066                response_buffer.len(),
17067                preview.chars().take(200).collect::<String>()
17068            )));
17069        }
17070
17071        {
17072            let dns = vm.dns.clone();
17073            let process = vm
17074                .active_processes
17075                .get_mut(target_process_id)
17076                .ok_or_else(|| {
17077                    SidecarError::InvalidState(format!(
17078                        "vm.fetch target process disappeared: {target_process_id}"
17079                    ))
17080                })?;
17081            service_host_fetch_target_event(
17082                bridge,
17083                vm_id,
17084                &dns,
17085                socket_paths,
17086                &mut vm.kernel,
17087                process,
17088                resource_limits,
17089                Duration::from_millis(5),
17090            )?;
17091        }
17092
17093        let poll = vm
17094            .kernel
17095            .poll_targets(
17096                EXECUTION_DRIVER_NAME,
17097                kernel_pid,
17098                vec![PollTargetEntry::socket(
17099                    socket_id,
17100                    POLLIN | POLLHUP | POLLERR,
17101                )],
17102                5,
17103            )
17104            .map_err(kernel_error)?;
17105        let revents = poll
17106            .targets
17107            .first()
17108            .map(|entry| entry.revents)
17109            .unwrap_or_else(PollEvents::empty);
17110        if revents.intersects(POLLERR) {
17111            return Err(SidecarError::Execution(String::from(
17112                "vm.fetch kernel TCP socket reported POLLERR",
17113            )));
17114        }
17115        if revents.intersects(POLLIN) {
17116            match vm
17117                .kernel
17118                .socket_read(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, 64 * 1024)
17119            {
17120                Ok(Some(bytes)) if !bytes.is_empty() => {
17121                    response_buffer.extend(bytes);
17122                    ensure_vm_fetch_raw_response_buffer_within_limit(
17123                        response_buffer.len(),
17124                        "vm.fetch",
17125                    )?;
17126                }
17127                Ok(Some(_)) => {}
17128                Ok(None) => peer_closed = true,
17129                Err(error) if error.code() == "EAGAIN" => {}
17130                Err(error) => return Err(kernel_error(error)),
17131            }
17132        }
17133        if revents.intersects(POLLHUP) {
17134            peer_closed = true;
17135        }
17136    }
17137}
17138
17139fn outbound_http_response_json(url: &Url, response: ureq::Response) -> Result<Value, SidecarError> {
17140    let status = response.status();
17141    let status_text = response.status_text().to_owned();
17142    let mut header_pairs = Vec::new();
17143    let mut raw_headers = Vec::new();
17144    for raw_name in response.headers_names() {
17145        for value in response.all(&raw_name) {
17146            header_pairs.push(json!([raw_name.to_ascii_lowercase(), value]));
17147            raw_headers.push(Value::String(raw_name.clone()));
17148            raw_headers.push(Value::String(value.to_owned()));
17149        }
17150    }
17151    let mut reader = response.into_reader();
17152    let mut body = Vec::new();
17153    reader.read_to_end(&mut body).map_err(|error| {
17154        SidecarError::Execution(format!("failed to read HTTP response: {error}"))
17155    })?;
17156    serde_json::to_string(&json!({
17157        "status": status,
17158        "statusText": status_text,
17159        "headers": header_pairs,
17160        "rawHeaders": raw_headers,
17161        "body": base64::engine::general_purpose::STANDARD.encode(body),
17162        "bodyEncoding": "base64",
17163        "url": url.as_str(),
17164    }))
17165    .map(Value::String)
17166    .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
17167}
17168
17169/// Split a ureq resolver `netloc` (`host:port`, with optional `[..]` IPv6
17170/// brackets) into its host and port components. Returns `None` if the port is
17171/// missing or unparseable.
17172fn split_netloc(netloc: &str) -> Option<(&str, u16)> {
17173    let (host, port) = netloc.rsplit_once(':')?;
17174    let port: u16 = port.parse().ok()?;
17175    let host = host
17176        .strip_prefix('[')
17177        .and_then(|rest| rest.strip_suffix(']'))
17178        .unwrap_or(host);
17179    Some((host, port))
17180}
17181
17182fn issue_outbound_http_request(
17183    url: &Url,
17184    options: &JavascriptHttpRequestOptions,
17185    headers: &HttpHeaderCollection,
17186    pinned_addresses: &[IpAddr],
17187) -> Result<Value, SidecarError> {
17188    let method = options.method.as_deref().unwrap_or("GET");
17189    // Pin the underlying resolver to the egress-vetted addresses. ureq performs
17190    // its own DNS resolution for the TCP/TLS connect; without this override an
17191    // https:// request would re-resolve the hostname through the host resolver
17192    // (a rebinding DNS server could then return a private/metadata IP that the
17193    // earlier range check would have rejected). The pinned resolver returns only
17194    // the vetted addresses and refuses any host it was not vetted for, while the
17195    // request URL keeps the original hostname so TLS SNI and the Host header stay
17196    // correct.
17197    let pinned_host = url.host_str().map(str::to_owned);
17198    let pinned: Vec<IpAddr> = pinned_addresses.to_vec();
17199    let resolver = move |netloc: &str| -> std::io::Result<Vec<SocketAddr>> {
17200        let (host, port) = split_netloc(netloc).ok_or_else(|| {
17201            std::io::Error::new(
17202                std::io::ErrorKind::InvalidInput,
17203                format!("invalid network location: {netloc}"),
17204            )
17205        })?;
17206        let expected_host = pinned_host.as_deref();
17207        if expected_host != Some(host) {
17208            return Err(std::io::Error::new(
17209                std::io::ErrorKind::PermissionDenied,
17210                format!(
17211                    "EACCES: outbound HTTP resolver pinned to {expected_host:?}, refusing {host}"
17212                ),
17213            ));
17214        }
17215        if pinned.is_empty() {
17216            return Err(std::io::Error::new(
17217                std::io::ErrorKind::PermissionDenied,
17218                "EACCES: no egress-vetted address available for outbound HTTP request",
17219            ));
17220        }
17221        Ok(pinned.iter().map(|ip| SocketAddr::new(*ip, port)).collect())
17222    };
17223    let mut agent_builder = ureq::AgentBuilder::new()
17224        .resolver(resolver)
17225        .timeout_connect(Duration::from_secs(5))
17226        .timeout_read(Duration::from_secs(15))
17227        .timeout_write(Duration::from_secs(15));
17228    if url.scheme() == "https" {
17229        let tls_options = JavascriptTlsBridgeOptions {
17230            is_server: false,
17231            servername: url.host_str().map(str::to_owned),
17232            alpn_protocols: Some(vec![String::from("http/1.1")]),
17233            reject_unauthorized: options.reject_unauthorized,
17234            ..JavascriptTlsBridgeOptions::default()
17235        };
17236        agent_builder = agent_builder.tls_config(Arc::new(build_client_tls_config(&tls_options)?));
17237    }
17238    let agent = agent_builder.build();
17239    let mut request = agent.request_url(method, url);
17240    for (name, values) in &headers.normalized {
17241        if name == "host" {
17242            continue;
17243        }
17244        let header_value = values.join(", ");
17245        request = request.set(name, &header_value);
17246    }
17247    let response = match options.body.as_deref() {
17248        Some(body) => request.send_string(body),
17249        None => request.call(),
17250    };
17251
17252    match response {
17253        Ok(response) => outbound_http_response_json(url, response),
17254        Err(ureq::Error::Status(_, response)) => outbound_http_response_json(url, response),
17255        Err(ureq::Error::Transport(error)) => Err(SidecarError::Execution(format!(
17256            "ERR_HTTP_REQUEST_FAILED: {error}"
17257        ))),
17258    }
17259}
17260
17261fn wait_for_loopback_http_response<B>(
17262    request: LoopbackHttpResponseWaitRequest<'_, B>,
17263) -> Result<String, SidecarError>
17264where
17265    B: NativeSidecarBridge + Send + 'static,
17266    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17267{
17268    let LoopbackHttpResponseWaitRequest {
17269        bridge,
17270        vm_id,
17271        dns,
17272        socket_paths,
17273        kernel,
17274        process,
17275        resource_limits,
17276        request_key,
17277    } = request;
17278    let deadline = Instant::now() + http_loopback_request_timeout();
17279    loop {
17280        if let Some(response) = process
17281            .pending_http_requests
17282            .get(&request_key)
17283            .and_then(|response| response.clone())
17284        {
17285            process.pending_http_requests.remove(&request_key);
17286            return Ok(response);
17287        }
17288
17289        if Instant::now() >= deadline {
17290            process.pending_http_requests.remove(&request_key);
17291            return Err(SidecarError::Execution(String::from(
17292                "HTTP loopback request timed out waiting for net.http_respond",
17293            )));
17294        }
17295
17296        let Some(event) = process
17297            .execution
17298            .poll_event_blocking(Duration::from_millis(10))
17299            .map_err(|error| SidecarError::Execution(error.to_string()))?
17300        else {
17301            continue;
17302        };
17303
17304        match event {
17305            ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
17306                let network_counts = process.network_resource_counts();
17307                let response = service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
17308                    bridge,
17309                    vm_id,
17310                    dns,
17311                    socket_paths,
17312                    kernel,
17313                    process,
17314                    sync_request: &request,
17315                    resource_limits,
17316                    network_counts,
17317                });
17318                match response {
17319                    Ok(result) => process
17320                        .execution
17321                        .respond_javascript_sync_rpc_success(request.id, result)
17322                        .or_else(ignore_stale_javascript_sync_rpc_response)?,
17323                    Err(error) => process
17324                        .execution
17325                        .respond_javascript_sync_rpc_error(
17326                            request.id,
17327                            javascript_sync_rpc_error_code(&error),
17328                            error.to_string(),
17329                        )
17330                        .or_else(ignore_stale_javascript_sync_rpc_response)?,
17331                }
17332            }
17333            ActiveExecutionEvent::Exited(code) => {
17334                process.pending_http_requests.remove(&request_key);
17335                return Err(SidecarError::Execution(format!(
17336                    "HTTP loopback server exited before responding (exit code {code})"
17337                )));
17338            }
17339            ActiveExecutionEvent::Stdout(_)
17340            | ActiveExecutionEvent::Stderr(_)
17341            | ActiveExecutionEvent::PythonVfsRpcRequest(_)
17342            | ActiveExecutionEvent::SignalState { .. } => {}
17343        }
17344    }
17345}
17346
17347pub(crate) fn dispatch_loopback_http_request<B>(
17348    request: LoopbackHttpDispatchRequest<'_, B>,
17349) -> Result<String, SidecarError>
17350where
17351    B: NativeSidecarBridge + Send + 'static,
17352    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17353{
17354    let LoopbackHttpDispatchRequest {
17355        bridge,
17356        vm_id,
17357        dns,
17358        socket_paths,
17359        kernel,
17360        process,
17361        resource_limits,
17362        server_id,
17363        request_json,
17364    } = request;
17365    let request_id = {
17366        let server = process.http_servers.get_mut(&server_id).ok_or_else(|| {
17367            SidecarError::InvalidState(format!("HTTP target server disappeared: {server_id}"))
17368        })?;
17369        server.next_request_id += 1;
17370        server.next_request_id
17371    };
17372    process
17373        .pending_http_requests
17374        .insert((server_id, request_id), None);
17375    process.execution.send_javascript_stream_event(
17376        "http_request",
17377        json!({
17378            "serverId": server_id,
17379            "requestId": request_id,
17380            "request": request_json,
17381        }),
17382    )?;
17383    wait_for_loopback_http_response(LoopbackHttpResponseWaitRequest {
17384        bridge,
17385        vm_id,
17386        dns,
17387        socket_paths,
17388        kernel,
17389        process,
17390        resource_limits,
17391        request_key: (server_id, request_id),
17392    })
17393}
17394
17395fn ensure_vm_fetch_response_within_limit(
17396    response_json: &str,
17397    operation: &str,
17398    limit: usize,
17399) -> Result<(), SidecarError> {
17400    let size = response_json.len();
17401    if size > limit {
17402        return Err(SidecarError::Execution(format!(
17403            "{operation} payload is {size} bytes, limit is {limit}"
17404        )));
17405    }
17406    Ok(())
17407}
17408
17409fn ensure_vm_fetch_raw_response_buffer_within_limit(
17410    size: usize,
17411    operation: &str,
17412) -> Result<(), SidecarError> {
17413    if size > VM_FETCH_BUFFER_LIMIT_BYTES {
17414        return Err(SidecarError::Execution(format!(
17415            "{operation} raw response buffer is {size} bytes, limit is {VM_FETCH_BUFFER_LIMIT_BYTES}"
17416        )));
17417    }
17418    Ok(())
17419}
17420
17421pub(crate) fn ensure_vm_fetch_response_frame_within_limit(
17422    response: &ResponseFrame,
17423    max_frame_bytes: usize,
17424) -> Result<(), SidecarError> {
17425    let max_frame_bytes = max_frame_bytes.min(VM_FETCH_BUFFER_LIMIT_BYTES);
17426    let frame = crate::protocol::to_generated_protocol_frame(
17427        &crate::protocol::ProtocolFrame::Response(response.clone()),
17428    )
17429    .map_err(|error| SidecarError::FrameTooLarge(error.to_string()))?;
17430    let WireProtocolFrame::ResponseFrame(_) = &frame else {
17431        return Err(SidecarError::FrameTooLarge(String::from(
17432            "vm fetch response converted to non-response wire frame",
17433        )));
17434    };
17435    WireFrameCodec::new(max_frame_bytes)
17436        .encode(&frame)
17437        .map(|_| ())
17438        .map_err(|error| SidecarError::FrameTooLarge(error.to_string()))
17439}
17440
17441fn service_javascript_dns_sync_rpc<B>(
17442    bridge: &SharedBridge<B>,
17443    kernel: &SidecarKernel,
17444    vm_id: &str,
17445    dns: &VmDnsConfig,
17446    request: &JavascriptSyncRpcRequest,
17447) -> Result<Value, SidecarError>
17448where
17449    B: NativeSidecarBridge + Send + 'static,
17450    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17451{
17452    match request.method.as_str() {
17453        "dns.lookup" => {
17454            let payload = request
17455                .args
17456                .first()
17457                .cloned()
17458                .ok_or_else(|| {
17459                    SidecarError::InvalidState(String::from(
17460                        "dns.lookup requires a request payload",
17461                    ))
17462                })
17463                .and_then(|value| {
17464                    serde_json::from_value::<JavascriptDnsLookupRequest>(value).map_err(|error| {
17465                        SidecarError::InvalidState(format!("invalid dns.lookup payload: {error}"))
17466                    })
17467                })?;
17468            let addresses = filter_dns_ip_addrs(
17469                resolve_dns_ip_addrs(
17470                    bridge,
17471                    kernel,
17472                    vm_id,
17473                    dns,
17474                    &payload.hostname,
17475                    DnsLookupPolicy::CheckPermissions,
17476                )?,
17477                payload.family,
17478            )?;
17479            let addresses = filter_dns_safe_ip_addrs(addresses, &payload.hostname)?;
17480            Ok(Value::Array(
17481                addresses
17482                    .into_iter()
17483                    .map(|ip| {
17484                        json!({
17485                            "address": ip.to_string(),
17486                            "family": if ip.is_ipv6() { 6 } else { 4 },
17487                        })
17488                    })
17489                    .collect(),
17490            ))
17491        }
17492        "dns.resolve" | "dns.resolve4" | "dns.resolve6" => {
17493            let payload = request
17494                .args
17495                .first()
17496                .cloned()
17497                .ok_or_else(|| {
17498                    SidecarError::InvalidState(String::from(
17499                        "dns.resolve requires a request payload",
17500                    ))
17501                })
17502                .and_then(|value| {
17503                    serde_json::from_value::<JavascriptDnsResolveRequest>(value).map_err(|error| {
17504                        SidecarError::InvalidState(format!("invalid dns.resolve payload: {error}"))
17505                    })
17506                })?;
17507            let requested_type = match request.method.as_str() {
17508                "dns.resolve4" => String::from("A"),
17509                "dns.resolve6" => String::from("AAAA"),
17510                _ => payload
17511                    .rrtype
17512                    .as_deref()
17513                    .unwrap_or("A")
17514                    .to_ascii_uppercase(),
17515            };
17516            let record_type = parse_dns_record_type(&requested_type)?;
17517            let resolution = resolve_dns_records(
17518                bridge,
17519                kernel,
17520                vm_id,
17521                dns,
17522                &payload.hostname,
17523                record_type,
17524                DnsLookupPolicy::CheckPermissions,
17525            )?;
17526            dns_resolution_to_node_value(&resolution, &requested_type)
17527        }
17528        other => Err(SidecarError::InvalidState(format!(
17529            "unsupported JavaScript dns sync RPC method {other}"
17530        ))),
17531    }
17532}
17533
17534fn service_javascript_dgram_sync_rpc<B>(
17535    request: JavascriptDgramSyncRpcServiceRequest<'_, B>,
17536) -> Result<Value, SidecarError>
17537where
17538    B: NativeSidecarBridge + Send + 'static,
17539    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17540{
17541    let JavascriptDgramSyncRpcServiceRequest {
17542        bridge,
17543        kernel,
17544        vm_id,
17545        dns,
17546        socket_paths,
17547        process,
17548        sync_request: request,
17549        resource_limits,
17550        network_counts,
17551    } = request;
17552    match request.method.as_str() {
17553        "dgram.createSocket" => {
17554            check_network_resource_limit(
17555                resource_limits.max_sockets,
17556                network_counts.sockets,
17557                1,
17558                "socket",
17559            )?;
17560            let payload = request
17561                .args
17562                .first()
17563                .cloned()
17564                .ok_or_else(|| {
17565                    SidecarError::InvalidState(String::from(
17566                        "dgram.createSocket requires a request payload",
17567                    ))
17568                })
17569                .and_then(|value| {
17570                    serde_json::from_value::<JavascriptDgramCreateSocketRequest>(value).map_err(
17571                        |error| {
17572                            SidecarError::InvalidState(format!(
17573                                "invalid dgram.createSocket payload: {error}"
17574                            ))
17575                        },
17576                    )
17577                })?;
17578            let family = JavascriptUdpFamily::from_socket_type(&payload.socket_type)?;
17579            let socket_id = process.allocate_udp_socket_id();
17580            process.udp_sockets.insert(
17581                socket_id.clone(),
17582                ActiveUdpSocket::new(kernel, process.kernel_pid, family)?,
17583            );
17584            Ok(json!({
17585                "socketId": socket_id,
17586                "type": family.socket_type(),
17587            }))
17588        }
17589        "dgram.bind" => {
17590            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.bind socket id")?;
17591            let payload = request
17592                .args
17593                .get(1)
17594                .cloned()
17595                .ok_or_else(|| {
17596                    SidecarError::InvalidState(String::from(
17597                        "dgram.bind requires a request payload",
17598                    ))
17599                })
17600                .and_then(|value| {
17601                    serde_json::from_value::<JavascriptDgramBindRequest>(value).map_err(|error| {
17602                        SidecarError::InvalidState(format!("invalid dgram.bind payload: {error}"))
17603                    })
17604                })?;
17605            let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
17606                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17607            })?;
17608            let local_addr = socket.bind(
17609                kernel,
17610                process.kernel_pid,
17611                payload.address.as_deref(),
17612                payload.port,
17613                socket_paths,
17614            )?;
17615            Ok(json!({
17616                "localAddress": local_addr.ip().to_string(),
17617                "localPort": local_addr.port(),
17618                "family": socket_addr_family(&local_addr),
17619            }))
17620        }
17621        "dgram.send" => {
17622            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.send socket id")?;
17623            let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "dgram.send payload")?;
17624            let payload = request
17625                .args
17626                .get(2)
17627                .cloned()
17628                .ok_or_else(|| {
17629                    SidecarError::InvalidState(String::from(
17630                        "dgram.send requires a request payload",
17631                    ))
17632                })
17633                .and_then(|value| {
17634                    serde_json::from_value::<JavascriptDgramSendRequest>(value).map_err(|error| {
17635                        SidecarError::InvalidState(format!("invalid dgram.send payload: {error}"))
17636                    })
17637                })?;
17638            let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
17639                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17640            })?;
17641            let (written, local_addr) = socket.send_to(ActiveUdpSendToRequest {
17642                bridge,
17643                kernel,
17644                kernel_pid: process.kernel_pid,
17645                vm_id,
17646                dns,
17647                host: payload.address.as_deref().unwrap_or("localhost"),
17648                port: payload.port,
17649                context: socket_paths,
17650                contents: &chunk,
17651            })?;
17652            Ok(json!({
17653                "bytes": written,
17654                "localAddress": local_addr.ip().to_string(),
17655                "localPort": local_addr.port(),
17656                "family": socket_addr_family(&local_addr),
17657            }))
17658        }
17659        "dgram.poll" => {
17660            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.poll socket id")?;
17661            let wait_ms =
17662                javascript_sync_rpc_arg_u64_optional(&request.args, 1, "dgram.poll wait ms")?
17663                    .unwrap_or_default();
17664            let event = {
17665                let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
17666                    SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17667                })?;
17668                socket.poll(kernel, process.kernel_pid, Duration::from_millis(wait_ms))?
17669            };
17670
17671            match event {
17672                Some(JavascriptUdpSocketEvent::Message { data, remote_addr }) => {
17673                    let family = JavascriptSocketFamily::from_ip(remote_addr.ip());
17674                    let guest_remote_port = if is_loopback_ip(remote_addr.ip()) {
17675                        socket_paths
17676                            .guest_udp_port_for_host_port(family, remote_addr.port())
17677                            .unwrap_or(remote_addr.port())
17678                    } else {
17679                        remote_addr.port()
17680                    };
17681                    Ok(json!({
17682                    "type": "message",
17683                    "data": javascript_sync_rpc_bytes_value(&data),
17684                    "remoteAddress": remote_addr.ip().to_string(),
17685                    "remotePort": guest_remote_port,
17686                    "remoteFamily": socket_addr_family(&remote_addr),
17687                    }))
17688                }
17689                Some(JavascriptUdpSocketEvent::Error { code, message }) => Ok(json!({
17690                    "type": "error",
17691                    "code": code,
17692                    "message": message,
17693                })),
17694                None => Ok(Value::Null),
17695            }
17696        }
17697        "dgram.close" => {
17698            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.close socket id")?;
17699            let mut socket = process.udp_sockets.remove(socket_id).ok_or_else(|| {
17700                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17701            })?;
17702            socket.close(kernel, process.kernel_pid);
17703            Ok(Value::Null)
17704        }
17705        "dgram.address" => {
17706            let socket_id =
17707                javascript_sync_rpc_arg_str(&request.args, 0, "dgram.address socket id")?;
17708            let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
17709                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17710            })?;
17711            let local_addr = socket.local_addr().ok_or_else(|| {
17712                SidecarError::Execution(String::from("EBADF: bad file descriptor"))
17713            })?;
17714            javascript_net_json_string(
17715                json!({
17716                    "address": local_addr.ip().to_string(),
17717                    "port": local_addr.port(),
17718                    "family": socket_addr_family(&local_addr),
17719                }),
17720                "dgram.address",
17721            )
17722        }
17723        "dgram.setBufferSize" => {
17724            let socket_id =
17725                javascript_sync_rpc_arg_str(&request.args, 0, "dgram.setBufferSize socket id")?;
17726            let which =
17727                javascript_sync_rpc_arg_str(&request.args, 1, "dgram.setBufferSize buffer kind")?;
17728            let size = javascript_sync_rpc_arg_u64(&request.args, 2, "dgram.setBufferSize size")?;
17729            let size = usize::try_from(size).map_err(|_| {
17730                SidecarError::InvalidState(String::from(
17731                    "dgram.setBufferSize size must fit within usize",
17732                ))
17733            })?;
17734            let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
17735                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17736            })?;
17737            socket.set_buffer_size(which, size)?;
17738            Ok(Value::Null)
17739        }
17740        "dgram.getBufferSize" => {
17741            let socket_id =
17742                javascript_sync_rpc_arg_str(&request.args, 0, "dgram.getBufferSize socket id")?;
17743            let which =
17744                javascript_sync_rpc_arg_str(&request.args, 1, "dgram.getBufferSize buffer kind")?;
17745            let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
17746                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17747            })?;
17748            let size = socket.get_buffer_size(which)?;
17749            Ok(json!(size))
17750        }
17751        other => Err(SidecarError::InvalidState(format!(
17752            "unsupported JavaScript dgram sync RPC method {other}"
17753        ))),
17754    }
17755}
17756
17757#[derive(Debug)]
17758struct ClientHttp2StreamState {
17759    send_stream: Option<h2::SendStream<Bytes>>,
17760}
17761
17762#[derive(Debug)]
17763struct ServerHttp2StreamState {
17764    send_response: Option<ServerHttp2Responder>,
17765    send_stream: Option<h2::SendStream<Bytes>>,
17766}
17767
17768#[derive(Debug)]
17769enum ServerHttp2Responder {
17770    Regular(server::SendResponse<Bytes>),
17771    Pushed(server::SendPushedResponse<Bytes>),
17772}
17773
17774const HTTP2_DEFAULT_WINDOW_SIZE: u32 = 65_535;
17775const HTTP2_POLL_DELAY: Duration = Duration::from_millis(10);
17776
17777fn http2_runtime_snapshot() -> Http2RuntimeSnapshot {
17778    Http2RuntimeSnapshot {
17779        effective_local_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
17780        local_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
17781        remote_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
17782        next_stream_id: 1,
17783        outbound_queue_size: 1,
17784        deflate_dynamic_table_size: 0,
17785        inflate_dynamic_table_size: 0,
17786    }
17787}
17788
17789fn http2_snapshot_json(snapshot: &Http2SessionSnapshot) -> Result<String, SidecarError> {
17790    serde_json::to_string(snapshot)
17791        .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
17792}
17793
17794fn http2_event_value(event: &Http2BridgeEvent) -> Result<Value, SidecarError> {
17795    serde_json::to_string(event)
17796        .map(Value::String)
17797        .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
17798}
17799
17800fn push_http2_server_event(
17801    shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17802    server_id: u64,
17803    event: Http2BridgeEvent,
17804) {
17805    if let Ok(mut state) = shared.lock() {
17806        state
17807            .server_events
17808            .entry(server_id)
17809            .or_default()
17810            .push_back(event);
17811    }
17812}
17813
17814fn push_http2_session_event(
17815    shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17816    session_id: u64,
17817    event: Http2BridgeEvent,
17818) {
17819    if let Ok(mut state) = shared.lock() {
17820        state
17821            .session_events
17822            .entry(session_id)
17823            .or_default()
17824            .push_back(event);
17825    }
17826}
17827
17828fn pop_http2_event(
17829    queue: &mut BTreeMap<u64, VecDeque<Http2BridgeEvent>>,
17830    id: u64,
17831) -> Option<Http2BridgeEvent> {
17832    queue.get_mut(&id).and_then(VecDeque::pop_front)
17833}
17834
17835fn wait_for_http2_event(
17836    shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17837    id: u64,
17838    is_server: bool,
17839    wait_ms: u64,
17840) -> Option<Http2BridgeEvent> {
17841    let deadline = Instant::now() + Duration::from_millis(wait_ms);
17842    loop {
17843        if let Ok(mut state) = shared.lock() {
17844            let queue = if is_server {
17845                &mut state.server_events
17846            } else {
17847                &mut state.session_events
17848            };
17849            if let Some(event) = pop_http2_event(queue, id) {
17850                return Some(event);
17851            }
17852        }
17853        if wait_ms == 0 || Instant::now() >= deadline {
17854            return None;
17855        }
17856        thread::sleep(HTTP2_POLL_DELAY);
17857    }
17858}
17859
17860fn next_http2_session_id(shared: &mut crate::state::Http2SharedState) -> u64 {
17861    shared.next_session_id += 1;
17862    shared.next_session_id
17863}
17864
17865fn next_http2_stream_id(shared: &mut crate::state::Http2SharedState) -> u64 {
17866    shared.next_stream_id += 1;
17867    shared.next_stream_id
17868}
17869
17870fn http2_reason(code: Option<u32>) -> Reason {
17871    code.unwrap_or(Reason::NO_ERROR.into()).into()
17872}
17873
17874fn http2_error_payload(message: impl Into<String>) -> String {
17875    serde_json::to_string(&json!({
17876        "name": "Error",
17877        "code": "ERR_HTTP2_ERROR",
17878        "message": message.into(),
17879    }))
17880    .unwrap_or_else(|_| {
17881        String::from(
17882            "{\"name\":\"Error\",\"code\":\"ERR_HTTP2_ERROR\",\"message\":\"HTTP/2 bridge error\"}",
17883        )
17884    })
17885}
17886
17887fn http2_socket_snapshot(local_addr: SocketAddr, remote_addr: SocketAddr) -> Http2SocketSnapshot {
17888    Http2SocketSnapshot {
17889        encrypted: false,
17890        allow_half_open: false,
17891        local_address: Some(local_addr.ip().to_string()),
17892        local_port: Some(local_addr.port()),
17893        local_family: Some(socket_addr_family(&local_addr).to_string()),
17894        remote_address: Some(remote_addr.ip().to_string()),
17895        remote_port: Some(remote_addr.port()),
17896        remote_family: Some(socket_addr_family(&remote_addr).to_string()),
17897        servername: None,
17898        alpn_protocol: Some(String::from("h2c")),
17899    }
17900}
17901
17902fn http2_wait_result(kind: &str, id: u64) -> Value {
17903    json!({
17904        "kind": kind,
17905        "id": id,
17906    })
17907}
17908
17909fn is_http2_terminal_event(event: &Http2BridgeEvent, is_server: bool, id: u64) -> bool {
17910    if is_server {
17911        event.kind == "serverClose" && event.id == id
17912    } else {
17913        event.kind == "sessionClose" && event.id == id
17914    }
17915}
17916
17917fn dispatch_http2_wait_loop(
17918    process: &ActiveProcess,
17919    id: u64,
17920    is_server: bool,
17921) -> Result<Value, SidecarError> {
17922    loop {
17923        if let Some(event) = wait_for_http2_event(&process.http2.shared, id, is_server, 50) {
17924            let payload = serde_json::to_value(&event).map_err(|error| {
17925                SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}"))
17926            })?;
17927            process
17928                .execution
17929                .send_javascript_stream_event("http2", payload.clone())?;
17930            if is_http2_terminal_event(&event, is_server, id) {
17931                return Ok(payload);
17932            }
17933            continue;
17934        }
17935
17936        let exists = process
17937            .http2
17938            .shared
17939            .lock()
17940            .map(|state| {
17941                if is_server {
17942                    state.servers.contains_key(&id)
17943                } else {
17944                    state.sessions.contains_key(&id)
17945                }
17946            })
17947            .unwrap_or(false);
17948        if !exists {
17949            return Ok(if is_server {
17950                http2_wait_result("serverClose", id)
17951            } else {
17952                http2_wait_result("sessionClose", id)
17953            });
17954        }
17955    }
17956}
17957
17958fn dispatch_http_wait_loop(process: &ActiveProcess, server_id: u64) -> Result<Value, SidecarError> {
17959    loop {
17960        if !process.http_servers.contains_key(&server_id) {
17961            return Ok(json!({
17962                "kind": "serverClose",
17963                "id": server_id,
17964            }));
17965        }
17966        thread::sleep(Duration::from_millis(25));
17967    }
17968}
17969
17970fn http2_settings_from_value(settings: &BTreeMap<String, Value>) -> BTreeMap<String, Value> {
17971    settings.clone()
17972}
17973
17974fn parse_http2_headers_json(
17975    headers_json: &str,
17976    label: &str,
17977) -> Result<BTreeMap<String, Value>, SidecarError> {
17978    serde_json::from_str::<BTreeMap<String, Value>>(headers_json)
17979        .map_err(|error| SidecarError::InvalidState(format!("{label} must be valid JSON: {error}")))
17980}
17981
17982fn apply_http2_header_values(
17983    header_map: &mut HeaderMap,
17984    name: &str,
17985    value: &Value,
17986) -> Result<(), SidecarError> {
17987    let header_name = HeaderName::from_bytes(name.as_bytes()).map_err(|error| {
17988        SidecarError::InvalidState(format!("invalid HTTP/2 header name {name:?}: {error}"))
17989    })?;
17990    match value {
17991        Value::Array(values) => {
17992            for value in values {
17993                apply_http2_header_values(header_map, name, value)?;
17994            }
17995        }
17996        Value::String(text) => {
17997            let value = HeaderValue::from_str(text).map_err(|error| {
17998                SidecarError::InvalidState(format!(
17999                    "invalid HTTP/2 header value for {name}: {error}"
18000                ))
18001            })?;
18002            header_map.append(header_name.clone(), value);
18003        }
18004        Value::Number(number) => {
18005            let value = HeaderValue::from_str(&number.to_string()).map_err(|error| {
18006                SidecarError::InvalidState(format!(
18007                    "invalid HTTP/2 numeric header value for {name}: {error}"
18008                ))
18009            })?;
18010            header_map.append(header_name.clone(), value);
18011        }
18012        Value::Bool(boolean) => {
18013            let value = HeaderValue::from_str(if *boolean { "true" } else { "false" }).map_err(
18014                |error| {
18015                    SidecarError::InvalidState(format!(
18016                        "invalid HTTP/2 boolean header value for {name}: {error}"
18017                    ))
18018                },
18019            )?;
18020            header_map.append(header_name.clone(), value);
18021        }
18022        Value::Null => {}
18023        Value::Object(_) => {
18024            return Err(SidecarError::InvalidState(format!(
18025                "unsupported HTTP/2 header object value for {name}"
18026            )));
18027        }
18028    }
18029    Ok(())
18030}
18031
18032fn build_http2_request(headers_json: &str) -> Result<Request<()>, SidecarError> {
18033    let headers = parse_http2_headers_json(headers_json, "HTTP/2 request headers")?;
18034    let method = headers
18035        .get(":method")
18036        .and_then(Value::as_str)
18037        .unwrap_or("GET");
18038    let path = headers.get(":path").and_then(Value::as_str).unwrap_or("/");
18039    let mut builder = Request::builder()
18040        .method(Method::from_bytes(method.as_bytes()).map_err(|error| {
18041            SidecarError::InvalidState(format!("invalid HTTP/2 method {method:?}: {error}"))
18042        })?)
18043        .uri(path.parse::<Uri>().map_err(|error| {
18044            SidecarError::InvalidState(format!("invalid HTTP/2 path {path:?}: {error}"))
18045        })?);
18046    {
18047        let header_map = builder.headers_mut().expect("request header map");
18048        for (name, value) in &headers {
18049            if name.starts_with(':') {
18050                continue;
18051            }
18052            apply_http2_header_values(header_map, name, value)?;
18053        }
18054    }
18055    builder
18056        .body(())
18057        .map_err(|error| SidecarError::InvalidState(format!("invalid HTTP/2 request: {error}")))
18058}
18059
18060fn build_http2_response(headers_json: &str) -> Result<Response<()>, SidecarError> {
18061    let headers = parse_http2_headers_json(headers_json, "HTTP/2 response headers")?;
18062    let status = headers
18063        .get(":status")
18064        .and_then(Value::as_u64)
18065        .or_else(|| {
18066            headers
18067                .get(":status")
18068                .and_then(Value::as_str)
18069                .and_then(|value| value.parse::<u16>().ok().map(u64::from))
18070        })
18071        .unwrap_or(200);
18072    let mut builder = Response::builder().status(status as u16);
18073    {
18074        let header_map = builder.headers_mut().expect("response header map");
18075        for (name, value) in &headers {
18076            if name.starts_with(':') {
18077                continue;
18078            }
18079            apply_http2_header_values(header_map, name, value)?;
18080        }
18081    }
18082    builder.body(()).map_err(|error| {
18083        SidecarError::InvalidState(format!("invalid HTTP/2 response headers: {error}"))
18084    })
18085}
18086
18087fn serialize_http2_headers_map(
18088    pseudo: BTreeMap<String, Value>,
18089    headers: &HeaderMap,
18090) -> Result<String, SidecarError> {
18091    let mut serialized = pseudo;
18092    for (name, value) in headers {
18093        let name = name.as_str().to_string();
18094        let value = Value::String(
18095            value
18096                .to_str()
18097                .map_err(|error| {
18098                    SidecarError::Execution(format!("invalid HTTP/2 header value: {error}"))
18099                })?
18100                .to_owned(),
18101        );
18102        match serialized.get_mut(&name) {
18103            Some(Value::Array(values)) => values.push(value),
18104            Some(existing) => {
18105                let first = existing.clone();
18106                *existing = Value::Array(vec![first, value]);
18107            }
18108            None => {
18109                serialized.insert(name, value);
18110            }
18111        }
18112    }
18113    serde_json::to_string(&serialized)
18114        .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
18115}
18116
18117fn serialize_http2_request_headers(
18118    request: &Request<h2::RecvStream>,
18119) -> Result<String, SidecarError> {
18120    let mut pseudo = BTreeMap::new();
18121    pseudo.insert(
18122        String::from(":method"),
18123        Value::String(request.method().as_str().to_string()),
18124    );
18125    pseudo.insert(
18126        String::from(":path"),
18127        Value::String(
18128            request
18129                .uri()
18130                .path_and_query()
18131                .map(|value| value.as_str().to_string())
18132                .unwrap_or_else(|| String::from("/")),
18133        ),
18134    );
18135    serialize_http2_headers_map(pseudo, request.headers())
18136}
18137
18138fn serialize_http2_response_headers(
18139    response: &Response<h2::RecvStream>,
18140) -> Result<String, SidecarError> {
18141    let mut pseudo = BTreeMap::new();
18142    pseudo.insert(
18143        String::from(":status"),
18144        Value::Number(serde_json::Number::from(response.status().as_u16())),
18145    );
18146    serialize_http2_headers_map(pseudo, response.headers())
18147}
18148
18149fn remove_http2_session_resources(
18150    shared: &Arc<Mutex<crate::state::Http2SharedState>>,
18151    session_id: u64,
18152) {
18153    if let Ok(mut state) = shared.lock() {
18154        state.sessions.remove(&session_id);
18155        state.session_events.remove(&session_id);
18156        let stream_ids = state
18157            .streams
18158            .iter()
18159            .filter_map(|(stream_id, stream)| {
18160                (stream.session_id == session_id).then_some(*stream_id)
18161            })
18162            .collect::<Vec<_>>();
18163        for stream_id in stream_ids {
18164            state.streams.remove(&stream_id);
18165        }
18166    }
18167}
18168
18169fn spawn_http2_client_session(
18170    shared: Arc<Mutex<crate::state::Http2SharedState>>,
18171    session_id: u64,
18172    remote_addr: SocketAddr,
18173    tls: Option<JavascriptTlsBridgeOptions>,
18174    snapshot: Arc<Mutex<Http2SessionSnapshot>>,
18175    mut command_rx: UnboundedReceiver<Http2SessionCommand>,
18176) {
18177    thread::spawn(move || {
18178        let runtime = match TokioRuntimeBuilder::new_current_thread()
18179            .enable_all()
18180            .build()
18181        {
18182            Ok(runtime) => runtime,
18183            Err(error) => {
18184                push_http2_session_event(
18185                    &shared,
18186                    session_id,
18187                    Http2BridgeEvent {
18188                        kind: String::from("sessionError"),
18189                        id: session_id,
18190                        data: Some(http2_error_payload(error.to_string())),
18191                        ..Http2BridgeEvent::default()
18192                    },
18193                );
18194                remove_http2_session_resources(&shared, session_id);
18195                return;
18196            }
18197        };
18198
18199        runtime.block_on(async move {
18200            let stream = match tokio::net::TcpStream::connect(remote_addr).await {
18201                Ok(stream) => stream,
18202                Err(error) => {
18203                    push_http2_session_event(
18204                        &shared,
18205                        session_id,
18206                        Http2BridgeEvent {
18207                            kind: String::from("sessionError"),
18208                            id: session_id,
18209                            data: Some(http2_error_payload(error.to_string())),
18210                            ..Http2BridgeEvent::default()
18211                        },
18212                    );
18213                    remove_http2_session_resources(&shared, session_id);
18214                    return;
18215                }
18216            };
18217
18218            let local_addr = match stream.local_addr() {
18219                Ok(addr) => addr,
18220                Err(error) => {
18221                    push_http2_session_event(
18222                        &shared,
18223                        session_id,
18224                        Http2BridgeEvent {
18225                            kind: String::from("sessionError"),
18226                            id: session_id,
18227                            data: Some(http2_error_payload(error.to_string())),
18228                            ..Http2BridgeEvent::default()
18229                        },
18230                    );
18231                    remove_http2_session_resources(&shared, session_id);
18232                    return;
18233                }
18234            };
18235
18236            {
18237                let mut snapshot_guard = snapshot.lock().expect("http2 snapshot lock");
18238                snapshot_guard.socket = http2_socket_snapshot(local_addr, remote_addr);
18239                if let Some(options) = tls.as_ref() {
18240                    snapshot_guard.encrypted = true;
18241                    snapshot_guard.alpn_protocol = Some(String::from("h2"));
18242                    snapshot_guard.socket.encrypted = true;
18243                    snapshot_guard.socket.servername = options.servername.clone();
18244                    snapshot_guard.socket.alpn_protocol = Some(String::from("h2"));
18245                }
18246                snapshot_guard.state = http2_runtime_snapshot();
18247            }
18248            if let Ok(snapshot_json) =
18249                http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())
18250            {
18251                push_http2_session_event(
18252                    &shared,
18253                    session_id,
18254                    Http2BridgeEvent {
18255                        kind: String::from("sessionConnect"),
18256                        id: session_id,
18257                        data: Some(snapshot_json),
18258                        ..Http2BridgeEvent::default()
18259                    },
18260                );
18261            }
18262
18263            let io: Pin<Box<dyn Http2AsyncIo>> = if let Some(options) = tls.as_ref() {
18264                let server_name = match ServerName::try_from(
18265                    options
18266                        .servername
18267                        .clone()
18268                        .unwrap_or_else(|| String::from("localhost")),
18269                ) {
18270                    Ok(server_name) => server_name,
18271                    Err(_) => {
18272                        push_http2_session_event(
18273                            &shared,
18274                            session_id,
18275                            Http2BridgeEvent {
18276                                kind: String::from("sessionError"),
18277                                id: session_id,
18278                                data: Some(http2_error_payload("invalid TLS servername")),
18279                                ..Http2BridgeEvent::default()
18280                            },
18281                        );
18282                        remove_http2_session_resources(&shared, session_id);
18283                        return;
18284                    }
18285                };
18286                let connector = match build_client_tls_config(options) {
18287                    Ok(config) => TlsConnector::from(Arc::new(config)),
18288                    Err(error) => {
18289                        push_http2_session_event(
18290                            &shared,
18291                            session_id,
18292                            Http2BridgeEvent {
18293                                kind: String::from("sessionError"),
18294                                id: session_id,
18295                                data: Some(http2_error_payload(error.to_string())),
18296                                ..Http2BridgeEvent::default()
18297                            },
18298                        );
18299                        remove_http2_session_resources(&shared, session_id);
18300                        return;
18301                    }
18302                };
18303                match connector.connect(server_name, stream).await {
18304                    Ok(tls_stream) => Box::pin(tls_stream),
18305                    Err(error) => {
18306                        push_http2_session_event(
18307                            &shared,
18308                            session_id,
18309                            Http2BridgeEvent {
18310                                kind: String::from("sessionError"),
18311                                id: session_id,
18312                                data: Some(http2_error_payload(error.to_string())),
18313                                ..Http2BridgeEvent::default()
18314                            },
18315                        );
18316                        remove_http2_session_resources(&shared, session_id);
18317                        return;
18318                    }
18319                }
18320            } else {
18321                Box::pin(stream)
18322            };
18323
18324            let (mut sender, connection) = match client::handshake(io).await {
18325                Ok(parts) => parts,
18326                Err(error) => {
18327                    push_http2_session_event(
18328                        &shared,
18329                        session_id,
18330                        Http2BridgeEvent {
18331                            kind: String::from("sessionError"),
18332                            id: session_id,
18333                            data: Some(http2_error_payload(error.to_string())),
18334                            ..Http2BridgeEvent::default()
18335                        },
18336                    );
18337                    remove_http2_session_resources(&shared, session_id);
18338                    return;
18339                }
18340            };
18341
18342            let (status_tx, mut status_rx) = unbounded_channel::<Result<(), String>>();
18343            tokio::spawn(async move {
18344                let _ = status_tx.send(connection.await.map_err(|error| error.to_string()));
18345            });
18346
18347            let streams: Arc<Mutex<BTreeMap<u64, ClientHttp2StreamState>>> =
18348                Arc::new(Mutex::new(BTreeMap::new()));
18349
18350            loop {
18351                tokio::select! {
18352                    Some(result) = status_rx.recv() => {
18353                        if let Err(message) = result {
18354                            push_http2_session_event(
18355                                &shared,
18356                                session_id,
18357                                Http2BridgeEvent {
18358                                    kind: String::from("sessionError"),
18359                                    id: session_id,
18360                                    data: Some(http2_error_payload(message)),
18361                                    ..Http2BridgeEvent::default()
18362                                },
18363                            );
18364                        }
18365                        push_http2_session_event(
18366                            &shared,
18367                            session_id,
18368                            Http2BridgeEvent {
18369                                kind: String::from("sessionClose"),
18370                                id: session_id,
18371                                ..Http2BridgeEvent::default()
18372                            },
18373                        );
18374                        remove_http2_session_resources(&shared, session_id);
18375                        break;
18376                    }
18377                    Some(command) = command_rx.recv() => {
18378                        match command {
18379                            Http2SessionCommand::Request { headers_json, options_json, respond_to } => {
18380                                let request = match build_http2_request(&headers_json) {
18381                                    Ok(request) => request,
18382                                    Err(error) => {
18383                                        let _ = respond_to.send(Err(error.to_string()));
18384                                        continue;
18385                                    }
18386                                };
18387                                let options: JavascriptHttp2RequestOptions =
18388                                    serde_json::from_str(&options_json).unwrap_or_default();
18389                                let stream_id = {
18390                                    let mut state = shared.lock().expect("http2 shared state");
18391                                    let stream_id = next_http2_stream_id(&mut state);
18392                                    state.streams.insert(
18393                                        stream_id,
18394                                        ActiveHttp2Stream {
18395                                            session_id,
18396                                            paused: Arc::new(AtomicBool::new(false)),
18397                                        },
18398                                    );
18399                                    stream_id
18400                                };
18401                                match sender.send_request(request, options.end_stream) {
18402                                    Ok((response_future, send_stream)) => {
18403                                        if !options.end_stream {
18404                                            streams
18405                                                .lock()
18406                                                .expect("http2 client streams")
18407                                                .insert(stream_id, ClientHttp2StreamState { send_stream: Some(send_stream) });
18408                                        }
18409                                        let shared_clone = Arc::clone(&shared);
18410                                        let snapshot_clone = Arc::clone(&snapshot);
18411                                        tokio::spawn(async move {
18412                                            match response_future.await {
18413                                                Ok(response) => {
18414                                                    if let Ok(headers_json) = serialize_http2_response_headers(&response) {
18415                                                        push_http2_session_event(
18416                                                            &shared_clone,
18417                                                            session_id,
18418                                                            Http2BridgeEvent {
18419                                                                kind: String::from("clientResponseHeaders"),
18420                                                                id: stream_id,
18421                                                                data: Some(headers_json),
18422                                                                ..Http2BridgeEvent::default()
18423                                                            },
18424                                                        );
18425                                                    }
18426                                                    let mut body = response.into_body();
18427                                                    while let Some(chunk) = body.data().await {
18428                                                        match chunk {
18429                                                            Ok(bytes) => {
18430                                                                let paused = {
18431                                                                    let state = shared_clone.lock().expect("http2 shared state");
18432                                                                    state.streams.get(&stream_id).map(|stream| Arc::clone(&stream.paused))
18433                                                                };
18434                                                                if let Some(paused) = paused {
18435                                                                    while paused.load(Ordering::SeqCst) {
18436                                                                        tokio::time::sleep(HTTP2_POLL_DELAY).await;
18437                                                                    }
18438                                                                }
18439                                                                let _ = body.flow_control().release_capacity(bytes.len());
18440                                                                push_http2_session_event(
18441                                                                    &shared_clone,
18442                                                                    session_id,
18443                                                                    Http2BridgeEvent {
18444                                                                        kind: String::from("clientData"),
18445                                                                        id: stream_id,
18446                                                                        data: Some(base64::engine::general_purpose::STANDARD.encode(bytes)),
18447                                                                        ..Http2BridgeEvent::default()
18448                                                                    },
18449                                                                );
18450                                                            }
18451                                                            Err(error) => {
18452                                                                push_http2_session_event(
18453                                                                    &shared_clone,
18454                                                                    session_id,
18455                                                                    Http2BridgeEvent {
18456                                                                        kind: String::from("clientError"),
18457                                                                        id: stream_id,
18458                                                                        data: Some(http2_error_payload(error.to_string())),
18459                                                                        ..Http2BridgeEvent::default()
18460                                                                    },
18461                                                                );
18462                                                                break;
18463                                                            }
18464                                                        }
18465                                                    }
18466                                                    {
18467                                                        let mut snapshot = snapshot_clone.lock().expect("http2 snapshot lock");
18468                                                        snapshot.state.next_stream_id =
18469                                                            snapshot.state.next_stream_id.saturating_add(2);
18470                                                    }
18471                                                    push_http2_session_event(
18472                                                        &shared_clone,
18473                                                        session_id,
18474                                                        Http2BridgeEvent {
18475                                                            kind: String::from("clientEnd"),
18476                                                            id: stream_id,
18477                                                            ..Http2BridgeEvent::default()
18478                                                        },
18479                                                    );
18480                                                    push_http2_session_event(
18481                                                        &shared_clone,
18482                                                        session_id,
18483                                                        Http2BridgeEvent {
18484                                                            kind: String::from("clientClose"),
18485                                                            id: stream_id,
18486                                                            extra_number: Some(0),
18487                                                            ..Http2BridgeEvent::default()
18488                                                        },
18489                                                    );
18490                                                    if let Ok(mut state) = shared_clone.lock() {
18491                                                        state.streams.remove(&stream_id);
18492                                                    }
18493                                                }
18494                                                Err(error) => {
18495                                                    push_http2_session_event(
18496                                                        &shared_clone,
18497                                                        session_id,
18498                                                        Http2BridgeEvent {
18499                                                            kind: String::from("clientError"),
18500                                                            id: stream_id,
18501                                                            data: Some(http2_error_payload(error.to_string())),
18502                                                            ..Http2BridgeEvent::default()
18503                                                        },
18504                                                    );
18505                                                    push_http2_session_event(
18506                                                        &shared_clone,
18507                                                        session_id,
18508                                                        Http2BridgeEvent {
18509                                                            kind: String::from("clientClose"),
18510                                                            id: stream_id,
18511                                                            extra_number: Some(u32::from(Reason::INTERNAL_ERROR) as u64),
18512                                                            ..Http2BridgeEvent::default()
18513                                                        },
18514                                                    );
18515                                                    if let Ok(mut state) = shared_clone.lock() {
18516                                                        state.streams.remove(&stream_id);
18517                                                    }
18518                                                }
18519                                            }
18520                                        });
18521                                        let _ = respond_to.send(Ok(json!(stream_id)));
18522                                    }
18523                                    Err(error) => {
18524                                        if let Ok(mut state) = shared.lock() {
18525                                            state.streams.remove(&stream_id);
18526                                        }
18527                                        let _ = respond_to.send(Err(error.to_string()));
18528                                    }
18529                                }
18530                            }
18531                            Http2SessionCommand::Settings { settings_json, respond_to } => {
18532                                let settings = serde_json::from_str::<BTreeMap<String, Value>>(&settings_json)
18533                                    .unwrap_or_default();
18534                                {
18535                                    let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18536                                    snapshot.local_settings = http2_settings_from_value(&settings);
18537                                }
18538                                if let Ok(headers_json) = serde_json::to_string(&settings) {
18539                                    push_http2_session_event(
18540                                        &shared,
18541                                        session_id,
18542                                        Http2BridgeEvent {
18543                                            kind: String::from("sessionLocalSettings"),
18544                                            id: session_id,
18545                                            data: Some(headers_json.clone()),
18546                                            ..Http2BridgeEvent::default()
18547                                        },
18548                                    );
18549                                    push_http2_session_event(
18550                                        &shared,
18551                                        session_id,
18552                                        Http2BridgeEvent {
18553                                            kind: String::from("sessionSettingsAck"),
18554                                            id: session_id,
18555                                            ..Http2BridgeEvent::default()
18556                                        },
18557                                    );
18558                                }
18559                                let _ = respond_to.send(Ok(Value::Null));
18560                            }
18561                            Http2SessionCommand::SetLocalWindowSize { size, respond_to } => {
18562                                {
18563                                    let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18564                                    snapshot.state.local_window_size = size;
18565                                    snapshot.state.effective_local_window_size = size;
18566                                }
18567                                let value = snapshot
18568                                    .lock()
18569                                    .ok()
18570                                    .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok())
18571                                    .map(Value::String)
18572                                    .unwrap_or(Value::Null);
18573                                let _ = respond_to.send(Ok(value));
18574                            }
18575                            Http2SessionCommand::Goaway { error_code, last_stream_id, opaque_data, respond_to } => {
18576                                push_http2_session_event(
18577                                    &shared,
18578                                    session_id,
18579                                    Http2BridgeEvent {
18580                                        kind: String::from("sessionGoaway"),
18581                                        id: session_id,
18582                                        data: opaque_data.map(|value| {
18583                                            base64::engine::general_purpose::STANDARD.encode(value)
18584                                        }),
18585                                        extra_number: Some(error_code as u64),
18586                                        flags: Some(last_stream_id as u64),
18587                                        ..Http2BridgeEvent::default()
18588                                    },
18589                                );
18590                                let _ = respond_to.send(Ok(Value::Null));
18591                            }
18592                            Http2SessionCommand::Close { respond_to, .. } => {
18593                                let _ = respond_to.send(Ok(Value::Null));
18594                                push_http2_session_event(
18595                                    &shared,
18596                                    session_id,
18597                                    Http2BridgeEvent {
18598                                        kind: String::from("sessionClose"),
18599                                        id: session_id,
18600                                        ..Http2BridgeEvent::default()
18601                                    },
18602                                );
18603                                remove_http2_session_resources(&shared, session_id);
18604                                break;
18605                            }
18606                            Http2SessionCommand::StreamWrite { stream_id, chunk, end_stream, respond_to } => {
18607                                let result = streams
18608                                    .lock()
18609                                    .expect("http2 client streams")
18610                                    .get_mut(&stream_id)
18611                                    .and_then(|stream| stream.send_stream.as_mut())
18612                                    .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 client stream {stream_id}")))
18613                                    .and_then(|stream| stream.send_data(Bytes::from(chunk), end_stream).map_err(|error| SidecarError::Execution(error.to_string())));
18614                                match result {
18615                                    Ok(()) => {
18616                                        if end_stream {
18617                                            streams.lock().expect("http2 client streams").remove(&stream_id);
18618                                        }
18619                                        let _ = respond_to.send(Ok(Value::Bool(true)));
18620                                    }
18621                                    Err(error) => {
18622                                        let _ = respond_to.send(Err(error.to_string()));
18623                                    }
18624                                }
18625                            }
18626                            Http2SessionCommand::StreamClose { stream_id, error_code, respond_to } => {
18627                                let mut streams = streams.lock().expect("http2 client streams");
18628                                let Some(mut state) = streams.remove(&stream_id) else {
18629                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 client stream {stream_id}")));
18630                                    continue;
18631                                };
18632                                if let Some(stream) = state.send_stream.as_mut() {
18633                                    stream.send_reset(http2_reason(error_code));
18634                                }
18635                                if let Ok(mut state) = shared.lock() {
18636                                    state.streams.remove(&stream_id);
18637                                }
18638                                push_http2_session_event(
18639                                    &shared,
18640                                    session_id,
18641                                    Http2BridgeEvent {
18642                                        kind: String::from("clientClose"),
18643                                        id: stream_id,
18644                                        extra_number: Some(u32::from(http2_reason(error_code)) as u64),
18645                                        ..Http2BridgeEvent::default()
18646                                    },
18647                                );
18648                                let _ = respond_to.send(Ok(Value::Null));
18649                            }
18650                            Http2SessionCommand::StreamRespond { respond_to, .. }
18651                            | Http2SessionCommand::StreamPush { respond_to, .. }
18652                            | Http2SessionCommand::StreamRespondWithFile { respond_to, .. } => {
18653                                let _ = respond_to.send(Err(String::from("HTTP/2 client streams cannot send server responses")));
18654                            }
18655                        }
18656                    }
18657                    else => break,
18658                }
18659            }
18660        });
18661    });
18662}
18663
18664fn spawn_http2_server_session(
18665    shared: Arc<Mutex<crate::state::Http2SharedState>>,
18666    server_id: u64,
18667    session_id: u64,
18668    stream: TcpStream,
18669    tls: Option<JavascriptTlsBridgeOptions>,
18670    snapshot: Arc<Mutex<Http2SessionSnapshot>>,
18671    mut command_rx: UnboundedReceiver<Http2SessionCommand>,
18672) {
18673    thread::spawn(move || {
18674        let runtime = match TokioRuntimeBuilder::new_current_thread()
18675            .enable_all()
18676            .build()
18677        {
18678            Ok(runtime) => runtime,
18679            Err(error) => {
18680                push_http2_server_event(
18681                    &shared,
18682                    server_id,
18683                    Http2BridgeEvent {
18684                        kind: String::from("serverStreamError"),
18685                        id: session_id,
18686                        data: Some(http2_error_payload(error.to_string())),
18687                        ..Http2BridgeEvent::default()
18688                    },
18689                );
18690                remove_http2_session_resources(&shared, session_id);
18691                return;
18692            }
18693        };
18694
18695        runtime.block_on(async move {
18696            if let Err(error) = stream.set_nonblocking(true) {
18697                push_http2_server_event(
18698                    &shared,
18699                    server_id,
18700                    Http2BridgeEvent {
18701                        kind: String::from("serverStreamError"),
18702                        id: session_id,
18703                        data: Some(http2_error_payload(error.to_string())),
18704                        ..Http2BridgeEvent::default()
18705                    },
18706                );
18707                remove_http2_session_resources(&shared, session_id);
18708                return;
18709            }
18710            let stream = match tokio::net::TcpStream::from_std(stream) {
18711                Ok(stream) => stream,
18712                Err(error) => {
18713                    push_http2_server_event(
18714                        &shared,
18715                        server_id,
18716                        Http2BridgeEvent {
18717                            kind: String::from("serverStreamError"),
18718                            id: session_id,
18719                            data: Some(http2_error_payload(error.to_string())),
18720                            ..Http2BridgeEvent::default()
18721                        },
18722                    );
18723                    remove_http2_session_resources(&shared, session_id);
18724                    return;
18725                }
18726            };
18727            let local_addr = match stream.local_addr() {
18728                Ok(addr) => addr,
18729                Err(error) => {
18730                    push_http2_server_event(
18731                        &shared,
18732                        server_id,
18733                        Http2BridgeEvent {
18734                            kind: String::from("serverStreamError"),
18735                            id: session_id,
18736                            data: Some(http2_error_payload(error.to_string())),
18737                            ..Http2BridgeEvent::default()
18738                        },
18739                    );
18740                    remove_http2_session_resources(&shared, session_id);
18741                    return;
18742                }
18743            };
18744            let remote_addr = match stream.peer_addr() {
18745                Ok(addr) => addr,
18746                Err(error) => {
18747                    push_http2_server_event(
18748                        &shared,
18749                        server_id,
18750                        Http2BridgeEvent {
18751                            kind: String::from("serverStreamError"),
18752                            id: session_id,
18753                            data: Some(http2_error_payload(error.to_string())),
18754                            ..Http2BridgeEvent::default()
18755                        },
18756                    );
18757                    remove_http2_session_resources(&shared, session_id);
18758                    return;
18759                }
18760            };
18761            {
18762                let mut snapshot_guard = snapshot.lock().expect("http2 snapshot lock");
18763                snapshot_guard.socket = http2_socket_snapshot(local_addr, remote_addr);
18764                if tls.is_some() {
18765                    snapshot_guard.encrypted = true;
18766                    snapshot_guard.alpn_protocol = Some(String::from("h2"));
18767                    snapshot_guard.socket.encrypted = true;
18768                    snapshot_guard.socket.alpn_protocol = Some(String::from("h2"));
18769                }
18770                snapshot_guard.state = http2_runtime_snapshot();
18771            }
18772            if let Ok(snapshot_json) =
18773                http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())
18774            {
18775                push_http2_server_event(
18776                    &shared,
18777                    server_id,
18778                    Http2BridgeEvent {
18779                        kind: String::from(if tls.is_some() {
18780                            "serverSecureConnection"
18781                        } else {
18782                            "serverConnection"
18783                        }),
18784                        id: server_id,
18785                        data: Some(serde_json::to_string(&http2_socket_snapshot(local_addr, remote_addr)).unwrap_or_default()),
18786                        ..Http2BridgeEvent::default()
18787                    },
18788                );
18789                push_http2_server_event(
18790                    &shared,
18791                    server_id,
18792                    Http2BridgeEvent {
18793                        kind: String::from("serverSession"),
18794                        id: server_id,
18795                        data: Some(snapshot_json),
18796                        extra_number: Some(session_id),
18797                        ..Http2BridgeEvent::default()
18798                    },
18799                );
18800            }
18801
18802            let io: Pin<Box<dyn Http2AsyncIo>> = if let Some(options) = tls.as_ref() {
18803                let acceptor = match build_server_tls_config(options) {
18804                    Ok(config) => TlsAcceptor::from(Arc::new(config)),
18805                    Err(error) => {
18806                        push_http2_server_event(
18807                            &shared,
18808                            server_id,
18809                            Http2BridgeEvent {
18810                                kind: String::from("serverStreamError"),
18811                                id: session_id,
18812                                data: Some(http2_error_payload(error.to_string())),
18813                                ..Http2BridgeEvent::default()
18814                            },
18815                        );
18816                        remove_http2_session_resources(&shared, session_id);
18817                        return;
18818                    }
18819                };
18820                match acceptor.accept(stream).await {
18821                    Ok(tls_stream) => Box::pin(tls_stream),
18822                    Err(error) => {
18823                        push_http2_server_event(
18824                            &shared,
18825                            server_id,
18826                            Http2BridgeEvent {
18827                                kind: String::from("serverStreamError"),
18828                                id: session_id,
18829                                data: Some(http2_error_payload(error.to_string())),
18830                                ..Http2BridgeEvent::default()
18831                            },
18832                        );
18833                        remove_http2_session_resources(&shared, session_id);
18834                        return;
18835                    }
18836                }
18837            } else {
18838                Box::pin(stream)
18839            };
18840
18841            let mut connection = match server::handshake(io).await {
18842                Ok(connection) => connection,
18843                Err(error) => {
18844                    push_http2_server_event(
18845                        &shared,
18846                        server_id,
18847                        Http2BridgeEvent {
18848                            kind: String::from("serverStreamError"),
18849                            id: session_id,
18850                            data: Some(http2_error_payload(error.to_string())),
18851                            ..Http2BridgeEvent::default()
18852                        },
18853                    );
18854                    remove_http2_session_resources(&shared, session_id);
18855                    return;
18856                }
18857            };
18858
18859            let streams: Arc<Mutex<BTreeMap<u64, ServerHttp2StreamState>>> =
18860                Arc::new(Mutex::new(BTreeMap::new()));
18861
18862            loop {
18863                tokio::select! {
18864                    incoming = connection.accept() => {
18865                        match incoming {
18866                            Some(Ok((request, respond))) => {
18867                                let headers_json = match serialize_http2_request_headers(&request) {
18868                                    Ok(headers) => headers,
18869                                    Err(error) => {
18870                                        push_http2_server_event(
18871                                            &shared,
18872                                            server_id,
18873                                            Http2BridgeEvent {
18874                                                kind: String::from("serverStreamError"),
18875                                                id: server_id,
18876                                                data: Some(http2_error_payload(error.to_string())),
18877                                                ..Http2BridgeEvent::default()
18878                                            },
18879                                        );
18880                                        continue;
18881                                    }
18882                                };
18883                                let stream_id = {
18884                                    let mut state = shared.lock().expect("http2 shared state");
18885                                    let stream_id = next_http2_stream_id(&mut state);
18886                                    state.streams.insert(
18887                                        stream_id,
18888                                        ActiveHttp2Stream {
18889                                            session_id,
18890                                            paused: Arc::new(AtomicBool::new(false)),
18891                                        },
18892                                    );
18893                                    stream_id
18894                                };
18895                                streams.lock().expect("http2 server streams").insert(
18896                                    stream_id,
18897                                    ServerHttp2StreamState {
18898                                        send_response: Some(ServerHttp2Responder::Regular(respond)),
18899                                        send_stream: None,
18900                                    },
18901                                );
18902                                let snapshot_json = snapshot
18903                                    .lock()
18904                                    .ok()
18905                                    .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok());
18906                                push_http2_server_event(
18907                                    &shared,
18908                                    server_id,
18909                                    Http2BridgeEvent {
18910                                        kind: String::from("serverStream"),
18911                                        id: server_id,
18912                                        data: Some(stream_id.to_string()),
18913                                        extra: snapshot_json,
18914                                        extra_number: Some(session_id),
18915                                        extra_headers: Some(headers_json),
18916                                        flags: Some(0),
18917                                    },
18918                                );
18919                                let shared_clone = Arc::clone(&shared);
18920                                tokio::spawn(async move {
18921                                    let mut body = request.into_body();
18922                                    while let Some(chunk) = body.data().await {
18923                                        match chunk {
18924                                            Ok(bytes) => {
18925                                                let paused = {
18926                                                    let state = shared_clone.lock().expect("http2 shared state");
18927                                                    state.streams.get(&stream_id).map(|stream| Arc::clone(&stream.paused))
18928                                                };
18929                                                if let Some(paused) = paused {
18930                                                    while paused.load(Ordering::SeqCst) {
18931                                                        tokio::time::sleep(HTTP2_POLL_DELAY).await;
18932                                                    }
18933                                                }
18934                                                let _ = body.flow_control().release_capacity(bytes.len());
18935                                                push_http2_server_event(
18936                                                    &shared_clone,
18937                                                    server_id,
18938                                                    Http2BridgeEvent {
18939                                                        kind: String::from("serverStreamData"),
18940                                                        id: stream_id,
18941                                                        data: Some(base64::engine::general_purpose::STANDARD.encode(bytes)),
18942                                                        ..Http2BridgeEvent::default()
18943                                                    },
18944                                                );
18945                                            }
18946                                            Err(error) => {
18947                                                push_http2_server_event(
18948                                                    &shared_clone,
18949                                                    server_id,
18950                                                    Http2BridgeEvent {
18951                                                        kind: String::from("serverStreamError"),
18952                                                        id: stream_id,
18953                                                        data: Some(http2_error_payload(error.to_string())),
18954                                                        ..Http2BridgeEvent::default()
18955                                                    },
18956                                                );
18957                                                break;
18958                                            }
18959                                        }
18960                                    }
18961                                    push_http2_server_event(
18962                                        &shared_clone,
18963                                        server_id,
18964                                        Http2BridgeEvent {
18965                                            kind: String::from("serverStreamEnd"),
18966                                            id: stream_id,
18967                                            ..Http2BridgeEvent::default()
18968                                        },
18969                                    );
18970                                });
18971                            }
18972                            Some(Err(error)) => {
18973                                push_http2_server_event(
18974                                    &shared,
18975                                    server_id,
18976                                    Http2BridgeEvent {
18977                                        kind: String::from("serverStreamError"),
18978                                        id: server_id,
18979                                        data: Some(http2_error_payload(error.to_string())),
18980                                        ..Http2BridgeEvent::default()
18981                                    },
18982                                );
18983                                break;
18984                            }
18985                            None => {
18986                                push_http2_server_event(
18987                                    &shared,
18988                                    server_id,
18989                                    Http2BridgeEvent {
18990                                        kind: String::from("sessionClose"),
18991                                        id: session_id,
18992                                        ..Http2BridgeEvent::default()
18993                                    },
18994                                );
18995                                remove_http2_session_resources(&shared, session_id);
18996                                break;
18997                            }
18998                        }
18999                    }
19000                    Some(command) = command_rx.recv() => {
19001                        match command {
19002                            Http2SessionCommand::Settings { settings_json, respond_to } => {
19003                                let settings = serde_json::from_str::<BTreeMap<String, Value>>(&settings_json)
19004                                    .unwrap_or_default();
19005                                if let Some(initial_window_size) = settings
19006                                    .get("initialWindowSize")
19007                                    .and_then(Value::as_u64)
19008                                {
19009                                    let _ = connection.set_initial_window_size(initial_window_size as u32);
19010                                }
19011                                {
19012                                    let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
19013                                    snapshot.local_settings = http2_settings_from_value(&settings);
19014                                }
19015                                if let Ok(headers_json) = serde_json::to_string(&settings) {
19016                                    push_http2_session_event(
19017                                        &shared,
19018                                        session_id,
19019                                        Http2BridgeEvent {
19020                                            kind: String::from("sessionLocalSettings"),
19021                                            id: session_id,
19022                                            data: Some(headers_json),
19023                                            ..Http2BridgeEvent::default()
19024                                        },
19025                                    );
19026                                }
19027                                let _ = respond_to.send(Ok(Value::Null));
19028                            }
19029                            Http2SessionCommand::SetLocalWindowSize { size, respond_to } => {
19030                                connection.set_target_window_size(size);
19031                                {
19032                                    let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
19033                                    snapshot.state.local_window_size = size;
19034                                    snapshot.state.effective_local_window_size = size;
19035                                }
19036                                let value = snapshot
19037                                    .lock()
19038                                    .ok()
19039                                    .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok())
19040                                    .map(Value::String)
19041                                    .unwrap_or(Value::Null);
19042                                let _ = respond_to.send(Ok(value));
19043                            }
19044                            Http2SessionCommand::Goaway { error_code, last_stream_id, opaque_data, respond_to } => {
19045                                connection.abrupt_shutdown(http2_reason(Some(error_code)));
19046                                push_http2_session_event(
19047                                    &shared,
19048                                    session_id,
19049                                    Http2BridgeEvent {
19050                                        kind: String::from("sessionGoaway"),
19051                                        id: session_id,
19052                                        data: opaque_data.map(|value| {
19053                                            base64::engine::general_purpose::STANDARD.encode(value)
19054                                        }),
19055                                        extra_number: Some(error_code as u64),
19056                                        flags: Some(last_stream_id as u64),
19057                                        ..Http2BridgeEvent::default()
19058                                    },
19059                                );
19060                                let _ = respond_to.send(Ok(Value::Null));
19061                            }
19062                            Http2SessionCommand::Close { abrupt, respond_to } => {
19063                                if abrupt {
19064                                    connection.abrupt_shutdown(Reason::NO_ERROR);
19065                                } else {
19066                                    connection.graceful_shutdown();
19067                                }
19068                                let _ = respond_to.send(Ok(Value::Null));
19069                                push_http2_session_event(
19070                                    &shared,
19071                                    session_id,
19072                                    Http2BridgeEvent {
19073                                        kind: String::from("sessionClose"),
19074                                        id: session_id,
19075                                        ..Http2BridgeEvent::default()
19076                                    },
19077                                );
19078                                remove_http2_session_resources(&shared, session_id);
19079                                break;
19080                            }
19081                            Http2SessionCommand::StreamRespond { stream_id, headers_json, respond_to } => {
19082                                let response = match build_http2_response(&headers_json) {
19083                                    Ok(response) => response,
19084                                    Err(error) => {
19085                                        let _ = respond_to.send(Err(error.to_string()));
19086                                        continue;
19087                                    }
19088                                };
19089                                let mut streams = streams.lock().expect("http2 server streams");
19090                                let Some(state) = streams.get_mut(&stream_id) else {
19091                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
19092                                    continue;
19093                                };
19094                                let Some(send_response) = state.send_response.as_mut() else {
19095                                    let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} already responded")));
19096                                    continue;
19097                                };
19098                                match match send_response {
19099                                    ServerHttp2Responder::Regular(send_response) => {
19100                                        send_response.send_response(response, false)
19101                                    }
19102                                    ServerHttp2Responder::Pushed(send_response) => {
19103                                        send_response.send_response(response, false)
19104                                    }
19105                                } {
19106                                    Ok(send_stream) => {
19107                                        state.send_stream = Some(send_stream);
19108                                        state.send_response = None;
19109                                        let _ = respond_to.send(Ok(Value::Null));
19110                                    }
19111                                    Err(error) => {
19112                                        let _ = respond_to.send(Err(error.to_string()));
19113                                    }
19114                                }
19115                            }
19116                            Http2SessionCommand::StreamPush { stream_id, headers_json, respond_to } => {
19117                                let request = match build_http2_request(&headers_json) {
19118                                    Ok(request) => request,
19119                                    Err(error) => {
19120                                        let _ = respond_to.send(Err(error.to_string()));
19121                                        continue;
19122                                    }
19123                                };
19124                                let mut streams_guard = streams.lock().expect("http2 server streams");
19125                                let Some(state) = streams_guard.get_mut(&stream_id) else {
19126                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
19127                                    continue;
19128                                };
19129                                let Some(send_response) = state.send_response.as_mut() else {
19130                                    let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} cannot push after responding")));
19131                                    continue;
19132                                };
19133                                let ServerHttp2Responder::Regular(send_response) = send_response else {
19134                                    let _ = respond_to.send(Err(format!("HTTP/2 pushed stream {stream_id} cannot create nested push promises")));
19135                                    continue;
19136                                };
19137                                match send_response.push_request(request) {
19138                                    Ok(pushed) => {
19139                                        let pushed_stream_id = {
19140                                            let mut state = shared.lock().expect("http2 shared state");
19141                                            let pushed_stream_id = next_http2_stream_id(&mut state);
19142                                            state.streams.insert(
19143                                                pushed_stream_id,
19144                                                ActiveHttp2Stream {
19145                                                    session_id,
19146                                                    paused: Arc::new(AtomicBool::new(false)),
19147                                                },
19148                                            );
19149                                            pushed_stream_id
19150                                        };
19151                                        streams_guard.insert(
19152                                            pushed_stream_id,
19153                                            ServerHttp2StreamState {
19154                                                send_response: Some(ServerHttp2Responder::Pushed(pushed)),
19155                                                send_stream: None,
19156                                            },
19157                                        );
19158                                        let _ = respond_to.send(Ok(json!({
19159                                            "streamId": pushed_stream_id,
19160                                            "headers": headers_json,
19161                                        }).to_string().into()));
19162                                    }
19163                                    Err(error) => {
19164                                        let _ = respond_to.send(Err(error.to_string()));
19165                                    }
19166                                }
19167                            }
19168                            Http2SessionCommand::StreamWrite { stream_id, chunk, end_stream, respond_to } => {
19169                                let mut streams = streams.lock().expect("http2 server streams");
19170                                let Some(state) = streams.get_mut(&stream_id) else {
19171                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
19172                                    continue;
19173                                };
19174                                let Some(send_stream) = state.send_stream.as_mut() else {
19175                                    let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} has not sent response headers")));
19176                                    continue;
19177                                };
19178                                match send_stream.send_data(Bytes::from(chunk), end_stream) {
19179                                    Ok(()) => {
19180                                        if end_stream {
19181                                            streams.remove(&stream_id);
19182                                            if let Ok(mut state) = shared.lock() {
19183                                                state.streams.remove(&stream_id);
19184                                            }
19185                                            push_http2_server_event(
19186                                                &shared,
19187                                                server_id,
19188                                                Http2BridgeEvent {
19189                                                    kind: String::from("serverStreamClose"),
19190                                                    id: stream_id,
19191                                                    extra_number: Some(0),
19192                                                    ..Http2BridgeEvent::default()
19193                                                },
19194                                            );
19195                                        }
19196                                        let _ = respond_to.send(Ok(Value::Bool(true)));
19197                                    }
19198                                    Err(error) => {
19199                                        let _ = respond_to.send(Err(error.to_string()));
19200                                    }
19201                                }
19202                            }
19203                            Http2SessionCommand::StreamClose { stream_id, error_code, respond_to } => {
19204                                let mut streams_guard = streams.lock().expect("http2 server streams");
19205                                let Some(mut state) = streams_guard.remove(&stream_id) else {
19206                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
19207                                    continue;
19208                                };
19209                                let reason = http2_reason(error_code);
19210                                if let Some(send_stream) = state.send_stream.as_mut() {
19211                                    send_stream.send_reset(reason);
19212                                }
19213                                if let Some(send_response) = state.send_response.as_mut() {
19214                                    match send_response {
19215                                        ServerHttp2Responder::Regular(send_response) => {
19216                                            send_response.send_reset(reason)
19217                                        }
19218                                        ServerHttp2Responder::Pushed(send_response) => {
19219                                            send_response.send_reset(reason)
19220                                        }
19221                                    }
19222                                }
19223                                if let Ok(mut shared_guard) = shared.lock() {
19224                                    shared_guard.streams.remove(&stream_id);
19225                                }
19226                                push_http2_server_event(
19227                                    &shared,
19228                                    server_id,
19229                                    Http2BridgeEvent {
19230                                        kind: String::from("serverStreamClose"),
19231                                        id: stream_id,
19232                                        extra_number: Some(u32::from(reason) as u64),
19233                                        ..Http2BridgeEvent::default()
19234                                    },
19235                                );
19236                                let _ = respond_to.send(Ok(Value::Null));
19237                            }
19238                            Http2SessionCommand::StreamRespondWithFile { stream_id, body, headers_json, options_json, respond_to } => {
19239                                let options: JavascriptHttp2FileResponseOptions =
19240                                    serde_json::from_str(&options_json).unwrap_or_default();
19241                                let response = match build_http2_response(&headers_json) {
19242                                    Ok(response) => response,
19243                                    Err(error) => {
19244                                        let _ = respond_to.send(Err(error.to_string()));
19245                                        continue;
19246                                    }
19247                                };
19248                                let offset = usize::try_from(options.offset.unwrap_or_default()).unwrap_or(0);
19249                                let body = if offset >= body.len() {
19250                                    Vec::new()
19251                                } else {
19252                                    let body = &body[offset..];
19253                                    match options.length {
19254                                        Some(length) if length >= 0 => {
19255                                            body[..body.len().min(length as usize)].to_vec()
19256                                        }
19257                                        _ => body.to_vec(),
19258                                    }
19259                                };
19260                                let mut streams_guard = streams.lock().expect("http2 server streams");
19261                                let Some(state) = streams_guard.get_mut(&stream_id) else {
19262                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
19263                                    continue;
19264                                };
19265                                let Some(send_response) = state.send_response.as_mut() else {
19266                                    let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} already responded")));
19267                                    continue;
19268                                };
19269                                match match send_response {
19270                                    ServerHttp2Responder::Regular(send_response) => {
19271                                        send_response.send_response(response, body.is_empty())
19272                                    }
19273                                    ServerHttp2Responder::Pushed(send_response) => {
19274                                        send_response.send_response(response, body.is_empty())
19275                                    }
19276                                } {
19277                                    Ok(mut send_stream) => {
19278                                        state.send_response = None;
19279                                        if body.is_empty() {
19280                                            streams_guard.remove(&stream_id);
19281                                            if let Ok(mut shared_guard) = shared.lock() {
19282                                                shared_guard.streams.remove(&stream_id);
19283                                            }
19284                                        } else {
19285                                            if let Err(error) = send_stream.send_data(Bytes::from(body), true) {
19286                                                let _ = respond_to.send(Err(error.to_string()));
19287                                                continue;
19288                                            }
19289                                            streams_guard.remove(&stream_id);
19290                                            if let Ok(mut shared_guard) = shared.lock() {
19291                                                shared_guard.streams.remove(&stream_id);
19292                                            }
19293                                        }
19294                                        push_http2_server_event(
19295                                            &shared,
19296                                            server_id,
19297                                            Http2BridgeEvent {
19298                                                kind: String::from("serverStreamClose"),
19299                                                id: stream_id,
19300                                                extra_number: Some(0),
19301                                                ..Http2BridgeEvent::default()
19302                                            },
19303                                        );
19304                                        let _ = respond_to.send(Ok(Value::Null));
19305                                    }
19306                                    Err(error) => {
19307                                        let _ = respond_to.send(Err(error.to_string()));
19308                                    }
19309                                }
19310                            }
19311                            Http2SessionCommand::Request { respond_to, .. } => {
19312                                let _ = respond_to.send(Err(String::from("HTTP/2 server sessions cannot initiate client requests")));
19313                            }
19314                        }
19315                    }
19316                    else => break,
19317                }
19318            }
19319        });
19320    });
19321}
19322
19323fn spawn_http2_server_accept_loop(
19324    shared: Arc<Mutex<crate::state::Http2SharedState>>,
19325    server_id: u64,
19326    listener: TcpListener,
19327) {
19328    thread::spawn(move || {
19329        let listener = listener;
19330        loop {
19331            let closed = shared
19332                .lock()
19333                .ok()
19334                .and_then(|state| {
19335                    state
19336                        .servers
19337                        .get(&server_id)
19338                        .map(|server| server.closed.load(Ordering::SeqCst))
19339                })
19340                .unwrap_or(true);
19341            if closed {
19342                break;
19343            }
19344            match listener.accept() {
19345                Ok((stream, _)) => {
19346                    let (command_tx, command_rx) = unbounded_channel();
19347                    let (guest_local_addr, secure, tls) = {
19348                        let state = shared.lock().expect("http2 shared state");
19349                        let server = state.servers.get(&server_id).expect("http2 server state");
19350                        (server.guest_local_addr, server.secure, server.tls.clone())
19351                    };
19352                    let (local_addr, remote_addr) = match (stream.local_addr(), stream.peer_addr())
19353                    {
19354                        (Ok(local_addr), Ok(remote_addr)) => (local_addr, remote_addr),
19355                        _ => continue,
19356                    };
19357                    let session_snapshot = Arc::new(Mutex::new(Http2SessionSnapshot {
19358                        encrypted: secure,
19359                        alpn_protocol: Some(if secure {
19360                            String::from("h2")
19361                        } else {
19362                            String::from("h2c")
19363                        }),
19364                        local_settings: BTreeMap::new(),
19365                        remote_settings: BTreeMap::new(),
19366                        state: http2_runtime_snapshot(),
19367                        socket: Http2SocketSnapshot {
19368                            local_address: Some(guest_local_addr.ip().to_string()),
19369                            local_port: Some(guest_local_addr.port()),
19370                            local_family: Some(socket_addr_family(&guest_local_addr).to_string()),
19371                            remote_address: Some(remote_addr.ip().to_string()),
19372                            remote_port: Some(remote_addr.port()),
19373                            remote_family: Some(socket_addr_family(&remote_addr).to_string()),
19374                            ..http2_socket_snapshot(local_addr, remote_addr)
19375                        },
19376                        ..Http2SessionSnapshot::default()
19377                    }));
19378                    let session_id = {
19379                        let mut state = shared.lock().expect("http2 shared state");
19380                        let session_id = next_http2_session_id(&mut state);
19381                        state
19382                            .sessions
19383                            .insert(session_id, ActiveHttp2Session { command_tx });
19384                        session_id
19385                    };
19386                    spawn_http2_server_session(
19387                        Arc::clone(&shared),
19388                        server_id,
19389                        session_id,
19390                        stream,
19391                        tls,
19392                        session_snapshot,
19393                        command_rx,
19394                    );
19395                }
19396                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
19397                    thread::sleep(HTTP2_POLL_DELAY);
19398                }
19399                Err(error) => {
19400                    push_http2_server_event(
19401                        &shared,
19402                        server_id,
19403                        Http2BridgeEvent {
19404                            kind: String::from("serverStreamError"),
19405                            id: server_id,
19406                            data: Some(http2_error_payload(error.to_string())),
19407                            ..Http2BridgeEvent::default()
19408                        },
19409                    );
19410                    thread::sleep(HTTP2_POLL_DELAY);
19411                }
19412            }
19413        }
19414    });
19415}
19416
19417fn send_http2_command(
19418    session: &ActiveHttp2Session,
19419    command: impl FnOnce(Sender<Result<Value, String>>) -> Http2SessionCommand,
19420) -> Result<Value, SidecarError> {
19421    let (respond_to, response_rx) = mpsc::channel();
19422    session.command_tx.send(command(respond_to)).map_err(|_| {
19423        SidecarError::InvalidState(String::from("HTTP/2 session command channel closed"))
19424    })?;
19425    response_rx
19426        .recv_timeout(Duration::from_secs(30))
19427        .map_err(|_| {
19428            SidecarError::Execution(String::from("timed out waiting for HTTP/2 session command"))
19429        })?
19430        .map_err(SidecarError::Execution)
19431}
19432
19433fn parse_http2_server_listen_payload(
19434    request: &JavascriptSyncRpcRequest,
19435) -> Result<JavascriptHttp2ServerListenRequest, SidecarError> {
19436    let payload_json =
19437        javascript_sync_rpc_arg_str(&request.args, 0, "net.http2_server_listen payload")?;
19438    serde_json::from_str(payload_json).map_err(|error| {
19439        SidecarError::InvalidState(format!(
19440            "net.http2_server_listen payload must be valid JSON: {error}"
19441        ))
19442    })
19443}
19444
19445fn parse_http2_connect_payload(
19446    request: &JavascriptSyncRpcRequest,
19447) -> Result<JavascriptHttp2SessionConnectRequest, SidecarError> {
19448    let payload_json =
19449        javascript_sync_rpc_arg_str(&request.args, 0, "net.http2_session_connect payload")?;
19450    serde_json::from_str(payload_json).map_err(|error| {
19451        SidecarError::InvalidState(format!(
19452            "net.http2_session_connect payload must be valid JSON: {error}"
19453        ))
19454    })
19455}
19456
19457fn http2_session_for_id(
19458    process: &ActiveProcess,
19459    session_id: u64,
19460) -> Result<ActiveHttp2Session, SidecarError> {
19461    let shared = process
19462        .http2
19463        .shared
19464        .lock()
19465        .map_err(|_| SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned")))?;
19466    shared
19467        .sessions
19468        .get(&session_id)
19469        .cloned()
19470        .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 session {session_id}")))
19471}
19472
19473fn http2_stream_for_id(
19474    process: &ActiveProcess,
19475    stream_id: u64,
19476) -> Result<ActiveHttp2Stream, SidecarError> {
19477    let shared = process
19478        .http2
19479        .shared
19480        .lock()
19481        .map_err(|_| SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned")))?;
19482    shared
19483        .streams
19484        .get(&stream_id)
19485        .cloned()
19486        .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 stream {stream_id}")))
19487}
19488
19489fn service_javascript_http2_sync_rpc<B>(
19490    request: JavascriptHttp2SyncRpcServiceRequest<'_, B>,
19491) -> Result<Value, SidecarError>
19492where
19493    B: NativeSidecarBridge + Send + 'static,
19494    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19495{
19496    let JavascriptHttp2SyncRpcServiceRequest {
19497        bridge,
19498        kernel,
19499        vm_id,
19500        dns,
19501        socket_paths,
19502        process,
19503        sync_request: request,
19504        resource_limits,
19505        network_counts,
19506    } = request;
19507    match request.method.as_str() {
19508        "net.http2_server_listen" => {
19509            check_network_resource_limit(
19510                resource_limits.max_sockets,
19511                network_counts.sockets,
19512                1,
19513                "socket",
19514            )?;
19515            let payload = parse_http2_server_listen_payload(request)?;
19516            let (family, bind_host, guest_host) =
19517                normalize_tcp_listen_host(payload.host.as_deref())?;
19518            let requested_port = payload.port.unwrap_or(0);
19519            bridge.require_network_access(
19520                vm_id,
19521                NetworkOperation::Listen,
19522                format_tcp_resource(bind_host, requested_port),
19523            )?;
19524            let port = allocate_guest_listen_port(
19525                requested_port,
19526                family,
19527                &socket_paths.used_tcp_guest_ports,
19528                socket_paths.listen_policy,
19529            )?;
19530            let mut listener =
19531                ActiveTcpListener::bind(bind_host, guest_host, port, payload.backlog)?;
19532            let guest_local_addr = listener.guest_local_addr();
19533            let closed = Arc::new(AtomicBool::new(false));
19534            {
19535                let mut state = process.http2.shared.lock().map_err(|_| {
19536                    SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19537                })?;
19538                state.servers.insert(
19539                    payload.server_id,
19540                    ActiveHttp2Server {
19541                        actual_local_addr: listener.local_addr(),
19542                        guest_local_addr,
19543                        secure: payload.secure,
19544                        tls: payload.tls.clone().map(|mut tls| {
19545                            tls.is_server = payload.secure;
19546                            if payload.secure && tls.alpn_protocols.is_none() {
19547                                tls.alpn_protocols = Some(vec![String::from("h2")]);
19548                            }
19549                            tls
19550                        }),
19551                        closed: Arc::clone(&closed),
19552                    },
19553                );
19554                state.server_events.entry(payload.server_id).or_default();
19555            }
19556            spawn_http2_server_accept_loop(
19557                Arc::clone(&process.http2.shared),
19558                payload.server_id,
19559                listener.listener.take().ok_or_else(|| {
19560                    SidecarError::InvalidState(String::from(
19561                        "HTTP/2 listener missing host TCP socket",
19562                    ))
19563                })?,
19564            );
19565            javascript_net_json_string(
19566                json!({
19567                    "address": {
19568                        "address": guest_local_addr.ip().to_string(),
19569                        "family": socket_addr_family(&guest_local_addr),
19570                        "port": guest_local_addr.port(),
19571                    }
19572                }),
19573                "net.http2_server_listen",
19574            )
19575        }
19576        "net.http2_server_poll" => {
19577            let server_id =
19578                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_poll server id")?;
19579            let wait_ms = javascript_sync_rpc_arg_u64_optional(
19580                &request.args,
19581                1,
19582                "net.http2_server_poll wait ms",
19583            )?
19584            .unwrap_or_default();
19585            match wait_for_http2_event(&process.http2.shared, server_id, true, wait_ms) {
19586                Some(event) => http2_event_value(&event),
19587                None => Ok(Value::Null),
19588            }
19589        }
19590        "net.http2_server_wait" => {
19591            let server_id =
19592                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_wait server id")?;
19593            dispatch_http2_wait_loop(process, server_id, true)
19594        }
19595        "net.http2_server_close" => {
19596            let server_id =
19597                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_close server id")?;
19598            let server = {
19599                let mut state = process.http2.shared.lock().map_err(|_| {
19600                    SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19601                })?;
19602                state.servers.remove(&server_id)
19603            }
19604            .ok_or_else(|| {
19605                SidecarError::InvalidState(format!("unknown HTTP/2 server {server_id}"))
19606            })?;
19607            server.closed.store(true, Ordering::SeqCst);
19608            push_http2_server_event(
19609                &process.http2.shared,
19610                server_id,
19611                Http2BridgeEvent {
19612                    kind: String::from("serverClose"),
19613                    id: server_id,
19614                    ..Http2BridgeEvent::default()
19615                },
19616            );
19617            Ok(Value::Null)
19618        }
19619        "net.http2_server_respond" => {
19620            let server_id = javascript_sync_rpc_arg_u64(
19621                &request.args,
19622                0,
19623                "net.http2_server_respond server id",
19624            )?;
19625            let request_id = javascript_sync_rpc_arg_u64(
19626                &request.args,
19627                1,
19628                "net.http2_server_respond request id",
19629            )?;
19630            let response_json =
19631                javascript_sync_rpc_arg_str(&request.args, 2, "net.http2_server_respond payload")?;
19632            ensure_vm_fetch_response_within_limit(
19633                response_json,
19634                "net.http2_server_respond",
19635                VM_FETCH_BUFFER_LIMIT_BYTES,
19636            )?;
19637            serde_json::from_str::<Value>(response_json).map_err(|error| {
19638                SidecarError::Execution(format!(
19639                    "net.http2_server_respond payload must be valid JSON: {error}"
19640                ))
19641            })?;
19642            let Some(pending) = process
19643                .pending_http_requests
19644                .get_mut(&(server_id, request_id))
19645            else {
19646                return Err(SidecarError::InvalidState(format!(
19647                    "unknown pending HTTP/2 request {request_id} for server {server_id}"
19648                )));
19649            };
19650            *pending = Some(response_json.to_owned());
19651            Ok(Value::Bool(true))
19652        }
19653        "net.http2_session_connect" => {
19654            check_network_resource_limit(
19655                resource_limits.max_sockets,
19656                network_counts.sockets,
19657                1,
19658                "socket",
19659            )?;
19660            check_network_resource_limit(
19661                resource_limits.max_connections,
19662                network_counts.connections,
19663                1,
19664                "connection",
19665            )?;
19666            let payload = parse_http2_connect_payload(request)?;
19667            let authority = payload.authority.clone().unwrap_or_else(|| {
19668                format!(
19669                    "{}://{}:{}",
19670                    payload.protocol.as_deref().unwrap_or("http"),
19671                    payload.host.as_deref().unwrap_or("localhost"),
19672                    payload.port.unwrap_or(80)
19673                )
19674            });
19675            let url = Url::parse(&authority).map_err(|error| {
19676                SidecarError::InvalidState(format!(
19677                    "invalid HTTP/2 authority {authority:?}: {error}"
19678                ))
19679            })?;
19680            let secure = url.scheme() == "https" || payload.protocol.as_deref() == Some("https:");
19681            let host = payload
19682                .host
19683                .as_deref()
19684                .or_else(|| url.host_str())
19685                .unwrap_or("localhost");
19686            let port = payload.port.or_else(|| url.port()).unwrap_or(80);
19687            bridge.require_network_access(
19688                vm_id,
19689                NetworkOperation::Http,
19690                format_tcp_resource(host, port),
19691            )?;
19692            let resolved = {
19693                let shared = process.http2.shared.lock().map_err(|_| {
19694                    SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19695                })?;
19696                shared
19697                    .servers
19698                    .values()
19699                    .find(|server| {
19700                        is_loopback_request_host(host) && server.guest_local_addr.port() == port
19701                    })
19702                    .map(|server| ResolvedTcpConnectAddr {
19703                        actual_addr: server.actual_local_addr,
19704                        guest_remote_addr: server.guest_local_addr,
19705                        use_kernel_loopback: false,
19706                    })
19707            };
19708            let resolved = match resolved {
19709                Some(resolved) => resolved,
19710                None => {
19711                    resolve_tcp_connect_addr(bridge, kernel, vm_id, dns, host, port, socket_paths)?
19712                }
19713            };
19714            let (command_tx, command_rx) = unbounded_channel();
19715            let snapshot = Arc::new(Mutex::new(Http2SessionSnapshot {
19716                encrypted: secure,
19717                alpn_protocol: Some(String::from(if secure { "h2" } else { "h2c" })),
19718                local_settings: http2_settings_from_value(&payload.settings),
19719                remote_settings: BTreeMap::new(),
19720                state: http2_runtime_snapshot(),
19721                socket: Http2SocketSnapshot {
19722                    encrypted: secure,
19723                    remote_address: Some(resolved.guest_remote_addr.ip().to_string()),
19724                    remote_port: Some(resolved.guest_remote_addr.port()),
19725                    remote_family: Some(
19726                        socket_addr_family(&resolved.guest_remote_addr).to_string(),
19727                    ),
19728                    servername: if secure {
19729                        payload
19730                            .tls
19731                            .as_ref()
19732                            .and_then(|tls| tls.servername.clone())
19733                            .or_else(|| Some(host.to_string()))
19734                    } else {
19735                        None
19736                    },
19737                    alpn_protocol: Some(String::from(if secure { "h2" } else { "h2c" })),
19738                    ..Http2SocketSnapshot::default()
19739                },
19740                ..Http2SessionSnapshot::default()
19741            }));
19742            let session_id = {
19743                let mut state = process.http2.shared.lock().map_err(|_| {
19744                    SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19745                })?;
19746                let session_id = next_http2_session_id(&mut state);
19747                state
19748                    .sessions
19749                    .insert(session_id, ActiveHttp2Session { command_tx });
19750                state.session_events.entry(session_id).or_default();
19751                session_id
19752            };
19753            spawn_http2_client_session(
19754                Arc::clone(&process.http2.shared),
19755                session_id,
19756                resolved.actual_addr,
19757                if secure {
19758                    Some(payload.tls.unwrap_or(JavascriptTlsBridgeOptions {
19759                        is_server: false,
19760                        servername: Some(host.to_string()),
19761                        alpn_protocols: Some(vec![String::from("h2")]),
19762                        ..JavascriptTlsBridgeOptions::default()
19763                    }))
19764                } else {
19765                    None
19766                },
19767                Arc::clone(&snapshot),
19768                command_rx,
19769            );
19770            let snapshot_json =
19771                http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())?;
19772            javascript_net_json_string(
19773                json!({
19774                    "sessionId": session_id,
19775                    "state": snapshot_json,
19776                }),
19777                "net.http2_session_connect",
19778            )
19779        }
19780        "net.http2_session_request" => {
19781            let session_id = javascript_sync_rpc_arg_u64(
19782                &request.args,
19783                0,
19784                "net.http2_session_request session id",
19785            )?;
19786            let headers_json =
19787                javascript_sync_rpc_arg_str(&request.args, 1, "net.http2_session_request headers")?;
19788            let options_json =
19789                javascript_sync_rpc_arg_str(&request.args, 2, "net.http2_session_request options")?;
19790            let session = http2_session_for_id(process, session_id)?;
19791            send_http2_command(&session, |respond_to| Http2SessionCommand::Request {
19792                headers_json: headers_json.to_owned(),
19793                options_json: options_json.to_owned(),
19794                respond_to,
19795            })
19796        }
19797        "net.http2_session_settings" => {
19798            let session_id = javascript_sync_rpc_arg_u64(
19799                &request.args,
19800                0,
19801                "net.http2_session_settings session id",
19802            )?;
19803            let settings_json = javascript_sync_rpc_arg_str(
19804                &request.args,
19805                1,
19806                "net.http2_session_settings settings",
19807            )?;
19808            let session = http2_session_for_id(process, session_id)?;
19809            send_http2_command(&session, |respond_to| Http2SessionCommand::Settings {
19810                settings_json: settings_json.to_owned(),
19811                respond_to,
19812            })
19813        }
19814        "net.http2_session_set_local_window_size" => {
19815            let session_id = javascript_sync_rpc_arg_u64(
19816                &request.args,
19817                0,
19818                "net.http2_session_set_local_window_size session id",
19819            )?;
19820            let window_size = javascript_sync_rpc_arg_u64(
19821                &request.args,
19822                1,
19823                "net.http2_session_set_local_window_size window size",
19824            )?;
19825            let session = http2_session_for_id(process, session_id)?;
19826            send_http2_command(&session, |respond_to| {
19827                Http2SessionCommand::SetLocalWindowSize {
19828                    size: window_size as u32,
19829                    respond_to,
19830                }
19831            })
19832        }
19833        "net.http2_session_goaway" => {
19834            let session_id = javascript_sync_rpc_arg_u64(
19835                &request.args,
19836                0,
19837                "net.http2_session_goaway session id",
19838            )?;
19839            let error_code = javascript_sync_rpc_arg_u64(
19840                &request.args,
19841                1,
19842                "net.http2_session_goaway error code",
19843            )?;
19844            let last_stream_id = javascript_sync_rpc_arg_u64(
19845                &request.args,
19846                2,
19847                "net.http2_session_goaway last stream id",
19848            )?;
19849            let opaque_data = request
19850                .args
19851                .get(3)
19852                .and_then(Value::as_str)
19853                .map(|value| {
19854                    base64::engine::general_purpose::STANDARD
19855                        .decode(value)
19856                        .map_err(|error| {
19857                            SidecarError::InvalidState(format!("invalid GOAWAY payload: {error}"))
19858                        })
19859                })
19860                .transpose()?;
19861            let session = http2_session_for_id(process, session_id)?;
19862            send_http2_command(&session, |respond_to| Http2SessionCommand::Goaway {
19863                error_code: error_code as u32,
19864                last_stream_id: last_stream_id as u32,
19865                opaque_data,
19866                respond_to,
19867            })
19868        }
19869        "net.http2_session_close" | "net.http2_session_destroy" => {
19870            let session_id = javascript_sync_rpc_arg_u64(
19871                &request.args,
19872                0,
19873                "net.http2_session_close session id",
19874            )?;
19875            let session = http2_session_for_id(process, session_id)?;
19876            send_http2_command(&session, |respond_to| Http2SessionCommand::Close {
19877                abrupt: request.method == "net.http2_session_destroy",
19878                respond_to,
19879            })
19880        }
19881        "net.http2_session_poll" => {
19882            let session_id =
19883                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_session_poll session id")?;
19884            let wait_ms = javascript_sync_rpc_arg_u64_optional(
19885                &request.args,
19886                1,
19887                "net.http2_session_poll wait ms",
19888            )?
19889            .unwrap_or_default();
19890            match wait_for_http2_event(&process.http2.shared, session_id, false, wait_ms) {
19891                Some(event) => http2_event_value(&event),
19892                None => Ok(Value::Null),
19893            }
19894        }
19895        "net.http2_session_wait" => {
19896            let session_id =
19897                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_session_wait session id")?;
19898            dispatch_http2_wait_loop(process, session_id, false)
19899        }
19900        "net.http2_stream_respond" => {
19901            let stream_id = javascript_sync_rpc_arg_u64(
19902                &request.args,
19903                0,
19904                "net.http2_stream_respond stream id",
19905            )?;
19906            let headers_json =
19907                javascript_sync_rpc_arg_str(&request.args, 1, "net.http2_stream_respond headers")?;
19908            let stream = http2_stream_for_id(process, stream_id)?;
19909            let session = http2_session_for_id(process, stream.session_id)?;
19910            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamRespond {
19911                stream_id,
19912                headers_json: headers_json.to_owned(),
19913                respond_to,
19914            })
19915        }
19916        "net.http2_stream_push_stream" => {
19917            let stream_id = javascript_sync_rpc_arg_u64(
19918                &request.args,
19919                0,
19920                "net.http2_stream_push_stream stream id",
19921            )?;
19922            let headers_json = javascript_sync_rpc_arg_str(
19923                &request.args,
19924                1,
19925                "net.http2_stream_push_stream headers",
19926            )?;
19927            let _options_json = javascript_sync_rpc_arg_str(
19928                &request.args,
19929                2,
19930                "net.http2_stream_push_stream options",
19931            )?;
19932            let stream = http2_stream_for_id(process, stream_id)?;
19933            let session = http2_session_for_id(process, stream.session_id)?;
19934            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamPush {
19935                stream_id,
19936                headers_json: headers_json.to_owned(),
19937                respond_to,
19938            })
19939        }
19940        "net.http2_stream_write" => {
19941            let stream_id =
19942                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_write stream id")?;
19943            let chunk =
19944                javascript_sync_rpc_base64_arg(&request.args, 1, "net.http2_stream_write data")?;
19945            let stream = http2_stream_for_id(process, stream_id)?;
19946            let session = http2_session_for_id(process, stream.session_id)?;
19947            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamWrite {
19948                stream_id,
19949                chunk,
19950                end_stream: false,
19951                respond_to,
19952            })
19953        }
19954        "net.http2_stream_end" => {
19955            let stream_id =
19956                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_end stream id")?;
19957            let chunk = request
19958                .args
19959                .get(1)
19960                .and_then(Value::as_str)
19961                .map(|value| {
19962                    base64::engine::general_purpose::STANDARD
19963                        .decode(value)
19964                        .map_err(|error| {
19965                            SidecarError::InvalidState(format!(
19966                                "invalid HTTP/2 stream payload: {error}"
19967                            ))
19968                        })
19969                })
19970                .transpose()?
19971                .unwrap_or_default();
19972            let stream = http2_stream_for_id(process, stream_id)?;
19973            let session = http2_session_for_id(process, stream.session_id)?;
19974            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamWrite {
19975                stream_id,
19976                chunk,
19977                end_stream: true,
19978                respond_to,
19979            })
19980        }
19981        "net.http2_stream_close" => {
19982            let stream_id =
19983                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_close stream id")?;
19984            let code = javascript_sync_rpc_arg_u64_optional(
19985                &request.args,
19986                1,
19987                "net.http2_stream_close error code",
19988            )?
19989            .map(|value| value as u32);
19990            let stream = http2_stream_for_id(process, stream_id)?;
19991            let session = http2_session_for_id(process, stream.session_id)?;
19992            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamClose {
19993                stream_id,
19994                error_code: code,
19995                respond_to,
19996            })
19997        }
19998        "net.http2_stream_pause" => {
19999            let stream_id =
20000                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_pause stream id")?;
20001            let stream = http2_stream_for_id(process, stream_id)?;
20002            stream.paused.store(true, Ordering::SeqCst);
20003            Ok(Value::Null)
20004        }
20005        "net.http2_stream_resume" => {
20006            let stream_id =
20007                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_resume stream id")?;
20008            let stream = http2_stream_for_id(process, stream_id)?;
20009            stream.paused.store(false, Ordering::SeqCst);
20010            Ok(Value::Null)
20011        }
20012        "net.http2_stream_respond_with_file" => {
20013            let stream_id = javascript_sync_rpc_arg_u64(
20014                &request.args,
20015                0,
20016                "net.http2_stream_respond_with_file stream id",
20017            )?;
20018            let path = javascript_sync_rpc_arg_str(
20019                &request.args,
20020                1,
20021                "net.http2_stream_respond_with_file path",
20022            )?;
20023            let headers_json = javascript_sync_rpc_arg_str(
20024                &request.args,
20025                2,
20026                "net.http2_stream_respond_with_file headers",
20027            )?;
20028            let options_json = javascript_sync_rpc_arg_str(
20029                &request.args,
20030                3,
20031                "net.http2_stream_respond_with_file options",
20032            )?;
20033            let stream = http2_stream_for_id(process, stream_id)?;
20034            let session = http2_session_for_id(process, stream.session_id)?;
20035            let guest_path = resolve_http2_file_response_guest_path(process, path);
20036            let body = kernel.read_file(&guest_path).map_err(kernel_error)?;
20037            send_http2_command(&session, |respond_to| {
20038                Http2SessionCommand::StreamRespondWithFile {
20039                    stream_id,
20040                    body,
20041                    headers_json: headers_json.to_owned(),
20042                    options_json: options_json.to_owned(),
20043                    respond_to,
20044                }
20045            })
20046        }
20047        other => Err(SidecarError::InvalidState(format!(
20048            "unsupported JavaScript HTTP/2 sync RPC method {other}"
20049        ))),
20050    }
20051}
20052
20053const JAVASCRIPT_NET_POLL_MAX_WAIT: Duration = Duration::from_millis(50);
20054const EXITED_PROCESS_SNAPSHOT_RETENTION: Duration = Duration::from_secs(2);
20055
20056fn resolve_http2_file_response_guest_path(process: &ActiveProcess, path: &str) -> String {
20057    if Path::new(path).is_absolute() {
20058        normalize_path(path)
20059    } else {
20060        normalize_path(&format!("{}/{}", process.guest_cwd, path))
20061    }
20062}
20063
20064pub(crate) fn clamp_javascript_net_poll_wait(wait_ms: u64) -> Duration {
20065    // WASM net.poll runs on the sidecar's sync-RPC main thread. Guest-controlled waits
20066    // must stay bounded so one VM cannot stall dispose/shutdown or unrelated VM work.
20067    if wait_ms == 0 {
20068        Duration::ZERO
20069    } else {
20070        Duration::from_millis(wait_ms).min(JAVASCRIPT_NET_POLL_MAX_WAIT)
20071    }
20072}
20073
20074pub(crate) fn service_javascript_net_sync_rpc<B>(
20075    request: JavascriptNetSyncRpcServiceRequest<'_, B>,
20076) -> Result<Value, SidecarError>
20077where
20078    B: NativeSidecarBridge + Send + 'static,
20079    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
20080{
20081    let JavascriptNetSyncRpcServiceRequest {
20082        bridge,
20083        vm_id,
20084        dns,
20085        socket_paths,
20086        kernel,
20087        process,
20088        sync_request: request,
20089        resource_limits,
20090        network_counts,
20091    } = request;
20092    match request.method.as_str() {
20093        "net.http_listen" => {
20094            check_network_resource_limit(
20095                resource_limits.max_sockets,
20096                network_counts.sockets,
20097                1,
20098                "socket",
20099            )?;
20100            let payload_json =
20101                javascript_sync_rpc_arg_str(&request.args, 0, "net.http_listen payload")?;
20102            let payload: JavascriptHttpListenRequest =
20103                serde_json::from_str(payload_json).map_err(|error| {
20104                    SidecarError::InvalidState(format!(
20105                        "net.http_listen payload must be valid JSON: {error}"
20106                    ))
20107                })?;
20108            let (family, bind_host, guest_host) =
20109                normalize_tcp_listen_host(payload.hostname.as_deref())?;
20110            let requested_port = payload.port.unwrap_or(0);
20111            bridge.require_network_access(
20112                vm_id,
20113                NetworkOperation::Listen,
20114                format_tcp_resource(bind_host, requested_port),
20115            )?;
20116            let port = allocate_guest_listen_port(
20117                requested_port,
20118                family,
20119                &socket_paths.used_tcp_guest_ports,
20120                socket_paths.listen_policy,
20121            )?;
20122            let mut listener = ActiveTcpListener::bind(
20123                bind_host,
20124                guest_host,
20125                port,
20126                Some(DEFAULT_JAVASCRIPT_NET_BACKLOG),
20127            )?;
20128            let guest_local_addr = listener.guest_local_addr();
20129            process.http_servers.insert(
20130                payload.server_id,
20131                ActiveHttpServer {
20132                    listener: listener.listener.take().ok_or_else(|| {
20133                        SidecarError::InvalidState(String::from(
20134                            "HTTP listener missing host TCP socket",
20135                        ))
20136                    })?,
20137                    guest_local_addr,
20138                    next_request_id: 0,
20139                },
20140            );
20141            serde_json::to_string(&json!({
20142                "address": {
20143                    "address": guest_local_addr.ip().to_string(),
20144                    "family": socket_addr_family(&guest_local_addr),
20145                    "port": guest_local_addr.port(),
20146                }
20147            }))
20148            .map(Value::String)
20149            .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
20150        }
20151        "net.http_close" => {
20152            let server_id =
20153                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_close server id")?;
20154            let server = process.http_servers.remove(&server_id).ok_or_else(|| {
20155                SidecarError::InvalidState(format!("unknown HTTP server {server_id}"))
20156            })?;
20157            drop(server.listener);
20158            process
20159                .pending_http_requests
20160                .retain(|(pending_server_id, _), _| *pending_server_id != server_id);
20161            Ok(Value::Null)
20162        }
20163        "net.http_wait" => {
20164            let server_id =
20165                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_wait server id")?;
20166            dispatch_http_wait_loop(process, server_id)
20167        }
20168        "net.http_respond" => {
20169            let server_id =
20170                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_respond server id")?;
20171            let request_id =
20172                javascript_sync_rpc_arg_u64(&request.args, 1, "net.http_respond request id")?;
20173            let response_json =
20174                javascript_sync_rpc_arg_str(&request.args, 2, "net.http_respond payload")?;
20175            ensure_vm_fetch_response_within_limit(
20176                response_json,
20177                "net.http_respond",
20178                VM_FETCH_BUFFER_LIMIT_BYTES,
20179            )?;
20180            serde_json::from_str::<Value>(response_json).map_err(|error| {
20181                SidecarError::Execution(format!(
20182                    "net.http_respond payload must be valid JSON: {error}"
20183                ))
20184            })?;
20185            let Some(pending) = process
20186                .pending_http_requests
20187                .get_mut(&(server_id, request_id))
20188            else {
20189                return Err(SidecarError::InvalidState(format!(
20190                    "unknown pending HTTP request {request_id} for server {server_id}"
20191                )));
20192            };
20193            *pending = Some(response_json.to_owned());
20194            Ok(Value::Null)
20195        }
20196        "net.reserve_tcp_port" => {
20197            let payload = request
20198                .args
20199                .first()
20200                .cloned()
20201                .ok_or_else(|| {
20202                    SidecarError::InvalidState(String::from(
20203                        "net.reserve_tcp_port requires a request payload",
20204                    ))
20205                })
20206                .and_then(|value| {
20207                    serde_json::from_value::<JavascriptNetReserveTcpPortRequest>(value).map_err(
20208                        |error| {
20209                            SidecarError::InvalidState(format!(
20210                                "invalid net.reserve_tcp_port payload: {error}"
20211                            ))
20212                        },
20213                    )
20214                })?;
20215            let (family, _bind_host, guest_host) =
20216                normalize_tcp_listen_host(payload.host.as_deref())?;
20217            let requested_port = payload.port.unwrap_or(0);
20218            let port = allocate_guest_listen_port(
20219                requested_port,
20220                family,
20221                &socket_paths.used_tcp_guest_ports,
20222                socket_paths.listen_policy,
20223            )?;
20224            let reservation_id = process.allocate_tcp_port_reservation_id();
20225            process
20226                .tcp_port_reservations
20227                .insert(reservation_id.clone(), (family, port));
20228            Ok(json!({
20229                "reservationId": reservation_id,
20230                "localAddress": guest_host,
20231                "localPort": port,
20232                "family": match family {
20233                    JavascriptSocketFamily::Ipv4 => "IPv4",
20234                    JavascriptSocketFamily::Ipv6 => "IPv6",
20235                },
20236            }))
20237        }
20238        "net.release_tcp_port" => {
20239            let reservation_id =
20240                javascript_sync_rpc_arg_str(&request.args, 0, "net.release_tcp_port reservation")?;
20241            process.tcp_port_reservations.remove(reservation_id);
20242            Ok(Value::Null)
20243        }
20244        "net.connect" => {
20245            check_network_resource_limit(
20246                resource_limits.max_sockets,
20247                network_counts.sockets,
20248                1,
20249                "socket",
20250            )?;
20251            check_network_resource_limit(
20252                resource_limits.max_connections,
20253                network_counts.connections,
20254                1,
20255                "connection",
20256            )?;
20257            let payload = request
20258                .args
20259                .first()
20260                .cloned()
20261                .ok_or_else(|| {
20262                    SidecarError::InvalidState(String::from(
20263                        "net.connect requires a request payload",
20264                    ))
20265                })
20266                .and_then(|value| {
20267                    serde_json::from_value::<JavascriptNetConnectRequest>(value).map_err(|error| {
20268                        SidecarError::InvalidState(format!("invalid net.connect payload: {error}"))
20269                    })
20270                })?;
20271            if let Some(path) = payload.path.as_deref() {
20272                let guest_path = normalize_path(path);
20273                let host_path = resolve_guest_socket_host_path(socket_paths, &guest_path);
20274                let socket = ActiveUnixSocket::connect(&host_path, &guest_path)?;
20275                let socket_id = process.allocate_unix_socket_id();
20276                process.unix_sockets.insert(socket_id.clone(), socket);
20277                Ok(json!({
20278                    "socketId": socket_id,
20279                    "remotePath": guest_path,
20280                }))
20281            } else {
20282                let port = payload.port.ok_or_else(|| {
20283                    SidecarError::InvalidState(String::from(
20284                        "net.connect requires either a path or port",
20285                    ))
20286                })?;
20287                let host = payload.host.as_deref().unwrap_or("localhost");
20288                let local_reservation = payload.local_reservation.as_deref().and_then(|id| {
20289                    process
20290                        .tcp_port_reservations
20291                        .remove(id)
20292                        .map(|reservation| (id.to_owned(), reservation))
20293                });
20294                bridge.require_network_access(
20295                    vm_id,
20296                    NetworkOperation::Http,
20297                    format_tcp_resource(host, port),
20298                )?;
20299                if is_loopback_socket_host(host) {
20300                    let families = [JavascriptSocketFamily::Ipv4, JavascriptSocketFamily::Ipv6];
20301                    if let Some((family, target)) = families.iter().find_map(|family| {
20302                        socket_paths
20303                            .http_loopback_target(*family, port)
20304                            .map(|target| (*family, target))
20305                    }) {
20306                        if let Some((reservation_id, reservation)) = local_reservation {
20307                            process
20308                                .tcp_port_reservations
20309                                .insert(reservation_id, reservation);
20310                        }
20311                        let remote_address = match family {
20312                            JavascriptSocketFamily::Ipv4 => "127.0.0.1",
20313                            JavascriptSocketFamily::Ipv6 => "::1",
20314                        };
20315                        return Ok(json!({
20316                            "loopbackHttpTarget": {
20317                                "processId": target.process_id.clone(),
20318                                "serverId": target.server_id,
20319                                "host": remote_address,
20320                                "port": port,
20321                            },
20322                            "localAddress": match family {
20323                                JavascriptSocketFamily::Ipv4 => "127.0.0.1",
20324                                JavascriptSocketFamily::Ipv6 => "::1",
20325                            },
20326                            "localPort": payload.local_port.unwrap_or(0),
20327                            "remoteAddress": remote_address,
20328                            "remotePort": port,
20329                            "remoteFamily": match family {
20330                                JavascriptSocketFamily::Ipv4 => "IPv4",
20331                                JavascriptSocketFamily::Ipv6 => "IPv6",
20332                            },
20333                        }));
20334                    }
20335                }
20336                let connect_result = ActiveTcpSocket::connect(ActiveTcpConnectRequest {
20337                    bridge,
20338                    kernel,
20339                    kernel_pid: process.kernel_pid,
20340                    vm_id,
20341                    dns,
20342                    host,
20343                    port,
20344                    local_address: payload.local_address.as_deref(),
20345                    local_port: payload.local_port,
20346                    local_reservation: local_reservation
20347                        .as_ref()
20348                        .map(|(_, reservation)| *reservation),
20349                    context: socket_paths,
20350                });
20351                if let Err(error) = connect_result {
20352                    if let Some((reservation_id, reservation)) = local_reservation {
20353                        process
20354                            .tcp_port_reservations
20355                            .insert(reservation_id, reservation);
20356                    }
20357                    return Err(error);
20358                }
20359                let socket = connect_result?;
20360                let socket_id = process.allocate_tcp_socket_id();
20361                let local_addr = socket.guest_local_addr;
20362                let remote_addr = socket.guest_remote_addr;
20363                process.tcp_sockets.insert(socket_id.clone(), socket);
20364                Ok(json!({
20365                    "socketId": socket_id,
20366                    "localAddress": local_addr.ip().to_string(),
20367                    "localPort": local_addr.port(),
20368                    "remoteAddress": remote_addr.ip().to_string(),
20369                    "remotePort": remote_addr.port(),
20370                    "remoteFamily": socket_addr_family(&remote_addr),
20371                }))
20372            }
20373        }
20374        "net.listen" => {
20375            check_network_resource_limit(
20376                resource_limits.max_sockets,
20377                network_counts.sockets,
20378                1,
20379                "socket",
20380            )?;
20381            let payload = request
20382                .args
20383                .first()
20384                .cloned()
20385                .ok_or_else(|| {
20386                    SidecarError::InvalidState(String::from(
20387                        "net.listen requires a request payload",
20388                    ))
20389                })
20390                .and_then(|value| match value {
20391                    Value::String(json) => {
20392                        serde_json::from_str::<JavascriptNetListenRequest>(&json).map_err(|error| {
20393                            SidecarError::InvalidState(format!(
20394                                "invalid net.listen payload: {error}"
20395                            ))
20396                        })
20397                    }
20398                    other => serde_json::from_value::<JavascriptNetListenRequest>(other).map_err(
20399                        |error| {
20400                            SidecarError::InvalidState(format!(
20401                                "invalid net.listen payload: {error}"
20402                            ))
20403                        },
20404                    ),
20405                })?;
20406            if let Some(path) = payload.path.as_deref() {
20407                let guest_path = normalize_path(path);
20408                if kernel.exists(&guest_path).map_err(kernel_error)? {
20409                    return Err(sidecar_net_error(std::io::Error::from_raw_os_error(
20410                        libc::EADDRINUSE,
20411                    )));
20412                }
20413
20414                let host_path = resolve_guest_socket_host_path(socket_paths, &guest_path);
20415                let on_host_mount =
20416                    host_mount_path_for_guest_path_from_mounts(&socket_paths.mounts, &guest_path)
20417                        .is_some();
20418                let listener = ActiveUnixListener::bind(&host_path, &guest_path, payload.backlog)?;
20419                if !on_host_mount {
20420                    ensure_kernel_parent_directories(kernel, &guest_path)?;
20421                    kernel
20422                        .write_file(&guest_path, Vec::new())
20423                        .map_err(kernel_error)?;
20424                }
20425                let listener_id = process.allocate_unix_listener_id();
20426                process.unix_listeners.insert(listener_id.clone(), listener);
20427                Ok(json!({
20428                    "serverId": listener_id,
20429                    "path": guest_path,
20430                }))
20431            } else {
20432                let (family, bind_host, guest_host) =
20433                    normalize_tcp_listen_host(payload.host.as_deref())?;
20434                let requested_port = payload.port.unwrap_or(0);
20435                bridge.require_network_access(
20436                    vm_id,
20437                    NetworkOperation::Listen,
20438                    format_tcp_resource(bind_host, requested_port),
20439                )?;
20440                let local_reservation = payload.local_reservation.as_deref().and_then(|id| {
20441                    process
20442                        .tcp_port_reservations
20443                        .remove(id)
20444                        .map(|reservation| (id.to_owned(), reservation))
20445                });
20446                let port = if requested_port != 0
20447                    && local_reservation
20448                        .as_ref()
20449                        .map(|(_, reservation)| *reservation)
20450                        == Some((family, requested_port))
20451                {
20452                    requested_port
20453                } else {
20454                    allocate_guest_listen_port(
20455                        requested_port,
20456                        family,
20457                        &socket_paths.used_tcp_guest_ports,
20458                        socket_paths.listen_policy,
20459                    )?
20460                };
20461                let listener_result = ActiveTcpListener::bind_kernel(
20462                    kernel,
20463                    process.kernel_pid,
20464                    guest_host,
20465                    port,
20466                    payload.backlog,
20467                );
20468                if let Err(error) = listener_result {
20469                    if let Some((reservation_id, reservation)) = local_reservation {
20470                        process
20471                            .tcp_port_reservations
20472                            .insert(reservation_id, reservation);
20473                    }
20474                    return Err(error);
20475                }
20476                let listener = listener_result?;
20477                let listener_id = process.allocate_tcp_listener_id();
20478                let local_addr = listener.guest_local_addr();
20479                process.tcp_listeners.insert(listener_id.clone(), listener);
20480                Ok(json!({
20481                    "serverId": listener_id,
20482                    "localAddress": local_addr.ip().to_string(),
20483                    "localPort": local_addr.port(),
20484                    "family": socket_addr_family(&local_addr),
20485                }))
20486            }
20487        }
20488        "net.poll" => {
20489            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.poll socket id")?;
20490            let wait_ms =
20491                javascript_sync_rpc_arg_u64_optional(&request.args, 1, "net.poll wait ms")?
20492                    .unwrap_or_default();
20493            let wait = clamp_javascript_net_poll_wait(wait_ms);
20494            let event = if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20495                socket.poll(kernel, process.kernel_pid, wait)?
20496            } else if let Some(socket) = process.unix_sockets.get_mut(socket_id) {
20497                socket.poll(wait)?
20498            } else {
20499                return Err(SidecarError::InvalidState(format!(
20500                    "unknown net socket {socket_id}"
20501                )));
20502            };
20503
20504            match event {
20505                Some(JavascriptTcpSocketEvent::Data(chunk)) => Ok(json!({
20506                    "type": "data",
20507                    "data": javascript_sync_rpc_bytes_value(&chunk),
20508                })),
20509                Some(JavascriptTcpSocketEvent::End) => Ok(json!({
20510                    "type": "end",
20511                })),
20512                Some(JavascriptTcpSocketEvent::Error { code, message }) => Ok(json!({
20513                    "type": "error",
20514                    "code": code,
20515                    "message": message,
20516                })),
20517                Some(JavascriptTcpSocketEvent::Close { had_error }) => {
20518                    if let Some(socket) = process.tcp_sockets.remove(socket_id) {
20519                        if let Some(listener_id) = socket.listener_id.as_deref() {
20520                            if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20521                                listener.release_connection(socket_id);
20522                            }
20523                        }
20524                    } else if let Some(socket) = process.unix_sockets.remove(socket_id) {
20525                        if let Some(listener_id) = socket.listener_id.as_deref() {
20526                            if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20527                                listener.release_connection(socket_id);
20528                            }
20529                        }
20530                    }
20531                    Ok(json!({
20532                        "type": "close",
20533                        "hadError": had_error,
20534                    }))
20535                }
20536                None => Ok(Value::Null),
20537            }
20538        }
20539        "net.socket_wait_connect" => {
20540            let socket_id =
20541                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_wait_connect socket id")?;
20542            if let Some(socket) = process.tcp_sockets.get(socket_id) {
20543                javascript_net_json_string(socket.socket_info(), "net.socket_wait_connect")
20544            } else {
20545                let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
20546                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20547                })?;
20548                javascript_net_json_string(socket.socket_info(), "net.socket_wait_connect")
20549            }
20550        }
20551        "net.socket_read" => {
20552            let socket_id =
20553                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_read socket id")?;
20554            if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20555                javascript_net_read_value(socket.poll(
20556                    kernel,
20557                    process.kernel_pid,
20558                    Duration::ZERO,
20559                )?)
20560            } else {
20561                let socket = process.unix_sockets.get_mut(socket_id).ok_or_else(|| {
20562                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20563                })?;
20564                javascript_net_read_value(socket.poll(Duration::ZERO)?)
20565            }
20566        }
20567        "net.socket_set_no_delay" => {
20568            let socket_id =
20569                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_set_no_delay socket id")?;
20570            let enable =
20571                javascript_sync_rpc_arg_bool(&request.args, 1, "net.socket_set_no_delay enabled")?;
20572            if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20573                socket.set_no_delay(enable)?;
20574            } else if !process.unix_sockets.contains_key(socket_id) {
20575                return Err(SidecarError::InvalidState(format!(
20576                    "unknown net socket {socket_id}"
20577                )));
20578            }
20579            Ok(Value::Null)
20580        }
20581        "net.socket_set_keep_alive" => {
20582            let socket_id = javascript_sync_rpc_arg_str(
20583                &request.args,
20584                0,
20585                "net.socket_set_keep_alive socket id",
20586            )?;
20587            let enable = javascript_sync_rpc_arg_bool(
20588                &request.args,
20589                1,
20590                "net.socket_set_keep_alive enabled",
20591            )?;
20592            let initial_delay_secs = javascript_sync_rpc_arg_u64_optional(
20593                &request.args,
20594                2,
20595                "net.socket_set_keep_alive initial delay seconds",
20596            )?;
20597            if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20598                socket.set_keep_alive(enable, initial_delay_secs)?;
20599            } else if !process.unix_sockets.contains_key(socket_id) {
20600                return Err(SidecarError::InvalidState(format!(
20601                    "unknown net socket {socket_id}"
20602                )));
20603            }
20604            Ok(Value::Null)
20605        }
20606        "net.socket_upgrade_tls" => {
20607            let socket_id =
20608                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_upgrade_tls socket id")?;
20609            let options_json =
20610                javascript_sync_rpc_arg_str(&request.args, 1, "net.socket_upgrade_tls options")?;
20611            let options: JavascriptTlsBridgeOptions =
20612                serde_json::from_str(options_json).map_err(|error| {
20613                    SidecarError::InvalidState(format!(
20614                        "net.socket_upgrade_tls options must be valid JSON: {error}"
20615                    ))
20616                })?;
20617            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20618                SidecarError::InvalidState(format!(
20619                    "unknown TCP socket {socket_id} for TLS upgrade"
20620                ))
20621            })?;
20622            socket.upgrade_tls(vm_id, kernel, options)?;
20623            Ok(Value::Null)
20624        }
20625        "net.socket_get_tls_client_hello" => {
20626            let socket_id = javascript_sync_rpc_arg_str(
20627                &request.args,
20628                0,
20629                "net.socket_get_tls_client_hello socket id",
20630            )?;
20631            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20632                SidecarError::InvalidState(format!(
20633                    "unknown TCP socket {socket_id} for TLS client hello query"
20634                ))
20635            })?;
20636            socket.tls_client_hello_json(vm_id, kernel)
20637        }
20638        "net.socket_tls_query" => {
20639            let socket_id =
20640                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_tls_query socket id")?;
20641            let query =
20642                javascript_sync_rpc_arg_str(&request.args, 1, "net.socket_tls_query query")?;
20643            let detailed = request
20644                .args
20645                .get(2)
20646                .and_then(Value::as_bool)
20647                .unwrap_or(false);
20648            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20649                SidecarError::InvalidState(format!("unknown TCP socket {socket_id} for TLS query"))
20650            })?;
20651            socket.tls_query(query, detailed)
20652        }
20653        "net.server_poll" => {
20654            let listener_id =
20655                javascript_sync_rpc_arg_str(&request.args, 0, "net.server_poll listener id")?;
20656            let wait_ms =
20657                javascript_sync_rpc_arg_u64_optional(&request.args, 1, "net.server_poll wait ms")?
20658                    .unwrap_or_default();
20659            let tcp_event = if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20660                Some(listener.poll(kernel, process.kernel_pid, Duration::from_millis(wait_ms))?)
20661            } else {
20662                None
20663            };
20664
20665            if let Some(event) = tcp_event {
20666                return match event {
20667                    Some(JavascriptTcpListenerEvent::Connection(pending)) => {
20668                        let PendingTcpSocket {
20669                            stream,
20670                            kernel_socket_id,
20671                            preallocated,
20672                            guest_local_addr,
20673                            guest_remote_addr,
20674                        } = pending;
20675                        if !preallocated {
20676                            if let Err(error) = check_network_resource_limit(
20677                                resource_limits.max_sockets,
20678                                network_counts.sockets,
20679                                1,
20680                                "socket",
20681                            )
20682                            .and_then(|()| {
20683                                check_network_resource_limit(
20684                                    resource_limits.max_connections,
20685                                    network_counts.connections,
20686                                    1,
20687                                    "connection",
20688                                )
20689                            }) {
20690                                if let Some(stream) = stream {
20691                                    let _ = stream.shutdown(Shutdown::Both);
20692                                }
20693                                return Ok(json!({
20694                                    "type": "error",
20695                                    "code": "EAGAIN",
20696                                    "message": error.to_string(),
20697                                }));
20698                            }
20699                        }
20700                        let socket = if let Some(stream) = stream {
20701                            ActiveTcpSocket::from_stream(
20702                                stream,
20703                                Some(listener_id.to_string()),
20704                                guest_local_addr,
20705                                guest_remote_addr,
20706                            )?
20707                        } else {
20708                            ActiveTcpSocket::from_kernel(
20709                                kernel_socket_id.ok_or_else(|| {
20710                                    SidecarError::InvalidState(String::from(
20711                                        "kernel TCP accept missing socket id",
20712                                    ))
20713                                })?,
20714                                Some(listener_id.to_string()),
20715                                guest_local_addr,
20716                                guest_remote_addr,
20717                            )
20718                        };
20719                        let socket_id = process.allocate_tcp_socket_id();
20720                        if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20721                            listener.register_connection(&socket_id);
20722                        }
20723                        process.tcp_sockets.insert(socket_id.clone(), socket);
20724                        Ok(json!({
20725                            "type": "connection",
20726                            "socketId": socket_id,
20727                            "localAddress": guest_local_addr.ip().to_string(),
20728                            "localPort": guest_local_addr.port(),
20729                            "remoteAddress": guest_remote_addr.ip().to_string(),
20730                            "remotePort": guest_remote_addr.port(),
20731                            "remoteFamily": socket_addr_family(&guest_remote_addr),
20732                        }))
20733                    }
20734                    Some(JavascriptTcpListenerEvent::Error { code, message }) => Ok(json!({
20735                        "type": "error",
20736                        "code": code,
20737                        "message": message,
20738                    })),
20739                    None => Ok(Value::Null),
20740                };
20741            }
20742
20743            let event = {
20744                let listener = process.unix_listeners.get_mut(listener_id).ok_or_else(|| {
20745                    SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20746                })?;
20747                listener.poll(Duration::from_millis(wait_ms))?
20748            };
20749
20750            match event {
20751                Some(JavascriptUnixListenerEvent::Connection(pending)) => {
20752                    if let Err(error) = check_network_resource_limit(
20753                        resource_limits.max_sockets,
20754                        network_counts.sockets,
20755                        1,
20756                        "socket",
20757                    )
20758                    .and_then(|()| {
20759                        check_network_resource_limit(
20760                            resource_limits.max_connections,
20761                            network_counts.connections,
20762                            1,
20763                            "connection",
20764                        )
20765                    }) {
20766                        let _ = pending.stream.shutdown(Shutdown::Both);
20767                        return Ok(json!({
20768                            "type": "error",
20769                            "code": "EAGAIN",
20770                            "message": error.to_string(),
20771                        }));
20772                    }
20773                    let socket = ActiveUnixSocket::from_stream(
20774                        pending.stream,
20775                        Some(listener_id.to_string()),
20776                        pending.local_path.clone(),
20777                        pending.remote_path.clone(),
20778                    )?;
20779                    let socket_id = process.allocate_unix_socket_id();
20780                    if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20781                        listener.register_connection(&socket_id);
20782                    }
20783                    process.unix_sockets.insert(socket_id.clone(), socket);
20784                    Ok(json!({
20785                        "type": "connection",
20786                        "socketId": socket_id,
20787                        "localPath": pending.local_path,
20788                        "remotePath": pending.remote_path,
20789                    }))
20790                }
20791                Some(JavascriptUnixListenerEvent::Error { code, message }) => Ok(json!({
20792                    "type": "error",
20793                    "code": code,
20794                    "message": message,
20795                })),
20796                None => Ok(Value::Null),
20797            }
20798        }
20799        "net.server_accept" => {
20800            let listener_id =
20801                javascript_sync_rpc_arg_str(&request.args, 0, "net.server_accept listener id")?;
20802            if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20803                return match listener.poll(kernel, process.kernel_pid, Duration::ZERO)? {
20804                    Some(JavascriptTcpListenerEvent::Connection(pending)) => {
20805                        let PendingTcpSocket {
20806                            stream,
20807                            kernel_socket_id,
20808                            preallocated,
20809                            guest_local_addr,
20810                            guest_remote_addr,
20811                        } = pending;
20812                        if !preallocated {
20813                            check_network_resource_limit(
20814                                resource_limits.max_sockets,
20815                                network_counts.sockets,
20816                                1,
20817                                "socket",
20818                            )?;
20819                            check_network_resource_limit(
20820                                resource_limits.max_connections,
20821                                network_counts.connections,
20822                                1,
20823                                "connection",
20824                            )?;
20825                        }
20826                        let info = json!({
20827                            "localAddress": guest_local_addr.ip().to_string(),
20828                            "localPort": guest_local_addr.port(),
20829                            "localFamily": socket_addr_family(&guest_local_addr),
20830                            "remoteAddress": guest_remote_addr.ip().to_string(),
20831                            "remotePort": guest_remote_addr.port(),
20832                            "remoteFamily": socket_addr_family(&guest_remote_addr),
20833                        });
20834                        let socket = if let Some(stream) = stream {
20835                            ActiveTcpSocket::from_stream(
20836                                stream,
20837                                Some(listener_id.to_string()),
20838                                guest_local_addr,
20839                                guest_remote_addr,
20840                            )?
20841                        } else {
20842                            ActiveTcpSocket::from_kernel(
20843                                kernel_socket_id.ok_or_else(|| {
20844                                    SidecarError::InvalidState(String::from(
20845                                        "kernel TCP accept missing socket id",
20846                                    ))
20847                                })?,
20848                                Some(listener_id.to_string()),
20849                                guest_local_addr,
20850                                guest_remote_addr,
20851                            )
20852                        };
20853                        let socket_id = process.allocate_tcp_socket_id();
20854                        if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20855                            listener.register_connection(&socket_id);
20856                        }
20857                        process.tcp_sockets.insert(socket_id.clone(), socket);
20858                        javascript_net_json_string(
20859                            json!({
20860                                "socketId": socket_id,
20861                                "info": info,
20862                            }),
20863                            "net.server_accept",
20864                        )
20865                    }
20866                    Some(JavascriptTcpListenerEvent::Error { code, message }) => {
20867                        let detail = code.unwrap_or_else(|| String::from("server accept"));
20868                        Err(SidecarError::Execution(format!("{detail}: {message}")))
20869                    }
20870                    None => Ok(javascript_net_timeout_value()),
20871                };
20872            }
20873
20874            let listener = process.unix_listeners.get_mut(listener_id).ok_or_else(|| {
20875                SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20876            })?;
20877            match listener.poll(Duration::ZERO)? {
20878                Some(JavascriptUnixListenerEvent::Connection(pending)) => {
20879                    check_network_resource_limit(
20880                        resource_limits.max_sockets,
20881                        network_counts.sockets,
20882                        1,
20883                        "socket",
20884                    )?;
20885                    check_network_resource_limit(
20886                        resource_limits.max_connections,
20887                        network_counts.connections,
20888                        1,
20889                        "connection",
20890                    )?;
20891                    let info = json!({
20892                        "localPath": pending.local_path.clone(),
20893                        "remotePath": pending.remote_path.clone(),
20894                    });
20895                    let socket = ActiveUnixSocket::from_stream(
20896                        pending.stream,
20897                        Some(listener_id.to_string()),
20898                        pending.local_path,
20899                        pending.remote_path,
20900                    )?;
20901                    let socket_id = process.allocate_unix_socket_id();
20902                    if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20903                        listener.register_connection(&socket_id);
20904                    }
20905                    process.unix_sockets.insert(socket_id.clone(), socket);
20906                    javascript_net_json_string(
20907                        json!({
20908                            "socketId": socket_id,
20909                            "info": info,
20910                        }),
20911                        "net.server_accept",
20912                    )
20913                }
20914                Some(JavascriptUnixListenerEvent::Error { code, message }) => {
20915                    let detail = code.unwrap_or_else(|| String::from("server accept"));
20916                    Err(SidecarError::Execution(format!("{detail}: {message}")))
20917                }
20918                None => Ok(javascript_net_timeout_value()),
20919            }
20920        }
20921        "net.server_connections" => {
20922            let listener_id = javascript_sync_rpc_arg_str(
20923                &request.args,
20924                0,
20925                "net.server_connections listener id",
20926            )?;
20927            if let Some(listener) = process.tcp_listeners.get(listener_id) {
20928                Ok(json!(listener.active_connection_count()))
20929            } else {
20930                let listener = process.unix_listeners.get(listener_id).ok_or_else(|| {
20931                    SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20932                })?;
20933                Ok(json!(listener.active_connection_count()))
20934            }
20935        }
20936        "net.upgrade_socket_write" => {
20937            let socket_id = javascript_sync_rpc_arg_str(
20938                &request.args,
20939                0,
20940                "net.upgrade_socket_write socket id",
20941            )?;
20942            let chunk =
20943                javascript_sync_rpc_base64_arg(&request.args, 1, "net.upgrade_socket_write chunk")?;
20944            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20945                SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
20946            })?;
20947            socket
20948                .write_all(kernel, process.kernel_pid, &chunk)
20949                .map(|written| json!(written))
20950        }
20951        "net.upgrade_socket_end" => {
20952            let socket_id =
20953                javascript_sync_rpc_arg_str(&request.args, 0, "net.upgrade_socket_end socket id")?;
20954            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20955                SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
20956            })?;
20957            socket.shutdown_write(kernel, process.kernel_pid)?;
20958            Ok(Value::Null)
20959        }
20960        "net.upgrade_socket_destroy" => {
20961            let socket_id = javascript_sync_rpc_arg_str(
20962                &request.args,
20963                0,
20964                "net.upgrade_socket_destroy socket id",
20965            )?;
20966            let socket = process.tcp_sockets.remove(socket_id).ok_or_else(|| {
20967                SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
20968            })?;
20969            if let Some(listener_id) = socket.listener_id.as_deref() {
20970                if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20971                    listener.release_connection(socket_id);
20972                }
20973            }
20974            let _ = socket.close(kernel, process.kernel_pid);
20975            Ok(Value::Null)
20976        }
20977        "net.write" => {
20978            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.write socket id")?;
20979            let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "net.write chunk")?;
20980            if let Some(socket) = process.tcp_sockets.get(socket_id) {
20981                socket
20982                    .write_all(kernel, process.kernel_pid, &chunk)
20983                    .map(|written| json!(written))
20984            } else {
20985                let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
20986                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20987                })?;
20988                socket.write_all(&chunk).map(|written| json!(written))
20989            }
20990        }
20991        "net.shutdown" => {
20992            let socket_id =
20993                javascript_sync_rpc_arg_str(&request.args, 0, "net.shutdown socket id")?;
20994            if let Some(socket) = process.tcp_sockets.get(socket_id) {
20995                socket.shutdown_write(kernel, process.kernel_pid)?;
20996            } else {
20997                let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
20998                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20999                })?;
21000                socket.shutdown_write()?;
21001            }
21002            Ok(Value::Null)
21003        }
21004        "net.destroy" => {
21005            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.destroy socket id")?;
21006            if let Some(socket) = process.tcp_sockets.remove(socket_id) {
21007                if let Some(listener_id) = socket.listener_id.as_deref() {
21008                    if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
21009                        listener.release_connection(socket_id);
21010                    }
21011                }
21012                let _ = socket.close(kernel, process.kernel_pid);
21013                Ok(Value::Null)
21014            } else {
21015                let socket = process.unix_sockets.remove(socket_id).ok_or_else(|| {
21016                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
21017                })?;
21018                if let Some(listener_id) = socket.listener_id.as_deref() {
21019                    if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
21020                        listener.release_connection(socket_id);
21021                    }
21022                }
21023                let _ = socket.close();
21024                Ok(Value::Null)
21025            }
21026        }
21027        "net.server_close" => {
21028            let listener_id =
21029                javascript_sync_rpc_arg_str(&request.args, 0, "net.server_close listener id")?;
21030            if let Some(listener) = process.tcp_listeners.remove(listener_id) {
21031                listener.close(kernel, process.kernel_pid)?;
21032                Ok(Value::Null)
21033            } else {
21034                let listener = process.unix_listeners.remove(listener_id).ok_or_else(|| {
21035                    SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
21036                })?;
21037                listener.close()?;
21038                Ok(Value::Null)
21039            }
21040        }
21041        "tls.get_ciphers" => javascript_net_json_string(
21042            Value::Array(
21043                tls_provider()
21044                    .cipher_suites
21045                    .iter()
21046                    .filter_map(|suite| {
21047                        suite
21048                            .suite()
21049                            .as_str()
21050                            .map(|value| Value::String(value.to_owned()))
21051                    })
21052                    .collect(),
21053            ),
21054            "tls.get_ciphers",
21055        ),
21056        _ => Err(SidecarError::InvalidState(format!(
21057            "unsupported JavaScript net sync RPC method {}",
21058            request.method
21059        ))),
21060    }
21061}
21062
21063fn signal_name_for_stream_event(signal: i32) -> Option<&'static str> {
21064    match signal {
21065        libc::SIGHUP => Some("SIGHUP"),
21066        libc::SIGINT => Some("SIGINT"),
21067        libc::SIGUSR1 => Some("SIGUSR1"),
21068        libc::SIGALRM => Some("SIGALRM"),
21069        libc::SIGCONT => Some("SIGCONT"),
21070        libc::SIGTERM => Some("SIGTERM"),
21071        libc::SIGCHLD => Some("SIGCHLD"),
21072        libc::SIGWINCH => Some("SIGWINCH"),
21073        _ => None,
21074    }
21075}
21076
21077pub(crate) fn canonical_signal_name(signal: i32) -> Option<&'static str> {
21078    match signal {
21079        1 => Some("SIGHUP"),
21080        2 => Some("SIGINT"),
21081        3 => Some("SIGQUIT"),
21082        4 => Some("SIGILL"),
21083        5 => Some("SIGTRAP"),
21084        6 => Some("SIGABRT"),
21085        7 => Some("SIGBUS"),
21086        8 => Some("SIGFPE"),
21087        9 => Some("SIGKILL"),
21088        10 => Some("SIGUSR1"),
21089        11 => Some("SIGSEGV"),
21090        12 => Some("SIGUSR2"),
21091        13 => Some("SIGPIPE"),
21092        14 => Some("SIGALRM"),
21093        15 => Some("SIGTERM"),
21094        17 => Some("SIGCHLD"),
21095        18 => Some("SIGCONT"),
21096        19 => Some("SIGSTOP"),
21097        20 => Some("SIGTSTP"),
21098        21 => Some("SIGTTIN"),
21099        22 => Some("SIGTTOU"),
21100        23 => Some("SIGURG"),
21101        24 => Some("SIGXCPU"),
21102        25 => Some("SIGXFSZ"),
21103        26 => Some("SIGVTALRM"),
21104        27 => Some("SIGPROF"),
21105        28 => Some("SIGWINCH"),
21106        29 => Some("SIGIO"),
21107        30 => Some("SIGPWR"),
21108        31 => Some("SIGSYS"),
21109        _ => None,
21110    }
21111}
21112
21113fn dispatch_v8_process_signal(process: &ActiveProcess, signal: i32) -> Result<bool, SidecarError> {
21114    let Some(signal_name) = signal_name_for_stream_event(signal) else {
21115        return Ok(false);
21116    };
21117    process.execution.send_javascript_stream_event(
21118        "signal",
21119        json!({
21120            "signal": signal_name,
21121            "number": signal,
21122            "action": "default",
21123        }),
21124    )?;
21125    Ok(true)
21126}
21127
21128fn dispatch_v8_session_signal_async(session: V8SessionHandle, signal: i32) {
21129    let Some(signal_name) = signal_name_for_stream_event(signal).map(str::to_owned) else {
21130        return;
21131    };
21132    thread::spawn(move || {
21133        thread::sleep(Duration::from_millis(1));
21134        let payload = v8_runtime::json_to_cbor_payload(&json!({
21135            "signal": signal_name,
21136            "number": signal,
21137            "action": "default",
21138        }))
21139        .unwrap_or_default();
21140        let _ = session.send_stream_event("signal", payload);
21141    });
21142}
21143
21144pub(crate) fn parse_signal(signal: &str) -> Result<i32, SidecarError> {
21145    let trimmed = signal.trim();
21146    if trimmed.is_empty() {
21147        return Err(SidecarError::InvalidState(String::from(
21148            "kill_process requires a non-empty signal",
21149        )));
21150    }
21151
21152    if let Ok(value) = trimmed.parse::<i32>() {
21153        return match value {
21154            0..=31 => Ok(value),
21155            _ => Err(SidecarError::InvalidState(format!(
21156                "unsupported kill_process signal {signal}"
21157            ))),
21158        };
21159    }
21160
21161    let upper = trimmed.to_ascii_uppercase();
21162    let normalized = upper.strip_prefix("SIG").unwrap_or(&upper);
21163
21164    signal_number_from_name(normalized).ok_or_else(|| {
21165        SidecarError::InvalidState(format!("unsupported kill_process signal {signal}"))
21166    })
21167}
21168
21169fn signal_number_from_name(signal: &str) -> Option<i32> {
21170    match signal {
21171        "0" => Some(0),
21172        "HUP" => Some(1),
21173        "INT" => Some(2),
21174        "QUIT" => Some(3),
21175        "ILL" => Some(4),
21176        "TRAP" => Some(5),
21177        "ABRT" | "IOT" => Some(6),
21178        "BUS" => Some(7),
21179        "FPE" => Some(8),
21180        "KILL" => Some(9),
21181        "USR1" => Some(10),
21182        "SEGV" => Some(11),
21183        "USR2" => Some(12),
21184        "PIPE" => Some(13),
21185        "ALRM" => Some(14),
21186        "TERM" => Some(15),
21187        "STKFLT" => Some(16),
21188        "CHLD" => Some(17),
21189        "CONT" => Some(18),
21190        "STOP" => Some(19),
21191        "TSTP" => Some(20),
21192        "TTIN" => Some(21),
21193        "TTOU" => Some(22),
21194        "URG" => Some(23),
21195        "XCPU" => Some(24),
21196        "XFSZ" => Some(25),
21197        "VTALRM" => Some(26),
21198        "PROF" => Some(27),
21199        "WINCH" => Some(28),
21200        "IO" | "POLL" => Some(29),
21201        "PWR" => Some(30),
21202        "SYS" => Some(31),
21203        _ => None,
21204    }
21205}
21206
21207pub(crate) fn runtime_child_is_alive(child_pid: u32) -> Result<bool, SidecarError> {
21208    Ok(runtime_child_exit_status(child_pid)?.is_none())
21209}
21210
21211#[cfg(not(target_os = "macos"))]
21212fn runtime_child_exit_status(child_pid: u32) -> Result<Option<i32>, SidecarError> {
21213    if child_pid == 0 {
21214        return Ok(Some(0));
21215    }
21216
21217    let wait_flags = WaitPidFlag::WNOHANG
21218        | WaitPidFlag::WNOWAIT
21219        | WaitPidFlag::WEXITED
21220        | WaitPidFlag::WUNTRACED
21221        | WaitPidFlag::WCONTINUED;
21222    match wait_on_child(WaitId::Pid(Pid::from_raw(child_pid as i32)), wait_flags) {
21223        Ok(WaitStatus::StillAlive)
21224        | Ok(WaitStatus::Stopped(_, _))
21225        | Ok(WaitStatus::Continued(_)) => Ok(None),
21226        Ok(WaitStatus::Exited(_, status)) => Ok(Some(status)),
21227        Ok(WaitStatus::Signaled(_, signal, _)) => Ok(Some(128 + signal as i32)),
21228        #[cfg(any(target_os = "linux", target_os = "android"))]
21229        Ok(WaitStatus::PtraceEvent(_, _, _) | WaitStatus::PtraceSyscall(_)) => Ok(None),
21230        Err(nix::errno::Errno::ECHILD) => Ok(Some(0)),
21231        Err(error) => Err(SidecarError::Execution(format!(
21232            "failed to inspect guest runtime process {child_pid}: {error}"
21233        ))),
21234    }
21235}
21236
21237// macOS nix exposes no `waitid`/`WNOWAIT`, so we poll with `waitpid(WNOHANG)`.
21238// NOTE: unlike Linux's `waitid(WNOWAIT)`, `waitpid` REAPS an exited child rather
21239// than leaving it waitable. That is correct for this poll (the sidecar is the
21240// reaping parent), but a second status query after exit returns ECHILD → treated
21241// as "exited(0)" below.
21242#[cfg(target_os = "macos")]
21243fn runtime_child_exit_status(child_pid: u32) -> Result<Option<i32>, SidecarError> {
21244    if child_pid == 0 {
21245        return Ok(Some(0));
21246    }
21247
21248    match waitpid(Pid::from_raw(child_pid as i32), Some(WaitPidFlag::WNOHANG)) {
21249        Ok(WaitStatus::StillAlive)
21250        | Ok(WaitStatus::Stopped(_, _))
21251        | Ok(WaitStatus::Continued(_)) => Ok(None),
21252        Ok(WaitStatus::Exited(_, status)) => Ok(Some(status)),
21253        Ok(WaitStatus::Signaled(_, signal, _)) => Ok(Some(128 + signal as i32)),
21254        Err(nix::errno::Errno::ECHILD) => Ok(Some(0)),
21255        Err(error) => Err(SidecarError::Execution(format!(
21256            "failed to inspect guest runtime process {child_pid}: {error}"
21257        ))),
21258    }
21259}
21260
21261pub(crate) fn signal_runtime_process(child_pid: u32, signal: i32) -> Result<(), SidecarError> {
21262    if child_pid == 0 {
21263        return Ok(());
21264    }
21265
21266    if !runtime_child_is_alive(child_pid)? {
21267        return Ok(());
21268    }
21269
21270    if signal == 0 {
21271        return Ok(());
21272    }
21273
21274    let parsed = Signal::try_from(signal).map_err(|_| {
21275        SidecarError::InvalidState(format!("unsupported kill_process signal {signal}"))
21276    })?;
21277    let result = send_signal(Pid::from_raw(child_pid as i32), Some(parsed));
21278
21279    match result {
21280        Ok(()) => Ok(()),
21281        Err(nix::errno::Errno::ESRCH) => Ok(()),
21282        Err(error) => Err(SidecarError::Execution(format!(
21283            "failed to signal guest runtime process {child_pid}: {error}"
21284        ))),
21285    }
21286}
21287
21288pub(crate) fn error_code(error: &SidecarError) -> &'static str {
21289    match error {
21290        SidecarError::InvalidState(_) => "invalid_state",
21291        SidecarError::ProtocolVersionMismatch(_) => "protocol_version_mismatch",
21292        SidecarError::BridgeVersionMismatch(_) => "bridge_version_mismatch",
21293        SidecarError::Conflict(_) => "conflict",
21294        SidecarError::Unauthorized(_) => "unauthorized",
21295        SidecarError::Unsupported(_) => "unsupported",
21296        SidecarError::FrameTooLarge(_) => "frame_too_large",
21297        SidecarError::Kernel(_) => "kernel_error",
21298        SidecarError::Plugin(_) => "plugin_error",
21299        SidecarError::Execution(_) => "execution_error",
21300        SidecarError::Bridge(_) => "bridge_error",
21301        SidecarError::Io(_) => "io_error",
21302    }
21303}
21304
21305fn guest_errno_code(message: &str) -> Option<&str> {
21306    const TRUSTED_PREFIXES: &[&str] = &[
21307        "ERR_AGENTOS_NODE_SYNC_RPC",
21308        "ERR_AGENTOS_PYTHON_VFS_RPC",
21309        "ERR_AGENTOS_BRIDGE",
21310    ];
21311
21312    let mut segments = message.split(':').map(str::trim);
21313    let first = segments.next()?;
21314    if is_guest_errno_segment(first) {
21315        return Some(first);
21316    }
21317
21318    if TRUSTED_PREFIXES.contains(&first) {
21319        let second = segments.next()?;
21320        if is_guest_errno_segment(second) {
21321            return Some(second);
21322        }
21323    }
21324
21325    None
21326}
21327
21328fn is_guest_errno_segment(segment: &str) -> bool {
21329    segment.len() >= 2
21330        && segment.starts_with('E')
21331        && !segment.starts_with("ERR_")
21332        && segment[1..]
21333            .bytes()
21334            .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_')
21335}
21336
21337pub(crate) fn javascript_sync_rpc_error_code(error: &SidecarError) -> String {
21338    let message = error.to_string();
21339    if let Some(code) = guest_errno_code(&message) {
21340        return code.to_owned();
21341    }
21342    if message.starts_with("ERR_NATIVE_BINARY_NOT_SUPPORTED:") {
21343        return String::from("ERR_NATIVE_BINARY_NOT_SUPPORTED");
21344    }
21345
21346    let lower = message.to_ascii_lowercase();
21347    if lower.contains("no such file or directory")
21348        || lower.contains("entry not found")
21349        || lower.contains("not found")
21350    {
21351        return String::from("ENOENT");
21352    }
21353    if lower.contains("permission denied") {
21354        return String::from("EACCES");
21355    }
21356    if lower.contains("already exists")
21357        || lower.contains("already registered")
21358        || lower.contains("file exists")
21359    {
21360        return String::from("EEXIST");
21361    }
21362    if lower.contains("invalid argument") {
21363        return String::from("EINVAL");
21364    }
21365
21366    String::from("ERR_AGENTOS_NODE_SYNC_RPC")
21367}
21368
21369pub(crate) fn ignore_stale_javascript_sync_rpc_response(
21370    error: SidecarError,
21371) -> Result<(), SidecarError> {
21372    match error {
21373        SidecarError::Execution(message)
21374            if message.ends_with("is no longer pending")
21375                && message.starts_with("sync RPC request ") =>
21376        {
21377            Ok(())
21378        }
21379        SidecarError::Execution(message) => {
21380            let lower = message.to_ascii_lowercase();
21381            if lower.contains("sync rpc response")
21382                && (lower.contains("broken pipe") || lower.contains("channel closed unexpectedly"))
21383            {
21384                Ok(())
21385            } else {
21386                Err(SidecarError::Execution(message))
21387            }
21388        }
21389        other => Err(other),
21390    }
21391}
21392
21393#[cfg(test)]
21394mod error_code_tests {
21395    use super::{guest_errno_code, javascript_sync_rpc_error_code, SidecarError};
21396
21397    #[test]
21398    fn guest_errno_code_rejects_guest_controlled_errno_segments() {
21399        assert_eq!(guest_errno_code("user said 'EACCES: denied'"), None);
21400        assert_eq!(
21401            guest_errno_code("prefix: user said 'EPERM': more text"),
21402            None
21403        );
21404        assert_eq!(guest_errno_code("ERR_AGENTOS_FAKE: EACCES: denied"), None);
21405    }
21406
21407    #[test]
21408    fn guest_errno_code_accepts_trusted_secure_exec_prefixes() {
21409        assert_eq!(
21410            guest_errno_code("ERR_AGENTOS_NODE_SYNC_RPC: EACCES: permission denied on /foo"),
21411            Some("EACCES")
21412        );
21413        assert_eq!(
21414            guest_errno_code("ERR_AGENTOS_PYTHON_VFS_RPC: ENOENT: missing file"),
21415            Some("ENOENT")
21416        );
21417        assert_eq!(guest_errno_code("EEXIST: already exists"), Some("EEXIST"));
21418    }
21419
21420    #[test]
21421    fn javascript_sync_rpc_error_code_ignores_spoofed_errnos() {
21422        let error = SidecarError::Execution(String::from("user said 'EACCES: denied'"));
21423        assert_eq!(
21424            javascript_sync_rpc_error_code(&error),
21425            "ERR_AGENTOS_NODE_SYNC_RPC"
21426        );
21427    }
21428
21429    #[test]
21430    fn javascript_sync_rpc_error_code_preserves_real_sidecar_errnos() {
21431        let error = SidecarError::Execution(String::from(
21432            "ERR_AGENTOS_NODE_SYNC_RPC: EACCES: permission denied on /foo",
21433        ));
21434        assert_eq!(javascript_sync_rpc_error_code(&error), "EACCES");
21435    }
21436
21437    #[test]
21438    fn javascript_sync_rpc_error_code_maps_file_exists_messages() {
21439        let error = SidecarError::Io(String::from(
21440            "failed to create mapped guest directory /.next/server: File exists (os error 17)",
21441        ));
21442        assert_eq!(javascript_sync_rpc_error_code(&error), "EEXIST");
21443    }
21444
21445    #[test]
21446    fn javascript_sync_rpc_error_code_preserves_native_binary_rejections() {
21447        let error = SidecarError::Execution(String::from(
21448            "ERR_NATIVE_BINARY_NOT_SUPPORTED: refused to execute native ELF guest binary at /tmp/fake-rg inside the VM",
21449        ));
21450        assert_eq!(
21451            javascript_sync_rpc_error_code(&error),
21452            "ERR_NATIVE_BINARY_NOT_SUPPORTED"
21453        );
21454    }
21455}
21456#[cfg(test)]
21457mod ssrf_egress_classifier_tests {
21458    // F-005/006/007 (sec-sidecar T1/T7/T11): the egress classifier must treat the
21459    // unspecified address (0.0.0.0 / ::), CGNAT (100.64.0.0/10), IPv6 spellings of
21460    // restricted IPv4 targets (::a.b.c.d), and reserved/multicast (240/4, 224/4) as
21461    // restricted. 0.0.0.0 routes to 127.0.0.1 on connect(), so leaving it
21462    // unclassified let a guest bypass the loopback port-ownership gate.
21463    //
21464    // These are bounded SAFEGUARD tests: they exercise the classifier and the DNS
21465    // egress filter directly (no network I/O, no Node), so they run fast and
21466    // deterministically. See FAILURES.md#F-005, #F-006, #F-007.
21467    use super::{
21468        filter_dns_safe_ip_addrs, is_loopback_ip, restricted_non_loopback_ip_range, SidecarError,
21469    };
21470    use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
21471
21472    fn assert_restricted(ip: IpAddr, expected_label: &str) {
21473        let classification = restricted_non_loopback_ip_range(ip);
21474        assert!(
21475            classification.is_some(),
21476            "{ip} must be classified as a restricted egress target"
21477        );
21478        let (_cidr, label) = classification.unwrap();
21479        assert_eq!(
21480            label, expected_label,
21481            "{ip} should be labelled {expected_label}, got {label}"
21482        );
21483    }
21484
21485    fn assert_dns_denied(ip: IpAddr, label: &str) {
21486        match filter_dns_safe_ip_addrs(vec![ip], "attacker.example") {
21487            Err(SidecarError::Execution(message)) => assert!(
21488                message.starts_with("EACCES:"),
21489                "{label}: egress filter must deny with EACCES, got: {message}"
21490            ),
21491            other => panic!("{label}: expected EACCES denial, got {other:?}"),
21492        }
21493    }
21494
21495    // F-005 (sec-sidecar T1).
21496    #[test]
21497    fn classifier_denies_unspecified_and_cgnat_targets() {
21498        // 0.0.0.0 (IPv4 unspecified) -> would route to host loopback.
21499        assert_restricted(IpAddr::V4(Ipv4Addr::UNSPECIFIED), "unspecified");
21500        // :: (IPv6 unspecified).
21501        assert_restricted(IpAddr::V6(Ipv6Addr::UNSPECIFIED), "unspecified");
21502
21503        // CGNAT 100.64.0.0/10 spans 100.64.x.x .. 100.127.x.x.
21504        assert_restricted(
21505            IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1)),
21506            "carrier-grade-nat",
21507        );
21508        assert_restricted(
21509            IpAddr::V4(Ipv4Addr::new(100, 127, 255, 254)),
21510            "carrier-grade-nat",
21511        );
21512
21513        // Guard against over-blocking: addresses just outside 100.64/10 stay allowed.
21514        assert!(
21515            restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(100, 63, 255, 255)))
21516                .is_none(),
21517            "100.63.255.255 is outside CGNAT and must remain allowed"
21518        );
21519        assert!(
21520            restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(100, 128, 0, 0))).is_none(),
21521            "100.128.0.0 is outside CGNAT and must remain allowed"
21522        );
21523
21524        // The DNS egress filter must also deny these via EACCES.
21525        assert_dns_denied(IpAddr::V4(Ipv4Addr::UNSPECIFIED), "0.0.0.0 (unspecified)");
21526        assert_dns_denied(IpAddr::V6(Ipv6Addr::UNSPECIFIED), ":: (unspecified)");
21527        assert_dns_denied(
21528            IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1)),
21529            "100.64.0.1 (CGNAT)",
21530        );
21531    }
21532
21533    // F-006 (sec-sidecar T7).
21534    #[test]
21535    fn classifier_denies_ipv6_spelled_metadata_addresses() {
21536        // The IPv4-mapped form (::ffff:169.254.169.254) was already handled; the
21537        // IPv4-compatible form (::169.254.169.254) is the gap this fixes.
21538        let mapped = "::ffff:169.254.169.254".parse::<Ipv6Addr>().unwrap();
21539        assert_restricted(IpAddr::V6(mapped), "link-local");
21540
21541        let compat = "::169.254.169.254".parse::<Ipv6Addr>().unwrap();
21542        assert_restricted(IpAddr::V6(compat), "link-local");
21543
21544        // Other IPv4-compatible private/CGNAT spellings must also be canonicalized.
21545        assert_restricted(
21546            IpAddr::V6("::10.0.0.1".parse::<Ipv6Addr>().unwrap()),
21547            "private",
21548        );
21549        assert_restricted(
21550            IpAddr::V6("::100.64.0.1".parse::<Ipv6Addr>().unwrap()),
21551            "carrier-grade-nat",
21552        );
21553
21554        // Guard against over-blocking: the IPv6 unspecified/loopback addresses
21555        // are not IPv4-compatible host targets, and a public IPv4-compatible
21556        // address must remain allowed.
21557        assert_eq!(
21558            restricted_non_loopback_ip_range(IpAddr::V6(Ipv6Addr::UNSPECIFIED)),
21559            Some(("::/128", "unspecified")),
21560            ":: must classify as unspecified, not via the IPv4-compat path"
21561        );
21562        assert!(
21563            restricted_non_loopback_ip_range(IpAddr::V6(Ipv6Addr::LOCALHOST)).is_none()
21564                || is_loopback_ip(IpAddr::V6(Ipv6Addr::LOCALHOST)),
21565            "::1 must not be classified as a restricted IPv4-compatible target"
21566        );
21567        assert!(
21568            restricted_non_loopback_ip_range(IpAddr::V6("::8.8.8.8".parse::<Ipv6Addr>().unwrap()))
21569                .is_none(),
21570            "::8.8.8.8 (public IPv4-compatible) must remain allowed"
21571        );
21572
21573        // The DNS egress filter must deny the IPv4-compat metadata spelling.
21574        assert_dns_denied(
21575            IpAddr::V6("::169.254.169.254".parse::<Ipv6Addr>().unwrap()),
21576            "::169.254.169.254 (IPv4-compat metadata)",
21577        );
21578    }
21579
21580    // F-007 (sec-sidecar T11).
21581    #[test]
21582    fn classifier_denies_reserved_and_multicast_targets() {
21583        // 224.0.0.0/4 (multicast) and 240.0.0.0/4 (reserved / future use) are not
21584        // legitimate unicast egress targets; a guest connect to them must be
21585        // classified as restricted and denied.
21586        assert_restricted(IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)), "multicast");
21587        assert_restricted(IpAddr::V4(Ipv4Addr::new(239, 255, 255, 255)), "multicast");
21588        assert_restricted(IpAddr::V4(Ipv4Addr::new(240, 0, 0, 1)), "reserved");
21589        // 255.255.255.255 (limited broadcast) falls in 240.0.0.0/4.
21590        assert_restricted(IpAddr::V4(Ipv4Addr::BROADCAST), "reserved");
21591
21592        // IPv4-compatible IPv6 spellings must canonicalize and be denied too.
21593        assert_restricted(
21594            IpAddr::V6("::224.0.0.1".parse::<Ipv6Addr>().unwrap()),
21595            "multicast",
21596        );
21597        assert_restricted(
21598            IpAddr::V6("::240.0.0.1".parse::<Ipv6Addr>().unwrap()),
21599            "reserved",
21600        );
21601
21602        // Guard against over-blocking: addresses just outside 224/4 stay allowed.
21603        assert!(
21604            restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(223, 255, 255, 255)))
21605                .is_none(),
21606            "223.255.255.255 is outside 224/4 and must remain allowed"
21607        );
21608
21609        // The DNS egress filter must also deny these via EACCES.
21610        assert_dns_denied(
21611            IpAddr::V4(Ipv4Addr::new(240, 0, 0, 1)),
21612            "240.0.0.1 (reserved)",
21613        );
21614        assert_dns_denied(
21615            IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)),
21616            "224.0.0.1 (multicast)",
21617        );
21618    }
21619}
21620
21621/// Adversarial coverage for the DNS-rebinding gap (VECTORS.md D.3) on the
21622/// Python/Pyodide `httpRequestSync` outbound HTTP path. The egress range guard
21623/// (`filter_dns_safe_ip_addrs`) runs at resolution time, but `ureq` performs its
21624/// own DNS resolution for the TCP/TLS connect, so a rebinding DNS server could
21625/// previously make the second lookup land on a private/link-local/metadata IP
21626/// the first check rejected. The fix pins `ureq`'s resolver to the vetted
21627/// address set; these tests prove the connect is pinned and refuses any other
21628/// host or an empty (fully-rejected) address set.
21629#[cfg(test)]
21630mod dns_rebinding_pin_tests {
21631    use super::{issue_outbound_http_request, split_netloc, JavascriptHttpRequestOptions};
21632    use std::collections::BTreeMap;
21633    use std::io::{Read, Write};
21634    use std::net::{IpAddr, Ipv4Addr, TcpListener};
21635    use std::thread;
21636    use url::Url;
21637
21638    fn empty_headers() -> super::HttpHeaderCollection {
21639        super::parse_http_header_collection(&BTreeMap::new(), "test headers")
21640            .expect("empty header collection")
21641    }
21642
21643    fn options() -> JavascriptHttpRequestOptions {
21644        JavascriptHttpRequestOptions {
21645            method: Some(String::from("GET")),
21646            headers: BTreeMap::new(),
21647            body: None,
21648            reject_unauthorized: None,
21649        }
21650    }
21651
21652    #[test]
21653    fn split_netloc_handles_hostnames_and_bracketed_ipv6() {
21654        assert_eq!(
21655            split_netloc("attacker.example:80"),
21656            Some(("attacker.example", 80))
21657        );
21658        assert_eq!(split_netloc("[::1]:443"), Some(("::1", 443)));
21659        assert_eq!(split_netloc("10.0.0.1:8080"), Some(("10.0.0.1", 8080)));
21660        assert_eq!(split_netloc("no-port"), None);
21661        assert_eq!(split_netloc("host:notaport"), None);
21662    }
21663
21664    /// A loopback HTTP server stands in for the egress-vetted target. The
21665    /// request URL uses a *different* hostname (`attacker.example`) whose real
21666    /// DNS would resolve elsewhere; pinning forces the connect onto the vetted
21667    /// IP only. If the resolver were unpinned, the request would fail to reach
21668    /// this server (and on a real host could land on a private/metadata IP).
21669    #[test]
21670    fn outbound_http_connect_is_pinned_to_vetted_ip() {
21671        let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).expect("bind loopback server");
21672        let port = listener.local_addr().expect("local addr").port();
21673        let server = thread::spawn(move || {
21674            let (mut stream, _) = listener.accept().expect("accept");
21675            let mut buf = [0u8; 1024];
21676            let _ = stream.read(&mut buf);
21677            stream
21678                .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi")
21679                .expect("write response");
21680            let _ = stream.flush();
21681        });
21682
21683        let url = Url::parse(&format!("http://attacker.example:{port}/")).expect("url");
21684        let pinned = vec![IpAddr::V4(Ipv4Addr::LOCALHOST)];
21685        let result = issue_outbound_http_request(&url, &options(), &empty_headers(), &pinned)
21686            .expect("pinned request should reach the vetted loopback target");
21687        let payload = result.as_str().expect("string payload");
21688        assert!(
21689            payload.contains("\"status\":200"),
21690            "expected 200 from pinned target, got: {payload}"
21691        );
21692        server.join().expect("server thread");
21693    }
21694
21695    /// With no vetted address (every resolved IP was rejected by the range
21696    /// guard, or the literal IP was a blocked range), the pinned resolver must
21697    /// refuse rather than fall back to the host resolver.
21698    #[test]
21699    fn outbound_http_refuses_when_no_vetted_address() {
21700        let url = Url::parse("https://attacker.example/").expect("url");
21701        let error = issue_outbound_http_request(&url, &options(), &empty_headers(), &[])
21702            .expect_err("empty pinned set must be refused");
21703        let message = error.to_string();
21704        assert!(
21705            message.contains("EACCES") || message.contains("ERR_HTTP_REQUEST_FAILED"),
21706            "expected an egress refusal, got: {message}"
21707        );
21708    }
21709}