Skip to main content

secure_exec_sidecar/
execution.rs

1//! Process execution, networking, and runtime event handling extracted from service.rs.
2
3use secure_exec_vm_config as vm_config;
4
5use crate::filesystem::{
6    handle_python_vfs_rpc_request as filesystem_handle_python_vfs_rpc_request,
7    service_javascript_fs_sync_rpc, service_javascript_module_sync_rpc,
8};
9use crate::protocol::{
10    BoundUdpSnapshotResponse, CloseStdinRequest, EventFrame, EventPayload, ExecuteRequest,
11    FindBoundUdpRequest, FindListenerRequest, GetProcessSnapshotRequest, GetSignalStateRequest,
12    GetZombieTimerCountRequest, GuestRuntimeKind, JavascriptChildProcessSpawnOptions,
13    JavascriptChildProcessSpawnRequest, JavascriptDgramBindRequest,
14    JavascriptDgramCreateSocketRequest, JavascriptDgramSendRequest, JavascriptDnsLookupRequest,
15    JavascriptDnsResolveRequest, JavascriptNetConnectRequest, JavascriptNetListenRequest,
16    JavascriptNetReserveTcpPortRequest, KillProcessRequest, ListenerSnapshotResponse,
17    OwnershipScope, ProcessExitedEvent, ProcessKilledResponse, ProcessOutputEvent,
18    ProcessSnapshotEntry, ProcessSnapshotResponse, ProcessSnapshotStatus, ProcessStartedResponse,
19    RequestFrame, ResponseFrame, ResponsePayload, SidecarRequestPayload, SignalDispositionAction,
20    SignalHandlerRegistration, SignalStateResponse, SocketStateEntry, StdinClosedResponse,
21    StdinWrittenResponse, StreamChannel, VmFetchRequest, VmFetchResponse, WasmPermissionTier,
22    WriteStdinRequest, ZombieTimerCountResponse,
23};
24use crate::service::{
25    audit_fields, dirname, emit_security_audit_event, emit_structured_event, javascript_error,
26    kernel_error, log_stale_process_event, normalize_host_path, normalize_path,
27    parse_javascript_child_process_spawn_request, path_is_within_root,
28    process_event_queue_overflow_error, python_error, wasm_error, MAX_PROCESS_EVENT_QUEUE,
29};
30use crate::state::{
31    ActiveCipherSession, ActiveDhSession, ActiveDiffieHellmanSession, ActiveEcdhSession,
32    ActiveExecution, ActiveExecutionEvent, ActiveHttp2Server, ActiveHttp2Session,
33    ActiveHttp2Stream, ActiveHttpServer, ActiveMappedHostFd, ActiveProcess, ActiveSqliteDatabase,
34    ActiveSqliteStatement, ActiveTcpListener, ActiveTcpSocket, ActiveTlsState, ActiveTlsStream,
35    ActiveUdpSocket, ActiveUnixListener, ActiveUnixSocket, BridgeError, ExitedProcessSnapshot,
36    Http2BridgeEvent, Http2RuntimeSnapshot, Http2SessionCommand, Http2SessionSnapshot,
37    Http2SocketSnapshot, JavascriptHttpLoopbackTarget, JavascriptSocketFamily,
38    JavascriptSocketPathContext, JavascriptTcpListenerEvent, JavascriptTcpSocketEvent,
39    JavascriptTlsBridgeOptions, JavascriptTlsClientHello, JavascriptTlsDataValue,
40    JavascriptTlsMaterial, JavascriptUdpFamily, JavascriptUdpSocketEvent,
41    JavascriptUnixListenerEvent, NetworkResourceCounts, PendingTcpSocket, PendingUnixSocket,
42    ProcNetEntry, ProcessEventEnvelope, ResolvedChildProcessExecution, ResolvedTcpConnectAddr,
43    SharedBridge, SharedSidecarRequestClient, SidecarKernel, SocketQueryKind, ToolExecution,
44    VmDnsConfig, VmListenPolicy, VmState, DEFAULT_JAVASCRIPT_NET_BACKLOG, EXECUTION_DRIVER_NAME,
45    EXECUTION_SANDBOX_ROOT_ENV, JAVASCRIPT_COMMAND, LOOPBACK_EXEMPT_PORTS_ENV,
46    MAPPED_HOST_FD_START, PYTHON_COMMAND, TOOL_DRIVER_NAME,
47    VM_LISTEN_ALLOW_PRIVILEGED_METADATA_KEY, WASM_COMMAND, WASM_STDIO_SYNC_RPC_ENV,
48};
49use crate::tools::{
50    format_tool_failure_output, is_tool_command, normalized_tool_command_name,
51    resolve_tool_command, ToolCommandResolution,
52};
53use crate::wire::{ProtocolFrame as WireProtocolFrame, WireFrameCodec, DEFAULT_MAX_FRAME_BYTES};
54use crate::{DispatchResult, NativeSidecar, NativeSidecarBridge, SidecarError};
55
56use base64::Engine;
57use bytes::Bytes;
58use h2::{client, server, Reason};
59use hickory_resolver::proto::rr::{RData, Record, RecordType};
60use hmac::{Hmac, Mac};
61use http::{HeaderMap, HeaderName, HeaderValue, Method, Request, Response, Uri};
62use md5::Md5;
63use nix::libc;
64use nix::sys::signal::{kill as send_signal, Signal};
65use nix::sys::wait::WaitStatus;
66#[cfg(not(target_os = "macos"))]
67use nix::sys::wait::{waitid as wait_on_child, Id as WaitId, WaitPidFlag};
68#[cfg(target_os = "macos")]
69use nix::sys::wait::{waitpid, WaitPidFlag};
70use nix::unistd::Pid;
71use openssl::bn::{BigNum, BigNumContext};
72use openssl::derive::Deriver;
73use openssl::dh::Dh;
74use openssl::ec::{EcGroup, EcKey, EcPoint, PointConversionForm};
75use openssl::hash::MessageDigest;
76use openssl::nid::Nid;
77use openssl::pkey::{Id as PKeyId, PKey, Params, Private, Public};
78use openssl::rand::rand_bytes;
79use openssl::rsa::{Padding, Rsa};
80use openssl::sign::{Signer, Verifier};
81use openssl::symm::{Cipher, Crypter, Mode};
82use pbkdf2::pbkdf2_hmac;
83use rusqlite::types::ValueRef as SqliteValueRef;
84use rusqlite::{
85    Connection as SqliteConnection, OpenFlags as SqliteOpenFlags, Statement as SqliteStatement,
86};
87use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
88use rustls::crypto::aws_lc_rs;
89use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName};
90use rustls::{
91    ClientConfig, ClientConnection, DigitallySignedStruct, RootCertStore, ServerConfig,
92    ServerConnection, SignatureScheme,
93};
94use scrypt::{scrypt, Params as ScryptParams};
95use secure_exec_bridge::LifecycleState;
96use secure_exec_execution::wasm::WasmExecutionError;
97use secure_exec_execution::{
98    javascript::handle_internal_bridge_call_from_host_context, v8_host::V8SessionHandle,
99    v8_runtime, CreateJavascriptContextRequest, CreatePythonContextRequest,
100    CreateWasmContextRequest, GuestModuleReader, GuestRuntimeConfig, JavascriptExecutionEvent,
101    JavascriptExecutionLimits, JavascriptSyncRpcRequest, ModuleFsReader,
102    NodeSignalDispositionAction, NodeSignalHandlerRegistration, PythonExecutionEvent,
103    PythonExecutionLimits, PythonVfsRpcMethod, PythonVfsRpcRequest, PythonVfsRpcResponsePayload,
104    StartJavascriptExecutionRequest, StartPythonExecutionRequest, StartWasmExecutionRequest,
105    WasmExecutionEvent, WasmExecutionLimits, WasmPermissionTier as ExecutionWasmPermissionTier,
106};
107use secure_exec_kernel::dns::{
108    DnsLookupPolicy, DnsRecordResolution, DnsResolutionSource as KernelDnsResolutionSource,
109};
110use secure_exec_kernel::kernel::{KernelProcessHandle, SpawnOptions, VirtualProcessOptions};
111use secure_exec_kernel::permissions::NetworkOperation;
112use secure_exec_kernel::poll::{PollEvents, PollFd, PollTargetEntry, POLLERR, POLLHUP, POLLIN};
113use secure_exec_kernel::process_table::{ProcessStatus, WaitPidFlags, SIGKILL, SIGTERM};
114use secure_exec_kernel::pty::LineDisciplineConfig;
115use secure_exec_kernel::resource_accounting::ResourceLimits;
116use secure_exec_kernel::root_fs::RootFilesystemMode;
117use secure_exec_kernel::socket_table::{
118    InetSocketAddress, SocketDomain, SocketId, SocketShutdown as KernelSocketShutdown, SocketSpec,
119    SocketState, SocketType,
120};
121use serde::{Deserialize, Serialize};
122use serde_json::{json, Map, Value};
123use sha1::Sha1;
124use sha2::{digest::Digest, Sha256, Sha512};
125use socket2::{SockRef, TcpKeepalive};
126use std::collections::VecDeque;
127use std::collections::{BTreeMap, BTreeSet};
128use std::fmt;
129use std::fs;
130use std::io::{Cursor, Read, Write};
131use std::net::{
132    IpAddr, Ipv4Addr, Ipv6Addr, Shutdown, SocketAddr, TcpListener, TcpStream, ToSocketAddrs,
133    UdpSocket,
134};
135use std::os::unix::fs::{MetadataExt, PermissionsExt};
136use std::os::unix::net::{SocketAddr as UnixSocketAddr, UnixListener, UnixStream};
137use std::path::{Path, PathBuf};
138use std::pin::Pin;
139use std::sync::atomic::{AtomicBool, Ordering};
140use std::sync::mpsc::{self, RecvTimeoutError, Sender};
141use std::sync::{Arc, Mutex, OnceLock, Weak};
142use std::thread;
143use std::time::{Duration, Instant};
144use tokio::io::{AsyncRead, AsyncWrite};
145use tokio::runtime::Builder as TokioRuntimeBuilder;
146use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver};
147use tokio_rustls::{TlsAcceptor, TlsConnector};
148use url::Url;
149
150const DEFAULT_KERNEL_STDIN_READ_MAX_BYTES: usize = 64 * 1024;
151const DEFAULT_KERNEL_STDIN_READ_TIMEOUT_MS: u64 = 100;
152const JAVASCRIPT_NET_TIMEOUT_SENTINEL: &str = "__secure_exec_net_timeout__";
153const PYTHON_PYODIDE_GUEST_ROOT: &str = "/__agentos_pyodide";
154const PYTHON_PYODIDE_CACHE_GUEST_ROOT: &str = "/__agentos_pyodide_cache";
155const TCP_SOCKET_POLL_TIMEOUT: Duration = Duration::from_millis(100);
156const TLS_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(5);
157const HTTP_LOOPBACK_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
158pub(crate) const MAX_PER_PROCESS_STATE_HANDLES: usize = 1024;
159const VM_FETCH_BUFFER_LIMIT_BYTES: usize = DEFAULT_MAX_FRAME_BYTES;
160const DEFAULT_SCRYPT_COST: u64 = 16_384;
161const DEFAULT_SCRYPT_BLOCK_SIZE: u32 = 8;
162const DEFAULT_SCRYPT_PARALLELIZATION: u32 = 1;
163const SQLITE_JS_SAFE_INTEGER_MAX: i64 = 9_007_199_254_740_991;
164const HTTP_LOOPBACK_REQUEST_TIMEOUT_MS_ENV: &str =
165    "SECURE_EXEC_TEST_HTTP_LOOPBACK_REQUEST_TIMEOUT_MS";
166
167trait Http2AsyncIo: AsyncRead + AsyncWrite + Unpin + Send {}
168
169impl<T> Http2AsyncIo for T where T: AsyncRead + AsyncWrite + Unpin + Send {}
170
171fn http_loopback_request_timeout() -> Duration {
172    static TIMEOUT: OnceLock<Duration> = OnceLock::new();
173    *TIMEOUT.get_or_init(|| {
174        std::env::var(HTTP_LOOPBACK_REQUEST_TIMEOUT_MS_ENV)
175            .ok()
176            .and_then(|value| value.parse::<u64>().ok())
177            .map(Duration::from_millis)
178            .unwrap_or(HTTP_LOOPBACK_REQUEST_TIMEOUT)
179    })
180}
181
182const DEFAULT_ALLOWED_NODE_BUILTINS: &[&str] = &[
183    "assert",
184    "buffer",
185    "console",
186    "child_process",
187    "crypto",
188    "dns",
189    "events",
190    "fs",
191    "http",
192    "http2",
193    "https",
194    "module",
195    "os",
196    "path",
197    "perf_hooks",
198    "querystring",
199    "sqlite",
200    "stream",
201    "string_decoder",
202    "timers",
203    "tls",
204    "tty",
205    "url",
206    "util",
207    "zlib",
208];
209
210#[derive(Debug, Clone, Copy, PartialEq, Eq)]
211enum JavascriptCryptoDigestAlgorithm {
212    Md5,
213    Sha1,
214    Sha256,
215    Sha512,
216}
217
218#[derive(Debug, Default, Deserialize)]
219#[serde(default, rename_all = "camelCase")]
220struct JavascriptScryptOptions {
221    #[serde(alias = "N")]
222    cost: Option<u64>,
223    #[serde(alias = "r")]
224    block_size: Option<u32>,
225    #[serde(alias = "p")]
226    parallelization: Option<u32>,
227}
228
229#[derive(Debug, Deserialize)]
230#[serde(rename_all = "camelCase")]
231struct JavascriptHttpListenRequest {
232    server_id: u64,
233    #[serde(default)]
234    port: Option<u16>,
235    #[serde(default)]
236    hostname: Option<String>,
237}
238
239#[derive(Debug, Default, Deserialize)]
240#[serde(default, rename_all = "camelCase")]
241struct JavascriptHttpRequestOptions {
242    method: Option<String>,
243    headers: BTreeMap<String, Value>,
244    body: Option<String>,
245    reject_unauthorized: Option<bool>,
246}
247
248#[derive(Debug, Default, Deserialize)]
249#[serde(default, rename_all = "camelCase")]
250struct JavascriptHttp2ServerListenRequest {
251    server_id: u64,
252    secure: bool,
253    port: Option<u16>,
254    host: Option<String>,
255    backlog: Option<u32>,
256    timeout: Option<u64>,
257    settings: BTreeMap<String, Value>,
258    tls: Option<JavascriptTlsBridgeOptions>,
259}
260
261#[derive(Debug, Default, Deserialize)]
262#[serde(default, rename_all = "camelCase")]
263struct JavascriptHttp2SessionConnectRequest {
264    authority: Option<String>,
265    protocol: Option<String>,
266    host: Option<String>,
267    port: Option<u16>,
268    settings: BTreeMap<String, Value>,
269    tls: Option<JavascriptTlsBridgeOptions>,
270}
271
272#[derive(Debug, Default, Deserialize)]
273#[serde(default, rename_all = "camelCase")]
274struct JavascriptHttp2RequestOptions {
275    end_stream: bool,
276}
277
278#[derive(Debug, Default, Deserialize)]
279#[serde(default, rename_all = "camelCase")]
280struct JavascriptHttp2FileResponseOptions {
281    offset: Option<u64>,
282    length: Option<i64>,
283}
284
285#[derive(Debug, Clone)]
286struct HttpHeaderCollection {
287    normalized: BTreeMap<String, Vec<String>>,
288    raw_pairs: Vec<(String, String)>,
289}
290
291#[derive(Debug)]
292struct InsecureTlsVerifier {
293    supported_schemes: Vec<SignatureScheme>,
294}
295
296impl ServerCertVerifier for InsecureTlsVerifier {
297    fn verify_server_cert(
298        &self,
299        _end_entity: &CertificateDer<'_>,
300        _intermediates: &[CertificateDer<'_>],
301        _server_name: &ServerName<'_>,
302        _ocsp_response: &[u8],
303        _now: rustls::pki_types::UnixTime,
304    ) -> Result<ServerCertVerified, rustls::Error> {
305        Ok(ServerCertVerified::assertion())
306    }
307
308    fn verify_tls12_signature(
309        &self,
310        _message: &[u8],
311        _cert: &CertificateDer<'_>,
312        _dss: &DigitallySignedStruct,
313    ) -> Result<HandshakeSignatureValid, rustls::Error> {
314        Ok(HandshakeSignatureValid::assertion())
315    }
316
317    fn verify_tls13_signature(
318        &self,
319        _message: &[u8],
320        _cert: &CertificateDer<'_>,
321        _dss: &DigitallySignedStruct,
322    ) -> Result<HandshakeSignatureValid, rustls::Error> {
323        Ok(HandshakeSignatureValid::assertion())
324    }
325
326    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
327        self.supported_schemes.clone()
328    }
329}
330
331impl ActiveProcess {
332    pub(crate) fn new(
333        kernel_pid: u32,
334        kernel_handle: KernelProcessHandle,
335        runtime: GuestRuntimeKind,
336        execution: ActiveExecution,
337    ) -> Self {
338        Self {
339            kernel_pid,
340            kernel_handle,
341            kernel_stdin_writer_fd: None,
342            runtime,
343            detached: false,
344            execution,
345            guest_cwd: String::from("/"),
346            env: BTreeMap::new(),
347            host_cwd: PathBuf::from("/"),
348            mapped_host_fds: BTreeMap::new(),
349            next_mapped_host_fd: MAPPED_HOST_FD_START,
350            pending_execution_events: VecDeque::new(),
351            pending_self_signal_exit: None,
352            child_processes: BTreeMap::new(),
353            next_child_process_id: 0,
354            http_servers: BTreeMap::new(),
355            pending_http_requests: BTreeMap::new(),
356            http2: Default::default(),
357            tcp_listeners: BTreeMap::new(),
358            next_tcp_listener_id: 0,
359            tcp_sockets: BTreeMap::new(),
360            next_tcp_socket_id: 0,
361            tcp_port_reservations: BTreeMap::new(),
362            next_tcp_port_reservation_id: 0,
363            unix_listeners: BTreeMap::new(),
364            next_unix_listener_id: 0,
365            unix_sockets: BTreeMap::new(),
366            next_unix_socket_id: 0,
367            udp_sockets: BTreeMap::new(),
368            next_udp_socket_id: 0,
369            cipher_sessions: BTreeMap::new(),
370            next_cipher_session_id: 0,
371            diffie_hellman_sessions: BTreeMap::new(),
372            next_diffie_hellman_session_id: 0,
373            sqlite_databases: BTreeMap::new(),
374            next_sqlite_database_id: 0,
375            sqlite_statements: BTreeMap::new(),
376            next_sqlite_statement_id: 0,
377            module_resolution_cache: secure_exec_execution::LocalModuleResolutionCache::default(),
378        }
379    }
380
381    pub(crate) fn queue_pending_execution_event(
382        &mut self,
383        event: ActiveExecutionEvent,
384    ) -> Result<(), SidecarError> {
385        if self.pending_execution_events.len() >= MAX_PROCESS_EVENT_QUEUE {
386            return Err(process_event_queue_overflow_error());
387        }
388        self.pending_execution_events.push_back(event);
389        Ok(())
390    }
391
392    pub(crate) fn with_host_cwd(mut self, host_cwd: PathBuf) -> Self {
393        self.host_cwd = host_cwd;
394        self
395    }
396
397    pub(crate) fn with_guest_cwd(mut self, guest_cwd: String) -> Self {
398        self.guest_cwd = guest_cwd;
399        self
400    }
401
402    pub(crate) fn with_env(mut self, env: BTreeMap<String, String>) -> Self {
403        self.env = env;
404        self
405    }
406
407    pub(crate) fn with_kernel_stdin_writer_fd(mut self, fd: u32) -> Self {
408        self.kernel_stdin_writer_fd = Some(fd);
409        self
410    }
411
412    pub(crate) fn with_detached(mut self, detached: bool) -> Self {
413        self.detached = detached;
414        self
415    }
416
417    pub(crate) fn allocate_mapped_host_fd(&mut self, fd: ActiveMappedHostFd) -> u32 {
418        let handle = self.next_mapped_host_fd;
419        self.next_mapped_host_fd = self
420            .next_mapped_host_fd
421            .checked_add(1)
422            .unwrap_or(MAPPED_HOST_FD_START);
423        self.mapped_host_fds.insert(handle, fd);
424        handle
425    }
426
427    pub(crate) fn mapped_host_fd(&self, fd: u32) -> Option<&ActiveMappedHostFd> {
428        self.mapped_host_fds.get(&fd)
429    }
430
431    pub(crate) fn mapped_host_fd_mut(&mut self, fd: u32) -> Option<&mut ActiveMappedHostFd> {
432        self.mapped_host_fds.get_mut(&fd)
433    }
434
435    pub(crate) fn close_mapped_host_fd(&mut self, fd: u32) -> bool {
436        self.mapped_host_fds.remove(&fd).is_some()
437    }
438
439    pub(crate) fn allocate_child_process_id(&mut self) -> String {
440        self.next_child_process_id += 1;
441        format!("child-{}", self.next_child_process_id)
442    }
443
444    fn allocate_tcp_listener_id(&mut self) -> String {
445        self.next_tcp_listener_id += 1;
446        format!("listener-{}", self.next_tcp_listener_id)
447    }
448
449    fn allocate_tcp_socket_id(&mut self) -> String {
450        self.next_tcp_socket_id += 1;
451        format!("socket-{}", self.next_tcp_socket_id)
452    }
453
454    fn allocate_tcp_port_reservation_id(&mut self) -> String {
455        self.next_tcp_port_reservation_id += 1;
456        format!("tcp-port-reservation-{}", self.next_tcp_port_reservation_id)
457    }
458
459    fn allocate_unix_listener_id(&mut self) -> String {
460        self.next_unix_listener_id += 1;
461        format!("unix-listener-{}", self.next_unix_listener_id)
462    }
463
464    fn allocate_unix_socket_id(&mut self) -> String {
465        self.next_unix_socket_id += 1;
466        format!("unix-socket-{}", self.next_unix_socket_id)
467    }
468
469    fn allocate_udp_socket_id(&mut self) -> String {
470        self.next_udp_socket_id += 1;
471        format!("udp-socket-{}", self.next_udp_socket_id)
472    }
473
474    pub(crate) fn network_resource_counts(&self) -> NetworkResourceCounts {
475        let mut counts = NetworkResourceCounts {
476            sockets: self.http_servers.len()
477                + self.tcp_listeners.len()
478                + self.tcp_sockets.len()
479                + self.unix_listeners.len()
480                + self.unix_sockets.len()
481                + self.udp_sockets.len(),
482            connections: self.tcp_sockets.len() + self.unix_sockets.len(),
483        };
484        if let Ok(http2) = self.http2.shared.lock() {
485            counts.sockets += http2.servers.len() + http2.sessions.len();
486            counts.connections += http2.sessions.len();
487        }
488
489        for child in self.child_processes.values() {
490            let child_counts = child.network_resource_counts();
491            counts.sockets += child_counts.sockets;
492            counts.connections += child_counts.connections;
493        }
494
495        counts
496    }
497
498    fn sidecar_only_network_resource_counts(&self) -> NetworkResourceCounts {
499        let mut counts = NetworkResourceCounts {
500            sockets: self.http_servers.len()
501                + self
502                    .tcp_listeners
503                    .values()
504                    .filter(|listener| listener.kernel_socket_id.is_none())
505                    .count()
506                + self
507                    .tcp_sockets
508                    .values()
509                    .filter(|socket| socket.kernel_socket_id.is_none())
510                    .count()
511                + self.unix_listeners.len()
512                + self.unix_sockets.len()
513                + self
514                    .udp_sockets
515                    .values()
516                    .filter(|socket| socket.kernel_socket_id.is_none())
517                    .count(),
518            connections: self
519                .tcp_sockets
520                .values()
521                .filter(|socket| socket.kernel_socket_id.is_none())
522                .count()
523                + self.unix_sockets.len(),
524        };
525        if let Ok(http2) = self.http2.shared.lock() {
526            counts.sockets += http2.servers.len() + http2.sessions.len();
527            counts.connections += http2.sessions.len();
528        }
529
530        for child in self.child_processes.values() {
531            let child_counts = child.sidecar_only_network_resource_counts();
532            counts.sockets += child_counts.sockets;
533            counts.connections += child_counts.connections;
534        }
535
536        counts
537    }
538}
539
540fn poll_tool_process_event(
541    execution: &ToolExecution,
542) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
543    let event = execution
544        .pending_events
545        .lock()
546        .unwrap_or_else(|poisoned| poisoned.into_inner())
547        .pop_front();
548    if event.is_some() {
549        return Ok(event);
550    }
551    if execution.events_overflowed.load(Ordering::Relaxed) {
552        return Err(process_event_queue_overflow_error());
553    }
554    Ok(None)
555}
556
557fn descendant_pending_execution_event_capacity(
558    root: &ActiveProcess,
559    child_path: &[&str],
560) -> Option<usize> {
561    let mut child = root;
562    for child_process_id in child_path {
563        child = child.child_processes.get(*child_process_id)?;
564    }
565    Some(MAX_PROCESS_EVENT_QUEUE.saturating_sub(child.pending_execution_events.len()))
566}
567
568fn poll_child_execution_after_exit(
569    child: &mut ActiveProcess,
570    wait: Duration,
571) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
572    match child.execution.poll_event_blocking(wait) {
573        Ok(event) => Ok(event),
574        Err(SidecarError::Execution(message))
575            if child.runtime == GuestRuntimeKind::WebAssembly
576                && message == WasmExecutionError::EventChannelClosed.to_string() =>
577        {
578            Ok(None)
579        }
580        Err(error) => Err(error),
581    }
582}
583
584fn closed_javascript_event_channel(message: &str) -> bool {
585    message == "guest JavaScript event channel closed unexpectedly"
586}
587
588fn closed_python_event_channel(message: &str) -> bool {
589    message == "guest Python event channel closed unexpectedly"
590}
591
592fn closed_wasm_event_channel(message: &str) -> bool {
593    message == WasmExecutionError::EventChannelClosed.to_string()
594}
595
596fn missing_vm_error(vm_id: &str) -> SidecarError {
597    SidecarError::InvalidState(format!("VM {vm_id} is no longer active"))
598}
599
600fn missing_process_error(vm_id: &str, process_id: &str) -> SidecarError {
601    SidecarError::InvalidState(format!(
602        "VM {vm_id} no longer has active process {process_id}"
603    ))
604}
605
606fn is_broken_pipe_error(error: &SidecarError) -> bool {
607    matches!(error, SidecarError::Execution(message) if message.contains("Broken pipe") || message.contains("os error 32") || message.contains("EPIPE"))
608}
609
610fn javascript_child_process_gone_error(process_id: &str, child_path: &[&str]) -> SidecarError {
611    let child_label = if child_path.is_empty() {
612        process_id.to_owned()
613    } else {
614        format!("{process_id}/{}", child_path.join("/"))
615    };
616    SidecarError::Execution(format!(
617        "ECHILD: child_process {child_label} is no longer available"
618    ))
619}
620
621fn is_javascript_child_process_gone_error(error: &SidecarError) -> bool {
622    matches!(
623        error,
624        SidecarError::Execution(message) if guest_errno_code(message) == Some("ECHILD")
625    )
626}
627
628fn loopback_tls_transport_registry(
629) -> &'static Mutex<BTreeMap<String, Weak<crate::state::LoopbackTlsTransportPair>>> {
630    static REGISTRY: OnceLock<
631        Mutex<BTreeMap<String, Weak<crate::state::LoopbackTlsTransportPair>>>,
632    > = OnceLock::new();
633    REGISTRY.get_or_init(|| Mutex::new(BTreeMap::new()))
634}
635
636fn loopback_tls_transport_key(
637    vm_id: &str,
638    socket_id: SocketId,
639    peer_socket_id: SocketId,
640) -> String {
641    let (lower, higher) = if socket_id <= peer_socket_id {
642        (socket_id, peer_socket_id)
643    } else {
644        (peer_socket_id, socket_id)
645    };
646    format!("{vm_id}:{lower}:{higher}")
647}
648
649fn loopback_tls_endpoint(
650    vm_id: &str,
651    socket_id: SocketId,
652    peer_socket_id: SocketId,
653) -> Result<crate::state::LoopbackTlsEndpoint, SidecarError> {
654    let key = loopback_tls_transport_key(vm_id, socket_id, peer_socket_id);
655    let registry = loopback_tls_transport_registry();
656    let mut transports = registry.lock().map_err(|_| {
657        SidecarError::InvalidState(String::from(
658            "loopback TLS transport registry lock poisoned",
659        ))
660    })?;
661    transports.retain(|_, pair| pair.strong_count() > 0);
662    let pair = transports
663        .get(&key)
664        .and_then(Weak::upgrade)
665        .unwrap_or_else(|| {
666            let pair = Arc::new(crate::state::LoopbackTlsTransportPair {
667                state: Mutex::new(crate::state::LoopbackTlsTransportPairState::default()),
668                ready: std::sync::Condvar::new(),
669            });
670            transports.insert(key, Arc::downgrade(&pair));
671            pair
672        });
673    Ok(crate::state::LoopbackTlsEndpoint {
674        pair,
675        is_lower_socket: socket_id <= peer_socket_id,
676    })
677}
678
679impl crate::state::LoopbackTlsEndpoint {
680    fn shutdown_write(&self) -> Result<(), SidecarError> {
681        let mut state = self.pair.state.lock().map_err(|_| {
682            SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
683        })?;
684        if self.is_lower_socket {
685            state.lower_write_closed = true;
686        } else {
687            state.higher_write_closed = true;
688        }
689        self.pair.ready.notify_all();
690        Ok(())
691    }
692
693    fn close_endpoint(&self) -> Result<(), SidecarError> {
694        let mut state = self.pair.state.lock().map_err(|_| {
695            SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
696        })?;
697        if self.is_lower_socket {
698            state.lower_write_closed = true;
699            state.lower_closed = true;
700        } else {
701            state.higher_write_closed = true;
702            state.higher_closed = true;
703        }
704        self.pair.ready.notify_all();
705        Ok(())
706    }
707}
708
709fn parse_tls_client_hello_from_bytes(
710    buffer: &[u8],
711) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
712    if buffer.is_empty() {
713        return Ok(None);
714    }
715
716    let mut acceptor = rustls::server::Acceptor::default();
717    let mut cursor = Cursor::new(buffer);
718    acceptor.read_tls(&mut cursor).map_err(sidecar_net_error)?;
719    let Some(accepted) = acceptor.accept().map_err(|(error, _)| {
720        SidecarError::Execution(format!("failed to parse TLS client hello: {error}"))
721    })?
722    else {
723        return Ok(None);
724    };
725    let client_hello = accepted.client_hello();
726    let alpn_protocols = client_hello.alpn().map(|protocols| {
727        protocols
728            .filter_map(|protocol| String::from_utf8(protocol.to_vec()).ok())
729            .collect::<Vec<_>>()
730    });
731    Ok(Some(JavascriptTlsClientHello {
732        servername: client_hello.server_name().map(str::to_owned),
733        alpn_protocols,
734    }))
735}
736
737fn peek_loopback_tls_client_hello(
738    vm_id: &str,
739    socket_id: SocketId,
740    peer_socket_id: SocketId,
741) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
742    let key = loopback_tls_transport_key(vm_id, socket_id, peer_socket_id);
743    let registry = loopback_tls_transport_registry();
744    let pair = registry
745        .lock()
746        .map_err(|_| {
747            SidecarError::InvalidState(String::from(
748                "loopback TLS transport registry lock poisoned",
749            ))
750        })?
751        .get(&key)
752        .and_then(Weak::upgrade);
753    let Some(pair) = pair else {
754        return Ok(None);
755    };
756    let is_lower_socket = socket_id <= peer_socket_id;
757    let state = pair.state.lock().map_err(|_| {
758        SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
759    })?;
760    let buffered = if is_lower_socket {
761        state.higher_to_lower.iter().copied().collect::<Vec<_>>()
762    } else {
763        state.lower_to_higher.iter().copied().collect::<Vec<_>>()
764    };
765    drop(state);
766    parse_tls_client_hello_from_bytes(&buffered)
767}
768
769fn wait_for_loopback_peer_socket_id(
770    kernel: &SidecarKernel,
771    socket_id: SocketId,
772) -> Option<SocketId> {
773    for _ in 0..50 {
774        if let Some(peer_socket_id) = kernel
775            .socket_get(socket_id)
776            .and_then(|record| record.peer_socket_id())
777        {
778            return Some(peer_socket_id);
779        }
780        std::thread::sleep(Duration::from_millis(10));
781    }
782    None
783}
784
785impl Drop for crate::state::LoopbackTlsEndpoint {
786    fn drop(&mut self) {
787        let _ = self.close_endpoint();
788    }
789}
790
791impl Read for crate::state::LoopbackTlsEndpoint {
792    fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
793        let mut state = self
794            .pair
795            .state
796            .lock()
797            .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
798
799        loop {
800            let (peer_write_closed, peer_closed) = if self.is_lower_socket {
801                (state.higher_write_closed, state.higher_closed)
802            } else {
803                (state.lower_write_closed, state.lower_closed)
804            };
805
806            let incoming = if self.is_lower_socket {
807                &mut state.higher_to_lower
808            } else {
809                &mut state.lower_to_higher
810            };
811
812            if !incoming.is_empty() {
813                let mut count = 0;
814                while count < buffer.len() {
815                    let Some(byte) = incoming.pop_front() else {
816                        break;
817                    };
818                    buffer[count] = byte;
819                    count += 1;
820                }
821                return Ok(count);
822            }
823
824            if peer_write_closed || peer_closed {
825                return Ok(0);
826            }
827
828            let (next_state, wait_result) = self
829                .pair
830                .ready
831                .wait_timeout(state, TCP_SOCKET_POLL_TIMEOUT)
832                .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
833            state = next_state;
834            if wait_result.timed_out() {
835                return Err(std::io::Error::new(
836                    std::io::ErrorKind::WouldBlock,
837                    "loopback TLS transport read timed out",
838                ));
839            }
840        }
841    }
842}
843
844impl Write for crate::state::LoopbackTlsEndpoint {
845    fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
846        let mut state = self
847            .pair
848            .state
849            .lock()
850            .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
851
852        let peer_closed = if self.is_lower_socket {
853            state.higher_closed
854        } else {
855            state.lower_closed
856        };
857        let outgoing = if self.is_lower_socket {
858            &mut state.lower_to_higher
859        } else {
860            &mut state.higher_to_lower
861        };
862        if peer_closed {
863            return Err(std::io::Error::new(
864                std::io::ErrorKind::BrokenPipe,
865                "loopback TLS peer is closed",
866            ));
867        }
868
869        outgoing.extend(buffer.iter().copied());
870        self.pair.ready.notify_all();
871        Ok(buffer.len())
872    }
873
874    fn flush(&mut self) -> std::io::Result<()> {
875        Ok(())
876    }
877}
878
879// TCP types moved to crate::state
880
881struct ActiveTcpConnectRequest<'a, B> {
882    bridge: &'a SharedBridge<B>,
883    kernel: &'a mut SidecarKernel,
884    kernel_pid: u32,
885    vm_id: &'a str,
886    dns: &'a VmDnsConfig,
887    host: &'a str,
888    port: u16,
889    local_address: Option<&'a str>,
890    local_port: Option<u16>,
891    local_reservation: Option<(JavascriptSocketFamily, u16)>,
892    context: &'a JavascriptSocketPathContext,
893}
894
895struct ActiveUdpSendToRequest<'a, B> {
896    bridge: &'a SharedBridge<B>,
897    kernel: &'a mut SidecarKernel,
898    kernel_pid: u32,
899    vm_id: &'a str,
900    dns: &'a VmDnsConfig,
901    host: &'a str,
902    port: u16,
903    context: &'a JavascriptSocketPathContext,
904    contents: &'a [u8],
905}
906
907struct UdpRemoteAddrRequest<'a, B> {
908    bridge: &'a SharedBridge<B>,
909    kernel: &'a SidecarKernel,
910    vm_id: &'a str,
911    dns: &'a VmDnsConfig,
912    host: &'a str,
913    port: u16,
914    family: JavascriptUdpFamily,
915    context: &'a JavascriptSocketPathContext,
916}
917
918pub(crate) struct JavascriptSyncRpcServiceRequest<'a, B> {
919    pub(crate) bridge: &'a SharedBridge<B>,
920    pub(crate) vm_id: &'a str,
921    pub(crate) dns: &'a VmDnsConfig,
922    pub(crate) socket_paths: &'a JavascriptSocketPathContext,
923    pub(crate) kernel: &'a mut SidecarKernel,
924    pub(crate) process: &'a mut ActiveProcess,
925    pub(crate) sync_request: &'a JavascriptSyncRpcRequest,
926    pub(crate) resource_limits: &'a ResourceLimits,
927    pub(crate) network_counts: NetworkResourceCounts,
928}
929
930pub(crate) struct JavascriptNetSyncRpcServiceRequest<'a, B> {
931    pub(crate) bridge: &'a SharedBridge<B>,
932    pub(crate) vm_id: &'a str,
933    pub(crate) dns: &'a VmDnsConfig,
934    pub(crate) socket_paths: &'a JavascriptSocketPathContext,
935    pub(crate) kernel: &'a mut SidecarKernel,
936    pub(crate) process: &'a mut ActiveProcess,
937    pub(crate) sync_request: &'a JavascriptSyncRpcRequest,
938    pub(crate) resource_limits: &'a ResourceLimits,
939    pub(crate) network_counts: NetworkResourceCounts,
940}
941
942struct LoopbackHttpResponseWaitRequest<'a, B> {
943    bridge: &'a SharedBridge<B>,
944    vm_id: &'a str,
945    dns: &'a VmDnsConfig,
946    socket_paths: &'a JavascriptSocketPathContext,
947    kernel: &'a mut SidecarKernel,
948    process: &'a mut ActiveProcess,
949    resource_limits: &'a ResourceLimits,
950    request_key: (u64, u64),
951}
952
953pub(crate) struct LoopbackHttpDispatchRequest<'a, B> {
954    pub(crate) bridge: &'a SharedBridge<B>,
955    pub(crate) vm_id: &'a str,
956    pub(crate) dns: &'a VmDnsConfig,
957    pub(crate) socket_paths: &'a JavascriptSocketPathContext,
958    pub(crate) kernel: &'a mut SidecarKernel,
959    pub(crate) process: &'a mut ActiveProcess,
960    pub(crate) resource_limits: &'a ResourceLimits,
961    pub(crate) server_id: u64,
962    pub(crate) request_json: &'a str,
963}
964
965struct JavascriptDgramSyncRpcServiceRequest<'a, B> {
966    bridge: &'a SharedBridge<B>,
967    kernel: &'a mut SidecarKernel,
968    vm_id: &'a str,
969    dns: &'a VmDnsConfig,
970    socket_paths: &'a JavascriptSocketPathContext,
971    process: &'a mut ActiveProcess,
972    sync_request: &'a JavascriptSyncRpcRequest,
973    resource_limits: &'a ResourceLimits,
974    network_counts: NetworkResourceCounts,
975}
976
977struct JavascriptHttp2SyncRpcServiceRequest<'a, B> {
978    bridge: &'a SharedBridge<B>,
979    kernel: &'a mut SidecarKernel,
980    vm_id: &'a str,
981    dns: &'a VmDnsConfig,
982    socket_paths: &'a JavascriptSocketPathContext,
983    process: &'a mut ActiveProcess,
984    sync_request: &'a JavascriptSyncRpcRequest,
985    resource_limits: &'a ResourceLimits,
986    network_counts: NetworkResourceCounts,
987}
988
989impl ActiveTcpSocket {
990    fn connect<B>(request: ActiveTcpConnectRequest<'_, B>) -> Result<Self, SidecarError>
991    where
992        B: NativeSidecarBridge + Send + 'static,
993        BridgeError<B>: fmt::Debug + Send + Sync + 'static,
994    {
995        let ActiveTcpConnectRequest {
996            bridge,
997            kernel,
998            kernel_pid,
999            vm_id,
1000            dns,
1001            host,
1002            port,
1003            local_address,
1004            local_port,
1005            local_reservation,
1006            context,
1007        } = request;
1008        let resolved = resolve_tcp_connect_addr(bridge, kernel, vm_id, dns, host, port, context)?;
1009        if resolved.use_kernel_loopback {
1010            let family = JavascriptSocketFamily::from_ip(resolved.guest_remote_addr.ip());
1011            let requested_local_port = local_port.unwrap_or(0);
1012            let local_port = if requested_local_port != 0
1013                && local_reservation == Some((family, requested_local_port))
1014            {
1015                requested_local_port
1016            } else {
1017                allocate_guest_listen_port(
1018                    requested_local_port,
1019                    family,
1020                    &context.used_tcp_guest_ports,
1021                    context.listen_policy,
1022                )?
1023            };
1024            let local_ip = match (family, local_address) {
1025                (JavascriptSocketFamily::Ipv4, Some("0.0.0.0")) => {
1026                    IpAddr::V4(Ipv4Addr::UNSPECIFIED)
1027                }
1028                (JavascriptSocketFamily::Ipv4, Some("127.0.0.1") | Some("localhost") | None) => {
1029                    IpAddr::V4(Ipv4Addr::LOCALHOST)
1030                }
1031                (JavascriptSocketFamily::Ipv6, Some("::")) => IpAddr::V6(Ipv6Addr::UNSPECIFIED),
1032                (JavascriptSocketFamily::Ipv6, Some("::1") | Some("localhost") | None) => {
1033                    IpAddr::V6(Ipv6Addr::LOCALHOST)
1034                }
1035                (JavascriptSocketFamily::Ipv4, Some(other)) => {
1036                    return Err(SidecarError::Execution(format!(
1037                        "EACCES: TCP sockets must bind to loopback or unspecified addresses, got {other}"
1038                    )));
1039                }
1040                (JavascriptSocketFamily::Ipv6, Some(other)) => {
1041                    return Err(SidecarError::Execution(format!(
1042                        "EACCES: TCP sockets must bind to loopback or unspecified addresses, got {other}"
1043                    )));
1044                }
1045            };
1046            let local_addr = SocketAddr::new(local_ip, local_port);
1047            let spec = match family {
1048                JavascriptSocketFamily::Ipv4 => SocketSpec::tcp(),
1049                JavascriptSocketFamily::Ipv6 => {
1050                    SocketSpec::new(SocketDomain::Inet6, SocketType::Stream)
1051                }
1052            };
1053            let socket_id = kernel
1054                .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
1055                .map_err(kernel_error)?;
1056            kernel
1057                .socket_bind_inet(
1058                    EXECUTION_DRIVER_NAME,
1059                    kernel_pid,
1060                    socket_id,
1061                    InetSocketAddress::new(local_ip.to_string(), local_port),
1062                )
1063                .map_err(kernel_error)?;
1064            kernel
1065                .socket_connect_inet_loopback(
1066                    EXECUTION_DRIVER_NAME,
1067                    kernel_pid,
1068                    socket_id,
1069                    InetSocketAddress::new(
1070                        resolved.guest_remote_addr.ip().to_string(),
1071                        resolved.guest_remote_addr.port(),
1072                    ),
1073                )
1074                .map_err(kernel_error)?;
1075            return Ok(Self::from_kernel(
1076                socket_id,
1077                None,
1078                local_addr,
1079                resolved.guest_remote_addr,
1080            ));
1081        }
1082
1083        let stream = TcpStream::connect_timeout(&resolved.actual_addr, Duration::from_secs(30))
1084            .map_err(sidecar_net_error)?;
1085        let guest_local_addr = stream.local_addr().map_err(sidecar_net_error)?;
1086        Self::from_stream(stream, None, guest_local_addr, resolved.guest_remote_addr)
1087    }
1088
1089    fn from_stream(
1090        stream: TcpStream,
1091        listener_id: Option<String>,
1092        guest_local_addr: SocketAddr,
1093        guest_remote_addr: SocketAddr,
1094    ) -> Result<Self, SidecarError> {
1095        let read_stream = stream.try_clone().map_err(sidecar_net_error)?;
1096        read_stream
1097            .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
1098            .map_err(sidecar_net_error)?;
1099        let stream = Arc::new(Mutex::new(stream));
1100        let pending_read_stream = Arc::new(Mutex::new(Some(read_stream)));
1101        let (sender, events) = mpsc::channel();
1102        let tls_mode = Arc::new(AtomicBool::new(false));
1103        let tls_stream = Arc::new(Mutex::new(None));
1104        let tls_state = Arc::new(Mutex::new(None));
1105        let saw_local_shutdown = Arc::new(AtomicBool::new(false));
1106        let saw_remote_end = Arc::new(AtomicBool::new(false));
1107        let close_notified = Arc::new(AtomicBool::new(false));
1108
1109        Ok(Self {
1110            stream: Some(stream),
1111            pending_read_stream: Some(pending_read_stream),
1112            events: Some(events),
1113            event_sender: Some(sender),
1114            kernel_socket_id: None,
1115            no_delay: false,
1116            keep_alive: false,
1117            keep_alive_initial_delay_secs: None,
1118            guest_local_addr,
1119            guest_remote_addr,
1120            listener_id,
1121            tls_mode,
1122            tls_stream,
1123            tls_state,
1124            saw_local_shutdown,
1125            saw_remote_end,
1126            close_notified,
1127        })
1128    }
1129
1130    fn from_kernel(
1131        socket_id: SocketId,
1132        listener_id: Option<String>,
1133        guest_local_addr: SocketAddr,
1134        guest_remote_addr: SocketAddr,
1135    ) -> Self {
1136        let (sender, events) = mpsc::channel();
1137        Self {
1138            stream: None,
1139            pending_read_stream: None,
1140            events: Some(events),
1141            event_sender: Some(sender),
1142            kernel_socket_id: Some(socket_id),
1143            no_delay: false,
1144            keep_alive: false,
1145            keep_alive_initial_delay_secs: None,
1146            guest_local_addr,
1147            guest_remote_addr,
1148            listener_id,
1149            tls_mode: Arc::new(AtomicBool::new(false)),
1150            tls_stream: Arc::new(Mutex::new(None)),
1151            tls_state: Arc::new(Mutex::new(None)),
1152            saw_local_shutdown: Arc::new(AtomicBool::new(false)),
1153            saw_remote_end: Arc::new(AtomicBool::new(false)),
1154            close_notified: Arc::new(AtomicBool::new(false)),
1155        }
1156    }
1157
1158    fn poll(
1159        &mut self,
1160        kernel: &mut SidecarKernel,
1161        kernel_pid: u32,
1162        wait: Duration,
1163    ) -> Result<Option<JavascriptTcpSocketEvent>, SidecarError> {
1164        if self.tls_mode.load(Ordering::SeqCst) {
1165            self.ensure_tcp_reader()?;
1166            return match self
1167                .events
1168                .as_ref()
1169                .ok_or_else(|| {
1170                    SidecarError::InvalidState(String::from("TCP socket event channel missing"))
1171                })?
1172                .recv_timeout(wait)
1173            {
1174                Ok(event) => Ok(Some(event)),
1175                Err(RecvTimeoutError::Timeout) => Ok(None),
1176                Err(RecvTimeoutError::Disconnected) => Ok(None),
1177            };
1178        }
1179
1180        if let Some(socket_id) = self.kernel_socket_id {
1181            let result = kernel
1182                .poll_targets(
1183                    EXECUTION_DRIVER_NAME,
1184                    kernel_pid,
1185                    vec![PollTargetEntry::socket(
1186                        socket_id,
1187                        POLLIN | POLLHUP | POLLERR,
1188                    )],
1189                    i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
1190                )
1191                .map_err(kernel_error)?;
1192            let revents = result
1193                .targets
1194                .first()
1195                .map(|entry| entry.revents)
1196                .unwrap_or_else(PollEvents::empty);
1197            if revents.is_empty() {
1198                return Ok(None);
1199            }
1200            if revents.intersects(POLLIN) {
1201                return match kernel.socket_read(
1202                    EXECUTION_DRIVER_NAME,
1203                    kernel_pid,
1204                    socket_id,
1205                    64 * 1024,
1206                ) {
1207                    Ok(Some(bytes)) if !bytes.is_empty() => {
1208                        Ok(Some(JavascriptTcpSocketEvent::Data(bytes)))
1209                    }
1210                    Ok(Some(_)) => Ok(Some(JavascriptTcpSocketEvent::Data(Vec::new()))),
1211                    Ok(None) => Ok(Some(JavascriptTcpSocketEvent::End)),
1212                    Err(error) if error.code() == "EAGAIN" => Ok(None),
1213                    Err(error) => Ok(Some(JavascriptTcpSocketEvent::Error {
1214                        code: Some(error.code().to_string()),
1215                        message: error.to_string(),
1216                    })),
1217                };
1218            }
1219            if revents.intersects(POLLHUP) {
1220                return Ok(Some(JavascriptTcpSocketEvent::End));
1221            }
1222            if revents.intersects(POLLERR) {
1223                return Ok(Some(JavascriptTcpSocketEvent::Error {
1224                    code: Some(String::from("EPIPE")),
1225                    message: String::from("kernel TCP socket reported POLLERR"),
1226                }));
1227            }
1228            return Ok(None);
1229        }
1230
1231        self.ensure_tcp_reader()?;
1232        match self
1233            .events
1234            .as_ref()
1235            .ok_or_else(|| {
1236                SidecarError::InvalidState(String::from("TCP socket event channel missing"))
1237            })?
1238            .recv_timeout(wait)
1239        {
1240            Ok(event) => Ok(Some(event)),
1241            Err(RecvTimeoutError::Timeout) => Ok(None),
1242            Err(RecvTimeoutError::Disconnected) => Ok(None),
1243        }
1244    }
1245
1246    fn ensure_tcp_reader(&self) -> Result<(), SidecarError> {
1247        if self.kernel_socket_id.is_some() {
1248            return Ok(());
1249        }
1250        if self.tls_mode.load(Ordering::SeqCst) {
1251            return Ok(());
1252        }
1253        let read_stream = self
1254            .pending_read_stream
1255            .as_ref()
1256            .ok_or_else(|| {
1257                SidecarError::InvalidState(String::from("TCP socket reader handle missing"))
1258            })?
1259            .lock()
1260            .map_err(|_| {
1261                SidecarError::InvalidState(String::from("TCP socket reader lock poisoned"))
1262            })?
1263            .take();
1264        if let Some(read_stream) = read_stream {
1265            spawn_tcp_socket_reader(
1266                read_stream,
1267                self.event_sender
1268                    .as_ref()
1269                    .ok_or_else(|| {
1270                        SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1271                    })?
1272                    .clone(),
1273                Arc::clone(&self.tls_mode),
1274                Arc::clone(&self.saw_local_shutdown),
1275                Arc::clone(&self.saw_remote_end),
1276                Arc::clone(&self.close_notified),
1277            );
1278        }
1279        Ok(())
1280    }
1281
1282    fn socket_info(&self) -> Value {
1283        json!({
1284            "localAddress": self.guest_local_addr.ip().to_string(),
1285            "localPort": self.guest_local_addr.port(),
1286            "localFamily": socket_addr_family(&self.guest_local_addr),
1287            "remoteAddress": self.guest_remote_addr.ip().to_string(),
1288            "remotePort": self.guest_remote_addr.port(),
1289            "remoteFamily": socket_addr_family(&self.guest_remote_addr),
1290        })
1291    }
1292
1293    fn set_no_delay(&mut self, enable: bool) -> Result<(), SidecarError> {
1294        self.no_delay = enable;
1295        if self.kernel_socket_id.is_some() {
1296            return Ok(());
1297        }
1298        let stream = self
1299            .stream
1300            .as_ref()
1301            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1302            .lock()
1303            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1304        stream.set_nodelay(enable).map_err(sidecar_net_error)
1305    }
1306
1307    fn set_keep_alive(
1308        &mut self,
1309        enable: bool,
1310        initial_delay_secs: Option<u64>,
1311    ) -> Result<(), SidecarError> {
1312        self.keep_alive = enable;
1313        self.keep_alive_initial_delay_secs = initial_delay_secs;
1314        if self.kernel_socket_id.is_some() {
1315            return Ok(());
1316        }
1317        let stream = self
1318            .stream
1319            .as_ref()
1320            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1321            .lock()
1322            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1323        let socket = SockRef::from(&*stream);
1324        socket.set_keepalive(enable).map_err(sidecar_net_error)?;
1325        if enable {
1326            if let Some(delay_secs) = initial_delay_secs.filter(|delay_secs| *delay_secs > 0) {
1327                socket
1328                    .set_tcp_keepalive(
1329                        &TcpKeepalive::new().with_time(Duration::from_secs(delay_secs)),
1330                    )
1331                    .map_err(sidecar_net_error)?;
1332            }
1333        }
1334        Ok(())
1335    }
1336
1337    fn upgrade_tls(
1338        &self,
1339        vm_id: &str,
1340        kernel: &SidecarKernel,
1341        options: JavascriptTlsBridgeOptions,
1342    ) -> Result<(), SidecarError> {
1343        if self.tls_mode.load(Ordering::SeqCst) {
1344            return Ok(());
1345        }
1346
1347        let client_hello = if options.is_server {
1348            self.peek_tls_client_hello(vm_id, kernel)?
1349        } else {
1350            None
1351        };
1352
1353        let tls_stream = if let Some(socket_id) = self.kernel_socket_id {
1354            let peer_socket_id = wait_for_loopback_peer_socket_id(kernel, socket_id)
1355                .ok_or_else(|| {
1356                    SidecarError::Execution(format!(
1357                        "ERR_NOT_IMPLEMENTED: kernel-backed loopback socket {socket_id} has no peer for TLS upgrade"
1358                    ))
1359                })?;
1360            let endpoint = loopback_tls_endpoint(vm_id, socket_id, peer_socket_id)?;
1361            if options.is_server {
1362                ActiveTlsStream::LoopbackServer(build_server_loopback_tls_stream(
1363                    endpoint, &options,
1364                )?)
1365            } else {
1366                ActiveTlsStream::LoopbackClient(build_client_loopback_tls_stream(
1367                    endpoint, &options,
1368                )?)
1369            }
1370        } else {
1371            self.pending_read_stream
1372                .as_ref()
1373                .ok_or_else(|| {
1374                    SidecarError::InvalidState(String::from("TCP socket reader handle missing"))
1375                })?
1376                .lock()
1377                .map_err(|_| {
1378                    SidecarError::InvalidState(String::from("TCP socket reader lock poisoned"))
1379                })?
1380                .take();
1381            let stream = self
1382                .stream
1383                .as_ref()
1384                .ok_or_else(|| {
1385                    SidecarError::InvalidState(String::from("TCP socket stream missing"))
1386                })?
1387                .lock()
1388                .map_err(|_| {
1389                    SidecarError::InvalidState(String::from("TCP socket lock poisoned"))
1390                })?;
1391            let cloned = stream.try_clone().map_err(sidecar_net_error)?;
1392            drop(stream);
1393
1394            if options.is_server {
1395                ActiveTlsStream::Server(build_server_tls_stream(cloned, &options)?)
1396            } else {
1397                ActiveTlsStream::Client(build_client_tls_stream(cloned, &options)?)
1398            }
1399        };
1400
1401        let tls_state = ActiveTlsState {
1402            client_hello,
1403            local_certificates: tls_local_certificates(&options)?,
1404            session_reused: false,
1405        };
1406
1407        self.tls_mode.store(true, Ordering::SeqCst);
1408        {
1409            let mut state = self
1410                .tls_state
1411                .lock()
1412                .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?;
1413            *state = Some(tls_state);
1414        }
1415        {
1416            let mut stream = self.tls_stream.lock().map_err(|_| {
1417                SidecarError::InvalidState(String::from("TLS stream lock poisoned"))
1418            })?;
1419            *stream = Some(tls_stream);
1420        }
1421
1422        spawn_tls_socket_reader(
1423            Arc::clone(&self.tls_stream),
1424            self.event_sender
1425                .as_ref()
1426                .ok_or_else(|| {
1427                    SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1428                })?
1429                .clone(),
1430            Arc::clone(&self.saw_local_shutdown),
1431            Arc::clone(&self.saw_remote_end),
1432            Arc::clone(&self.close_notified),
1433        );
1434        Ok(())
1435    }
1436
1437    fn peek_tls_client_hello(
1438        &self,
1439        vm_id: &str,
1440        kernel: &SidecarKernel,
1441    ) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
1442        if let Some(socket_id) = self.kernel_socket_id {
1443            let Some(peer_socket_id) = kernel
1444                .socket_get(socket_id)
1445                .and_then(|record| record.peer_socket_id())
1446            else {
1447                return Ok(None);
1448            };
1449            return peek_loopback_tls_client_hello(vm_id, socket_id, peer_socket_id);
1450        }
1451
1452        let stream = self
1453            .stream
1454            .as_ref()
1455            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1456            .lock()
1457            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1458        let mut buffer = vec![0_u8; 16 * 1024];
1459        let bytes = match stream.peek(&mut buffer) {
1460            Ok(0) => return Ok(None),
1461            Ok(bytes) => bytes,
1462            Err(error)
1463                if matches!(
1464                    error.kind(),
1465                    std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
1466                ) =>
1467            {
1468                return Ok(None);
1469            }
1470            Err(error) => return Err(sidecar_net_error(error)),
1471        };
1472        parse_tls_client_hello_from_bytes(&buffer[..bytes])
1473    }
1474
1475    fn tls_client_hello_json(
1476        &self,
1477        vm_id: &str,
1478        kernel: &SidecarKernel,
1479    ) -> Result<Value, SidecarError> {
1480        if let Some(client_hello) = self
1481            .tls_state
1482            .lock()
1483            .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?
1484            .as_ref()
1485            .and_then(|state| state.client_hello.clone())
1486        {
1487            return javascript_net_json_string(
1488                serde_json::to_value(client_hello).map_err(|error| {
1489                    SidecarError::InvalidState(format!(
1490                        "failed to serialize TLS client hello: {error}"
1491                    ))
1492                })?,
1493                "net.socket_get_tls_client_hello",
1494            );
1495        }
1496
1497        javascript_net_json_string(
1498            serde_json::to_value(
1499                self.peek_tls_client_hello(vm_id, kernel)?
1500                    .unwrap_or_default(),
1501            )
1502            .map_err(|error| {
1503                SidecarError::InvalidState(format!("failed to serialize TLS client hello: {error}"))
1504            })?,
1505            "net.socket_get_tls_client_hello",
1506        )
1507    }
1508
1509    fn tls_query(&self, query: &str, detailed: bool) -> Result<Value, SidecarError> {
1510        let state = self
1511            .tls_state
1512            .lock()
1513            .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?
1514            .clone();
1515        let mut tls_stream = self
1516            .tls_stream
1517            .lock()
1518            .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?;
1519        let Some(stream) = tls_stream.as_mut() else {
1520            return javascript_net_json_string(
1521                tls_bridge_undefined_value(),
1522                "net.socket_tls_query",
1523            );
1524        };
1525
1526        let payload = match query {
1527            "getSession" => tls_bridge_undefined_value(),
1528            "isSessionReused" => Value::Bool(
1529                state
1530                    .as_ref()
1531                    .is_some_and(|tls_state| tls_state.session_reused),
1532            ),
1533            "getPeerCertificate" => {
1534                let certificate = stream
1535                    .peer_certificates()
1536                    .and_then(|certificates| certificates.first())
1537                    .map(|certificate| {
1538                        tls_certificate_bridge_value(certificate.as_ref(), detailed)
1539                    });
1540                certificate.unwrap_or_else(tls_bridge_undefined_value)
1541            }
1542            "getCertificate" => state
1543                .as_ref()
1544                .and_then(|tls_state| tls_state.local_certificates.first())
1545                .map(|certificate| tls_certificate_bridge_value(certificate, detailed))
1546                .unwrap_or_else(tls_bridge_undefined_value),
1547            "getProtocol" => stream
1548                .protocol_version()
1549                .map(tls_protocol_name)
1550                .map(Value::String)
1551                .unwrap_or(Value::Null),
1552            "getCipher" => stream
1553                .negotiated_cipher_suite()
1554                .map(tls_cipher_bridge_value)
1555                .unwrap_or_else(tls_bridge_undefined_value),
1556            other => {
1557                return Err(SidecarError::InvalidState(format!(
1558                    "unsupported TLS query {other}"
1559                )));
1560            }
1561        };
1562        javascript_net_json_string(payload, "net.socket_tls_query")
1563    }
1564
1565    fn write_all(
1566        &self,
1567        kernel: &mut SidecarKernel,
1568        kernel_pid: u32,
1569        contents: &[u8],
1570    ) -> Result<usize, SidecarError> {
1571        if self.tls_mode.load(Ordering::SeqCst) {
1572            let mut tls_stream = self.tls_stream.lock().map_err(|_| {
1573                SidecarError::InvalidState(String::from("TLS stream lock poisoned"))
1574            })?;
1575            let stream = tls_stream.as_mut().ok_or_else(|| {
1576                SidecarError::InvalidState(String::from("TLS stream missing for upgraded socket"))
1577            })?;
1578            stream.write_all(contents)?;
1579            return Ok(contents.len());
1580        }
1581        if let Some(socket_id) = self.kernel_socket_id {
1582            return kernel
1583                .socket_write(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, contents)
1584                .map_err(kernel_error);
1585        }
1586
1587        let mut stream = self
1588            .stream
1589            .as_ref()
1590            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1591            .lock()
1592            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1593        stream.write_all(contents).map_err(sidecar_net_error)?;
1594        Ok(contents.len())
1595    }
1596
1597    fn shutdown_write(
1598        &self,
1599        kernel: &mut SidecarKernel,
1600        kernel_pid: u32,
1601    ) -> Result<(), SidecarError> {
1602        if self.tls_mode.load(Ordering::SeqCst) {
1603            if let Some(stream) = self
1604                .tls_stream
1605                .lock()
1606                .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?
1607                .as_mut()
1608            {
1609                let _ = stream.send_close_notify();
1610                let _ = stream.shutdown_write();
1611            }
1612            if self.kernel_socket_id.is_some() {
1613                self.saw_local_shutdown.store(true, Ordering::SeqCst);
1614                return Ok(());
1615            }
1616        }
1617        if let Some(socket_id) = self.kernel_socket_id {
1618            return kernel
1619                .socket_shutdown(
1620                    EXECUTION_DRIVER_NAME,
1621                    kernel_pid,
1622                    socket_id,
1623                    KernelSocketShutdown::Write,
1624                )
1625                .map_err(kernel_error);
1626        }
1627        let stream = self
1628            .stream
1629            .as_ref()
1630            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1631            .lock()
1632            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1633        self.saw_local_shutdown.store(true, Ordering::SeqCst);
1634        match stream.shutdown(Shutdown::Write) {
1635            Ok(()) => {}
1636            Err(error) if error.kind() == std::io::ErrorKind::NotConnected => {}
1637            Err(error) => return Err(sidecar_net_error(error)),
1638        }
1639        if self.saw_remote_end.load(Ordering::SeqCst)
1640            && !self.close_notified.swap(true, Ordering::SeqCst)
1641        {
1642            let _ = self
1643                .event_sender
1644                .as_ref()
1645                .ok_or_else(|| {
1646                    SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1647                })?
1648                .send(JavascriptTcpSocketEvent::Close { had_error: false });
1649        }
1650        Ok(())
1651    }
1652
1653    fn close(&self, kernel: &mut SidecarKernel, kernel_pid: u32) -> Result<(), SidecarError> {
1654        if self.tls_mode.load(Ordering::SeqCst) {
1655            if let Some(stream) = self
1656                .tls_stream
1657                .lock()
1658                .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?
1659                .as_mut()
1660            {
1661                let _ = stream.send_close_notify();
1662                let _ = stream.close();
1663            }
1664            if self.kernel_socket_id.is_some() {
1665                return Ok(());
1666            }
1667        }
1668        if let Some(socket_id) = self.kernel_socket_id {
1669            return kernel
1670                .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id)
1671                .map_err(kernel_error);
1672        }
1673        let stream = self
1674            .stream
1675            .as_ref()
1676            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1677            .lock()
1678            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1679        stream.shutdown(Shutdown::Both).map_err(sidecar_net_error)
1680    }
1681}
1682
1683impl ActiveTlsStream {
1684    fn write_all(&mut self, contents: &[u8]) -> Result<(), SidecarError> {
1685        match self {
1686            Self::Client(stream) => {
1687                stream.write_all(contents).map_err(sidecar_net_error)?;
1688                stream.flush().map_err(sidecar_net_error)
1689            }
1690            Self::Server(stream) => {
1691                stream.write_all(contents).map_err(sidecar_net_error)?;
1692                stream.flush().map_err(sidecar_net_error)
1693            }
1694            Self::LoopbackClient(stream) => {
1695                stream.write_all(contents).map_err(sidecar_net_error)?;
1696                stream.flush().map_err(sidecar_net_error)
1697            }
1698            Self::LoopbackServer(stream) => {
1699                stream.write_all(contents).map_err(sidecar_net_error)?;
1700                stream.flush().map_err(sidecar_net_error)
1701            }
1702        }
1703    }
1704
1705    fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
1706        match self {
1707            Self::Client(stream) => stream.read(buffer),
1708            Self::Server(stream) => stream.read(buffer),
1709            Self::LoopbackClient(stream) => stream.read(buffer),
1710            Self::LoopbackServer(stream) => stream.read(buffer),
1711        }
1712    }
1713
1714    fn send_close_notify(&mut self) -> Result<(), SidecarError> {
1715        match self {
1716            Self::Client(stream) => {
1717                stream.conn.send_close_notify();
1718                let _ = stream.conn.complete_io(&mut stream.sock);
1719            }
1720            Self::Server(stream) => {
1721                stream.conn.send_close_notify();
1722                let _ = stream.conn.complete_io(&mut stream.sock);
1723            }
1724            Self::LoopbackClient(stream) => {
1725                stream.conn.send_close_notify();
1726                let _ = stream.conn.complete_io(&mut stream.sock);
1727            }
1728            Self::LoopbackServer(stream) => {
1729                stream.conn.send_close_notify();
1730                let _ = stream.conn.complete_io(&mut stream.sock);
1731            }
1732        }
1733        Ok(())
1734    }
1735
1736    fn shutdown_write(&mut self) -> Result<(), SidecarError> {
1737        match self {
1738            Self::Client(stream) => stream
1739                .sock
1740                .shutdown(Shutdown::Write)
1741                .map_err(sidecar_net_error),
1742            Self::Server(stream) => stream
1743                .sock
1744                .shutdown(Shutdown::Write)
1745                .map_err(sidecar_net_error),
1746            Self::LoopbackClient(stream) => stream.sock.shutdown_write(),
1747            Self::LoopbackServer(stream) => stream.sock.shutdown_write(),
1748        }
1749    }
1750
1751    fn close(&mut self) -> Result<(), SidecarError> {
1752        match self {
1753            Self::Client(stream) => stream
1754                .sock
1755                .shutdown(Shutdown::Both)
1756                .map_err(sidecar_net_error),
1757            Self::Server(stream) => stream
1758                .sock
1759                .shutdown(Shutdown::Both)
1760                .map_err(sidecar_net_error),
1761            Self::LoopbackClient(stream) => stream.sock.close_endpoint(),
1762            Self::LoopbackServer(stream) => stream.sock.close_endpoint(),
1763        }
1764    }
1765
1766    fn peer_certificates(&self) -> Option<&[CertificateDer<'static>]> {
1767        match self {
1768            Self::Client(stream) => stream.conn.peer_certificates(),
1769            Self::Server(stream) => stream.conn.peer_certificates(),
1770            Self::LoopbackClient(stream) => stream.conn.peer_certificates(),
1771            Self::LoopbackServer(stream) => stream.conn.peer_certificates(),
1772        }
1773    }
1774
1775    fn negotiated_cipher_suite(&self) -> Option<rustls::SupportedCipherSuite> {
1776        match self {
1777            Self::Client(stream) => stream.conn.negotiated_cipher_suite(),
1778            Self::Server(stream) => stream.conn.negotiated_cipher_suite(),
1779            Self::LoopbackClient(stream) => stream.conn.negotiated_cipher_suite(),
1780            Self::LoopbackServer(stream) => stream.conn.negotiated_cipher_suite(),
1781        }
1782    }
1783
1784    fn protocol_version(&self) -> Option<rustls::ProtocolVersion> {
1785        match self {
1786            Self::Client(stream) => stream.conn.protocol_version(),
1787            Self::Server(stream) => stream.conn.protocol_version(),
1788            Self::LoopbackClient(stream) => stream.conn.protocol_version(),
1789            Self::LoopbackServer(stream) => stream.conn.protocol_version(),
1790        }
1791    }
1792}
1793
1794// ActiveTcpListener moved to crate::state
1795
1796// Unix socket types moved to crate::state
1797
1798impl ActiveUnixSocket {
1799    fn connect(host_path: &Path, guest_path: &str) -> Result<Self, SidecarError> {
1800        let stream = UnixStream::connect(host_path).map_err(sidecar_net_error)?;
1801        Self::from_stream(stream, None, None, Some(guest_path.to_owned()))
1802    }
1803
1804    fn from_stream(
1805        stream: UnixStream,
1806        listener_id: Option<String>,
1807        local_path: Option<String>,
1808        remote_path: Option<String>,
1809    ) -> Result<Self, SidecarError> {
1810        let read_stream = stream.try_clone().map_err(sidecar_net_error)?;
1811        let stream = Arc::new(Mutex::new(stream));
1812        let (sender, events) = mpsc::channel();
1813        let saw_local_shutdown = Arc::new(AtomicBool::new(false));
1814        let saw_remote_end = Arc::new(AtomicBool::new(false));
1815        let close_notified = Arc::new(AtomicBool::new(false));
1816        spawn_unix_socket_reader(
1817            read_stream,
1818            sender.clone(),
1819            Arc::clone(&saw_local_shutdown),
1820            Arc::clone(&saw_remote_end),
1821            Arc::clone(&close_notified),
1822        );
1823
1824        Ok(Self {
1825            stream,
1826            events,
1827            event_sender: sender,
1828            listener_id,
1829            local_path,
1830            remote_path,
1831            saw_local_shutdown,
1832            saw_remote_end,
1833            close_notified,
1834        })
1835    }
1836
1837    fn poll(&mut self, wait: Duration) -> Result<Option<JavascriptTcpSocketEvent>, SidecarError> {
1838        match self.events.recv_timeout(wait) {
1839            Ok(event) => Ok(Some(event)),
1840            Err(RecvTimeoutError::Timeout) => Ok(None),
1841            Err(RecvTimeoutError::Disconnected) => Ok(None),
1842        }
1843    }
1844
1845    fn socket_info(&self) -> Value {
1846        json!({
1847            "localPath": self.local_path.clone(),
1848            "remotePath": self.remote_path.clone(),
1849        })
1850    }
1851
1852    fn write_all(&self, contents: &[u8]) -> Result<usize, SidecarError> {
1853        let mut stream = self
1854            .stream
1855            .lock()
1856            .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
1857        stream.write_all(contents).map_err(sidecar_net_error)?;
1858        Ok(contents.len())
1859    }
1860
1861    fn shutdown_write(&self) -> Result<(), SidecarError> {
1862        let stream = self
1863            .stream
1864            .lock()
1865            .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
1866        self.saw_local_shutdown.store(true, Ordering::SeqCst);
1867        stream
1868            .shutdown(Shutdown::Write)
1869            .map_err(sidecar_net_error)?;
1870        if self.saw_remote_end.load(Ordering::SeqCst)
1871            && !self.close_notified.swap(true, Ordering::SeqCst)
1872        {
1873            let _ = self
1874                .event_sender
1875                .send(JavascriptTcpSocketEvent::Close { had_error: false });
1876        }
1877        Ok(())
1878    }
1879
1880    fn close(&self) -> Result<(), SidecarError> {
1881        let stream = self
1882            .stream
1883            .lock()
1884            .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
1885        stream.shutdown(Shutdown::Both).map_err(sidecar_net_error)
1886    }
1887}
1888
1889// ActiveUnixListener moved to crate::state
1890
1891impl ActiveUnixListener {
1892    fn bind(
1893        host_path: &Path,
1894        guest_path: &str,
1895        backlog: Option<u32>,
1896    ) -> Result<Self, SidecarError> {
1897        if let Some(parent) = host_path.parent() {
1898            fs::create_dir_all(parent).map_err(sidecar_net_error)?;
1899        }
1900        let listener = UnixListener::bind(host_path).map_err(sidecar_net_error)?;
1901        listener.set_nonblocking(true).map_err(sidecar_net_error)?;
1902        Ok(Self {
1903            listener,
1904            path: guest_path.to_owned(),
1905            backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
1906                .expect("default backlog fits within usize"),
1907            active_connection_ids: BTreeSet::new(),
1908        })
1909    }
1910
1911    fn path(&self) -> &str {
1912        &self.path
1913    }
1914
1915    fn poll(
1916        &mut self,
1917        wait: Duration,
1918    ) -> Result<Option<JavascriptUnixListenerEvent>, SidecarError> {
1919        let deadline = Instant::now() + wait;
1920        loop {
1921            match self.listener.accept() {
1922                Ok((stream, remote_addr)) => {
1923                    if self.active_connection_ids.len() >= self.backlog {
1924                        let _ = stream.shutdown(Shutdown::Both);
1925                        if wait.is_zero() || Instant::now() >= deadline {
1926                            return Ok(None);
1927                        }
1928                        continue;
1929                    }
1930
1931                    let local_path = Some(self.path.clone());
1932                    let remote_path = unix_socket_path(&remote_addr);
1933                    return Ok(Some(JavascriptUnixListenerEvent::Connection(
1934                        PendingUnixSocket {
1935                            stream,
1936                            local_path,
1937                            remote_path,
1938                        },
1939                    )));
1940                }
1941                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
1942                    if wait.is_zero() || Instant::now() >= deadline {
1943                        return Ok(None);
1944                    }
1945                    thread::sleep(Duration::from_millis(10));
1946                }
1947                Err(error) => {
1948                    return Ok(Some(JavascriptUnixListenerEvent::Error {
1949                        code: io_error_code(&error),
1950                        message: error.to_string(),
1951                    }));
1952                }
1953            }
1954        }
1955    }
1956
1957    fn close(&self) -> Result<(), SidecarError> {
1958        Ok(())
1959    }
1960
1961    fn active_connection_count(&self) -> usize {
1962        self.active_connection_ids.len()
1963    }
1964
1965    fn register_connection(&mut self, socket_id: &str) {
1966        self.active_connection_ids.insert(socket_id.to_string());
1967    }
1968
1969    fn release_connection(&mut self, socket_id: &str) {
1970        self.active_connection_ids.remove(socket_id);
1971    }
1972}
1973
1974impl ActiveTcpListener {
1975    fn bind(
1976        bind_host: &str,
1977        guest_host: &str,
1978        guest_port: u16,
1979        backlog: Option<u32>,
1980    ) -> Result<Self, SidecarError> {
1981        let bind_addr = resolve_tcp_bind_addr(bind_host, 0)?;
1982        let guest_addr = resolve_tcp_bind_addr(guest_host, guest_port)?;
1983        let listener = TcpListener::bind(bind_addr).map_err(sidecar_net_error)?;
1984        listener.set_nonblocking(true).map_err(sidecar_net_error)?;
1985        let local_addr = listener.local_addr().map_err(sidecar_net_error)?;
1986        Ok(Self {
1987            listener: Some(listener),
1988            kernel_socket_id: None,
1989            local_addr: Some(local_addr),
1990            guest_local_addr: guest_addr,
1991            backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
1992                .expect("default backlog fits within usize"),
1993            active_connection_ids: BTreeSet::new(),
1994        })
1995    }
1996
1997    fn bind_kernel(
1998        kernel: &mut SidecarKernel,
1999        kernel_pid: u32,
2000        guest_host: &str,
2001        guest_port: u16,
2002        backlog: Option<u32>,
2003    ) -> Result<Self, SidecarError> {
2004        let guest_addr = resolve_tcp_bind_addr(guest_host, guest_port)?;
2005        let spec = match guest_addr {
2006            SocketAddr::V4(_) => SocketSpec::tcp(),
2007            SocketAddr::V6(_) => SocketSpec::new(SocketDomain::Inet6, SocketType::Stream),
2008        };
2009        let socket_id = kernel
2010            .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
2011            .map_err(kernel_error)?;
2012        kernel
2013            .socket_bind_inet(
2014                EXECUTION_DRIVER_NAME,
2015                kernel_pid,
2016                socket_id,
2017                InetSocketAddress::new(guest_addr.ip().to_string(), guest_addr.port()),
2018            )
2019            .map_err(kernel_error)?;
2020        kernel
2021            .socket_listen(
2022                EXECUTION_DRIVER_NAME,
2023                kernel_pid,
2024                socket_id,
2025                usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
2026                    .expect("default backlog fits within usize"),
2027            )
2028            .map_err(kernel_error)?;
2029        Ok(Self {
2030            listener: None,
2031            kernel_socket_id: Some(socket_id),
2032            local_addr: Some(guest_addr),
2033            guest_local_addr: guest_addr,
2034            backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
2035                .expect("default backlog fits within usize"),
2036            active_connection_ids: BTreeSet::new(),
2037        })
2038    }
2039
2040    pub(crate) fn local_addr(&self) -> SocketAddr {
2041        self.local_addr.unwrap_or(self.guest_local_addr)
2042    }
2043
2044    fn guest_local_addr(&self) -> SocketAddr {
2045        self.guest_local_addr
2046    }
2047
2048    fn poll(
2049        &mut self,
2050        kernel: &mut SidecarKernel,
2051        kernel_pid: u32,
2052        wait: Duration,
2053    ) -> Result<Option<JavascriptTcpListenerEvent>, SidecarError> {
2054        if let Some(socket_id) = self.kernel_socket_id {
2055            let result = kernel
2056                .poll_targets(
2057                    EXECUTION_DRIVER_NAME,
2058                    kernel_pid,
2059                    vec![PollTargetEntry::socket(socket_id, POLLIN)],
2060                    i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
2061                )
2062                .map_err(kernel_error)?;
2063            let revents = result
2064                .targets
2065                .first()
2066                .map(|entry| entry.revents)
2067                .unwrap_or_else(PollEvents::empty);
2068            if revents.is_empty() {
2069                return Ok(None);
2070            }
2071            let accepted_socket_id =
2072                match kernel.socket_accept(EXECUTION_DRIVER_NAME, kernel_pid, socket_id) {
2073                    Ok(accepted_socket_id) => accepted_socket_id,
2074                    Err(error) if error.code() == "EAGAIN" => return Ok(None),
2075                    Err(error) => {
2076                        return Ok(Some(JavascriptTcpListenerEvent::Error {
2077                            code: Some(error.code().to_string()),
2078                            message: error.to_string(),
2079                        }));
2080                    }
2081                };
2082            let accepted = kernel.socket_get(accepted_socket_id).ok_or_else(|| {
2083                SidecarError::InvalidState(format!(
2084                    "accepted kernel TCP socket {accepted_socket_id} is missing"
2085                ))
2086            })?;
2087            let local_addr = accepted.local_address().ok_or_else(|| {
2088                SidecarError::InvalidState(format!(
2089                    "accepted kernel TCP socket {accepted_socket_id} missing local address"
2090                ))
2091            })?;
2092            let remote_addr = accepted.peer_address().ok_or_else(|| {
2093                SidecarError::InvalidState(format!(
2094                    "accepted kernel TCP socket {accepted_socket_id} missing peer address"
2095                ))
2096            })?;
2097            return Ok(Some(JavascriptTcpListenerEvent::Connection(
2098                PendingTcpSocket {
2099                    stream: None,
2100                    kernel_socket_id: Some(accepted_socket_id),
2101                    preallocated: true,
2102                    guest_local_addr: resolve_tcp_bind_addr(local_addr.host(), local_addr.port())?,
2103                    guest_remote_addr: resolve_tcp_bind_addr(
2104                        remote_addr.host(),
2105                        remote_addr.port(),
2106                    )?,
2107                },
2108            )));
2109        }
2110
2111        let deadline = Instant::now() + wait;
2112        loop {
2113            match self
2114                .listener
2115                .as_ref()
2116                .ok_or_else(|| {
2117                    SidecarError::InvalidState(String::from("TCP listener socket missing"))
2118                })?
2119                .accept()
2120            {
2121                Ok((stream, remote_addr)) => {
2122                    if self.active_connection_ids.len() >= self.backlog {
2123                        let _ = stream.shutdown(Shutdown::Both);
2124                        if wait.is_zero() || Instant::now() >= deadline {
2125                            return Ok(None);
2126                        }
2127                        continue;
2128                    }
2129                    return Ok(Some(JavascriptTcpListenerEvent::Connection(
2130                        PendingTcpSocket {
2131                            stream: Some(stream),
2132                            kernel_socket_id: None,
2133                            preallocated: false,
2134                            guest_local_addr: self.guest_local_addr,
2135                            guest_remote_addr: SocketAddr::new(
2136                                remote_addr.ip(),
2137                                remote_addr.port(),
2138                            ),
2139                        },
2140                    )));
2141                }
2142                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
2143                    if wait.is_zero() || Instant::now() >= deadline {
2144                        return Ok(None);
2145                    }
2146                    thread::sleep(Duration::from_millis(10));
2147                }
2148                Err(error) => {
2149                    return Ok(Some(JavascriptTcpListenerEvent::Error {
2150                        code: io_error_code(&error),
2151                        message: error.to_string(),
2152                    }));
2153                }
2154            }
2155        }
2156    }
2157
2158    fn close(&self, kernel: &mut SidecarKernel, kernel_pid: u32) -> Result<(), SidecarError> {
2159        if let Some(socket_id) = self.kernel_socket_id {
2160            kernel
2161                .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id)
2162                .map_err(kernel_error)?;
2163        }
2164        Ok(())
2165    }
2166
2167    fn active_connection_count(&self) -> usize {
2168        self.active_connection_ids.len()
2169    }
2170
2171    fn register_connection(&mut self, socket_id: &str) {
2172        self.active_connection_ids.insert(socket_id.to_string());
2173    }
2174
2175    fn release_connection(&mut self, socket_id: &str) {
2176        self.active_connection_ids.remove(socket_id);
2177    }
2178}
2179
2180// UDP types moved to crate::state
2181
2182impl ActiveUdpSocket {
2183    fn new(
2184        kernel: &mut SidecarKernel,
2185        kernel_pid: u32,
2186        family: JavascriptUdpFamily,
2187    ) -> Result<Self, SidecarError> {
2188        let spec = match family {
2189            JavascriptUdpFamily::Ipv4 => SocketSpec::udp(),
2190            JavascriptUdpFamily::Ipv6 => SocketSpec::new(SocketDomain::Inet6, SocketType::Datagram),
2191        };
2192        let socket_id = kernel
2193            .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
2194            .map_err(kernel_error)?;
2195        Ok(Self {
2196            family,
2197            socket: None,
2198            kernel_socket_id: Some(socket_id),
2199            guest_local_addr: None,
2200            recv_buffer_size: 0,
2201            send_buffer_size: 0,
2202        })
2203    }
2204
2205    fn local_addr(&self) -> Option<SocketAddr> {
2206        self.guest_local_addr
2207    }
2208
2209    fn socket(&self) -> Result<&UdpSocket, SidecarError> {
2210        self.socket
2211            .as_ref()
2212            .ok_or_else(|| SidecarError::Execution(String::from("EBADF: bad file descriptor")))
2213    }
2214
2215    fn bind(
2216        &mut self,
2217        kernel: &mut SidecarKernel,
2218        kernel_pid: u32,
2219        host: Option<&str>,
2220        port: u16,
2221        context: &JavascriptSocketPathContext,
2222    ) -> Result<SocketAddr, SidecarError> {
2223        if self.socket.is_some() || self.guest_local_addr.is_some() {
2224            return Err(SidecarError::Execution(String::from(
2225                "EINVAL: secure-exec dgram socket is already bound",
2226            )));
2227        }
2228
2229        let (bind_host, guest_host, guest_family) = normalize_udp_bind_host(host, self.family)?;
2230        let guest_port = allocate_guest_listen_port(
2231            port,
2232            guest_family,
2233            &context.used_udp_guest_ports,
2234            context.listen_policy,
2235        )?;
2236        let local_addr = resolve_udp_bind_addr(guest_host, guest_port, self.family)?;
2237        if let Some(socket_id) = self.kernel_socket_id {
2238            kernel
2239                .socket_bind_inet(
2240                    EXECUTION_DRIVER_NAME,
2241                    kernel_pid,
2242                    socket_id,
2243                    InetSocketAddress::new(local_addr.ip().to_string(), local_addr.port()),
2244                )
2245                .map_err(kernel_error)?;
2246        } else {
2247            let bind_addr = resolve_udp_bind_addr(bind_host, 0, self.family)?;
2248            let socket = UdpSocket::bind(bind_addr).map_err(sidecar_net_error)?;
2249            socket.set_nonblocking(true).map_err(sidecar_net_error)?;
2250            self.socket = Some(socket);
2251        }
2252        self.guest_local_addr = Some(local_addr);
2253        Ok(local_addr)
2254    }
2255
2256    fn ensure_bound_for_send(
2257        &mut self,
2258        kernel: &mut SidecarKernel,
2259        kernel_pid: u32,
2260        context: &JavascriptSocketPathContext,
2261    ) -> Result<SocketAddr, SidecarError> {
2262        if let Some(local_addr) = self.local_addr() {
2263            return Ok(local_addr);
2264        }
2265
2266        self.bind(kernel, kernel_pid, None, 0, context)
2267    }
2268
2269    fn send_to<B>(
2270        &mut self,
2271        request: ActiveUdpSendToRequest<'_, B>,
2272    ) -> Result<(usize, SocketAddr), SidecarError>
2273    where
2274        B: NativeSidecarBridge + Send + 'static,
2275        BridgeError<B>: fmt::Debug + Send + Sync + 'static,
2276    {
2277        let ActiveUdpSendToRequest {
2278            bridge,
2279            kernel,
2280            kernel_pid,
2281            vm_id,
2282            dns,
2283            host,
2284            port,
2285            context,
2286            contents,
2287        } = request;
2288        let remote_addr = resolve_udp_addr(UdpRemoteAddrRequest {
2289            bridge,
2290            kernel,
2291            vm_id,
2292            dns,
2293            host,
2294            port,
2295            family: self.family,
2296            context,
2297        })?;
2298        let local_addr = self.ensure_bound_for_send(kernel, kernel_pid, context)?;
2299        let written = if let Some(socket_id) = self.kernel_socket_id {
2300            if is_loopback_ip(remote_addr.ip()) && remote_addr.port() == port {
2301                kernel
2302                    .socket_send_to_inet_loopback(
2303                        EXECUTION_DRIVER_NAME,
2304                        kernel_pid,
2305                        socket_id,
2306                        InetSocketAddress::new(remote_addr.ip().to_string(), remote_addr.port()),
2307                        contents,
2308                    )
2309                    .map_err(kernel_error)?
2310            } else {
2311                return Err(SidecarError::Execution(String::from(
2312                    "ERR_NOT_IMPLEMENTED: external UDP datagrams are not yet supported by the kernel-backed V8 bridge",
2313                )));
2314            }
2315        } else {
2316            let socket = self.socket.as_ref().ok_or_else(|| {
2317                SidecarError::InvalidState(String::from("UDP socket is not initialized"))
2318            })?;
2319            socket
2320                .send_to(contents, remote_addr)
2321                .map_err(sidecar_net_error)?
2322        };
2323        Ok((written, local_addr))
2324    }
2325
2326    fn poll(
2327        &self,
2328        kernel: &mut SidecarKernel,
2329        kernel_pid: u32,
2330        wait: Duration,
2331    ) -> Result<Option<JavascriptUdpSocketEvent>, SidecarError> {
2332        if let Some(socket_id) = self.kernel_socket_id {
2333            let result = kernel
2334                .poll_targets(
2335                    EXECUTION_DRIVER_NAME,
2336                    kernel_pid,
2337                    vec![PollTargetEntry::socket(socket_id, POLLIN)],
2338                    i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
2339                )
2340                .map_err(kernel_error)?;
2341            let revents = result
2342                .targets
2343                .first()
2344                .map(|entry| entry.revents)
2345                .unwrap_or_else(PollEvents::empty);
2346            if revents.is_empty() {
2347                return Ok(None);
2348            }
2349            return match kernel.socket_recv_datagram(
2350                EXECUTION_DRIVER_NAME,
2351                kernel_pid,
2352                socket_id,
2353                64 * 1024,
2354            ) {
2355                Ok(Some(datagram)) => {
2356                    let (source_address, payload) = datagram.into_parts();
2357                    let remote_addr = source_address
2358                        .map(|source| {
2359                            resolve_udp_bind_addr(source.host(), source.port(), self.family)
2360                        })
2361                        .transpose()?
2362                        .unwrap_or_else(|| match self.family {
2363                            JavascriptUdpFamily::Ipv4 => {
2364                                SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)
2365                            }
2366                            JavascriptUdpFamily::Ipv6 => {
2367                                SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0)
2368                            }
2369                        });
2370                    Ok(Some(JavascriptUdpSocketEvent::Message {
2371                        data: payload,
2372                        remote_addr,
2373                    }))
2374                }
2375                Ok(None) => Ok(None),
2376                Err(error) if error.code() == "EAGAIN" => Ok(None),
2377                Err(error) => Ok(Some(JavascriptUdpSocketEvent::Error {
2378                    code: Some(error.code().to_string()),
2379                    message: error.to_string(),
2380                })),
2381            };
2382        }
2383        let socket = self.socket()?;
2384        let deadline = Instant::now() + wait;
2385        let mut buffer = vec![0_u8; 64 * 1024];
2386
2387        loop {
2388            match socket.recv_from(&mut buffer) {
2389                Ok((bytes_read, remote_addr)) => {
2390                    return Ok(Some(JavascriptUdpSocketEvent::Message {
2391                        data: buffer[..bytes_read].to_vec(),
2392                        remote_addr,
2393                    }));
2394                }
2395                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
2396                    if wait.is_zero() || Instant::now() >= deadline {
2397                        return Ok(None);
2398                    }
2399                    thread::sleep(Duration::from_millis(10));
2400                }
2401                Err(error) => {
2402                    return Ok(Some(JavascriptUdpSocketEvent::Error {
2403                        code: io_error_code(&error),
2404                        message: error.to_string(),
2405                    }));
2406                }
2407            }
2408        }
2409    }
2410
2411    fn close(&mut self, kernel: &mut SidecarKernel, kernel_pid: u32) {
2412        if let Some(socket_id) = self.kernel_socket_id {
2413            let _ = kernel.socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id);
2414        }
2415        self.socket.take();
2416        self.guest_local_addr = None;
2417    }
2418
2419    fn set_buffer_size(&mut self, which: &str, size: usize) -> Result<(), SidecarError> {
2420        match which {
2421            "recv" => self.recv_buffer_size = size,
2422            "send" => self.send_buffer_size = size,
2423            other => {
2424                return Err(SidecarError::InvalidState(format!(
2425                    "unsupported UDP buffer size kind {other}"
2426                )));
2427            }
2428        }
2429        if self.kernel_socket_id.is_some() {
2430            return Ok(());
2431        }
2432        let socket = self.socket()?;
2433        let socket = SockRef::from(socket);
2434        match which {
2435            "recv" => socket.set_recv_buffer_size(size).map_err(sidecar_net_error),
2436            "send" => socket.set_send_buffer_size(size).map_err(sidecar_net_error),
2437            other => Err(SidecarError::InvalidState(format!(
2438                "unsupported UDP buffer size kind {other}"
2439            ))),
2440        }
2441    }
2442
2443    fn get_buffer_size(&self, which: &str) -> Result<usize, SidecarError> {
2444        if self.kernel_socket_id.is_some() {
2445            return Ok(match which {
2446                "recv" => self.recv_buffer_size,
2447                "send" => self.send_buffer_size,
2448                other => {
2449                    return Err(SidecarError::InvalidState(format!(
2450                        "unsupported UDP buffer size kind {other}"
2451                    )));
2452                }
2453            });
2454        }
2455        let socket = self.socket()?;
2456        let socket = SockRef::from(socket);
2457        match which {
2458            "recv" => socket.recv_buffer_size().map_err(sidecar_net_error),
2459            "send" => socket.send_buffer_size().map_err(sidecar_net_error),
2460            other => Err(SidecarError::InvalidState(format!(
2461                "unsupported UDP buffer size kind {other}"
2462            ))),
2463        }
2464    }
2465}
2466
2467// ActiveExecution, ActiveExecutionEvent, SocketQueryKind moved to crate::state
2468
2469impl ActiveExecution {
2470    pub(crate) fn uses_shared_v8_runtime(&self) -> bool {
2471        match self {
2472            Self::Javascript(execution) => execution.uses_shared_v8_runtime(),
2473            Self::Python(execution) => execution.uses_shared_v8_runtime(),
2474            Self::Wasm(execution) => execution.uses_shared_v8_runtime(),
2475            Self::Tool(_) => false,
2476        }
2477    }
2478
2479    pub(crate) fn child_pid(&self) -> u32 {
2480        match self {
2481            Self::Javascript(execution) => execution.child_pid(),
2482            Self::Python(execution) => execution.child_pid(),
2483            Self::Wasm(execution) => execution.child_pid(),
2484            Self::Tool(_) => 0,
2485        }
2486    }
2487
2488    pub(crate) fn write_stdin(&mut self, chunk: &[u8]) -> Result<(), SidecarError> {
2489        match self {
2490            Self::Javascript(execution) => execution
2491                .write_stdin(chunk)
2492                .map_err(|error| SidecarError::Execution(error.to_string())),
2493            Self::Python(execution) => execution
2494                .write_stdin(chunk)
2495                .map_err(|error| SidecarError::Execution(error.to_string())),
2496            Self::Wasm(execution) => execution
2497                .write_stdin(chunk)
2498                .map_err(|error| SidecarError::Execution(error.to_string())),
2499            Self::Tool(_) => Ok(()),
2500        }
2501    }
2502
2503    pub(crate) fn close_stdin(&mut self) -> Result<(), SidecarError> {
2504        match self {
2505            Self::Javascript(execution) => execution
2506                .close_stdin()
2507                .map_err(|error| SidecarError::Execution(error.to_string())),
2508            Self::Python(execution) => execution
2509                .close_stdin()
2510                .map_err(|error| SidecarError::Execution(error.to_string())),
2511            Self::Wasm(execution) => execution
2512                .close_stdin()
2513                .map_err(|error| SidecarError::Execution(error.to_string())),
2514            Self::Tool(_) => Ok(()),
2515        }
2516    }
2517
2518    pub(crate) fn respond_python_vfs_rpc_success(
2519        &mut self,
2520        id: u64,
2521        payload: PythonVfsRpcResponsePayload,
2522    ) -> Result<(), SidecarError> {
2523        match self {
2524            Self::Python(execution) => execution
2525                .respond_vfs_rpc_success(id, payload)
2526                .map_err(|error| SidecarError::Execution(error.to_string())),
2527            _ => Err(SidecarError::InvalidState(String::from(
2528                "only Python executions can service Python VFS RPC responses",
2529            ))),
2530        }
2531    }
2532
2533    pub(crate) fn respond_python_vfs_rpc_error(
2534        &mut self,
2535        id: u64,
2536        code: impl Into<String>,
2537        message: impl Into<String>,
2538    ) -> Result<(), SidecarError> {
2539        match self {
2540            Self::Python(execution) => execution
2541                .respond_vfs_rpc_error(id, code, message)
2542                .map_err(|error| SidecarError::Execution(error.to_string())),
2543            _ => Err(SidecarError::InvalidState(String::from(
2544                "only Python executions can service Python VFS RPC responses",
2545            ))),
2546        }
2547    }
2548
2549    pub(crate) fn send_javascript_stream_event(
2550        &self,
2551        event_type: &str,
2552        payload: Value,
2553    ) -> Result<(), SidecarError> {
2554        match self {
2555            Self::Javascript(execution) => execution
2556                .send_stream_event(event_type, payload)
2557                .map_err(|error| SidecarError::Execution(error.to_string())),
2558            Self::Wasm(execution) => execution
2559                .send_stream_event(event_type, payload)
2560                .map_err(|error| SidecarError::Execution(error.to_string())),
2561            _ => Err(SidecarError::InvalidState(String::from(
2562                "only embedded V8 executions can receive JavaScript stream events",
2563            ))),
2564        }
2565    }
2566
2567    pub(crate) fn javascript_v8_session_handle(&self) -> Option<V8SessionHandle> {
2568        match self {
2569            Self::Javascript(execution) => Some(execution.v8_session_handle()),
2570            Self::Wasm(execution) => Some(execution.v8_session_handle()),
2571            _ => None,
2572        }
2573    }
2574
2575    pub(crate) fn terminate(&mut self) -> Result<(), SidecarError> {
2576        match self {
2577            Self::Javascript(execution) => execution
2578                .terminate()
2579                .map_err(|error| SidecarError::Execution(error.to_string())),
2580            Self::Python(execution) => execution
2581                .kill()
2582                .map_err(|error| SidecarError::Execution(error.to_string())),
2583            Self::Wasm(execution) => execution
2584                .terminate()
2585                .map_err(|error| SidecarError::Execution(error.to_string())),
2586            Self::Tool(_) => Ok(()),
2587        }
2588    }
2589
2590    pub(crate) fn respond_javascript_sync_rpc_success(
2591        &mut self,
2592        id: u64,
2593        result: Value,
2594    ) -> Result<(), SidecarError> {
2595        match self {
2596            Self::Javascript(execution) => execution
2597                .respond_sync_rpc_success(id, result)
2598                .map_err(|error| SidecarError::Execution(error.to_string())),
2599            Self::Python(execution) => execution
2600                .respond_javascript_sync_rpc_success(id, result)
2601                .map_err(|error| SidecarError::Execution(error.to_string())),
2602            Self::Wasm(execution) => execution
2603                .respond_sync_rpc_success(id, result)
2604                .map_err(|error| SidecarError::Execution(error.to_string())),
2605            _ => Err(SidecarError::InvalidState(String::from(
2606                "only JavaScript, Python, and WebAssembly executions can service JavaScript sync RPC responses",
2607            ))),
2608        }
2609    }
2610
2611    pub(crate) fn respond_javascript_sync_rpc_error(
2612        &mut self,
2613        id: u64,
2614        code: impl Into<String>,
2615        message: impl Into<String>,
2616    ) -> Result<(), SidecarError> {
2617        match self {
2618            Self::Javascript(execution) => execution
2619                .respond_sync_rpc_error(id, code, message)
2620                .map_err(|error| SidecarError::Execution(error.to_string())),
2621            Self::Python(execution) => execution
2622                .respond_javascript_sync_rpc_error(id, code, message)
2623                .map_err(|error| SidecarError::Execution(error.to_string())),
2624            Self::Wasm(execution) => execution
2625                .respond_sync_rpc_error(id, code, message)
2626                .map_err(|error| SidecarError::Execution(error.to_string())),
2627            _ => Err(SidecarError::InvalidState(String::from(
2628                "only JavaScript, Python, and WebAssembly executions can service JavaScript sync RPC responses",
2629            ))),
2630        }
2631    }
2632
2633    pub(crate) async fn poll_event(
2634        &mut self,
2635        timeout: Duration,
2636    ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
2637        match self {
2638            Self::Javascript(execution) => execution
2639                .poll_event(timeout)
2640                .await
2641                .map(|event| {
2642                    event.map(|event| match event {
2643                        JavascriptExecutionEvent::Stdout(chunk) => {
2644                            ActiveExecutionEvent::Stdout(chunk)
2645                        }
2646                        JavascriptExecutionEvent::Stderr(chunk) => {
2647                            ActiveExecutionEvent::Stderr(chunk)
2648                        }
2649                        JavascriptExecutionEvent::SyncRpcRequest(request) => {
2650                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2651                        }
2652                        JavascriptExecutionEvent::SignalState {
2653                            signal,
2654                            registration,
2655                        } => ActiveExecutionEvent::SignalState {
2656                            signal,
2657                            registration: map_node_signal_registration(registration),
2658                        },
2659                        JavascriptExecutionEvent::Exited(code) => {
2660                            ActiveExecutionEvent::Exited(code)
2661                        }
2662                    })
2663                })
2664                .map_err(|error| SidecarError::Execution(error.to_string())),
2665            Self::Python(execution) => execution
2666                .poll_event(timeout)
2667                .await
2668                .map(|event| {
2669                    event.map(|event| match event {
2670                        PythonExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2671                        PythonExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2672                        PythonExecutionEvent::JavascriptSyncRpcRequest(request) => {
2673                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2674                        }
2675                        PythonExecutionEvent::VfsRpcRequest(request) => {
2676                            ActiveExecutionEvent::PythonVfsRpcRequest(request)
2677                        }
2678                        PythonExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2679                    })
2680                })
2681                .map_err(|error| SidecarError::Execution(error.to_string())),
2682            Self::Wasm(execution) => execution
2683                .poll_event(timeout)
2684                .await
2685                .map(|event| {
2686                    event.map(|event| match event {
2687                        WasmExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2688                        WasmExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2689                        WasmExecutionEvent::SyncRpcRequest(request) => {
2690                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2691                        }
2692                        WasmExecutionEvent::SignalState {
2693                            signal,
2694                            registration,
2695                        } => ActiveExecutionEvent::SignalState {
2696                            signal,
2697                            registration: map_wasm_signal_registration(registration),
2698                        },
2699                        WasmExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2700                    })
2701                })
2702                .map_err(|error| SidecarError::Execution(error.to_string())),
2703            Self::Tool(execution) => {
2704                let _ = timeout;
2705                poll_tool_process_event(execution)
2706            }
2707        }
2708    }
2709
2710    pub(crate) fn poll_event_blocking(
2711        &mut self,
2712        timeout: Duration,
2713    ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
2714        match self {
2715            Self::Javascript(execution) => execution
2716                .poll_event_blocking(timeout)
2717                .map(|event| {
2718                    event.map(|event| match event {
2719                        JavascriptExecutionEvent::Stdout(chunk) => {
2720                            ActiveExecutionEvent::Stdout(chunk)
2721                        }
2722                        JavascriptExecutionEvent::Stderr(chunk) => {
2723                            ActiveExecutionEvent::Stderr(chunk)
2724                        }
2725                        JavascriptExecutionEvent::SyncRpcRequest(request) => {
2726                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2727                        }
2728                        JavascriptExecutionEvent::SignalState {
2729                            signal,
2730                            registration,
2731                        } => ActiveExecutionEvent::SignalState {
2732                            signal,
2733                            registration: map_node_signal_registration(registration),
2734                        },
2735                        JavascriptExecutionEvent::Exited(code) => {
2736                            ActiveExecutionEvent::Exited(code)
2737                        }
2738                    })
2739                })
2740                .map_err(|error| SidecarError::Execution(error.to_string())),
2741            Self::Python(execution) => execution
2742                .poll_event_blocking(timeout)
2743                .map(|event| {
2744                    event.map(|event| match event {
2745                        PythonExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2746                        PythonExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2747                        PythonExecutionEvent::JavascriptSyncRpcRequest(request) => {
2748                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2749                        }
2750                        PythonExecutionEvent::VfsRpcRequest(request) => {
2751                            ActiveExecutionEvent::PythonVfsRpcRequest(request)
2752                        }
2753                        PythonExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2754                    })
2755                })
2756                .map_err(|error| SidecarError::Execution(error.to_string())),
2757            Self::Wasm(execution) => execution
2758                .poll_event_blocking(timeout)
2759                .map(|event| {
2760                    event.map(|event| match event {
2761                        WasmExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2762                        WasmExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2763                        WasmExecutionEvent::SyncRpcRequest(request) => {
2764                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2765                        }
2766                        WasmExecutionEvent::SignalState {
2767                            signal,
2768                            registration,
2769                        } => ActiveExecutionEvent::SignalState {
2770                            signal,
2771                            registration: map_wasm_signal_registration(registration),
2772                        },
2773                        WasmExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2774                    })
2775                })
2776                .map_err(|error| SidecarError::Execution(error.to_string())),
2777            Self::Tool(execution) => {
2778                let _ = timeout;
2779                poll_tool_process_event(execution)
2780            }
2781        }
2782    }
2783}
2784
2785struct ToolProcessEventRequest {
2786    sidecar_requests: SharedSidecarRequestClient,
2787    connection_id: String,
2788    session_id: String,
2789    vm_id: String,
2790    tool_resolution: ToolCommandResolution,
2791    cancelled: Arc<AtomicBool>,
2792    pending_events: Arc<Mutex<VecDeque<ActiveExecutionEvent>>>,
2793    events_overflowed: Arc<AtomicBool>,
2794}
2795
2796pub(crate) fn send_tool_process_event(
2797    pending_events: &Arc<Mutex<VecDeque<ActiveExecutionEvent>>>,
2798    events_overflowed: &AtomicBool,
2799    event: ActiveExecutionEvent,
2800) -> bool {
2801    let mut pending_events = pending_events
2802        .lock()
2803        .unwrap_or_else(|poisoned| poisoned.into_inner());
2804    if pending_events.len() >= MAX_PROCESS_EVENT_QUEUE {
2805        events_overflowed.store(true, Ordering::Relaxed);
2806        return false;
2807    }
2808    pending_events.push_back(event);
2809    true
2810}
2811
2812fn spawn_tool_process_events(request: ToolProcessEventRequest) {
2813    let ToolProcessEventRequest {
2814        sidecar_requests,
2815        connection_id,
2816        session_id,
2817        vm_id,
2818        tool_resolution,
2819        cancelled,
2820        pending_events,
2821        events_overflowed,
2822    } = request;
2823    std::thread::spawn(move || match tool_resolution {
2824        ToolCommandResolution::Failure(message) => {
2825            if !send_tool_process_event(
2826                &pending_events,
2827                &events_overflowed,
2828                ActiveExecutionEvent::Stderr(format_tool_failure_output(&message)),
2829            ) {
2830                return;
2831            }
2832            let _ = send_tool_process_event(
2833                &pending_events,
2834                &events_overflowed,
2835                ActiveExecutionEvent::Exited(1),
2836            );
2837        }
2838        ToolCommandResolution::Invoke { request, timeout } => {
2839            let response = sidecar_requests.invoke(
2840                OwnershipScope::vm(connection_id.clone(), session_id.clone(), vm_id.clone()),
2841                SidecarRequestPayload::HostCallback(request.clone()),
2842                timeout,
2843            );
2844            if cancelled.load(Ordering::Relaxed) {
2845                return;
2846            }
2847
2848            match response {
2849                Ok(crate::protocol::SidecarResponsePayload::HostCallbackResult(result)) => {
2850                    if let Some(value) = result.result {
2851                        let value: serde_json::Value = serde_json::from_str(&value)
2852                            .unwrap_or(serde_json::Value::String(value));
2853                        let stdout = serde_json::to_vec(&json!({
2854                            "ok": true,
2855                            "result": value,
2856                        }))
2857                        .unwrap_or_else(|error| {
2858                            format_tool_failure_output(&format!(
2859                                "failed to serialize tool result: {error}"
2860                            ))
2861                        });
2862                        if !send_tool_process_event(
2863                            &pending_events,
2864                            &events_overflowed,
2865                            ActiveExecutionEvent::Stdout(stdout),
2866                        ) {
2867                            return;
2868                        }
2869                        let _ = send_tool_process_event(
2870                            &pending_events,
2871                            &events_overflowed,
2872                            ActiveExecutionEvent::Exited(0),
2873                        );
2874                    } else {
2875                        let message = result
2876                            .error
2877                            .unwrap_or_else(|| String::from("tool invocation returned no result"));
2878                        if !send_tool_process_event(
2879                            &pending_events,
2880                            &events_overflowed,
2881                            ActiveExecutionEvent::Stderr(format_tool_failure_output(&message)),
2882                        ) {
2883                            return;
2884                        }
2885                        let _ = send_tool_process_event(
2886                            &pending_events,
2887                            &events_overflowed,
2888                            ActiveExecutionEvent::Exited(1),
2889                        );
2890                    }
2891                }
2892                Ok(_) => {
2893                    if !send_tool_process_event(
2894                        &pending_events,
2895                        &events_overflowed,
2896                        ActiveExecutionEvent::Stderr(format_tool_failure_output(
2897                            "unexpected sidecar tool response",
2898                        )),
2899                    ) {
2900                        return;
2901                    }
2902                    let _ = send_tool_process_event(
2903                        &pending_events,
2904                        &events_overflowed,
2905                        ActiveExecutionEvent::Exited(1),
2906                    );
2907                }
2908                Err(error) => {
2909                    if !send_tool_process_event(
2910                        &pending_events,
2911                        &events_overflowed,
2912                        ActiveExecutionEvent::Stderr(format_tool_failure_output(
2913                            &error.to_string(),
2914                        )),
2915                    ) {
2916                        return;
2917                    }
2918                    let _ = send_tool_process_event(
2919                        &pending_events,
2920                        &events_overflowed,
2921                        ActiveExecutionEvent::Exited(1),
2922                    );
2923                }
2924            }
2925        }
2926    });
2927}
2928
2929impl<B> NativeSidecar<B>
2930where
2931    B: NativeSidecarBridge + Send + 'static,
2932    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
2933{
2934    pub(crate) async fn execute(
2935        &mut self,
2936        request: &RequestFrame,
2937        payload: ExecuteRequest,
2938    ) -> Result<DispatchResult, SidecarError> {
2939        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
2940        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
2941
2942        let vm = self
2943            .vms
2944            .get_mut(&vm_id)
2945            .ok_or_else(|| missing_vm_error(&vm_id))?;
2946        if vm.active_processes.contains_key(&payload.process_id) {
2947            return Err(SidecarError::InvalidState(format!(
2948                "VM {vm_id} already has an active process with id {}",
2949                payload.process_id
2950            )));
2951        }
2952
2953        if let Some(command) = payload.command.as_deref() {
2954            if let Some(tool_resolution) =
2955                resolve_tool_command(vm, command, &payload.args, payload.cwd.as_deref())?
2956            {
2957                let guest_cwd = payload
2958                    .cwd
2959                    .as_deref()
2960                    .map(normalize_path)
2961                    .unwrap_or_else(|| vm.guest_cwd.clone());
2962                let kernel_handle = vm
2963                    .kernel
2964                    .create_virtual_process(
2965                        EXECUTION_DRIVER_NAME,
2966                        TOOL_DRIVER_NAME,
2967                        command,
2968                        std::iter::once(command.to_owned())
2969                            .chain(payload.args.iter().cloned())
2970                            .collect(),
2971                        VirtualProcessOptions {
2972                            env: vm.guest_env.clone(),
2973                            cwd: Some(guest_cwd.clone()),
2974                            ..VirtualProcessOptions::default()
2975                        },
2976                    )
2977                    .map_err(kernel_error)?;
2978                let kernel_pid = kernel_handle.pid();
2979                let tool_execution = ToolExecution::default();
2980                let cancelled = tool_execution.cancelled.clone();
2981                let pending_events = tool_execution.pending_events.clone();
2982                let events_overflowed = tool_execution.events_overflowed.clone();
2983                vm.active_processes.insert(
2984                    payload.process_id.clone(),
2985                    ActiveProcess::new(
2986                        kernel_pid,
2987                        kernel_handle,
2988                        GuestRuntimeKind::JavaScript,
2989                        ActiveExecution::Tool(tool_execution),
2990                    )
2991                    .with_guest_cwd(guest_cwd.clone())
2992                    .with_host_cwd(resolve_vm_guest_path_to_host(vm, &guest_cwd)),
2993                );
2994                self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy)?;
2995                spawn_tool_process_events(ToolProcessEventRequest {
2996                    sidecar_requests: self.sidecar_requests.clone(),
2997                    connection_id: connection_id.clone(),
2998                    session_id: session_id.clone(),
2999                    vm_id: vm_id.clone(),
3000                    tool_resolution,
3001                    cancelled,
3002                    pending_events,
3003                    events_overflowed,
3004                });
3005
3006                return Ok(DispatchResult {
3007                    response: self.respond(
3008                        request,
3009                        ResponsePayload::ProcessStarted(ProcessStartedResponse {
3010                            process_id: payload.process_id,
3011                            pid: Some(kernel_pid),
3012                        }),
3013                    ),
3014                    events: Vec::new(),
3015                });
3016            }
3017        }
3018
3019        let resolved = resolve_execute_request(vm, &payload)?;
3020        let mut env = resolved.env.clone();
3021        let sandbox_root = normalize_host_path(&vm.cwd);
3022        env.insert(
3023            String::from(EXECUTION_SANDBOX_ROOT_ENV),
3024            sandbox_root.to_string_lossy().into_owned(),
3025        );
3026        if resolved.runtime == GuestRuntimeKind::JavaScript {
3027            env.insert(
3028                String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
3029                String::from("1"),
3030            );
3031        } else if resolved.runtime == GuestRuntimeKind::WebAssembly {
3032            env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
3033        }
3034        let argv = std::iter::once(resolved.entrypoint.clone())
3035            .chain(resolved.execution_args.iter().cloned())
3036            .collect::<Vec<_>>();
3037        let kernel_handle = vm
3038            .kernel
3039            .spawn_process(
3040                &resolved.command,
3041                argv,
3042                SpawnOptions {
3043                    requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
3044                    cwd: Some(resolved.guest_cwd.clone()),
3045                    ..SpawnOptions::default()
3046                },
3047            )
3048            .map_err(kernel_error)?;
3049        let kernel_pid = kernel_handle.pid();
3050
3051        let (execution, process_env) = match resolved.runtime {
3052            GuestRuntimeKind::JavaScript => {
3053                let inline_code = load_javascript_entrypoint_source(
3054                    vm,
3055                    &resolved.host_cwd,
3056                    &resolved.entrypoint,
3057                    &env,
3058                );
3059                prepare_javascript_shadow(vm, &resolved)?;
3060
3061                let context =
3062                    self.javascript_engine
3063                        .create_context(CreateJavascriptContextRequest {
3064                            vm_id: vm_id.clone(),
3065                            bootstrap_module: None,
3066                            compile_cache_root: Some(self.cache_root.join("node-compile-cache")),
3067                        });
3068                let built_reader = build_module_reader(vm, &resolved);
3069                let guest_reader = built_reader.clone().map(|reader| {
3070                    Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader))
3071                        as Box<dyn GuestModuleReader>
3072                });
3073                let module_reader =
3074                    built_reader.map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
3075                let execution = self
3076                    .javascript_engine
3077                    .start_execution_with_module_reader(
3078                        StartJavascriptExecutionRequest {
3079                            guest_runtime: guest_runtime_identity(vm, None, None),
3080                            vm_id: vm_id.clone(),
3081                            context_id: context.context_id,
3082                            argv: std::iter::once(resolved.entrypoint.clone())
3083                                .chain(resolved.execution_args.iter().cloned())
3084                                .collect(),
3085                            env: env.clone(),
3086                            cwd: resolved.host_cwd.clone(),
3087                            limits: javascript_execution_limits(vm),
3088                            inline_code,
3089                        },
3090                        module_reader,
3091                        guest_reader,
3092                    )
3093                    .map_err(javascript_error)?;
3094                (ActiveExecution::Javascript(execution), env.clone())
3095            }
3096            GuestRuntimeKind::Python => {
3097                let python_file_path = python_file_entrypoint(&resolved.entrypoint);
3098                let pyodide_dist_path = self
3099                    .python_engine
3100                    .bundled_pyodide_dist_path_for_vm(&vm_id)
3101                    .map_err(python_error)?;
3102                let pyodide_cache_path = pyodide_dist_path
3103                    .parent()
3104                    .and_then(Path::parent)
3105                    .unwrap_or(pyodide_dist_path.as_path())
3106                    .join("pyodide-package-cache");
3107                add_runtime_guest_path_mapping(
3108                    &mut env,
3109                    PYTHON_PYODIDE_GUEST_ROOT,
3110                    &pyodide_dist_path,
3111                );
3112                add_runtime_guest_path_mapping(
3113                    &mut env,
3114                    PYTHON_PYODIDE_CACHE_GUEST_ROOT,
3115                    &pyodide_cache_path,
3116                );
3117                add_runtime_host_access_path(
3118                    &mut env,
3119                    "AGENTOS_EXTRA_FS_READ_PATHS",
3120                    &pyodide_dist_path,
3121                    true,
3122                );
3123                add_runtime_host_access_path(
3124                    &mut env,
3125                    "AGENTOS_EXTRA_FS_READ_PATHS",
3126                    &pyodide_cache_path,
3127                    true,
3128                );
3129                add_runtime_host_access_path(
3130                    &mut env,
3131                    "AGENTOS_EXTRA_FS_WRITE_PATHS",
3132                    &pyodide_cache_path,
3133                    false,
3134                );
3135                let context = self
3136                    .python_engine
3137                    .create_context(CreatePythonContextRequest {
3138                        vm_id: vm_id.clone(),
3139                        pyodide_dist_path,
3140                    });
3141                let execution = self
3142                    .python_engine
3143                    .start_execution(StartPythonExecutionRequest {
3144                        vm_id: vm_id.clone(),
3145                        context_id: context.context_id,
3146                        code: resolved.entrypoint.clone(),
3147                        file_path: python_file_path,
3148                        env: env.clone(),
3149                        cwd: resolved.host_cwd.clone(),
3150                        limits: python_execution_limits(vm),
3151                        guest_runtime: guest_runtime_identity(vm, None, None),
3152                    })
3153                    .map_err(python_error)?;
3154                (ActiveExecution::Python(execution), env.clone())
3155            }
3156            GuestRuntimeKind::WebAssembly => {
3157                let wasm_limits = wasm_execution_limits(vm);
3158                let wasm_guest_runtime =
3159                    guest_runtime_identity(vm, Some(u64::from(kernel_pid)), Some(0));
3160                let wasm_permission_tier = resolved.wasm_permission_tier.unwrap_or_else(|| {
3161                    resolve_wasm_permission_tier(
3162                        vm,
3163                        Some(&resolved.command),
3164                        None,
3165                        &resolved.entrypoint,
3166                    )
3167                });
3168                let context = self.wasm_engine.create_context(CreateWasmContextRequest {
3169                    vm_id: vm_id.clone(),
3170                    module_path: Some(resolved.entrypoint.clone()),
3171                });
3172                let execution = self
3173                    .wasm_engine
3174                    .start_execution(StartWasmExecutionRequest {
3175                        vm_id: vm_id.clone(),
3176                        context_id: context.context_id,
3177                        argv: resolved.process_args.clone(),
3178                        env: env.clone(),
3179                        cwd: resolved.host_cwd.clone(),
3180                        permission_tier: execution_wasm_permission_tier(wasm_permission_tier),
3181                        limits: wasm_limits,
3182                        guest_runtime: wasm_guest_runtime,
3183                    })
3184                    .map_err(wasm_error)?;
3185                (ActiveExecution::Wasm(Box::new(execution)), env)
3186            }
3187        };
3188        let child_pid = execution.child_pid();
3189        let kernel_stdin_writer_fd = install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?;
3190        vm.active_processes.insert(
3191            payload.process_id.clone(),
3192            ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
3193                .with_kernel_stdin_writer_fd(kernel_stdin_writer_fd)
3194                .with_guest_cwd(resolved.guest_cwd.clone())
3195                .with_env(process_env)
3196                .with_host_cwd(resolved.host_cwd.clone()),
3197        );
3198        self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy)?;
3199
3200        Ok(DispatchResult {
3201            response: self.respond(
3202                request,
3203                ResponsePayload::ProcessStarted(ProcessStartedResponse {
3204                    process_id: payload.process_id,
3205                    pid: Some(if child_pid == 0 {
3206                        kernel_pid
3207                    } else {
3208                        child_pid
3209                    }),
3210                }),
3211            ),
3212            events: Vec::new(),
3213        })
3214    }
3215
3216    pub(crate) async fn write_stdin(
3217        &mut self,
3218        request: &RequestFrame,
3219        payload: WriteStdinRequest,
3220    ) -> Result<DispatchResult, SidecarError> {
3221        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3222        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3223
3224        let vm = self
3225            .vms
3226            .get_mut(&vm_id)
3227            .ok_or_else(|| missing_vm_error(&vm_id))?;
3228        let process = vm
3229            .active_processes
3230            .get_mut(&payload.process_id)
3231            .ok_or_else(|| {
3232                SidecarError::InvalidState(format!(
3233                    "VM {vm_id} has no active process {}",
3234                    payload.process_id
3235                ))
3236            })?;
3237        process.execution.write_stdin(&payload.chunk)?;
3238        write_kernel_process_stdin(&mut vm.kernel, process, &payload.chunk)?;
3239
3240        Ok(DispatchResult {
3241            response: self.respond(
3242                request,
3243                ResponsePayload::StdinWritten(StdinWrittenResponse {
3244                    process_id: payload.process_id,
3245                    accepted_bytes: payload.chunk.len() as u64,
3246                }),
3247            ),
3248            events: Vec::new(),
3249        })
3250    }
3251
3252    pub(crate) async fn close_stdin(
3253        &mut self,
3254        request: &RequestFrame,
3255        payload: CloseStdinRequest,
3256    ) -> Result<DispatchResult, SidecarError> {
3257        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3258        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3259
3260        let vm = self
3261            .vms
3262            .get_mut(&vm_id)
3263            .ok_or_else(|| missing_vm_error(&vm_id))?;
3264        let process = vm
3265            .active_processes
3266            .get_mut(&payload.process_id)
3267            .ok_or_else(|| {
3268                SidecarError::InvalidState(format!(
3269                    "VM {vm_id} has no active process {}",
3270                    payload.process_id
3271                ))
3272            })?;
3273        process.execution.close_stdin()?;
3274        close_kernel_process_stdin(&mut vm.kernel, process)?;
3275
3276        Ok(DispatchResult {
3277            response: self.respond(
3278                request,
3279                ResponsePayload::StdinClosed(StdinClosedResponse {
3280                    process_id: payload.process_id,
3281                }),
3282            ),
3283            events: Vec::new(),
3284        })
3285    }
3286
3287    pub(crate) async fn kill_process(
3288        &mut self,
3289        request: &RequestFrame,
3290        payload: KillProcessRequest,
3291    ) -> Result<DispatchResult, SidecarError> {
3292        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3293        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3294        self.kill_process_internal(&vm_id, &payload.process_id, &payload.signal)?;
3295
3296        Ok(DispatchResult {
3297            response: self.respond(
3298                request,
3299                ResponsePayload::ProcessKilled(ProcessKilledResponse {
3300                    process_id: payload.process_id,
3301                }),
3302            ),
3303            events: Vec::new(),
3304        })
3305    }
3306
3307    pub(crate) async fn find_listener(
3308        &mut self,
3309        request: &RequestFrame,
3310        payload: FindListenerRequest,
3311    ) -> Result<DispatchResult, SidecarError> {
3312        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3313        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3314        require_vm_inspection_permission(
3315            &self.bridge,
3316            &vm_id,
3317            "network.inspect",
3318            "network",
3319            &socket_query_resource(SocketQueryKind::TcpListener, &payload),
3320        )?;
3321
3322        let listener =
3323            find_socket_state_entry(self.vms.get(&vm_id), SocketQueryKind::TcpListener, &payload)?;
3324
3325        Ok(DispatchResult {
3326            response: self.respond(
3327                request,
3328                ResponsePayload::ListenerSnapshot(ListenerSnapshotResponse { listener }),
3329            ),
3330            events: Vec::new(),
3331        })
3332    }
3333
3334    pub(crate) async fn get_process_snapshot(
3335        &mut self,
3336        request: &RequestFrame,
3337        _payload: GetProcessSnapshotRequest,
3338    ) -> Result<DispatchResult, SidecarError> {
3339        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3340        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3341        require_vm_inspection_permission(
3342            &self.bridge,
3343            &vm_id,
3344            "process.inspect",
3345            "process",
3346            "process://snapshot",
3347        )?;
3348
3349        let processes = self
3350            .vms
3351            .get_mut(&vm_id)
3352            .map(|vm| {
3353                prune_exited_process_snapshots(vm);
3354                snapshot_vm_processes(vm)
3355            })
3356            .unwrap_or_default();
3357
3358        Ok(DispatchResult {
3359            response: self.respond(
3360                request,
3361                ResponsePayload::ProcessSnapshot(ProcessSnapshotResponse { processes }),
3362            ),
3363            events: Vec::new(),
3364        })
3365    }
3366
3367    pub(crate) async fn find_bound_udp(
3368        &mut self,
3369        request: &RequestFrame,
3370        payload: FindBoundUdpRequest,
3371    ) -> Result<DispatchResult, SidecarError> {
3372        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3373        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3374
3375        let lookup_request = FindListenerRequest {
3376            host: payload.host,
3377            port: payload.port,
3378            path: None,
3379        };
3380        require_vm_inspection_permission(
3381            &self.bridge,
3382            &vm_id,
3383            "network.inspect",
3384            "network",
3385            &socket_query_resource(SocketQueryKind::UdpBound, &lookup_request),
3386        )?;
3387        let socket = find_socket_state_entry(
3388            self.vms.get(&vm_id),
3389            SocketQueryKind::UdpBound,
3390            &lookup_request,
3391        )?;
3392
3393        Ok(DispatchResult {
3394            response: self.respond(
3395                request,
3396                ResponsePayload::BoundUdpSnapshot(BoundUdpSnapshotResponse { socket }),
3397            ),
3398            events: Vec::new(),
3399        })
3400    }
3401
3402    pub(crate) async fn vm_fetch(
3403        &mut self,
3404        request: &RequestFrame,
3405        payload: VmFetchRequest,
3406    ) -> Result<DispatchResult, SidecarError> {
3407        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3408        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3409
3410        let vm = self
3411            .vms
3412            .get_mut(&vm_id)
3413            .ok_or_else(|| SidecarError::InvalidState(String::from("unknown sidecar VM")))?;
3414        let target_path = if payload.path.starts_with('/') {
3415            payload.path.clone()
3416        } else {
3417            format!("/{}", payload.path)
3418        };
3419        let request_url = Url::parse(&format!("http://127.0.0.1:{}{target_path}", payload.port))
3420            .map_err(|error| {
3421                SidecarError::InvalidState(format!(
3422                    "invalid vm.fetch target {target_path:?}: {error}"
3423                ))
3424            })?;
3425        let header_values: BTreeMap<String, Value> = serde_json::from_str(&payload.headers_json)
3426            .map_err(|error| {
3427                SidecarError::InvalidState(format!(
3428                    "vm.fetch headers_json must be valid JSON: {error}"
3429                ))
3430            })?;
3431        let options = JavascriptHttpRequestOptions {
3432            method: Some(payload.method),
3433            headers: header_values,
3434            body: payload.body,
3435            reject_unauthorized: None,
3436        };
3437        let headers = parse_http_header_collection(&options.headers, "vm.fetch headers")?;
3438        let target_process_id = find_kernel_http_listener_process(vm, payload.port);
3439        if let Some(target_process_id) = target_process_id {
3440            let max_fetch_response_bytes = vm.limits.http.max_fetch_response_bytes;
3441            let response_json = match dispatch_kernel_http_fetch(
3442                &self.bridge,
3443                &vm_id,
3444                vm,
3445                &target_process_id,
3446                payload.port,
3447                &target_path,
3448                &options,
3449                &headers,
3450                max_fetch_response_bytes,
3451            ) {
3452                Ok(response_json) => response_json,
3453                Err(error) => {
3454                    if let Some(exit_code) = kernel_http_fetch_target_exit_code(&error) {
3455                        let _ = vm;
3456                        self.finish_active_process_exit(&vm_id, &target_process_id, exit_code)?;
3457                    }
3458                    return Err(error);
3459                }
3460            };
3461            let response = self.respond(
3462                request,
3463                ResponsePayload::VmFetchResult(VmFetchResponse { response_json }),
3464            );
3465            ensure_vm_fetch_response_frame_within_limit(&response, self.config.max_frame_bytes)?;
3466
3467            return Ok(DispatchResult {
3468                response,
3469                events: Vec::new(),
3470            });
3471        }
3472
3473        let Some((target_process_id, server_id)) =
3474            vm.active_processes
3475                .iter()
3476                .find_map(|(process_id, process)| {
3477                    process
3478                        .http_servers
3479                        .iter()
3480                        .find(|(_, server)| server.guest_local_addr.port() == payload.port)
3481                        .map(|(server_id, _)| (process_id.clone(), *server_id))
3482                })
3483        else {
3484            return Err(SidecarError::Execution(format!(
3485                "vm.fetch could not find a guest HTTP listener on port {}",
3486                payload.port
3487            )));
3488        };
3489        let socket_paths = build_javascript_socket_path_context(vm)?;
3490        let resource_limits = vm.kernel.resource_limits().clone();
3491        let process = vm
3492            .active_processes
3493            .get_mut(&target_process_id)
3494            .ok_or_else(|| {
3495                SidecarError::InvalidState(format!(
3496                    "vm.fetch target process disappeared: {target_process_id}"
3497                ))
3498            })?;
3499        let request_json = serialize_http_loopback_request(&request_url, &options, &headers)?;
3500        let response_json = dispatch_loopback_http_request(LoopbackHttpDispatchRequest {
3501            bridge: &self.bridge,
3502            vm_id: &vm_id,
3503            dns: &vm.dns,
3504            socket_paths: &socket_paths,
3505            kernel: &mut vm.kernel,
3506            process,
3507            resource_limits: &resource_limits,
3508            server_id,
3509            request_json: &request_json,
3510        })?;
3511
3512        let response = self.respond(
3513            request,
3514            ResponsePayload::VmFetchResult(VmFetchResponse { response_json }),
3515        );
3516        ensure_vm_fetch_response_frame_within_limit(&response, self.config.max_frame_bytes)?;
3517
3518        Ok(DispatchResult {
3519            response,
3520            events: Vec::new(),
3521        })
3522    }
3523
3524    pub(crate) async fn get_signal_state(
3525        &mut self,
3526        request: &RequestFrame,
3527        payload: GetSignalStateRequest,
3528    ) -> Result<DispatchResult, SidecarError> {
3529        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3530        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3531
3532        let handlers = self
3533            .vms
3534            .get(&vm_id)
3535            .and_then(|vm| vm.signal_states.get(&payload.process_id))
3536            .cloned()
3537            .unwrap_or_default();
3538
3539        Ok(DispatchResult {
3540            response: self.respond(
3541                request,
3542                ResponsePayload::SignalState(SignalStateResponse {
3543                    process_id: payload.process_id,
3544                    handlers: handlers.into_iter().collect(),
3545                }),
3546            ),
3547            events: Vec::new(),
3548        })
3549    }
3550
3551    pub(crate) async fn get_zombie_timer_count(
3552        &mut self,
3553        request: &RequestFrame,
3554        _payload: GetZombieTimerCountRequest,
3555    ) -> Result<DispatchResult, SidecarError> {
3556        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3557        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3558
3559        let count = self
3560            .vms
3561            .get(&vm_id)
3562            .map(|vm| vm.kernel.zombie_timer_count() as u64)
3563            .unwrap_or_default();
3564
3565        Ok(DispatchResult {
3566            response: self.respond(
3567                request,
3568                ResponsePayload::ZombieTimerCount(ZombieTimerCountResponse { count }),
3569            ),
3570            events: Vec::new(),
3571        })
3572    }
3573
3574    pub(crate) fn kill_process_internal(
3575        &mut self,
3576        vm_id: &str,
3577        process_id: &str,
3578        signal: &str,
3579    ) -> Result<(), SidecarError> {
3580        let signal_name = signal.to_owned();
3581        let signal = parse_signal(signal)?;
3582        let vm = self
3583            .vms
3584            .get_mut(vm_id)
3585            .ok_or_else(|| SidecarError::InvalidState(format!("unknown sidecar VM {vm_id}")))?;
3586        let process = vm.active_processes.get_mut(process_id).ok_or_else(|| {
3587            SidecarError::InvalidState(format!("VM {vm_id} has no active process {process_id}"))
3588        })?;
3589        let kernel_pid = process.kernel_pid;
3590
3591        enum KillBehavior {
3592            Tool,
3593            SharedV8StateOnly,
3594            SharedV8Continue,
3595            SharedV8Terminate,
3596            SharedV8DispatchOrTerminate,
3597            Noop,
3598            HostPid(u32),
3599        }
3600
3601        let behavior = match &process.execution {
3602            ActiveExecution::Tool(_) => KillBehavior::Tool,
3603            ActiveExecution::Javascript(execution)
3604                if execution.uses_shared_v8_runtime() && matches!(signal, 0 | libc::SIGSTOP) =>
3605            {
3606                KillBehavior::SharedV8StateOnly
3607            }
3608            ActiveExecution::Javascript(execution)
3609                if execution.uses_shared_v8_runtime() && signal == libc::SIGCONT =>
3610            {
3611                KillBehavior::SharedV8Continue
3612            }
3613            ActiveExecution::Wasm(execution)
3614                if execution.uses_shared_v8_runtime()
3615                    && matches!(signal, 0 | libc::SIGSTOP | libc::SIGCONT) =>
3616            {
3617                KillBehavior::SharedV8StateOnly
3618            }
3619            ActiveExecution::Python(execution)
3620                if execution.uses_shared_v8_runtime()
3621                    && matches!(signal, 0 | libc::SIGSTOP | libc::SIGCONT) =>
3622            {
3623                KillBehavior::SharedV8StateOnly
3624            }
3625            ActiveExecution::Javascript(execution)
3626                if execution.uses_shared_v8_runtime() && signal == SIGKILL =>
3627            {
3628                KillBehavior::SharedV8Terminate
3629            }
3630            ActiveExecution::Wasm(execution)
3631                if execution.uses_shared_v8_runtime() && signal == SIGKILL =>
3632            {
3633                KillBehavior::SharedV8Terminate
3634            }
3635            ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime() => {
3636                KillBehavior::SharedV8DispatchOrTerminate
3637            }
3638            ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime() => {
3639                KillBehavior::SharedV8Terminate
3640            }
3641            ActiveExecution::Python(execution) if execution.uses_shared_v8_runtime() => {
3642                KillBehavior::SharedV8Terminate
3643            }
3644            ActiveExecution::Javascript(execution) if execution.child_pid() == 0 => {
3645                KillBehavior::Noop
3646            }
3647            _ => KillBehavior::HostPid(process.execution.child_pid()),
3648        };
3649
3650        match behavior {
3651            KillBehavior::Tool => {
3652                let ActiveExecution::Tool(execution) = &process.execution else {
3653                    unreachable!("kill behavior must match tool execution");
3654                };
3655                if signal != 0 {
3656                    execution.cancelled.store(true, Ordering::Relaxed);
3657                    process.queue_pending_execution_event(ActiveExecutionEvent::Exited(
3658                        128 + signal,
3659                    ))?;
3660                }
3661            }
3662            KillBehavior::SharedV8StateOnly => {
3663                if matches!(signal, libc::SIGSTOP | libc::SIGCONT) {
3664                    vm.kernel
3665                        .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal)
3666                        .map_err(kernel_error)?;
3667                }
3668            }
3669            KillBehavior::SharedV8Continue => {
3670                vm.kernel
3671                    .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal)
3672                    .map_err(kernel_error)?;
3673                if signal != 0 && !dispatch_v8_process_signal(process, signal)? {
3674                    process.execution.terminate()?;
3675                }
3676            }
3677            KillBehavior::SharedV8Terminate => {
3678                if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
3679                    close_kernel_process_stdin(&mut vm.kernel, process)?;
3680                }
3681                process.execution.terminate()?;
3682                let needs_synthetic_exit = matches!(process.execution, ActiveExecution::Wasm(_))
3683                    || (signal == SIGKILL
3684                        && matches!(process.execution, ActiveExecution::Javascript(_)));
3685                if signal != 0 && needs_synthetic_exit {
3686                    process.queue_pending_execution_event(ActiveExecutionEvent::Exited(
3687                        128 + signal,
3688                    ))?;
3689                }
3690            }
3691            KillBehavior::SharedV8DispatchOrTerminate => {
3692                if signal != 0 && !dispatch_v8_process_signal(process, signal)? {
3693                    process.execution.terminate()?;
3694                }
3695            }
3696            KillBehavior::Noop => {}
3697            KillBehavior::HostPid(pid) => {
3698                if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
3699                    close_kernel_process_stdin(&mut vm.kernel, process)?;
3700                }
3701                signal_runtime_process(pid, signal)?;
3702            }
3703        }
3704        emit_security_audit_event(
3705            &self.bridge,
3706            vm_id,
3707            "security.process.kill",
3708            audit_fields([
3709                (String::from("source"), String::from("control_plane")),
3710                (String::from("source_pid"), String::from("0")),
3711                (String::from("target_pid"), process.kernel_pid.to_string()),
3712                (String::from("process_id"), process_id.to_owned()),
3713                (String::from("signal"), signal_name),
3714                (
3715                    String::from("host_pid"),
3716                    process.execution.child_pid().to_string(),
3717                ),
3718            ]),
3719        );
3720        Ok(())
3721    }
3722
3723    pub async fn pump_process_events(
3724        &mut self,
3725        ownership: &OwnershipScope,
3726    ) -> Result<bool, SidecarError> {
3727        let mut emitted_any = false;
3728
3729        let mut queued_envelopes = Vec::new();
3730        {
3731            let pending_capacity = self.pending_process_event_capacity();
3732            let receiver = self.process_event_receiver.as_mut().ok_or_else(|| {
3733                SidecarError::InvalidState(String::from("process event receiver unavailable"))
3734            })?;
3735            loop {
3736                if queued_envelopes.len() >= pending_capacity {
3737                    if receiver.is_empty() {
3738                        break;
3739                    }
3740                    return Err(process_event_queue_overflow_error());
3741                }
3742                match receiver.try_recv() {
3743                    Ok(envelope) => {
3744                        queued_envelopes.push(envelope);
3745                        emitted_any = true;
3746                    }
3747                    Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
3748                    Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
3749                }
3750            }
3751        }
3752        for envelope in queued_envelopes {
3753            self.queue_pending_process_event(envelope)?;
3754        }
3755
3756        let vm_ids = self.vm_ids_for_scope(ownership)?;
3757        for vm_id in vm_ids {
3758            while let Some(vm) = self.vms.get(&vm_id) {
3759                let connection_id = vm.connection_id.clone();
3760                let session_id = vm.session_id.clone();
3761                let process_ids = self
3762                    .vms
3763                    .get(&vm_id)
3764                    .map(|vm| vm.active_processes.keys().cloned().collect::<Vec<_>>())
3765                    .unwrap_or_default();
3766                let mut emitted_this_pass = false;
3767
3768                for process_id in process_ids {
3769                    if self
3770                        .vms
3771                        .get(&vm_id)
3772                        .is_some_and(|vm| vm.detached_child_processes.contains(&process_id))
3773                    {
3774                        continue;
3775                    }
3776                    enum ProcessPollResult {
3777                        Event(Box<Option<ActiveExecutionEvent>>),
3778                        RecoverClosedChannel,
3779                    }
3780                    let poll_result = {
3781                        let Some(vm) = self.vms.get_mut(&vm_id) else {
3782                            continue;
3783                        };
3784                        let Some(process) = vm.active_processes.get_mut(&process_id) else {
3785                            continue;
3786                        };
3787                        if let Some(event) = process.pending_execution_events.pop_front() {
3788                            ProcessPollResult::Event(Box::new(Some(event)))
3789                        } else {
3790                            match process.execution.poll_event(Duration::ZERO).await {
3791                                Ok(event) => ProcessPollResult::Event(Box::new(event)),
3792                                Err(SidecarError::Execution(message))
3793                                    if (process.runtime == GuestRuntimeKind::JavaScript
3794                                        && closed_javascript_event_channel(&message))
3795                                        || (process.runtime == GuestRuntimeKind::Python
3796                                            && closed_python_event_channel(&message))
3797                                        || (process.runtime == GuestRuntimeKind::WebAssembly
3798                                            && closed_wasm_event_channel(&message)) =>
3799                                {
3800                                    ProcessPollResult::RecoverClosedChannel
3801                                }
3802                                Err(other) => return Err(other),
3803                            }
3804                        }
3805                    };
3806                    let event = match poll_result {
3807                        ProcessPollResult::Event(event) => *event,
3808                        ProcessPollResult::RecoverClosedChannel => {
3809                            self.recover_closed_root_runtime_process_event(&vm_id, &process_id)?
3810                        }
3811                    };
3812
3813                    let Some(event) = event else {
3814                        continue;
3815                    };
3816
3817                    if Self::internal_execution_event(&event) {
3818                        // These events are sidecar work items, not client-facing
3819                        // process events. Handle them immediately so a sibling
3820                        // process can service sync RPCs while another request
3821                        // waits on VM-local networking.
3822                        self.handle_execution_event(&vm_id, &process_id, event)?;
3823                    } else {
3824                        self.queue_pending_process_event(ProcessEventEnvelope {
3825                            connection_id: connection_id.clone(),
3826                            session_id: session_id.clone(),
3827                            vm_id: vm_id.clone(),
3828                            process_id: process_id.clone(),
3829                            event,
3830                        })?;
3831                    }
3832                    emitted_any = true;
3833                    emitted_this_pass = true;
3834                }
3835
3836                if !emitted_this_pass {
3837                    break;
3838                }
3839            }
3840
3841            if self.pump_detached_child_process_events(&vm_id)? {
3842                emitted_any = true;
3843            }
3844        }
3845
3846        Ok(emitted_any)
3847    }
3848
3849    fn internal_execution_event(event: &ActiveExecutionEvent) -> bool {
3850        matches!(
3851            event,
3852            ActiveExecutionEvent::JavascriptSyncRpcRequest(_)
3853                | ActiveExecutionEvent::PythonVfsRpcRequest(_)
3854                | ActiveExecutionEvent::SignalState { .. }
3855        )
3856    }
3857
3858    fn recover_closed_root_runtime_process_event(
3859        &mut self,
3860        vm_id: &str,
3861        process_id: &str,
3862    ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
3863        let Some(vm) = self.vms.get_mut(vm_id) else {
3864            return Ok(None);
3865        };
3866        let Some(process) = vm.active_processes.get(process_id) else {
3867            return Ok(None);
3868        };
3869        if process.execution.uses_shared_v8_runtime() {
3870            return Ok(None);
3871        }
3872        if process.runtime != GuestRuntimeKind::JavaScript
3873            && process.runtime != GuestRuntimeKind::Python
3874            && process.runtime != GuestRuntimeKind::WebAssembly
3875        {
3876            return Ok(None);
3877        }
3878        let runtime_child_pid = process.execution.child_pid();
3879        if runtime_child_pid == 0 {
3880            return Ok(None);
3881        }
3882        if let Some(status) = runtime_child_exit_status(runtime_child_pid)? {
3883            return Ok(Some(ActiveExecutionEvent::Exited(status)));
3884        }
3885        if runtime_child_is_alive(runtime_child_pid)? {
3886            return Ok(None);
3887        }
3888        Ok(Some(ActiveExecutionEvent::Exited(0)))
3889    }
3890
3891    fn active_process_by_path<'a>(
3892        process: &'a ActiveProcess,
3893        child_path: &[&str],
3894    ) -> Option<&'a ActiveProcess> {
3895        let mut current = process;
3896        for child_id in child_path {
3897            current = current.child_processes.get(*child_id)?;
3898        }
3899        Some(current)
3900    }
3901
3902    fn active_process_by_path_mut<'a>(
3903        process: &'a mut ActiveProcess,
3904        child_path: &[&str],
3905    ) -> Option<&'a mut ActiveProcess> {
3906        let mut current = process;
3907        for child_id in child_path {
3908            current = current.child_processes.get_mut(*child_id)?;
3909        }
3910        Some(current)
3911    }
3912
3913    fn active_process_by_owned_path_mut<'a>(
3914        process: &'a mut ActiveProcess,
3915        child_path: &[String],
3916    ) -> Option<&'a mut ActiveProcess> {
3917        let mut current = process;
3918        for child_id in child_path {
3919            current = current.child_processes.get_mut(child_id)?;
3920        }
3921        Some(current)
3922    }
3923
3924    fn active_process_path_by_kernel_pid(
3925        process: &ActiveProcess,
3926        kernel_pid: u32,
3927    ) -> Option<Vec<String>> {
3928        if process.kernel_pid == kernel_pid {
3929            return Some(Vec::new());
3930        }
3931
3932        for (child_id, child) in &process.child_processes {
3933            let Some(mut path) = Self::active_process_path_by_kernel_pid(child, kernel_pid) else {
3934                continue;
3935            };
3936            path.insert(0, child_id.clone());
3937            return Some(path);
3938        }
3939
3940        None
3941    }
3942
3943    fn descendant_parent_process<'a>(
3944        vm: &'a VmState,
3945        process_id: &str,
3946        child_path: &[&str],
3947    ) -> Option<&'a ActiveProcess> {
3948        let root = vm.active_processes.get(process_id)?;
3949        Self::active_process_by_path(root, child_path)
3950    }
3951
3952    fn descendant_parent_process_mut<'a>(
3953        vm: &'a mut VmState,
3954        process_id: &str,
3955        child_path: &[&str],
3956    ) -> Option<&'a mut ActiveProcess> {
3957        let root = vm.active_processes.get_mut(process_id)?;
3958        Self::active_process_by_path_mut(root, child_path)
3959    }
3960
3961    fn child_process_path_label(process_id: &str, child_path: &[&str]) -> String {
3962        if child_path.is_empty() {
3963            process_id.to_owned()
3964        } else {
3965            format!("{process_id}/{}", child_path.join("/"))
3966        }
3967    }
3968
3969    fn adopt_detached_child_processes(
3970        current_process_id: &str,
3971        process: &mut ActiveProcess,
3972    ) -> Vec<(String, ActiveProcess)> {
3973        let mut adopted = Vec::new();
3974        let child_ids = process.child_processes.keys().cloned().collect::<Vec<_>>();
3975        for child_id in child_ids {
3976            let child_process_id = format!("{current_process_id}/{child_id}");
3977            let Some(mut child) = process.child_processes.remove(&child_id) else {
3978                continue;
3979            };
3980            if child.detached {
3981                adopted.push((child_process_id, child));
3982                continue;
3983            }
3984
3985            adopted.extend(Self::adopt_detached_child_processes(
3986                &child_process_id,
3987                &mut child,
3988            ));
3989            process.child_processes.insert(child_id, child);
3990        }
3991        adopted
3992    }
3993
3994    fn child_process_signal_key<'a>(process_id: &'a str, child_path: &[&'a str]) -> &'a str {
3995        child_path.last().copied().unwrap_or(process_id)
3996    }
3997
3998    fn resolve_detached_child_process_path(
3999        vm: &VmState,
4000        detached_process_id: &str,
4001    ) -> Option<(String, Vec<String>)> {
4002        let root_process_id = vm
4003            .active_processes
4004            .keys()
4005            .filter(|candidate| {
4006                detached_process_id == candidate.as_str()
4007                    || detached_process_id
4008                        .strip_prefix(candidate.as_str())
4009                        .is_some_and(|remainder| remainder.starts_with('/'))
4010            })
4011            .max_by_key(|candidate| candidate.len())?
4012            .clone();
4013
4014        let remainder = detached_process_id
4015            .strip_prefix(root_process_id.as_str())
4016            .unwrap_or_default();
4017        if remainder.is_empty() {
4018            return Some((root_process_id, Vec::new()));
4019        }
4020
4021        Some((
4022            root_process_id,
4023            remainder
4024                .trim_start_matches('/')
4025                .split('/')
4026                .map(str::to_owned)
4027                .collect(),
4028        ))
4029    }
4030
4031    fn pump_detached_child_process_events(&mut self, vm_id: &str) -> Result<bool, SidecarError> {
4032        let detached_process_ids = self
4033            .vms
4034            .get(vm_id)
4035            .map(|vm| {
4036                vm.detached_child_processes
4037                    .iter()
4038                    .cloned()
4039                    .collect::<Vec<_>>()
4040            })
4041            .unwrap_or_default();
4042        let mut emitted_any = false;
4043        for detached_process_id in detached_process_ids {
4044            let Some((root_process_id, child_path)) = self
4045                .vms
4046                .get(vm_id)
4047                .and_then(|vm| Self::resolve_detached_child_process_path(vm, &detached_process_id))
4048            else {
4049                if let Some(vm) = self.vms.get_mut(vm_id) {
4050                    vm.detached_child_processes.remove(&detached_process_id);
4051                }
4052                continue;
4053            };
4054            if child_path.is_empty() {
4055                loop {
4056                    enum ProcessPollResult {
4057                        Event(Box<Option<ActiveExecutionEvent>>),
4058                        RecoverClosedChannel,
4059                    }
4060                    let poll_result = {
4061                        let Some(vm) = self.vms.get_mut(vm_id) else {
4062                            break;
4063                        };
4064                        let Some(process) = vm.active_processes.get_mut(&root_process_id) else {
4065                            break;
4066                        };
4067                        if let Some(event) = process.pending_execution_events.pop_front() {
4068                            ProcessPollResult::Event(Box::new(Some(event)))
4069                        } else {
4070                            match process.execution.poll_event_blocking(Duration::ZERO) {
4071                                Ok(event) => ProcessPollResult::Event(Box::new(event)),
4072                                Err(SidecarError::Execution(message))
4073                                    if (process.runtime == GuestRuntimeKind::JavaScript
4074                                        && closed_javascript_event_channel(&message))
4075                                        || (process.runtime == GuestRuntimeKind::Python
4076                                            && closed_python_event_channel(&message))
4077                                        || (process.runtime == GuestRuntimeKind::WebAssembly
4078                                            && closed_wasm_event_channel(&message)) =>
4079                                {
4080                                    ProcessPollResult::RecoverClosedChannel
4081                                }
4082                                Err(error) => return Err(error),
4083                            }
4084                        }
4085                    };
4086                    let event = match poll_result {
4087                        ProcessPollResult::Event(event) => *event,
4088                        ProcessPollResult::RecoverClosedChannel => {
4089                            self.recover_closed_root_runtime_process_event(vm_id, &root_process_id)?
4090                        }
4091                    };
4092                    let Some(event) = event else {
4093                        break;
4094                    };
4095                    let Some((connection_id, session_id)) = self
4096                        .vms
4097                        .get(vm_id)
4098                        .map(|vm| (vm.connection_id.clone(), vm.session_id.clone()))
4099                    else {
4100                        break;
4101                    };
4102                    match event {
4103                        ActiveExecutionEvent::Stdout(chunk) => {
4104                            self.queue_pending_process_event(ProcessEventEnvelope {
4105                                connection_id,
4106                                session_id,
4107                                vm_id: vm_id.to_owned(),
4108                                process_id: detached_process_id.clone(),
4109                                event: ActiveExecutionEvent::Stdout(chunk),
4110                            })?;
4111                            emitted_any = true;
4112                        }
4113                        ActiveExecutionEvent::Stderr(chunk) => {
4114                            self.queue_pending_process_event(ProcessEventEnvelope {
4115                                connection_id,
4116                                session_id,
4117                                vm_id: vm_id.to_owned(),
4118                                process_id: detached_process_id.clone(),
4119                                event: ActiveExecutionEvent::Stderr(chunk),
4120                            })?;
4121                            emitted_any = true;
4122                        }
4123                        ActiveExecutionEvent::Exited(exit_code) => {
4124                            if let Some(vm) = self.vms.get_mut(vm_id) {
4125                                vm.detached_child_processes.remove(&detached_process_id);
4126                            }
4127                            self.queue_pending_process_event(ProcessEventEnvelope {
4128                                connection_id,
4129                                session_id,
4130                                vm_id: vm_id.to_owned(),
4131                                process_id: detached_process_id.clone(),
4132                                event: ActiveExecutionEvent::Exited(exit_code),
4133                            })?;
4134                            emitted_any = true;
4135                            break;
4136                        }
4137                        ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
4138                            self.handle_javascript_sync_rpc_request(
4139                                vm_id,
4140                                &root_process_id,
4141                                request,
4142                            )?;
4143                        }
4144                        ActiveExecutionEvent::PythonVfsRpcRequest(request) => {
4145                            self.handle_python_vfs_rpc_request(vm_id, &root_process_id, *request)?;
4146                        }
4147                        ActiveExecutionEvent::SignalState {
4148                            signal,
4149                            registration,
4150                        } => {
4151                            if let Some(vm) = self.vms.get_mut(vm_id) {
4152                                vm.signal_states
4153                                    .entry(root_process_id.clone())
4154                                    .or_default()
4155                                    .insert(signal, registration);
4156                            }
4157                        }
4158                    }
4159                }
4160                continue;
4161            }
4162
4163            let parent_path = child_path[..child_path.len() - 1]
4164                .iter()
4165                .map(String::as_str)
4166                .collect::<Vec<_>>();
4167            let child_process_id = child_path.last().expect("child path cannot be empty");
4168
4169            loop {
4170                let event = match self.poll_descendant_javascript_child_process(
4171                    vm_id,
4172                    &root_process_id,
4173                    &parent_path,
4174                    child_process_id,
4175                    0,
4176                ) {
4177                    Ok(event) => event,
4178                    Err(SidecarError::InvalidState(message))
4179                        if message.contains("unknown child process")
4180                            || message.contains("unknown child process path") =>
4181                    {
4182                        if let Some(vm) = self.vms.get_mut(vm_id) {
4183                            vm.detached_child_processes.remove(&detached_process_id);
4184                        }
4185                        break;
4186                    }
4187                    Err(error) if is_javascript_child_process_gone_error(&error) => {
4188                        if let Some(vm) = self.vms.get_mut(vm_id) {
4189                            vm.detached_child_processes.remove(&detached_process_id);
4190                        }
4191                        break;
4192                    }
4193                    Err(error) => return Err(error),
4194                };
4195
4196                let Some(event_type) = event.get("type").and_then(Value::as_str) 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
4207                let envelope = match event_type {
4208                    "stdout" => Some(ProcessEventEnvelope {
4209                        connection_id: connection_id.clone(),
4210                        session_id: session_id.clone(),
4211                        vm_id: vm_id.to_owned(),
4212                        process_id: detached_process_id.clone(),
4213                        event: ActiveExecutionEvent::Stdout(javascript_sync_rpc_bytes_arg(
4214                            &[event.get("data").cloned().unwrap_or(Value::Null)],
4215                            0,
4216                            "detached child_process stdout",
4217                        )?),
4218                    }),
4219                    "stderr" => Some(ProcessEventEnvelope {
4220                        connection_id: connection_id.clone(),
4221                        session_id: session_id.clone(),
4222                        vm_id: vm_id.to_owned(),
4223                        process_id: detached_process_id.clone(),
4224                        event: ActiveExecutionEvent::Stderr(javascript_sync_rpc_bytes_arg(
4225                            &[event.get("data").cloned().unwrap_or(Value::Null)],
4226                            0,
4227                            "detached child_process stderr",
4228                        )?),
4229                    }),
4230                    "exit" => {
4231                        if let Some(vm) = self.vms.get_mut(vm_id) {
4232                            vm.detached_child_processes.remove(&detached_process_id);
4233                        }
4234                        Some(ProcessEventEnvelope {
4235                            connection_id,
4236                            session_id,
4237                            vm_id: vm_id.to_owned(),
4238                            process_id: detached_process_id.clone(),
4239                            event: ActiveExecutionEvent::Exited(
4240                                event
4241                                    .get("exitCode")
4242                                    .and_then(Value::as_i64)
4243                                    .map(|value| value as i32)
4244                                    .unwrap_or(1),
4245                            ),
4246                        })
4247                    }
4248                    _ => None,
4249                };
4250
4251                let Some(envelope) = envelope else {
4252                    break;
4253                };
4254                self.queue_pending_process_event(envelope)?;
4255                emitted_any = true;
4256
4257                if event_type == "exit" {
4258                    break;
4259                }
4260            }
4261        }
4262
4263        Ok(emitted_any)
4264    }
4265    pub(crate) fn drain_queued_descendant_javascript_child_process_events(
4266        &mut self,
4267        vm_id: &str,
4268        process_id: &str,
4269        child_path: &[&str],
4270    ) -> Result<(), SidecarError> {
4271        if child_path.is_empty() {
4272            return Ok(());
4273        }
4274        let target_process_id = Self::child_process_path_label(process_id, child_path);
4275        let mut child_capacity = self
4276            .vms
4277            .get(vm_id)
4278            .and_then(|vm| vm.active_processes.get(process_id))
4279            .and_then(|root| descendant_pending_execution_event_capacity(root, child_path));
4280
4281        let mut deferred = VecDeque::new();
4282        while let Some(envelope) = self.pending_process_events.pop_front() {
4283            if envelope.vm_id == vm_id && envelope.process_id == target_process_id {
4284                if matches!(child_capacity, Some(0)) {
4285                    self.pending_process_events.push_front(envelope);
4286                    while let Some(deferred_envelope) = deferred.pop_back() {
4287                        self.pending_process_events.push_front(deferred_envelope);
4288                    }
4289                    return Err(process_event_queue_overflow_error());
4290                }
4291                if let Some(vm) = self.vms.get_mut(vm_id) {
4292                    if let Some(root) = vm.active_processes.get_mut(process_id) {
4293                        if let Some(child) = Self::active_process_by_path_mut(root, child_path) {
4294                            child.queue_pending_execution_event(envelope.event)?;
4295                            child_capacity = child_capacity.map(|capacity| capacity - 1);
4296                            continue;
4297                        }
4298                    }
4299                }
4300            }
4301            deferred.push_back(envelope);
4302        }
4303        self.pending_process_events = deferred;
4304
4305        let mut queued = Vec::new();
4306        {
4307            let transfer_capacity = self
4308                .pending_process_event_capacity()
4309                .min(child_capacity.unwrap_or(usize::MAX));
4310            let receiver = self.process_event_receiver.as_mut().ok_or_else(|| {
4311                SidecarError::InvalidState(String::from("process event receiver unavailable"))
4312            })?;
4313            loop {
4314                if queued.len() >= transfer_capacity {
4315                    if receiver.is_empty() {
4316                        break;
4317                    }
4318                    return Err(process_event_queue_overflow_error());
4319                }
4320                match receiver.try_recv() {
4321                    Ok(envelope) => queued.push(envelope),
4322                    Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
4323                    Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
4324                }
4325            }
4326        }
4327        for envelope in queued {
4328            if envelope.vm_id == vm_id && envelope.process_id == target_process_id {
4329                if let Some(vm) = self.vms.get_mut(vm_id) {
4330                    if let Some(root) = vm.active_processes.get_mut(process_id) {
4331                        if let Some(child) = Self::active_process_by_path_mut(root, child_path) {
4332                            child.queue_pending_execution_event(envelope.event)?;
4333                            continue;
4334                        }
4335                    }
4336                }
4337            }
4338            self.queue_pending_process_event(envelope)?;
4339        }
4340
4341        Ok(())
4342    }
4343
4344    pub(crate) fn handle_execution_event(
4345        &mut self,
4346        vm_id: &str,
4347        process_id: &str,
4348        event: ActiveExecutionEvent,
4349    ) -> Result<Option<EventFrame>, SidecarError> {
4350        let Some(vm) = self.vms.get(vm_id) else {
4351            log_stale_process_event(&self.bridge, vm_id, process_id, "execution event dispatch");
4352            return Ok(None);
4353        };
4354        if !vm.active_processes.contains_key(process_id) {
4355            log_stale_process_event(&self.bridge, vm_id, process_id, "execution event dispatch");
4356            return Ok(None);
4357        }
4358        let (connection_id, session_id) = { (vm.connection_id.clone(), vm.session_id.clone()) };
4359        let ownership = OwnershipScope::vm(&connection_id, &session_id, vm_id);
4360
4361        if self.capture_extension_process_output_event(vm_id, process_id, &event) {
4362            return Ok(None);
4363        }
4364
4365        match event {
4366            ActiveExecutionEvent::Stdout(chunk) => Ok(Some(EventFrame::new(
4367                ownership,
4368                EventPayload::ProcessOutput(ProcessOutputEvent {
4369                    process_id: process_id.to_owned(),
4370                    channel: StreamChannel::Stdout,
4371                    chunk,
4372                }),
4373            ))),
4374            ActiveExecutionEvent::Stderr(chunk) => Ok(Some(EventFrame::new(
4375                ownership,
4376                EventPayload::ProcessOutput(ProcessOutputEvent {
4377                    process_id: process_id.to_owned(),
4378                    channel: StreamChannel::Stderr,
4379                    chunk,
4380                }),
4381            ))),
4382            ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
4383                self.handle_javascript_sync_rpc_request(vm_id, process_id, request)?;
4384                Ok(None)
4385            }
4386            ActiveExecutionEvent::PythonVfsRpcRequest(request) => {
4387                self.handle_python_vfs_rpc_request(vm_id, process_id, *request)?;
4388                Ok(None)
4389            }
4390            ActiveExecutionEvent::SignalState {
4391                signal,
4392                registration,
4393            } => {
4394                let Some(vm) = self.vms.get_mut(vm_id) else {
4395                    return Ok(None);
4396                };
4397                if !vm.active_processes.contains_key(process_id) {
4398                    return Ok(None);
4399                }
4400                vm.signal_states
4401                    .entry(process_id.to_owned())
4402                    .or_default()
4403                    .insert(signal, registration);
4404                Ok(None)
4405            }
4406            ActiveExecutionEvent::Exited(exit_code) => {
4407                let became_idle = self
4408                    .finish_active_process_exit(vm_id, process_id, exit_code)?
4409                    .unwrap_or(false);
4410
4411                if became_idle {
4412                    self.bridge.emit_lifecycle(vm_id, LifecycleState::Ready)?;
4413                }
4414
4415                Ok(Some(EventFrame::new(
4416                    ownership,
4417                    EventPayload::ProcessExited(ProcessExitedEvent {
4418                        process_id: process_id.to_owned(),
4419                        exit_code,
4420                    }),
4421                )))
4422            }
4423        }
4424    }
4425
4426    pub(crate) fn finish_active_process_exit(
4427        &mut self,
4428        vm_id: &str,
4429        process_id: &str,
4430        exit_code: i32,
4431    ) -> Result<Option<bool>, SidecarError> {
4432        let Some(vm) = self.vms.get_mut(vm_id) else {
4433            log_stale_process_event(&self.bridge, vm_id, process_id, "process exit cleanup");
4434            return Ok(None);
4435        };
4436        if !vm.active_processes.contains_key(process_id) {
4437            log_stale_process_event(&self.bridge, vm_id, process_id, "process exit cleanup");
4438            return Ok(None);
4439        }
4440
4441        prune_exited_process_snapshots(vm);
4442        let process_table = vm.kernel.list_processes();
4443        let Some(mut process) = vm.active_processes.remove(process_id) else {
4444            return Ok(None);
4445        };
4446        if let Some(info) = process_table.get(&process.kernel_pid) {
4447            vm.exited_process_snapshots
4448                .push_back(ExitedProcessSnapshot {
4449                    captured_at: Instant::now(),
4450                    process: build_process_snapshot_entry(
4451                        process_id,
4452                        &process,
4453                        info,
4454                        Some(exit_code),
4455                    ),
4456                });
4457        }
4458        let detached_children = Self::adopt_detached_child_processes(process_id, &mut process);
4459        sync_process_host_writes_to_kernel(vm, &process)?;
4460        terminate_child_process_tree(&mut vm.kernel, &mut process);
4461        process.kernel_handle.finish(exit_code);
4462        let _ = vm.kernel.wait_and_reap(process.kernel_pid);
4463        vm.signal_states.remove(process_id);
4464        for (detached_process_id, detached_child) in detached_children {
4465            vm.detached_child_processes
4466                .insert(detached_process_id.clone());
4467            vm.active_processes
4468                .insert(detached_process_id, detached_child);
4469        }
4470        let became_idle = vm.active_processes.is_empty();
4471        self.prune_extension_process_resource(process_id);
4472
4473        Ok(Some(became_idle))
4474    }
4475
4476    pub(crate) fn drain_process_events_blocking_with_limit(
4477        &mut self,
4478        vm_id: &str,
4479        process_id: &str,
4480        max_events: usize,
4481    ) -> Result<Vec<ActiveExecutionEvent>, SidecarError> {
4482        let mut events = Vec::new();
4483        if max_events == 0 {
4484            return Ok(events);
4485        }
4486        let mut deadline = Instant::now() + Duration::from_millis(150);
4487
4488        loop {
4489            if events.len() >= max_events {
4490                break;
4491            }
4492            let event = {
4493                let Some(vm) = self.vms.get_mut(vm_id) else {
4494                    break;
4495                };
4496                let Some(process) = vm.active_processes.get_mut(process_id) else {
4497                    break;
4498                };
4499                if let Some(event) = process.pending_execution_events.pop_front() {
4500                    Some(event)
4501                } else {
4502                    match process.execution.poll_event_blocking(Duration::ZERO) {
4503                        Ok(event) => event,
4504                        Err(SidecarError::Execution(_)) => None,
4505                        Err(other) => return Err(other),
4506                    }
4507                }
4508            };
4509
4510            let Some(event) = event else {
4511                if Instant::now() >= deadline {
4512                    break;
4513                }
4514                let blocking_wait = deadline.saturating_duration_since(Instant::now());
4515                if blocking_wait.is_zero() {
4516                    break;
4517                }
4518                if events.len() >= max_events {
4519                    break;
4520                }
4521                let delayed_event = {
4522                    let Some(vm) = self.vms.get_mut(vm_id) else {
4523                        break;
4524                    };
4525                    let Some(process) = vm.active_processes.get_mut(process_id) else {
4526                        break;
4527                    };
4528                    if let Some(event) = process.pending_execution_events.pop_front() {
4529                        Some(event)
4530                    } else {
4531                        match process.execution.poll_event_blocking(blocking_wait) {
4532                            Ok(event) => event,
4533                            Err(SidecarError::Execution(_)) => None,
4534                            Err(other) => return Err(other),
4535                        }
4536                    }
4537                };
4538                let Some(event) = delayed_event else {
4539                    break;
4540                };
4541                events.push(event);
4542                deadline = Instant::now() + Duration::from_millis(150);
4543                continue;
4544            };
4545            events.push(event);
4546            deadline = Instant::now() + Duration::from_millis(150);
4547        }
4548
4549        Ok(events)
4550    }
4551
4552    pub(crate) fn handle_python_vfs_rpc_request(
4553        &mut self,
4554        vm_id: &str,
4555        process_id: &str,
4556        request: PythonVfsRpcRequest,
4557    ) -> Result<(), SidecarError> {
4558        match request.method {
4559            PythonVfsRpcMethod::Read
4560            | PythonVfsRpcMethod::Write
4561            | PythonVfsRpcMethod::Stat
4562            | PythonVfsRpcMethod::ReadDir
4563            | PythonVfsRpcMethod::Mkdir => {
4564                filesystem_handle_python_vfs_rpc_request(self, vm_id, process_id, request)
4565            }
4566            PythonVfsRpcMethod::HttpRequest => {
4567                self.handle_python_http_rpc_request(vm_id, process_id, request)
4568            }
4569            PythonVfsRpcMethod::DnsLookup => {
4570                self.handle_python_dns_rpc_request(vm_id, process_id, request)
4571            }
4572            PythonVfsRpcMethod::SubprocessRun => {
4573                self.handle_python_subprocess_rpc_request(vm_id, process_id, request)
4574            }
4575        }
4576    }
4577
4578    fn handle_python_http_rpc_request(
4579        &mut self,
4580        vm_id: &str,
4581        process_id: &str,
4582        request: PythonVfsRpcRequest,
4583    ) -> Result<(), SidecarError> {
4584        let Some(vm) = self.vms.get(vm_id) else {
4585            return Ok(());
4586        };
4587        if !vm.active_processes.contains_key(process_id) {
4588            return Ok(());
4589        }
4590        let response = (|| {
4591            let url_text = request.url.as_deref().ok_or_else(|| {
4592                SidecarError::InvalidState(String::from("python httpRequest requires a url"))
4593            })?;
4594            let url = Url::parse(url_text)
4595                .map_err(|error| SidecarError::Execution(format!("ERR_INVALID_URL: {error}")))?;
4596            let host = url.host_str().ok_or_else(|| {
4597                SidecarError::Execution(String::from("ERR_INVALID_URL: missing host"))
4598            })?;
4599            let port = url.port_or_known_default().ok_or_else(|| {
4600                SidecarError::Execution(String::from("ERR_INVALID_URL: missing port"))
4601            })?;
4602            self.bridge.require_network_access(
4603                vm_id,
4604                NetworkOperation::Http,
4605                format_tcp_resource(host, port),
4606            )?;
4607            // Pin the outbound connection to the IP addresses that pass the
4608            // egress range guard at resolution time. A literal IP is validated
4609            // directly; a hostname is resolved once here and the resulting
4610            // address set is pinned into the HTTP client's resolver below so a
4611            // rebinding DNS server cannot make the second (TLS/TCP) lookup land
4612            // on a private/link-local/metadata IP that this check rejected.
4613            let pinned_addresses = if let Ok(literal_ip) = host.parse::<IpAddr>() {
4614                filter_dns_safe_ip_addrs(vec![literal_ip], host)?
4615            } else {
4616                filter_dns_safe_ip_addrs(
4617                    resolve_dns_ip_addrs(
4618                        &self.bridge,
4619                        &vm.kernel,
4620                        vm_id,
4621                        &vm.dns,
4622                        host,
4623                        DnsLookupPolicy::SkipPermissions,
4624                    )?,
4625                    host,
4626                )?
4627            };
4628            let mut headers = BTreeMap::new();
4629            for (name, value) in &request.headers {
4630                headers.insert(name.clone(), Value::String(value.clone()));
4631            }
4632            let options = JavascriptHttpRequestOptions {
4633                method: Some(
4634                    request
4635                        .http_method
4636                        .clone()
4637                        .unwrap_or_else(|| String::from("GET")),
4638                ),
4639                headers,
4640                body: request.body_base64.as_deref().map(|body| {
4641                    String::from_utf8(
4642                        base64::engine::general_purpose::STANDARD
4643                            .decode(body)
4644                            .unwrap_or_default(),
4645                    )
4646                    .unwrap_or_default()
4647                }),
4648                reject_unauthorized: None,
4649            };
4650            let headers =
4651                parse_http_header_collection(&options.headers, "python httpRequest headers")?;
4652            let response =
4653                issue_outbound_http_request(&url, &options, &headers, &pinned_addresses)?;
4654            let payload_json = response.as_str().ok_or_else(|| {
4655                SidecarError::Execution(String::from(
4656                    "python httpRequest returned a non-string response payload",
4657                ))
4658            })?;
4659            let payload: Value = serde_json::from_str(payload_json).map_err(|error| {
4660                SidecarError::Execution(format!(
4661                    "python httpRequest response must be valid JSON: {error}"
4662                ))
4663            })?;
4664            let header_map = payload
4665                .get("headers")
4666                .and_then(Value::as_array)
4667                .map(|entries| {
4668                    let mut normalized = BTreeMap::<String, Vec<String>>::new();
4669                    for entry in entries {
4670                        let Some(pair) = entry.as_array() else {
4671                            continue;
4672                        };
4673                        let Some(name) = pair.first().and_then(Value::as_str) else {
4674                            continue;
4675                        };
4676                        let Some(value) = pair.get(1).and_then(Value::as_str) else {
4677                            continue;
4678                        };
4679                        normalized
4680                            .entry(name.to_owned())
4681                            .or_default()
4682                            .push(value.to_owned());
4683                    }
4684                    normalized
4685                })
4686                .unwrap_or_default();
4687            Ok(PythonVfsRpcResponsePayload::Http {
4688                status: payload
4689                    .get("status")
4690                    .and_then(Value::as_u64)
4691                    .map(|value| value as u16)
4692                    .unwrap_or_default(),
4693                reason: payload
4694                    .get("statusText")
4695                    .and_then(Value::as_str)
4696                    .unwrap_or_default()
4697                    .to_owned(),
4698                url: payload
4699                    .get("url")
4700                    .and_then(Value::as_str)
4701                    .unwrap_or(url_text)
4702                    .to_owned(),
4703                headers: header_map,
4704                body_base64: payload
4705                    .get("body")
4706                    .and_then(Value::as_str)
4707                    .unwrap_or_default()
4708                    .to_owned(),
4709            })
4710        })();
4711
4712        self.respond_python_rpc(vm_id, process_id, request.id, response)
4713    }
4714
4715    fn handle_python_dns_rpc_request(
4716        &mut self,
4717        vm_id: &str,
4718        process_id: &str,
4719        request: PythonVfsRpcRequest,
4720    ) -> Result<(), SidecarError> {
4721        let Some(vm) = self.vms.get(vm_id) else {
4722            return Ok(());
4723        };
4724        if !vm.active_processes.contains_key(process_id) {
4725            return Ok(());
4726        }
4727        let response = (|| {
4728            let hostname = request.hostname.as_deref().ok_or_else(|| {
4729                SidecarError::InvalidState(String::from("python dnsLookup requires a hostname"))
4730            })?;
4731            let mut addresses = filter_dns_safe_ip_addrs(
4732                resolve_dns_ip_addrs(
4733                    &self.bridge,
4734                    &vm.kernel,
4735                    vm_id,
4736                    &vm.dns,
4737                    hostname,
4738                    DnsLookupPolicy::CheckPermissions,
4739                )?,
4740                hostname,
4741            )?;
4742            if let Some(family) = request.family {
4743                addresses.retain(|address| {
4744                    matches!((family, address), (4, IpAddr::V4(_)) | (6, IpAddr::V6(_)))
4745                });
4746            }
4747            Ok(PythonVfsRpcResponsePayload::DnsLookup {
4748                addresses: addresses
4749                    .into_iter()
4750                    .map(|address| address.to_string())
4751                    .collect(),
4752            })
4753        })();
4754
4755        self.respond_python_rpc(vm_id, process_id, request.id, response)
4756    }
4757
4758    fn handle_python_subprocess_rpc_request(
4759        &mut self,
4760        vm_id: &str,
4761        process_id: &str,
4762        request: PythonVfsRpcRequest,
4763    ) -> Result<(), SidecarError> {
4764        let command = request.command.clone().ok_or_else(|| {
4765            SidecarError::InvalidState(String::from("python subprocessRun requires a command"))
4766        })?;
4767        let (internal_bootstrap_env, cwd) = {
4768            let Some(vm) = self.vms.get(vm_id) else {
4769                return Ok(());
4770            };
4771            let Some(process) = vm.active_processes.get(process_id) else {
4772                return Ok(());
4773            };
4774            let virtual_home = guest_virtual_home(vm);
4775            let cwd = request.cwd.clone().or_else(|| {
4776                guest_runtime_path_for_host_path(
4777                    &vm.guest_env,
4778                    &virtual_home,
4779                    &vm.host_cwd,
4780                    &process.host_cwd.to_string_lossy(),
4781                )
4782            });
4783            (
4784                sanitize_javascript_child_process_internal_bootstrap_env(&vm.guest_env),
4785                cwd,
4786            )
4787        };
4788        let response = self
4789            .spawn_javascript_child_process_sync(
4790                vm_id,
4791                process_id,
4792                JavascriptChildProcessSpawnRequest {
4793                    command,
4794                    args: request.args.clone(),
4795                    options: JavascriptChildProcessSpawnOptions {
4796                        cwd,
4797                        env: request.env.clone(),
4798                        input: None,
4799                        internal_bootstrap_env,
4800                        shell: request.shell,
4801                        detached: false,
4802                        stdio: vec![
4803                            String::from("pipe"),
4804                            String::from("pipe"),
4805                            String::from("pipe"),
4806                        ],
4807                        timeout: None,
4808                        kill_signal: None,
4809                    },
4810                },
4811                request.max_buffer,
4812            )
4813            .map(|payload| PythonVfsRpcResponsePayload::SubprocessRun {
4814                exit_code: payload
4815                    .get("code")
4816                    .and_then(Value::as_i64)
4817                    .map(|value| value as i32)
4818                    .unwrap_or(1),
4819                stdout: payload
4820                    .get("stdout")
4821                    .and_then(Value::as_str)
4822                    .unwrap_or_default()
4823                    .to_owned(),
4824                stderr: payload
4825                    .get("stderr")
4826                    .and_then(Value::as_str)
4827                    .unwrap_or_default()
4828                    .to_owned(),
4829                max_buffer_exceeded: payload
4830                    .get("maxBufferExceeded")
4831                    .and_then(Value::as_bool)
4832                    .unwrap_or(false),
4833            });
4834
4835        self.respond_python_rpc(vm_id, process_id, request.id, response)
4836    }
4837
4838    fn respond_python_rpc(
4839        &mut self,
4840        vm_id: &str,
4841        process_id: &str,
4842        request_id: u64,
4843        response: Result<PythonVfsRpcResponsePayload, SidecarError>,
4844    ) -> Result<(), SidecarError> {
4845        let Some(vm) = self.vms.get_mut(vm_id) else {
4846            return Ok(());
4847        };
4848        let Some(process) = vm.active_processes.get_mut(process_id) else {
4849            return Ok(());
4850        };
4851        let result = match response {
4852            Ok(payload) => process
4853                .execution
4854                .respond_python_vfs_rpc_success(request_id, payload),
4855            Err(error) => process.execution.respond_python_vfs_rpc_error(
4856                request_id,
4857                "ERR_AGENTOS_PYTHON_VFS_RPC",
4858                error.to_string(),
4859            ),
4860        };
4861        match result {
4862            Ok(()) => Ok(()),
4863            Err(error) if is_broken_pipe_error(&error) => Ok(()),
4864            Err(error) => Err(error),
4865        }
4866    }
4867
4868    pub(crate) fn resolve_javascript_child_process_execution(
4869        &self,
4870        vm: &VmState,
4871        parent_env: &BTreeMap<String, String>,
4872        parent_guest_cwd: &str,
4873        parent_host_cwd: &Path,
4874        request: &JavascriptChildProcessSpawnRequest,
4875    ) -> Result<ResolvedChildProcessExecution, SidecarError> {
4876        let mut runtime_env = parent_env.clone();
4877        runtime_env.extend(request.options.internal_bootstrap_env.clone());
4878        let (guest_cwd, host_cwd_override) = request
4879            .options
4880            .cwd
4881            .as_deref()
4882            .map(|cwd| {
4883                let normalized_parent_host_cwd = normalize_host_path(parent_host_cwd);
4884                let requested_host_cwd = normalize_host_path(Path::new(cwd));
4885                if path_is_within_root(&requested_host_cwd, &normalized_parent_host_cwd) {
4886                    let relative = requested_host_cwd
4887                        .strip_prefix(&normalized_parent_host_cwd)
4888                        .unwrap_or_else(|_| Path::new(""));
4889                    let relative = relative.to_string_lossy().replace('\\', "/");
4890                    let guest_cwd = if relative.is_empty() {
4891                        parent_guest_cwd.to_owned()
4892                    } else {
4893                        normalize_path(&format!("{parent_guest_cwd}/{relative}"))
4894                    };
4895                    (guest_cwd, Some(requested_host_cwd))
4896                } else if Path::new(cwd).is_relative() {
4897                    (
4898                        normalize_path(&format!("{parent_guest_cwd}/{cwd}")),
4899                        Some(normalize_host_path(&parent_host_cwd.join(cwd))),
4900                    )
4901                } else {
4902                    (normalize_path(cwd), None)
4903                }
4904            })
4905            .unwrap_or_else(|| (parent_guest_cwd.to_owned(), None));
4906        let inherited_host_cwd = (host_cwd_override.is_none() && guest_cwd == parent_guest_cwd)
4907            .then(|| normalize_host_path(parent_host_cwd));
4908        let host_cwd = host_cwd_override
4909            .or(inherited_host_cwd)
4910            .or_else(|| {
4911                host_runtime_path_for_guest_path_with_env(
4912                    vm,
4913                    &runtime_env,
4914                    &guest_cwd,
4915                    parent_host_cwd,
4916                )
4917            })
4918            .unwrap_or_else(|| {
4919                let candidate = PathBuf::from(&guest_cwd);
4920                if guest_cwd == parent_guest_cwd {
4921                    normalize_host_path(parent_host_cwd)
4922                } else if candidate.is_absolute() {
4923                    shadow_path_for_guest(vm, &guest_cwd)
4924                } else {
4925                    vm.host_cwd.clone()
4926                }
4927            });
4928        let mut env = parent_env.clone();
4929        env.extend(request.options.env.clone());
4930        // Child JavaScript executions must resolve their own entrypoint/eval state.
4931        // Reusing the parent's values makes the sidecar load the wrong source file.
4932        env.remove("AGENTOS_GUEST_ENTRYPOINT");
4933        env.remove("AGENTOS_NODE_EVAL");
4934
4935        let (command, process_args) = if request.options.shell {
4936            let tokens = tokenize_shell_free_command(&request.command);
4937            let requires_shell = command_requires_shell(&request.command)
4938                || tokens.first().is_some_and(|command| {
4939                    is_posix_shell_builtin(command) || shell_first_token_requires_shell(command)
4940                });
4941            if requires_shell {
4942                if !vm.command_guest_paths.contains_key("sh") {
4943                    return Err(SidecarError::InvalidState(format!(
4944                        "shell-mode child_process command requires /bin/sh, which is not \
4945                         installed in this VM (install a software package that provides sh, \
4946                         for example @secure-exec/coreutils): {}",
4947                        request.command
4948                    )));
4949                }
4950                (
4951                    String::from("sh"),
4952                    vec![String::from("-c"), request.command.clone()],
4953                )
4954            } else {
4955                let Some((command, args)) = tokens.split_first() else {
4956                    return Err(SidecarError::InvalidState(String::from(
4957                        "child_process shell command must not be empty",
4958                    )));
4959                };
4960                (command.clone(), args.to_vec())
4961            }
4962        } else {
4963            (request.command.clone(), request.args.clone())
4964        };
4965        let process_args = apply_shell_cwd_prefix(&command, process_args, &guest_cwd);
4966        if is_tool_command(vm, &command) {
4967            let command = normalized_tool_command_name(&command).unwrap_or(command);
4968            return Ok(ResolvedChildProcessExecution {
4969                command: command.clone(),
4970                process_args: std::iter::once(command.clone())
4971                    .chain(process_args.iter().cloned())
4972                    .collect(),
4973                runtime: GuestRuntimeKind::JavaScript,
4974                entrypoint: command,
4975                execution_args: process_args,
4976                env,
4977                guest_cwd,
4978                host_cwd,
4979                wasm_permission_tier: None,
4980                tool_command: true,
4981            });
4982        }
4983
4984        if is_path_like_specifier(&command)
4985            && matches!(
4986                Path::new(&command).extension().and_then(|ext| ext.to_str()),
4987                Some("js" | "mjs" | "cjs" | "ts" | "mts" | "cts")
4988            )
4989        {
4990            let guest_entrypoint = if command.starts_with('/') {
4991                normalize_path(&command)
4992            } else if command.starts_with("file:") {
4993                normalize_path(command.trim_start_matches("file:"))
4994            } else {
4995                normalize_path(&format!("{guest_cwd}/{command}"))
4996            };
4997            let host_entrypoint = if command.starts_with("./") || command.starts_with("../") {
4998                normalize_host_path(&host_cwd.join(&command))
4999            } else {
5000                host_runtime_path_for_guest_path_with_env(
5001                    vm,
5002                    &runtime_env,
5003                    &guest_entrypoint,
5004                    parent_host_cwd,
5005                )
5006                .unwrap_or_else(|| {
5007                    let candidate = PathBuf::from(&guest_entrypoint);
5008                    if candidate.is_absolute() {
5009                        candidate
5010                    } else {
5011                        host_cwd.join(&guest_entrypoint)
5012                    }
5013                })
5014            };
5015            env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint);
5016            let guest_entrypoint = env.get("AGENTOS_GUEST_ENTRYPOINT").cloned();
5017            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
5018
5019            return Ok(ResolvedChildProcessExecution {
5020                command: command.clone(),
5021                process_args: std::iter::once(command)
5022                    .chain(process_args.iter().cloned())
5023                    .collect(),
5024                runtime: GuestRuntimeKind::JavaScript,
5025                entrypoint: host_entrypoint.to_string_lossy().into_owned(),
5026                execution_args: process_args,
5027                env,
5028                guest_cwd,
5029                host_cwd,
5030                wasm_permission_tier: None,
5031                tool_command: false,
5032            });
5033        }
5034
5035        if is_node_runtime_command(&command) {
5036            if let Some(cli) = resolve_host_node_cli_entrypoint(&command) {
5037                env.insert(
5038                    String::from("AGENTOS_NODE_EVAL"),
5039                    build_host_node_cli_eval(&cli),
5040                );
5041                prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
5042                add_runtime_guest_path_mapping(&mut env, &cli.guest_root, &cli.package_root);
5043                add_runtime_host_access_path(
5044                    &mut env,
5045                    "AGENTOS_EXTRA_FS_READ_PATHS",
5046                    &cli.package_root,
5047                    true,
5048                );
5049
5050                return Ok(ResolvedChildProcessExecution {
5051                    command: command.clone(),
5052                    process_args: std::iter::once(command.clone())
5053                        .chain(process_args.iter().cloned())
5054                        .collect(),
5055                    runtime: GuestRuntimeKind::JavaScript,
5056                    entrypoint: String::from("-e"),
5057                    execution_args: std::iter::once(cli.guest_entrypoint.clone())
5058                        .chain(process_args.iter().cloned())
5059                        .collect(),
5060                    env,
5061                    guest_cwd,
5062                    host_cwd,
5063                    wasm_permission_tier: None,
5064                    tool_command: false,
5065                });
5066            }
5067
5068            if process_args.is_empty() {
5069                env.insert(String::from("AGENTOS_NODE_EVAL"), String::new());
5070                prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
5071
5072                return Ok(ResolvedChildProcessExecution {
5073                    command: command.clone(),
5074                    process_args: vec![command.clone()],
5075                    runtime: GuestRuntimeKind::JavaScript,
5076                    entrypoint: String::from("-e"),
5077                    execution_args: Vec::new(),
5078                    env,
5079                    guest_cwd,
5080                    host_cwd,
5081                    wasm_permission_tier: None,
5082                    tool_command: false,
5083                });
5084            }
5085
5086            if let Some((entrypoint, execution_args)) =
5087                resolve_special_node_cli_invocation(&process_args, &mut env)
5088            {
5089                prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
5090
5091                return Ok(ResolvedChildProcessExecution {
5092                    command: command.clone(),
5093                    process_args: std::iter::once(command.clone())
5094                        .chain(process_args.iter().cloned())
5095                        .collect(),
5096                    runtime: GuestRuntimeKind::JavaScript,
5097                    entrypoint,
5098                    execution_args,
5099                    env,
5100                    guest_cwd,
5101                    host_cwd,
5102                    wasm_permission_tier: None,
5103                    tool_command: false,
5104                });
5105            }
5106
5107            let Some(entrypoint_specifier) = process_args.first() else {
5108                return Err(SidecarError::InvalidState(format!(
5109                    "{command} child_process spawn requires an entrypoint"
5110                )));
5111            };
5112
5113            let (entrypoint, execution_args) = if is_path_like_specifier(entrypoint_specifier) {
5114                let guest_entrypoint = if entrypoint_specifier.starts_with('/') {
5115                    normalize_path(entrypoint_specifier)
5116                } else if entrypoint_specifier.starts_with("file:") {
5117                    normalize_path(entrypoint_specifier.trim_start_matches("file:"))
5118                } else {
5119                    normalize_path(&format!("{guest_cwd}/{entrypoint_specifier}"))
5120                };
5121                let host_entrypoint = if entrypoint_specifier.starts_with("./")
5122                    || entrypoint_specifier.starts_with("../")
5123                {
5124                    normalize_host_path(&host_cwd.join(entrypoint_specifier))
5125                } else {
5126                    host_runtime_path_for_guest_path_with_env(
5127                        vm,
5128                        &runtime_env,
5129                        &guest_entrypoint,
5130                        parent_host_cwd,
5131                    )
5132                    .unwrap_or_else(|| {
5133                        let candidate = PathBuf::from(&guest_entrypoint);
5134                        if candidate.is_absolute() {
5135                            candidate
5136                        } else {
5137                            host_cwd.join(&guest_entrypoint)
5138                        }
5139                    })
5140                };
5141                env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint);
5142                (
5143                    host_entrypoint.to_string_lossy().into_owned(),
5144                    process_args.iter().skip(1).cloned().collect(),
5145                )
5146            } else {
5147                (
5148                    entrypoint_specifier.clone(),
5149                    process_args.iter().skip(1).cloned().collect(),
5150                )
5151            };
5152            let guest_entrypoint = env.get("AGENTOS_GUEST_ENTRYPOINT").cloned();
5153            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
5154
5155            return Ok(ResolvedChildProcessExecution {
5156                command: command.clone(),
5157                process_args: std::iter::once(command)
5158                    .chain(process_args.iter().cloned())
5159                    .collect(),
5160                runtime: GuestRuntimeKind::JavaScript,
5161                entrypoint,
5162                execution_args,
5163                env,
5164                guest_cwd,
5165                host_cwd,
5166                wasm_permission_tier: None,
5167                tool_command: false,
5168            });
5169        }
5170
5171        if command == PYTHON_COMMAND {
5172            return Err(SidecarError::InvalidState(String::from(
5173                "nested python child_process execution is not supported yet",
5174            )));
5175        }
5176
5177        let guest_entrypoint = resolve_guest_command_entrypoint(
5178            vm,
5179            &guest_cwd,
5180            &command,
5181            env.get("PATH").map(String::as_str),
5182        )
5183        .ok_or_else(|| SidecarError::InvalidState(format!("command not found: {command}")))?;
5184        let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint);
5185        let wasm_permission_tier = vm.command_permissions.get(&command).copied().or_else(|| {
5186            Path::new(&guest_entrypoint)
5187                .file_name()
5188                .and_then(|name| name.to_str())
5189                .and_then(|name| vm.command_permissions.get(name).copied())
5190        });
5191        if let Some((javascript_guest_entrypoint, javascript_host_entrypoint)) =
5192            resolve_javascript_command_entrypoint(vm, &guest_entrypoint, &host_entrypoint)
5193        {
5194            prepare_guest_runtime_env(
5195                vm,
5196                &mut env,
5197                &guest_cwd,
5198                &host_cwd,
5199                Some(javascript_guest_entrypoint),
5200            )?;
5201
5202            return Ok(ResolvedChildProcessExecution {
5203                command: command.clone(),
5204                process_args: std::iter::once(command)
5205                    .chain(process_args.iter().cloned())
5206                    .collect(),
5207                runtime: GuestRuntimeKind::JavaScript,
5208                entrypoint: javascript_host_entrypoint.to_string_lossy().into_owned(),
5209                execution_args: process_args,
5210                env,
5211                guest_cwd,
5212                host_cwd,
5213                wasm_permission_tier: None,
5214                tool_command: false,
5215            });
5216        }
5217        prepare_guest_runtime_env(
5218            vm,
5219            &mut env,
5220            &guest_cwd,
5221            &host_cwd,
5222            Some(guest_entrypoint.clone()),
5223        )?;
5224
5225        Ok(ResolvedChildProcessExecution {
5226            command: command.clone(),
5227            process_args: std::iter::once(command)
5228                .chain(process_args.iter().cloned())
5229                .collect(),
5230            runtime: GuestRuntimeKind::WebAssembly,
5231            entrypoint: host_entrypoint.to_string_lossy().into_owned(),
5232            execution_args: process_args,
5233            env,
5234            guest_cwd,
5235            host_cwd,
5236            wasm_permission_tier,
5237            tool_command: false,
5238        })
5239    }
5240
5241    pub(crate) fn spawn_javascript_child_process(
5242        &mut self,
5243        vm_id: &str,
5244        process_id: &str,
5245        request: JavascriptChildProcessSpawnRequest,
5246    ) -> Result<Value, SidecarError> {
5247        let resolved = {
5248            let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?;
5249            let parent = vm
5250                .active_processes
5251                .get(process_id)
5252                .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5253            self.resolve_javascript_child_process_execution(
5254                vm,
5255                &parent.env,
5256                &parent.guest_cwd,
5257                &parent.host_cwd,
5258                &request,
5259            )?
5260        };
5261        let (parent_kernel_pid, child_process_id) = {
5262            let vm = self
5263                .vms
5264                .get_mut(vm_id)
5265                .ok_or_else(|| missing_vm_error(vm_id))?;
5266            let process = vm
5267                .active_processes
5268                .get_mut(process_id)
5269                .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5270            (process.kernel_pid, process.allocate_child_process_id())
5271        };
5272        let sidecar_requests = self.sidecar_requests.clone();
5273        let vm = self
5274            .vms
5275            .get_mut(vm_id)
5276            .ok_or_else(|| missing_vm_error(vm_id))?;
5277        let (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd) = if resolved
5278            .tool_command
5279        {
5280            let tool_resolution = resolve_tool_command(
5281                vm,
5282                &resolved.command,
5283                &resolved.execution_args,
5284                Some(&resolved.guest_cwd),
5285            )?
5286            .ok_or_else(|| {
5287                SidecarError::InvalidState(format!(
5288                    "tool command no longer resolves: {}",
5289                    resolved.command
5290                ))
5291            })?;
5292            let kernel_handle = vm
5293                .kernel
5294                .create_virtual_process(
5295                    EXECUTION_DRIVER_NAME,
5296                    TOOL_DRIVER_NAME,
5297                    &resolved.command,
5298                    resolved.process_args.clone(),
5299                    VirtualProcessOptions {
5300                        parent_pid: Some(parent_kernel_pid),
5301                        env: resolved.env.clone(),
5302                        cwd: Some(resolved.guest_cwd.clone()),
5303                    },
5304                )
5305                .map_err(kernel_error)?;
5306            let kernel_pid = kernel_handle.pid();
5307            let tool_execution = ToolExecution::default();
5308            let cancelled = tool_execution.cancelled.clone();
5309            let pending_events = tool_execution.pending_events.clone();
5310            let events_overflowed = tool_execution.events_overflowed.clone();
5311            spawn_tool_process_events(ToolProcessEventRequest {
5312                sidecar_requests: sidecar_requests.clone(),
5313                connection_id: vm.connection_id.clone(),
5314                session_id: vm.session_id.clone(),
5315                vm_id: vm_id.to_owned(),
5316                tool_resolution,
5317                cancelled,
5318                pending_events,
5319                events_overflowed,
5320            });
5321            (
5322                kernel_pid,
5323                kernel_handle,
5324                ActiveExecution::Tool(tool_execution),
5325                None,
5326            )
5327        } else {
5328            let kernel_command = match resolved.runtime {
5329                GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND,
5330                GuestRuntimeKind::WebAssembly => WASM_COMMAND,
5331                GuestRuntimeKind::Python => {
5332                    unreachable!("python child_process execution is rejected")
5333                }
5334            };
5335            let kernel_handle = vm
5336                .kernel
5337                .spawn_process(
5338                    kernel_command,
5339                    resolved.process_args.clone(),
5340                    SpawnOptions {
5341                        requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
5342                        parent_pid: Some(parent_kernel_pid),
5343                        env: resolved.env.clone(),
5344                        cwd: Some(resolved.guest_cwd.clone()),
5345                    },
5346                )
5347                .map_err(kernel_error)?;
5348            let kernel_pid = kernel_handle.pid();
5349            if request.options.detached {
5350                vm.kernel
5351                    .setsid(EXECUTION_DRIVER_NAME, kernel_pid)
5352                    .map_err(kernel_error)?;
5353            }
5354            let mut execution_env = resolved.env.clone();
5355            execution_env.insert(
5356                String::from(EXECUTION_SANDBOX_ROOT_ENV),
5357                normalize_host_path(&vm.cwd).to_string_lossy().into_owned(),
5358            );
5359
5360            let execution = match resolved.runtime {
5361                GuestRuntimeKind::JavaScript => {
5362                    execution_env.extend(sanitize_javascript_child_process_internal_bootstrap_env(
5363                        &request.options.internal_bootstrap_env,
5364                    ));
5365                    execution_env.insert(
5366                        String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
5367                        String::from("1"),
5368                    );
5369                    let context =
5370                        self.javascript_engine
5371                            .create_context(CreateJavascriptContextRequest {
5372                                vm_id: vm_id.to_owned(),
5373                                bootstrap_module: None,
5374                                compile_cache_root: Some(
5375                                    self.cache_root.join("node-compile-cache"),
5376                                ),
5377                            });
5378                    let inline_code = load_javascript_entrypoint_source(
5379                        vm,
5380                        &resolved.host_cwd,
5381                        &resolved.entrypoint,
5382                        &execution_env,
5383                    );
5384                    prepare_javascript_shadow(vm, &resolved)?;
5385
5386                    let built_reader = build_module_reader(vm, &resolved);
5387                    let guest_reader = built_reader.clone().map(|reader| {
5388                        Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader))
5389                            as Box<dyn GuestModuleReader>
5390                    });
5391                    let module_reader = built_reader
5392                        .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
5393                    let execution = self
5394                        .javascript_engine
5395                        .start_execution_with_module_reader(
5396                            StartJavascriptExecutionRequest {
5397                                guest_runtime: guest_runtime_identity(
5398                                    vm,
5399                                    Some(u64::from(kernel_pid)),
5400                                    Some(u64::from(parent_kernel_pid)),
5401                                ),
5402                                vm_id: vm_id.to_owned(),
5403                                context_id: context.context_id,
5404                                argv: std::iter::once(resolved.entrypoint.clone())
5405                                    .chain(resolved.execution_args.clone())
5406                                    .collect(),
5407                                env: execution_env,
5408                                cwd: resolved.host_cwd.clone(),
5409                                limits: javascript_execution_limits(vm),
5410                                inline_code,
5411                            },
5412                            module_reader,
5413                            guest_reader,
5414                        )
5415                        .map_err(javascript_error)?;
5416                    ActiveExecution::Javascript(execution)
5417                }
5418                GuestRuntimeKind::WebAssembly => {
5419                    execution_env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
5420                    let wasm_limits = wasm_execution_limits(vm);
5421                    let wasm_guest_runtime = guest_runtime_identity(
5422                        vm,
5423                        Some(u64::from(kernel_pid)),
5424                        Some(u64::from(parent_kernel_pid)),
5425                    );
5426                    let context = self.wasm_engine.create_context(CreateWasmContextRequest {
5427                        vm_id: vm_id.to_owned(),
5428                        module_path: Some(resolved.entrypoint.clone()),
5429                    });
5430                    let execution = self
5431                        .wasm_engine
5432                        .start_execution(StartWasmExecutionRequest {
5433                            vm_id: vm_id.to_owned(),
5434                            context_id: context.context_id,
5435                            argv: resolved.process_args.clone(),
5436                            env: execution_env,
5437                            cwd: resolved.host_cwd.clone(),
5438                            permission_tier: execution_wasm_permission_tier(
5439                                resolved
5440                                    .wasm_permission_tier
5441                                    .unwrap_or(WasmPermissionTier::Full),
5442                            ),
5443                            limits: wasm_limits,
5444                            guest_runtime: wasm_guest_runtime,
5445                        })
5446                        .map_err(wasm_error)?;
5447                    ActiveExecution::Wasm(Box::new(execution))
5448                }
5449                GuestRuntimeKind::Python => {
5450                    unreachable!("python child_process execution is rejected")
5451                }
5452            };
5453            let kernel_stdin_writer_fd = match javascript_child_process_stdin_mode(&request) {
5454                "pipe" => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5455                "ignore" => {
5456                    vm.kernel
5457                        .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, 0)
5458                        .map_err(kernel_error)?;
5459                    None
5460                }
5461                "inherit" => None,
5462                _ => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5463            };
5464            (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd)
5465        };
5466
5467        let process = vm
5468            .active_processes
5469            .get_mut(process_id)
5470            .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5471        process.child_processes.insert(
5472            child_process_id.clone(),
5473            ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
5474                .with_detached(request.options.detached)
5475                .with_guest_cwd(resolved.guest_cwd.clone())
5476                .with_env(resolved.env.clone())
5477                .with_host_cwd(resolved.host_cwd.clone()),
5478        );
5479        if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd {
5480            process
5481                .child_processes
5482                .get_mut(&child_process_id)
5483                .ok_or_else(|| {
5484                    SidecarError::InvalidState(format!(
5485                        "child process {child_process_id} disappeared during spawn"
5486                    ))
5487                })?
5488                .kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd);
5489        }
5490        Ok(json!({
5491            "childId": child_process_id,
5492            "pid": kernel_pid,
5493            "command": resolved.command,
5494            "args": resolved.process_args,
5495        }))
5496    }
5497
5498    pub(crate) fn spawn_javascript_child_process_sync(
5499        &mut self,
5500        vm_id: &str,
5501        process_id: &str,
5502        request: JavascriptChildProcessSpawnRequest,
5503        max_buffer: Option<usize>,
5504    ) -> Result<Value, SidecarError> {
5505        let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?;
5506        let timeout_deadline = request
5507            .options
5508            .timeout
5509            .map(|timeout_ms| Instant::now() + Duration::from_millis(timeout_ms));
5510        let timeout_signal = request
5511            .options
5512            .kill_signal
5513            .clone()
5514            .unwrap_or_else(|| String::from("SIGTERM"));
5515        let spawned = self.spawn_javascript_child_process(vm_id, process_id, request)?;
5516        let child_process_id = spawned
5517            .get("childId")
5518            .and_then(Value::as_str)
5519            .ok_or_else(|| {
5520                SidecarError::InvalidState(String::from(
5521                    "child_process.spawn_sync response is missing childId",
5522                ))
5523            })?
5524            .to_owned();
5525
5526        if let Some(input) = sync_input.as_deref() {
5527            self.write_javascript_child_process_stdin(vm_id, process_id, &child_process_id, input)?;
5528        }
5529        self.close_javascript_child_process_stdin(vm_id, process_id, &child_process_id)?;
5530
5531        let max_buffer = max_buffer.unwrap_or(1024 * 1024);
5532        let mut stdout = Vec::new();
5533        let mut stderr = Vec::new();
5534        let mut max_buffer_exceeded = false;
5535        let mut kill_sent = false;
5536        let mut timed_out = false;
5537
5538        let exit_code = loop {
5539            let wait_ms = if let Some(deadline) = timeout_deadline {
5540                let now = Instant::now();
5541                if now >= deadline {
5542                    if !kill_sent {
5543                        timed_out = true;
5544                        self.kill_javascript_child_process(
5545                            vm_id,
5546                            process_id,
5547                            &child_process_id,
5548                            &timeout_signal,
5549                        )?;
5550                        kill_sent = true;
5551                    }
5552                    0
5553                } else {
5554                    u64::try_from(deadline.saturating_duration_since(now).as_millis().min(50))
5555                        .unwrap_or(50)
5556                }
5557            } else {
5558                50
5559            };
5560            let event =
5561                self.poll_javascript_child_process(vm_id, process_id, &child_process_id, wait_ms)?;
5562            if event.is_null() {
5563                continue;
5564            }
5565
5566            match event.get("type").and_then(Value::as_str) {
5567                Some("stdout") => {
5568                    let chunk = javascript_sync_rpc_bytes_arg(
5569                        &[event.get("data").cloned().unwrap_or(Value::Null)],
5570                        0,
5571                        "child_process.spawn_sync stdout",
5572                    )?;
5573                    stdout.extend_from_slice(&chunk);
5574                    if stdout.len() > max_buffer && !kill_sent {
5575                        max_buffer_exceeded = true;
5576                        self.kill_javascript_child_process(
5577                            vm_id,
5578                            process_id,
5579                            &child_process_id,
5580                            "SIGTERM",
5581                        )?;
5582                        kill_sent = true;
5583                    }
5584                }
5585                Some("stderr") => {
5586                    let chunk = javascript_sync_rpc_bytes_arg(
5587                        &[event.get("data").cloned().unwrap_or(Value::Null)],
5588                        0,
5589                        "child_process.spawn_sync stderr",
5590                    )?;
5591                    stderr.extend_from_slice(&chunk);
5592                    if stderr.len() > max_buffer && !kill_sent {
5593                        max_buffer_exceeded = true;
5594                        self.kill_javascript_child_process(
5595                            vm_id,
5596                            process_id,
5597                            &child_process_id,
5598                            "SIGTERM",
5599                        )?;
5600                        kill_sent = true;
5601                    }
5602                }
5603                Some("exit") => {
5604                    break event
5605                        .get("exitCode")
5606                        .and_then(Value::as_i64)
5607                        .map(|value| value as i32)
5608                        .unwrap_or(1);
5609                }
5610                _ => {}
5611            }
5612        };
5613
5614        Ok(json!({
5615            "stdout": String::from_utf8_lossy(&stdout),
5616            "stderr": String::from_utf8_lossy(&stderr),
5617            "code": exit_code,
5618            "signal": if timed_out { Value::String(timeout_signal) } else { Value::Null },
5619            "timedOut": timed_out,
5620            "maxBufferExceeded": max_buffer_exceeded,
5621        }))
5622    }
5623
5624    fn spawn_descendant_javascript_child_process(
5625        &mut self,
5626        vm_id: &str,
5627        process_id: &str,
5628        current_process_path: &[&str],
5629        request: JavascriptChildProcessSpawnRequest,
5630    ) -> Result<Value, SidecarError> {
5631        let current_process_label =
5632            Self::child_process_path_label(process_id, current_process_path);
5633        let (resolved, parent_kernel_pid) = {
5634            let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?;
5635            let root = vm
5636                .active_processes
5637                .get(process_id)
5638                .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5639            let parent =
5640                Self::active_process_by_path(root, current_process_path).ok_or_else(|| {
5641                    SidecarError::InvalidState(format!(
5642                        "unknown child process path {current_process_label} during nested spawn"
5643                    ))
5644                })?;
5645            (
5646                self.resolve_javascript_child_process_execution(
5647                    vm,
5648                    &parent.env,
5649                    &parent.guest_cwd,
5650                    &parent.host_cwd,
5651                    &request,
5652                )?,
5653                parent.kernel_pid,
5654            )
5655        };
5656
5657        let sidecar_requests = self.sidecar_requests.clone();
5658        let vm = self
5659            .vms
5660            .get_mut(vm_id)
5661            .ok_or_else(|| missing_vm_error(vm_id))?;
5662        let child_process_id = {
5663            let root = vm
5664                .active_processes
5665                .get_mut(process_id)
5666                .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5667            let parent =
5668                Self::active_process_by_path_mut(root, current_process_path).ok_or_else(|| {
5669                    SidecarError::InvalidState(format!(
5670                        "unknown child process path {current_process_label} during nested spawn"
5671                    ))
5672                })?;
5673            parent.allocate_child_process_id()
5674        };
5675        let mut child_path = current_process_path.to_vec();
5676        child_path.push(child_process_id.as_str());
5677        let (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd) = if resolved
5678            .tool_command
5679        {
5680            let tool_resolution = resolve_tool_command(
5681                vm,
5682                &resolved.command,
5683                &resolved.execution_args,
5684                Some(&resolved.guest_cwd),
5685            )?
5686            .ok_or_else(|| {
5687                SidecarError::InvalidState(format!(
5688                    "tool command no longer resolves: {}",
5689                    resolved.command
5690                ))
5691            })?;
5692            let kernel_handle = vm
5693                .kernel
5694                .create_virtual_process(
5695                    EXECUTION_DRIVER_NAME,
5696                    TOOL_DRIVER_NAME,
5697                    &resolved.command,
5698                    resolved.process_args.clone(),
5699                    VirtualProcessOptions {
5700                        parent_pid: Some(parent_kernel_pid),
5701                        env: resolved.env.clone(),
5702                        cwd: Some(resolved.guest_cwd.clone()),
5703                    },
5704                )
5705                .map_err(kernel_error)?;
5706            let kernel_pid = kernel_handle.pid();
5707            let tool_execution = ToolExecution::default();
5708            let cancelled = tool_execution.cancelled.clone();
5709            let pending_events = tool_execution.pending_events.clone();
5710            let events_overflowed = tool_execution.events_overflowed.clone();
5711            spawn_tool_process_events(ToolProcessEventRequest {
5712                sidecar_requests: sidecar_requests.clone(),
5713                connection_id: vm.connection_id.clone(),
5714                session_id: vm.session_id.clone(),
5715                vm_id: vm_id.to_owned(),
5716                tool_resolution,
5717                cancelled,
5718                pending_events,
5719                events_overflowed,
5720            });
5721            (
5722                kernel_pid,
5723                kernel_handle,
5724                ActiveExecution::Tool(tool_execution),
5725                None,
5726            )
5727        } else {
5728            let kernel_command = match resolved.runtime {
5729                GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND,
5730                GuestRuntimeKind::WebAssembly => WASM_COMMAND,
5731                GuestRuntimeKind::Python => {
5732                    unreachable!("python child_process execution is rejected")
5733                }
5734            };
5735            let kernel_handle = vm
5736                .kernel
5737                .spawn_process(
5738                    kernel_command,
5739                    resolved.process_args.clone(),
5740                    SpawnOptions {
5741                        requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
5742                        parent_pid: Some(parent_kernel_pid),
5743                        env: resolved.env.clone(),
5744                        cwd: Some(resolved.guest_cwd.clone()),
5745                    },
5746                )
5747                .map_err(kernel_error)?;
5748            let kernel_pid = kernel_handle.pid();
5749            if request.options.detached {
5750                vm.kernel
5751                    .setsid(EXECUTION_DRIVER_NAME, kernel_pid)
5752                    .map_err(kernel_error)?;
5753            }
5754            let mut execution_env = resolved.env.clone();
5755            execution_env.insert(
5756                String::from(EXECUTION_SANDBOX_ROOT_ENV),
5757                normalize_host_path(&vm.cwd).to_string_lossy().into_owned(),
5758            );
5759            let execution = match resolved.runtime {
5760                GuestRuntimeKind::JavaScript => {
5761                    execution_env.extend(sanitize_javascript_child_process_internal_bootstrap_env(
5762                        &request.options.internal_bootstrap_env,
5763                    ));
5764                    execution_env.insert(
5765                        String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
5766                        String::from("1"),
5767                    );
5768                    let context =
5769                        self.javascript_engine
5770                            .create_context(CreateJavascriptContextRequest {
5771                                vm_id: vm_id.to_owned(),
5772                                bootstrap_module: None,
5773                                compile_cache_root: Some(
5774                                    self.cache_root.join("node-compile-cache"),
5775                                ),
5776                            });
5777                    let inline_code = load_javascript_entrypoint_source(
5778                        vm,
5779                        &resolved.host_cwd,
5780                        &resolved.entrypoint,
5781                        &execution_env,
5782                    );
5783                    prepare_javascript_shadow(vm, &resolved)?;
5784
5785                    let built_reader = build_module_reader(vm, &resolved);
5786                    let guest_reader = built_reader.clone().map(|reader| {
5787                        Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader))
5788                            as Box<dyn GuestModuleReader>
5789                    });
5790                    let module_reader = built_reader
5791                        .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
5792                    let execution = self
5793                        .javascript_engine
5794                        .start_execution_with_module_reader(
5795                            StartJavascriptExecutionRequest {
5796                                guest_runtime: guest_runtime_identity(
5797                                    vm,
5798                                    Some(u64::from(kernel_pid)),
5799                                    Some(u64::from(parent_kernel_pid)),
5800                                ),
5801                                vm_id: vm_id.to_owned(),
5802                                context_id: context.context_id,
5803                                argv: std::iter::once(resolved.entrypoint.clone())
5804                                    .chain(resolved.execution_args.clone())
5805                                    .collect(),
5806                                env: execution_env,
5807                                cwd: resolved.host_cwd.clone(),
5808                                limits: javascript_execution_limits(vm),
5809                                inline_code,
5810                            },
5811                            module_reader,
5812                            guest_reader,
5813                        )
5814                        .map_err(javascript_error)?;
5815                    ActiveExecution::Javascript(execution)
5816                }
5817                GuestRuntimeKind::WebAssembly => {
5818                    execution_env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
5819                    let wasm_limits = wasm_execution_limits(vm);
5820                    let wasm_guest_runtime = guest_runtime_identity(
5821                        vm,
5822                        Some(u64::from(kernel_pid)),
5823                        Some(u64::from(parent_kernel_pid)),
5824                    );
5825                    let context = self.wasm_engine.create_context(CreateWasmContextRequest {
5826                        vm_id: vm_id.to_owned(),
5827                        module_path: Some(resolved.entrypoint.clone()),
5828                    });
5829                    let execution = self
5830                        .wasm_engine
5831                        .start_execution(StartWasmExecutionRequest {
5832                            vm_id: vm_id.to_owned(),
5833                            context_id: context.context_id,
5834                            argv: resolved.process_args.clone(),
5835                            env: execution_env,
5836                            cwd: resolved.host_cwd.clone(),
5837                            permission_tier: execution_wasm_permission_tier(
5838                                resolved
5839                                    .wasm_permission_tier
5840                                    .unwrap_or(WasmPermissionTier::Full),
5841                            ),
5842                            limits: wasm_limits,
5843                            guest_runtime: wasm_guest_runtime,
5844                        })
5845                        .map_err(wasm_error)?;
5846                    ActiveExecution::Wasm(Box::new(execution))
5847                }
5848                GuestRuntimeKind::Python => {
5849                    unreachable!("python child_process execution is rejected")
5850                }
5851            };
5852            let kernel_stdin_writer_fd = match javascript_child_process_stdin_mode(&request) {
5853                "pipe" => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5854                "ignore" => {
5855                    vm.kernel
5856                        .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, 0)
5857                        .map_err(kernel_error)?;
5858                    None
5859                }
5860                "inherit" => None,
5861                _ => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5862            };
5863            (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd)
5864        };
5865
5866        let root = vm
5867            .active_processes
5868            .get_mut(process_id)
5869            .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5870        let parent =
5871            Self::active_process_by_path_mut(root, current_process_path).ok_or_else(|| {
5872                SidecarError::InvalidState(format!(
5873                    "unknown child process path {current_process_label} during nested spawn"
5874                ))
5875            })?;
5876        parent.child_processes.insert(
5877            child_process_id.clone(),
5878            ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
5879                .with_detached(request.options.detached)
5880                .with_guest_cwd(resolved.guest_cwd.clone())
5881                .with_env(resolved.env.clone())
5882                .with_host_cwd(resolved.host_cwd.clone()),
5883        );
5884        if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd {
5885            parent
5886                .child_processes
5887                .get_mut(&child_process_id)
5888                .ok_or_else(|| {
5889                    SidecarError::InvalidState(format!(
5890                        "child process {child_process_id} disappeared during nested spawn"
5891                    ))
5892                })?
5893                .kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd);
5894        }
5895        Ok(json!({
5896            "childId": child_process_id,
5897            "pid": kernel_pid,
5898            "command": resolved.command,
5899            "args": resolved.process_args,
5900        }))
5901    }
5902
5903    fn spawn_descendant_javascript_child_process_sync(
5904        &mut self,
5905        vm_id: &str,
5906        process_id: &str,
5907        current_process_path: &[&str],
5908        request: JavascriptChildProcessSpawnRequest,
5909        max_buffer: Option<usize>,
5910    ) -> Result<Value, SidecarError> {
5911        let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?;
5912        let timeout_deadline = request
5913            .options
5914            .timeout
5915            .map(|timeout_ms| Instant::now() + Duration::from_millis(timeout_ms));
5916        let timeout_signal = request
5917            .options
5918            .kill_signal
5919            .clone()
5920            .unwrap_or_else(|| String::from("SIGTERM"));
5921        let spawned = self.spawn_descendant_javascript_child_process(
5922            vm_id,
5923            process_id,
5924            current_process_path,
5925            request,
5926        )?;
5927        let child_process_id = spawned
5928            .get("childId")
5929            .and_then(Value::as_str)
5930            .ok_or_else(|| {
5931                SidecarError::InvalidState(String::from(
5932                    "child_process.spawn_sync response is missing childId",
5933                ))
5934            })?
5935            .to_owned();
5936
5937        if let Some(input) = sync_input.as_deref() {
5938            self.write_descendant_javascript_child_process_stdin(
5939                vm_id,
5940                process_id,
5941                current_process_path,
5942                &child_process_id,
5943                input,
5944            )?;
5945        }
5946        self.close_descendant_javascript_child_process_stdin(
5947            vm_id,
5948            process_id,
5949            current_process_path,
5950            &child_process_id,
5951        )?;
5952
5953        let max_buffer = max_buffer.unwrap_or(1024 * 1024);
5954        let mut stdout = Vec::new();
5955        let mut stderr = Vec::new();
5956        let mut max_buffer_exceeded = false;
5957        let mut kill_sent = false;
5958        let mut timed_out = false;
5959
5960        let exit_code = loop {
5961            let wait_ms = if let Some(deadline) = timeout_deadline {
5962                let now = Instant::now();
5963                if now >= deadline {
5964                    if !kill_sent {
5965                        timed_out = true;
5966                        self.kill_descendant_javascript_child_process(
5967                            vm_id,
5968                            process_id,
5969                            current_process_path,
5970                            &child_process_id,
5971                            &timeout_signal,
5972                        )?;
5973                        kill_sent = true;
5974                    }
5975                    0
5976                } else {
5977                    u64::try_from(deadline.saturating_duration_since(now).as_millis().min(50))
5978                        .unwrap_or(50)
5979                }
5980            } else {
5981                50
5982            };
5983            let event = self.poll_descendant_javascript_child_process(
5984                vm_id,
5985                process_id,
5986                current_process_path,
5987                &child_process_id,
5988                wait_ms,
5989            )?;
5990            if event.is_null() {
5991                continue;
5992            }
5993
5994            match event.get("type").and_then(Value::as_str) {
5995                Some("stdout") => {
5996                    let chunk = javascript_sync_rpc_bytes_arg(
5997                        &[event.get("data").cloned().unwrap_or(Value::Null)],
5998                        0,
5999                        "child_process.spawn_sync stdout",
6000                    )?;
6001                    stdout.extend_from_slice(&chunk);
6002                    if stdout.len() > max_buffer && !kill_sent {
6003                        max_buffer_exceeded = true;
6004                        self.kill_descendant_javascript_child_process(
6005                            vm_id,
6006                            process_id,
6007                            current_process_path,
6008                            &child_process_id,
6009                            "SIGTERM",
6010                        )?;
6011                        kill_sent = true;
6012                    }
6013                }
6014                Some("stderr") => {
6015                    let chunk = javascript_sync_rpc_bytes_arg(
6016                        &[event.get("data").cloned().unwrap_or(Value::Null)],
6017                        0,
6018                        "child_process.spawn_sync stderr",
6019                    )?;
6020                    stderr.extend_from_slice(&chunk);
6021                    if stderr.len() > max_buffer && !kill_sent {
6022                        max_buffer_exceeded = true;
6023                        self.kill_descendant_javascript_child_process(
6024                            vm_id,
6025                            process_id,
6026                            current_process_path,
6027                            &child_process_id,
6028                            "SIGTERM",
6029                        )?;
6030                        kill_sent = true;
6031                    }
6032                }
6033                Some("exit") => {
6034                    break event
6035                        .get("exitCode")
6036                        .and_then(Value::as_i64)
6037                        .map(|value| value as i32)
6038                        .unwrap_or(1);
6039                }
6040                _ => {}
6041            }
6042        };
6043
6044        Ok(json!({
6045            "stdout": String::from_utf8_lossy(&stdout),
6046            "stderr": String::from_utf8_lossy(&stderr),
6047            "code": exit_code,
6048            "signal": if timed_out { Value::String(timeout_signal) } else { Value::Null },
6049            "timedOut": timed_out,
6050            "maxBufferExceeded": max_buffer_exceeded,
6051        }))
6052    }
6053
6054    fn handle_descendant_javascript_child_process_rpc(
6055        &mut self,
6056        vm_id: &str,
6057        process_id: &str,
6058        current_process_path: &[&str],
6059        request: &JavascriptSyncRpcRequest,
6060    ) -> Result<Value, SidecarError> {
6061        match request.method.as_str() {
6062            "child_process.spawn" => {
6063                let Some(vm) = self.vms.get(vm_id) else {
6064                    return Ok(Value::Null);
6065                };
6066                let (payload, _) = parse_javascript_child_process_spawn_request(vm, &request.args)?;
6067                self.spawn_descendant_javascript_child_process(
6068                    vm_id,
6069                    process_id,
6070                    current_process_path,
6071                    payload,
6072                )
6073            }
6074            "child_process.spawn_sync" => {
6075                let Some(vm) = self.vms.get(vm_id) else {
6076                    return Ok(Value::Null);
6077                };
6078                let (payload, max_buffer) =
6079                    parse_javascript_child_process_spawn_request(vm, &request.args)?;
6080                self.spawn_descendant_javascript_child_process_sync(
6081                    vm_id,
6082                    process_id,
6083                    current_process_path,
6084                    payload,
6085                    max_buffer,
6086                )
6087            }
6088            "child_process.poll" => {
6089                let child_process_id =
6090                    javascript_sync_rpc_arg_str(&request.args, 0, "child_process.poll child id")?;
6091                let wait_ms = javascript_sync_rpc_arg_u64_optional(
6092                    &request.args,
6093                    1,
6094                    "child_process.poll wait ms",
6095                )?
6096                .unwrap_or_default();
6097                self.poll_descendant_javascript_child_process(
6098                    vm_id,
6099                    process_id,
6100                    current_process_path,
6101                    child_process_id,
6102                    wait_ms,
6103                )
6104            }
6105            "child_process.write_stdin" => {
6106                let child_process_id = javascript_sync_rpc_arg_str(
6107                    &request.args,
6108                    0,
6109                    "child_process.write_stdin child id",
6110                )?;
6111                let chunk = javascript_sync_rpc_bytes_arg(
6112                    &request.args,
6113                    1,
6114                    "child_process.write_stdin chunk",
6115                )?;
6116                self.write_descendant_javascript_child_process_stdin(
6117                    vm_id,
6118                    process_id,
6119                    current_process_path,
6120                    child_process_id,
6121                    &chunk,
6122                )?;
6123                Ok(Value::Null)
6124            }
6125            "child_process.close_stdin" => {
6126                let child_process_id = javascript_sync_rpc_arg_str(
6127                    &request.args,
6128                    0,
6129                    "child_process.close_stdin child id",
6130                )?;
6131                self.close_descendant_javascript_child_process_stdin(
6132                    vm_id,
6133                    process_id,
6134                    current_process_path,
6135                    child_process_id,
6136                )?;
6137                Ok(Value::Null)
6138            }
6139            "child_process.kill" => {
6140                let child_process_id =
6141                    javascript_sync_rpc_arg_str(&request.args, 0, "child_process.kill child id")?;
6142                let signal =
6143                    javascript_sync_rpc_arg_str(&request.args, 1, "child_process.kill signal")?;
6144                self.kill_descendant_javascript_child_process(
6145                    vm_id,
6146                    process_id,
6147                    current_process_path,
6148                    child_process_id,
6149                    signal,
6150                )?;
6151                Ok(Value::Null)
6152            }
6153            _ => Err(SidecarError::InvalidState(format!(
6154                "unsupported nested child process RPC method {}",
6155                request.method
6156            ))),
6157        }
6158    }
6159
6160    fn poll_descendant_javascript_child_process(
6161        &mut self,
6162        vm_id: &str,
6163        process_id: &str,
6164        current_process_path: &[&str],
6165        child_process_id: &str,
6166        wait_ms: u64,
6167    ) -> Result<Value, SidecarError> {
6168        let mut child_path = current_process_path.to_vec();
6169        child_path.push(child_process_id);
6170        let child_gone_error = || javascript_child_process_gone_error(process_id, &child_path);
6171        let deadline = Instant::now() + Duration::from_millis(wait_ms);
6172        let mut polled_once = false;
6173
6174        loop {
6175            self.drain_queued_descendant_javascript_child_process_events(
6176                vm_id,
6177                process_id,
6178                &child_path,
6179            )?;
6180            enum ChildPollResult {
6181                Event(Box<Option<ActiveExecutionEvent>>),
6182                RecoverRuntimeExit,
6183                Timeout,
6184            }
6185            let wait = if wait_ms == 0 {
6186                Duration::ZERO
6187            } else {
6188                deadline.saturating_duration_since(Instant::now())
6189            };
6190            let poll_result = {
6191                let Some(vm) = self.vms.get_mut(vm_id) else {
6192                    return Ok(Value::Null);
6193                };
6194                let Some(parent) =
6195                    Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6196                else {
6197                    return Err(child_gone_error());
6198                };
6199                let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6200                    return Err(child_gone_error());
6201                };
6202                if let Some(event) = child.pending_execution_events.pop_front() {
6203                    ChildPollResult::Event(Box::new(Some(event)))
6204                } else if polled_once && wait.is_zero() {
6205                    ChildPollResult::Timeout
6206                } else {
6207                    polled_once = true;
6208                    match child.execution.poll_event_blocking(wait) {
6209                        Ok(Some(event)) => ChildPollResult::Event(Box::new(Some(event))),
6210                        Ok(None) => ChildPollResult::RecoverRuntimeExit,
6211                        Err(SidecarError::Execution(message))
6212                            if (child.runtime == GuestRuntimeKind::JavaScript
6213                                && closed_javascript_event_channel(&message))
6214                                || (child.runtime == GuestRuntimeKind::Python
6215                                    && closed_python_event_channel(&message))
6216                                || (child.runtime == GuestRuntimeKind::WebAssembly
6217                                    && closed_wasm_event_channel(&message)) =>
6218                        {
6219                            ChildPollResult::RecoverRuntimeExit
6220                        }
6221                        Err(error) => return Err(error),
6222                    }
6223                }
6224            };
6225            let event = match poll_result {
6226                ChildPollResult::Event(event) => *event,
6227                ChildPollResult::Timeout => return Ok(Value::Null),
6228                ChildPollResult::RecoverRuntimeExit => self
6229                    .recover_descendant_runtime_child_process_event(
6230                        vm_id,
6231                        process_id,
6232                        current_process_path,
6233                        child_process_id,
6234                        wait.as_millis().try_into().unwrap_or(u64::MAX),
6235                    )?,
6236            };
6237
6238            let Some(event) = event else {
6239                return Ok(Value::Null);
6240            };
6241
6242            match event {
6243                ActiveExecutionEvent::Stdout(chunk) => {
6244                    return Ok(json!({
6245                        "type": "stdout",
6246                        "data": javascript_sync_rpc_bytes_value(&chunk),
6247                    }));
6248                }
6249                ActiveExecutionEvent::Stderr(chunk) => {
6250                    return Ok(json!({
6251                        "type": "stderr",
6252                        "data": javascript_sync_rpc_bytes_value(&chunk),
6253                    }));
6254                }
6255                ActiveExecutionEvent::Exited(exit_code) => {
6256                    let had_trailing_events = {
6257                        let Some(vm) = self.vms.get_mut(vm_id) else {
6258                            return Ok(Value::Null);
6259                        };
6260                        let Some(parent) = Self::descendant_parent_process_mut(
6261                            vm,
6262                            process_id,
6263                            current_process_path,
6264                        ) else {
6265                            return Ok(Value::Null);
6266                        };
6267                        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6268                            return Ok(Value::Null);
6269                        };
6270                        let deadline = Instant::now() + Duration::from_millis(150);
6271                        loop {
6272                            let wait = deadline.saturating_duration_since(Instant::now());
6273                            let next = poll_child_execution_after_exit(child, wait)?;
6274                            let Some(next) = next else {
6275                                break;
6276                            };
6277                            if matches!(next, ActiveExecutionEvent::Exited(_)) {
6278                                continue;
6279                            }
6280                            child.queue_pending_execution_event(next)?;
6281                            if Instant::now() >= deadline {
6282                                break;
6283                            }
6284                        }
6285                        if !child.pending_execution_events.is_empty() {
6286                            child.queue_pending_execution_event(ActiveExecutionEvent::Exited(
6287                                exit_code,
6288                            ))?;
6289                            true
6290                        } else {
6291                            false
6292                        }
6293                    };
6294                    if had_trailing_events {
6295                        continue;
6296                    }
6297
6298                    let parent_signal_key =
6299                        Self::child_process_signal_key(process_id, current_process_path);
6300                    let Some(vm) = self.vms.get_mut(vm_id) else {
6301                        return Ok(Value::Null);
6302                    };
6303                    let signal_name = {
6304                        let Some(parent) = Self::descendant_parent_process_mut(
6305                            vm,
6306                            process_id,
6307                            current_process_path,
6308                        ) else {
6309                            return Ok(Value::Null);
6310                        };
6311                        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6312                            return Ok(Value::Null);
6313                        };
6314                        child.pending_self_signal_exit.take().and_then(|signal| {
6315                            if exit_code == 128 + signal {
6316                                canonical_signal_name(signal).map(str::to_owned)
6317                            } else {
6318                                None
6319                            }
6320                        })
6321                    };
6322                    let (parent_runtime_pid, parent_v8_signal_session, should_signal_parent) = {
6323                        let Some(parent) =
6324                            Self::descendant_parent_process(vm, process_id, current_process_path)
6325                        else {
6326                            return Ok(Value::Null);
6327                        };
6328                        (
6329                            parent.execution.child_pid(),
6330                            parent.execution.javascript_v8_session_handle().filter(|_| {
6331                                matches!(
6332                                    &parent.execution,
6333                                    ActiveExecution::Javascript(execution)
6334                                        if execution.uses_shared_v8_runtime()
6335                                )
6336                            }),
6337                            vm.signal_states
6338                                .get(parent_signal_key)
6339                                .and_then(|handlers| handlers.get(&(libc::SIGCHLD as u32)))
6340                                .is_some_and(|registration| {
6341                                    registration.action != SignalDispositionAction::Default
6342                                }),
6343                        )
6344                    };
6345                    let Some(parent) =
6346                        Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6347                    else {
6348                        return Ok(Value::Null);
6349                    };
6350                    let Some(mut child) = parent.child_processes.remove(child_process_id) else {
6351                        return Ok(Value::Null);
6352                    };
6353                    let child_process_label =
6354                        Self::child_process_path_label(process_id, &child_path);
6355                    let detached_children =
6356                        Self::adopt_detached_child_processes(&child_process_label, &mut child);
6357                    sync_process_host_writes_to_kernel(vm, &child)?;
6358                    terminate_child_process_tree(&mut vm.kernel, &mut child);
6359                    child.kernel_handle.finish(exit_code);
6360                    let _ = vm.kernel.wait_and_reap(child.kernel_pid);
6361                    vm.signal_states.remove(child_process_id);
6362                    for (detached_process_id, detached_child) in detached_children {
6363                        vm.detached_child_processes
6364                            .insert(detached_process_id.clone());
6365                        vm.active_processes
6366                            .insert(detached_process_id, detached_child);
6367                    }
6368                    if should_signal_parent {
6369                        if let Some(session) = parent_v8_signal_session {
6370                            dispatch_v8_session_signal_async(session, libc::SIGCHLD);
6371                        } else {
6372                            signal_runtime_process(parent_runtime_pid, libc::SIGCHLD)?;
6373                        }
6374                    }
6375                    let mut payload = Map::new();
6376                    payload.insert(String::from("type"), Value::String(String::from("exit")));
6377                    payload.insert(String::from("exitCode"), Value::from(exit_code));
6378                    if let Some(signal_name) = signal_name {
6379                        payload.insert(String::from("signal"), Value::String(signal_name));
6380                    }
6381                    return Ok(Value::Object(payload));
6382                }
6383                ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
6384                    let mut current_child_path = current_process_path.to_vec();
6385                    current_child_path.push(child_process_id);
6386                    let response = if request.method == "process.signal_state" {
6387                        let (signal, registration) =
6388                            parse_process_signal_state_request(&request.args)?;
6389                        let Some(vm) = self.vms.get_mut(vm_id) else {
6390                            return Ok(Value::Null);
6391                        };
6392                        let signal_key =
6393                            Self::child_process_signal_key(process_id, &current_child_path)
6394                                .to_owned();
6395                        apply_process_signal_state_update(
6396                            &mut vm.signal_states,
6397                            &signal_key,
6398                            signal,
6399                            registration,
6400                        );
6401                        Ok(Value::Null)
6402                    } else if request.method == "process.kill" {
6403                        self.handle_descendant_process_kill_rpc(
6404                            vm_id,
6405                            process_id,
6406                            current_process_path,
6407                            child_process_id,
6408                            &request,
6409                        )
6410                    } else if request.method.starts_with("child_process.") {
6411                        self.handle_descendant_javascript_child_process_rpc(
6412                            vm_id,
6413                            process_id,
6414                            &current_child_path,
6415                            &request,
6416                        )
6417                    } else {
6418                        let Some(vm) = self.vms.get_mut(vm_id) else {
6419                            return Ok(Value::Null);
6420                        };
6421                        let resource_limits = vm.kernel.resource_limits().clone();
6422                        let network_counts = vm_network_resource_counts(vm);
6423                        let socket_paths = build_javascript_socket_path_context(vm)?;
6424                        let Some(root) = vm.active_processes.get_mut(process_id) else {
6425                            return Ok(Value::Null);
6426                        };
6427                        let Some(parent) =
6428                            Self::active_process_by_path_mut(root, current_process_path)
6429                        else {
6430                            return Ok(Value::Null);
6431                        };
6432                        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6433                            return Ok(Value::Null);
6434                        };
6435                        service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
6436                            bridge: &self.bridge,
6437                            vm_id,
6438                            dns: &vm.dns,
6439                            socket_paths: &socket_paths,
6440                            kernel: &mut vm.kernel,
6441                            process: child,
6442                            sync_request: &request,
6443                            resource_limits: &resource_limits,
6444                            network_counts,
6445                        })
6446                    };
6447
6448                    let Some(vm) = self.vms.get_mut(vm_id) else {
6449                        return Ok(Value::Null);
6450                    };
6451                    let Some(parent) =
6452                        Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6453                    else {
6454                        return Ok(Value::Null);
6455                    };
6456                    let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6457                        return Ok(Value::Null);
6458                    };
6459                    let parent_signal_event = response.as_ref().ok().and_then(|result| {
6460                        let target_path_label =
6461                            Self::child_process_path_label(process_id, current_process_path);
6462                        if request.method != "process.kill"
6463                            || result.get("action").and_then(Value::as_str) != Some("user")
6464                            || result.get("targetProcessPath").and_then(Value::as_str)
6465                                != Some(target_path_label.as_str())
6466                        {
6467                            return None;
6468                        }
6469                        Some(json!({
6470                            "type": "signal",
6471                            "signal": result.get("signal").and_then(Value::as_str).unwrap_or_default(),
6472                            "number": result.get("number").and_then(Value::as_i64).unwrap_or_default(),
6473                        }))
6474                    });
6475                    match response {
6476                        Ok(result) => child
6477                            .execution
6478                            .respond_javascript_sync_rpc_success(request.id, result)
6479                            .or_else(ignore_stale_javascript_sync_rpc_response)?,
6480                        Err(error) => child
6481                            .execution
6482                            .respond_javascript_sync_rpc_error(
6483                                request.id,
6484                                javascript_sync_rpc_error_code(&error),
6485                                error.to_string(),
6486                            )
6487                            .or_else(ignore_stale_javascript_sync_rpc_response)?,
6488                    }
6489                    if let Some(event) = parent_signal_event {
6490                        return Ok(event);
6491                    }
6492                }
6493                ActiveExecutionEvent::PythonVfsRpcRequest(_) => {
6494                    return Err(SidecarError::InvalidState(String::from(
6495                        "nested Python child_process execution is not supported yet",
6496                    )));
6497                }
6498                ActiveExecutionEvent::SignalState {
6499                    signal,
6500                    registration,
6501                } => {
6502                    let Some(vm) = self.vms.get_mut(vm_id) else {
6503                        return Ok(Value::Null);
6504                    };
6505                    let signal_key =
6506                        Self::child_process_signal_key(process_id, &child_path).to_owned();
6507                    apply_process_signal_state_update(
6508                        &mut vm.signal_states,
6509                        &signal_key,
6510                        signal,
6511                        registration.clone(),
6512                    );
6513                    return Ok(json!({
6514                        "type": "signal_state",
6515                        "signal": signal,
6516                        "registration": registration,
6517                    }));
6518                }
6519            }
6520        }
6521    }
6522
6523    fn recover_descendant_runtime_child_process_event(
6524        &mut self,
6525        vm_id: &str,
6526        process_id: &str,
6527        current_process_path: &[&str],
6528        child_process_id: &str,
6529        wait_ms: u64,
6530    ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
6531        let (
6532            parent_kernel_pid,
6533            child_kernel_pid,
6534            child_runtime_pid,
6535            child_runtime,
6536            child_shared_runtime,
6537        ) = {
6538            let mut child_path = current_process_path.to_vec();
6539            child_path.push(child_process_id);
6540            let Some(vm) = self.vms.get_mut(vm_id) else {
6541                return Ok(None);
6542            };
6543            let Some(parent) =
6544                Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6545            else {
6546                return Err(javascript_child_process_gone_error(process_id, &child_path));
6547            };
6548            let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6549                return Err(javascript_child_process_gone_error(process_id, &child_path));
6550            };
6551            (
6552                parent.kernel_pid,
6553                child.kernel_pid,
6554                child.execution.child_pid(),
6555                child.runtime.clone(),
6556                child.execution.uses_shared_v8_runtime(),
6557            )
6558        };
6559        if child_runtime != GuestRuntimeKind::JavaScript
6560            && child_runtime != GuestRuntimeKind::Python
6561            && child_runtime != GuestRuntimeKind::WebAssembly
6562        {
6563            return Ok(None);
6564        }
6565        let wait_deadline = Instant::now() + Duration::from_millis(wait_ms.min(25));
6566        loop {
6567            let Some(vm) = self.vms.get_mut(vm_id) else {
6568                return Ok(None);
6569            };
6570            if let Some(process_info) = vm.kernel.list_processes().get(&child_kernel_pid) {
6571                if process_info.status == ProcessStatus::Exited {
6572                    return Ok(Some(ActiveExecutionEvent::Exited(
6573                        process_info.exit_code.unwrap_or(0),
6574                    )));
6575                }
6576            }
6577            if let Some(wait_result) = vm
6578                .kernel
6579                .waitpid_with_options(
6580                    EXECUTION_DRIVER_NAME,
6581                    parent_kernel_pid,
6582                    child_kernel_pid as i32,
6583                    WaitPidFlags::WNOHANG,
6584                )
6585                .map_err(kernel_error)?
6586            {
6587                return Ok(Some(ActiveExecutionEvent::Exited(wait_result.status)));
6588            }
6589
6590            if !child_shared_runtime && child_runtime_pid != 0 {
6591                if let Some(status) = runtime_child_exit_status(child_runtime_pid)? {
6592                    return Ok(Some(ActiveExecutionEvent::Exited(status)));
6593                }
6594                if !runtime_child_is_alive(child_runtime_pid)? {
6595                    return Ok(Some(ActiveExecutionEvent::Exited(0)));
6596                }
6597            }
6598            if Instant::now() >= wait_deadline {
6599                return Ok(None);
6600            }
6601            std::thread::sleep(Duration::from_millis(5));
6602        }
6603    }
6604
6605    fn write_descendant_javascript_child_process_stdin(
6606        &mut self,
6607        vm_id: &str,
6608        process_id: &str,
6609        current_process_path: &[&str],
6610        child_process_id: &str,
6611        chunk: &[u8],
6612    ) -> Result<(), SidecarError> {
6613        let mut child_path = current_process_path.to_vec();
6614        child_path.push(child_process_id);
6615        let Some(vm) = self.vms.get_mut(vm_id) else {
6616            return Err(javascript_child_process_gone_error(process_id, &child_path));
6617        };
6618        let Some(root) = vm.active_processes.get_mut(process_id) else {
6619            return Err(javascript_child_process_gone_error(process_id, &child_path));
6620        };
6621        let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
6622            return Err(javascript_child_process_gone_error(process_id, &child_path));
6623        };
6624        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6625            return Err(javascript_child_process_gone_error(process_id, &child_path));
6626        };
6627        if let Err(error) = child.execution.write_stdin(chunk) {
6628            if is_broken_pipe_error(&error) {
6629                return Ok(());
6630            }
6631            return Err(error);
6632        }
6633        write_kernel_process_stdin(&mut vm.kernel, child, chunk)
6634    }
6635
6636    fn close_descendant_javascript_child_process_stdin(
6637        &mut self,
6638        vm_id: &str,
6639        process_id: &str,
6640        current_process_path: &[&str],
6641        child_process_id: &str,
6642    ) -> Result<(), SidecarError> {
6643        let mut child_path = current_process_path.to_vec();
6644        child_path.push(child_process_id);
6645        let Some(vm) = self.vms.get_mut(vm_id) else {
6646            return Err(javascript_child_process_gone_error(process_id, &child_path));
6647        };
6648        let Some(root) = vm.active_processes.get_mut(process_id) else {
6649            return Err(javascript_child_process_gone_error(process_id, &child_path));
6650        };
6651        let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
6652            return Err(javascript_child_process_gone_error(process_id, &child_path));
6653        };
6654        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6655            return Err(javascript_child_process_gone_error(process_id, &child_path));
6656        };
6657        child.execution.close_stdin()?;
6658        close_kernel_process_stdin(&mut vm.kernel, child)
6659    }
6660
6661    fn kill_descendant_javascript_child_process(
6662        &mut self,
6663        vm_id: &str,
6664        process_id: &str,
6665        current_process_path: &[&str],
6666        child_process_id: &str,
6667        signal: &str,
6668    ) -> Result<(), SidecarError> {
6669        let signal_name = signal.to_owned();
6670        let signal = parse_signal(signal)?;
6671        let Some(vm) = self.vms.get_mut(vm_id) else {
6672            return Ok(());
6673        };
6674        let Some(root) = vm.active_processes.get_mut(process_id) else {
6675            return Ok(());
6676        };
6677        let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
6678            return Ok(());
6679        };
6680        let source_pid = parent.kernel_pid;
6681        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6682            return Ok(());
6683        };
6684        terminate_tracked_child_process_for_signal(&mut vm.kernel, child, signal)?;
6685        let child_process_label = if current_process_path.is_empty() {
6686            child_process_id.to_owned()
6687        } else {
6688            format!("{}/{}", current_process_path.join("/"), child_process_id)
6689        };
6690        emit_security_audit_event(
6691            &self.bridge,
6692            vm_id,
6693            "security.process.kill",
6694            audit_fields([
6695                (String::from("source"), String::from("guest_child_process")),
6696                (String::from("source_pid"), source_pid.to_string()),
6697                (String::from("target_pid"), child.kernel_pid.to_string()),
6698                (String::from("process_id"), process_id.to_owned()),
6699                (String::from("child_process_id"), child_process_label),
6700                (String::from("signal"), signal_name),
6701            ]),
6702        );
6703        Ok(())
6704    }
6705
6706    fn handle_descendant_process_kill_rpc(
6707        &mut self,
6708        vm_id: &str,
6709        process_id: &str,
6710        current_process_path: &[&str],
6711        child_process_id: &str,
6712        request: &JavascriptSyncRpcRequest,
6713    ) -> Result<Value, SidecarError> {
6714        let target_pid = javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?;
6715        let signal_name = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?;
6716        let signal = parse_signal(signal_name)?;
6717
6718        let mut source_path = current_process_path.to_vec();
6719        source_path.push(child_process_id);
6720
6721        if signal != 0 && target_pid < 0 {
6722            let pgid = target_pid.unsigned_abs();
6723            let caller_kernel_pid = {
6724                let Some(vm) = self.vms.get(vm_id) else {
6725                    return Err(SidecarError::InvalidState(String::from(
6726                        "ESRCH: unknown VM during process.kill",
6727                    )));
6728                };
6729                let Some(root) = vm.active_processes.get(process_id) else {
6730                    return Err(SidecarError::InvalidState(format!(
6731                        "ESRCH: unknown process {process_id} during process.kill",
6732                    )));
6733                };
6734                let Some(source) = Self::active_process_by_path(root, &source_path) else {
6735                    return Err(SidecarError::InvalidState(format!(
6736                        "ESRCH: unknown child process {child_process_id} during process.kill",
6737                    )));
6738                };
6739                source.kernel_pid
6740            };
6741            let caller_is_member =
6742                self.signal_vm_process_group(vm_id, caller_kernel_pid, pgid, signal_name)?;
6743            if !caller_is_member {
6744                return Ok(Value::Null);
6745            }
6746            let Some(vm) = self.vms.get_mut(vm_id) else {
6747                return Ok(Value::Null);
6748            };
6749            let Some(root) = vm.active_processes.get_mut(process_id) else {
6750                return Ok(Value::Null);
6751            };
6752            let Some(source) = Self::active_process_by_path_mut(root, &source_path) else {
6753                return Ok(Value::Null);
6754            };
6755            source.pending_self_signal_exit = None;
6756            if !matches!(
6757                canonical_signal_name(signal),
6758                Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
6759            ) {
6760                source.pending_self_signal_exit = Some(signal);
6761            }
6762            return Ok(json!({
6763                "self": true,
6764                "action": "default",
6765            }));
6766        }
6767
6768        let Some(vm) = self.vms.get_mut(vm_id) else {
6769            return Err(SidecarError::InvalidState(String::from(
6770                "ESRCH: unknown VM during process.kill",
6771            )));
6772        };
6773
6774        if signal == 0 {
6775            vm.kernel
6776                .signal_process(EXECUTION_DRIVER_NAME, target_pid, signal)
6777                .map_err(kernel_error)?;
6778            return Ok(Value::Null);
6779        }
6780
6781        let target_kernel_pid = u32::try_from(target_pid).map_err(|_| {
6782            SidecarError::InvalidState(format!("EINVAL: invalid process pid {target_pid}"))
6783        })?;
6784        let (source_pid, located_target_path) = {
6785            let Some(root) = vm.active_processes.get(process_id) else {
6786                return Err(SidecarError::InvalidState(format!(
6787                    "ESRCH: unknown process {process_id} during process.kill",
6788                )));
6789            };
6790            let Some(source) = Self::active_process_by_path(root, &source_path) else {
6791                return Err(SidecarError::InvalidState(format!(
6792                    "ESRCH: unknown child process {child_process_id} during process.kill",
6793                )));
6794            };
6795            vm.kernel
6796                .signal_process(EXECUTION_DRIVER_NAME, target_pid, 0)
6797                .map_err(kernel_error)?;
6798            (
6799                source.kernel_pid,
6800                Self::active_process_path_by_kernel_pid(root, target_kernel_pid),
6801            )
6802        };
6803        let Some(target_path) = located_target_path else {
6804            // The target is alive but not part of this root's process tree.
6805            // Resolve it VM-wide so cross-tree pids and untracked kernel
6806            // processes still receive the signal.
6807            self.signal_vm_kernel_pid(vm_id, target_kernel_pid, signal_name)?;
6808            return Ok(Value::Null);
6809        };
6810        let Some(vm) = self.vms.get_mut(vm_id) else {
6811            return Err(SidecarError::InvalidState(String::from(
6812                "ESRCH: unknown VM during process.kill",
6813            )));
6814        };
6815
6816        if source_pid == target_kernel_pid {
6817            let Some(root) = vm.active_processes.get_mut(process_id) else {
6818                return Ok(Value::Null);
6819            };
6820            let Some(source) = Self::active_process_by_path_mut(root, &source_path) else {
6821                return Ok(Value::Null);
6822            };
6823            source.pending_self_signal_exit = None;
6824            if !matches!(
6825                canonical_signal_name(signal),
6826                Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
6827            ) {
6828                source.pending_self_signal_exit = Some(signal);
6829            }
6830            return Ok(json!({
6831                "self": true,
6832                "action": "default",
6833            }));
6834        }
6835
6836        let signal_key = target_path.last().map(String::as_str).unwrap_or(process_id);
6837        let registration = vm
6838            .signal_states
6839            .get(signal_key)
6840            .and_then(|handlers| handlers.get(&(signal as u32)))
6841            .cloned();
6842
6843        let action = match registration
6844            .as_ref()
6845            .map(|registration| &registration.action)
6846        {
6847            Some(SignalDispositionAction::Ignore) => "ignore",
6848            Some(SignalDispositionAction::User) => {
6849                let Some(root) = vm.active_processes.get_mut(process_id) else {
6850                    return Ok(Value::Null);
6851                };
6852                let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path)
6853                else {
6854                    return Err(SidecarError::InvalidState(format!(
6855                        "ESRCH: unknown process pid {target_pid}"
6856                    )));
6857                };
6858                if let Some(session) = target.execution.javascript_v8_session_handle().filter(
6859                    |_| matches!(&target.execution, ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime())
6860                        || matches!(&target.execution, ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime()),
6861                ) {
6862                    dispatch_v8_session_signal_async(session, signal);
6863                } else if !dispatch_v8_process_signal(target, signal)? {
6864                    return Err(SidecarError::InvalidState(format!(
6865                        "unsupported guest signal delivery for pid {target_pid}"
6866                    )));
6867                }
6868                "user"
6869            }
6870            Some(SignalDispositionAction::Default) | None
6871                if matches!(
6872                    canonical_signal_name(signal),
6873                    Some("SIGWINCH" | "SIGCHLD" | "SIGURG")
6874                ) =>
6875            {
6876                "ignore"
6877            }
6878            Some(SignalDispositionAction::Default) | None => {
6879                let Some(root) = vm.active_processes.get_mut(process_id) else {
6880                    return Ok(Value::Null);
6881                };
6882                let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path)
6883                else {
6884                    return Err(SidecarError::InvalidState(format!(
6885                        "ESRCH: unknown process pid {target_pid}"
6886                    )));
6887                };
6888                apply_active_process_default_signal(&mut vm.kernel, target, signal)?;
6889                "default"
6890            }
6891        };
6892
6893        let target_path_label = Self::child_process_path_label(
6894            process_id,
6895            &target_path.iter().map(String::as_str).collect::<Vec<_>>(),
6896        );
6897        emit_security_audit_event(
6898            &self.bridge,
6899            vm_id,
6900            "security.process.kill",
6901            audit_fields([
6902                (String::from("source"), String::from("guest_process")),
6903                (String::from("source_pid"), source_pid.to_string()),
6904                (String::from("target_pid"), target_pid.to_string()),
6905                (String::from("process_id"), process_id.to_owned()),
6906                (
6907                    String::from("target_process_path"),
6908                    target_path_label.clone(),
6909                ),
6910                (String::from("signal"), signal_name.to_owned()),
6911            ]),
6912        );
6913
6914        Ok(json!({
6915            "self": false,
6916            "action": action,
6917            "signal": signal_name,
6918            "number": signal,
6919            "targetProcessPath": target_path_label,
6920        }))
6921    }
6922
6923    pub(crate) fn poll_javascript_child_process(
6924        &mut self,
6925        vm_id: &str,
6926        process_id: &str,
6927        child_process_id: &str,
6928        wait_ms: u64,
6929    ) -> Result<Value, SidecarError> {
6930        self.poll_descendant_javascript_child_process(
6931            vm_id,
6932            process_id,
6933            &[],
6934            child_process_id,
6935            wait_ms,
6936        )
6937    }
6938
6939    pub(crate) fn write_javascript_child_process_stdin(
6940        &mut self,
6941        vm_id: &str,
6942        process_id: &str,
6943        child_process_id: &str,
6944        chunk: &[u8],
6945    ) -> Result<(), SidecarError> {
6946        let Some(vm) = self.vms.get_mut(vm_id) else {
6947            return Err(javascript_child_process_gone_error(
6948                process_id,
6949                &[child_process_id],
6950            ));
6951        };
6952        let Some(child) = vm
6953            .active_processes
6954            .get_mut(process_id)
6955            .ok_or_else(|| missing_process_error(vm_id, process_id))?
6956            .child_processes
6957            .get_mut(child_process_id)
6958        else {
6959            return Err(javascript_child_process_gone_error(
6960                process_id,
6961                &[child_process_id],
6962            ));
6963        };
6964        if let Err(error) = child.execution.write_stdin(chunk) {
6965            if is_broken_pipe_error(&error) {
6966                return Ok(());
6967            }
6968            return Err(error);
6969        }
6970        write_kernel_process_stdin(&mut vm.kernel, child, chunk)
6971    }
6972
6973    pub(crate) fn close_javascript_child_process_stdin(
6974        &mut self,
6975        vm_id: &str,
6976        process_id: &str,
6977        child_process_id: &str,
6978    ) -> Result<(), SidecarError> {
6979        let Some(vm) = self.vms.get_mut(vm_id) else {
6980            return Err(javascript_child_process_gone_error(
6981                process_id,
6982                &[child_process_id],
6983            ));
6984        };
6985        let Some(child) = vm
6986            .active_processes
6987            .get_mut(process_id)
6988            .ok_or_else(|| missing_process_error(vm_id, process_id))?
6989            .child_processes
6990            .get_mut(child_process_id)
6991        else {
6992            return Err(javascript_child_process_gone_error(
6993                process_id,
6994                &[child_process_id],
6995            ));
6996        };
6997        child.execution.close_stdin()?;
6998        close_kernel_process_stdin(&mut vm.kernel, child)
6999    }
7000
7001    pub(crate) fn kill_javascript_child_process(
7002        &mut self,
7003        vm_id: &str,
7004        process_id: &str,
7005        child_process_id: &str,
7006        signal: &str,
7007    ) -> Result<(), SidecarError> {
7008        let signal_name = signal.to_owned();
7009        let signal = parse_signal(signal)?;
7010        let Some(vm) = self.vms.get_mut(vm_id) else {
7011            return Ok(());
7012        };
7013        let process = vm
7014            .active_processes
7015            .get_mut(process_id)
7016            .ok_or_else(|| missing_process_error(vm_id, process_id))?;
7017        let source_pid = process.kernel_pid;
7018        let child = process
7019            .child_processes
7020            .get_mut(child_process_id)
7021            .ok_or_else(|| {
7022                SidecarError::InvalidState(format!(
7023                    "unknown child process {child_process_id} during kill"
7024                ))
7025            })?;
7026        terminate_tracked_child_process_for_signal(&mut vm.kernel, child, signal)?;
7027        emit_security_audit_event(
7028            &self.bridge,
7029            vm_id,
7030            "security.process.kill",
7031            audit_fields([
7032                (String::from("source"), String::from("guest_child_process")),
7033                (String::from("source_pid"), source_pid.to_string()),
7034                (String::from("target_pid"), child.kernel_pid.to_string()),
7035                (String::from("process_id"), process_id.to_owned()),
7036                (
7037                    String::from("child_process_id"),
7038                    child_process_id.to_owned(),
7039                ),
7040                (String::from("signal"), signal_name),
7041            ]),
7042        );
7043        Ok(())
7044    }
7045
7046    /// Delivers a signal to one kernel pid inside a VM, resolving the target
7047    /// through the active-process tree first so tracked sidecar executions get
7048    /// the same termination handling as a direct `child_process.kill`.
7049    /// Untracked kernel processes (for example WASM subprocess trees) receive
7050    /// the signal through the kernel process table directly.
7051    pub(crate) fn signal_vm_kernel_pid(
7052        &mut self,
7053        vm_id: &str,
7054        target_kernel_pid: u32,
7055        signal_name: &str,
7056    ) -> Result<(), SidecarError> {
7057        let signal = parse_signal(signal_name)?;
7058        let located = {
7059            let Some(vm) = self.vms.get(vm_id) else {
7060                return Err(SidecarError::InvalidState(String::from(
7061                    "ESRCH: unknown VM during process.kill",
7062                )));
7063            };
7064            let alive = vm
7065                .kernel
7066                .list_processes()
7067                .get(&target_kernel_pid)
7068                .is_some_and(|info| info.status != ProcessStatus::Exited);
7069            if !alive {
7070                return Err(SidecarError::InvalidState(format!(
7071                    "ESRCH: no such process {target_kernel_pid}"
7072                )));
7073            }
7074            vm.active_processes.iter().find_map(|(process_id, root)| {
7075                Self::active_process_path_by_kernel_pid(root, target_kernel_pid)
7076                    .map(|path| (process_id.clone(), path))
7077            })
7078        };
7079
7080        match located {
7081            Some((process_id, path)) if path.is_empty() => {
7082                self.kill_process_internal(vm_id, &process_id, signal_name)
7083            }
7084            Some((process_id, path)) => {
7085                let Some(vm) = self.vms.get_mut(vm_id) else {
7086                    return Ok(());
7087                };
7088                let Some(root) = vm.active_processes.get_mut(&process_id) else {
7089                    return Ok(());
7090                };
7091                let Some(target) = Self::active_process_by_owned_path_mut(root, &path) else {
7092                    return Err(SidecarError::InvalidState(format!(
7093                        "ESRCH: no such process {target_kernel_pid}"
7094                    )));
7095                };
7096                terminate_tracked_child_process_for_signal(&mut vm.kernel, target, signal)?;
7097                emit_security_audit_event(
7098                    &self.bridge,
7099                    vm_id,
7100                    "security.process.kill",
7101                    audit_fields([
7102                        (String::from("source"), String::from("guest_process")),
7103                        (String::from("target_pid"), target_kernel_pid.to_string()),
7104                        (String::from("process_id"), process_id),
7105                        (String::from("signal"), signal_name.to_owned()),
7106                    ]),
7107                );
7108                Ok(())
7109            }
7110            None => {
7111                let Some(vm) = self.vms.get_mut(vm_id) else {
7112                    return Ok(());
7113                };
7114                let target_pid = i32::try_from(target_kernel_pid).map_err(|_| {
7115                    SidecarError::InvalidState(format!(
7116                        "EINVAL: invalid process pid {target_kernel_pid}"
7117                    ))
7118                })?;
7119                vm.kernel
7120                    .signal_process(EXECUTION_DRIVER_NAME, target_pid, signal)
7121                    .map_err(kernel_error)?;
7122                emit_security_audit_event(
7123                    &self.bridge,
7124                    vm_id,
7125                    "security.process.kill",
7126                    audit_fields([
7127                        (String::from("source"), String::from("guest_process")),
7128                        (String::from("target_pid"), target_kernel_pid.to_string()),
7129                        (String::from("signal"), signal_name.to_owned()),
7130                    ]),
7131                );
7132                Ok(())
7133            }
7134        }
7135    }
7136
7137    /// Delivers a signal to every live member of a VM process group, matching
7138    /// Linux `kill(-pgid, sig)` semantics. Returns whether the caller itself
7139    /// is a member of the group so entry points can apply self-signal
7140    /// delivery; the caller is intentionally skipped here.
7141    pub(crate) fn signal_vm_process_group(
7142        &mut self,
7143        vm_id: &str,
7144        caller_kernel_pid: u32,
7145        pgid: u32,
7146        signal_name: &str,
7147    ) -> Result<bool, SidecarError> {
7148        parse_signal(signal_name)?;
7149        let members = {
7150            let Some(vm) = self.vms.get(vm_id) else {
7151                return Err(SidecarError::InvalidState(String::from(
7152                    "ESRCH: unknown VM during process.kill",
7153                )));
7154            };
7155            vm.kernel
7156                .list_processes()
7157                .into_iter()
7158                .filter(|(_, info)| info.pgid == pgid && info.status != ProcessStatus::Exited)
7159                .map(|(pid, _)| pid)
7160                .collect::<Vec<_>>()
7161        };
7162        if members.is_empty() {
7163            return Err(SidecarError::InvalidState(format!(
7164                "ESRCH: no such process group {pgid}"
7165            )));
7166        }
7167
7168        let mut caller_is_member = false;
7169        for member_pid in members {
7170            if member_pid == caller_kernel_pid {
7171                caller_is_member = true;
7172                continue;
7173            }
7174            match self.signal_vm_kernel_pid(vm_id, member_pid, signal_name) {
7175                Ok(()) => {}
7176                // Group members can exit while the group is being signaled. A
7177                // vanished member is not an error for the group kill overall.
7178                Err(error) if sidecar_error_is_esrch(&error) => {}
7179                Err(error) => return Err(error),
7180            }
7181        }
7182        Ok(caller_is_member)
7183    }
7184}
7185
7186/// Applies a kill signal to a tracked child execution. Shared-runtime
7187/// executions for lethal signals are terminated directly with a synthetic
7188/// signal exit so child polls observe a prompt close; everything else routes
7189/// through the kernel process table.
7190fn terminate_tracked_child_process_for_signal(
7191    kernel: &mut SidecarKernel,
7192    child: &mut ActiveProcess,
7193    signal: i32,
7194) -> Result<(), SidecarError> {
7195    let should_terminate_shared_runtime = child.execution.uses_shared_v8_runtime()
7196        && signal != 0
7197        && !matches!(
7198            signal,
7199            libc::SIGHUP
7200                | libc::SIGINT
7201                | libc::SIGTERM
7202                | libc::SIGCHLD
7203                | libc::SIGWINCH
7204                | libc::SIGSTOP
7205                | libc::SIGCONT
7206        );
7207    if should_terminate_shared_runtime {
7208        child.execution.terminate()?;
7209        child.pending_self_signal_exit = Some(signal);
7210        child.queue_pending_execution_event(ActiveExecutionEvent::Exited(128 + signal))?;
7211    } else {
7212        kernel
7213            .kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, signal)
7214            .map_err(kernel_error)?;
7215    }
7216    Ok(())
7217}
7218
7219fn sidecar_error_is_esrch(error: &SidecarError) -> bool {
7220    error.to_string().contains("ESRCH")
7221}
7222
7223fn apply_active_process_default_signal(
7224    kernel: &mut SidecarKernel,
7225    process: &mut ActiveProcess,
7226    signal: i32,
7227) -> Result<(), SidecarError> {
7228    if matches!(signal, libc::SIGSTOP | libc::SIGCONT) {
7229        return kernel
7230            .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal)
7231            .map_err(kernel_error);
7232    }
7233
7234    if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
7235        close_kernel_process_stdin(kernel, process)?;
7236    }
7237
7238    if process.execution.uses_shared_v8_runtime() {
7239        process.execution.terminate()?;
7240        if signal != 0 && matches!(process.execution, ActiveExecution::Wasm(_)) {
7241            process.queue_pending_execution_event(ActiveExecutionEvent::Exited(128 + signal))?;
7242        }
7243        return Ok(());
7244    }
7245
7246    kernel
7247        .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal)
7248        .map_err(kernel_error)
7249}
7250
7251fn map_wasm_signal_registration(
7252    registration: secure_exec_execution::wasm::WasmSignalHandlerRegistration,
7253) -> SignalHandlerRegistration {
7254    SignalHandlerRegistration {
7255        action: match registration.action {
7256            secure_exec_execution::wasm::WasmSignalDispositionAction::Default => {
7257                crate::protocol::SignalDispositionAction::Default
7258            }
7259            secure_exec_execution::wasm::WasmSignalDispositionAction::Ignore => {
7260                crate::protocol::SignalDispositionAction::Ignore
7261            }
7262            secure_exec_execution::wasm::WasmSignalDispositionAction::User => {
7263                crate::protocol::SignalDispositionAction::User
7264            }
7265        },
7266        mask: registration.mask,
7267        flags: registration.flags,
7268    }
7269}
7270
7271fn parse_process_signal_state_request(
7272    args: &[Value],
7273) -> Result<(u32, SignalHandlerRegistration), SidecarError> {
7274    let signal = javascript_sync_rpc_arg_u32(args, 0, "process.signal_state signal")?;
7275    let action = javascript_sync_rpc_arg_str(args, 1, "process.signal_state action")?;
7276    let mask_json = javascript_sync_rpc_arg_str(args, 2, "process.signal_state mask")?;
7277    let flags = javascript_sync_rpc_arg_u32(args, 3, "process.signal_state flags")?;
7278    let mask: Vec<u32> = serde_json::from_str(mask_json).map_err(|error| {
7279        SidecarError::InvalidState(format!(
7280            "process.signal_state mask must be valid JSON: {error}"
7281        ))
7282    })?;
7283    let action = match action.trim().to_ascii_lowercase().as_str() {
7284        "default" => SignalDispositionAction::Default,
7285        "ignore" => SignalDispositionAction::Ignore,
7286        "user" => SignalDispositionAction::User,
7287        other => {
7288            return Err(SidecarError::InvalidState(format!(
7289                "unsupported process.signal_state action {other}"
7290            )));
7291        }
7292    };
7293
7294    Ok((
7295        signal,
7296        SignalHandlerRegistration {
7297            action,
7298            mask,
7299            flags,
7300        },
7301    ))
7302}
7303
7304fn apply_process_signal_state_update(
7305    signal_states: &mut BTreeMap<String, BTreeMap<u32, SignalHandlerRegistration>>,
7306    process_id: &str,
7307    signal: u32,
7308    registration: SignalHandlerRegistration,
7309) {
7310    if registration.action == SignalDispositionAction::Default
7311        && registration.mask.is_empty()
7312        && registration.flags == 0
7313    {
7314        let remove_process_entry = signal_states
7315            .get_mut(process_id)
7316            .map(|handlers| {
7317                handlers.remove(&signal);
7318                handlers.is_empty()
7319            })
7320            .unwrap_or(false);
7321        if remove_process_entry {
7322            signal_states.remove(process_id);
7323        }
7324        return;
7325    }
7326
7327    signal_states
7328        .entry(process_id.to_owned())
7329        .or_default()
7330        .insert(signal, registration);
7331}
7332
7333fn map_node_signal_registration(
7334    registration: NodeSignalHandlerRegistration,
7335) -> SignalHandlerRegistration {
7336    SignalHandlerRegistration {
7337        action: match registration.action {
7338            NodeSignalDispositionAction::Default => SignalDispositionAction::Default,
7339            NodeSignalDispositionAction::Ignore => SignalDispositionAction::Ignore,
7340            NodeSignalDispositionAction::User => SignalDispositionAction::User,
7341        },
7342        mask: registration.mask,
7343        flags: registration.flags,
7344    }
7345}
7346
7347fn javascript_child_process_sync_input_bytes(
7348    value: Option<&Value>,
7349) -> Result<Option<Vec<u8>>, SidecarError> {
7350    let Some(value) = value else {
7351        return Ok(None);
7352    };
7353
7354    match value {
7355        Value::Null => Ok(None),
7356        Value::String(text) => Ok(Some(text.as_bytes().to_vec())),
7357        other => javascript_sync_rpc_bytes_arg(
7358            std::slice::from_ref(other),
7359            0,
7360            "child_process.spawn_sync input",
7361        )
7362        .map(Some),
7363    }
7364}
7365
7366// bridge_permissions moved to crate::bridge
7367
7368// reconcile_mounts, resolve_cwd moved to crate::vm
7369
7370fn resolve_execute_request(
7371    vm: &VmState,
7372    payload: &ExecuteRequest,
7373) -> Result<ResolvedChildProcessExecution, SidecarError> {
7374    let payload_env: BTreeMap<String, String> = payload
7375        .env
7376        .iter()
7377        .map(|(k, v)| (k.clone(), v.clone()))
7378        .collect();
7379    if let Some(command) = payload.command.as_deref() {
7380        return resolve_command_execution(
7381            vm,
7382            command,
7383            &payload.args,
7384            &payload_env,
7385            payload.cwd.as_deref(),
7386            payload.wasm_permission_tier,
7387        );
7388    }
7389
7390    let runtime = payload.runtime.clone().ok_or_else(|| {
7391        SidecarError::InvalidState(String::from("execute requires either command or runtime"))
7392    })?;
7393    let entrypoint = payload.entrypoint.clone().ok_or_else(|| {
7394        SidecarError::InvalidState(String::from(
7395            "execute requires either command or entrypoint",
7396        ))
7397    })?;
7398    let (guest_cwd, host_cwd, allow_host_path_overrides) =
7399        resolve_execution_cwds(vm, payload.cwd.as_deref());
7400    let mut env = vm.guest_env.clone();
7401    env.extend(payload_env.clone());
7402
7403    let requested_host_entrypoint = resolve_host_entrypoint_within_vm_host_cwd(vm, &entrypoint);
7404    if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
7405        let requested_cwd = payload.cwd.as_deref().unwrap_or(guest_cwd.as_str());
7406        return Err(SidecarError::InvalidState(format!(
7407            "execution cwd {requested_cwd} is outside sandbox root {}",
7408            vm.host_cwd.to_string_lossy()
7409        )));
7410    }
7411    let host_entrypoint_override = allow_host_path_overrides
7412        .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, &entrypoint))
7413        .flatten();
7414
7415    let guest_entrypoint = host_entrypoint_override
7416        .as_ref()
7417        .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
7418        .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, &entrypoint));
7419    prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
7420
7421    Ok(ResolvedChildProcessExecution {
7422        command: match runtime {
7423            GuestRuntimeKind::JavaScript => String::from(JAVASCRIPT_COMMAND),
7424            GuestRuntimeKind::Python => String::from(PYTHON_COMMAND),
7425            GuestRuntimeKind::WebAssembly => String::from(WASM_COMMAND),
7426        },
7427        process_args: std::iter::once(entrypoint.clone())
7428            .chain(payload.args.iter().cloned())
7429            .collect(),
7430        runtime,
7431        entrypoint: host_entrypoint_override
7432            .map(|(_, host_entrypoint)| host_entrypoint)
7433            .unwrap_or(entrypoint),
7434        execution_args: payload.args.clone(),
7435        env,
7436        guest_cwd,
7437        host_cwd,
7438        wasm_permission_tier: payload.wasm_permission_tier,
7439        tool_command: false,
7440    })
7441}
7442
7443fn resolve_command_execution(
7444    vm: &VmState,
7445    command: &str,
7446    args: &[String],
7447    extra_env: &BTreeMap<String, String>,
7448    cwd: Option<&str>,
7449    explicit_wasm_permission_tier: Option<WasmPermissionTier>,
7450) -> Result<ResolvedChildProcessExecution, SidecarError> {
7451    let (guest_cwd, host_cwd, allow_host_path_overrides) = resolve_execution_cwds(vm, cwd);
7452    let mut env = vm.guest_env.clone();
7453    env.extend(extra_env.clone());
7454    let args = apply_shell_cwd_prefix(command, args.to_vec(), &guest_cwd);
7455
7456    if is_tool_command(vm, command) {
7457        let command = normalized_tool_command_name(command).unwrap_or_else(|| command.to_owned());
7458        return Ok(ResolvedChildProcessExecution {
7459            command: command.clone(),
7460            process_args: std::iter::once(command.clone())
7461                .chain(args.iter().cloned())
7462                .collect(),
7463            runtime: GuestRuntimeKind::JavaScript,
7464            entrypoint: command,
7465            execution_args: args,
7466            env,
7467            guest_cwd,
7468            host_cwd,
7469            wasm_permission_tier: None,
7470            tool_command: true,
7471        });
7472    }
7473
7474    if is_node_runtime_command(command) {
7475        if let Some(cli) = resolve_host_node_cli_entrypoint(command) {
7476            env.insert(
7477                String::from("AGENTOS_NODE_EVAL"),
7478                build_host_node_cli_eval(&cli),
7479            );
7480            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
7481            add_runtime_guest_path_mapping(&mut env, &cli.guest_root, &cli.package_root);
7482            add_runtime_host_access_path(
7483                &mut env,
7484                "AGENTOS_EXTRA_FS_READ_PATHS",
7485                &cli.package_root,
7486                true,
7487            );
7488
7489            return Ok(ResolvedChildProcessExecution {
7490                command: String::from(JAVASCRIPT_COMMAND),
7491                process_args: std::iter::once(command.to_owned())
7492                    .chain(args.iter().cloned())
7493                    .collect(),
7494                runtime: GuestRuntimeKind::JavaScript,
7495                entrypoint: String::from("-e"),
7496                execution_args: std::iter::once(cli.guest_entrypoint.clone())
7497                    .chain(args.iter().cloned())
7498                    .collect(),
7499                env,
7500                guest_cwd,
7501                host_cwd,
7502                wasm_permission_tier: None,
7503                tool_command: false,
7504            });
7505        }
7506
7507        if args.is_empty() {
7508            env.insert(String::from("AGENTOS_NODE_EVAL"), String::new());
7509            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
7510
7511            return Ok(ResolvedChildProcessExecution {
7512                command: String::from(JAVASCRIPT_COMMAND),
7513                process_args: vec![command.to_owned()],
7514                runtime: GuestRuntimeKind::JavaScript,
7515                entrypoint: String::from("-e"),
7516                execution_args: Vec::new(),
7517                env,
7518                guest_cwd,
7519                host_cwd,
7520                wasm_permission_tier: None,
7521                tool_command: false,
7522            });
7523        }
7524
7525        if let Some((entrypoint, execution_args)) =
7526            resolve_special_node_cli_invocation(&args, &mut env)
7527        {
7528            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
7529
7530            return Ok(ResolvedChildProcessExecution {
7531                command: String::from(JAVASCRIPT_COMMAND),
7532                process_args: std::iter::once(command.to_owned())
7533                    .chain(args.iter().cloned())
7534                    .collect(),
7535                runtime: GuestRuntimeKind::JavaScript,
7536                entrypoint,
7537                execution_args,
7538                env,
7539                guest_cwd,
7540                host_cwd,
7541                wasm_permission_tier: None,
7542                tool_command: false,
7543            });
7544        }
7545
7546        let Some(entrypoint_specifier) = args.first() else {
7547            return Err(SidecarError::InvalidState(format!(
7548                "{command} execution requires an entrypoint"
7549            )));
7550        };
7551
7552        let (entrypoint, execution_args, guest_entrypoint) = {
7553            let requested_host_entrypoint =
7554                resolve_host_entrypoint_within_vm_host_cwd(vm, entrypoint_specifier);
7555            if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
7556                let requested_cwd = cwd.unwrap_or(guest_cwd.as_str());
7557                return Err(SidecarError::InvalidState(format!(
7558                    "execution cwd {requested_cwd} is outside sandbox root {}",
7559                    vm.host_cwd.to_string_lossy()
7560                )));
7561            }
7562            let host_entrypoint_override = allow_host_path_overrides
7563                .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, entrypoint_specifier))
7564                .flatten();
7565            let guest_entrypoint = host_entrypoint_override
7566                .as_ref()
7567                .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
7568                .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, entrypoint_specifier));
7569            let entrypoint = host_entrypoint_override.map_or_else(
7570                || {
7571                    guest_entrypoint.as_ref().map_or_else(
7572                        || entrypoint_specifier.clone(),
7573                        |guest_entrypoint| {
7574                            resolve_vm_guest_path_to_host(vm, guest_entrypoint)
7575                                .to_string_lossy()
7576                                .into_owned()
7577                        },
7578                    )
7579                },
7580                |(_, host_entrypoint)| host_entrypoint,
7581            );
7582            (
7583                entrypoint,
7584                args.iter().skip(1).cloned().collect(),
7585                guest_entrypoint,
7586            )
7587        };
7588
7589        prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
7590
7591        return Ok(ResolvedChildProcessExecution {
7592            command: String::from(JAVASCRIPT_COMMAND),
7593            process_args: std::iter::once(command.to_owned())
7594                .chain(args.iter().cloned())
7595                .collect(),
7596            runtime: GuestRuntimeKind::JavaScript,
7597            entrypoint,
7598            execution_args,
7599            env,
7600            guest_cwd,
7601            host_cwd,
7602            wasm_permission_tier: None,
7603            tool_command: false,
7604        });
7605    }
7606
7607    if command.ends_with(".js") || command.ends_with(".mjs") || command.ends_with(".cjs") {
7608        let requested_host_entrypoint = resolve_host_entrypoint_within_vm_host_cwd(vm, command);
7609        if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
7610            let requested_cwd = cwd.unwrap_or(guest_cwd.as_str());
7611            return Err(SidecarError::InvalidState(format!(
7612                "execution cwd {requested_cwd} is outside sandbox root {}",
7613                vm.host_cwd.to_string_lossy()
7614            )));
7615        }
7616        let host_entrypoint_override = allow_host_path_overrides
7617            .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, command))
7618            .flatten();
7619        let guest_entrypoint = host_entrypoint_override
7620            .as_ref()
7621            .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
7622            .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, command));
7623        let entrypoint = host_entrypoint_override.map_or_else(
7624            || {
7625                guest_entrypoint.as_ref().map_or_else(
7626                    || command.to_owned(),
7627                    |guest_entrypoint| {
7628                        resolve_vm_guest_path_to_host(vm, guest_entrypoint)
7629                            .to_string_lossy()
7630                            .into_owned()
7631                    },
7632                )
7633            },
7634            |(_, host_entrypoint)| host_entrypoint,
7635        );
7636        prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
7637
7638        return Ok(ResolvedChildProcessExecution {
7639            command: String::from(JAVASCRIPT_COMMAND),
7640            process_args: std::iter::once(command.to_owned())
7641                .chain(args.iter().cloned())
7642                .collect(),
7643            runtime: GuestRuntimeKind::JavaScript,
7644            entrypoint,
7645            execution_args: args.to_vec(),
7646            env,
7647            guest_cwd,
7648            host_cwd,
7649            wasm_permission_tier: None,
7650            tool_command: false,
7651        });
7652    }
7653
7654    let guest_entrypoint = resolve_guest_command_entrypoint(
7655        vm,
7656        &guest_cwd,
7657        command,
7658        env.get("PATH").map(String::as_str),
7659    )
7660    .ok_or_else(|| {
7661        SidecarError::InvalidState(format!(
7662            "command not found on native sidecar path: {command}"
7663        ))
7664    })?;
7665    let wasm_permission_tier = explicit_wasm_permission_tier
7666        .or_else(|| vm.command_permissions.get(command).copied())
7667        .or_else(|| {
7668            Path::new(&guest_entrypoint)
7669                .file_name()
7670                .and_then(|name| name.to_str())
7671                .and_then(|name| vm.command_permissions.get(name).copied())
7672        });
7673
7674    let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint);
7675    if let Some((javascript_guest_entrypoint, javascript_host_entrypoint)) =
7676        resolve_javascript_command_entrypoint(vm, &guest_entrypoint, &host_entrypoint)
7677    {
7678        prepare_guest_runtime_env(
7679            vm,
7680            &mut env,
7681            &guest_cwd,
7682            &host_cwd,
7683            Some(javascript_guest_entrypoint),
7684        )?;
7685
7686        return Ok(ResolvedChildProcessExecution {
7687            command: command.to_owned(),
7688            process_args: std::iter::once(command.to_owned())
7689                .chain(args.iter().cloned())
7690                .collect(),
7691            runtime: GuestRuntimeKind::JavaScript,
7692            entrypoint: javascript_host_entrypoint.to_string_lossy().into_owned(),
7693            execution_args: args.to_vec(),
7694            env,
7695            guest_cwd,
7696            host_cwd,
7697            wasm_permission_tier: None,
7698            tool_command: false,
7699        });
7700    }
7701    prepare_guest_runtime_env(
7702        vm,
7703        &mut env,
7704        &guest_cwd,
7705        &host_cwd,
7706        Some(guest_entrypoint.clone()),
7707    )?;
7708
7709    Ok(ResolvedChildProcessExecution {
7710        command: command.to_owned(),
7711        process_args: std::iter::once(command.to_owned())
7712            .chain(args.iter().cloned())
7713            .collect(),
7714        runtime: GuestRuntimeKind::WebAssembly,
7715        entrypoint: host_entrypoint.to_string_lossy().into_owned(),
7716        execution_args: args.to_vec(),
7717        env,
7718        guest_cwd,
7719        host_cwd,
7720        wasm_permission_tier,
7721        tool_command: false,
7722    })
7723}
7724
7725const MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH: usize = 4;
7726
7727fn resolve_javascript_command_entrypoint(
7728    vm: &VmState,
7729    guest_entrypoint: &str,
7730    host_entrypoint: &Path,
7731) -> Option<(String, PathBuf)> {
7732    resolve_javascript_command_entrypoint_inner(
7733        vm,
7734        guest_entrypoint,
7735        host_entrypoint,
7736        MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH,
7737    )
7738}
7739
7740fn resolve_javascript_command_entrypoint_inner(
7741    vm: &VmState,
7742    guest_entrypoint: &str,
7743    host_entrypoint: &Path,
7744    redirects_remaining: usize,
7745) -> Option<(String, PathBuf)> {
7746    if redirects_remaining > 0 {
7747        let symlink_target = fs::symlink_metadata(host_entrypoint)
7748            .ok()
7749            .filter(|metadata| metadata.file_type().is_symlink())
7750            .and_then(|_| fs::read_link(host_entrypoint).ok());
7751        if let Some(symlink_target) = symlink_target {
7752            let guest_parent = Path::new(guest_entrypoint)
7753                .parent()
7754                .and_then(|path| path.to_str())
7755                .unwrap_or("/");
7756            let symlink_guest_entrypoint = if symlink_target.is_absolute() {
7757                normalize_path(&symlink_target.to_string_lossy())
7758            } else {
7759                normalize_path(&format!(
7760                    "{guest_parent}/{}",
7761                    symlink_target.to_string_lossy().replace('\\', "/")
7762                ))
7763            };
7764            let symlink_host_entrypoint =
7765                resolve_vm_guest_path_to_host(vm, &symlink_guest_entrypoint);
7766            return resolve_javascript_command_entrypoint_inner(
7767                vm,
7768                &symlink_guest_entrypoint,
7769                &symlink_host_entrypoint,
7770                redirects_remaining - 1,
7771            );
7772        }
7773    }
7774
7775    let script = load_executable_script_preview(host_entrypoint)?;
7776    let interpreter = parse_script_interpreter_name(&script);
7777
7778    if interpreter.is_none() && is_probable_javascript_entrypoint(host_entrypoint, &script) {
7779        return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf()));
7780    }
7781
7782    let interpreter = interpreter?;
7783    if interpreter == "node" {
7784        return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf()));
7785    }
7786
7787    if redirects_remaining == 0 || !matches!(interpreter.as_str(), "sh" | "bash" | "dash") {
7788        return None;
7789    }
7790
7791    let shim_target = parse_node_shell_shim_target(&script)?;
7792    let guest_parent = Path::new(guest_entrypoint)
7793        .parent()
7794        .and_then(|path| path.to_str())
7795        .unwrap_or("/");
7796    let shim_guest_entrypoint = normalize_path(&format!("{guest_parent}/{shim_target}"));
7797    let shim_host_entrypoint = resolve_vm_guest_path_to_host(vm, &shim_guest_entrypoint);
7798    resolve_javascript_command_entrypoint_inner(
7799        vm,
7800        &shim_guest_entrypoint,
7801        &shim_host_entrypoint,
7802        redirects_remaining - 1,
7803    )
7804}
7805
7806fn load_executable_script_preview(path: &Path) -> Option<String> {
7807    let bytes = fs::read(path).ok()?;
7808    let preview_len = bytes.len().min(16 * 1024);
7809    Some(String::from_utf8_lossy(&bytes[..preview_len]).into_owned())
7810}
7811
7812fn parse_script_interpreter_name(script: &str) -> Option<String> {
7813    let shebang = script.lines().next()?.strip_prefix("#!")?.trim();
7814    let mut tokens = shebang.split_whitespace();
7815    let command = tokens.next()?;
7816    let command_name = Path::new(command).file_name()?.to_str()?;
7817    if command_name == "env" {
7818        for token in tokens {
7819            if token.starts_with('-') {
7820                continue;
7821            }
7822            return Path::new(token)
7823                .file_name()
7824                .and_then(|name| name.to_str())
7825                .map(ToOwned::to_owned);
7826        }
7827        return None;
7828    }
7829
7830    Some(command_name.to_owned())
7831}
7832
7833fn parse_node_shell_shim_target(script: &str) -> Option<String> {
7834    for line in script.lines() {
7835        let trimmed = line.trim();
7836        if !trimmed.starts_with("exec ") {
7837            continue;
7838        }
7839
7840        let mut remaining = trimmed;
7841        while let Some(start) = remaining.find("\"$basedir/") {
7842            let after_prefix = &remaining[start + "\"$basedir/".len()..];
7843            let end = after_prefix.find('"')?;
7844            let candidate = &after_prefix[..end];
7845            remaining = &after_prefix[end + 1..];
7846
7847            if candidate.is_empty() || candidate == "node" || candidate.ends_with("/node") {
7848                continue;
7849            }
7850
7851            return Some(candidate.to_owned());
7852        }
7853    }
7854
7855    None
7856}
7857
7858fn is_probable_javascript_entrypoint(path: &Path, script: &str) -> bool {
7859    let extension = path
7860        .extension()
7861        .and_then(|value| value.to_str())
7862        .unwrap_or_default();
7863    if matches!(extension, "js" | "cjs" | "mjs") {
7864        return true;
7865    }
7866
7867    if !path
7868        .components()
7869        .any(|component| component.as_os_str() == "node_modules")
7870    {
7871        return false;
7872    }
7873
7874    let preview = script.trim_start_matches('\u{feff}').trim_start();
7875    !preview.is_empty()
7876        && !preview.starts_with("#!")
7877        && (preview.starts_with("\"use strict\"")
7878            || preview.starts_with("'use strict'")
7879            || preview.starts_with("import ")
7880            || preview.starts_with("export ")
7881            || preview.starts_with("const ")
7882            || preview.starts_with("let ")
7883            || preview.starts_with("var ")
7884            || preview.starts_with("Object.defineProperty(exports")
7885            || preview.starts_with("module.exports")
7886            || preview.starts_with("require("))
7887}
7888
7889fn resolve_guest_execution_cwd(vm: &VmState, value: Option<&str>) -> String {
7890    value
7891        .map(normalize_path)
7892        .unwrap_or_else(|| vm.guest_cwd.clone())
7893}
7894
7895fn resolve_execution_cwds(vm: &VmState, value: Option<&str>) -> (String, PathBuf, bool) {
7896    if let Some(raw_cwd) = value {
7897        let normalized_vm_host_cwd = normalize_host_path(&vm.host_cwd);
7898        let requested_host_cwd = normalize_host_path(Path::new(raw_cwd));
7899        if path_is_within_root(&requested_host_cwd, &normalized_vm_host_cwd) {
7900            let relative = requested_host_cwd
7901                .strip_prefix(&normalized_vm_host_cwd)
7902                .unwrap_or_else(|_| Path::new(""));
7903            let relative = relative.to_string_lossy().replace('\\', "/");
7904            let guest_cwd = if relative.is_empty() {
7905                String::from("/")
7906            } else {
7907                normalize_path(&format!("/{relative}"))
7908            };
7909            return (guest_cwd, requested_host_cwd, true);
7910        }
7911    }
7912
7913    let guest_cwd = resolve_guest_execution_cwd(vm, value);
7914    let host_cwd = if value.is_none() {
7915        vm.host_cwd.clone()
7916    } else {
7917        resolve_vm_guest_path_to_host(vm, &guest_cwd)
7918    };
7919    (guest_cwd, host_cwd, value.is_none())
7920}
7921
7922fn resolve_vm_guest_path_to_host(vm: &VmState, guest_path: &str) -> PathBuf {
7923    host_mount_path_for_guest_path(vm, guest_path)
7924        .unwrap_or_else(|| shadow_path_for_guest(vm, guest_path))
7925}
7926
7927fn shadow_path_for_guest(vm: &VmState, guest_path: &str) -> PathBuf {
7928    let normalized = normalize_path(guest_path);
7929    let relative = normalized.trim_start_matches('/');
7930    if relative.is_empty() {
7931        return vm.cwd.clone();
7932    }
7933    vm.cwd.join(relative)
7934}
7935
7936fn apply_shell_cwd_prefix(command: &str, mut args: Vec<String>, guest_cwd: &str) -> Vec<String> {
7937    if guest_cwd == "/" || !is_shell_command(command) {
7938        return args;
7939    }
7940
7941    let Some(flag) = args.first() else {
7942        return args;
7943    };
7944    if !matches!(flag.as_str(), "-c" | "-lc") || args.len() < 2 {
7945        return args;
7946    }
7947
7948    let command_text = args[1].clone();
7949    let quoted_cwd = shell_single_quote(guest_cwd);
7950    args[1] = format!("cd {quoted_cwd} && {command_text}");
7951    args
7952}
7953
7954fn is_shell_command(command: &str) -> bool {
7955    Path::new(command)
7956        .file_name()
7957        .and_then(|name| name.to_str())
7958        .unwrap_or(command)
7959        .trim_end_matches(".exe")
7960        .eq("sh")
7961        || Path::new(command)
7962            .file_name()
7963            .and_then(|name| name.to_str())
7964            .unwrap_or(command)
7965            .trim_end_matches(".exe")
7966            .eq("bash")
7967}
7968
7969fn shell_single_quote(value: &str) -> String {
7970    if value.is_empty() {
7971        return String::from("''");
7972    }
7973    format!("'{}'", value.replace('\'', "'\"'\"'"))
7974}
7975
7976pub(crate) fn sync_active_process_host_writes_to_kernel(
7977    vm: &mut VmState,
7978) -> Result<(), SidecarError> {
7979    if vm.root_filesystem_mode != RootFilesystemMode::ReadOnly {
7980        let shadow_root = vm.cwd.clone();
7981        sync_host_directory_tree_to_kernel(vm, &shadow_root, "/")?;
7982    }
7983
7984    let normalized_vm_root = normalize_host_path(&vm.cwd);
7985    let extra_roots = collect_active_process_host_sync_roots(vm, &normalized_vm_root);
7986    for (host_cwd, guest_cwd) in extra_roots {
7987        sync_host_directory_tree_to_kernel(vm, &host_cwd, &guest_cwd)?;
7988    }
7989
7990    Ok(())
7991}
7992
7993fn collect_active_process_host_sync_roots(
7994    vm: &VmState,
7995    normalized_vm_root: &Path,
7996) -> Vec<(PathBuf, String)> {
7997    let mut roots = Vec::new();
7998    let mut seen = BTreeSet::new();
7999
8000    for process in vm.active_processes.values() {
8001        collect_process_host_sync_roots(process, normalized_vm_root, &mut seen, &mut roots);
8002    }
8003
8004    roots
8005}
8006
8007fn collect_process_host_sync_roots(
8008    process: &ActiveProcess,
8009    normalized_vm_root: &Path,
8010    seen: &mut BTreeSet<(PathBuf, String)>,
8011    roots: &mut Vec<(PathBuf, String)>,
8012) {
8013    let normalized_host_cwd = normalize_host_path(&process.host_cwd);
8014    if !path_is_within_root(&normalized_host_cwd, normalized_vm_root) {
8015        let guest_cwd = normalize_path(&process.guest_cwd);
8016        if seen.insert((normalized_host_cwd.clone(), guest_cwd.clone())) {
8017            roots.push((normalized_host_cwd, guest_cwd));
8018        }
8019    }
8020
8021    for child in process.child_processes.values() {
8022        collect_process_host_sync_roots(child, normalized_vm_root, seen, roots);
8023    }
8024}
8025
8026fn sync_process_host_writes_to_kernel(
8027    vm: &mut VmState,
8028    process: &ActiveProcess,
8029) -> Result<(), SidecarError> {
8030    if vm.root_filesystem_mode != RootFilesystemMode::ReadOnly {
8031        let shadow_root = vm.cwd.clone();
8032        sync_host_directory_tree_to_kernel(vm, &shadow_root, "/")?;
8033    }
8034
8035    if !path_is_within_root(
8036        &normalize_host_path(&process.host_cwd),
8037        &normalize_host_path(&vm.cwd),
8038    ) {
8039        sync_host_directory_tree_to_kernel(vm, &process.host_cwd, &process.guest_cwd)?;
8040    }
8041
8042    Ok(())
8043}
8044
8045fn sync_host_directory_tree_to_kernel(
8046    vm: &mut VmState,
8047    host_root: &Path,
8048    guest_root: &str,
8049) -> Result<(), SidecarError> {
8050    let normalized_host_root = normalize_host_path(host_root);
8051    let normalized_guest_root = normalize_path(guest_root);
8052    let mut synced_file_times = BTreeMap::new();
8053    sync_host_directory_tree_to_kernel_inner(
8054        vm,
8055        &normalized_host_root,
8056        &normalized_host_root,
8057        &normalized_guest_root,
8058        &mut synced_file_times,
8059    )
8060}
8061
8062fn sync_host_directory_tree_to_kernel_inner(
8063    vm: &mut VmState,
8064    host_root: &Path,
8065    current_host_dir: &Path,
8066    guest_root: &str,
8067    synced_file_times: &mut BTreeMap<(u64, u64), (u64, u64)>,
8068) -> Result<(), SidecarError> {
8069    let entries = match fs::read_dir(current_host_dir) {
8070        Ok(entries) => entries,
8071        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
8072        Err(error) => {
8073            return Err(SidecarError::Io(format!(
8074                "failed to read host shadow directory {}: {error}",
8075                current_host_dir.display()
8076            )));
8077        }
8078    };
8079
8080    for entry in entries {
8081        let entry = entry.map_err(|error| {
8082            SidecarError::Io(format!(
8083                "failed to read host shadow entry in {}: {error}",
8084                current_host_dir.display()
8085            ))
8086        })?;
8087        let host_path = entry.path();
8088        let file_type = entry.file_type().map_err(|error| {
8089            SidecarError::Io(format!(
8090                "failed to stat host shadow entry {}: {error}",
8091                host_path.display()
8092            ))
8093        })?;
8094        let relative_path = host_path
8095            .strip_prefix(host_root)
8096            .map_err(|error| {
8097                SidecarError::InvalidState(format!(
8098                    "failed to relativize host shadow path {} against {}: {error}",
8099                    host_path.display(),
8100                    host_root.display()
8101                ))
8102            })?
8103            .to_string_lossy()
8104            .replace('\\', "/");
8105        let guest_path = if guest_root == "/" {
8106            normalize_path(&format!("/{relative_path}"))
8107        } else {
8108            normalize_path(&format!(
8109                "{}/{}",
8110                guest_root.trim_end_matches('/'),
8111                relative_path
8112            ))
8113        };
8114
8115        if should_skip_shadow_sync_path(vm, &guest_path) {
8116            continue;
8117        }
8118
8119        if file_type.is_dir() {
8120            let metadata = entry.metadata().map_err(|error| {
8121                SidecarError::Io(format!(
8122                    "failed to read host shadow metadata {}: {error}",
8123                    host_path.display()
8124                ))
8125            })?;
8126            if !is_shadow_bootstrap_dir(&guest_path)
8127                && !vm.kernel.exists(&guest_path).unwrap_or(false)
8128            {
8129                vm.kernel.mkdir(&guest_path, true).map_err(|error| {
8130                    SidecarError::InvalidState(format!(
8131                        "failed to sync host shadow directory {} to guest {}: {}",
8132                        host_path.display(),
8133                        guest_path,
8134                        kernel_error(error)
8135                    ))
8136                })?;
8137                vm.kernel
8138                    .chmod(&guest_path, host_shadow_mode(&metadata))
8139                    .map_err(|error| {
8140                        SidecarError::InvalidState(format!(
8141                            "failed to sync host shadow directory mode {} to guest {}: {}",
8142                            host_path.display(),
8143                            guest_path,
8144                            kernel_error(error)
8145                        ))
8146                    })?;
8147            }
8148            sync_host_directory_tree_to_kernel_inner(
8149                vm,
8150                host_root,
8151                &host_path,
8152                guest_root,
8153                synced_file_times,
8154            )?;
8155            continue;
8156        }
8157
8158        if file_type.is_file() {
8159            let metadata = entry.metadata().map_err(|error| {
8160                SidecarError::Io(format!(
8161                    "failed to read host shadow metadata {}: {error}",
8162                    host_path.display()
8163                ))
8164            })?;
8165            let timestamp_key = (metadata.dev(), metadata.ino());
8166            let (atime_ms, mtime_ms) =
8167                *synced_file_times.entry(timestamp_key).or_insert_with(|| {
8168                    (
8169                        metadata_time_ms(metadata.atime(), metadata.atime_nsec()),
8170                        metadata_time_ms(metadata.mtime(), metadata.mtime_nsec()),
8171                    )
8172                });
8173            let desired_mode = host_shadow_mode(&metadata);
8174            let bytes = read_host_shadow_file(&host_path, desired_mode).map_err(|error| {
8175                SidecarError::Io(format!(
8176                    "failed to read host shadow file {}: {error}",
8177                    host_path.display()
8178                ))
8179            })?;
8180            vm.kernel.write_file(&guest_path, bytes).map_err(|error| {
8181                SidecarError::InvalidState(format!(
8182                    "failed to sync host shadow file {} to guest {}: {}",
8183                    host_path.display(),
8184                    guest_path,
8185                    kernel_error(error)
8186                ))
8187            })?;
8188            vm.kernel
8189                .chmod(&guest_path, desired_mode)
8190                .map_err(|error| {
8191                    SidecarError::InvalidState(format!(
8192                        "failed to sync host shadow file mode {} to guest {}: {}",
8193                        host_path.display(),
8194                        guest_path,
8195                        kernel_error(error)
8196                    ))
8197                })?;
8198            vm.kernel
8199                .utimes(&guest_path, atime_ms, mtime_ms)
8200                .map_err(|error| {
8201                    SidecarError::InvalidState(format!(
8202                        "failed to sync host shadow file times {} to guest {}: {}",
8203                        host_path.display(),
8204                        guest_path,
8205                        kernel_error(error)
8206                    ))
8207                })?;
8208            continue;
8209        }
8210
8211        if file_type.is_symlink() {
8212            let target = match fs::read_link(&host_path) {
8213                Ok(target) => target,
8214                Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
8215                Err(error) => {
8216                    return Err(SidecarError::Io(format!(
8217                        "failed to read host shadow symlink {}: {error}",
8218                        host_path.display()
8219                    )));
8220                }
8221            };
8222            replace_kernel_symlink(vm, &guest_path, &target.to_string_lossy())?;
8223        }
8224    }
8225
8226    Ok(())
8227}
8228
8229fn replace_kernel_symlink(
8230    vm: &mut VmState,
8231    guest_path: &str,
8232    target: &str,
8233) -> Result<(), SidecarError> {
8234    if vm.kernel.symlink(target, guest_path).is_ok() {
8235        return Ok(());
8236    }
8237
8238    if let Ok(existing_target) = vm.kernel.read_link(guest_path) {
8239        if existing_target == target {
8240            return Ok(());
8241        }
8242    }
8243
8244    let _ = vm.kernel.remove_file(guest_path);
8245    let _ = vm.kernel.remove_dir(guest_path);
8246    vm.kernel
8247        .symlink(target, guest_path)
8248        .map_err(kernel_error)?;
8249    Ok(())
8250}
8251
8252fn host_shadow_mode(metadata: &fs::Metadata) -> u32 {
8253    metadata.permissions().mode() & 0o7777
8254}
8255
8256/// Reads a shadow-root file back into the kernel even when guest-visible mode
8257/// bits make it unreadable for the host user. The sidecar is the kernel for
8258/// this tree, so guest permission bits (for example a 0o200 write-only file
8259/// produced by `chmod` plus a shell append redirect) must not break the
8260/// exit-time shadow sync. The original mode is restored after the read.
8261fn read_host_shadow_file(host_path: &Path, mode: u32) -> std::io::Result<Vec<u8>> {
8262    match fs::read(host_path) {
8263        Ok(bytes) => Ok(bytes),
8264        Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => {
8265            fs::set_permissions(host_path, fs::Permissions::from_mode(mode | 0o400))?;
8266            let result = fs::read(host_path);
8267            fs::set_permissions(host_path, fs::Permissions::from_mode(mode))?;
8268            result
8269        }
8270        Err(error) => Err(error),
8271    }
8272}
8273
8274fn metadata_time_ms(seconds: i64, nanos: i64) -> u64 {
8275    let seconds = seconds.max(0) as u64;
8276    let nanos = nanos.max(0) as u64;
8277    seconds
8278        .saturating_mul(1_000)
8279        .saturating_add(nanos / 1_000_000)
8280}
8281
8282fn is_shadow_bootstrap_dir(path: &str) -> bool {
8283    matches!(
8284        path,
8285        "/dev"
8286            | "/proc"
8287            | "/tmp"
8288            | "/bin"
8289            | "/lib"
8290            | "/sbin"
8291            | "/boot"
8292            | "/etc"
8293            | "/root"
8294            | "/run"
8295            | "/srv"
8296            | "/sys"
8297            | "/opt"
8298            | "/mnt"
8299            | "/media"
8300            | "/home"
8301            | "/home/agentos"
8302            | "/usr"
8303            | "/usr/bin"
8304            | "/usr/games"
8305            | "/usr/include"
8306            | "/usr/lib"
8307            | "/usr/libexec"
8308            | "/usr/man"
8309            | "/usr/local"
8310            | "/usr/local/bin"
8311            | "/usr/sbin"
8312            | "/usr/share"
8313            | "/usr/share/man"
8314            | "/var"
8315            | "/var/cache"
8316            | "/var/empty"
8317            | "/var/lib"
8318            | "/var/lock"
8319            | "/var/log"
8320            | "/var/run"
8321            | "/var/spool"
8322            | "/var/tmp"
8323            | "/etc/agentos"
8324            | "/workspace"
8325    )
8326}
8327
8328#[cfg(test)]
8329mod shadow_sync_tests {
8330    use super::{is_protected_agentos_shadow_sync_path, is_shadow_bootstrap_dir};
8331
8332    #[test]
8333    fn shadow_bootstrap_sync_skips_virtual_home_tree() {
8334        assert!(is_shadow_bootstrap_dir("/home"));
8335        assert!(is_shadow_bootstrap_dir("/home/agentos"));
8336    }
8337
8338    #[test]
8339    fn protected_agentos_paths_are_not_shadow_synced() {
8340        assert!(is_protected_agentos_shadow_sync_path("/etc/agentos"));
8341        assert!(is_protected_agentos_shadow_sync_path(
8342            "/etc/agentos/instructions.md"
8343        ));
8344        assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos-copy"));
8345        assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos.md"));
8346    }
8347}
8348
8349fn is_kernel_owned_shadow_sync_path(path: &str) -> bool {
8350    matches!(path, "/dev" | "/proc" | "/sys")
8351        || path.starts_with("/dev/")
8352        || path.starts_with("/proc/")
8353        || path.starts_with("/sys/")
8354}
8355
8356pub(crate) fn is_protected_agentos_shadow_sync_path(path: &str) -> bool {
8357    path == "/etc/agentos" || path.starts_with("/etc/agentos/")
8358}
8359
8360fn should_skip_shadow_sync_path(vm: &VmState, guest_path: &str) -> bool {
8361    is_kernel_owned_shadow_sync_path(guest_path)
8362        || is_protected_agentos_shadow_sync_path(guest_path)
8363        || host_mount_path_for_guest_path_from_mounts(&vm.configuration.mounts, guest_path)
8364            .is_some()
8365}
8366
8367fn resolve_path_like_guest_specifier(cwd: &str, specifier: &str) -> String {
8368    if specifier.starts_with("file://") {
8369        normalize_path(specifier.trim_start_matches("file://"))
8370    } else if specifier.starts_with("file:") {
8371        normalize_path(specifier.trim_start_matches("file:"))
8372    } else if specifier.starts_with('/') {
8373        normalize_path(specifier)
8374    } else {
8375        normalize_path(&format!("{cwd}/{specifier}"))
8376    }
8377}
8378
8379fn guest_entrypoint_for_specifier(cwd: &str, specifier: &str) -> Option<String> {
8380    is_path_like_specifier(specifier).then(|| resolve_path_like_guest_specifier(cwd, specifier))
8381}
8382
8383fn is_node_runtime_command(command: &str) -> bool {
8384    matches!(command, "node" | "npm" | "npx")
8385        || Path::new(command)
8386            .file_name()
8387            .and_then(|name| name.to_str())
8388            .is_some_and(|name| matches!(name, "node" | "npm" | "npx"))
8389}
8390
8391fn resolve_special_node_cli_invocation(
8392    args: &[String],
8393    env: &mut BTreeMap<String, String>,
8394) -> Option<(String, Vec<String>)> {
8395    let first = args.first()?;
8396    match first.as_str() {
8397        "-e" | "--eval" => {
8398            env.insert(
8399                String::from("AGENTOS_NODE_EVAL"),
8400                args.get(1).cloned().unwrap_or_default(),
8401            );
8402            Some((first.clone(), args.iter().skip(2).cloned().collect()))
8403        }
8404        "-v" | "--version" => {
8405            env.insert(
8406                String::from("AGENTOS_NODE_EVAL"),
8407                String::from("console.log(process.version);"),
8408            );
8409            Some((String::from("-e"), args.to_vec()))
8410        }
8411        _ => None,
8412    }
8413}
8414
8415fn node_runtime_command_name(command: &str) -> Option<&str> {
8416    let name = Path::new(command)
8417        .file_name()
8418        .and_then(|name| name.to_str())?;
8419    matches!(name, "node" | "npm" | "npx").then_some(name)
8420}
8421
8422struct ResolvedHostNodeCliEntrypoint {
8423    command_name: String,
8424    guest_root: String,
8425    guest_entrypoint: String,
8426    package_root: PathBuf,
8427}
8428
8429fn resolve_host_node_cli_entrypoint(command: &str) -> Option<ResolvedHostNodeCliEntrypoint> {
8430    let command_name = node_runtime_command_name(command)?;
8431    if !matches!(command_name, "npm" | "npx") {
8432        return None;
8433    }
8434
8435    let path = std::env::var_os("PATH")?;
8436    for root in std::env::split_paths(&path) {
8437        let candidate = root.join(command_name);
8438        if !candidate.is_file() {
8439            continue;
8440        }
8441        let entrypoint = candidate.canonicalize().ok().unwrap_or(candidate);
8442        let package_root = entrypoint.parent()?.parent()?.to_path_buf();
8443        let guest_root = format!("/__secure_exec/node-runtime/{command_name}");
8444        let relative_entrypoint = entrypoint.strip_prefix(&package_root).ok()?;
8445        let guest_entrypoint = normalize_path(&format!(
8446            "{guest_root}/{}",
8447            relative_entrypoint.to_string_lossy().replace('\\', "/")
8448        ));
8449        return Some(ResolvedHostNodeCliEntrypoint {
8450            command_name: command_name.to_owned(),
8451            guest_root,
8452            guest_entrypoint,
8453            package_root,
8454        });
8455    }
8456
8457    None
8458}
8459
8460fn build_host_node_cli_eval(cli: &ResolvedHostNodeCliEntrypoint) -> String {
8461    let guest_npm_main = normalize_path(&format!("{}/lib/npm.js", cli.guest_root));
8462    let guest_npm_cli = normalize_path(&format!("{}/bin/npm-cli.js", cli.guest_root));
8463    let guest_package_json = normalize_path(&format!("{}/package.json", cli.guest_root));
8464    let guest_display_module = normalize_path(&format!("{}/lib/utils/display.js", cli.guest_root));
8465    let guest_log_file_module =
8466        normalize_path(&format!("{}/lib/utils/log-file.js", cli.guest_root));
8467    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); } } }";
8468    let display_stub = format!(
8469        "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 }};",
8470        display_module = serde_json::to_string(&guest_display_module)
8471            .unwrap_or_else(|_| format!("\"{guest_display_module}\"")),
8472        log_file_module = serde_json::to_string(&guest_log_file_module)
8473            .unwrap_or_else(|_| format!("\"{guest_log_file_module}\"")),
8474    );
8475    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)); }";
8476    match cli.command_name.as_str() {
8477        "npx" => format!(
8478            "{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); }});",
8479            debug_preamble = debug_preamble,
8480            display_stub = display_stub,
8481            registry_fetch_stub = registry_fetch_stub.replace(
8482                "__AGENTOS_NPM_MAIN__",
8483                &serde_json::to_string(&guest_npm_main)
8484                    .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8485            ),
8486            npm_main = serde_json::to_string(&guest_npm_main)
8487                .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8488            npm_cli = serde_json::to_string(&guest_npm_cli)
8489                .unwrap_or_else(|_| format!("\"{guest_npm_cli}\"")),
8490            package_json = serde_json::to_string(&guest_package_json)
8491                .unwrap_or_else(|_| format!("\"{guest_package_json}\"")),
8492        ),
8493        _ => format!(
8494            "{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); }});",
8495            debug_preamble = debug_preamble,
8496            display_stub = display_stub,
8497            registry_fetch_stub = registry_fetch_stub.replace(
8498                "__AGENTOS_NPM_MAIN__",
8499                &serde_json::to_string(&guest_npm_main)
8500                    .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8501            ),
8502            npm_main = serde_json::to_string(&guest_npm_main)
8503                .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8504            package_json = serde_json::to_string(&guest_package_json)
8505                .unwrap_or_else(|_| format!("\"{guest_package_json}\"")),
8506        ),
8507    }
8508}
8509
8510fn resolve_guest_command_entrypoint(
8511    vm: &VmState,
8512    guest_cwd: &str,
8513    command: &str,
8514    path_env: Option<&str>,
8515) -> Option<String> {
8516    if !is_path_like_specifier(command) {
8517        if let Some(entrypoint) = vm.command_guest_paths.get(command) {
8518            return Some(entrypoint.clone());
8519        }
8520
8521        for search_dir in guest_command_search_dirs(vm, guest_cwd, path_env) {
8522            let candidate = normalize_path(&format!("{search_dir}/{command}"));
8523            if let Some(entrypoint) = resolve_guest_command_path_candidate(vm, &candidate) {
8524                return Some(entrypoint);
8525            }
8526        }
8527
8528        return None;
8529    }
8530
8531    let normalized = resolve_path_like_guest_specifier(guest_cwd, command);
8532    resolve_guest_command_path_candidate(vm, &normalized).or_else(|| {
8533        // Some guest shells materialize PATH lookups into absolute candidate paths.
8534        // If that path points into a searched directory but does not exist, fall
8535        // back to the command basename so the sidecar can remap VM command packages.
8536        let parent_dir = Path::new(&normalized).parent()?.to_str()?;
8537        if !guest_command_search_dirs(vm, guest_cwd, path_env)
8538            .iter()
8539            .any(|search_dir| normalize_path(search_dir) == normalize_path(parent_dir))
8540        {
8541            return None;
8542        }
8543
8544        let file_name = Path::new(&normalized).file_name()?.to_str()?;
8545        vm.command_guest_paths.get(file_name).cloned()
8546    })
8547}
8548
8549fn guest_command_search_dirs(vm: &VmState, guest_cwd: &str, path_env: Option<&str>) -> Vec<String> {
8550    let mut search_dirs = Vec::new();
8551    let mut seen = BTreeSet::new();
8552
8553    if let Some(path) = path_env.or_else(|| vm.guest_env.get("PATH").map(String::as_str)) {
8554        for segment in path.split(':') {
8555            let trimmed = segment.trim();
8556            if trimmed.is_empty() {
8557                continue;
8558            }
8559            let normalized = if trimmed.starts_with('/') {
8560                normalize_path(trimmed)
8561            } else {
8562                normalize_path(&format!("{guest_cwd}/{trimmed}"))
8563            };
8564            if seen.insert(normalized.clone()) {
8565                search_dirs.push(normalized);
8566            }
8567        }
8568    }
8569
8570    for fallback in ["/bin", "/usr/bin", "/usr/local/bin"] {
8571        let normalized = String::from(fallback);
8572        if seen.insert(normalized.clone()) {
8573            search_dirs.push(normalized);
8574        }
8575    }
8576
8577    search_dirs
8578}
8579
8580fn resolve_guest_command_path_candidate(vm: &VmState, candidate: &str) -> Option<String> {
8581    if candidate.starts_with("/bin/")
8582        || candidate.starts_with("/usr/bin/")
8583        || candidate.starts_with("/usr/local/bin/")
8584        || candidate.starts_with("/__secure_exec/commands/")
8585    {
8586        if let Some(file_name) = Path::new(candidate)
8587            .file_name()
8588            .and_then(|name| name.to_str())
8589        {
8590            if let Some(guest_entrypoint) = vm.command_guest_paths.get(file_name) {
8591                return Some(guest_entrypoint.clone());
8592            }
8593        }
8594    }
8595
8596    if vm
8597        .kernel
8598        .exists(candidate)
8599        .ok()
8600        .is_some_and(|exists| exists)
8601    {
8602        return Some(normalize_path(candidate));
8603    }
8604
8605    resolve_vm_guest_path_to_host(vm, candidate)
8606        .is_file()
8607        .then(|| normalize_path(candidate))
8608}
8609
8610fn resolve_host_entrypoint_within_vm_host_cwd(
8611    vm: &VmState,
8612    specifier: &str,
8613) -> Option<(String, String)> {
8614    let candidate = Path::new(specifier);
8615    if !candidate.is_absolute() {
8616        return None;
8617    }
8618
8619    let normalized_entrypoint = normalize_host_path(candidate);
8620    let normalized_host_cwd = normalize_host_path(&vm.host_cwd);
8621    if !path_is_within_root(&normalized_entrypoint, &normalized_host_cwd) {
8622        return None;
8623    }
8624
8625    let relative = normalized_entrypoint
8626        .strip_prefix(&normalized_host_cwd)
8627        .ok()?
8628        .to_string_lossy()
8629        .replace('\\', "/");
8630    let guest_entrypoint = if relative.is_empty() {
8631        String::from("/")
8632    } else {
8633        normalize_path(&format!("/{relative}"))
8634    };
8635    Some((
8636        guest_entrypoint,
8637        normalized_entrypoint.to_string_lossy().into_owned(),
8638    ))
8639}
8640
8641fn prepare_guest_runtime_env(
8642    vm: &VmState,
8643    env: &mut BTreeMap<String, String>,
8644    guest_cwd: &str,
8645    host_cwd: &Path,
8646    guest_entrypoint: Option<String>,
8647) -> Result<(), SidecarError> {
8648    let user = vm.kernel.user_profile();
8649    let path_mappings = runtime_guest_path_mappings(vm);
8650    let read_paths = expand_host_access_paths(
8651        std::iter::once(vm.cwd.clone())
8652            .chain(
8653                path_mappings
8654                    .iter()
8655                    .map(|mapping| PathBuf::from(&mapping.host_path)),
8656            )
8657            .chain(std::iter::once(host_cwd.to_path_buf()))
8658            .collect::<Vec<_>>()
8659            .as_slice(),
8660    );
8661    let write_paths = dedupe_host_paths(
8662        std::iter::once(vm.cwd.clone())
8663            .chain(std::iter::once(host_cwd.to_path_buf()))
8664            .chain(runtime_guest_writable_host_paths(vm))
8665            .collect::<Vec<_>>()
8666            .as_slice(),
8667    );
8668    let allowed_node_builtins = configured_allowed_node_builtins(vm);
8669    let loopback_exempt_ports = configured_loopback_exempt_ports(vm);
8670
8671    env.insert(
8672        String::from("AGENTOS_GUEST_PATH_MAPPINGS"),
8673        serde_json::to_string(&path_mappings).map_err(|error| {
8674            SidecarError::InvalidState(format!("failed to encode guest path mappings: {error}"))
8675        })?,
8676    );
8677    env.entry(String::from(EXECUTION_SANDBOX_ROOT_ENV))
8678        .or_insert_with(|| normalize_host_path(&vm.cwd).to_string_lossy().into_owned());
8679    env.insert(
8680        String::from("AGENTOS_EXTRA_FS_READ_PATHS"),
8681        serde_json::to_string(
8682            &read_paths
8683                .iter()
8684                .map(|path| path.to_string_lossy().into_owned())
8685                .collect::<Vec<_>>(),
8686        )
8687        .map_err(|error| {
8688            SidecarError::InvalidState(format!("failed to encode read paths: {error}"))
8689        })?,
8690    );
8691    env.insert(
8692        String::from("AGENTOS_EXTRA_FS_WRITE_PATHS"),
8693        serde_json::to_string(
8694            &write_paths
8695                .iter()
8696                .map(|path| path.to_string_lossy().into_owned())
8697                .collect::<Vec<_>>(),
8698        )
8699        .map_err(|error| {
8700            SidecarError::InvalidState(format!("failed to encode write paths: {error}"))
8701        })?,
8702    );
8703    env.insert(
8704        String::from("AGENTOS_ALLOWED_NODE_BUILTINS"),
8705        serde_json::to_string(&allowed_node_builtins).map_err(|error| {
8706            SidecarError::InvalidState(format!("failed to encode allowed builtins: {error}"))
8707        })?,
8708    );
8709    // The guest JS host platform drives subtractive global scrubbing in the
8710    // per-execution runtime shim (see prepend_v8_runtime_shim).
8711    env.insert(
8712        String::from("AGENTOS_JS_PLATFORM"),
8713        js_runtime_platform_env(vm).to_owned(),
8714    );
8715    // Module-resolution mode (omitted when full Node resolution / the default).
8716    if let Some(resolution) = js_runtime_module_resolution_env(vm) {
8717        env.insert(
8718            String::from("AGENTOS_JS_MODULE_RESOLUTION"),
8719            resolution.to_owned(),
8720        );
8721    }
8722    // Builtin allow-list gate for the live resolver. Present only when builtins
8723    // should be restricted (non-node platform => deny all; node + explicit
8724    // allow-list => exactly those). Absent => unrestricted (node default).
8725    if let Some(allowlist) = js_runtime_enforced_builtins(vm) {
8726        env.insert(
8727            String::from("AGENTOS_JS_BUILTIN_ALLOWLIST"),
8728            serde_json::to_string(&allowlist).map_err(|error| {
8729                SidecarError::InvalidState(format!(
8730                    "failed to encode jsRuntime builtin allow-list: {error}"
8731                ))
8732            })?,
8733        );
8734    }
8735    // Virtual OS identity (os.cpus/totalmem/freemem/homedir/userInfo/...) now
8736    // rides the typed `guest_runtime` (see `guest_runtime_identity`), exposed to
8737    // the guest as the `__agentOSVirtualOs` structured global by the runtime
8738    // shim — no longer the `AGENTOS_VIRTUAL_OS_*` env vars.
8739    // Virtual process uid/gid now ride the typed `guest_runtime` identity
8740    // (see `guest_runtime_identity`), not the `AGENTOS_VIRTUAL_PROCESS_*` env.
8741    env.entry(String::from("HOME"))
8742        .or_insert_with(|| user.homedir.clone());
8743    env.entry(String::from("USER"))
8744        .or_insert_with(|| user.username.clone());
8745    env.entry(String::from("LOGNAME"))
8746        .or_insert_with(|| user.username.clone());
8747    env.entry(String::from("SHELL"))
8748        .or_insert_with(|| user.shell.clone());
8749    env.entry(String::from("PATH")).or_insert_with(|| {
8750        vm.guest_env
8751            .get("PATH")
8752            .cloned()
8753            .unwrap_or_else(|| crate::vm::DEFAULT_GUEST_PATH_ENV.to_owned())
8754    });
8755    env.entry(String::from("TMPDIR"))
8756        .or_insert_with(|| String::from("/tmp"));
8757    env.insert(String::from("PWD"), guest_cwd.to_owned());
8758    if !loopback_exempt_ports.is_empty() {
8759        env.insert(
8760            String::from(LOOPBACK_EXEMPT_PORTS_ENV),
8761            serde_json::to_string(&loopback_exempt_ports).map_err(|error| {
8762                SidecarError::InvalidState(format!("failed to encode loopback exemptions: {error}"))
8763            })?,
8764        );
8765    }
8766    if let Some(guest_entrypoint) = guest_entrypoint {
8767        env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint);
8768    }
8769    Ok(())
8770}
8771
8772fn virtual_os_cpu_count(resource_limits: &ResourceLimits) -> usize {
8773    resource_limits.virtual_cpu_count.unwrap_or(1).max(1)
8774}
8775
8776fn virtual_os_totalmem_bytes(resource_limits: &ResourceLimits) -> u64 {
8777    resource_limits
8778        .max_wasm_memory_bytes
8779        .unwrap_or(1024 * 1024 * 1024)
8780}
8781
8782fn virtual_os_freemem_bytes(resource_limits: &ResourceLimits) -> u64 {
8783    resource_limits
8784        .max_wasm_memory_bytes
8785        .unwrap_or(512 * 1024 * 1024)
8786}
8787
8788/// Build the typed per-execution JavaScript limits from the per-VM `VmLimits`
8789/// (sourced from `CreateVmConfig` on the BARE wire). These ride the execution
8790/// request, not `AGENTOS_*` env vars — see the env-vs-wire rule in
8791/// `crates/sidecar/CLAUDE.md`.
8792fn javascript_execution_limits(vm: &VmState) -> JavascriptExecutionLimits {
8793    JavascriptExecutionLimits {
8794        v8_heap_limit_mb: vm.limits.js_runtime.v8_heap_limit_mb,
8795        sync_rpc_wait_timeout_ms: vm.limits.js_runtime.sync_rpc_wait_timeout_ms,
8796    }
8797}
8798
8799/// Build the typed per-execution guest-runtime identity (virtual `process.*`)
8800/// from kernel state. Replaces the `AGENTOS_VIRTUAL_PROCESS_{UID,GID,PID,PPID}`
8801/// env round-trip: the runtime shim reads these from `guest_runtime`, not env.
8802/// `uid`/`gid` come from the VM user profile (applied to every guest);
8803/// `pid`/`ppid` are per-process and only set for paths that assigned them.
8804fn guest_runtime_identity(
8805    vm: &VmState,
8806    virtual_pid: Option<u64>,
8807    virtual_ppid: Option<u64>,
8808) -> GuestRuntimeConfig {
8809    let user = vm.kernel.user_profile();
8810    let resource_limits = vm.kernel.resource_limits();
8811    GuestRuntimeConfig {
8812        virtual_uid: Some(u64::from(user.uid)),
8813        virtual_gid: Some(u64::from(user.gid)),
8814        virtual_pid,
8815        virtual_ppid,
8816        virtual_exec_path: None,
8817        os_cpu_count: Some(virtual_os_cpu_count(resource_limits) as u64),
8818        os_totalmem: Some(virtual_os_totalmem_bytes(resource_limits)),
8819        os_freemem: Some(virtual_os_freemem_bytes(resource_limits)),
8820        os_homedir: Some(user.homedir.clone()),
8821        os_hostname: None,
8822        os_shell: Some(user.shell.clone()),
8823        os_user: Some(user.username.clone()),
8824        // Userland bundle to bake into the per-sidecar snapshot, supplied by the
8825        // (trusted) client via jsRuntime.snapshotUserlandCode. The agent-os layer
8826        // sets this to the agent SDK bundle for snapshot-enabled agents; `None`
8827        // keeps the bridge-only snapshot.
8828        snapshot_userland_code: vm
8829            .configuration
8830            .js_runtime
8831            .as_ref()
8832            .and_then(|cfg| cfg.snapshot_userland_code.clone()),
8833    }
8834}
8835
8836/// The guest's virtual home directory, sourced from the VM user profile (the
8837/// same value carried to the guest as `os.homedir()` via `guest_runtime`). Used
8838/// by sidecar-internal `~`-path resolution; falls back to `/root` for a
8839/// non-absolute profile value.
8840fn guest_virtual_home(vm: &VmState) -> String {
8841    let homedir = vm.kernel.user_profile().homedir;
8842    if homedir.starts_with('/') {
8843        homedir
8844    } else {
8845        String::from("/root")
8846    }
8847}
8848
8849/// Build the typed per-execution Python limits from the per-VM `VmLimits`.
8850fn python_execution_limits(vm: &VmState) -> PythonExecutionLimits {
8851    PythonExecutionLimits {
8852        output_buffer_max_bytes: Some(vm.limits.python.output_buffer_max_bytes),
8853        execution_timeout_ms: Some(vm.limits.python.execution_timeout_ms),
8854        max_old_space_mb: Some(vm.limits.python.max_old_space_mb),
8855        vfs_rpc_timeout_ms: Some(vm.limits.python.vfs_rpc_timeout_ms),
8856    }
8857}
8858
8859/// Build the typed per-execution WebAssembly limits from the per-VM kernel
8860/// `ResourceLimits`. Replaces the old `apply_wasm_limit_env` env round-trip;
8861/// notably this is the path that finally enforces the stack cap that the
8862/// `AGENTOS_WASM_MAX_STACK_BYTES` env knob set but no reader consumed.
8863fn wasm_execution_limits(vm: &VmState) -> WasmExecutionLimits {
8864    let resource_limits = vm.kernel.resource_limits();
8865    WasmExecutionLimits {
8866        max_fuel: resource_limits.max_wasm_fuel,
8867        max_memory_bytes: resource_limits.max_wasm_memory_bytes,
8868        max_stack_bytes: resource_limits
8869            .max_wasm_stack_bytes
8870            .map(|value| value as u64),
8871    }
8872}
8873
8874/// The guest JavaScript host platform configured for this VM, defaulting to
8875/// full Node.js emulation when no `jsRuntime` config was supplied at create.
8876fn js_runtime_platform(vm: &VmState) -> vm_config::JsRuntimePlatform {
8877    vm.configuration
8878        .js_runtime
8879        .as_ref()
8880        .map(|cfg| cfg.platform)
8881        .unwrap_or(vm_config::JsRuntimePlatform::Node)
8882}
8883
8884/// Lowercase wire name for the configured platform, mirroring the serde
8885/// representation of `vm_config::JsRuntimePlatform`.
8886fn js_runtime_platform_env(vm: &VmState) -> &'static str {
8887    match js_runtime_platform(vm) {
8888        vm_config::JsRuntimePlatform::Node => "node",
8889        vm_config::JsRuntimePlatform::Browser => "browser",
8890        vm_config::JsRuntimePlatform::Neutral => "neutral",
8891        vm_config::JsRuntimePlatform::Bare => "bare",
8892    }
8893}
8894
8895/// Wire name for the configured module-resolution mode, or `None` when it is the
8896/// full-Node default (which the live resolver also assumes when the env is unset).
8897fn js_runtime_module_resolution_env(vm: &VmState) -> Option<&'static str> {
8898    let resolution = vm
8899        .configuration
8900        .js_runtime
8901        .as_ref()
8902        .map(|cfg| cfg.module_resolution)
8903        .unwrap_or(vm_config::JsModuleResolution::Node);
8904    match resolution {
8905        vm_config::JsModuleResolution::Node => None,
8906        vm_config::JsModuleResolution::Relative => Some("relative"),
8907        vm_config::JsModuleResolution::None => Some("none"),
8908    }
8909}
8910
8911/// The builtin allow-list the live resolver should enforce, or `None` to leave
8912/// builtins unrestricted (full Node default — preserving today's behavior).
8913/// Non-node platforms enforce an empty list (deny all builtins).
8914fn js_runtime_enforced_builtins(vm: &VmState) -> Option<Vec<String>> {
8915    if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node {
8916        return Some(Vec::new());
8917    }
8918    vm.configuration
8919        .js_runtime
8920        .as_ref()
8921        .and_then(|cfg| cfg.allowed_builtins.clone())
8922}
8923
8924fn configured_allowed_node_builtins(vm: &VmState) -> Vec<String> {
8925    // Non-node platforms expose no Node builtin modules at all.
8926    if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node {
8927        return Vec::new();
8928    }
8929    // Under the node platform an explicit allow-list wins — including an explicit
8930    // empty list, which means deny all. Absence falls back to the engine default.
8931    let configured = match vm
8932        .configuration
8933        .js_runtime
8934        .as_ref()
8935        .and_then(|cfg| cfg.allowed_builtins.as_ref())
8936    {
8937        Some(list) => list.clone(),
8938        None => DEFAULT_ALLOWED_NODE_BUILTINS
8939            .iter()
8940            .map(|value| (*value).to_owned())
8941            .collect::<Vec<_>>(),
8942    };
8943    dedupe_strings(&configured)
8944}
8945
8946fn configured_loopback_exempt_ports(vm: &VmState) -> Vec<String> {
8947    if !vm.configuration.loopback_exempt_ports.is_empty() {
8948        return vm
8949            .configuration
8950            .loopback_exempt_ports
8951            .iter()
8952            .map(ToString::to_string)
8953            .collect();
8954    }
8955
8956    vm.create_loopback_exempt_ports
8957        .iter()
8958        .map(ToString::to_string)
8959        .collect()
8960}
8961
8962/// Extract the `hostPath` string from a mount plugin's JSON-encoded config.
8963fn mount_config_host_path(config: &str) -> Option<String> {
8964    serde_json::from_str::<Value>(config)
8965        .ok()?
8966        .get("hostPath")
8967        .and_then(Value::as_str)
8968        .map(str::to_owned)
8969}
8970
8971fn runtime_guest_writable_host_paths(vm: &VmState) -> Vec<PathBuf> {
8972    vm.configuration
8973        .mounts
8974        .iter()
8975        .filter(|mount| !mount.read_only)
8976        .filter_map(|mount| {
8977            ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
8978                .then(|| mount_config_host_path(&mount.plugin.config))
8979                .flatten()
8980                .map(PathBuf::from)
8981        })
8982        .collect()
8983}
8984
8985fn runtime_guest_path_mappings(vm: &VmState) -> Vec<RuntimeGuestPathMapping> {
8986    let mut mappings = vm
8987        .configuration
8988        .mounts
8989        .iter()
8990        .filter_map(|mount| {
8991            ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
8992                .then(|| {
8993                    mount_config_host_path(&mount.plugin.config).map(|host_path| {
8994                        RuntimeGuestPathMapping {
8995                            guest_path: normalize_path(&mount.guest_path),
8996                            host_path,
8997                            read_only: mount.read_only,
8998                        }
8999                    })
9000                })
9001                .flatten()
9002        })
9003        .collect::<Vec<_>>();
9004    let mut command_root_mappings = vm
9005        .command_guest_paths
9006        .values()
9007        .filter_map(|guest_path| {
9008            Path::new(guest_path)
9009                .parent()
9010                .and_then(|parent| parent.to_str())
9011                .map(normalize_path)
9012        })
9013        .collect::<BTreeSet<_>>()
9014        .into_iter()
9015        .map(|guest_path| RuntimeGuestPathMapping {
9016            host_path: resolve_vm_guest_path_to_host(vm, &guest_path)
9017                .to_string_lossy()
9018                .into_owned(),
9019            guest_path,
9020            read_only: false,
9021        })
9022        .collect::<Vec<_>>();
9023    mappings.append(&mut command_root_mappings);
9024    let mut extra_node_modules_roots = mappings
9025        .iter()
9026        .filter(|mapping| mapping.guest_path.starts_with("/root/node_modules/"))
9027        .filter_map(|mapping| {
9028            host_node_modules_root(Path::new(&mapping.host_path)).map(|host_root| {
9029                RuntimeGuestPathMapping {
9030                    guest_path: String::from("/root/node_modules"),
9031                    host_path: host_root.to_string_lossy().into_owned(),
9032                    read_only: mapping.read_only,
9033                }
9034            })
9035        })
9036        .collect::<Vec<_>>();
9037    mappings.append(&mut extra_node_modules_roots);
9038    mappings.push(RuntimeGuestPathMapping {
9039        guest_path: String::from("/"),
9040        host_path: vm.cwd.to_string_lossy().into_owned(),
9041        read_only: false,
9042    });
9043    mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.guest_path.len()));
9044    mappings.dedup_by(|left, right| {
9045        left.guest_path == right.guest_path && left.host_path == right.host_path
9046    });
9047    mappings
9048}
9049
9050/// Build a `Send`-able, read-only VFS module reader over the VM's read-only
9051/// `host_dir`/`module_access` mounts (and the derived `/root/node_modules` root
9052/// for nested mounts). When present, the V8 bridge thread resolves modules
9053/// inline against this reader — concurrently with the service loop — so a large
9054/// cold-start module graph never serializes behind / starves an in-flight ACP
9055/// `session/new` bootstrap on the single service-loop thread. The reader reads
9056/// the same mounted tree the guest sees (anchored `openat2`, escaping-symlink
9057/// refusal), never the host-direct path translator. Returns `None` when the VM
9058/// has no usable read-only mount, so resolution falls back to the service-loop
9059/// kernel reader.
9060fn build_module_reader(
9061    vm: &VmState,
9062    resolved: &ResolvedChildProcessExecution,
9063) -> Option<crate::plugins::host_dir::HostDirModuleReader> {
9064    let mut pairs: Vec<(String, PathBuf)> = vm
9065        .configuration
9066        .mounts
9067        .iter()
9068        .filter(|mount| mount.read_only)
9069        .filter(|mount| (mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
9070        .filter_map(|mount| {
9071            mount_config_host_path(&mount.plugin.config)
9072                .map(|host_path| (normalize_path(&mount.guest_path), PathBuf::from(host_path)))
9073        })
9074        .collect();
9075
9076    let guest_entrypoint = resolved
9077        .env
9078        .get("AGENTOS_GUEST_ENTRYPOINT")
9079        .map(|path| normalize_path(path));
9080    if let Some(guest_entrypoint) = guest_entrypoint.as_deref() {
9081        let entrypoint_in_read_only_mount = pairs.iter().any(|(guest_path, _)| {
9082            guest_entrypoint == guest_path
9083                || guest_entrypoint.starts_with(&format!("{guest_path}/"))
9084        });
9085        if !entrypoint_in_read_only_mount {
9086            return None;
9087        }
9088    }
9089
9090    // Mirror runtime_guest_path_mappings: a mount nested under
9091    // `/root/node_modules/<pkg>` implies a `/root/node_modules` root the resolver
9092    // walks, so expose that root too (e.g. software-package mounts).
9093    let extra_roots: Vec<(String, PathBuf)> = pairs
9094        .iter()
9095        .filter(|(guest_path, _)| guest_path.starts_with("/root/node_modules/"))
9096        .filter_map(|(_, host_path)| {
9097            host_node_modules_root(host_path).map(|root| (String::from("/root/node_modules"), root))
9098        })
9099        .collect();
9100    pairs.extend(extra_roots);
9101
9102    crate::plugins::host_dir::HostDirModuleReader::from_mounts(pairs)
9103}
9104
9105fn host_node_modules_root(path: &Path) -> Option<PathBuf> {
9106    if let Some(root) = path
9107        .ancestors()
9108        .filter(|candidate| {
9109            candidate.file_name().and_then(|name| name.to_str()) == Some("node_modules")
9110        })
9111        .last()
9112        .map(Path::to_path_buf)
9113    {
9114        return Some(root);
9115    }
9116
9117    fs::canonicalize(path)
9118        .ok()?
9119        .ancestors()
9120        .filter(|candidate| {
9121            candidate.file_name().and_then(|name| name.to_str()) == Some("node_modules")
9122        })
9123        .last()
9124        .map(Path::to_path_buf)
9125}
9126
9127#[cfg(test)]
9128mod runtime_guest_path_mapping_tests {
9129    use super::{host_node_modules_root, javascript_sync_rpc_option_bool};
9130    use serde_json::json;
9131    use std::fs;
9132    use std::time::{SystemTime, UNIX_EPOCH};
9133
9134    #[test]
9135    fn host_node_modules_root_prefers_workspace_root_over_pnpm_package_node_modules() {
9136        let unique = SystemTime::now()
9137            .duration_since(UNIX_EPOCH)
9138            .expect("clock should be monotonic")
9139            .as_nanos();
9140        let temp = std::env::temp_dir().join(format!("secure-exec-sidecar-node-modules-{unique}"));
9141        let workspace_node_modules = temp.join("node_modules");
9142        let package_root = workspace_node_modules
9143            .join(".pnpm")
9144            .join("example@1.0.0")
9145            .join("node_modules")
9146            .join("@scope")
9147            .join("pkg");
9148        fs::create_dir_all(&package_root).expect("package root should be created");
9149
9150        let resolved =
9151            host_node_modules_root(&package_root).expect("node_modules root should resolve");
9152
9153        assert_eq!(resolved, workspace_node_modules);
9154
9155        fs::remove_dir_all(&temp).expect("temp tree should be removed");
9156    }
9157
9158    #[test]
9159    fn host_node_modules_root_preserves_symlinked_workspace_node_modules_path() {
9160        let unique = SystemTime::now()
9161            .duration_since(UNIX_EPOCH)
9162            .expect("clock should be monotonic")
9163            .as_nanos();
9164        let temp =
9165            std::env::temp_dir().join(format!("secure-exec-sidecar-node-modules-symlink-{unique}"));
9166        let workspace_node_modules = temp.join("node_modules");
9167        let package_link = workspace_node_modules.join("@scope").join("pkg");
9168        let real_package = temp.join("registry").join("agent").join("pkg");
9169        fs::create_dir_all(package_link.parent().expect("package parent should exist"))
9170            .expect("scoped parent should be created");
9171        fs::create_dir_all(&real_package).expect("real package root should be created");
9172        std::os::unix::fs::symlink(&real_package, &package_link)
9173            .expect("package symlink should be created");
9174
9175        let resolved =
9176            host_node_modules_root(&package_link).expect("node_modules root should resolve");
9177
9178        assert_eq!(resolved, workspace_node_modules);
9179
9180        fs::remove_dir_all(&temp).expect("temp tree should be removed");
9181    }
9182
9183    #[test]
9184    fn javascript_sync_rpc_option_bool_accepts_boolean_recursive_argument() {
9185        assert_eq!(
9186            javascript_sync_rpc_option_bool(&[json!("/workspace"), json!(true)], 1, "recursive"),
9187            Some(true)
9188        );
9189        assert_eq!(
9190            javascript_sync_rpc_option_bool(
9191                &[json!("/workspace"), json!({ "recursive": false })],
9192                1,
9193                "recursive"
9194            ),
9195            Some(false)
9196        );
9197    }
9198}
9199
9200#[cfg(test)]
9201mod kernel_poll_sync_rpc_tests {
9202    use super::{
9203        service_javascript_kernel_poll_sync_rpc, ActiveExecution, ActiveProcess,
9204        JavascriptSyncRpcRequest, KernelPollFdResponse, SidecarKernel, ToolExecution,
9205        EXECUTION_DRIVER_NAME, JAVASCRIPT_COMMAND,
9206    };
9207    use secure_exec_kernel::command_registry::CommandDriver;
9208    use secure_exec_kernel::kernel::{KernelVmConfig, SpawnOptions};
9209    use secure_exec_kernel::mount_table::MountTable;
9210    use secure_exec_kernel::permissions::Permissions;
9211    use secure_exec_kernel::poll::{POLLHUP, POLLIN};
9212    use secure_exec_kernel::vfs::MemoryFileSystem;
9213    use serde_json::{json, Value};
9214    #[test]
9215    fn javascript_kernel_poll_sync_rpc_reports_multiple_kernel_fds() {
9216        let mut config = KernelVmConfig::new("vm-js-kernel-poll");
9217        config.permissions = Permissions::allow_all();
9218        let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config);
9219        kernel
9220            .register_driver(CommandDriver::new(
9221                EXECUTION_DRIVER_NAME,
9222                [JAVASCRIPT_COMMAND],
9223            ))
9224            .expect("register execution driver");
9225
9226        let kernel_handle = kernel
9227            .spawn_process(
9228                JAVASCRIPT_COMMAND,
9229                Vec::new(),
9230                SpawnOptions {
9231                    requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
9232                    ..SpawnOptions::default()
9233                },
9234            )
9235            .expect("spawn javascript kernel process");
9236        let pid = kernel_handle.pid();
9237
9238        let (stdin_read_fd, stdin_write_fd) = kernel
9239            .open_pipe(EXECUTION_DRIVER_NAME, pid)
9240            .expect("open kernel stdin pipe");
9241        kernel
9242            .fd_dup2(EXECUTION_DRIVER_NAME, pid, stdin_read_fd, 0)
9243            .expect("dup stdin pipe onto fd 0");
9244        kernel
9245            .fd_close(EXECUTION_DRIVER_NAME, pid, stdin_read_fd)
9246            .expect("close original stdin read fd");
9247
9248        let process = ActiveProcess::new(
9249            pid,
9250            kernel_handle,
9251            super::GuestRuntimeKind::JavaScript,
9252            ActiveExecution::Tool(ToolExecution::default()),
9253        );
9254
9255        kernel
9256            .fd_write(EXECUTION_DRIVER_NAME, pid, stdin_write_fd, b"poll-ready")
9257            .expect("write kernel stdin payload");
9258        kernel
9259            .fd_close(EXECUTION_DRIVER_NAME, pid, stdin_write_fd)
9260            .expect("close kernel stdin writer");
9261
9262        let response = service_javascript_kernel_poll_sync_rpc(
9263            &mut kernel,
9264            &process,
9265            &JavascriptSyncRpcRequest {
9266                id: 1,
9267                method: String::from("__kernel_poll"),
9268                args: vec![
9269                    json!([
9270                        { "fd": 0, "events": POLLIN.bits() },
9271                        { "fd": 1, "events": POLLIN.bits() }
9272                    ]),
9273                    json!(250),
9274                ],
9275            },
9276        )
9277        .expect("poll kernel fds");
9278
9279        assert_eq!(response["readyCount"], Value::from(1));
9280        let fds: Vec<KernelPollFdResponse> =
9281            serde_json::from_value(response["fds"].clone()).expect("kernel poll fd response");
9282        assert_eq!(
9283            fds,
9284            vec![
9285                KernelPollFdResponse {
9286                    fd: 0,
9287                    events: POLLIN.bits(),
9288                    revents: (POLLIN | POLLHUP).bits(),
9289                },
9290                KernelPollFdResponse {
9291                    fd: 1,
9292                    events: POLLIN.bits(),
9293                    revents: 0,
9294                },
9295            ]
9296        );
9297
9298        process.kernel_handle.finish(0);
9299        kernel.waitpid(pid).expect("wait javascript kernel process");
9300    }
9301}
9302
9303fn dedupe_strings(values: &[String]) -> Vec<String> {
9304    let mut seen = BTreeSet::new();
9305    let mut deduped = Vec::new();
9306    for value in values {
9307        if seen.insert(value.clone()) {
9308            deduped.push(value.clone());
9309        }
9310    }
9311    deduped
9312}
9313
9314fn dedupe_host_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
9315    let mut seen = BTreeSet::new();
9316    let mut deduped = Vec::new();
9317    for path in paths {
9318        let normalized = normalize_host_path(path);
9319        let key = normalized.to_string_lossy().into_owned();
9320        if seen.insert(key) {
9321            deduped.push(normalized);
9322        }
9323    }
9324    deduped
9325}
9326
9327fn expand_host_access_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
9328    let mut expanded = Vec::new();
9329    let mut seen = BTreeSet::new();
9330
9331    let mut add_path = |candidate: PathBuf| {
9332        let normalized = normalize_host_path(&candidate);
9333        let key = normalized.to_string_lossy().into_owned();
9334        if seen.insert(key) {
9335            expanded.push(normalized);
9336        }
9337    };
9338
9339    for host_path in paths {
9340        add_path(host_path.clone());
9341        if let Ok(realpath) = fs::canonicalize(host_path) {
9342            add_path(realpath);
9343        }
9344
9345        if host_path.file_name().and_then(|name| name.to_str()) != Some("node_modules") {
9346            continue;
9347        }
9348
9349        let mut current = host_path.parent();
9350        while let Some(parent) = current {
9351            let candidate = parent.join("node_modules");
9352            if candidate.exists() {
9353                add_path(candidate.clone());
9354                if let Ok(realpath) = fs::canonicalize(&candidate) {
9355                    add_path(realpath);
9356                }
9357            }
9358            current = parent.parent();
9359        }
9360    }
9361
9362    expanded
9363}
9364
9365fn prepare_javascript_shadow(
9366    vm: &mut VmState,
9367    resolved: &ResolvedChildProcessExecution,
9368) -> Result<(), SidecarError> {
9369    let guest_entrypoint = resolved
9370        .env
9371        .get("AGENTOS_GUEST_ENTRYPOINT")
9372        .cloned()
9373        // An absolute `entrypoint` may be a host path that lives inside the VM's
9374        // host cwd (callers can pass a fully-qualified host path). The guest sees
9375        // it at its translated guest path (host_cwd -> guest_cwd), so the shadow
9376        // must be keyed by that guest path rather than the raw host path. Falling
9377        // back to the host path here would materialize the file at the wrong guest
9378        // location and the runtime's `require()` would fail with "Cannot find
9379        // module".
9380        .or_else(|| {
9381            resolve_host_entrypoint_within_vm_host_cwd(vm, &resolved.entrypoint)
9382                .map(|(guest_entrypoint, _)| guest_entrypoint)
9383        })
9384        .or_else(|| {
9385            resolved
9386                .entrypoint
9387                .starts_with('/')
9388                .then(|| normalize_path(&resolved.entrypoint))
9389        });
9390    let Some(guest_entrypoint) = guest_entrypoint else {
9391        return Ok(());
9392    };
9393    if host_mount_path_for_guest_path(vm, &guest_entrypoint).is_some() {
9394        return Ok(());
9395    }
9396    if vm.kernel.lstat(&guest_entrypoint).is_err() {
9397        let host_entrypoint = {
9398            let candidate = Path::new(&resolved.entrypoint);
9399            if candidate.is_absolute() {
9400                candidate.to_path_buf()
9401            } else {
9402                resolved.host_cwd.join(candidate)
9403            }
9404        };
9405        if host_entrypoint.exists() {
9406            materialize_host_path_to_shadow(vm, &guest_entrypoint, &host_entrypoint)?;
9407            // The shadow write only stages the file on the host side; the runtime
9408            // resolves modules against the kernel VFS, so the staged entrypoint
9409            // must be synced into the kernel before execution starts (otherwise
9410            // `require()` reports "Cannot find module").
9411            return sync_shadow_entrypoint_into_kernel(vm, &guest_entrypoint);
9412        }
9413    }
9414    materialize_guest_path_to_shadow(vm, &guest_entrypoint)
9415}
9416
9417/// Sync a freshly-staged shadow entrypoint into the kernel VFS so the runtime's
9418/// kernel-backed module resolver can read it. Mirrors the host->kernel file sync
9419/// used by the broader shadow reconciliation, but scoped to the single
9420/// entrypoint we just materialized.
9421fn sync_shadow_entrypoint_into_kernel(
9422    vm: &mut VmState,
9423    guest_entrypoint: &str,
9424) -> Result<(), SidecarError> {
9425    if vm.kernel.exists(guest_entrypoint).unwrap_or(false) {
9426        return Ok(());
9427    }
9428    let shadow_path = shadow_path_for_guest(vm, guest_entrypoint);
9429    let bytes = match fs::read(&shadow_path) {
9430        Ok(bytes) => bytes,
9431        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
9432        Err(error) => {
9433            return Err(SidecarError::Io(format!(
9434                "failed to read staged shadow entrypoint {}: {error}",
9435                shadow_path.display()
9436            )));
9437        }
9438    };
9439    if let Some(parent) = guest_parent_path(guest_entrypoint) {
9440        if !vm.kernel.exists(&parent).unwrap_or(false) {
9441            vm.kernel.mkdir(&parent, true).map_err(kernel_error)?;
9442        }
9443    }
9444    vm.kernel
9445        .write_file(guest_entrypoint, bytes)
9446        .map_err(kernel_error)?;
9447    Ok(())
9448}
9449
9450fn guest_parent_path(guest_path: &str) -> Option<String> {
9451    let parent = Path::new(guest_path).parent()?;
9452    let parent = parent.to_string_lossy();
9453    if parent.is_empty() || parent == "/" {
9454        None
9455    } else {
9456        Some(parent.into_owned())
9457    }
9458}
9459
9460fn materialize_host_path_to_shadow(
9461    vm: &VmState,
9462    guest_path: &str,
9463    host_path: &Path,
9464) -> Result<(), SidecarError> {
9465    let shadow_path = shadow_path_for_guest(vm, guest_path);
9466    let metadata = fs::symlink_metadata(host_path)
9467        .map_err(|error| SidecarError::Io(format!("failed to stat host entrypoint: {error}")))?;
9468
9469    if metadata.file_type().is_symlink() {
9470        if let Some(parent) = shadow_path.parent() {
9471            fs::create_dir_all(parent).map_err(|error| {
9472                SidecarError::Io(format!("failed to create shadow symlink parent: {error}"))
9473            })?;
9474        }
9475        let _ = fs::remove_file(&shadow_path);
9476        let _ = fs::remove_dir_all(&shadow_path);
9477        let target = fs::read_link(host_path)
9478            .map_err(|error| SidecarError::Io(format!("failed to read host symlink: {error}")))?;
9479        std::os::unix::fs::symlink(&target, &shadow_path)
9480            .map_err(|error| SidecarError::Io(format!("failed to mirror host symlink: {error}")))?;
9481        return Ok(());
9482    }
9483
9484    if metadata.is_dir() {
9485        fs::create_dir_all(&shadow_path).map_err(|error| {
9486            SidecarError::Io(format!("failed to create shadow directory: {error}"))
9487        })?;
9488        fs::set_permissions(
9489            &shadow_path,
9490            fs::Permissions::from_mode(metadata.permissions().mode() & 0o7777),
9491        )
9492        .map_err(|error| {
9493            SidecarError::Io(format!(
9494                "failed to set shadow directory mode on {}: {error}",
9495                shadow_path.display()
9496            ))
9497        })?;
9498        return Ok(());
9499    }
9500
9501    if let Some(parent) = shadow_path.parent() {
9502        fs::create_dir_all(parent).map_err(|error| {
9503            SidecarError::Io(format!("failed to create shadow parent: {error}"))
9504        })?;
9505    }
9506    let bytes = fs::read(host_path)
9507        .map_err(|error| SidecarError::Io(format!("failed to read host entrypoint: {error}")))?;
9508    fs::write(&shadow_path, bytes).map_err(|error| {
9509        SidecarError::Io(format!(
9510            "failed to mirror host file into shadow root: {error}"
9511        ))
9512    })?;
9513    fs::set_permissions(
9514        &shadow_path,
9515        fs::Permissions::from_mode(metadata.permissions().mode() & 0o7777),
9516    )
9517    .map_err(|error| {
9518        SidecarError::Io(format!(
9519            "failed to set shadow file mode on {}: {error}",
9520            shadow_path.display()
9521        ))
9522    })?;
9523    Ok(())
9524}
9525
9526fn materialize_guest_path_to_shadow(
9527    vm: &mut VmState,
9528    guest_path: &str,
9529) -> Result<(), SidecarError> {
9530    let stat = vm.kernel.lstat(guest_path).map_err(kernel_error)?;
9531    let shadow_path = shadow_path_for_guest(vm, guest_path);
9532
9533    if stat.is_symbolic_link {
9534        if let Some(parent) = shadow_path.parent() {
9535            fs::create_dir_all(parent).map_err(|error| {
9536                SidecarError::Io(format!("failed to create shadow symlink parent: {error}"))
9537            })?;
9538        }
9539        let _ = fs::remove_file(&shadow_path);
9540        let _ = fs::remove_dir_all(&shadow_path);
9541        let target = vm.kernel.read_link(guest_path).map_err(kernel_error)?;
9542        std::os::unix::fs::symlink(&target, &shadow_path)
9543            .map_err(|error| SidecarError::Io(format!("failed to mirror symlink: {error}")))?;
9544        return Ok(());
9545    }
9546
9547    if stat.is_directory {
9548        fs::create_dir_all(&shadow_path).map_err(|error| {
9549            SidecarError::Io(format!("failed to create shadow directory: {error}"))
9550        })?;
9551        fs::set_permissions(&shadow_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err(
9552            |error| {
9553                SidecarError::Io(format!(
9554                    "failed to set shadow directory mode on {}: {error}",
9555                    shadow_path.display()
9556                ))
9557            },
9558        )?;
9559        return Ok(());
9560    }
9561
9562    if let Some(parent) = shadow_path.parent() {
9563        fs::create_dir_all(parent).map_err(|error| {
9564            SidecarError::Io(format!("failed to create shadow parent: {error}"))
9565        })?;
9566    }
9567    let bytes = vm.kernel.read_file(guest_path).map_err(kernel_error)?;
9568    fs::write(&shadow_path, bytes).map_err(|error| {
9569        SidecarError::Io(format!(
9570            "failed to mirror guest file into shadow root: {error}"
9571        ))
9572    })?;
9573    fs::set_permissions(&shadow_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err(
9574        |error| {
9575            SidecarError::Io(format!(
9576                "failed to set shadow file mode on {}: {error}",
9577                shadow_path.display()
9578            ))
9579        },
9580    )?;
9581    Ok(())
9582}
9583
9584fn load_javascript_entrypoint_source(
9585    vm: &mut VmState,
9586    host_cwd: &Path,
9587    entrypoint: &str,
9588    env: &BTreeMap<String, String>,
9589) -> Option<String> {
9590    let mut read_guest_file = |path: &str| {
9591        vm.kernel
9592            .read_file(path)
9593            .ok()
9594            .and_then(|bytes| String::from_utf8(bytes).ok())
9595    };
9596
9597    if let Some(source) = env
9598        .get("AGENTOS_GUEST_ENTRYPOINT")
9599        .filter(|path| path.starts_with('/'))
9600        .and_then(|path| read_guest_file(path))
9601    {
9602        return Some(source);
9603    }
9604
9605    if entrypoint.starts_with('/') {
9606        if let Some(source) = read_guest_file(entrypoint) {
9607            return Some(source);
9608        }
9609    }
9610
9611    let host_entrypoint = if Path::new(entrypoint).is_absolute() {
9612        PathBuf::from(entrypoint)
9613    } else {
9614        host_cwd.join(entrypoint)
9615    };
9616    let normalized_entrypoint = normalize_host_path(&host_entrypoint);
9617    let sandbox_root = normalize_host_path(&vm.cwd);
9618    let host_cwd = normalize_host_path(&vm.host_cwd);
9619    if !path_is_within_root(&normalized_entrypoint, &sandbox_root)
9620        && !path_is_within_root(&normalized_entrypoint, &host_cwd)
9621    {
9622        return None;
9623    }
9624
9625    fs::read_to_string(&normalized_entrypoint).ok()
9626}
9627
9628fn emit_dns_resolution_event<B>(
9629    bridge: &SharedBridge<B>,
9630    vm_id: &str,
9631    hostname: &str,
9632    source: KernelDnsResolutionSource,
9633    addresses: &[IpAddr],
9634    dns: &VmDnsConfig,
9635) where
9636    B: NativeSidecarBridge + Send + 'static,
9637    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9638{
9639    let _ = emit_structured_event(
9640        bridge,
9641        vm_id,
9642        "network.dns.resolved",
9643        audit_fields([
9644            ("hostname", hostname.to_owned()),
9645            ("source", source.as_str().to_owned()),
9646            (
9647                "addresses",
9648                addresses
9649                    .iter()
9650                    .map(ToString::to_string)
9651                    .collect::<Vec<_>>()
9652                    .join(","),
9653            ),
9654            ("address_count", addresses.len().to_string()),
9655            ("resolver_count", dns.name_servers.len().to_string()),
9656            (
9657                "resolvers",
9658                dns.name_servers
9659                    .iter()
9660                    .map(ToString::to_string)
9661                    .collect::<Vec<_>>()
9662                    .join(","),
9663            ),
9664        ]),
9665    );
9666}
9667
9668fn emit_dns_record_resolution_event<B>(
9669    bridge: &SharedBridge<B>,
9670    vm_id: &str,
9671    hostname: &str,
9672    resolution: &DnsRecordResolution,
9673    dns: &VmDnsConfig,
9674) where
9675    B: NativeSidecarBridge + Send + 'static,
9676    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9677{
9678    if let Some(addresses) = dns_resolution_ip_addrs(resolution.records()) {
9679        emit_dns_resolution_event(
9680            bridge,
9681            vm_id,
9682            hostname,
9683            resolution.source(),
9684            &addresses,
9685            dns,
9686        );
9687        return;
9688    }
9689
9690    let _ = emit_structured_event(
9691        bridge,
9692        vm_id,
9693        "network.dns.resolved",
9694        audit_fields([
9695            ("hostname", hostname.to_owned()),
9696            ("source", resolution.source().as_str().to_owned()),
9697            (
9698                "addresses",
9699                resolution
9700                    .records()
9701                    .iter()
9702                    .map(summarize_dns_record)
9703                    .collect::<Vec<_>>()
9704                    .join(","),
9705            ),
9706            ("address_count", resolution.records().len().to_string()),
9707            ("resolver_count", dns.name_servers.len().to_string()),
9708            (
9709                "resolvers",
9710                dns.name_servers
9711                    .iter()
9712                    .map(ToString::to_string)
9713                    .collect::<Vec<_>>()
9714                    .join(","),
9715            ),
9716        ]),
9717    );
9718}
9719
9720fn emit_dns_resolution_failure_event<B>(
9721    bridge: &SharedBridge<B>,
9722    vm_id: &str,
9723    hostname: &str,
9724    dns: &VmDnsConfig,
9725    error: &SidecarError,
9726) where
9727    B: NativeSidecarBridge + Send + 'static,
9728    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9729{
9730    let _ = emit_structured_event(
9731        bridge,
9732        vm_id,
9733        "network.dns.resolve_failed",
9734        audit_fields([
9735            ("hostname", hostname.to_owned()),
9736            ("reason", error.to_string()),
9737            ("resolver_count", dns.name_servers.len().to_string()),
9738            (
9739                "resolvers",
9740                dns.name_servers
9741                    .iter()
9742                    .map(ToString::to_string)
9743                    .collect::<Vec<_>>()
9744                    .join(","),
9745            ),
9746        ]),
9747    );
9748}
9749
9750fn parse_dns_record_type(rrtype: &str) -> Result<RecordType, SidecarError> {
9751    match rrtype {
9752        "A" => Ok(RecordType::A),
9753        "AAAA" => Ok(RecordType::AAAA),
9754        "MX" => Ok(RecordType::MX),
9755        "TXT" => Ok(RecordType::TXT),
9756        "SRV" => Ok(RecordType::SRV),
9757        "CNAME" => Ok(RecordType::CNAME),
9758        "PTR" => Ok(RecordType::PTR),
9759        "NS" => Ok(RecordType::NS),
9760        "SOA" => Ok(RecordType::SOA),
9761        "NAPTR" => Ok(RecordType::NAPTR),
9762        "CAA" => Ok(RecordType::CAA),
9763        "ANY" => Ok(RecordType::ANY),
9764        other => Err(SidecarError::Execution(format!(
9765            "ERR_NOT_IMPLEMENTED: dns rrtype {other} is not supported by the secure-exec dns bridge"
9766        ))),
9767    }
9768}
9769
9770fn dns_resolution_to_node_value(
9771    resolution: &DnsRecordResolution,
9772    requested_type: &str,
9773) -> Result<Value, SidecarError> {
9774    let safe_ips = dns_resolution_safe_ip_set(resolution.records(), resolution.hostname())?;
9775    match requested_type {
9776        "A" | "AAAA" => Ok(Value::Array(
9777            resolution
9778                .records()
9779                .iter()
9780                .filter_map(|record| dns_record_ip_string(record, &safe_ips))
9781                .map(Value::String)
9782                .collect(),
9783        )),
9784        "MX" => Ok(Value::Array(
9785            resolution
9786                .records()
9787                .iter()
9788                .filter_map(|record| match record.data() {
9789                    RData::MX(mx) => Some(json!({
9790                        "priority": mx.preference,
9791                        "exchange": normalize_dns_name_for_node(&mx.exchange),
9792                        "type": "MX",
9793                    })),
9794                    _ => None,
9795                })
9796                .collect(),
9797        )),
9798        "TXT" => Ok(Value::Array(
9799            resolution
9800                .records()
9801                .iter()
9802                .filter_map(|record| match record.data() {
9803                    RData::TXT(txt) => Some(Value::Array(
9804                        txt.txt_data
9805                            .iter()
9806                            .map(|entry| Value::String(String::from_utf8_lossy(entry).into_owned()))
9807                            .collect(),
9808                    )),
9809                    _ => None,
9810                })
9811                .collect(),
9812        )),
9813        "SRV" => Ok(Value::Array(
9814            resolution
9815                .records()
9816                .iter()
9817                .filter_map(|record| match record.data() {
9818                    RData::SRV(srv) => Some(json!({
9819                        "priority": srv.priority,
9820                        "weight": srv.weight,
9821                        "port": srv.port,
9822                        "name": normalize_dns_name_for_node(&srv.target),
9823                        "type": "SRV",
9824                    })),
9825                    _ => None,
9826                })
9827                .collect(),
9828        )),
9829        "CNAME" => Ok(Value::Array(
9830            resolution
9831                .records()
9832                .iter()
9833                .filter_map(|record| match record.data() {
9834                    RData::CNAME(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9835                    _ => None,
9836                })
9837                .collect(),
9838        )),
9839        "PTR" => Ok(Value::Array(
9840            resolution
9841                .records()
9842                .iter()
9843                .filter_map(|record| match record.data() {
9844                    RData::PTR(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9845                    _ => None,
9846                })
9847                .collect(),
9848        )),
9849        "NS" => Ok(Value::Array(
9850            resolution
9851                .records()
9852                .iter()
9853                .filter_map(|record| match record.data() {
9854                    RData::NS(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9855                    _ => None,
9856                })
9857                .collect(),
9858        )),
9859        "SOA" => resolution
9860            .records()
9861            .iter()
9862            .find_map(|record| match record.data() {
9863                RData::SOA(soa) => Some(json!({
9864                    "nsname": normalize_dns_name_for_node(&soa.mname),
9865                    "hostmaster": normalize_dns_name_for_node(&soa.rname),
9866                    "serial": soa.serial,
9867                    "refresh": soa.refresh,
9868                    "retry": soa.retry,
9869                    "expire": soa.expire,
9870                    "minttl": soa.minimum,
9871                })),
9872                _ => None,
9873            })
9874            .ok_or_else(|| {
9875                SidecarError::Execution(String::from("failed to resolve DNS SOA record"))
9876            }),
9877        "NAPTR" => Ok(Value::Array(
9878            resolution
9879                .records()
9880                .iter()
9881                .filter_map(|record| match record.data() {
9882                    RData::NAPTR(naptr) => Some(json!({
9883                        "flags": String::from_utf8_lossy(&naptr.flags).into_owned(),
9884                        "service": String::from_utf8_lossy(&naptr.services).into_owned(),
9885                        "regexp": String::from_utf8_lossy(&naptr.regexp).into_owned(),
9886                        "replacement": normalize_dns_name_for_node(&naptr.replacement),
9887                        "order": naptr.order,
9888                        "preference": naptr.preference,
9889                    })),
9890                    _ => None,
9891                })
9892                .collect(),
9893        )),
9894        "CAA" => Ok(Value::Array(
9895            resolution
9896                .records()
9897                .iter()
9898                .filter_map(|record| match record.data() {
9899                    RData::CAA(caa) => {
9900                        let mut value = serde_json::Map::new();
9901                        value.insert(
9902                            "critical".to_owned(),
9903                            Value::from(u8::from(caa.issuer_critical)),
9904                        );
9905                        value.insert("type".to_owned(), Value::String(String::from("CAA")));
9906                        if caa.tag.eq_ignore_ascii_case("iodef") {
9907                            value.insert(
9908                                "iodef".to_owned(),
9909                                Value::String(
9910                                    caa.value_as_iodef()
9911                                        .map(|url| url.to_string())
9912                                        .unwrap_or_else(|_| {
9913                                            String::from_utf8_lossy(&caa.value).into_owned()
9914                                        }),
9915                                ),
9916                            );
9917                        } else if let Ok((issuer, _params)) = caa.value_as_issue() {
9918                            let field = if caa.tag.eq_ignore_ascii_case("issuewild") {
9919                                "issuewild"
9920                            } else {
9921                                "issue"
9922                            };
9923                            value.insert(
9924                                field.to_owned(),
9925                                Value::String(
9926                                    issuer.as_ref().map(ToString::to_string).unwrap_or_else(|| {
9927                                        String::from_utf8_lossy(&caa.value).into_owned()
9928                                    }),
9929                                ),
9930                            );
9931                        } else {
9932                            value.insert(
9933                                caa.tag.to_ascii_lowercase(),
9934                                Value::String(String::from_utf8_lossy(&caa.value).into_owned()),
9935                            );
9936                        }
9937                        Some(Value::Object(value))
9938                    }
9939                    _ => None,
9940                })
9941                .collect(),
9942        )),
9943        "ANY" => Ok(Value::Array(
9944            resolution
9945                .records()
9946                .iter()
9947                .filter_map(|record| dns_any_record_to_value(record, &safe_ips))
9948                .collect(),
9949        )),
9950        other => Err(SidecarError::Execution(format!(
9951            "ERR_NOT_IMPLEMENTED: dns rrtype {other} is not supported by the secure-exec dns bridge"
9952        ))),
9953    }
9954}
9955
9956fn dns_resolution_safe_ip_set(
9957    records: &[Record],
9958    hostname: &str,
9959) -> Result<BTreeSet<IpAddr>, SidecarError> {
9960    let ips = records
9961        .iter()
9962        .filter_map(dns_record_ip_addr)
9963        .collect::<Vec<_>>();
9964    if ips.is_empty() {
9965        return Ok(BTreeSet::new());
9966    }
9967    Ok(filter_dns_safe_ip_addrs(ips, hostname)?
9968        .into_iter()
9969        .collect())
9970}
9971
9972fn dns_resolution_ip_addrs(records: &[Record]) -> Option<Vec<IpAddr>> {
9973    let ips = records
9974        .iter()
9975        .filter_map(dns_record_ip_addr)
9976        .collect::<Vec<_>>();
9977    if ips.is_empty() {
9978        return None;
9979    }
9980    Some(ips)
9981}
9982
9983fn dns_record_ip_addr(record: &Record) -> Option<IpAddr> {
9984    match record.data() {
9985        RData::A(address) => Some(IpAddr::V4(**address)),
9986        RData::AAAA(address) => Some(IpAddr::V6(**address)),
9987        _ => None,
9988    }
9989}
9990
9991fn dns_record_ip_string(record: &Record, safe_ips: &BTreeSet<IpAddr>) -> Option<String> {
9992    let ip = dns_record_ip_addr(record)?;
9993    safe_ips.contains(&ip).then(|| ip.to_string())
9994}
9995
9996fn dns_any_record_to_value(record: &Record, safe_ips: &BTreeSet<IpAddr>) -> Option<Value> {
9997    let value = match record.data() {
9998        RData::A(_) | RData::AAAA(_) => json!({
9999            "address": dns_record_ip_string(record, safe_ips)?,
10000            "ttl": record.ttl(),
10001            "type": record.record_type().to_string(),
10002        }),
10003        RData::MX(mx) => json!({
10004            "exchange": normalize_dns_name_for_node(&mx.exchange),
10005            "priority": mx.preference,
10006            "type": "MX",
10007        }),
10008        RData::TXT(txt) => json!({
10009            "entries": txt
10010                .txt_data
10011                .iter()
10012                .map(|entry| String::from_utf8_lossy(entry).into_owned())
10013                .collect::<Vec<_>>(),
10014            "type": "TXT",
10015        }),
10016        RData::SRV(srv) => json!({
10017            "name": normalize_dns_name_for_node(&srv.target),
10018            "port": srv.port,
10019            "priority": srv.priority,
10020            "weight": srv.weight,
10021            "type": "SRV",
10022        }),
10023        RData::CNAME(name) => json!({
10024            "value": normalize_dns_name_for_node(&name.0),
10025            "type": "CNAME",
10026        }),
10027        RData::PTR(name) => json!({
10028            "value": normalize_dns_name_for_node(&name.0),
10029            "type": "PTR",
10030        }),
10031        RData::NS(name) => json!({
10032            "value": normalize_dns_name_for_node(&name.0),
10033            "type": "NS",
10034        }),
10035        RData::SOA(soa) => json!({
10036            "nsname": normalize_dns_name_for_node(&soa.mname),
10037            "hostmaster": normalize_dns_name_for_node(&soa.rname),
10038            "serial": soa.serial,
10039            "refresh": soa.refresh,
10040            "retry": soa.retry,
10041            "expire": soa.expire,
10042            "minttl": soa.minimum,
10043            "type": "SOA",
10044        }),
10045        RData::NAPTR(naptr) => json!({
10046            "flags": String::from_utf8_lossy(&naptr.flags).into_owned(),
10047            "service": String::from_utf8_lossy(&naptr.services).into_owned(),
10048            "regexp": String::from_utf8_lossy(&naptr.regexp).into_owned(),
10049            "replacement": normalize_dns_name_for_node(&naptr.replacement),
10050            "order": naptr.order,
10051            "preference": naptr.preference,
10052            "type": "NAPTR",
10053        }),
10054        RData::CAA(caa) => {
10055            let mut value = serde_json::Map::new();
10056            value.insert(
10057                "critical".to_owned(),
10058                Value::from(u8::from(caa.issuer_critical)),
10059            );
10060            value.insert("type".to_owned(), Value::String(String::from("CAA")));
10061            if caa.tag.eq_ignore_ascii_case("iodef") {
10062                value.insert(
10063                    "iodef".to_owned(),
10064                    Value::String(
10065                        caa.value_as_iodef()
10066                            .map(|url| url.to_string())
10067                            .unwrap_or_else(|_| String::from_utf8_lossy(&caa.value).into_owned()),
10068                    ),
10069                );
10070            } else if let Ok((issuer, _params)) = caa.value_as_issue() {
10071                let field = if caa.tag.eq_ignore_ascii_case("issuewild") {
10072                    "issuewild"
10073                } else {
10074                    "issue"
10075                };
10076                value.insert(
10077                    field.to_owned(),
10078                    Value::String(
10079                        issuer
10080                            .as_ref()
10081                            .map(ToString::to_string)
10082                            .unwrap_or_else(|| String::from_utf8_lossy(&caa.value).into_owned()),
10083                    ),
10084                );
10085            }
10086            Value::Object(value)
10087        }
10088        _ => return None,
10089    };
10090    Some(value)
10091}
10092
10093fn normalize_dns_name_for_node(name: &impl ToString) -> String {
10094    name.to_string().trim_end_matches('.').to_owned()
10095}
10096
10097fn summarize_dns_record(record: &Record) -> String {
10098    match record.data() {
10099        RData::A(_) | RData::AAAA(_) => record.data().to_string(),
10100        _ => format!("{} {}", record.record_type(), record.data()),
10101    }
10102}
10103
10104// build_root_filesystem, convert_root_lower_descriptor, convert_root_filesystem_entry,
10105// root_snapshot_entry moved to crate::bootstrap
10106
10107// apply_root_filesystem_entry, ensure_parent_directories moved to crate::bootstrap
10108
10109// ProcNetEntry moved to crate::state
10110
10111fn find_socket_state_entry(
10112    vm: Option<&VmState>,
10113    kind: SocketQueryKind,
10114    request: &FindListenerRequest,
10115) -> Result<Option<SocketStateEntry>, SidecarError> {
10116    let vm = vm.ok_or_else(|| SidecarError::InvalidState(String::from("unknown sidecar VM")))?;
10117
10118    for (process_id, process) in &vm.active_processes {
10119        if let Some(path) = request.path.as_deref() {
10120            if matches!(kind, SocketQueryKind::TcpListener) {
10121                for listener in process.unix_listeners.values() {
10122                    if listener.path() != path {
10123                        continue;
10124                    }
10125                    return Ok(Some(SocketStateEntry {
10126                        process_id: process_id.to_owned(),
10127                        host: None,
10128                        port: None,
10129                        path: Some(path.to_owned()),
10130                    }));
10131                }
10132            }
10133        }
10134
10135        if request.path.is_none() {
10136            if let Some(entry) =
10137                find_kernel_socket_state_entry(&vm.kernel, process_id, process, kind, request)?
10138            {
10139                return Ok(Some(entry));
10140            }
10141
10142            match kind {
10143                SocketQueryKind::TcpListener => {
10144                    for server in process.http_servers.values() {
10145                        let local_addr = server.guest_local_addr;
10146                        let local_host = local_addr.ip().to_string();
10147                        if !socket_host_matches(request.host.as_deref(), &local_host) {
10148                            continue;
10149                        }
10150                        if let Some(port) = request.port {
10151                            if local_addr.port() != port {
10152                                continue;
10153                            }
10154                        }
10155                        return Ok(Some(SocketStateEntry {
10156                            process_id: process_id.to_owned(),
10157                            host: Some(local_host),
10158                            port: Some(local_addr.port()),
10159                            path: None,
10160                        }));
10161                    }
10162
10163                    for listener in process.tcp_listeners.values() {
10164                        if listener.kernel_socket_id.is_some() {
10165                            continue;
10166                        }
10167                        let local_addr = listener.guest_local_addr();
10168                        let local_host = local_addr.ip().to_string();
10169                        if !socket_host_matches(request.host.as_deref(), &local_host) {
10170                            continue;
10171                        }
10172                        if let Some(port) = request.port {
10173                            if local_addr.port() != port {
10174                                continue;
10175                            }
10176                        }
10177                        return Ok(Some(SocketStateEntry {
10178                            process_id: process_id.to_owned(),
10179                            host: Some(local_host),
10180                            port: Some(local_addr.port()),
10181                            path: None,
10182                        }));
10183                    }
10184                }
10185                SocketQueryKind::UdpBound => {
10186                    for socket in process.udp_sockets.values() {
10187                        if socket.kernel_socket_id.is_some() {
10188                            continue;
10189                        }
10190                        let Some(local_addr) = socket.local_addr() else {
10191                            continue;
10192                        };
10193                        let local_host = local_addr.ip().to_string();
10194                        if !socket_host_matches(request.host.as_deref(), &local_host) {
10195                            continue;
10196                        }
10197                        if let Some(port) = request.port {
10198                            if local_addr.port() != port {
10199                                continue;
10200                            }
10201                        }
10202                        return Ok(Some(SocketStateEntry {
10203                            process_id: process_id.to_owned(),
10204                            host: Some(local_host),
10205                            port: Some(local_addr.port()),
10206                            path: None,
10207                        }));
10208                    }
10209                }
10210            }
10211        }
10212
10213        let child_pid = process.execution.child_pid();
10214        let inodes = socket_inodes_for_pid(child_pid)?;
10215        if inodes.is_empty() {
10216            continue;
10217        }
10218
10219        if let Some(path) = request.path.as_deref() {
10220            if let Some(listener) = find_unix_socket_for_pid(child_pid, &inodes, path, process_id)?
10221            {
10222                return Ok(Some(listener));
10223            }
10224            continue;
10225        }
10226
10227        let table_paths = match kind {
10228            SocketQueryKind::TcpListener => [
10229                format!("/proc/{child_pid}/net/tcp"),
10230                format!("/proc/{child_pid}/net/tcp6"),
10231            ],
10232            SocketQueryKind::UdpBound => [
10233                format!("/proc/{child_pid}/net/udp"),
10234                format!("/proc/{child_pid}/net/udp6"),
10235            ],
10236        };
10237        for table_path in table_paths {
10238            if let Some(entry) = find_inet_socket_for_pid(
10239                &table_path,
10240                &inodes,
10241                kind,
10242                request.host.as_deref(),
10243                request.port,
10244                process_id,
10245            )? {
10246                return Ok(Some(entry));
10247            }
10248        }
10249    }
10250
10251    Ok(None)
10252}
10253
10254fn require_vm_inspection_permission<B>(
10255    bridge: &SharedBridge<B>,
10256    vm_id: &str,
10257    capability: &str,
10258    domain: &str,
10259    resource: &str,
10260) -> Result<(), SidecarError>
10261where
10262    B: NativeSidecarBridge + Send + 'static,
10263    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
10264{
10265    let decision = bridge.static_permission_decision(vm_id, capability, domain, Some(resource));
10266    if decision.as_ref().is_some_and(|decision| decision.allow) {
10267        return Ok(());
10268    }
10269
10270    let reason = decision
10271        .and_then(|decision| decision.reason)
10272        .unwrap_or_else(|| format!("{capability} permission required"));
10273    Err(SidecarError::Execution(format!(
10274        "EACCES: permission denied, {resource}: {reason}"
10275    )))
10276}
10277
10278fn socket_query_resource(kind: SocketQueryKind, request: &FindListenerRequest) -> String {
10279    if let Some(path) = request.path.as_deref() {
10280        return format!("unix://{path}");
10281    }
10282
10283    let host = request.host.as_deref().unwrap_or("*");
10284    let port = request
10285        .port
10286        .map_or_else(|| String::from("*"), |port| port.to_string());
10287    match kind {
10288        SocketQueryKind::TcpListener => format!("tcp://{host}:{port}"),
10289        SocketQueryKind::UdpBound => format!("udp://{host}:{port}"),
10290    }
10291}
10292
10293fn snapshot_vm_processes(vm: &VmState) -> Vec<ProcessSnapshotEntry> {
10294    let process_table = vm.kernel.list_processes();
10295    snapshot_vm_processes_inner(vm, &process_table)
10296}
10297
10298fn snapshot_vm_processes_inner(
10299    vm: &VmState,
10300    process_table: &BTreeMap<u32, secure_exec_kernel::process_table::ProcessInfo>,
10301) -> Vec<ProcessSnapshotEntry> {
10302    let mut entries = Vec::new();
10303
10304    for (process_id, process) in &vm.active_processes {
10305        collect_process_snapshot_entries(process_id, process, process_table, &mut entries);
10306    }
10307
10308    for exited in &vm.exited_process_snapshots {
10309        entries.push(exited.process.clone());
10310    }
10311
10312    entries
10313}
10314
10315fn prune_exited_process_snapshots(vm: &mut VmState) {
10316    let cutoff = Instant::now() - EXITED_PROCESS_SNAPSHOT_RETENTION;
10317    while vm
10318        .exited_process_snapshots
10319        .front()
10320        .is_some_and(|snapshot| snapshot.captured_at < cutoff)
10321    {
10322        vm.exited_process_snapshots.pop_front();
10323    }
10324}
10325
10326fn build_process_snapshot_entry(
10327    process_id: &str,
10328    process: &ActiveProcess,
10329    info: &secure_exec_kernel::process_table::ProcessInfo,
10330    exit_code: Option<i32>,
10331) -> ProcessSnapshotEntry {
10332    ProcessSnapshotEntry {
10333        process_id: process_id.to_owned(),
10334        pid: info.pid,
10335        ppid: info.ppid,
10336        pgid: info.pgid,
10337        sid: info.sid,
10338        driver: info.driver.clone(),
10339        command: info.command.clone(),
10340        args: Vec::new(),
10341        cwd: process.guest_cwd.clone(),
10342        status: if exit_code.is_some() {
10343            ProcessSnapshotStatus::Exited
10344        } else {
10345            match info.status {
10346                ProcessStatus::Running => ProcessSnapshotStatus::Running,
10347                ProcessStatus::Stopped => ProcessSnapshotStatus::Stopped,
10348                ProcessStatus::Exited => ProcessSnapshotStatus::Exited,
10349            }
10350        },
10351        exit_code: exit_code.or(info.exit_code),
10352    }
10353}
10354
10355fn collect_process_snapshot_entries(
10356    process_id: &str,
10357    process: &ActiveProcess,
10358    process_table: &BTreeMap<u32, secure_exec_kernel::process_table::ProcessInfo>,
10359    entries: &mut Vec<ProcessSnapshotEntry>,
10360) {
10361    if let Some(info) = process_table.get(&process.kernel_pid) {
10362        entries.push(build_process_snapshot_entry(
10363            process_id, process, info, None,
10364        ));
10365    }
10366
10367    for (child_id, child) in &process.child_processes {
10368        let child_process_id = format!("{process_id}/{child_id}");
10369        collect_process_snapshot_entries(&child_process_id, child, process_table, entries);
10370    }
10371}
10372
10373fn find_kernel_socket_state_entry(
10374    kernel: &SidecarKernel,
10375    process_id: &str,
10376    process: &ActiveProcess,
10377    kind: SocketQueryKind,
10378    request: &FindListenerRequest,
10379) -> Result<Option<SocketStateEntry>, SidecarError> {
10380    let entry = match kind {
10381        SocketQueryKind::TcpListener => process
10382            .tcp_listeners
10383            .values()
10384            .filter_map(|listener| listener.kernel_socket_id)
10385            .find_map(|socket_id| {
10386                kernel_socket_state_entry(kernel, process_id, socket_id, kind, request)
10387            }),
10388        SocketQueryKind::UdpBound => process
10389            .udp_sockets
10390            .values()
10391            .filter_map(|socket| socket.kernel_socket_id)
10392            .find_map(|socket_id| {
10393                kernel_socket_state_entry(kernel, process_id, socket_id, kind, request)
10394            }),
10395    };
10396
10397    if entry.is_some() {
10398        return Ok(entry);
10399    }
10400
10401    for child in process.child_processes.values() {
10402        if let Some(entry) =
10403            find_kernel_socket_state_entry(kernel, process_id, child, kind, request)?
10404        {
10405            return Ok(Some(entry));
10406        }
10407    }
10408
10409    Ok(None)
10410}
10411
10412fn kernel_socket_state_entry(
10413    kernel: &SidecarKernel,
10414    process_id: &str,
10415    socket_id: SocketId,
10416    kind: SocketQueryKind,
10417    request: &FindListenerRequest,
10418) -> Option<SocketStateEntry> {
10419    let record = kernel.socket_get(socket_id)?;
10420    let local_address = record.local_address()?;
10421    match kind {
10422        SocketQueryKind::TcpListener if record.state() == SocketState::Listening => {}
10423        SocketQueryKind::TcpListener => return None,
10424        SocketQueryKind::UdpBound => {}
10425    }
10426
10427    if !socket_host_matches(request.host.as_deref(), local_address.host()) {
10428        return None;
10429    }
10430    if request
10431        .port
10432        .is_some_and(|port| local_address.port() != port)
10433    {
10434        return None;
10435    }
10436
10437    Some(SocketStateEntry {
10438        process_id: process_id.to_owned(),
10439        host: Some(local_address.host().to_owned()),
10440        port: Some(local_address.port()),
10441        path: None,
10442    })
10443}
10444
10445fn socket_inodes_for_pid(pid: u32) -> Result<BTreeSet<u64>, SidecarError> {
10446    let fd_dir = PathBuf::from(format!("/proc/{pid}/fd"));
10447    let entries = match fs::read_dir(&fd_dir) {
10448        Ok(entries) => entries,
10449        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(BTreeSet::new()),
10450        Err(error) => {
10451            return Err(SidecarError::Io(format!(
10452                "failed to read socket descriptors for process {pid}: {error}"
10453            )));
10454        }
10455    };
10456
10457    let mut inodes = BTreeSet::new();
10458    for entry in entries {
10459        let entry = entry.map_err(|error| {
10460            SidecarError::Io(format!(
10461                "failed to inspect fd entry for process {pid}: {error}"
10462            ))
10463        })?;
10464        let target = match fs::read_link(entry.path()) {
10465            Ok(target) => target,
10466            Err(_) => continue,
10467        };
10468        if let Some(inode) = parse_socket_inode(&target) {
10469            inodes.insert(inode);
10470        }
10471    }
10472
10473    Ok(inodes)
10474}
10475
10476fn parse_socket_inode(target: &Path) -> Option<u64> {
10477    let value = target.to_string_lossy();
10478    let trimmed = value.strip_prefix("socket:[")?.strip_suffix(']')?;
10479    trimmed.parse().ok()
10480}
10481
10482fn unix_socket_path(addr: &UnixSocketAddr) -> Option<String> {
10483    addr.as_pathname()
10484        .map(|path| path.to_string_lossy().into_owned())
10485}
10486
10487fn find_unix_socket_for_pid(
10488    pid: u32,
10489    inodes: &BTreeSet<u64>,
10490    path: &str,
10491    process_id: &str,
10492) -> Result<Option<SocketStateEntry>, SidecarError> {
10493    let table_path = format!("/proc/{pid}/net/unix");
10494    let contents = match fs::read_to_string(&table_path) {
10495        Ok(contents) => contents,
10496        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
10497        Err(error) => {
10498            return Err(SidecarError::Io(format!(
10499                "failed to inspect unix sockets for process {pid}: {error}"
10500            )));
10501        }
10502    };
10503
10504    for line in contents.lines().skip(1) {
10505        let columns = line.split_whitespace().collect::<Vec<_>>();
10506        if columns.len() < 8 {
10507            continue;
10508        }
10509        let Ok(inode) = columns[6].parse::<u64>() else {
10510            continue;
10511        };
10512        if !inodes.contains(&inode) || columns[7] != path {
10513            continue;
10514        }
10515        return Ok(Some(SocketStateEntry {
10516            process_id: process_id.to_owned(),
10517            host: None,
10518            port: None,
10519            path: Some(path.to_owned()),
10520        }));
10521    }
10522
10523    Ok(None)
10524}
10525
10526fn find_inet_socket_for_pid(
10527    table_path: &str,
10528    inodes: &BTreeSet<u64>,
10529    kind: SocketQueryKind,
10530    requested_host: Option<&str>,
10531    requested_port: Option<u16>,
10532    process_id: &str,
10533) -> Result<Option<SocketStateEntry>, SidecarError> {
10534    for entry in parse_proc_net_entries(table_path)? {
10535        if !inodes.contains(&entry.inode) {
10536            continue;
10537        }
10538        if matches!(kind, SocketQueryKind::TcpListener) && entry.state != "0A" {
10539            continue;
10540        }
10541        if !socket_host_matches(requested_host, &entry.local_host) {
10542            continue;
10543        }
10544        if let Some(port) = requested_port {
10545            if entry.local_port != port {
10546                continue;
10547            }
10548        }
10549        return Ok(Some(SocketStateEntry {
10550            process_id: process_id.to_owned(),
10551            host: Some(entry.local_host),
10552            port: Some(entry.local_port),
10553            path: None,
10554        }));
10555    }
10556
10557    Ok(None)
10558}
10559
10560fn is_unspecified_socket_host(host: &str) -> bool {
10561    host == "0.0.0.0" || host == "::"
10562}
10563
10564fn is_loopback_socket_host(host: &str) -> bool {
10565    host == "127.0.0.1" || host == "::1" || host.eq_ignore_ascii_case("localhost")
10566}
10567
10568pub(crate) fn vm_network_resource_counts(vm: &VmState) -> NetworkResourceCounts {
10569    let snapshot = vm.kernel.resource_snapshot();
10570    let mut counts = NetworkResourceCounts {
10571        sockets: snapshot.sockets,
10572        connections: snapshot.socket_connections,
10573    };
10574    for process in vm.active_processes.values() {
10575        let process_counts = process.sidecar_only_network_resource_counts();
10576        counts.sockets += process_counts.sockets;
10577        counts.connections += process_counts.connections;
10578    }
10579    counts
10580}
10581
10582#[allow(clippy::too_many_arguments)]
10583fn collect_javascript_socket_port_state(
10584    kernel: &SidecarKernel,
10585    process_id: &str,
10586    process: &ActiveProcess,
10587    tcp_guest_to_host: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10588    http_loopback_targets: &mut BTreeMap<
10589        (JavascriptSocketFamily, u16),
10590        JavascriptHttpLoopbackTarget,
10591    >,
10592    udp_guest_to_host: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10593    udp_host_to_guest: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10594    used_tcp_ports: &mut BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10595    used_udp_ports: &mut BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10596) {
10597    for (family, port) in process.tcp_port_reservations.values() {
10598        used_tcp_ports.entry(*family).or_default().insert(*port);
10599    }
10600
10601    let mut record_tcp_listener = |guest_addr: SocketAddr, host_port: u16| {
10602        let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10603        used_tcp_ports
10604            .entry(family)
10605            .or_default()
10606            .insert(guest_addr.port());
10607        // VM-local loopback connects should also resolve listeners bound to
10608        // unspecified guest addresses like 0.0.0.0/::.
10609        tcp_guest_to_host.insert((family, guest_addr.port()), host_port);
10610    };
10611
10612    for listener in process.tcp_listeners.values() {
10613        let local_addr = listener
10614            .kernel_socket_id
10615            .and_then(|socket_id| kernel.socket_get(socket_id))
10616            .and_then(|record| record.local_address().cloned())
10617            .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
10618            .unwrap_or_else(|| listener.guest_local_addr());
10619        record_tcp_listener(local_addr, local_addr.port());
10620    }
10621
10622    for (server_id, server) in &process.http_servers {
10623        let host_port = match server.listener.local_addr() {
10624            Ok(addr) => addr.port(),
10625            Err(_) => continue,
10626        };
10627        record_tcp_listener(server.guest_local_addr, host_port);
10628        let family = JavascriptSocketFamily::from_ip(server.guest_local_addr.ip());
10629        http_loopback_targets.insert(
10630            (family, server.guest_local_addr.port()),
10631            JavascriptHttpLoopbackTarget {
10632                process_id: process_id.to_owned(),
10633                server_id: *server_id,
10634            },
10635        );
10636    }
10637
10638    if let Ok(http2) = process.http2.shared.lock() {
10639        for server in http2.servers.values() {
10640            record_tcp_listener(server.guest_local_addr, server.actual_local_addr.port());
10641        }
10642    }
10643
10644    for socket in process.tcp_sockets.values() {
10645        let guest_addr = socket
10646            .kernel_socket_id
10647            .and_then(|socket_id| kernel.socket_get(socket_id))
10648            .and_then(|record| record.local_address().cloned())
10649            .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
10650            .unwrap_or(socket.guest_local_addr);
10651        let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10652        used_tcp_ports
10653            .entry(family)
10654            .or_default()
10655            .insert(guest_addr.port());
10656    }
10657
10658    for socket in process.udp_sockets.values() {
10659        let guest_addr = socket
10660            .kernel_socket_id
10661            .and_then(|socket_id| kernel.socket_get(socket_id))
10662            .and_then(|record| record.local_address().cloned())
10663            .and_then(|address| {
10664                resolve_udp_bind_addr(address.host(), address.port(), socket.family).ok()
10665            })
10666            .or_else(|| socket.local_addr());
10667        let Some(guest_addr) = guest_addr else {
10668            continue;
10669        };
10670        let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10671        used_udp_ports
10672            .entry(family)
10673            .or_default()
10674            .insert(guest_addr.port());
10675        if let Some(host_addr) = socket
10676            .socket
10677            .as_ref()
10678            .and_then(|socket| socket.local_addr().ok())
10679        {
10680            if is_loopback_ip(guest_addr.ip()) {
10681                udp_guest_to_host.insert((family, guest_addr.port()), host_addr.port());
10682                udp_host_to_guest.insert((family, host_addr.port()), guest_addr.port());
10683            }
10684        } else if socket.kernel_socket_id.is_some() && is_loopback_ip(guest_addr.ip()) {
10685            udp_guest_to_host.insert((family, guest_addr.port()), guest_addr.port());
10686            udp_host_to_guest.insert((family, guest_addr.port()), guest_addr.port());
10687        }
10688    }
10689
10690    for (child_process_id, child) in &process.child_processes {
10691        let child_id = format!("{process_id}/{child_process_id}");
10692        collect_javascript_socket_port_state(
10693            kernel,
10694            &child_id,
10695            child,
10696            tcp_guest_to_host,
10697            http_loopback_targets,
10698            udp_guest_to_host,
10699            udp_host_to_guest,
10700            used_tcp_ports,
10701            used_udp_ports,
10702        );
10703    }
10704}
10705
10706pub(crate) fn build_javascript_socket_path_context(
10707    vm: &VmState,
10708) -> Result<JavascriptSocketPathContext, SidecarError> {
10709    let mut loopback_exempt_ports = vm.create_loopback_exempt_ports.clone();
10710    loopback_exempt_ports.extend(vm.configuration.loopback_exempt_ports.iter().copied());
10711    let mut tcp_loopback_guest_to_host_ports = BTreeMap::new();
10712    let mut http_loopback_targets = BTreeMap::new();
10713    let mut udp_loopback_guest_to_host_ports = BTreeMap::new();
10714    let mut udp_loopback_host_to_guest_ports = BTreeMap::new();
10715    let mut used_tcp_guest_ports = BTreeMap::new();
10716    let mut used_udp_guest_ports = BTreeMap::new();
10717    for (process_id, process) in &vm.active_processes {
10718        collect_javascript_socket_port_state(
10719            &vm.kernel,
10720            process_id,
10721            process,
10722            &mut tcp_loopback_guest_to_host_ports,
10723            &mut http_loopback_targets,
10724            &mut udp_loopback_guest_to_host_ports,
10725            &mut udp_loopback_host_to_guest_ports,
10726            &mut used_tcp_guest_ports,
10727            &mut used_udp_guest_ports,
10728        );
10729    }
10730    Ok(JavascriptSocketPathContext {
10731        sandbox_root: vm.cwd.clone(),
10732        mounts: vm.configuration.mounts.clone(),
10733        listen_policy: vm.listen_policy,
10734        loopback_exempt_ports,
10735        tcp_loopback_guest_to_host_ports,
10736        http_loopback_targets,
10737        udp_loopback_guest_to_host_ports,
10738        udp_loopback_host_to_guest_ports,
10739        used_tcp_guest_ports,
10740        used_udp_guest_ports,
10741    })
10742}
10743
10744fn check_network_resource_limit(
10745    limit: Option<usize>,
10746    current: usize,
10747    additional: usize,
10748    label: &str,
10749) -> Result<(), SidecarError> {
10750    if let Some(limit) = limit {
10751        if current.saturating_add(additional) > limit {
10752            return Err(SidecarError::Execution(format!(
10753                "EAGAIN: maximum {label} count reached"
10754            )));
10755        }
10756    }
10757    Ok(())
10758}
10759
10760fn normalize_tcp_listen_host(
10761    host: Option<&str>,
10762) -> Result<(JavascriptSocketFamily, &'static str, &'static str), SidecarError> {
10763    match host.unwrap_or("127.0.0.1") {
10764        "127.0.0.1" | "localhost" => Ok((JavascriptSocketFamily::Ipv4, "127.0.0.1", "127.0.0.1")),
10765        "::1" => Ok((JavascriptSocketFamily::Ipv6, "::1", "::1")),
10766        "0.0.0.0" => Ok((JavascriptSocketFamily::Ipv4, "127.0.0.1", "0.0.0.0")),
10767        "::" => Ok((JavascriptSocketFamily::Ipv6, "::1", "::")),
10768        other => Err(SidecarError::Execution(format!(
10769            "EACCES: TCP listeners must bind to loopback or unspecified addresses, got {other}"
10770        ))),
10771    }
10772}
10773
10774fn normalize_udp_bind_host(
10775    host: Option<&str>,
10776    family: JavascriptUdpFamily,
10777) -> Result<(&'static str, &'static str, JavascriptSocketFamily), SidecarError> {
10778    match (family, host) {
10779        (JavascriptUdpFamily::Ipv4, None) | (JavascriptUdpFamily::Ipv4, Some("0.0.0.0")) => {
10780            Ok(("127.0.0.1", "0.0.0.0", JavascriptSocketFamily::Ipv4))
10781        }
10782        (JavascriptUdpFamily::Ipv4, Some("127.0.0.1"))
10783        | (JavascriptUdpFamily::Ipv4, Some("localhost")) => {
10784            Ok(("127.0.0.1", "127.0.0.1", JavascriptSocketFamily::Ipv4))
10785        }
10786        (JavascriptUdpFamily::Ipv6, None) | (JavascriptUdpFamily::Ipv6, Some("::")) => {
10787            Ok(("::1", "::", JavascriptSocketFamily::Ipv6))
10788        }
10789        (JavascriptUdpFamily::Ipv6, Some("::1"))
10790        | (JavascriptUdpFamily::Ipv6, Some("localhost")) => {
10791            Ok(("::1", "::1", JavascriptSocketFamily::Ipv6))
10792        }
10793        (JavascriptUdpFamily::Ipv4, Some(other)) => Err(SidecarError::Execution(format!(
10794            "EACCES: udp4 sockets must bind to 127.0.0.1 or 0.0.0.0, got {other}"
10795        ))),
10796        (JavascriptUdpFamily::Ipv6, Some(other)) => Err(SidecarError::Execution(format!(
10797            "EACCES: udp6 sockets must bind to ::1 or ::, got {other}"
10798        ))),
10799    }
10800}
10801
10802fn allocate_guest_listen_port(
10803    requested_port: u16,
10804    family: JavascriptSocketFamily,
10805    used_ports: &BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10806    policy: VmListenPolicy,
10807) -> Result<u16, SidecarError> {
10808    let is_allowed = |port: u16| {
10809        port >= policy.port_min
10810            && port <= policy.port_max
10811            && (policy.allow_privileged || port >= 1024)
10812    };
10813    let used = used_ports.get(&family);
10814
10815    if requested_port != 0 {
10816        if !is_allowed(requested_port) {
10817            let reason = if requested_port < 1024 && !policy.allow_privileged {
10818                format!(
10819                    "EACCES: privileged listen port {requested_port} requires {}=true",
10820                    VM_LISTEN_ALLOW_PRIVILEGED_METADATA_KEY
10821                )
10822            } else {
10823                format!(
10824                    "EACCES: listen port {requested_port} is outside the allowed range {}-{}",
10825                    policy.port_min, policy.port_max
10826                )
10827            };
10828            return Err(SidecarError::Execution(reason));
10829        }
10830        if used.is_some_and(|ports| ports.contains(&requested_port)) {
10831            return Err(sidecar_net_error(std::io::Error::from_raw_os_error(
10832                libc::EADDRINUSE,
10833            )));
10834        }
10835        return Ok(requested_port);
10836    }
10837
10838    let allocation_start = policy
10839        .port_min
10840        .max(if policy.allow_privileged { 1 } else { 1024 });
10841    for candidate in allocation_start..=policy.port_max {
10842        if used.is_some_and(|ports| ports.contains(&candidate)) {
10843            continue;
10844        }
10845        return Ok(candidate);
10846    }
10847
10848    Err(sidecar_net_error(std::io::Error::from_raw_os_error(
10849        libc::EADDRINUSE,
10850    )))
10851}
10852
10853fn socket_host_matches(requested: Option<&str>, actual: &str) -> bool {
10854    match requested {
10855        None => true,
10856        Some(requested) if requested == actual => true,
10857        Some(requested)
10858            if is_unspecified_socket_host(requested) && is_unspecified_socket_host(actual) =>
10859        {
10860            true
10861        }
10862        Some(requested) if is_unspecified_socket_host(requested) => is_loopback_socket_host(actual),
10863        Some(requested) if requested.eq_ignore_ascii_case("localhost") => {
10864            is_loopback_socket_host(actual)
10865        }
10866        _ => false,
10867    }
10868}
10869
10870fn parse_proc_net_entries(table_path: &str) -> Result<Vec<ProcNetEntry>, SidecarError> {
10871    let contents = match fs::read_to_string(table_path) {
10872        Ok(contents) => contents,
10873        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
10874        Err(error) => {
10875            return Err(SidecarError::Io(format!(
10876                "failed to inspect socket table {table_path}: {error}"
10877            )));
10878        }
10879    };
10880
10881    let mut entries = Vec::new();
10882    for line in contents.lines().skip(1) {
10883        let columns = line.split_whitespace().collect::<Vec<_>>();
10884        if columns.len() < 10 {
10885            continue;
10886        }
10887        let Some((host, port)) = parse_proc_ip_port(columns[1]) else {
10888            continue;
10889        };
10890        let Ok(inode) = columns[9].parse::<u64>() else {
10891            continue;
10892        };
10893        entries.push(ProcNetEntry {
10894            local_host: host,
10895            local_port: port,
10896            state: columns[3].to_owned(),
10897            inode,
10898        });
10899    }
10900
10901    Ok(entries)
10902}
10903
10904fn parse_proc_ip_port(value: &str) -> Option<(String, u16)> {
10905    let (raw_ip, raw_port) = value.split_once(':')?;
10906    let port = u16::from_str_radix(raw_port, 16).ok()?;
10907    let host = match raw_ip.len() {
10908        8 => {
10909            let raw = u32::from_str_radix(raw_ip, 16).ok()?;
10910            Ipv4Addr::from(raw.to_le_bytes()).to_string()
10911        }
10912        32 => {
10913            let mut bytes = [0_u8; 16];
10914            for (index, chunk) in raw_ip.as_bytes().chunks(8).enumerate() {
10915                let word = u32::from_str_radix(std::str::from_utf8(chunk).ok()?, 16).ok()?;
10916                bytes[index * 4..(index + 1) * 4].copy_from_slice(&word.to_le_bytes());
10917            }
10918            Ipv6Addr::from(bytes).to_string()
10919        }
10920        _ => return None,
10921    };
10922    Some((host, port))
10923}
10924
10925fn python_file_entrypoint(entrypoint: &str) -> Option<PathBuf> {
10926    let path = Path::new(entrypoint);
10927    (path.extension().and_then(|extension| extension.to_str()) == Some("py"))
10928        .then(|| path.to_path_buf())
10929}
10930
10931fn add_runtime_guest_path_mapping(
10932    env: &mut BTreeMap<String, String>,
10933    guest_path: &str,
10934    host_path: &Path,
10935) {
10936    let mut mappings = env
10937        .get("AGENTOS_GUEST_PATH_MAPPINGS")
10938        .and_then(|value| serde_json::from_str::<Vec<Value>>(value).ok())
10939        .unwrap_or_default();
10940    mappings.retain(|mapping| {
10941        mapping
10942            .get("guestPath")
10943            .and_then(Value::as_str)
10944            .map(|existing| normalize_path(existing) != normalize_path(guest_path))
10945            .unwrap_or(true)
10946    });
10947    mappings.push(json!({
10948        "guestPath": normalize_path(guest_path),
10949        "hostPath": host_path.display().to_string(),
10950    }));
10951    if let Ok(serialized) = serde_json::to_string(&mappings) {
10952        env.insert(String::from("AGENTOS_GUEST_PATH_MAPPINGS"), serialized);
10953    }
10954}
10955
10956fn add_runtime_host_access_path(
10957    env: &mut BTreeMap<String, String>,
10958    key: &str,
10959    host_path: &Path,
10960    expand: bool,
10961) {
10962    let existing = env
10963        .get(key)
10964        .and_then(|value| serde_json::from_str::<Vec<String>>(value).ok())
10965        .unwrap_or_default()
10966        .into_iter()
10967        .map(PathBuf::from)
10968        .collect::<Vec<_>>();
10969    let mut paths = existing;
10970    paths.push(host_path.to_path_buf());
10971    let normalized = if expand {
10972        expand_host_access_paths(&paths)
10973    } else {
10974        dedupe_host_paths(&paths)
10975    };
10976    let serialized = normalized
10977        .iter()
10978        .map(|path| path.to_string_lossy().into_owned())
10979        .collect::<Vec<_>>();
10980    if let Ok(serialized) = serde_json::to_string(&serialized) {
10981        env.insert(key.to_owned(), serialized);
10982    }
10983}
10984
10985// discover_command_guest_paths moved to crate::bootstrap
10986
10987fn is_path_like_specifier(specifier: &str) -> bool {
10988    specifier.starts_with('/')
10989        || specifier.starts_with("./")
10990        || specifier.starts_with("../")
10991        || specifier.starts_with("file:")
10992}
10993
10994fn execution_wasm_permission_tier(tier: WasmPermissionTier) -> ExecutionWasmPermissionTier {
10995    match tier {
10996        WasmPermissionTier::Full => ExecutionWasmPermissionTier::Full,
10997        WasmPermissionTier::ReadWrite => ExecutionWasmPermissionTier::ReadWrite,
10998        WasmPermissionTier::ReadOnly => ExecutionWasmPermissionTier::ReadOnly,
10999        WasmPermissionTier::Isolated => ExecutionWasmPermissionTier::Isolated,
11000    }
11001}
11002
11003fn resolve_wasm_permission_tier(
11004    vm: &VmState,
11005    command_name: Option<&str>,
11006    explicit_tier: Option<WasmPermissionTier>,
11007    entrypoint: &str,
11008) -> WasmPermissionTier {
11009    explicit_tier
11010        .or_else(|| command_name.and_then(|command| vm.command_permissions.get(command).copied()))
11011        .or_else(|| {
11012            Path::new(entrypoint)
11013                .file_name()
11014                .and_then(|name| name.to_str())
11015                .and_then(|command| vm.command_permissions.get(command).copied())
11016        })
11017        .unwrap_or(WasmPermissionTier::Full)
11018}
11019
11020fn tokenize_shell_free_command(command: &str) -> Vec<String> {
11021    command
11022        .split_whitespace()
11023        .filter(|segment| !segment.is_empty())
11024        .map(str::to_owned)
11025        .collect()
11026}
11027
11028fn is_posix_shell_builtin(command: &str) -> bool {
11029    matches!(
11030        command,
11031        "." | ":"
11032            | "break"
11033            | "cd"
11034            | "continue"
11035            | "eval"
11036            | "exec"
11037            | "exit"
11038            | "export"
11039            | "readonly"
11040            | "return"
11041            | "set"
11042            | "shift"
11043            | "times"
11044            | "trap"
11045            | "umask"
11046            | "unset"
11047    )
11048}
11049
11050/// Single-token checks for shell-mode commands whose first word forces a real
11051/// shell even when the command string has no shell metacharacters. This is not
11052/// a parser: env-assignment prefixes (`FOO=bar cmd`) and shell reserved words
11053/// have no meaning outside `sh`, so whitespace-tokenizing them would silently
11054/// run the wrong program.
11055fn shell_first_token_requires_shell(token: &str) -> bool {
11056    token.contains('=') || is_shell_reserved_word(token)
11057}
11058
11059fn is_shell_reserved_word(token: &str) -> bool {
11060    matches!(
11061        token,
11062        "if" | "then"
11063            | "elif"
11064            | "else"
11065            | "fi"
11066            | "for"
11067            | "in"
11068            | "do"
11069            | "done"
11070            | "while"
11071            | "until"
11072            | "case"
11073            | "esac"
11074            | "{"
11075            | "}"
11076            | "!"
11077    )
11078}
11079
11080fn command_requires_shell(command: &str) -> bool {
11081    command.chars().any(|ch| {
11082        matches!(
11083            ch,
11084            '|' | '&'
11085                | ';'
11086                | '<'
11087                | '>'
11088                | '('
11089                | ')'
11090                | '$'
11091                | '`'
11092                | '*'
11093                | '?'
11094                | '['
11095                | ']'
11096                | '{'
11097                | '}'
11098                | '~'
11099                | '\''
11100                | '"'
11101                | '\\'
11102                | '\n'
11103        )
11104    })
11105}
11106
11107fn host_mount_path_for_guest_path(vm: &VmState, guest_path: &str) -> Option<PathBuf> {
11108    let normalized = normalize_path(guest_path);
11109
11110    let mut mounts = vm
11111        .configuration
11112        .mounts
11113        .iter()
11114        .filter_map(|mount| {
11115            ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
11116                .then(|| {
11117                    mount_config_host_path(&mount.plugin.config)
11118                        .map(|host_path| (mount.guest_path.as_str(), host_path))
11119                })
11120                .flatten()
11121        })
11122        .collect::<Vec<_>>();
11123    mounts.sort_by_key(|mount| std::cmp::Reverse(mount.0.len()));
11124
11125    for (guest_root, host_root) in mounts {
11126        if normalized != guest_root && !normalized.starts_with(&format!("{guest_root}/")) {
11127            continue;
11128        }
11129
11130        let suffix = normalized
11131            .strip_prefix(guest_root)
11132            .unwrap_or_default()
11133            .trim_start_matches('/');
11134        let mut path = PathBuf::from(host_root);
11135        if !suffix.is_empty() {
11136            path.push(suffix);
11137        }
11138        return Some(path);
11139    }
11140
11141    None
11142}
11143
11144fn host_runtime_path_for_guest_path_with_env(
11145    vm: &VmState,
11146    runtime_env: &BTreeMap<String, String>,
11147    guest_path: &str,
11148    default_host_cwd: &Path,
11149) -> Option<PathBuf> {
11150    if let Some(path) = host_mount_path_for_guest_path(vm, guest_path) {
11151        return Some(path);
11152    }
11153    if let Some(path) = host_path_from_runtime_guest_mappings(runtime_env, guest_path) {
11154        return Some(path);
11155    }
11156
11157    let normalized = normalize_path(guest_path);
11158    let virtual_home = guest_virtual_home(vm);
11159
11160    if normalized == virtual_home || normalized.starts_with(&format!("{virtual_home}/")) {
11161        let suffix = normalized
11162            .strip_prefix(&virtual_home)
11163            .unwrap_or_default()
11164            .trim_start_matches('/');
11165        let mut host_path = default_host_cwd.to_path_buf();
11166        if !suffix.is_empty() {
11167            host_path.push(suffix);
11168        }
11169        return Some(host_path);
11170    }
11171
11172    None
11173}
11174
11175#[derive(Deserialize, Serialize)]
11176struct RuntimeGuestPathMapping {
11177    #[serde(rename = "guestPath")]
11178    guest_path: String,
11179    #[serde(rename = "hostPath")]
11180    host_path: String,
11181    #[serde(rename = "readOnly", default)]
11182    read_only: bool,
11183}
11184
11185pub(crate) fn host_path_from_runtime_guest_mappings(
11186    runtime_env: &BTreeMap<String, String>,
11187    guest_path: &str,
11188) -> Option<PathBuf> {
11189    let mappings = runtime_env
11190        .get("AGENTOS_GUEST_PATH_MAPPINGS")
11191        .and_then(|value| serde_json::from_str::<Vec<RuntimeGuestPathMapping>>(value).ok())?;
11192    let normalized = normalize_path(guest_path);
11193
11194    let mut sorted_mappings = mappings
11195        .into_iter()
11196        .filter_map(|mapping| {
11197            (!mapping.guest_path.is_empty() && !mapping.host_path.is_empty()).then_some((
11198                normalize_path(&mapping.guest_path),
11199                PathBuf::from(mapping.host_path),
11200            ))
11201        })
11202        .collect::<Vec<_>>();
11203    sorted_mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.0.len()));
11204
11205    for (guest_root, mut host_root) in sorted_mappings {
11206        if guest_root != "/"
11207            && normalized != guest_root
11208            && !normalized.starts_with(&format!("{guest_root}/"))
11209        {
11210            continue;
11211        }
11212        if guest_root == "/" && !normalized.starts_with('/') {
11213            continue;
11214        }
11215
11216        if host_root.is_relative() {
11217            host_root = std::env::current_dir().ok()?.join(host_root);
11218        }
11219
11220        let suffix = if guest_root == "/" {
11221            normalized.trim_start_matches('/')
11222        } else {
11223            normalized
11224                .strip_prefix(&guest_root)
11225                .unwrap_or_default()
11226                .trim_start_matches('/')
11227        };
11228        if !suffix.is_empty() {
11229            host_root.push(suffix);
11230        }
11231        return Some(host_root);
11232    }
11233
11234    None
11235}
11236
11237fn guest_runtime_path_for_host_path(
11238    runtime_env: &BTreeMap<String, String>,
11239    virtual_home: &str,
11240    cwd: &Path,
11241    host_path: &str,
11242) -> Option<String> {
11243    let resolved = if host_path.starts_with("file://") {
11244        PathBuf::from(host_path.trim_start_matches("file://"))
11245    } else if host_path.starts_with("file:") {
11246        PathBuf::from(host_path.trim_start_matches("file:"))
11247    } else {
11248        let candidate = PathBuf::from(host_path);
11249        if candidate.is_absolute() {
11250            candidate
11251        } else if host_path.starts_with("./") || host_path.starts_with("../") {
11252            cwd.join(candidate)
11253        } else {
11254            return None;
11255        }
11256    };
11257    let normalized = normalize_host_path(&resolved);
11258
11259    if let Some(path) = guest_path_from_runtime_host_mappings(runtime_env, &normalized) {
11260        return Some(path);
11261    }
11262
11263    let normalized_cwd = normalize_host_path(cwd);
11264    if !path_is_within_root(&normalized, &normalized_cwd) {
11265        return None;
11266    }
11267
11268    let virtual_home = if virtual_home.starts_with('/') {
11269        virtual_home.to_string()
11270    } else {
11271        String::from("/root")
11272    };
11273    let suffix = normalized
11274        .strip_prefix(&normalized_cwd)
11275        .ok()?
11276        .to_string_lossy()
11277        .replace('\\', "/")
11278        .trim_start_matches('/')
11279        .to_owned();
11280
11281    Some(if suffix.is_empty() {
11282        virtual_home
11283    } else {
11284        normalize_path(&format!("{virtual_home}/{suffix}"))
11285    })
11286}
11287
11288fn guest_path_from_runtime_host_mappings(
11289    runtime_env: &BTreeMap<String, String>,
11290    host_path: &Path,
11291) -> Option<String> {
11292    let mappings = runtime_env
11293        .get("AGENTOS_GUEST_PATH_MAPPINGS")
11294        .and_then(|value| serde_json::from_str::<Vec<RuntimeGuestPathMapping>>(value).ok())?;
11295    let normalized = normalize_host_path(host_path);
11296
11297    let mut sorted_mappings = mappings
11298        .into_iter()
11299        .filter_map(|mapping| {
11300            (!mapping.guest_path.is_empty() && !mapping.host_path.is_empty()).then_some((
11301                normalize_path(&mapping.guest_path),
11302                normalize_host_path(Path::new(&mapping.host_path)),
11303            ))
11304        })
11305        .collect::<Vec<_>>();
11306    sorted_mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.1.as_os_str().len()));
11307
11308    for (guest_root, host_root) in sorted_mappings {
11309        if !path_is_within_root(&normalized, &host_root) {
11310            continue;
11311        }
11312        let suffix = normalized
11313            .strip_prefix(&host_root)
11314            .ok()?
11315            .to_string_lossy()
11316            .replace('\\', "/")
11317            .trim_start_matches('/')
11318            .to_owned();
11319
11320        return Some(if suffix.is_empty() {
11321            guest_root
11322        } else if guest_root == "/" {
11323            normalize_path(&format!("/{suffix}"))
11324        } else {
11325            normalize_path(&format!("{guest_root}/{suffix}"))
11326        });
11327    }
11328
11329    None
11330}
11331
11332fn host_mount_path_for_guest_path_from_mounts(
11333    mounts: &[crate::protocol::MountDescriptor],
11334    guest_path: &str,
11335) -> Option<PathBuf> {
11336    let normalized = normalize_path(guest_path);
11337
11338    let mut host_mounts = mounts
11339        .iter()
11340        .filter_map(|mount| {
11341            ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
11342                .then(|| {
11343                    mount_config_host_path(&mount.plugin.config)
11344                        .map(|host_path| (mount.guest_path.as_str(), host_path))
11345                })
11346                .flatten()
11347        })
11348        .collect::<Vec<_>>();
11349    host_mounts.sort_by_key(|mount| std::cmp::Reverse(mount.0.len()));
11350
11351    for (guest_root, host_root) in host_mounts {
11352        if normalized != guest_root && !normalized.starts_with(&format!("{guest_root}/")) {
11353            continue;
11354        }
11355
11356        let suffix = normalized
11357            .strip_prefix(guest_root)
11358            .unwrap_or_default()
11359            .trim_start_matches('/');
11360        let mut path = PathBuf::from(host_root);
11361        if !suffix.is_empty() {
11362            path.push(suffix);
11363        }
11364        return Some(path);
11365    }
11366
11367    None
11368}
11369
11370#[cfg(test)]
11371mod host_mount_path_for_guest_path_from_mounts_tests {
11372    use super::host_mount_path_for_guest_path_from_mounts;
11373    use crate::protocol::{MountDescriptor, MountPluginDescriptor};
11374    use serde_json::json;
11375    use std::path::PathBuf;
11376
11377    #[test]
11378    fn resolves_module_access_mount_paths() {
11379        let mounts = vec![MountDescriptor {
11380            guest_path: String::from("/root/node_modules"),
11381            read_only: true,
11382            plugin: MountPluginDescriptor {
11383                id: String::from("module_access"),
11384                config: json!({
11385                    "hostPath": "/tmp/workspace/node_modules",
11386                })
11387                .to_string(),
11388            },
11389        }];
11390
11391        let resolved =
11392            host_mount_path_for_guest_path_from_mounts(&mounts, "/root/node_modules/pkg/index.js")
11393                .expect("module_access mount should resolve");
11394
11395        assert_eq!(
11396            resolved,
11397            PathBuf::from("/tmp/workspace/node_modules/pkg/index.js")
11398        );
11399    }
11400}
11401
11402fn resolve_guest_socket_host_path(
11403    context: &JavascriptSocketPathContext,
11404    guest_path: &str,
11405) -> PathBuf {
11406    if let Some(path) = host_mount_path_for_guest_path_from_mounts(&context.mounts, guest_path) {
11407        return path;
11408    }
11409
11410    let normalized = normalize_path(guest_path);
11411    let mut host_path = context.sandbox_root.clone();
11412    let suffix = normalized.trim_start_matches('/');
11413    if !suffix.is_empty() {
11414        host_path.push(suffix);
11415    }
11416    host_path
11417}
11418
11419fn ensure_kernel_parent_directories(
11420    kernel: &mut SidecarKernel,
11421    path: &str,
11422) -> Result<(), SidecarError> {
11423    let parent = dirname(path);
11424    if parent != "/" && !kernel.exists(&parent).map_err(kernel_error)? {
11425        kernel.mkdir(&parent, true).map_err(kernel_error)?;
11426    }
11427    Ok(())
11428}
11429
11430// JavascriptChildProcessSpawnOptions, JavascriptChildProcessSpawnRequest moved to crate::protocol
11431// ResolvedChildProcessExecution moved to crate::state
11432
11433pub(crate) fn sanitize_javascript_child_process_internal_bootstrap_env(
11434    env: &BTreeMap<String, String>,
11435) -> BTreeMap<String, String> {
11436    const ALLOWED_KEYS: &[&str] = &[
11437        "AGENTOS_ALLOWED_NODE_BUILTINS",
11438        "AGENTOS_GUEST_PATH_MAPPINGS",
11439        "AGENTOS_LOOPBACK_EXEMPT_PORTS",
11440        "AGENTOS_VIRTUAL_PROCESS_EXEC_PATH",
11441        "AGENTOS_VIRTUAL_PROCESS_UID",
11442        "AGENTOS_VIRTUAL_PROCESS_GID",
11443        "AGENTOS_VIRTUAL_PROCESS_VERSION",
11444    ];
11445
11446    env.iter()
11447        .filter(|(key, _)| {
11448            ALLOWED_KEYS.contains(&key.as_str()) || key.starts_with("AGENTOS_VIRTUAL_OS_")
11449        })
11450        .map(|(key, value)| (key.clone(), value.clone()))
11451        .collect()
11452}
11453
11454// Network request types moved to crate::protocol
11455
11456// VmDnsConfig, DnsResolutionSource moved to crate::state
11457
11458fn resolve_tcp_bind_addr(host: &str, port: u16) -> Result<SocketAddr, SidecarError> {
11459    (host, port)
11460        .to_socket_addrs()
11461        .map_err(sidecar_net_error)?
11462        .next()
11463        .ok_or_else(|| {
11464            SidecarError::Execution(format!("failed to resolve TCP bind address {host}:{port}"))
11465        })
11466}
11467
11468pub(crate) fn format_dns_resource(hostname: &str) -> String {
11469    format!("dns://{hostname}")
11470}
11471
11472pub(crate) fn format_tcp_resource(host: &str, port: u16) -> String {
11473    format!("tcp://{host}:{port}")
11474}
11475
11476fn is_loopback_ip(ip: IpAddr) -> bool {
11477    match ip {
11478        IpAddr::V4(ip) => ip.is_loopback(),
11479        IpAddr::V6(ip) => {
11480            ip.is_loopback()
11481                || ip
11482                    .to_ipv4_mapped()
11483                    .is_some_and(|mapped| mapped.is_loopback())
11484        }
11485    }
11486}
11487
11488fn loopback_cidr(ip: IpAddr) -> &'static str {
11489    match ip {
11490        IpAddr::V4(ip) if ip.is_loopback() => "127.0.0.0/8",
11491        IpAddr::V6(ip)
11492            if ip
11493                .to_ipv4_mapped()
11494                .is_some_and(|mapped| mapped.is_loopback()) =>
11495        {
11496            "127.0.0.0/8"
11497        }
11498        IpAddr::V6(_) => "::1/128",
11499        IpAddr::V4(_) => "127.0.0.0/8",
11500    }
11501}
11502
11503/// Returns the embedded IPv4 address of an IPv4-compatible IPv6 address
11504/// (`::a.b.c.d`): the first six 16-bit segments are zero and the final 32 bits
11505/// hold the IPv4 address. The all-zero (`::`) and loopback (`::1`) addresses are
11506/// deliberately excluded so they are handled by the unspecified/loopback paths
11507/// rather than treated as IPv4-compatible.
11508fn ipv4_compatible_embedded(ip: Ipv6Addr) -> Option<Ipv4Addr> {
11509    let segments = ip.segments();
11510    if segments[0..6].iter().any(|&s| s != 0) {
11511        return None;
11512    }
11513    let embedded = (u32::from(segments[6]) << 16) | u32::from(segments[7]);
11514    // Skip :: (0.0.0.0) and ::1 (0.0.0.1) — these are the IPv6 unspecified /
11515    // loopback addresses, not IPv4-compatible representations of an IPv4 host.
11516    if embedded == 0 || embedded == 1 {
11517        return None;
11518    }
11519    Some(Ipv4Addr::from(embedded))
11520}
11521
11522fn restricted_non_loopback_ip_range(ip: IpAddr) -> Option<(&'static str, &'static str)> {
11523    match ip {
11524        IpAddr::V4(ip) => {
11525            if ip.is_unspecified() {
11526                // 0.0.0.0 is unspecified; the host stack routes a connect() to
11527                // it back to 127.0.0.1, so it must not bypass the loopback gate.
11528                return Some(("0.0.0.0/32", "unspecified"));
11529            }
11530            let [first, second, ..] = ip.octets();
11531            match (first, second) {
11532                (10, _) => Some(("10.0.0.0/8", "private")),
11533                (100, 64..=127) => Some(("100.64.0.0/10", "carrier-grade-nat")),
11534                (172, 16..=31) => Some(("172.16.0.0/12", "private")),
11535                (192, 168) => Some(("192.168.0.0/16", "private")),
11536                (169, 254) => Some(("169.254.0.0/16", "link-local")),
11537                // 224.0.0.0/4 is the IPv4 multicast range and 240.0.0.0/4 is
11538                // reserved/future-use (255.255.255.255 broadcast falls in it).
11539                // Neither is a legitimate unicast egress target, so a guest
11540                // connect to them must be denied rather than attempted.
11541                (224..=239, _) => Some(("224.0.0.0/4", "multicast")),
11542                (240..=255, _) => Some(("240.0.0.0/4", "reserved")),
11543                _ => None,
11544            }
11545        }
11546        IpAddr::V6(ip) => {
11547            if let Some(mapped) = ip.to_ipv4_mapped() {
11548                return restricted_non_loopback_ip_range(IpAddr::V4(mapped));
11549            }
11550            // IPv4-compatible IPv6 (::a.b.c.d): the first six segments are zero
11551            // and the last two carry an embedded IPv4 address. `to_ipv4_mapped`
11552            // returns None for this form, so without canonicalizing it here a
11553            // guest could spell a restricted IPv4 target (e.g. cloud-metadata
11554            // ::169.254.169.254) and bypass the IPv4 classifier. `::`/`::1` are
11555            // excluded so they fall through to the unspecified/loopback paths.
11556            if let Some(compat) = ipv4_compatible_embedded(ip) {
11557                return restricted_non_loopback_ip_range(IpAddr::V4(compat));
11558            }
11559
11560            if ip.is_unspecified() {
11561                // :: is the IPv6 unspecified address; same routing hazard as
11562                // 0.0.0.0, so deny it rather than letting it reach the host.
11563                return Some(("::/128", "unspecified"));
11564            }
11565
11566            let segments = ip.segments();
11567            if (segments[0] & 0xfe00) == 0xfc00 {
11568                return Some(("fc00::/7", "unique-local"));
11569            }
11570            if (segments[0] & 0xffc0) == 0xfe80 {
11571                return Some(("fe80::/10", "link-local"));
11572            }
11573            None
11574        }
11575    }
11576}
11577
11578fn blocked_dns_resolution_error(
11579    resource: &str,
11580    ip: IpAddr,
11581    cidr: &str,
11582    label: &str,
11583) -> SidecarError {
11584    SidecarError::Execution(format!(
11585        "EACCES: blocked outbound network access to {resource}: {ip} is within restricted {label} range {cidr}"
11586    ))
11587}
11588
11589fn blocked_loopback_connect_error(resource: &str, ip: IpAddr, port: u16) -> SidecarError {
11590    SidecarError::Execution(format!(
11591        "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}",
11592        loopback_cidr(ip)
11593    ))
11594}
11595
11596fn filter_dns_safe_ip_addrs(
11597    addresses: Vec<IpAddr>,
11598    hostname: &str,
11599) -> Result<Vec<IpAddr>, SidecarError> {
11600    let resource = format_dns_resource(hostname);
11601    let mut allowed = Vec::new();
11602    let mut blocked = None;
11603
11604    for ip in addresses {
11605        if let Some((cidr, label)) = restricted_non_loopback_ip_range(ip) {
11606            blocked.get_or_insert((ip, cidr, label));
11607            continue;
11608        }
11609        allowed.push(ip);
11610    }
11611
11612    if allowed.is_empty() {
11613        let (ip, cidr, label) = blocked.expect("blocked DNS results should capture a reason");
11614        return Err(blocked_dns_resolution_error(&resource, ip, cidr, label));
11615    }
11616
11617    Ok(allowed)
11618}
11619
11620fn loopback_connect_allowed(context: &JavascriptSocketPathContext, port: u16) -> bool {
11621    context.loopback_port_allowed(port)
11622}
11623
11624fn filter_tcp_connect_ip_addrs(
11625    addresses: Vec<IpAddr>,
11626    host: &str,
11627    port: u16,
11628    context: &JavascriptSocketPathContext,
11629) -> Result<Vec<IpAddr>, SidecarError> {
11630    let resource = format_tcp_resource(host, port);
11631    let mut allowed = Vec::new();
11632    let mut blocked = None;
11633
11634    for ip in addresses {
11635        if let Some((cidr, label)) = restricted_non_loopback_ip_range(ip) {
11636            blocked.get_or_insert_with(|| blocked_dns_resolution_error(&resource, ip, cidr, label));
11637            continue;
11638        }
11639        if is_loopback_ip(ip) && !loopback_connect_allowed(context, port) {
11640            blocked.get_or_insert_with(|| blocked_loopback_connect_error(&resource, ip, port));
11641            continue;
11642        }
11643        allowed.push(ip);
11644    }
11645
11646    if allowed.is_empty() {
11647        return Err(blocked.expect("blocked TCP connect results should capture a reason"));
11648    }
11649
11650    Ok(allowed)
11651}
11652
11653fn resolve_tcp_connect_addr<B>(
11654    bridge: &SharedBridge<B>,
11655    kernel: &SidecarKernel,
11656    vm_id: &str,
11657    dns: &VmDnsConfig,
11658    host: &str,
11659    port: u16,
11660    context: &JavascriptSocketPathContext,
11661) -> Result<ResolvedTcpConnectAddr, SidecarError>
11662where
11663    B: NativeSidecarBridge + Send + 'static,
11664    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11665{
11666    let allowed = filter_tcp_connect_ip_addrs(
11667        resolve_dns_ip_addrs(
11668            bridge,
11669            kernel,
11670            vm_id,
11671            dns,
11672            host,
11673            DnsLookupPolicy::SkipPermissions,
11674        )?,
11675        host,
11676        port,
11677        context,
11678    )?;
11679    let ip = allowed
11680        .iter()
11681        .copied()
11682        .find(|candidate| {
11683            let family = JavascriptSocketFamily::from_ip(*candidate);
11684            context.translate_tcp_loopback_port(family, port).is_some()
11685        })
11686        // We do not implement Happy Eyeballs yet, so prefer IPv4 over a
11687        // verbatim IPv6-first DNS answer for general outbound TCP connects.
11688        .or_else(|| allowed.iter().copied().find(IpAddr::is_ipv4))
11689        .or_else(|| allowed.first().copied())
11690        .ok_or_else(|| {
11691            SidecarError::Execution(format!("failed to resolve TCP address {host}:{port}"))
11692        })?;
11693    let family = JavascriptSocketFamily::from_ip(ip);
11694    let translated_loopback_port = context.translate_tcp_loopback_port(family, port);
11695    let use_kernel_loopback = is_loopback_ip(ip) && translated_loopback_port == Some(port);
11696    let actual_port = if is_loopback_ip(ip) {
11697        translated_loopback_port.unwrap_or(port)
11698    } else {
11699        port
11700    };
11701    Ok(ResolvedTcpConnectAddr {
11702        actual_addr: SocketAddr::new(ip, actual_port),
11703        guest_remote_addr: SocketAddr::new(ip, port),
11704        use_kernel_loopback,
11705    })
11706}
11707
11708fn resolve_dns_ip_addrs<B>(
11709    bridge: &SharedBridge<B>,
11710    kernel: &SidecarKernel,
11711    vm_id: &str,
11712    dns: &VmDnsConfig,
11713    hostname: &str,
11714    policy: DnsLookupPolicy,
11715) -> Result<Vec<IpAddr>, SidecarError>
11716where
11717    B: NativeSidecarBridge + Send + 'static,
11718    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11719{
11720    let resolution = match kernel.resolve_dns(hostname, policy) {
11721        Ok(resolution) => resolution,
11722        Err(error) => {
11723            let sidecar_error = kernel_error(error.clone());
11724            if error.code() != "EACCES" {
11725                emit_dns_resolution_failure_event(bridge, vm_id, hostname, dns, &sidecar_error);
11726            }
11727            return Err(sidecar_error);
11728        }
11729    };
11730    emit_dns_resolution_event(
11731        bridge,
11732        vm_id,
11733        hostname,
11734        resolution.source(),
11735        resolution.addresses(),
11736        dns,
11737    );
11738    Ok(resolution.addresses().to_vec())
11739}
11740
11741fn resolve_dns_records<B>(
11742    bridge: &SharedBridge<B>,
11743    kernel: &SidecarKernel,
11744    vm_id: &str,
11745    dns: &VmDnsConfig,
11746    hostname: &str,
11747    record_type: RecordType,
11748    policy: DnsLookupPolicy,
11749) -> Result<DnsRecordResolution, SidecarError>
11750where
11751    B: NativeSidecarBridge + Send + 'static,
11752    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11753{
11754    let resolution = match kernel.resolve_dns_records(hostname, record_type, policy) {
11755        Ok(resolution) => resolution,
11756        Err(error) => {
11757            let sidecar_error = kernel_error(error.clone());
11758            if error.code() != "EACCES" {
11759                emit_dns_resolution_failure_event(bridge, vm_id, hostname, dns, &sidecar_error);
11760            }
11761            return Err(sidecar_error);
11762        }
11763    };
11764    emit_dns_record_resolution_event(bridge, vm_id, hostname, &resolution, dns);
11765    Ok(resolution)
11766}
11767
11768fn filter_dns_ip_addrs(
11769    addresses: Vec<IpAddr>,
11770    family: Option<u8>,
11771) -> Result<Vec<IpAddr>, SidecarError> {
11772    let filtered: Vec<_> = match family.unwrap_or(0) {
11773        0 => addresses,
11774        4 => addresses
11775            .into_iter()
11776            .filter(|ip| matches!(ip, IpAddr::V4(_)))
11777            .collect(),
11778        6 => addresses
11779            .into_iter()
11780            .filter(|ip| matches!(ip, IpAddr::V6(_)))
11781            .collect(),
11782        other => {
11783            return Err(SidecarError::InvalidState(format!(
11784                "unsupported dns family {other}"
11785            )));
11786        }
11787    };
11788
11789    if filtered.is_empty() {
11790        return Err(SidecarError::Execution(String::from(
11791            "failed to resolve DNS address for requested family",
11792        )));
11793    }
11794
11795    Ok(filtered)
11796}
11797
11798fn resolve_udp_bind_addr(
11799    host: &str,
11800    port: u16,
11801    family: JavascriptUdpFamily,
11802) -> Result<SocketAddr, SidecarError> {
11803    (host, port)
11804        .to_socket_addrs()
11805        .map_err(sidecar_net_error)?
11806        .find(|addr| family.matches_addr(addr))
11807        .ok_or_else(|| {
11808            SidecarError::Execution(format!(
11809                "failed to resolve {} UDP bind address {host}:{port}",
11810                family.socket_type()
11811            ))
11812        })
11813}
11814
11815fn resolve_udp_addr<B>(request: UdpRemoteAddrRequest<'_, B>) -> Result<SocketAddr, SidecarError>
11816where
11817    B: NativeSidecarBridge + Send + 'static,
11818    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11819{
11820    let UdpRemoteAddrRequest {
11821        bridge,
11822        kernel,
11823        vm_id,
11824        dns,
11825        host,
11826        port,
11827        family,
11828        context,
11829    } = request;
11830    resolve_dns_ip_addrs(
11831        bridge,
11832        kernel,
11833        vm_id,
11834        dns,
11835        host,
11836        DnsLookupPolicy::SkipPermissions,
11837    )?
11838    .into_iter()
11839    .map(|ip| {
11840        let family_key = JavascriptSocketFamily::from_ip(ip);
11841        let actual_port = if is_loopback_ip(ip) {
11842            context
11843                .translate_udp_loopback_port(family_key, port)
11844                .unwrap_or(port)
11845        } else {
11846            port
11847        };
11848        SocketAddr::new(ip, actual_port)
11849    })
11850    .find(|addr| family.matches_addr(addr))
11851    .ok_or_else(|| {
11852        SidecarError::Execution(format!(
11853            "failed to resolve {} UDP address {host}:{port}",
11854            family.socket_type()
11855        ))
11856    })
11857}
11858
11859fn socket_addr_family(addr: &SocketAddr) -> &'static str {
11860    match addr {
11861        SocketAddr::V4(_) => "IPv4",
11862        SocketAddr::V6(_) => "IPv6",
11863    }
11864}
11865
11866fn javascript_net_timeout_value() -> Value {
11867    Value::String(String::from(JAVASCRIPT_NET_TIMEOUT_SENTINEL))
11868}
11869
11870fn javascript_net_json_string(value: Value, label: &str) -> Result<Value, SidecarError> {
11871    serde_json::to_string(&value)
11872        .map(Value::String)
11873        .map_err(|error| {
11874            SidecarError::InvalidState(format!("failed to serialize {label} payload: {error}"))
11875        })
11876}
11877
11878fn javascript_net_read_value(
11879    event: Option<JavascriptTcpSocketEvent>,
11880) -> Result<Value, SidecarError> {
11881    match event {
11882        Some(JavascriptTcpSocketEvent::Data(chunk)) => Ok(Value::String(
11883            base64::engine::general_purpose::STANDARD.encode(chunk),
11884        )),
11885        Some(JavascriptTcpSocketEvent::End | JavascriptTcpSocketEvent::Close { .. }) => {
11886            Ok(Value::Null)
11887        }
11888        Some(JavascriptTcpSocketEvent::Error { code, message }) => {
11889            let detail = code.unwrap_or_else(|| String::from("socket read"));
11890            Err(SidecarError::Execution(format!("{detail}: {message}")))
11891        }
11892        None => Ok(javascript_net_timeout_value()),
11893    }
11894}
11895
11896fn io_error_code(error: &std::io::Error) -> Option<String> {
11897    match error.raw_os_error() {
11898        Some(libc::EADDRINUSE) => Some(String::from("EADDRINUSE")),
11899        Some(libc::EADDRNOTAVAIL) => Some(String::from("EADDRNOTAVAIL")),
11900        Some(libc::ECONNREFUSED) => Some(String::from("ECONNREFUSED")),
11901        Some(libc::ECONNRESET) => Some(String::from("ECONNRESET")),
11902        Some(libc::EINVAL) => Some(String::from("EINVAL")),
11903        Some(libc::EPIPE) => Some(String::from("EPIPE")),
11904        Some(libc::ETIMEDOUT) => Some(String::from("ETIMEDOUT")),
11905        Some(libc::EHOSTUNREACH) => Some(String::from("EHOSTUNREACH")),
11906        Some(libc::ENETUNREACH) => Some(String::from("ENETUNREACH")),
11907        _ => None,
11908    }
11909}
11910
11911fn sidecar_net_error(error: std::io::Error) -> SidecarError {
11912    let message = match io_error_code(&error) {
11913        Some(code) => format!("{code}: {error}"),
11914        None => error.to_string(),
11915    };
11916    SidecarError::Execution(message)
11917}
11918
11919fn tls_provider() -> Arc<rustls::crypto::CryptoProvider> {
11920    Arc::new(aws_lc_rs::default_provider())
11921}
11922
11923fn tls_local_certificates(
11924    options: &JavascriptTlsBridgeOptions,
11925) -> Result<Vec<Vec<u8>>, SidecarError> {
11926    let Some(certificates) = options.cert.as_ref() else {
11927        return Ok(Vec::new());
11928    };
11929    tls_material_entries(certificates)
11930}
11931
11932fn tls_material_entries(material: &JavascriptTlsMaterial) -> Result<Vec<Vec<u8>>, SidecarError> {
11933    match material {
11934        JavascriptTlsMaterial::Single(entry) => tls_data_value(entry).map(|value| vec![value]),
11935        JavascriptTlsMaterial::Many(entries) => entries.iter().map(tls_data_value).collect(),
11936    }
11937}
11938
11939fn tls_data_value(value: &JavascriptTlsDataValue) -> Result<Vec<u8>, SidecarError> {
11940    match value {
11941        JavascriptTlsDataValue::Buffer { data } => base64::engine::general_purpose::STANDARD
11942            .decode(data)
11943            .map_err(|error| {
11944                SidecarError::InvalidState(format!("TLS material contains invalid base64: {error}"))
11945            }),
11946        JavascriptTlsDataValue::String { data } => Ok(data.as_bytes().to_vec()),
11947    }
11948}
11949
11950fn tls_certificates_from_material(
11951    material: &JavascriptTlsMaterial,
11952) -> Result<Vec<CertificateDer<'static>>, SidecarError> {
11953    let mut certificates = Vec::new();
11954    for entry in tls_material_entries(material)? {
11955        let mut reader = std::io::BufReader::new(Cursor::new(entry.clone()));
11956        let parsed = rustls_pemfile::certs(&mut reader)
11957            .collect::<Result<Vec<_>, _>>()
11958            .map_err(sidecar_net_error)?;
11959        if parsed.is_empty() {
11960            certificates.push(CertificateDer::from(entry));
11961        } else {
11962            certificates.extend(parsed);
11963        }
11964    }
11965    if certificates.is_empty() {
11966        return Err(SidecarError::InvalidState(String::from(
11967            "TLS certificate material did not contain any certificates",
11968        )));
11969    }
11970    Ok(certificates)
11971}
11972
11973fn tls_private_key_from_material(
11974    material: &JavascriptTlsMaterial,
11975) -> Result<PrivateKeyDer<'static>, SidecarError> {
11976    for entry in tls_material_entries(material)? {
11977        let mut reader = std::io::BufReader::new(Cursor::new(entry));
11978        if let Some(key) = rustls_pemfile::private_key(&mut reader).map_err(sidecar_net_error)? {
11979            return Ok(key);
11980        }
11981    }
11982    Err(SidecarError::InvalidState(String::from(
11983        "TLS private key material did not contain a supported key",
11984    )))
11985}
11986
11987fn tls_root_store(options: &JavascriptTlsBridgeOptions) -> Result<RootCertStore, SidecarError> {
11988    let mut roots = RootCertStore::empty();
11989    if let Some(ca) = options.ca.as_ref() {
11990        for certificate in tls_certificates_from_material(ca)? {
11991            roots.add(certificate).map_err(|error| {
11992                SidecarError::InvalidState(format!("failed to add TLS CA certificate: {error}"))
11993            })?;
11994        }
11995        return Ok(roots);
11996    }
11997
11998    for certificate in rustls_native_certs::load_native_certs().certs {
11999        roots.add(certificate).map_err(|error| {
12000            SidecarError::InvalidState(format!(
12001                "failed to add native TLS certificate to root store: {error}"
12002            ))
12003        })?;
12004    }
12005    Ok(roots)
12006}
12007
12008fn build_client_tls_stream(
12009    stream: TcpStream,
12010    options: &JavascriptTlsBridgeOptions,
12011) -> Result<rustls::StreamOwned<ClientConnection, TcpStream>, SidecarError> {
12012    let config = build_client_tls_config(options)?;
12013    let server_name = options
12014        .servername
12015        .clone()
12016        .unwrap_or_else(|| String::from("localhost"));
12017    let server_name = ServerName::try_from(server_name)
12018        .map_err(|_| SidecarError::InvalidState(String::from("invalid TLS servername")))?;
12019    stream
12020        .set_read_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
12021        .map_err(sidecar_net_error)?;
12022    stream
12023        .set_write_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
12024        .map_err(sidecar_net_error)?;
12025    let mut tls_stream = rustls::StreamOwned::new(
12026        ClientConnection::new(Arc::new(config), server_name).map_err(|error| {
12027            SidecarError::Execution(format!("failed to start TLS client: {error}"))
12028        })?,
12029        stream,
12030    );
12031    while tls_stream.conn.is_handshaking() {
12032        tls_stream
12033            .conn
12034            .complete_io(&mut tls_stream.sock)
12035            .map_err(sidecar_net_error)?;
12036    }
12037    tls_stream
12038        .sock
12039        .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
12040        .map_err(sidecar_net_error)?;
12041    tls_stream
12042        .sock
12043        .set_write_timeout(None)
12044        .map_err(sidecar_net_error)?;
12045    Ok(tls_stream)
12046}
12047
12048fn build_client_loopback_tls_stream(
12049    transport: crate::state::LoopbackTlsEndpoint,
12050    options: &JavascriptTlsBridgeOptions,
12051) -> Result<rustls::StreamOwned<ClientConnection, crate::state::LoopbackTlsEndpoint>, SidecarError>
12052{
12053    let config = build_client_tls_config(options)?;
12054    let server_name = options
12055        .servername
12056        .clone()
12057        .unwrap_or_else(|| String::from("localhost"));
12058    let server_name = ServerName::try_from(server_name)
12059        .map_err(|_| SidecarError::InvalidState(String::from("invalid TLS servername")))?;
12060    let mut tls_stream = rustls::StreamOwned::new(
12061        ClientConnection::new(Arc::new(config), server_name).map_err(|error| {
12062            SidecarError::Execution(format!("failed to start TLS client: {error}"))
12063        })?,
12064        transport,
12065    );
12066    match tls_stream.conn.complete_io(&mut tls_stream.sock) {
12067        Ok(_) => {}
12068        Err(error)
12069            if matches!(
12070                error.kind(),
12071                std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
12072            ) => {}
12073        Err(error) => return Err(sidecar_net_error(error)),
12074    }
12075    Ok(tls_stream)
12076}
12077
12078fn build_client_tls_config(
12079    options: &JavascriptTlsBridgeOptions,
12080) -> Result<ClientConfig, SidecarError> {
12081    let provider = tls_provider();
12082    let builder = ClientConfig::builder_with_provider(provider.clone())
12083        .with_safe_default_protocol_versions()
12084        .map_err(|error| {
12085            SidecarError::InvalidState(format!("invalid TLS protocol config: {error}"))
12086        })?;
12087
12088    let mut config = if options.reject_unauthorized == Some(false) {
12089        let verifier = Arc::new(InsecureTlsVerifier {
12090            supported_schemes: provider
12091                .signature_verification_algorithms
12092                .supported_schemes(),
12093        });
12094        builder
12095            .dangerous()
12096            .with_custom_certificate_verifier(verifier)
12097            .with_no_client_auth()
12098    } else {
12099        builder
12100            .with_root_certificates(tls_root_store(options)?)
12101            .with_no_client_auth()
12102    };
12103
12104    if let Some(protocols) = options.alpn_protocols.as_ref() {
12105        config.alpn_protocols = protocols
12106            .iter()
12107            .map(|protocol| protocol.as_bytes().to_vec())
12108            .collect();
12109    }
12110    Ok(config)
12111}
12112
12113fn build_server_tls_stream(
12114    stream: TcpStream,
12115    options: &JavascriptTlsBridgeOptions,
12116) -> Result<rustls::StreamOwned<ServerConnection, TcpStream>, SidecarError> {
12117    let config = build_server_tls_config(options)?;
12118    stream
12119        .set_read_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
12120        .map_err(sidecar_net_error)?;
12121    stream
12122        .set_write_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
12123        .map_err(sidecar_net_error)?;
12124    let mut tls_stream = rustls::StreamOwned::new(
12125        ServerConnection::new(Arc::new(config)).map_err(|error| {
12126            SidecarError::Execution(format!("failed to start TLS server: {error}"))
12127        })?,
12128        stream,
12129    );
12130    while tls_stream.conn.is_handshaking() {
12131        tls_stream
12132            .conn
12133            .complete_io(&mut tls_stream.sock)
12134            .map_err(sidecar_net_error)?;
12135    }
12136    tls_stream
12137        .sock
12138        .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
12139        .map_err(sidecar_net_error)?;
12140    tls_stream
12141        .sock
12142        .set_write_timeout(None)
12143        .map_err(sidecar_net_error)?;
12144    Ok(tls_stream)
12145}
12146
12147fn build_server_loopback_tls_stream(
12148    transport: crate::state::LoopbackTlsEndpoint,
12149    options: &JavascriptTlsBridgeOptions,
12150) -> Result<rustls::StreamOwned<ServerConnection, crate::state::LoopbackTlsEndpoint>, SidecarError>
12151{
12152    let config = build_server_tls_config(options)?;
12153    Ok(rustls::StreamOwned::new(
12154        ServerConnection::new(Arc::new(config)).map_err(|error| {
12155            SidecarError::Execution(format!("failed to start TLS server: {error}"))
12156        })?,
12157        transport,
12158    ))
12159}
12160
12161fn build_server_tls_config(
12162    options: &JavascriptTlsBridgeOptions,
12163) -> Result<ServerConfig, SidecarError> {
12164    let certificates = tls_certificates_from_material(options.cert.as_ref().ok_or_else(|| {
12165        SidecarError::InvalidState(String::from("TLS server upgrade requires a certificate"))
12166    })?)?;
12167    let key = tls_private_key_from_material(options.key.as_ref().ok_or_else(|| {
12168        SidecarError::InvalidState(String::from("TLS server upgrade requires a private key"))
12169    })?)?;
12170
12171    let mut config = ServerConfig::builder_with_provider(tls_provider())
12172        .with_safe_default_protocol_versions()
12173        .map_err(|error| {
12174            SidecarError::InvalidState(format!("invalid TLS protocol config: {error}"))
12175        })?
12176        .with_no_client_auth()
12177        .with_single_cert(certificates, key)
12178        .map_err(|error| {
12179            SidecarError::InvalidState(format!("invalid TLS server config: {error}"))
12180        })?;
12181
12182    if let Some(protocols) = options.alpn_protocols.as_ref() {
12183        config.alpn_protocols = protocols
12184            .iter()
12185            .map(|protocol| protocol.as_bytes().to_vec())
12186            .collect();
12187    }
12188    Ok(config)
12189}
12190
12191fn tls_protocol_name(version: rustls::ProtocolVersion) -> String {
12192    match version {
12193        rustls::ProtocolVersion::TLSv1_2 => String::from("TLSv1.2"),
12194        rustls::ProtocolVersion::TLSv1_3 => String::from("TLSv1.3"),
12195        other => other
12196            .as_str()
12197            .map(str::to_owned)
12198            .unwrap_or_else(|| format!("{other:?}")),
12199    }
12200}
12201
12202fn tls_cipher_bridge_value(suite: rustls::SupportedCipherSuite) -> Value {
12203    tls_bridge_object(vec![
12204        (
12205            "name",
12206            suite
12207                .suite()
12208                .as_str()
12209                .map(|value| Value::String(value.to_owned()))
12210                .unwrap_or(Value::Null),
12211        ),
12212        (
12213            "standardName",
12214            suite
12215                .suite()
12216                .as_str()
12217                .map(|value| Value::String(value.to_owned()))
12218                .unwrap_or(Value::Null),
12219        ),
12220        (
12221            "version",
12222            Value::String(if suite.tls13().is_some() {
12223                String::from("TLSv1.3")
12224            } else {
12225                String::from("TLSv1.2")
12226            }),
12227        ),
12228    ])
12229}
12230
12231fn tls_certificate_bridge_value(certificate: &[u8], detailed: bool) -> Value {
12232    let mut fields = vec![("raw", tls_bridge_buffer_value(certificate))];
12233    if detailed {
12234        fields.push(("issuerCertificate", tls_bridge_undefined_value()));
12235    }
12236    tls_bridge_object(fields)
12237}
12238
12239fn tls_bridge_buffer_value(bytes: &[u8]) -> Value {
12240    json!({
12241        "type": "buffer",
12242        "data": base64::engine::general_purpose::STANDARD.encode(bytes),
12243    })
12244}
12245
12246fn tls_bridge_object(entries: Vec<(&str, Value)>) -> Value {
12247    let value = entries
12248        .into_iter()
12249        .map(|(key, value)| (key.to_owned(), value))
12250        .collect::<serde_json::Map<String, Value>>();
12251    json!({
12252        "type": "object",
12253        "id": 1,
12254        "value": value,
12255    })
12256}
12257
12258fn tls_bridge_undefined_value() -> Value {
12259    json!({
12260        "type": "undefined",
12261    })
12262}
12263
12264fn spawn_tcp_socket_reader(
12265    stream: TcpStream,
12266    sender: Sender<JavascriptTcpSocketEvent>,
12267    tls_mode: Arc<AtomicBool>,
12268    saw_local_shutdown: Arc<AtomicBool>,
12269    saw_remote_end: Arc<AtomicBool>,
12270    close_notified: Arc<AtomicBool>,
12271) {
12272    thread::spawn(move || {
12273        let mut stream = stream;
12274        let mut buffer = vec![0_u8; 64 * 1024];
12275        loop {
12276            if tls_mode.load(Ordering::SeqCst) {
12277                break;
12278            }
12279            match stream.read(&mut buffer) {
12280                Ok(0) => {
12281                    saw_remote_end.store(true, Ordering::SeqCst);
12282                    let _ = sender.send(JavascriptTcpSocketEvent::End);
12283                    if saw_local_shutdown.load(Ordering::SeqCst)
12284                        && !close_notified.swap(true, Ordering::SeqCst)
12285                    {
12286                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12287                    }
12288                    break;
12289                }
12290                Ok(bytes_read) => {
12291                    if sender
12292                        .send(JavascriptTcpSocketEvent::Data(
12293                            buffer[..bytes_read].to_vec(),
12294                        ))
12295                        .is_err()
12296                    {
12297                        break;
12298                    }
12299                }
12300                Err(error)
12301                    if matches!(
12302                        error.kind(),
12303                        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
12304                    ) =>
12305                {
12306                    continue;
12307                }
12308                Err(error) => {
12309                    let code = io_error_code(&error);
12310                    let _ = sender.send(JavascriptTcpSocketEvent::Error {
12311                        code,
12312                        message: error.to_string(),
12313                    });
12314                    if !close_notified.swap(true, Ordering::SeqCst) {
12315                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12316                    }
12317                    break;
12318                }
12319            }
12320        }
12321    });
12322}
12323
12324fn spawn_tls_socket_reader(
12325    tls_stream: Arc<Mutex<Option<ActiveTlsStream>>>,
12326    sender: Sender<JavascriptTcpSocketEvent>,
12327    saw_local_shutdown: Arc<AtomicBool>,
12328    saw_remote_end: Arc<AtomicBool>,
12329    close_notified: Arc<AtomicBool>,
12330) {
12331    thread::spawn(move || {
12332        let mut buffer = vec![0_u8; 64 * 1024];
12333        loop {
12334            let read_result = {
12335                let mut guard = match tls_stream.lock() {
12336                    Ok(guard) => guard,
12337                    Err(_) => return,
12338                };
12339                let Some(stream) = guard.as_mut() else {
12340                    return;
12341                };
12342                stream.read(&mut buffer)
12343            };
12344
12345            match read_result {
12346                Ok(0) => {
12347                    saw_remote_end.store(true, Ordering::SeqCst);
12348                    let _ = sender.send(JavascriptTcpSocketEvent::End);
12349                    if saw_local_shutdown.load(Ordering::SeqCst)
12350                        && !close_notified.swap(true, Ordering::SeqCst)
12351                    {
12352                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12353                    }
12354                    break;
12355                }
12356                Ok(bytes_read) => {
12357                    if sender
12358                        .send(JavascriptTcpSocketEvent::Data(
12359                            buffer[..bytes_read].to_vec(),
12360                        ))
12361                        .is_err()
12362                    {
12363                        break;
12364                    }
12365                }
12366                Err(error)
12367                    if matches!(
12368                        error.kind(),
12369                        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
12370                    ) =>
12371                {
12372                    // The TLS reader and writer share one rustls stream mutex. Yield after
12373                    // timed-out reads so request writes can acquire the lock promptly.
12374                    std::thread::sleep(Duration::from_millis(1));
12375                    continue;
12376                }
12377                Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => {
12378                    saw_remote_end.store(true, Ordering::SeqCst);
12379                    let _ = sender.send(JavascriptTcpSocketEvent::End);
12380                    if saw_local_shutdown.load(Ordering::SeqCst)
12381                        && !close_notified.swap(true, Ordering::SeqCst)
12382                    {
12383                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12384                    }
12385                    break;
12386                }
12387                Err(error) => {
12388                    let code = io_error_code(&error);
12389                    let _ = sender.send(JavascriptTcpSocketEvent::Error {
12390                        code,
12391                        message: error.to_string(),
12392                    });
12393                    if !close_notified.swap(true, Ordering::SeqCst) {
12394                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12395                    }
12396                    break;
12397                }
12398            }
12399        }
12400    });
12401}
12402
12403fn spawn_unix_socket_reader(
12404    stream: UnixStream,
12405    sender: Sender<JavascriptTcpSocketEvent>,
12406    saw_local_shutdown: Arc<AtomicBool>,
12407    saw_remote_end: Arc<AtomicBool>,
12408    close_notified: Arc<AtomicBool>,
12409) {
12410    thread::spawn(move || {
12411        let mut stream = stream;
12412        let mut buffer = vec![0_u8; 64 * 1024];
12413        loop {
12414            match stream.read(&mut buffer) {
12415                Ok(0) => {
12416                    saw_remote_end.store(true, Ordering::SeqCst);
12417                    let _ = sender.send(JavascriptTcpSocketEvent::End);
12418                    if saw_local_shutdown.load(Ordering::SeqCst)
12419                        && !close_notified.swap(true, Ordering::SeqCst)
12420                    {
12421                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12422                    }
12423                    break;
12424                }
12425                Ok(bytes_read) => {
12426                    if sender
12427                        .send(JavascriptTcpSocketEvent::Data(
12428                            buffer[..bytes_read].to_vec(),
12429                        ))
12430                        .is_err()
12431                    {
12432                        break;
12433                    }
12434                }
12435                Err(error) => {
12436                    let code = io_error_code(&error);
12437                    let _ = sender.send(JavascriptTcpSocketEvent::Error {
12438                        code,
12439                        message: error.to_string(),
12440                    });
12441                    if !close_notified.swap(true, Ordering::SeqCst) {
12442                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12443                    }
12444                    break;
12445                }
12446            }
12447        }
12448    });
12449}
12450
12451fn terminate_child_process_tree(kernel: &mut SidecarKernel, process: &mut ActiveProcess) {
12452    let sqlite_database_ids = process.sqlite_databases.keys().copied().collect::<Vec<_>>();
12453    for database_id in sqlite_database_ids {
12454        let _ = close_sqlite_database(kernel, process, database_id);
12455    }
12456    process.sqlite_statements.clear();
12457    process.http_servers.clear();
12458    process.pending_http_requests.clear();
12459    if let Ok(mut http2) = process.http2.shared.lock() {
12460        let sessions = http2.sessions.values().cloned().collect::<Vec<_>>();
12461        http2.server_events.clear();
12462        http2.session_events.clear();
12463        http2.streams.clear();
12464        http2.servers.clear();
12465        http2.sessions.clear();
12466        drop(http2);
12467        for session in sessions {
12468            let (respond_to, _rx) = mpsc::channel();
12469            let _ = session.command_tx.send(Http2SessionCommand::Close {
12470                abrupt: true,
12471                respond_to,
12472            });
12473        }
12474    }
12475
12476    let listener_ids = process.tcp_listeners.keys().cloned().collect::<Vec<_>>();
12477    for listener_id in listener_ids {
12478        if let Some(listener) = process.tcp_listeners.remove(&listener_id) {
12479            let _ = listener.close(kernel, process.kernel_pid);
12480        }
12481    }
12482
12483    let sockets = process.tcp_sockets.keys().cloned().collect::<Vec<_>>();
12484    for socket_id in sockets {
12485        if let Some(socket) = process.tcp_sockets.remove(&socket_id) {
12486            let _ = socket.close(kernel, process.kernel_pid);
12487        }
12488    }
12489
12490    let unix_listener_ids = process.unix_listeners.keys().cloned().collect::<Vec<_>>();
12491    for listener_id in unix_listener_ids {
12492        if let Some(listener) = process.unix_listeners.remove(&listener_id) {
12493            let _ = listener.close();
12494        }
12495    }
12496
12497    let unix_sockets = process.unix_sockets.keys().cloned().collect::<Vec<_>>();
12498    for socket_id in unix_sockets {
12499        if let Some(socket) = process.unix_sockets.remove(&socket_id) {
12500            let _ = socket.close();
12501        }
12502    }
12503
12504    let udp_socket_ids = process.udp_sockets.keys().cloned().collect::<Vec<_>>();
12505    for socket_id in udp_socket_ids {
12506        if let Some(mut socket) = process.udp_sockets.remove(&socket_id) {
12507            socket.close(kernel, process.kernel_pid);
12508        }
12509    }
12510
12511    let child_ids = process.child_processes.keys().cloned().collect::<Vec<_>>();
12512    for child_id in child_ids {
12513        let Some(mut child) = process.child_processes.remove(&child_id) else {
12514            continue;
12515        };
12516        terminate_child_process_tree(kernel, &mut child);
12517        let _ = kernel.kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, SIGTERM);
12518        let _ = signal_runtime_process(child.execution.child_pid(), SIGTERM);
12519        child.kernel_handle.finish(0);
12520        let _ = kernel.wait_and_reap(child.kernel_pid);
12521    }
12522}
12523
12524fn service_javascript_sqlite_sync_rpc(
12525    kernel: &mut SidecarKernel,
12526    process: &mut ActiveProcess,
12527    request: &JavascriptSyncRpcRequest,
12528) -> Result<Value, SidecarError> {
12529    match request.method.as_str() {
12530        "sqlite.constants" => Ok(json!({})),
12531        "sqlite.open" => sqlite_open_database(kernel, process, request),
12532        "sqlite.close" => {
12533            let database_id =
12534                javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.close database id")?;
12535            close_sqlite_database(kernel, process, database_id)?;
12536            Ok(Value::Null)
12537        }
12538        "sqlite.exec" => sqlite_exec_database(kernel, process, request),
12539        "sqlite.query" => sqlite_query_database(process, request),
12540        "sqlite.prepare" => sqlite_prepare_statement(process, request),
12541        "sqlite.location" => {
12542            let database_id =
12543                javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.location database id")?;
12544            let database = sqlite_database(process, database_id)?;
12545            Ok(database
12546                .vm_path
12547                .as_ref()
12548                .map(|path| Value::String(path.clone()))
12549                .unwrap_or(Value::Null))
12550        }
12551        "sqlite.checkpoint" => {
12552            let database_id =
12553                javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.checkpoint database id")?;
12554            let kernel_pid = process.kernel_pid;
12555            let database = sqlite_database_mut(process, database_id)?;
12556            sqlite_sync_database(kernel, kernel_pid, database)?;
12557            Ok(Value::Null)
12558        }
12559        "sqlite.statement.run" => sqlite_run_statement(kernel, process, request),
12560        "sqlite.statement.get" => sqlite_get_statement(process, request),
12561        "sqlite.statement.all" | "sqlite.statement.iterate" => {
12562            sqlite_all_statement(process, request)
12563        }
12564        "sqlite.statement.columns" => sqlite_statement_columns(process, request),
12565        "sqlite.statement.setReturnArrays" => {
12566            let statement_id = javascript_sync_rpc_arg_u64(
12567                &request.args,
12568                0,
12569                "sqlite.statement.setReturnArrays statement id",
12570            )?;
12571            let enabled = javascript_sync_rpc_arg_bool(
12572                &request.args,
12573                1,
12574                "sqlite.statement.setReturnArrays enabled",
12575            )?;
12576            sqlite_statement_mut(process, statement_id)?.return_arrays = enabled;
12577            Ok(Value::Null)
12578        }
12579        "sqlite.statement.setReadBigInts" => {
12580            let statement_id = javascript_sync_rpc_arg_u64(
12581                &request.args,
12582                0,
12583                "sqlite.statement.setReadBigInts statement id",
12584            )?;
12585            let enabled = javascript_sync_rpc_arg_bool(
12586                &request.args,
12587                1,
12588                "sqlite.statement.setReadBigInts enabled",
12589            )?;
12590            sqlite_statement_mut(process, statement_id)?.read_bigints = enabled;
12591            Ok(Value::Null)
12592        }
12593        "sqlite.statement.setAllowBareNamedParameters" => {
12594            let statement_id = javascript_sync_rpc_arg_u64(
12595                &request.args,
12596                0,
12597                "sqlite.statement.setAllowBareNamedParameters statement id",
12598            )?;
12599            let enabled = javascript_sync_rpc_arg_bool(
12600                &request.args,
12601                1,
12602                "sqlite.statement.setAllowBareNamedParameters enabled",
12603            )?;
12604            sqlite_statement_mut(process, statement_id)?.allow_bare_named_parameters = enabled;
12605            Ok(Value::Null)
12606        }
12607        "sqlite.statement.setAllowUnknownNamedParameters" => {
12608            let statement_id = javascript_sync_rpc_arg_u64(
12609                &request.args,
12610                0,
12611                "sqlite.statement.setAllowUnknownNamedParameters statement id",
12612            )?;
12613            let enabled = javascript_sync_rpc_arg_bool(
12614                &request.args,
12615                1,
12616                "sqlite.statement.setAllowUnknownNamedParameters enabled",
12617            )?;
12618            sqlite_statement_mut(process, statement_id)?.allow_unknown_named_parameters = enabled;
12619            Ok(Value::Null)
12620        }
12621        "sqlite.statement.finalize" => {
12622            let statement_id = javascript_sync_rpc_arg_u64(
12623                &request.args,
12624                0,
12625                "sqlite.statement.finalize statement id",
12626            )?;
12627            process
12628                .sqlite_statements
12629                .remove(&statement_id)
12630                .ok_or_else(|| {
12631                    SidecarError::InvalidState(format!(
12632                        "sqlite statement handle not found: {statement_id}"
12633                    ))
12634                })?;
12635            Ok(Value::Null)
12636        }
12637        other => Err(SidecarError::InvalidState(format!(
12638            "unsupported JavaScript sqlite sync RPC method {other}"
12639        ))),
12640    }
12641}
12642
12643fn sqlite_open_database(
12644    kernel: &mut SidecarKernel,
12645    process: &mut ActiveProcess,
12646    request: &JavascriptSyncRpcRequest,
12647) -> Result<Value, SidecarError> {
12648    ensure_per_process_state_handle_capacity(process.sqlite_databases.len(), "sqlite database")?;
12649    let path = request.args.first().and_then(Value::as_str);
12650    let vm_path = path.filter(|value| !value.is_empty() && *value != ":memory:");
12651    let options = request.args.get(1);
12652    let read_only = sqlite_option_bool(options, "readOnly").unwrap_or(false);
12653    let create = sqlite_option_bool(options, "create").unwrap_or(!read_only);
12654    let timeout_ms = sqlite_option_u64(options, "timeout");
12655
12656    process.next_sqlite_database_id += 1;
12657    let database_id = process.next_sqlite_database_id;
12658
12659    let host_path = if vm_path.is_some() {
12660        Some(
12661            std::env::temp_dir()
12662                .join(format!(
12663                    "secure-exec-sidecar-sqlite-{}-{database_id}",
12664                    process.kernel_pid
12665                ))
12666                .join("database.sqlite"),
12667        )
12668    } else {
12669        None
12670    };
12671
12672    if let Some(host_path) = host_path.as_ref() {
12673        if let Some(parent) = host_path.parent() {
12674            fs::create_dir_all(parent).map_err(|error| {
12675                SidecarError::Io(format!(
12676                    "failed to prepare sqlite temp directory {}: {error}",
12677                    parent.display()
12678                ))
12679            })?;
12680        }
12681    }
12682
12683    if let (Some(vm_path), Some(host_path)) = (vm_path, host_path.as_ref()) {
12684        if kernel
12685            .exists_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, vm_path)
12686            .map_err(kernel_error)?
12687        {
12688            let contents = kernel
12689                .read_file_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, vm_path)
12690                .map_err(kernel_error)?;
12691            fs::write(host_path, contents).map_err(|error| {
12692                SidecarError::Io(format!(
12693                    "failed to materialize sqlite database {}: {error}",
12694                    host_path.display()
12695                ))
12696            })?;
12697        } else if read_only && !create {
12698            return Err(SidecarError::InvalidState(format!(
12699                "sqlite database does not exist: {vm_path}"
12700            )));
12701        }
12702    }
12703
12704    let target = host_path
12705        .as_ref()
12706        .map(|path| path.to_string_lossy().into_owned())
12707        .unwrap_or_else(|| String::from(":memory:"));
12708    let mut flags = if read_only {
12709        SqliteOpenFlags::SQLITE_OPEN_READ_ONLY
12710    } else {
12711        SqliteOpenFlags::SQLITE_OPEN_READ_WRITE
12712    };
12713    if create && !read_only {
12714        flags |= SqliteOpenFlags::SQLITE_OPEN_CREATE;
12715    }
12716
12717    let connection = SqliteConnection::open_with_flags(&target, flags).map_err(|error| {
12718        SidecarError::InvalidState(format!(
12719            "sqlite database open failed for {}: {error}",
12720            vm_path.unwrap_or(":memory:")
12721        ))
12722    })?;
12723    if let Some(timeout_ms) = timeout_ms {
12724        connection
12725            .busy_timeout(Duration::from_millis(timeout_ms))
12726            .map_err(sqlite_error)?;
12727    }
12728    if host_path.is_some() && !read_only {
12729        let _ = connection.pragma_update(None, "journal_mode", "WAL");
12730    }
12731
12732    process.sqlite_databases.insert(
12733        database_id,
12734        ActiveSqliteDatabase {
12735            connection,
12736            host_path,
12737            vm_path: vm_path.map(String::from),
12738            dirty: false,
12739            transaction_depth: 0,
12740            read_only,
12741        },
12742    );
12743
12744    Ok(json!(database_id))
12745}
12746
12747fn sqlite_exec_database(
12748    kernel: &mut SidecarKernel,
12749    process: &mut ActiveProcess,
12750    request: &JavascriptSyncRpcRequest,
12751) -> Result<Value, SidecarError> {
12752    let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.exec database id")?;
12753    let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.exec sql")?;
12754    let kernel_pid = process.kernel_pid;
12755    let database = sqlite_database_mut(process, database_id)?;
12756    let before = database.connection.total_changes();
12757    database
12758        .connection
12759        .execute_batch(sql)
12760        .map_err(sqlite_error)?;
12761    mark_sqlite_mutation(database, sql);
12762    sqlite_sync_database(kernel, kernel_pid, database)?;
12763    Ok(json!(database
12764        .connection
12765        .total_changes()
12766        .saturating_sub(before)))
12767}
12768
12769fn sqlite_query_database(
12770    process: &mut ActiveProcess,
12771    request: &JavascriptSyncRpcRequest,
12772) -> Result<Value, SidecarError> {
12773    let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.query database id")?;
12774    let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.query sql")?;
12775    let params = request.args.get(2);
12776    let options = request.args.get(3);
12777    let return_arrays = sqlite_option_bool(options, "returnArrays").unwrap_or(false);
12778    let read_bigints = sqlite_option_bool(options, "readBigInts").unwrap_or(false);
12779    let database = sqlite_database_mut(process, database_id)?;
12780    sqlite_query_rows(
12781        &mut database.connection,
12782        sql,
12783        params,
12784        return_arrays,
12785        read_bigints,
12786        true,
12787        false,
12788    )
12789}
12790
12791fn sqlite_prepare_statement(
12792    process: &mut ActiveProcess,
12793    request: &JavascriptSyncRpcRequest,
12794) -> Result<Value, SidecarError> {
12795    ensure_per_process_state_handle_capacity(process.sqlite_statements.len(), "sqlite statement")?;
12796    let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.prepare database id")?;
12797    let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.prepare sql")?;
12798    let _ = sqlite_database(process, database_id)?;
12799    process.next_sqlite_statement_id += 1;
12800    let statement_id = process.next_sqlite_statement_id;
12801    process.sqlite_statements.insert(
12802        statement_id,
12803        ActiveSqliteStatement {
12804            database_id,
12805            sql: sql.to_owned(),
12806            return_arrays: false,
12807            read_bigints: false,
12808            allow_bare_named_parameters: false,
12809            allow_unknown_named_parameters: false,
12810        },
12811    );
12812    Ok(json!(statement_id))
12813}
12814
12815fn sqlite_run_statement(
12816    kernel: &mut SidecarKernel,
12817    process: &mut ActiveProcess,
12818    request: &JavascriptSyncRpcRequest,
12819) -> Result<Value, SidecarError> {
12820    let statement_id =
12821        javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.run statement id")?;
12822    let params = request.args.get(1);
12823    let statement_state = sqlite_statement(process, statement_id)?.clone();
12824    let kernel_pid = process.kernel_pid;
12825    let database = sqlite_database_mut(process, statement_state.database_id)?;
12826    let before = database.connection.total_changes();
12827    {
12828        let mut statement = database
12829            .connection
12830            .prepare(&statement_state.sql)
12831            .map_err(sqlite_error)?;
12832        bind_sqlite_parameters(
12833            &mut statement,
12834            params,
12835            statement_state.allow_bare_named_parameters,
12836            statement_state.allow_unknown_named_parameters,
12837        )?;
12838        statement.raw_execute().map_err(sqlite_error)?;
12839    }
12840    let changes = database.connection.total_changes().saturating_sub(before);
12841    let last_insert_rowid = database.connection.last_insert_rowid();
12842    mark_sqlite_mutation(database, &statement_state.sql);
12843    sqlite_sync_database(kernel, kernel_pid, database)?;
12844    let result = json!({
12845        "changes": changes,
12846        "lastInsertRowid": encode_sqlite_integer(last_insert_rowid, true),
12847    });
12848    Ok(result)
12849}
12850
12851fn sqlite_get_statement(
12852    process: &mut ActiveProcess,
12853    request: &JavascriptSyncRpcRequest,
12854) -> Result<Value, SidecarError> {
12855    let statement_id =
12856        javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.get statement id")?;
12857    let params = request.args.get(1);
12858    let statement_state = sqlite_statement(process, statement_id)?.clone();
12859    let database = sqlite_database_mut(process, statement_state.database_id)?;
12860    let rows = sqlite_query_rows(
12861        &mut database.connection,
12862        &statement_state.sql,
12863        params,
12864        statement_state.return_arrays,
12865        statement_state.read_bigints,
12866        statement_state.allow_bare_named_parameters,
12867        statement_state.allow_unknown_named_parameters,
12868    )?;
12869    Ok(rows
12870        .as_array()
12871        .and_then(|rows| rows.first().cloned())
12872        .unwrap_or(Value::Null))
12873}
12874
12875fn sqlite_all_statement(
12876    process: &mut ActiveProcess,
12877    request: &JavascriptSyncRpcRequest,
12878) -> Result<Value, SidecarError> {
12879    let statement_id =
12880        javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.all statement id")?;
12881    let params = request.args.get(1);
12882    let statement_state = sqlite_statement(process, statement_id)?.clone();
12883    let database = sqlite_database_mut(process, statement_state.database_id)?;
12884    sqlite_query_rows(
12885        &mut database.connection,
12886        &statement_state.sql,
12887        params,
12888        statement_state.return_arrays,
12889        statement_state.read_bigints,
12890        statement_state.allow_bare_named_parameters,
12891        statement_state.allow_unknown_named_parameters,
12892    )
12893}
12894
12895fn sqlite_statement_columns(
12896    process: &mut ActiveProcess,
12897    request: &JavascriptSyncRpcRequest,
12898) -> Result<Value, SidecarError> {
12899    let statement_id =
12900        javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.columns statement id")?;
12901    let statement_state = sqlite_statement(process, statement_id)?.clone();
12902    let database = sqlite_database_mut(process, statement_state.database_id)?;
12903    let statement = database
12904        .connection
12905        .prepare(&statement_state.sql)
12906        .map_err(sqlite_error)?;
12907    Ok(Value::Array(
12908        statement
12909            .column_names()
12910            .iter()
12911            .map(|name| json!({ "name": name }))
12912            .collect(),
12913    ))
12914}
12915
12916fn sqlite_query_rows(
12917    connection: &mut SqliteConnection,
12918    sql: &str,
12919    params: Option<&Value>,
12920    return_arrays: bool,
12921    read_bigints: bool,
12922    allow_bare_named_parameters: bool,
12923    allow_unknown_named_parameters: bool,
12924) -> Result<Value, SidecarError> {
12925    let mut statement = connection.prepare(sql).map_err(sqlite_error)?;
12926    let column_names = statement
12927        .column_names()
12928        .iter()
12929        .map(|name| (*name).to_owned())
12930        .collect::<Vec<_>>();
12931    let column_count = statement.column_count();
12932    bind_sqlite_parameters(
12933        &mut statement,
12934        params,
12935        allow_bare_named_parameters,
12936        allow_unknown_named_parameters,
12937    )?;
12938    let mut rows = statement.raw_query();
12939    let mut encoded_rows = Vec::new();
12940    while let Some(row) = rows.next().map_err(sqlite_error)? {
12941        encoded_rows.push(encode_sqlite_row(
12942            row,
12943            &column_names,
12944            column_count,
12945            return_arrays,
12946            read_bigints,
12947        )?);
12948    }
12949    Ok(Value::Array(encoded_rows))
12950}
12951
12952fn encode_sqlite_row(
12953    row: &rusqlite::Row<'_>,
12954    column_names: &[String],
12955    column_count: usize,
12956    return_arrays: bool,
12957    read_bigints: bool,
12958) -> Result<Value, SidecarError> {
12959    if return_arrays {
12960        let mut values = Vec::with_capacity(column_count);
12961        for index in 0..column_count {
12962            values.push(encode_sqlite_value_ref(
12963                row.get_ref(index).map_err(sqlite_error)?,
12964                read_bigints,
12965            )?);
12966        }
12967        return Ok(Value::Array(values));
12968    }
12969
12970    let mut object = Map::with_capacity(column_count);
12971    for (index, name) in column_names.iter().enumerate() {
12972        object.insert(
12973            name.clone(),
12974            encode_sqlite_value_ref(row.get_ref(index).map_err(sqlite_error)?, read_bigints)?,
12975        );
12976    }
12977    Ok(Value::Object(object))
12978}
12979
12980fn encode_sqlite_value_ref(
12981    value: SqliteValueRef<'_>,
12982    read_bigints: bool,
12983) -> Result<Value, SidecarError> {
12984    Ok(match value {
12985        SqliteValueRef::Null => Value::Null,
12986        SqliteValueRef::Integer(number) => encode_sqlite_integer(number, read_bigints),
12987        SqliteValueRef::Real(number) => json!(number),
12988        SqliteValueRef::Text(text) => Value::String(String::from_utf8_lossy(text).into_owned()),
12989        SqliteValueRef::Blob(bytes) => json!({
12990            "__agentosSqliteType": "uint8array",
12991            "value": base64::engine::general_purpose::STANDARD.encode(bytes),
12992        }),
12993    })
12994}
12995
12996fn encode_sqlite_integer(number: i64, read_bigints: bool) -> Value {
12997    if read_bigints || number.abs() > SQLITE_JS_SAFE_INTEGER_MAX {
12998        json!({
12999            "__agentosSqliteType": "bigint",
13000            "value": number.to_string(),
13001        })
13002    } else {
13003        json!(number)
13004    }
13005}
13006
13007fn bind_sqlite_parameters(
13008    statement: &mut SqliteStatement<'_>,
13009    params: Option<&Value>,
13010    allow_bare_named_parameters: bool,
13011    allow_unknown_named_parameters: bool,
13012) -> Result<(), SidecarError> {
13013    let Some(params) = params else {
13014        return Ok(());
13015    };
13016    match params {
13017        Value::Null => Ok(()),
13018        Value::Array(values) => {
13019            for (index, value) in values.iter().enumerate() {
13020                statement
13021                    .raw_bind_parameter(index + 1, decode_sqlite_parameter(value)?)
13022                    .map_err(sqlite_error)?;
13023            }
13024            Ok(())
13025        }
13026        Value::Object(map)
13027            if map
13028                .get("__agentosSqliteType")
13029                .and_then(Value::as_str)
13030                .is_none() =>
13031        {
13032            for (key, value) in map {
13033                let index =
13034                    resolve_sqlite_parameter_index(statement, key, allow_bare_named_parameters)?;
13035                let Some(index) = index else {
13036                    if allow_unknown_named_parameters {
13037                        continue;
13038                    }
13039                    return Err(SidecarError::InvalidState(format!(
13040                        "sqlite named parameter not found: {key}"
13041                    )));
13042                };
13043                statement
13044                    .raw_bind_parameter(index, decode_sqlite_parameter(value)?)
13045                    .map_err(sqlite_error)?;
13046            }
13047            Ok(())
13048        }
13049        other => statement
13050            .raw_bind_parameter(1, decode_sqlite_parameter(other)?)
13051            .map_err(sqlite_error),
13052    }
13053}
13054
13055fn resolve_sqlite_parameter_index(
13056    statement: &mut SqliteStatement<'_>,
13057    key: &str,
13058    allow_bare_named_parameters: bool,
13059) -> Result<Option<usize>, SidecarError> {
13060    let mut candidates = vec![key.to_owned()];
13061    if allow_bare_named_parameters
13062        && !key.starts_with(':')
13063        && !key.starts_with('@')
13064        && !key.starts_with('$')
13065    {
13066        candidates.push(format!(":{key}"));
13067        candidates.push(format!("@{key}"));
13068        candidates.push(format!("${key}"));
13069    }
13070    for candidate in candidates {
13071        if let Some(index) = statement
13072            .parameter_index(&candidate)
13073            .map_err(sqlite_error)?
13074        {
13075            return Ok(Some(index));
13076        }
13077    }
13078    Ok(None)
13079}
13080
13081fn decode_sqlite_parameter(value: &Value) -> Result<rusqlite::types::Value, SidecarError> {
13082    Ok(match value {
13083        Value::Null => rusqlite::types::Value::Null,
13084        Value::Bool(value) => rusqlite::types::Value::Integer(i64::from(*value)),
13085        Value::Number(value) => match (value.as_i64(), value.as_f64()) {
13086            (Some(integer), _) => rusqlite::types::Value::Integer(integer),
13087            (_, Some(real)) => rusqlite::types::Value::Real(real),
13088            _ => {
13089                return Err(SidecarError::InvalidState(String::from(
13090                    "sqlite parameter number is not representable",
13091                )));
13092            }
13093        },
13094        Value::String(value) => rusqlite::types::Value::Text(value.clone()),
13095        Value::Array(_) => {
13096            return Err(SidecarError::InvalidState(String::from(
13097                "sqlite parameters do not support nested arrays",
13098            )));
13099        }
13100        Value::Object(map) => match map.get("__agentosSqliteType").and_then(Value::as_str) {
13101            Some("bigint") => rusqlite::types::Value::Integer(
13102                map.get("value")
13103                    .and_then(Value::as_str)
13104                    .ok_or_else(|| {
13105                        SidecarError::InvalidState(String::from(
13106                            "sqlite bigint parameter missing string value",
13107                        ))
13108                    })?
13109                    .parse::<i64>()
13110                    .map_err(|error| {
13111                        SidecarError::InvalidState(format!(
13112                            "sqlite bigint parameter is not a signed 64-bit integer: {error}"
13113                        ))
13114                    })?,
13115            ),
13116            Some("uint8array") => rusqlite::types::Value::Blob(
13117                base64::engine::general_purpose::STANDARD
13118                    .decode(map.get("value").and_then(Value::as_str).ok_or_else(|| {
13119                        SidecarError::InvalidState(String::from(
13120                            "sqlite blob parameter missing base64 value",
13121                        ))
13122                    })?)
13123                    .map_err(|error| {
13124                        SidecarError::InvalidState(format!(
13125                            "sqlite blob parameter contains invalid base64: {error}"
13126                        ))
13127                    })?,
13128            ),
13129            Some(other) => {
13130                return Err(SidecarError::InvalidState(format!(
13131                    "unsupported sqlite tagged parameter type {other}"
13132                )));
13133            }
13134            None => {
13135                return Err(SidecarError::InvalidState(String::from(
13136                    "sqlite named parameter objects must be passed as the top-level params object",
13137                )));
13138            }
13139        },
13140    })
13141}
13142
13143fn close_sqlite_database(
13144    kernel: &mut SidecarKernel,
13145    process: &mut ActiveProcess,
13146    database_id: u64,
13147) -> Result<(), SidecarError> {
13148    let mut database = process
13149        .sqlite_databases
13150        .remove(&database_id)
13151        .ok_or_else(|| {
13152            SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
13153        })?;
13154    process
13155        .sqlite_statements
13156        .retain(|_, statement| statement.database_id != database_id);
13157    sqlite_sync_database(kernel, process.kernel_pid, &mut database)?;
13158    let host_path = database.host_path.clone();
13159    drop(database);
13160    cleanup_sqlite_host_artifacts(host_path.as_deref())?;
13161    Ok(())
13162}
13163
13164fn ensure_per_process_state_handle_capacity(len: usize, label: &str) -> Result<(), SidecarError> {
13165    if len >= MAX_PER_PROCESS_STATE_HANDLES {
13166        return Err(SidecarError::InvalidState(format!(
13167            "{label} handle limit exceeded: limit is {MAX_PER_PROCESS_STATE_HANDLES}"
13168        )));
13169    }
13170    Ok(())
13171}
13172
13173fn sqlite_sync_database(
13174    kernel: &mut SidecarKernel,
13175    kernel_pid: u32,
13176    database: &mut ActiveSqliteDatabase,
13177) -> Result<(), SidecarError> {
13178    if !database.dirty
13179        || database.transaction_depth > 0
13180        || database.read_only
13181        || database.host_path.is_none()
13182        || database.vm_path.is_none()
13183    {
13184        return Ok(());
13185    }
13186
13187    let _ = database
13188        .connection
13189        .execute_batch("PRAGMA wal_checkpoint(TRUNCATE)");
13190    let host_path = database.host_path.as_ref().expect("sqlite host path");
13191    if !host_path.exists() {
13192        return Ok(());
13193    }
13194    ensure_vm_parent_dir(
13195        kernel,
13196        kernel_pid,
13197        database.vm_path.as_deref().expect("sqlite vm path"),
13198    )?;
13199    let contents = fs::read(host_path).map_err(|error| {
13200        SidecarError::Io(format!(
13201            "failed to read sqlite temp database {}: {error}",
13202            host_path.display()
13203        ))
13204    })?;
13205    kernel
13206        .write_file_for_process(
13207            EXECUTION_DRIVER_NAME,
13208            kernel_pid,
13209            database.vm_path.as_deref().expect("sqlite vm path"),
13210            contents,
13211            None,
13212        )
13213        .map_err(kernel_error)?;
13214    database.dirty = false;
13215    Ok(())
13216}
13217
13218fn cleanup_sqlite_host_artifacts(host_path: Option<&Path>) -> Result<(), SidecarError> {
13219    let Some(host_path) = host_path else {
13220        return Ok(());
13221    };
13222    let parent = host_path.parent().map(PathBuf::from);
13223    for suffix in ["", "-wal", "-shm"] {
13224        let path = PathBuf::from(format!("{}{}", host_path.display(), suffix));
13225        if path.exists() {
13226            fs::remove_file(&path).map_err(|error| {
13227                SidecarError::Io(format!(
13228                    "failed to remove sqlite temp artifact {}: {error}",
13229                    path.display()
13230                ))
13231            })?;
13232        }
13233    }
13234    if let Some(parent) = parent {
13235        let _ = fs::remove_dir_all(parent);
13236    }
13237    Ok(())
13238}
13239
13240fn ensure_vm_parent_dir(
13241    kernel: &mut SidecarKernel,
13242    kernel_pid: u32,
13243    path: &str,
13244) -> Result<(), SidecarError> {
13245    let parent = dirname(path);
13246    if parent == "/" || parent == "." {
13247        return Ok(());
13248    }
13249    let mut current = String::new();
13250    for segment in parent.split('/').filter(|segment| !segment.is_empty()) {
13251        current.push('/');
13252        current.push_str(segment);
13253        if !kernel
13254            .exists_for_process(EXECUTION_DRIVER_NAME, kernel_pid, &current)
13255            .map_err(kernel_error)?
13256        {
13257            kernel
13258                .mkdir_for_process(EXECUTION_DRIVER_NAME, kernel_pid, &current, false, None)
13259                .map_err(kernel_error)?;
13260        }
13261    }
13262    Ok(())
13263}
13264
13265fn sqlite_database(
13266    process: &ActiveProcess,
13267    database_id: u64,
13268) -> Result<&ActiveSqliteDatabase, SidecarError> {
13269    process.sqlite_databases.get(&database_id).ok_or_else(|| {
13270        SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
13271    })
13272}
13273
13274fn sqlite_database_mut(
13275    process: &mut ActiveProcess,
13276    database_id: u64,
13277) -> Result<&mut ActiveSqliteDatabase, SidecarError> {
13278    process
13279        .sqlite_databases
13280        .get_mut(&database_id)
13281        .ok_or_else(|| {
13282            SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
13283        })
13284}
13285
13286fn sqlite_statement(
13287    process: &ActiveProcess,
13288    statement_id: u64,
13289) -> Result<&ActiveSqliteStatement, SidecarError> {
13290    process.sqlite_statements.get(&statement_id).ok_or_else(|| {
13291        SidecarError::InvalidState(format!("sqlite statement handle not found: {statement_id}"))
13292    })
13293}
13294
13295fn sqlite_statement_mut(
13296    process: &mut ActiveProcess,
13297    statement_id: u64,
13298) -> Result<&mut ActiveSqliteStatement, SidecarError> {
13299    process
13300        .sqlite_statements
13301        .get_mut(&statement_id)
13302        .ok_or_else(|| {
13303            SidecarError::InvalidState(format!("sqlite statement handle not found: {statement_id}"))
13304        })
13305}
13306
13307fn mark_sqlite_mutation(database: &mut ActiveSqliteDatabase, sql: &str) {
13308    let normalized = sql.trim_start().to_ascii_lowercase();
13309    if normalized.starts_with("begin") || normalized.starts_with("savepoint") {
13310        database.dirty = true;
13311        database.transaction_depth += 1;
13312        return;
13313    }
13314    if normalized.starts_with("commit") || normalized.starts_with("release savepoint") {
13315        database.dirty = true;
13316        database.transaction_depth = database.transaction_depth.saturating_sub(1);
13317        return;
13318    }
13319    if normalized.starts_with("rollback") && !normalized.starts_with("rollback to") {
13320        database.dirty = true;
13321        database.transaction_depth = database.transaction_depth.saturating_sub(1);
13322        return;
13323    }
13324    if normalized.starts_with("insert")
13325        || normalized.starts_with("update")
13326        || normalized.starts_with("delete")
13327        || normalized.starts_with("replace")
13328        || normalized.starts_with("create")
13329        || normalized.starts_with("alter")
13330        || normalized.starts_with("drop")
13331        || normalized.starts_with("vacuum")
13332        || normalized.starts_with("reindex")
13333        || normalized.starts_with("analyze")
13334        || normalized.starts_with("attach")
13335        || normalized.starts_with("detach")
13336        || normalized.starts_with("pragma")
13337    {
13338        database.dirty = true;
13339    }
13340}
13341
13342fn sqlite_option_bool(options: Option<&Value>, key: &str) -> Option<bool> {
13343    options
13344        .and_then(|value| value.get(key))
13345        .and_then(Value::as_bool)
13346}
13347
13348fn sqlite_option_u64(options: Option<&Value>, key: &str) -> Option<u64> {
13349    options
13350        .and_then(|value| value.get(key))
13351        .and_then(Value::as_u64)
13352}
13353
13354fn sqlite_error(error: rusqlite::Error) -> SidecarError {
13355    SidecarError::InvalidState(format!("sqlite error: {error}"))
13356}
13357
13358pub(crate) fn javascript_sync_rpc_arg_str<'a>(
13359    args: &'a [Value],
13360    index: usize,
13361    label: &str,
13362) -> Result<&'a str, SidecarError> {
13363    args.get(index)
13364        .and_then(Value::as_str)
13365        .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a string argument")))
13366}
13367
13368pub(crate) fn javascript_sync_rpc_arg_bool(
13369    args: &[Value],
13370    index: usize,
13371    label: &str,
13372) -> Result<bool, SidecarError> {
13373    args.get(index)
13374        .and_then(Value::as_bool)
13375        .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a boolean argument")))
13376}
13377
13378pub(crate) fn javascript_sync_rpc_encoding(args: &[Value]) -> Option<String> {
13379    args.get(1).and_then(|value| {
13380        value.as_str().map(str::to_owned).or_else(|| {
13381            value
13382                .get("encoding")
13383                .and_then(Value::as_str)
13384                .map(str::to_owned)
13385        })
13386    })
13387}
13388
13389pub(crate) fn javascript_sync_rpc_option_bool(
13390    args: &[Value],
13391    index: usize,
13392    key: &str,
13393) -> Option<bool> {
13394    let value = args.get(index)?;
13395    if key == "recursive" {
13396        if let Some(boolean) = value.as_bool() {
13397            return Some(boolean);
13398        }
13399    }
13400    value.get(key).and_then(Value::as_bool)
13401}
13402
13403pub(crate) fn javascript_sync_rpc_option_u32(
13404    args: &[Value],
13405    index: usize,
13406    key: &str,
13407) -> Result<Option<u32>, SidecarError> {
13408    let Some(value) = args.get(index).and_then(|value| {
13409        if value.is_object() {
13410            value.get(key)
13411        } else if key == "mode" && value.is_number() {
13412            Some(value)
13413        } else {
13414            None
13415        }
13416    }) else {
13417        return Ok(None);
13418    };
13419    if value.is_null() {
13420        return Ok(None);
13421    }
13422
13423    let numeric = value
13424        .as_u64()
13425        .or_else(|| {
13426            value
13427                .as_f64()
13428                .filter(|number| number.is_finite() && *number >= 0.0)
13429                .map(|number| number as u64)
13430        })
13431        .ok_or_else(|| SidecarError::InvalidState(format!("{key} must be numeric")))?;
13432
13433    u32::try_from(numeric)
13434        .map(Some)
13435        .map_err(|_| SidecarError::InvalidState(format!("{key} must fit within u32")))
13436}
13437
13438pub(crate) fn javascript_sync_rpc_arg_u32(
13439    args: &[Value],
13440    index: usize,
13441    label: &str,
13442) -> Result<u32, SidecarError> {
13443    let value = javascript_sync_rpc_arg_u64(args, index, label)?;
13444    u32::try_from(value)
13445        .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")))
13446}
13447
13448pub(crate) fn javascript_sync_rpc_arg_i32(
13449    args: &[Value],
13450    index: usize,
13451    label: &str,
13452) -> Result<i32, SidecarError> {
13453    let Some(value) = args.get(index) else {
13454        return Err(SidecarError::InvalidState(format!("{label} is required")));
13455    };
13456
13457    let numeric = value
13458        .as_i64()
13459        .or_else(|| {
13460            value
13461                .as_f64()
13462                .filter(|number| number.is_finite())
13463                .map(|number| number as i64)
13464        })
13465        .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a numeric argument")))?;
13466
13467    i32::try_from(numeric)
13468        .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within i32")))
13469}
13470
13471pub(crate) fn javascript_sync_rpc_arg_u32_optional(
13472    args: &[Value],
13473    index: usize,
13474    label: &str,
13475) -> Result<Option<u32>, SidecarError> {
13476    javascript_sync_rpc_arg_u64_optional(args, index, label)?
13477        .map(|value| {
13478            u32::try_from(value)
13479                .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")))
13480        })
13481        .transpose()
13482}
13483
13484pub(crate) fn javascript_sync_rpc_arg_u64(
13485    args: &[Value],
13486    index: usize,
13487    label: &str,
13488) -> Result<u64, SidecarError> {
13489    let Some(value) = args.get(index) else {
13490        return Err(SidecarError::InvalidState(format!("{label} is required")));
13491    };
13492
13493    value
13494        .as_u64()
13495        .or_else(|| {
13496            value
13497                .as_f64()
13498                .filter(|number| number.is_finite() && *number >= 0.0)
13499                .map(|number| number as u64)
13500        })
13501        .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a numeric argument")))
13502}
13503
13504pub(crate) fn javascript_sync_rpc_arg_u64_optional(
13505    args: &[Value],
13506    index: usize,
13507    label: &str,
13508) -> Result<Option<u64>, SidecarError> {
13509    let Some(value) = args.get(index) else {
13510        return Ok(None);
13511    };
13512    if value.is_null() {
13513        return Ok(None);
13514    }
13515    javascript_sync_rpc_arg_u64(args, index, label).map(Some)
13516}
13517
13518pub(crate) fn javascript_sync_rpc_bytes_arg(
13519    args: &[Value],
13520    index: usize,
13521    label: &str,
13522) -> Result<Vec<u8>, SidecarError> {
13523    let Some(value) = args.get(index) else {
13524        return Err(SidecarError::InvalidState(format!("{label} is required")));
13525    };
13526
13527    if let Some(text) = value.as_str() {
13528        return Ok(text.as_bytes().to_vec());
13529    }
13530
13531    let Some(base64_value) = value
13532        .get("__agentOSType")
13533        .and_then(Value::as_str)
13534        .filter(|kind| *kind == "bytes")
13535        .and_then(|_| value.get("base64"))
13536        .and_then(Value::as_str)
13537    else {
13538        return Err(SidecarError::InvalidState(format!(
13539            "{label} must be a string or encoded bytes payload"
13540        )));
13541    };
13542
13543    base64::engine::general_purpose::STANDARD
13544        .decode(base64_value)
13545        .map_err(|error| {
13546            SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
13547        })
13548}
13549
13550pub(crate) fn javascript_sync_rpc_bytes_value(bytes: &[u8]) -> Value {
13551    json!({
13552        "__agentOSType": "bytes",
13553        "base64": base64::engine::general_purpose::STANDARD.encode(bytes),
13554    })
13555}
13556
13557#[derive(Debug, Deserialize)]
13558struct KernelPollFdRequest {
13559    fd: u32,
13560    events: u16,
13561}
13562
13563#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
13564struct KernelPollFdResponse {
13565    fd: u32,
13566    events: u16,
13567    revents: u16,
13568}
13569
13570fn javascript_sync_rpc_base64_arg(
13571    args: &[Value],
13572    index: usize,
13573    label: &str,
13574) -> Result<Vec<u8>, SidecarError> {
13575    let value = javascript_sync_rpc_arg_str(args, index, label)?;
13576    base64::engine::general_purpose::STANDARD
13577        .decode(value)
13578        .map_err(|error| {
13579            SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
13580        })
13581}
13582
13583// ── Sync-RPC round-trip counting (opt-in via AGENTOS_SYNC_RPC_TRACE=1) ──
13584// Each guest fs/module/net sync RPC funnels through service_javascript_sync_rpc,
13585// so this is the one place to measure the kernel-VFS "syscall storm" that makes
13586// metadata-heavy phases (resourceLoader.reload, createAgentSession) 40-90x slower
13587// in the VM than on bare node. Emits a perf log line every 200 calls with the
13588// running per-method breakdown.
13589static SYNC_RPC_STATS: std::sync::OnceLock<
13590    std::sync::Mutex<std::collections::BTreeMap<String, u64>>,
13591> = std::sync::OnceLock::new();
13592
13593fn sync_rpc_trace_enabled() -> bool {
13594    std::env::var("AGENTOS_SYNC_RPC_TRACE").as_deref() == Ok("1")
13595}
13596
13597fn record_sync_rpc(method: &str) {
13598    let stats =
13599        SYNC_RPC_STATS.get_or_init(|| std::sync::Mutex::new(std::collections::BTreeMap::new()));
13600    let Ok(mut map) = stats.lock() else {
13601        return;
13602    };
13603    *map.entry(method.to_string()).or_insert(0) += 1;
13604    let total: u64 = map.values().sum();
13605    if total == 1 || total.is_multiple_of(50) {
13606        let mut top: Vec<(&String, &u64)> = map.iter().collect();
13607        top.sort_by(|a, b| b.1.cmp(a.1));
13608        let breakdown = top
13609            .iter()
13610            .take(8)
13611            .map(|(m, c)| format!("{m}={c}"))
13612            .collect::<Vec<_>>()
13613            .join(" ");
13614        tracing::info!(target: "secure_exec_sidecar::perf", total, %breakdown, "sync_rpc count");
13615    }
13616}
13617
13618pub(crate) fn service_javascript_sync_rpc<B>(
13619    request: JavascriptSyncRpcServiceRequest<'_, B>,
13620) -> Result<Value, SidecarError>
13621where
13622    B: NativeSidecarBridge + Send + 'static,
13623    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
13624{
13625    if sync_rpc_trace_enabled() {
13626        record_sync_rpc(request.sync_request.method.as_str());
13627    }
13628    let JavascriptSyncRpcServiceRequest {
13629        bridge,
13630        vm_id,
13631        dns,
13632        socket_paths,
13633        kernel,
13634        process,
13635        sync_request: request,
13636        resource_limits,
13637        network_counts,
13638    } = request;
13639    match request.method.as_str() {
13640        // Module resolution / loading / format detection read the kernel VFS so
13641        // the resolver sees exactly what the guest and `kernel.readFile()` see.
13642        "_resolveModule"
13643        | "_resolveModuleSync"
13644        | "__resolve_module"
13645        | "_batchResolveModules"
13646        | "__batch_resolve_modules"
13647        | "_loadFile"
13648        | "_loadFileSync"
13649        | "__load_file"
13650        | "_moduleFormat"
13651        | "__module_format" => service_javascript_module_sync_rpc(kernel, process, request),
13652        // Polyfills are static guest expressions, not VFS reads.
13653        "_loadPolyfill" | "__load_polyfill" => {
13654            service_javascript_internal_bridge_sync_rpc(process, request)
13655        }
13656        "__kernel_stdin_read" => match &process.execution {
13657            ActiveExecution::Javascript(execution) => execution
13658                .read_kernel_stdin_sync_rpc(request)
13659                .map_err(|error| SidecarError::Execution(error.to_string())),
13660            ActiveExecution::Python(_) | ActiveExecution::Wasm(_) | ActiveExecution::Tool(_) => {
13661                service_javascript_kernel_stdin_sync_rpc(kernel, process, request)
13662            }
13663        },
13664        "__kernel_stdio_write" => {
13665            service_javascript_kernel_stdio_write_sync_rpc(kernel, process, request)
13666        }
13667        "__kernel_poll" => service_javascript_kernel_poll_sync_rpc(kernel, process, request),
13668        "__pty_set_raw_mode" => {
13669            service_javascript_pty_set_raw_mode_sync_rpc(kernel, process, request)
13670        }
13671        "crypto.hashDigest"
13672        | "crypto.hmacDigest"
13673        | "crypto.pbkdf2"
13674        | "crypto.scrypt"
13675        | "crypto.cipheriv"
13676        | "crypto.decipheriv"
13677        | "crypto.cipherivCreate"
13678        | "crypto.cipherivUpdate"
13679        | "crypto.cipherivFinal"
13680        | "crypto.sign"
13681        | "crypto.verify"
13682        | "crypto.asymmetricOp"
13683        | "crypto.createKeyObject"
13684        | "crypto.generateKeyPairSync"
13685        | "crypto.generateKeySync"
13686        | "crypto.generatePrimeSync"
13687        | "crypto.diffieHellman"
13688        | "crypto.diffieHellmanGroup"
13689        | "crypto.diffieHellmanSessionCreate"
13690        | "crypto.diffieHellmanSessionCall"
13691        | "crypto.diffieHellmanSessionDestroy"
13692        | "crypto.subtle" => service_javascript_crypto_sync_rpc(process, request),
13693        "dns.lookup" | "dns.resolve" | "dns.resolve4" | "dns.resolve6" => {
13694            service_javascript_dns_sync_rpc(bridge, kernel, vm_id, dns, request)
13695        }
13696        "net.http_listen" | "net.http_close" | "net.http_wait" | "net.http_respond" => {
13697            service_javascript_net_sync_rpc(JavascriptNetSyncRpcServiceRequest {
13698                bridge,
13699                vm_id,
13700                dns,
13701                socket_paths,
13702                kernel,
13703                process,
13704                sync_request: request,
13705                resource_limits,
13706                network_counts,
13707            })
13708        }
13709        "net.http2_server_listen"
13710        | "net.http2_server_poll"
13711        | "net.http2_server_close"
13712        | "net.http2_server_respond"
13713        | "net.http2_server_wait"
13714        | "net.http2_session_connect"
13715        | "net.http2_session_request"
13716        | "net.http2_session_settings"
13717        | "net.http2_session_set_local_window_size"
13718        | "net.http2_session_goaway"
13719        | "net.http2_session_close"
13720        | "net.http2_session_destroy"
13721        | "net.http2_session_poll"
13722        | "net.http2_session_wait"
13723        | "net.http2_stream_respond"
13724        | "net.http2_stream_push_stream"
13725        | "net.http2_stream_write"
13726        | "net.http2_stream_end"
13727        | "net.http2_stream_close"
13728        | "net.http2_stream_pause"
13729        | "net.http2_stream_resume"
13730        | "net.http2_stream_respond_with_file" => {
13731            service_javascript_http2_sync_rpc(JavascriptHttp2SyncRpcServiceRequest {
13732                bridge,
13733                kernel,
13734                vm_id,
13735                dns,
13736                socket_paths,
13737                process,
13738                sync_request: request,
13739                resource_limits,
13740                network_counts,
13741            })
13742        }
13743        "net.connect"
13744        | "net.reserve_tcp_port"
13745        | "net.release_tcp_port"
13746        | "net.listen"
13747        | "net.poll"
13748        | "net.socket_wait_connect"
13749        | "net.socket_read"
13750        | "net.socket_set_no_delay"
13751        | "net.socket_set_keep_alive"
13752        | "net.socket_upgrade_tls"
13753        | "net.socket_get_tls_client_hello"
13754        | "net.socket_tls_query"
13755        | "net.server_poll"
13756        | "net.server_accept"
13757        | "net.server_connections"
13758        | "net.upgrade_socket_write"
13759        | "net.upgrade_socket_end"
13760        | "net.upgrade_socket_destroy"
13761        | "net.write"
13762        | "net.shutdown"
13763        | "net.destroy"
13764        | "net.server_close"
13765        | "tls.get_ciphers" => {
13766            service_javascript_net_sync_rpc(JavascriptNetSyncRpcServiceRequest {
13767                bridge,
13768                vm_id,
13769                dns,
13770                socket_paths,
13771                kernel,
13772                process,
13773                sync_request: request,
13774                resource_limits,
13775                network_counts,
13776            })
13777        }
13778        "dgram.createSocket"
13779        | "dgram.bind"
13780        | "dgram.send"
13781        | "dgram.poll"
13782        | "dgram.close"
13783        | "dgram.address"
13784        | "dgram.setBufferSize"
13785        | "dgram.getBufferSize" => {
13786            service_javascript_dgram_sync_rpc(JavascriptDgramSyncRpcServiceRequest {
13787                bridge,
13788                kernel,
13789                vm_id,
13790                dns,
13791                socket_paths,
13792                process,
13793                sync_request: request,
13794                resource_limits,
13795                network_counts,
13796            })
13797        }
13798        "sqlite.constants"
13799        | "sqlite.open"
13800        | "sqlite.close"
13801        | "sqlite.exec"
13802        | "sqlite.query"
13803        | "sqlite.prepare"
13804        | "sqlite.location"
13805        | "sqlite.checkpoint"
13806        | "sqlite.statement.run"
13807        | "sqlite.statement.get"
13808        | "sqlite.statement.all"
13809        | "sqlite.statement.iterate"
13810        | "sqlite.statement.columns"
13811        | "sqlite.statement.setReturnArrays"
13812        | "sqlite.statement.setReadBigInts"
13813        | "sqlite.statement.setAllowBareNamedParameters"
13814        | "sqlite.statement.setAllowUnknownNamedParameters"
13815        | "sqlite.statement.finalize" => {
13816            service_javascript_sqlite_sync_rpc(kernel, process, request)
13817        }
13818        "process.kill" => {
13819            let target_pid =
13820                javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?;
13821            let signal = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?;
13822            let parsed_signal = parse_signal(signal)?;
13823            if parsed_signal == 0 {
13824                kernel
13825                    .signal_process(EXECUTION_DRIVER_NAME, target_pid, parsed_signal)
13826                    .map_err(kernel_error)?;
13827                return Ok(Value::Null);
13828            }
13829            let process_pid = i32::try_from(process.kernel_pid)
13830                .map_err(|_| SidecarError::InvalidState("process pid exceeds i32".into()))?;
13831            if target_pid != process_pid {
13832                return Err(SidecarError::InvalidState(format!(
13833                    "unknown process pid {target_pid}"
13834                )));
13835            }
13836            process.pending_self_signal_exit = None;
13837            if parsed_signal != 0
13838                && !matches!(
13839                    canonical_signal_name(parsed_signal),
13840                    Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
13841                )
13842            {
13843                process.pending_self_signal_exit = Some(parsed_signal);
13844            }
13845            Ok(json!({
13846                "self": true,
13847                "action": "default",
13848            }))
13849        }
13850        "process.umask" => {
13851            let new_mask = javascript_sync_rpc_arg_u32_optional(&request.args, 0, "process umask")?;
13852            kernel
13853                .umask(EXECUTION_DRIVER_NAME, process.kernel_pid, new_mask)
13854                .map(|mask| json!(mask))
13855                .map_err(kernel_error)
13856        }
13857        "fs.chmodSync" | "fs.promises.chmod" => {
13858            let response =
13859                service_javascript_fs_sync_rpc(kernel, process, process.kernel_pid, request)?;
13860            mirror_process_chmod_to_host(process, request)?;
13861            Ok(response)
13862        }
13863        _ => service_javascript_fs_sync_rpc(kernel, process, process.kernel_pid, request),
13864    }
13865}
13866
13867fn service_javascript_internal_bridge_sync_rpc(
13868    process: &ActiveProcess,
13869    request: &JavascriptSyncRpcRequest,
13870) -> Result<Value, SidecarError> {
13871    // Module resolution / loading / format now reads the kernel VFS via
13872    // `service_javascript_module_sync_rpc`. This host-context path only handles
13873    // polyfills, which are static guest expressions independent of the FS.
13874    let method = match request.method.as_str() {
13875        "_loadPolyfill" | "__load_polyfill" => "_loadPolyfill",
13876        other => {
13877            return Err(SidecarError::InvalidState(format!(
13878                "unsupported JavaScript internal bridge method {other}"
13879            )));
13880        }
13881    };
13882
13883    handle_internal_bridge_call_from_host_context(
13884        &process.host_cwd,
13885        &process.guest_cwd,
13886        &process.env,
13887        method,
13888        &request.args,
13889    )
13890    .ok_or_else(|| {
13891        SidecarError::InvalidState(format!(
13892            "JavaScript internal bridge method {method} returned no value"
13893        ))
13894    })
13895}
13896
13897fn mirror_process_chmod_to_host(
13898    process: &ActiveProcess,
13899    request: &JavascriptSyncRpcRequest,
13900) -> Result<(), SidecarError> {
13901    let guest_path = javascript_sync_rpc_arg_str(&request.args, 0, "filesystem chmod path")?;
13902    let mode = javascript_sync_rpc_arg_u32(&request.args, 1, "filesystem chmod mode")? & 0o7777;
13903    let Some(host_path) = resolve_process_guest_path_to_host(process, guest_path) else {
13904        return Ok(());
13905    };
13906    if !host_path.exists() {
13907        return Ok(());
13908    }
13909    fs::set_permissions(&host_path, fs::Permissions::from_mode(mode)).map_err(|error| {
13910        SidecarError::Io(format!(
13911            "failed to mirror chmod to host path {}: {error}",
13912            host_path.display()
13913        ))
13914    })
13915}
13916
13917fn resolve_process_guest_path_to_host(
13918    process: &ActiveProcess,
13919    guest_path: &str,
13920) -> Option<PathBuf> {
13921    let normalized_guest_path = if guest_path.starts_with('/') {
13922        normalize_path(guest_path)
13923    } else {
13924        normalize_path(&format!(
13925            "{}/{}",
13926            process.guest_cwd.trim_end_matches('/'),
13927            guest_path
13928        ))
13929    };
13930    if let Some(host_path) =
13931        host_path_from_runtime_guest_mappings(&process.env, &normalized_guest_path)
13932    {
13933        return Some(host_path);
13934    }
13935    let normalized_guest_cwd = normalize_path(&process.guest_cwd);
13936    let mut host_root = normalize_host_path(&process.host_cwd);
13937    for _ in normalized_guest_cwd
13938        .trim_start_matches('/')
13939        .split('/')
13940        .filter(|segment| !segment.is_empty())
13941    {
13942        host_root = host_root.parent()?.to_path_buf();
13943    }
13944    if normalized_guest_path == "/" {
13945        Some(host_root)
13946    } else {
13947        Some(host_root.join(normalized_guest_path.trim_start_matches('/')))
13948    }
13949}
13950
13951pub(crate) fn service_javascript_crypto_sync_rpc(
13952    process: &mut ActiveProcess,
13953    request: &JavascriptSyncRpcRequest,
13954) -> Result<Value, SidecarError> {
13955    match request.method.as_str() {
13956        "crypto.hashDigest" => {
13957            let algorithm = javascript_crypto_digest_algorithm(
13958                &request.args,
13959                0,
13960                "crypto.hashDigest algorithm",
13961            )?;
13962            let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.hashDigest data")?;
13963            Ok(Value::String(
13964                base64::engine::general_purpose::STANDARD.encode(algorithm.digest(&data)),
13965            ))
13966        }
13967        "crypto.hmacDigest" => {
13968            let algorithm = javascript_crypto_digest_algorithm(
13969                &request.args,
13970                0,
13971                "crypto.hmacDigest algorithm",
13972            )?;
13973            let key = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.hmacDigest key")?;
13974            let data = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.hmacDigest data")?;
13975            Ok(Value::String(
13976                base64::engine::general_purpose::STANDARD.encode(algorithm.hmac(&key, &data)?),
13977            ))
13978        }
13979        "crypto.pbkdf2" => {
13980            let password =
13981                javascript_sync_rpc_base64_arg(&request.args, 0, "crypto.pbkdf2 password")?;
13982            let salt = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.pbkdf2 salt")?;
13983            let iterations =
13984                javascript_sync_rpc_arg_u32(&request.args, 2, "crypto.pbkdf2 iterations")?;
13985            if iterations == 0 {
13986                return Err(SidecarError::InvalidState(String::from(
13987                    "crypto.pbkdf2 iterations must be greater than zero",
13988                )));
13989            }
13990            let key_len = usize::try_from(javascript_sync_rpc_arg_u64(
13991                &request.args,
13992                3,
13993                "crypto.pbkdf2 key length",
13994            )?)
13995            .map_err(|_| {
13996                SidecarError::InvalidState(String::from(
13997                    "crypto.pbkdf2 key length must fit within usize",
13998                ))
13999            })?;
14000            let algorithm =
14001                javascript_crypto_digest_algorithm(&request.args, 4, "crypto.pbkdf2 digest")?;
14002            let mut output = vec![0u8; key_len];
14003            algorithm.pbkdf2(&password, &salt, iterations, &mut output);
14004            Ok(Value::String(
14005                base64::engine::general_purpose::STANDARD.encode(output),
14006            ))
14007        }
14008        "crypto.scrypt" => {
14009            let password =
14010                javascript_sync_rpc_base64_arg(&request.args, 0, "crypto.scrypt password")?;
14011            let salt = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.scrypt salt")?;
14012            let key_len = usize::try_from(javascript_sync_rpc_arg_u64(
14013                &request.args,
14014                2,
14015                "crypto.scrypt key length",
14016            )?)
14017            .map_err(|_| {
14018                SidecarError::InvalidState(String::from(
14019                    "crypto.scrypt key length must fit within usize",
14020                ))
14021            })?;
14022            let options_json =
14023                javascript_sync_rpc_arg_str(&request.args, 3, "crypto.scrypt options")?;
14024            let options: JavascriptScryptOptions =
14025                serde_json::from_str(options_json).map_err(|error| {
14026                    SidecarError::InvalidState(format!(
14027                        "crypto.scrypt options must be valid JSON: {error}"
14028                    ))
14029                })?;
14030            let cost = options.cost.unwrap_or(DEFAULT_SCRYPT_COST);
14031            if cost == 0 || !cost.is_power_of_two() {
14032                return Err(SidecarError::InvalidState(String::from(
14033                    "crypto.scrypt cost must be a positive power of two",
14034                )));
14035            }
14036            let log_n = u8::try_from(cost.ilog2()).map_err(|_| {
14037                SidecarError::InvalidState(String::from(
14038                    "crypto.scrypt cost exceeds supported parameter range",
14039                ))
14040            })?;
14041            let params = ScryptParams::new(
14042                log_n,
14043                options.block_size.unwrap_or(DEFAULT_SCRYPT_BLOCK_SIZE),
14044                options
14045                    .parallelization
14046                    .unwrap_or(DEFAULT_SCRYPT_PARALLELIZATION),
14047                key_len,
14048            )
14049            .map_err(|error| {
14050                SidecarError::InvalidState(format!("crypto.scrypt options are invalid: {error}"))
14051            })?;
14052            let mut output = vec![0u8; key_len];
14053            scrypt(&password, &salt, &params, &mut output).map_err(|error| {
14054                SidecarError::Execution(format!("crypto.scrypt failed: {error}"))
14055            })?;
14056            Ok(Value::String(
14057                base64::engine::general_purpose::STANDARD.encode(output),
14058            ))
14059        }
14060        "crypto.cipheriv" => service_javascript_crypto_cipheriv_sync_rpc(request),
14061        "crypto.decipheriv" => service_javascript_crypto_decipheriv_sync_rpc(request),
14062        "crypto.cipherivCreate" => {
14063            service_javascript_crypto_cipheriv_create_sync_rpc(process, request)
14064        }
14065        "crypto.cipherivUpdate" => {
14066            service_javascript_crypto_cipheriv_update_sync_rpc(process, request)
14067        }
14068        "crypto.cipherivFinal" => {
14069            service_javascript_crypto_cipheriv_final_sync_rpc(process, request)
14070        }
14071        "crypto.sign" => service_javascript_crypto_sign_sync_rpc(request),
14072        "crypto.verify" => service_javascript_crypto_verify_sync_rpc(request),
14073        "crypto.asymmetricOp" => service_javascript_crypto_asymmetric_op_sync_rpc(request),
14074        "crypto.createKeyObject" => service_javascript_crypto_create_key_object_sync_rpc(request),
14075        "crypto.generateKeyPairSync" => {
14076            service_javascript_crypto_generate_key_pair_sync_rpc(request)
14077        }
14078        "crypto.generateKeySync" => service_javascript_crypto_generate_key_sync_rpc(request),
14079        "crypto.generatePrimeSync" => service_javascript_crypto_generate_prime_sync_rpc(request),
14080        "crypto.diffieHellman" => service_javascript_crypto_diffie_hellman_sync_rpc(request),
14081        "crypto.diffieHellmanGroup" => {
14082            service_javascript_crypto_diffie_hellman_group_sync_rpc(request)
14083        }
14084        "crypto.diffieHellmanSessionCreate" => {
14085            service_javascript_crypto_diffie_hellman_session_create_sync_rpc(process, request)
14086        }
14087        "crypto.diffieHellmanSessionCall" => {
14088            service_javascript_crypto_diffie_hellman_session_call_sync_rpc(process, request)
14089        }
14090        "crypto.diffieHellmanSessionDestroy" => {
14091            service_javascript_crypto_diffie_hellman_session_destroy_sync_rpc(process, request)
14092        }
14093        "crypto.subtle" => service_javascript_crypto_subtle_sync_rpc(request),
14094        _ => Err(SidecarError::InvalidState(format!(
14095            "unsupported JavaScript crypto sync RPC method {}",
14096            request.method
14097        ))),
14098    }
14099}
14100
14101fn javascript_crypto_digest_algorithm(
14102    args: &[Value],
14103    index: usize,
14104    label: &str,
14105) -> Result<JavascriptCryptoDigestAlgorithm, SidecarError> {
14106    JavascriptCryptoDigestAlgorithm::parse(javascript_sync_rpc_arg_str(args, index, label)?)
14107}
14108
14109impl JavascriptCryptoDigestAlgorithm {
14110    fn parse(value: &str) -> Result<Self, SidecarError> {
14111        match value.trim().to_ascii_lowercase().replace('-', "").as_str() {
14112            "md5" => Ok(Self::Md5),
14113            "sha1" => Ok(Self::Sha1),
14114            "sha256" => Ok(Self::Sha256),
14115            "sha512" => Ok(Self::Sha512),
14116            _ => Err(SidecarError::InvalidState(format!(
14117                "unsupported crypto digest algorithm {value}"
14118            ))),
14119        }
14120    }
14121
14122    fn digest(self, data: &[u8]) -> Vec<u8> {
14123        match self {
14124            Self::Md5 => Md5::digest(data).to_vec(),
14125            Self::Sha1 => Sha1::digest(data).to_vec(),
14126            Self::Sha256 => Sha256::digest(data).to_vec(),
14127            Self::Sha512 => Sha512::digest(data).to_vec(),
14128        }
14129    }
14130
14131    fn hmac(self, key: &[u8], data: &[u8]) -> Result<Vec<u8>, SidecarError> {
14132        match self {
14133            Self::Md5 => {
14134                let mut mac = Hmac::<Md5>::new_from_slice(key).map_err(|error| {
14135                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14136                })?;
14137                mac.update(data);
14138                Ok(mac.finalize().into_bytes().to_vec())
14139            }
14140            Self::Sha1 => {
14141                let mut mac = Hmac::<Sha1>::new_from_slice(key).map_err(|error| {
14142                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14143                })?;
14144                mac.update(data);
14145                Ok(mac.finalize().into_bytes().to_vec())
14146            }
14147            Self::Sha256 => {
14148                let mut mac = Hmac::<Sha256>::new_from_slice(key).map_err(|error| {
14149                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14150                })?;
14151                mac.update(data);
14152                Ok(mac.finalize().into_bytes().to_vec())
14153            }
14154            Self::Sha512 => {
14155                let mut mac = Hmac::<Sha512>::new_from_slice(key).map_err(|error| {
14156                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14157                })?;
14158                mac.update(data);
14159                Ok(mac.finalize().into_bytes().to_vec())
14160            }
14161        }
14162    }
14163
14164    fn pbkdf2(self, password: &[u8], salt: &[u8], iterations: u32, output: &mut [u8]) {
14165        match self {
14166            Self::Md5 => pbkdf2_hmac::<Md5>(password, salt, iterations, output),
14167            Self::Sha1 => pbkdf2_hmac::<Sha1>(password, salt, iterations, output),
14168            Self::Sha256 => pbkdf2_hmac::<Sha256>(password, salt, iterations, output),
14169            Self::Sha512 => pbkdf2_hmac::<Sha512>(password, salt, iterations, output),
14170        }
14171    }
14172}
14173
14174#[derive(Debug, Clone)]
14175enum JavascriptCryptoKeyMaterial {
14176    Private(PKey<Private>),
14177    Public(PKey<Public>),
14178    Secret(Vec<u8>),
14179}
14180
14181#[derive(Debug, Clone, Deserialize, Serialize)]
14182struct JavascriptSerializedSandboxKeyObject {
14183    #[serde(rename = "type")]
14184    kind: String,
14185    #[serde(skip_serializing_if = "Option::is_none")]
14186    pem: Option<String>,
14187    #[serde(skip_serializing_if = "Option::is_none")]
14188    raw: Option<String>,
14189    #[serde(skip_serializing_if = "Option::is_none", rename = "asymmetricKeyType")]
14190    asymmetric_key_type: Option<String>,
14191    #[serde(
14192        skip_serializing_if = "Option::is_none",
14193        rename = "asymmetricKeyDetails"
14194    )]
14195    asymmetric_key_details: Option<Map<String, Value>>,
14196    #[serde(skip_serializing_if = "Option::is_none")]
14197    jwk: Option<Value>,
14198}
14199
14200#[derive(Debug, Clone)]
14201struct JavascriptDirectKeyInput {
14202    key: JavascriptCryptoKeyMaterial,
14203    padding: Option<Padding>,
14204}
14205
14206fn service_javascript_crypto_cipheriv_sync_rpc(
14207    request: &JavascriptSyncRpcRequest,
14208) -> Result<Value, SidecarError> {
14209    service_javascript_crypto_cipheriv_inner(request, false)
14210}
14211
14212fn service_javascript_crypto_decipheriv_sync_rpc(
14213    request: &JavascriptSyncRpcRequest,
14214) -> Result<Value, SidecarError> {
14215    service_javascript_crypto_cipheriv_inner(request, true)
14216}
14217
14218fn service_javascript_crypto_cipheriv_create_sync_rpc(
14219    process: &mut ActiveProcess,
14220    request: &JavascriptSyncRpcRequest,
14221) -> Result<Value, SidecarError> {
14222    ensure_per_process_state_handle_capacity(process.cipher_sessions.len(), "cipher session")?;
14223    let mode = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.cipherivCreate mode")?;
14224    let decrypt = mode == "decipher";
14225    let algorithm =
14226        javascript_sync_rpc_arg_str(&request.args, 1, "crypto.cipherivCreate algorithm")?;
14227    let key = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.cipherivCreate key")?;
14228    let iv = javascript_sync_rpc_base64_arg_optional(&request.args, 3, "crypto.cipherivCreate iv")?;
14229    let options =
14230        javascript_sync_rpc_json_arg_optional(&request.args, 4, "crypto.cipherivCreate options")?;
14231    let auth_tag_len = javascript_crypto_requested_aead_tag_len(algorithm, options.as_ref())?;
14232    let context = javascript_crypto_build_cipher_context(
14233        algorithm,
14234        &key,
14235        iv.as_deref(),
14236        decrypt,
14237        options.as_ref(),
14238    )?;
14239    process.next_cipher_session_id += 1;
14240    let session_id = process.next_cipher_session_id;
14241    process.cipher_sessions.insert(
14242        session_id,
14243        ActiveCipherSession {
14244            algorithm: algorithm.to_string(),
14245            auth_tag_len,
14246            context,
14247        },
14248    );
14249    Ok(json!(session_id))
14250}
14251
14252fn service_javascript_crypto_cipheriv_update_sync_rpc(
14253    process: &mut ActiveProcess,
14254    request: &JavascriptSyncRpcRequest,
14255) -> Result<Value, SidecarError> {
14256    let session_id =
14257        javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.cipherivUpdate session id")?;
14258    let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.cipherivUpdate data")?;
14259    let session = process
14260        .cipher_sessions
14261        .get_mut(&session_id)
14262        .ok_or_else(|| {
14263            SidecarError::InvalidState(format!("Cipher session {session_id} not found"))
14264        })?;
14265    let result = javascript_crypto_cipher_update(&mut session.context, &data)?;
14266    Ok(Value::String(
14267        base64::engine::general_purpose::STANDARD.encode(result),
14268    ))
14269}
14270
14271fn service_javascript_crypto_cipheriv_final_sync_rpc(
14272    process: &mut ActiveProcess,
14273    request: &JavascriptSyncRpcRequest,
14274) -> Result<Value, SidecarError> {
14275    let session_id =
14276        javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.cipherivFinal session id")?;
14277    let mut session = process.cipher_sessions.remove(&session_id).ok_or_else(|| {
14278        SidecarError::InvalidState(format!("Cipher session {session_id} not found"))
14279    })?;
14280    let data = javascript_crypto_cipher_finalize(&mut session.context)?;
14281    let mut response = Map::new();
14282    response.insert(
14283        String::from("data"),
14284        Value::String(base64::engine::general_purpose::STANDARD.encode(data)),
14285    );
14286    if javascript_crypto_is_aead(&session.algorithm) {
14287        let mut auth_tag = vec![0_u8; session.auth_tag_len];
14288        session
14289            .context
14290            .get_tag(&mut auth_tag)
14291            .map_err(javascript_crypto_openssl_error)?;
14292        response.insert(
14293            String::from("authTag"),
14294            Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
14295        );
14296    }
14297    Ok(Value::String(serde_json::to_string(&response).map_err(
14298        |error| SidecarError::InvalidState(format!("serialize cipher final response: {error}")),
14299    )?))
14300}
14301
14302fn service_javascript_crypto_sign_sync_rpc(
14303    request: &JavascriptSyncRpcRequest,
14304) -> Result<Value, SidecarError> {
14305    let algorithm = request.args.first().and_then(Value::as_str);
14306    let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.sign data")?;
14307    let key_json = javascript_sync_rpc_arg_str(&request.args, 2, "crypto.sign key")?;
14308    let key_input =
14309        javascript_crypto_parse_direct_key_input(key_json, Some("private"), "crypto.sign key")?;
14310    let private_key = javascript_crypto_expect_private_key(key_input.key, "crypto.sign key")?;
14311    let mut signer = javascript_crypto_new_signer(algorithm, &private_key)?;
14312    if let Some(padding) = key_input.padding {
14313        signer
14314            .set_rsa_padding(padding)
14315            .map_err(javascript_crypto_openssl_error)?;
14316    }
14317    signer
14318        .update(&data)
14319        .map_err(javascript_crypto_openssl_error)?;
14320    Ok(Value::String(
14321        base64::engine::general_purpose::STANDARD.encode(
14322            signer
14323                .sign_to_vec()
14324                .map_err(javascript_crypto_openssl_error)?,
14325        ),
14326    ))
14327}
14328
14329fn service_javascript_crypto_verify_sync_rpc(
14330    request: &JavascriptSyncRpcRequest,
14331) -> Result<Value, SidecarError> {
14332    let algorithm = request.args.first().and_then(Value::as_str);
14333    let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.verify data")?;
14334    let key_json = javascript_sync_rpc_arg_str(&request.args, 2, "crypto.verify key")?;
14335    let signature = javascript_sync_rpc_base64_arg(&request.args, 3, "crypto.verify signature")?;
14336    let key_input =
14337        javascript_crypto_parse_direct_key_input(key_json, Some("public"), "crypto.verify key")?;
14338    let public_key = javascript_crypto_expect_public_key(key_input.key, "crypto.verify key")?;
14339    let mut verifier = javascript_crypto_new_verifier(algorithm, &public_key)?;
14340    if let Some(padding) = key_input.padding {
14341        verifier
14342            .set_rsa_padding(padding)
14343            .map_err(javascript_crypto_openssl_error)?;
14344    }
14345    verifier
14346        .update(&data)
14347        .map_err(javascript_crypto_openssl_error)?;
14348    Ok(json!(verifier
14349        .verify(&signature)
14350        .map_err(javascript_crypto_openssl_error)?))
14351}
14352
14353fn service_javascript_crypto_asymmetric_op_sync_rpc(
14354    request: &JavascriptSyncRpcRequest,
14355) -> Result<Value, SidecarError> {
14356    let operation = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.asymmetricOp operation")?;
14357    let key_json = javascript_sync_rpc_arg_str(&request.args, 1, "crypto.asymmetricOp key")?;
14358    let data = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.asymmetricOp data")?;
14359    let expect_kind = match operation {
14360        "publicEncrypt" | "publicDecrypt" => Some("public"),
14361        "privateEncrypt" | "privateDecrypt" => Some("private"),
14362        other => {
14363            return Err(SidecarError::InvalidState(format!(
14364                "Unsupported asymmetric crypto operation: {other}"
14365            )));
14366        }
14367    };
14368    let key_input =
14369        javascript_crypto_parse_direct_key_input(key_json, expect_kind, "crypto.asymmetricOp key")?;
14370    let padding = key_input.padding.unwrap_or(Padding::PKCS1);
14371    let mut output = vec![0_u8; javascript_crypto_rsa_output_size(&key_input.key)?];
14372    let written = match (operation, key_input.key) {
14373        ("publicEncrypt", JavascriptCryptoKeyMaterial::Public(key))
14374        | ("publicDecrypt", JavascriptCryptoKeyMaterial::Public(key)) => {
14375            let rsa = key.rsa().map_err(javascript_crypto_openssl_error)?;
14376            if operation == "publicEncrypt" {
14377                rsa.public_encrypt(&data, &mut output, padding)
14378                    .map_err(javascript_crypto_openssl_error)?
14379            } else {
14380                rsa.public_decrypt(&data, &mut output, padding)
14381                    .map_err(javascript_crypto_openssl_error)?
14382            }
14383        }
14384        ("privateEncrypt", JavascriptCryptoKeyMaterial::Private(key))
14385        | ("privateDecrypt", JavascriptCryptoKeyMaterial::Private(key)) => {
14386            let rsa = key.rsa().map_err(javascript_crypto_openssl_error)?;
14387            if operation == "privateEncrypt" {
14388                rsa.private_encrypt(&data, &mut output, padding)
14389                    .map_err(javascript_crypto_openssl_error)?
14390            } else {
14391                rsa.private_decrypt(&data, &mut output, padding)
14392                    .map_err(javascript_crypto_openssl_error)?
14393            }
14394        }
14395        _ => {
14396            return Err(SidecarError::InvalidState(format!(
14397                "{operation} requires an RSA {} key",
14398                expect_kind.unwrap_or("asymmetric")
14399            )));
14400        }
14401    };
14402    output.truncate(written);
14403    Ok(Value::String(
14404        base64::engine::general_purpose::STANDARD.encode(output),
14405    ))
14406}
14407
14408fn service_javascript_crypto_create_key_object_sync_rpc(
14409    request: &JavascriptSyncRpcRequest,
14410) -> Result<Value, SidecarError> {
14411    let operation =
14412        javascript_sync_rpc_arg_str(&request.args, 0, "crypto.createKeyObject operation")?;
14413    let key_json = javascript_sync_rpc_arg_str(&request.args, 1, "crypto.createKeyObject key")?;
14414    let expected = match operation {
14415        "createPrivateKey" => Some("private"),
14416        "createPublicKey" => Some("public"),
14417        other => {
14418            return Err(SidecarError::InvalidState(format!(
14419                "Unsupported key creation operation: {other}"
14420            )));
14421        }
14422    };
14423    let key_input =
14424        javascript_crypto_parse_direct_key_input(key_json, expected, "crypto.createKeyObject key")?;
14425    Ok(Value::String(
14426        serde_json::to_string(&javascript_crypto_serialize_sandbox_key_object(
14427            &key_input.key,
14428        )?)
14429        .map_err(|error| {
14430            SidecarError::InvalidState(format!("serialize crypto key object: {error}"))
14431        })?,
14432    ))
14433}
14434
14435fn service_javascript_crypto_generate_key_pair_sync_rpc(
14436    request: &JavascriptSyncRpcRequest,
14437) -> Result<Value, SidecarError> {
14438    let key_type =
14439        javascript_sync_rpc_arg_str(&request.args, 0, "crypto.generateKeyPairSync type")?;
14440    let options = javascript_crypto_parse_serialized_options_arg(
14441        &request.args,
14442        1,
14443        "crypto.generateKeyPairSync options",
14444    )?
14445    .unwrap_or(Value::Object(Map::new()));
14446    let public_encoding = options.get("publicKeyEncoding").cloned();
14447    let private_encoding = options.get("privateKeyEncoding").cloned();
14448
14449    let private_key = match key_type {
14450        "rsa" => {
14451            let bits = options
14452                .get("modulusLength")
14453                .and_then(Value::as_u64)
14454                .unwrap_or(2048) as u32;
14455            let exponent = options
14456                .get("publicExponent")
14457                .map(|value| javascript_crypto_u32_from_bridge_value(value, "rsa publicExponent"))
14458                .transpose()?
14459                .unwrap_or(65_537);
14460            let exponent = BigNum::from_u32(exponent).map_err(javascript_crypto_openssl_error)?;
14461            let rsa =
14462                Rsa::generate_with_e(bits, &exponent).map_err(javascript_crypto_openssl_error)?;
14463            PKey::from_rsa(rsa).map_err(javascript_crypto_openssl_error)?
14464        }
14465        "ec" => {
14466            let named_curve = options
14467                .get("namedCurve")
14468                .and_then(Value::as_str)
14469                .ok_or_else(|| {
14470                    SidecarError::InvalidState(String::from(
14471                        "crypto.generateKeyPairSync ec requires namedCurve",
14472                    ))
14473                })?;
14474            let group = EcGroup::from_curve_name(javascript_crypto_curve_nid(named_curve)?)
14475                .map_err(javascript_crypto_openssl_error)?;
14476            let key = EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?;
14477            PKey::from_ec_key(key).map_err(javascript_crypto_openssl_error)?
14478        }
14479        "ed25519" => PKey::generate_ed25519().map_err(javascript_crypto_openssl_error)?,
14480        "x25519" => PKey::generate_x25519().map_err(javascript_crypto_openssl_error)?,
14481        other => {
14482            return Err(SidecarError::InvalidState(format!(
14483                "unsupported crypto key pair type {other}"
14484            )));
14485        }
14486    };
14487    let public_key = PKey::public_key_from_pem(
14488        &private_key
14489            .public_key_to_pem()
14490            .map_err(javascript_crypto_openssl_error)?,
14491    )
14492    .map_err(javascript_crypto_openssl_error)?;
14493    let response = if public_encoding.is_some() || private_encoding.is_some() {
14494        json!({
14495            "publicKey": javascript_crypto_serialize_encoded_key_value_public(&public_key, public_encoding.as_ref())?,
14496            "privateKey": javascript_crypto_serialize_encoded_key_value_private(&private_key, private_encoding.as_ref())?,
14497        })
14498    } else {
14499        json!({
14500            "publicKey": javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Public(public_key))?,
14501            "privateKey": javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Private(private_key))?,
14502        })
14503    };
14504    Ok(Value::String(serde_json::to_string(&response).map_err(
14505        |error| SidecarError::InvalidState(format!("serialize generated key pair: {error}")),
14506    )?))
14507}
14508
14509fn service_javascript_crypto_generate_key_sync_rpc(
14510    request: &JavascriptSyncRpcRequest,
14511) -> Result<Value, SidecarError> {
14512    let key_type = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.generateKeySync type")?;
14513    let options = javascript_crypto_parse_serialized_options_arg(
14514        &request.args,
14515        1,
14516        "crypto.generateKeySync options",
14517    )?
14518    .unwrap_or(Value::Object(Map::new()));
14519    let bit_length = options
14520        .get("length")
14521        .and_then(Value::as_u64)
14522        .ok_or_else(|| {
14523            SidecarError::InvalidState(String::from(
14524                "crypto.generateKeySync options.length is required",
14525            ))
14526        })? as usize;
14527    let mut raw = vec![0_u8; bit_length.div_ceil(8)];
14528    rand_bytes(&mut raw).map_err(javascript_crypto_openssl_error)?;
14529    let serialized = match key_type {
14530        "hmac" => javascript_crypto_serialize_sandbox_key_object(
14531            &JavascriptCryptoKeyMaterial::Secret(raw),
14532        )?,
14533        "aes" => javascript_crypto_serialize_sandbox_key_object(
14534            &JavascriptCryptoKeyMaterial::Secret(raw),
14535        )?,
14536        other => {
14537            return Err(SidecarError::InvalidState(format!(
14538                "unsupported crypto.generateKeySync type {other}"
14539            )));
14540        }
14541    };
14542    Ok(Value::String(serde_json::to_string(&serialized).map_err(
14543        |error| SidecarError::InvalidState(format!("serialize generated key: {error}")),
14544    )?))
14545}
14546
14547fn service_javascript_crypto_generate_prime_sync_rpc(
14548    request: &JavascriptSyncRpcRequest,
14549) -> Result<Value, SidecarError> {
14550    let bits =
14551        javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.generatePrimeSync size")? as i32;
14552    let options = javascript_crypto_parse_serialized_options_arg(
14553        &request.args,
14554        1,
14555        "crypto.generatePrimeSync options",
14556    )?
14557    .unwrap_or(Value::Object(Map::new()));
14558    let safe = options
14559        .get("safe")
14560        .and_then(Value::as_bool)
14561        .unwrap_or(false);
14562    let add = options
14563        .get("add")
14564        .map(|value| javascript_crypto_bignum_from_bridge_value(value, "prime add"))
14565        .transpose()?;
14566    let rem = options
14567        .get("rem")
14568        .map(|value| javascript_crypto_bignum_from_bridge_value(value, "prime rem"))
14569        .transpose()?;
14570    let mut prime = BigNum::new().map_err(javascript_crypto_openssl_error)?;
14571    prime
14572        .generate_prime(bits, safe, add.as_deref(), rem.as_deref())
14573        .map_err(javascript_crypto_openssl_error)?;
14574    let payload = if options
14575        .get("bigint")
14576        .and_then(Value::as_bool)
14577        .unwrap_or(false)
14578    {
14579        json!({
14580            "__type": "bigint",
14581            "value": prime.to_dec_str().map_err(javascript_crypto_openssl_error)?.to_string(),
14582        })
14583    } else {
14584        json!({
14585            "__type": "buffer",
14586            "value": base64::engine::general_purpose::STANDARD.encode(prime.to_vec()),
14587        })
14588    };
14589    Ok(Value::String(serde_json::to_string(&payload).map_err(
14590        |error| SidecarError::InvalidState(format!("serialize generated prime: {error}")),
14591    )?))
14592}
14593
14594fn service_javascript_crypto_diffie_hellman_sync_rpc(
14595    request: &JavascriptSyncRpcRequest,
14596) -> Result<Value, SidecarError> {
14597    let options = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.diffieHellman options")?;
14598    let parsed: Value = serde_json::from_str(options).map_err(|error| {
14599        SidecarError::InvalidState(format!(
14600            "crypto.diffieHellman options must be valid JSON: {error}"
14601        ))
14602    })?;
14603    let private_key = javascript_crypto_parse_key_material_value(
14604        parsed.get("privateKey").ok_or_else(|| {
14605            SidecarError::InvalidState(String::from("crypto.diffieHellman missing privateKey"))
14606        })?,
14607        Some("private"),
14608        "crypto.diffieHellman privateKey",
14609    )?;
14610    let public_key = javascript_crypto_parse_key_material_value(
14611        parsed.get("publicKey").ok_or_else(|| {
14612            SidecarError::InvalidState(String::from("crypto.diffieHellman missing publicKey"))
14613        })?,
14614        Some("public"),
14615        "crypto.diffieHellman publicKey",
14616    )?;
14617    let private_key =
14618        javascript_crypto_expect_private_key(private_key, "crypto.diffieHellman privateKey")?;
14619    let public_key =
14620        javascript_crypto_expect_public_key(public_key, "crypto.diffieHellman publicKey")?;
14621    let mut deriver = Deriver::new(&private_key).map_err(javascript_crypto_openssl_error)?;
14622    deriver
14623        .set_peer(&public_key)
14624        .map_err(javascript_crypto_openssl_error)?;
14625    let secret = deriver
14626        .derive_to_vec()
14627        .map_err(javascript_crypto_openssl_error)?;
14628    Ok(Value::String(
14629        serde_json::to_string(&json!({
14630            "__type": "buffer",
14631            "value": base64::engine::general_purpose::STANDARD.encode(secret),
14632        }))
14633        .map_err(|error| {
14634            SidecarError::InvalidState(format!("serialize derived secret: {error}"))
14635        })?,
14636    ))
14637}
14638
14639fn service_javascript_crypto_diffie_hellman_group_sync_rpc(
14640    request: &JavascriptSyncRpcRequest,
14641) -> Result<Value, SidecarError> {
14642    let name = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.diffieHellmanGroup name")?;
14643    let params = javascript_crypto_named_dh_group(name)?;
14644    let response = json!({
14645        "prime": {
14646            "__type": "buffer",
14647            "value": base64::engine::general_purpose::STANDARD.encode(params.prime_p().to_vec()),
14648        },
14649        "generator": {
14650            "__type": "buffer",
14651            "value": base64::engine::general_purpose::STANDARD.encode(params.generator().to_vec()),
14652        },
14653    });
14654    Ok(Value::String(serde_json::to_string(&response).map_err(
14655        |error| {
14656            SidecarError::InvalidState(format!("serialize diffieHellmanGroup response: {error}"))
14657        },
14658    )?))
14659}
14660
14661fn service_javascript_crypto_diffie_hellman_session_create_sync_rpc(
14662    process: &mut ActiveProcess,
14663    request: &JavascriptSyncRpcRequest,
14664) -> Result<Value, SidecarError> {
14665    ensure_per_process_state_handle_capacity(
14666        process.diffie_hellman_sessions.len(),
14667        "diffie-hellman session",
14668    )?;
14669    let raw = javascript_sync_rpc_arg_str(
14670        &request.args,
14671        0,
14672        "crypto.diffieHellmanSessionCreate request",
14673    )?;
14674    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14675        SidecarError::InvalidState(format!(
14676            "crypto.diffieHellmanSessionCreate request must be valid JSON: {error}"
14677        ))
14678    })?;
14679    let session = match parsed.get("type").and_then(Value::as_str) {
14680        Some("group") => {
14681            let name = parsed.get("name").and_then(Value::as_str).ok_or_else(|| {
14682                SidecarError::InvalidState(String::from(
14683                    "crypto.diffieHellmanSessionCreate group requires name",
14684                ))
14685            })?;
14686            ActiveDiffieHellmanSession::Dh(ActiveDhSession {
14687                params: javascript_crypto_named_dh_group(name)?,
14688                key_pair: None,
14689            })
14690        }
14691        Some("dh") => {
14692            let args = parsed
14693                .get("args")
14694                .and_then(Value::as_array)
14695                .ok_or_else(|| {
14696                    SidecarError::InvalidState(String::from(
14697                        "crypto.diffieHellmanSessionCreate dh requires args",
14698                    ))
14699                })?;
14700            let params = javascript_crypto_build_dh_params(args)?;
14701            ActiveDiffieHellmanSession::Dh(ActiveDhSession {
14702                params,
14703                key_pair: None,
14704            })
14705        }
14706        Some("ecdh") => {
14707            let curve = parsed.get("name").and_then(Value::as_str).ok_or_else(|| {
14708                SidecarError::InvalidState(String::from(
14709                    "crypto.diffieHellmanSessionCreate ecdh requires name",
14710                ))
14711            })?;
14712            ActiveDiffieHellmanSession::Ecdh(ActiveEcdhSession {
14713                curve: curve.to_string(),
14714                key_pair: None,
14715            })
14716        }
14717        other => {
14718            return Err(SidecarError::InvalidState(format!(
14719                "Unsupported Diffie-Hellman session type: {}",
14720                other.unwrap_or("<missing>")
14721            )));
14722        }
14723    };
14724    process.next_diffie_hellman_session_id += 1;
14725    let session_id = process.next_diffie_hellman_session_id;
14726    process.diffie_hellman_sessions.insert(session_id, session);
14727    Ok(json!(session_id))
14728}
14729
14730fn service_javascript_crypto_diffie_hellman_session_call_sync_rpc(
14731    process: &mut ActiveProcess,
14732    request: &JavascriptSyncRpcRequest,
14733) -> Result<Value, SidecarError> {
14734    let session_id = javascript_sync_rpc_arg_u64(
14735        &request.args,
14736        0,
14737        "crypto.diffieHellmanSessionCall session id",
14738    )?;
14739    let raw =
14740        javascript_sync_rpc_arg_str(&request.args, 1, "crypto.diffieHellmanSessionCall request")?;
14741    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14742        SidecarError::InvalidState(format!(
14743            "crypto.diffieHellmanSessionCall request must be valid JSON: {error}"
14744        ))
14745    })?;
14746    let method = parsed
14747        .get("method")
14748        .and_then(Value::as_str)
14749        .ok_or_else(|| {
14750            SidecarError::InvalidState(String::from(
14751                "crypto.diffieHellmanSessionCall request missing method",
14752            ))
14753        })?;
14754    let args = parsed
14755        .get("args")
14756        .and_then(Value::as_array)
14757        .cloned()
14758        .unwrap_or_default();
14759    let session = process
14760        .diffie_hellman_sessions
14761        .get_mut(&session_id)
14762        .ok_or_else(|| {
14763            SidecarError::InvalidState(format!("Diffie-Hellman session {session_id} not found"))
14764        })?;
14765    let (result, has_result) = match session {
14766        ActiveDiffieHellmanSession::Dh(session) => {
14767            javascript_crypto_call_dh_session(session, method, &args)?
14768        }
14769        ActiveDiffieHellmanSession::Ecdh(session) => {
14770            javascript_crypto_call_ecdh_session(session, method, &args)?
14771        }
14772    };
14773    Ok(Value::String(
14774        serde_json::to_string(&json!({
14775            "result": result,
14776            "hasResult": has_result,
14777        }))
14778        .map_err(|error| {
14779            SidecarError::InvalidState(format!("serialize diffie session result: {error}"))
14780        })?,
14781    ))
14782}
14783
14784fn service_javascript_crypto_diffie_hellman_session_destroy_sync_rpc(
14785    process: &mut ActiveProcess,
14786    request: &JavascriptSyncRpcRequest,
14787) -> Result<Value, SidecarError> {
14788    let session_id = javascript_sync_rpc_arg_u64(
14789        &request.args,
14790        0,
14791        "crypto.diffieHellmanSessionDestroy session id",
14792    )?;
14793    process
14794        .diffie_hellman_sessions
14795        .remove(&session_id)
14796        .ok_or_else(|| {
14797            SidecarError::InvalidState(format!("Diffie-Hellman session {session_id} not found"))
14798        })?;
14799    Ok(Value::Null)
14800}
14801
14802fn service_javascript_crypto_subtle_sync_rpc(
14803    request: &JavascriptSyncRpcRequest,
14804) -> Result<Value, SidecarError> {
14805    let raw = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.subtle request")?;
14806    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14807        SidecarError::InvalidState(format!("crypto.subtle request must be valid JSON: {error}"))
14808    })?;
14809    let op = parsed.get("op").and_then(Value::as_str).ok_or_else(|| {
14810        SidecarError::InvalidState(String::from("crypto.subtle request missing op"))
14811    })?;
14812    match op {
14813        "digest" => {
14814            let algorithm = parsed
14815                .get("algorithm")
14816                .and_then(Value::as_str)
14817                .ok_or_else(|| {
14818                    SidecarError::InvalidState(String::from(
14819                        "crypto.subtle.digest missing algorithm",
14820                    ))
14821                })?;
14822            let data = parsed.get("data").and_then(Value::as_str).ok_or_else(|| {
14823                SidecarError::InvalidState(String::from("crypto.subtle.digest missing data"))
14824            })?;
14825            let bytes = base64::engine::general_purpose::STANDARD
14826                .decode(data)
14827                .map_err(|error| {
14828                    SidecarError::InvalidState(format!("crypto.subtle.digest data base64: {error}"))
14829                })?;
14830            let digest = JavascriptCryptoDigestAlgorithm::parse(algorithm)?.digest(&bytes);
14831            Ok(Value::String(
14832                serde_json::to_string(&json!({
14833                    "data": base64::engine::general_purpose::STANDARD.encode(digest),
14834                }))
14835                .map_err(|error| {
14836                    SidecarError::InvalidState(format!("serialize crypto.subtle digest: {error}"))
14837                })?,
14838            ))
14839        }
14840        "generateKey" => {
14841            let algorithm = parsed.get("algorithm").ok_or_else(|| {
14842                SidecarError::InvalidState(String::from(
14843                    "crypto.subtle.generateKey missing algorithm",
14844                ))
14845            })?;
14846            let name =
14847                javascript_crypto_subtle_algorithm_name(algorithm, "crypto.subtle.generateKey")?;
14848            if !matches!(name, "AES-GCM" | "AES-CBC" | "AES-CTR" | "AES-KW") {
14849                return Err(SidecarError::InvalidState(format!(
14850                    "Unsupported key algorithm: {name}"
14851                )));
14852            }
14853            let length_bits = algorithm
14854                .get("length")
14855                .and_then(Value::as_u64)
14856                .ok_or_else(|| {
14857                    SidecarError::InvalidState(String::from(
14858                        "crypto.subtle.generateKey AES algorithm requires length",
14859                    ))
14860                })?;
14861            if length_bits % 8 != 0 {
14862                return Err(SidecarError::InvalidState(String::from(
14863                    "crypto.subtle.generateKey length must be byte-aligned",
14864                )));
14865            }
14866            let length_bytes = usize::try_from(length_bits / 8).map_err(|_| {
14867                SidecarError::InvalidState(String::from(
14868                    "crypto.subtle.generateKey length is too large",
14869                ))
14870            })?;
14871            let mut raw = vec![0_u8; length_bytes];
14872            rand_bytes(&mut raw).map_err(javascript_crypto_openssl_error)?;
14873            let key = javascript_crypto_serialize_subtle_secret_key(
14874                &raw,
14875                javascript_crypto_normalize_subtle_secret_algorithm(algorithm.clone(), &raw)?,
14876                parsed
14877                    .get("extractable")
14878                    .and_then(Value::as_bool)
14879                    .unwrap_or(false),
14880                parsed.get("usages").cloned().unwrap_or_else(|| json!([])),
14881            )?;
14882            Ok(Value::String(
14883                serde_json::to_string(&json!({ "key": key })).map_err(|error| {
14884                    SidecarError::InvalidState(format!(
14885                        "serialize crypto.subtle generated key: {error}"
14886                    ))
14887                })?,
14888            ))
14889        }
14890        "importKey" => {
14891            let format = parsed
14892                .get("format")
14893                .and_then(Value::as_str)
14894                .ok_or_else(|| {
14895                    SidecarError::InvalidState(String::from(
14896                        "crypto.subtle.importKey missing format",
14897                    ))
14898                })?;
14899            if format != "raw" {
14900                return Err(SidecarError::InvalidState(format!(
14901                    "Unsupported import format: {format}"
14902                )));
14903            }
14904            let key_data = parsed
14905                .get("keyData")
14906                .and_then(Value::as_str)
14907                .ok_or_else(|| {
14908                    SidecarError::InvalidState(String::from(
14909                        "crypto.subtle.importKey missing keyData",
14910                    ))
14911                })?;
14912            let raw = base64::engine::general_purpose::STANDARD
14913                .decode(key_data)
14914                .map_err(|error| {
14915                    SidecarError::InvalidState(format!(
14916                        "crypto.subtle.importKey keyData base64: {error}"
14917                    ))
14918                })?;
14919            let algorithm = parsed.get("algorithm").ok_or_else(|| {
14920                SidecarError::InvalidState(String::from(
14921                    "crypto.subtle.importKey missing algorithm",
14922                ))
14923            })?;
14924            let key = javascript_crypto_serialize_subtle_secret_key(
14925                &raw,
14926                javascript_crypto_normalize_subtle_secret_algorithm(algorithm.clone(), &raw)?,
14927                parsed
14928                    .get("extractable")
14929                    .and_then(Value::as_bool)
14930                    .unwrap_or(false),
14931                parsed.get("usages").cloned().unwrap_or_else(|| json!([])),
14932            )?;
14933            Ok(Value::String(
14934                serde_json::to_string(&json!({ "key": key })).map_err(|error| {
14935                    SidecarError::InvalidState(format!(
14936                        "serialize crypto.subtle imported key: {error}"
14937                    ))
14938                })?,
14939            ))
14940        }
14941        "exportKey" => {
14942            let format = parsed
14943                .get("format")
14944                .and_then(Value::as_str)
14945                .ok_or_else(|| {
14946                    SidecarError::InvalidState(String::from(
14947                        "crypto.subtle.exportKey missing format",
14948                    ))
14949                })?;
14950            if format != "raw" {
14951                return Err(SidecarError::InvalidState(format!(
14952                    "Unsupported export format: {format}"
14953                )));
14954            }
14955            let raw = javascript_crypto_subtle_key_raw(
14956                parsed.get("key").ok_or_else(|| {
14957                    SidecarError::InvalidState(String::from("crypto.subtle.exportKey missing key"))
14958                })?,
14959                "crypto.subtle.exportKey key",
14960            )?;
14961            Ok(Value::String(
14962                serde_json::to_string(&json!({
14963                    "data": base64::engine::general_purpose::STANDARD.encode(raw),
14964                }))
14965                .map_err(|error| {
14966                    SidecarError::InvalidState(format!("serialize crypto.subtle export: {error}"))
14967                })?,
14968            ))
14969        }
14970        "encrypt" | "decrypt" => service_javascript_crypto_subtle_aes_crypt_sync_rpc(op, &parsed),
14971        _ => Err(SidecarError::InvalidState(format!(
14972            "Unsupported subtle operation: {op}"
14973        ))),
14974    }
14975}
14976
14977fn javascript_crypto_subtle_algorithm_name<'a>(
14978    algorithm: &'a Value,
14979    label: &str,
14980) -> Result<&'a str, SidecarError> {
14981    if let Some(name) = algorithm.as_str() {
14982        return Ok(name);
14983    }
14984    algorithm
14985        .get("name")
14986        .and_then(Value::as_str)
14987        .ok_or_else(|| SidecarError::InvalidState(format!("{label} algorithm missing name")))
14988}
14989
14990fn javascript_crypto_normalize_subtle_secret_algorithm(
14991    algorithm: Value,
14992    raw: &[u8],
14993) -> Result<Value, SidecarError> {
14994    let mut object = match algorithm {
14995        Value::String(name) => {
14996            let mut object = Map::new();
14997            object.insert(String::from("name"), Value::String(name));
14998            object
14999        }
15000        Value::Object(object) => object,
15001        _ => {
15002            return Err(SidecarError::InvalidState(String::from(
15003                "crypto.subtle secret algorithm must be a string or object",
15004            )));
15005        }
15006    };
15007    let name = object
15008        .get("name")
15009        .and_then(Value::as_str)
15010        .ok_or_else(|| {
15011            SidecarError::InvalidState(String::from("crypto.subtle secret algorithm missing name"))
15012        })?
15013        .to_string();
15014    if matches!(name.as_str(), "AES-GCM" | "AES-CBC" | "AES-CTR" | "AES-KW")
15015        && !object.contains_key("length")
15016    {
15017        object.insert(String::from("length"), json!(raw.len() * 8));
15018    }
15019    Ok(Value::Object(object))
15020}
15021
15022fn javascript_crypto_serialize_subtle_secret_key(
15023    raw: &[u8],
15024    algorithm: Value,
15025    extractable: bool,
15026    usages: Value,
15027) -> Result<Value, SidecarError> {
15028    let raw_base64 = base64::engine::general_purpose::STANDARD.encode(raw);
15029    let source_key_object_data = javascript_crypto_serialize_sandbox_key_object(
15030        &JavascriptCryptoKeyMaterial::Secret(raw.to_vec()),
15031    )?;
15032    Ok(json!({
15033        "type": "secret",
15034        "algorithm": algorithm,
15035        "extractable": extractable,
15036        "usages": usages,
15037        "_raw": raw_base64,
15038        "_sourceKeyObjectData": source_key_object_data,
15039    }))
15040}
15041
15042fn javascript_crypto_subtle_key_raw(key: &Value, label: &str) -> Result<Vec<u8>, SidecarError> {
15043    let raw = key.get("_raw").and_then(Value::as_str).ok_or_else(|| {
15044        SidecarError::InvalidState(format!("{label} must be a raw secret CryptoKey"))
15045    })?;
15046    base64::engine::general_purpose::STANDARD
15047        .decode(raw)
15048        .map_err(|error| SidecarError::InvalidState(format!("{label} raw base64: {error}")))
15049}
15050
15051fn service_javascript_crypto_subtle_aes_crypt_sync_rpc(
15052    op: &str,
15053    parsed: &Value,
15054) -> Result<Value, SidecarError> {
15055    let algorithm = parsed.get("algorithm").ok_or_else(|| {
15056        SidecarError::InvalidState(format!("crypto.subtle.{op} missing algorithm"))
15057    })?;
15058    let name = javascript_crypto_subtle_algorithm_name(algorithm, &format!("crypto.subtle.{op}"))?;
15059    if name != "AES-GCM" {
15060        return Err(SidecarError::InvalidState(format!(
15061            "Unsupported subtle AES operation algorithm: {name}"
15062        )));
15063    }
15064    let key = javascript_crypto_subtle_key_raw(
15065        parsed
15066            .get("key")
15067            .ok_or_else(|| SidecarError::InvalidState(format!("crypto.subtle.{op} missing key")))?,
15068        &format!("crypto.subtle.{op} key"),
15069    )?;
15070    let iv = algorithm.get("iv").and_then(Value::as_str).ok_or_else(|| {
15071        SidecarError::InvalidState(format!("crypto.subtle.{op} AES-GCM missing iv"))
15072    })?;
15073    let iv = base64::engine::general_purpose::STANDARD
15074        .decode(iv)
15075        .map_err(|error| {
15076            SidecarError::InvalidState(format!("crypto.subtle.{op} iv base64: {error}"))
15077        })?;
15078    let data = parsed
15079        .get("data")
15080        .and_then(Value::as_str)
15081        .ok_or_else(|| SidecarError::InvalidState(format!("crypto.subtle.{op} missing data")))?;
15082    let mut data = base64::engine::general_purpose::STANDARD
15083        .decode(data)
15084        .map_err(|error| {
15085            SidecarError::InvalidState(format!("crypto.subtle.{op} data base64: {error}"))
15086        })?;
15087    let tag_len = javascript_crypto_subtle_aes_gcm_tag_len(algorithm)?;
15088    let mut options = Map::new();
15089    options.insert(String::from("authTagLength"), json!(tag_len));
15090    if let Some(additional_data) = algorithm.get("additionalData").and_then(Value::as_str) {
15091        options.insert(
15092            String::from("aad"),
15093            Value::String(additional_data.to_string()),
15094        );
15095    }
15096    let decrypt = op == "decrypt";
15097    if decrypt {
15098        if data.len() < tag_len {
15099            return Err(SidecarError::InvalidState(String::from(
15100                "crypto.subtle.decrypt AES-GCM data shorter than auth tag",
15101            )));
15102        }
15103        let auth_tag = data.split_off(data.len() - tag_len);
15104        options.insert(
15105            String::from("authTag"),
15106            Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
15107        );
15108    }
15109    let cipher_name = format!("aes-{}-gcm", key.len() * 8);
15110    let mut context = javascript_crypto_build_cipher_context(
15111        &cipher_name,
15112        &key,
15113        Some(&iv),
15114        decrypt,
15115        Some(&Value::Object(options)),
15116    )?;
15117    let mut output = javascript_crypto_cipher_update(&mut context, &data)?;
15118    output.extend(javascript_crypto_cipher_finalize(&mut context)?);
15119    if !decrypt {
15120        let mut auth_tag = vec![0_u8; tag_len];
15121        context
15122            .get_tag(&mut auth_tag)
15123            .map_err(javascript_crypto_openssl_error)?;
15124        output.extend(auth_tag);
15125    }
15126    Ok(Value::String(
15127        serde_json::to_string(&json!({
15128            "data": base64::engine::general_purpose::STANDARD.encode(output),
15129        }))
15130        .map_err(|error| {
15131            SidecarError::InvalidState(format!("serialize crypto.subtle {op}: {error}"))
15132        })?,
15133    ))
15134}
15135
15136fn javascript_crypto_subtle_aes_gcm_tag_len(algorithm: &Value) -> Result<usize, SidecarError> {
15137    let tag_bits = algorithm
15138        .get("tagLength")
15139        .and_then(Value::as_u64)
15140        .unwrap_or(128);
15141    if !tag_bits.is_multiple_of(8) {
15142        return Err(SidecarError::InvalidState(String::from(
15143            "crypto.subtle AES-GCM tagLength must be byte-aligned",
15144        )));
15145    }
15146    usize::try_from(tag_bits / 8).map_err(|_| {
15147        SidecarError::InvalidState(String::from("crypto.subtle AES-GCM tagLength too large"))
15148    })
15149}
15150
15151fn service_javascript_crypto_cipheriv_inner(
15152    request: &JavascriptSyncRpcRequest,
15153    decrypt: bool,
15154) -> Result<Value, SidecarError> {
15155    let label = if decrypt {
15156        "crypto.decipheriv"
15157    } else {
15158        "crypto.cipheriv"
15159    };
15160    let algorithm = javascript_sync_rpc_arg_str(&request.args, 0, &format!("{label} algorithm"))?;
15161    let key = javascript_sync_rpc_base64_arg(&request.args, 1, &format!("{label} key"))?;
15162    let iv = javascript_sync_rpc_base64_arg_optional(&request.args, 2, &format!("{label} iv"))?;
15163    let data = javascript_sync_rpc_base64_arg(&request.args, 3, &format!("{label} data"))?;
15164    let options =
15165        javascript_sync_rpc_json_arg_optional(&request.args, 4, &format!("{label} options"))?;
15166    let auth_tag_len = javascript_crypto_requested_aead_tag_len(algorithm, options.as_ref())?;
15167    let mut context = javascript_crypto_build_cipher_context(
15168        algorithm,
15169        &key,
15170        iv.as_deref(),
15171        decrypt,
15172        options.as_ref(),
15173    )?;
15174    let payload = javascript_crypto_cipher_update(&mut context, &data)?;
15175    let final_bytes = javascript_crypto_cipher_finalize(&mut context)?;
15176    if decrypt {
15177        let mut output = payload;
15178        output.extend(final_bytes);
15179        return Ok(Value::String(
15180            base64::engine::general_purpose::STANDARD.encode(output),
15181        ));
15182    }
15183
15184    let mut response = Map::new();
15185    let mut encrypted = payload;
15186    encrypted.extend(final_bytes);
15187    response.insert(
15188        String::from("data"),
15189        Value::String(base64::engine::general_purpose::STANDARD.encode(encrypted)),
15190    );
15191    if javascript_crypto_is_aead(algorithm) {
15192        let mut auth_tag = vec![0_u8; auth_tag_len];
15193        context
15194            .get_tag(&mut auth_tag)
15195            .map_err(javascript_crypto_openssl_error)?;
15196        response.insert(
15197            String::from("authTag"),
15198            Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
15199        );
15200    }
15201    Ok(Value::String(serde_json::to_string(&response).map_err(
15202        |error| SidecarError::InvalidState(format!("serialize {label} response: {error}")),
15203    )?))
15204}
15205
15206fn javascript_sync_rpc_base64_arg_optional(
15207    args: &[Value],
15208    index: usize,
15209    label: &str,
15210) -> Result<Option<Vec<u8>>, SidecarError> {
15211    if args.get(index).is_none() || args[index].is_null() {
15212        return Ok(None);
15213    }
15214    javascript_sync_rpc_base64_arg(args, index, label).map(Some)
15215}
15216
15217fn javascript_sync_rpc_json_arg_optional(
15218    args: &[Value],
15219    index: usize,
15220    label: &str,
15221) -> Result<Option<Value>, SidecarError> {
15222    if args.get(index).is_none() || args[index].is_null() {
15223        return Ok(None);
15224    }
15225    let raw = javascript_sync_rpc_arg_str(args, index, label)?;
15226    serde_json::from_str(raw)
15227        .map(Some)
15228        .map_err(|error| SidecarError::InvalidState(format!("{label} must be valid JSON: {error}")))
15229}
15230
15231fn javascript_crypto_parse_direct_key_input(
15232    raw: &str,
15233    expected: Option<&str>,
15234    label: &str,
15235) -> Result<JavascriptDirectKeyInput, SidecarError> {
15236    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
15237        SidecarError::InvalidState(format!("{label} must be valid JSON: {error}"))
15238    })?;
15239    let padding = match parsed.as_object().and_then(|value| value.get("padding")) {
15240        Some(value) => javascript_crypto_padding_from_value(value)?,
15241        None => None,
15242    };
15243    Ok(JavascriptDirectKeyInput {
15244        key: javascript_crypto_parse_key_material_value(&parsed, expected, label)?,
15245        padding,
15246    })
15247}
15248
15249fn javascript_crypto_parse_key_material_value(
15250    value: &Value,
15251    expected: Option<&str>,
15252    label: &str,
15253) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15254    if let Some(object) = value.as_object() {
15255        if object.get("__type").and_then(Value::as_str) == Some("keyObject") {
15256            let serialized = object.get("value").ok_or_else(|| {
15257                SidecarError::InvalidState(format!("{label} keyObject is missing a value"))
15258            })?;
15259            return javascript_crypto_parse_serialized_key_object(serialized, expected, label);
15260        }
15261        if object.contains_key("type") && (object.contains_key("pem") || object.contains_key("raw"))
15262        {
15263            return javascript_crypto_parse_serialized_key_object(value, expected, label);
15264        }
15265        if let Some(source) = object.get("key") {
15266            return javascript_crypto_parse_key_source(
15267                source,
15268                object.get("format").and_then(Value::as_str),
15269                object.get("type").and_then(Value::as_str),
15270                expected,
15271                label,
15272            );
15273        }
15274    }
15275    javascript_crypto_parse_key_source(value, None, None, expected, label)
15276}
15277
15278fn javascript_crypto_parse_key_source(
15279    source: &Value,
15280    format: Option<&str>,
15281    kind: Option<&str>,
15282    expected: Option<&str>,
15283    label: &str,
15284) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15285    match source {
15286        Value::String(pem) => javascript_crypto_parse_key_from_pem(pem.as_bytes(), expected, label),
15287        Value::Object(object) if object.get("__type").and_then(Value::as_str) == Some("buffer") => {
15288            let data = javascript_crypto_decode_bridge_buffer(source, label)?;
15289            javascript_crypto_parse_key_from_bytes(&data, format, kind, expected, label)
15290        }
15291        Value::Object(_) => {
15292            if format == Some("jwk") {
15293                return Err(SidecarError::InvalidState(format!(
15294                    "{label} jwk inputs are not supported yet"
15295                )));
15296            }
15297            Err(SidecarError::InvalidState(format!(
15298                "{label} has an unsupported key shape"
15299            )))
15300        }
15301        _ => Err(SidecarError::InvalidState(format!(
15302            "{label} has an unsupported key value"
15303        ))),
15304    }
15305}
15306
15307fn javascript_crypto_parse_key_from_pem(
15308    pem: &[u8],
15309    expected: Option<&str>,
15310    label: &str,
15311) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15312    match expected {
15313        Some("private") => PKey::private_key_from_pem(pem)
15314            .map(JavascriptCryptoKeyMaterial::Private)
15315            .map_err(|error| {
15316                SidecarError::InvalidState(format!("{label} private key is invalid: {error}"))
15317            }),
15318        Some("public") => PKey::public_key_from_pem(pem)
15319            .map(JavascriptCryptoKeyMaterial::Public)
15320            .map_err(|error| {
15321                SidecarError::InvalidState(format!("{label} public key is invalid: {error}"))
15322            }),
15323        _ => PKey::private_key_from_pem(pem)
15324            .map(JavascriptCryptoKeyMaterial::Private)
15325            .or_else(|_| PKey::public_key_from_pem(pem).map(JavascriptCryptoKeyMaterial::Public))
15326            .map_err(|error| {
15327                SidecarError::InvalidState(format!("{label} PEM key is invalid: {error}"))
15328            }),
15329    }
15330}
15331
15332fn javascript_crypto_parse_key_from_bytes(
15333    der: &[u8],
15334    format: Option<&str>,
15335    kind: Option<&str>,
15336    expected: Option<&str>,
15337    label: &str,
15338) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15339    match (format.unwrap_or("der"), kind.or(expected)) {
15340        ("der", Some("pkcs8")) | ("der", Some("private")) => PKey::private_key_from_der(der)
15341            .map(JavascriptCryptoKeyMaterial::Private)
15342            .map_err(|error| {
15343                SidecarError::InvalidState(format!("{label} private key DER is invalid: {error}"))
15344            }),
15345        ("der", Some("spki")) | ("der", Some("public")) => PKey::public_key_from_der(der)
15346            .map(JavascriptCryptoKeyMaterial::Public)
15347            .map_err(|error| {
15348                SidecarError::InvalidState(format!("{label} public key DER is invalid: {error}"))
15349            }),
15350        _ => Err(SidecarError::InvalidState(format!(
15351            "{label} unsupported key bytes format"
15352        ))),
15353    }
15354}
15355
15356fn javascript_crypto_parse_serialized_key_object(
15357    value: &Value,
15358    expected: Option<&str>,
15359    label: &str,
15360) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15361    let serialized: JavascriptSerializedSandboxKeyObject = serde_json::from_value(value.clone())
15362        .map_err(|error| {
15363            SidecarError::InvalidState(format!("{label} keyObject is invalid: {error}"))
15364        })?;
15365    match serialized.kind.as_str() {
15366        "secret" => {
15367            if expected == Some("public") || expected == Some("private") {
15368                return Err(SidecarError::InvalidState(format!(
15369                    "{label} expected an asymmetric key"
15370                )));
15371            }
15372            Ok(JavascriptCryptoKeyMaterial::Secret(
15373                base64::engine::general_purpose::STANDARD
15374                    .decode(serialized.raw.unwrap_or_default())
15375                    .map_err(|error| {
15376                        SidecarError::InvalidState(format!(
15377                            "{label} secret key contains invalid base64: {error}"
15378                        ))
15379                    })?,
15380            ))
15381        }
15382        "private" => {
15383            let pem = serialized.pem.ok_or_else(|| {
15384                SidecarError::InvalidState(format!("{label} private keyObject is missing pem"))
15385            })?;
15386            javascript_crypto_parse_key_from_pem(pem.as_bytes(), Some("private"), label)
15387        }
15388        "public" => {
15389            let pem = serialized.pem.ok_or_else(|| {
15390                SidecarError::InvalidState(format!("{label} public keyObject is missing pem"))
15391            })?;
15392            javascript_crypto_parse_key_from_pem(pem.as_bytes(), Some("public"), label)
15393        }
15394        other => Err(SidecarError::InvalidState(format!(
15395            "{label} has unsupported keyObject type {other}"
15396        ))),
15397    }
15398}
15399
15400fn javascript_crypto_expect_private_key(
15401    key: JavascriptCryptoKeyMaterial,
15402    label: &str,
15403) -> Result<PKey<Private>, SidecarError> {
15404    match key {
15405        JavascriptCryptoKeyMaterial::Private(key) => Ok(key),
15406        _ => Err(SidecarError::InvalidState(format!(
15407            "{label} requires a private key"
15408        ))),
15409    }
15410}
15411
15412fn javascript_crypto_expect_public_key(
15413    key: JavascriptCryptoKeyMaterial,
15414    label: &str,
15415) -> Result<PKey<Public>, SidecarError> {
15416    match key {
15417        JavascriptCryptoKeyMaterial::Public(key) => Ok(key),
15418        JavascriptCryptoKeyMaterial::Private(key) => {
15419            let pem = key
15420                .public_key_to_pem()
15421                .map_err(javascript_crypto_openssl_error)?;
15422            PKey::public_key_from_pem(&pem).map_err(javascript_crypto_openssl_error)
15423        }
15424        _ => Err(SidecarError::InvalidState(format!(
15425            "{label} requires a public key"
15426        ))),
15427    }
15428}
15429
15430fn javascript_crypto_new_signer<'a>(
15431    algorithm: Option<&'a str>,
15432    key: &'a PKey<Private>,
15433) -> Result<Signer<'a>, SidecarError> {
15434    if matches!(key.id(), PKeyId::ED25519 | PKeyId::ED448) || algorithm.is_none() {
15435        return Signer::new_without_digest(key).map_err(javascript_crypto_openssl_error);
15436    }
15437    Signer::new(
15438        javascript_crypto_message_digest_from_name(algorithm.ok_or_else(|| {
15439            SidecarError::InvalidState(String::from("crypto.sign requires a digest algorithm"))
15440        })?)?,
15441        key,
15442    )
15443    .map_err(javascript_crypto_openssl_error)
15444}
15445
15446fn javascript_crypto_new_verifier<'a>(
15447    algorithm: Option<&'a str>,
15448    key: &'a PKey<Public>,
15449) -> Result<Verifier<'a>, SidecarError> {
15450    if matches!(key.id(), PKeyId::ED25519 | PKeyId::ED448) || algorithm.is_none() {
15451        return Verifier::new_without_digest(key).map_err(javascript_crypto_openssl_error);
15452    }
15453    Verifier::new(
15454        javascript_crypto_message_digest_from_name(algorithm.ok_or_else(|| {
15455            SidecarError::InvalidState(String::from("crypto.verify requires a digest algorithm"))
15456        })?)?,
15457        key,
15458    )
15459    .map_err(javascript_crypto_openssl_error)
15460}
15461
15462fn javascript_crypto_message_digest_from_name(name: &str) -> Result<MessageDigest, SidecarError> {
15463    match name.trim().to_ascii_lowercase().replace('-', "").as_str() {
15464        "md5" => Ok(MessageDigest::md5()),
15465        "sha1" => Ok(MessageDigest::sha1()),
15466        "sha256" => Ok(MessageDigest::sha256()),
15467        "sha384" => Ok(MessageDigest::sha384()),
15468        "sha512" => Ok(MessageDigest::sha512()),
15469        other => Err(SidecarError::InvalidState(format!(
15470            "unsupported crypto digest algorithm {other}"
15471        ))),
15472    }
15473}
15474
15475fn javascript_crypto_padding_from_value(value: &Value) -> Result<Option<Padding>, SidecarError> {
15476    let Some(number) = value.as_i64() else {
15477        return Ok(None);
15478    };
15479    let padding = match number {
15480        1 => Padding::PKCS1,
15481        3 => Padding::NONE,
15482        4 => Padding::PKCS1_OAEP,
15483        6 => Padding::PKCS1_PSS,
15484        other => {
15485            return Err(SidecarError::InvalidState(format!(
15486                "unsupported RSA padding constant {other}"
15487            )));
15488        }
15489    };
15490    Ok(Some(padding))
15491}
15492
15493fn javascript_crypto_decode_bridge_buffer(
15494    value: &Value,
15495    label: &str,
15496) -> Result<Vec<u8>, SidecarError> {
15497    let base64_value = value
15498        .as_object()
15499        .filter(|object| object.get("__type").and_then(Value::as_str) == Some("buffer"))
15500        .and_then(|object| object.get("value"))
15501        .and_then(Value::as_str)
15502        .ok_or_else(|| {
15503            SidecarError::InvalidState(format!("{label} must be a serialized bridge buffer"))
15504        })?;
15505    base64::engine::general_purpose::STANDARD
15506        .decode(base64_value)
15507        .map_err(|error| {
15508            SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
15509        })
15510}
15511
15512fn javascript_crypto_serialize_sandbox_key_object(
15513    key: &JavascriptCryptoKeyMaterial,
15514) -> Result<Value, SidecarError> {
15515    let serialized = match key {
15516        JavascriptCryptoKeyMaterial::Private(key) => JavascriptSerializedSandboxKeyObject {
15517            kind: String::from("private"),
15518            pem: Some(
15519                String::from_utf8(
15520                    key.private_key_to_pem_pkcs8()
15521                        .map_err(javascript_crypto_openssl_error)?,
15522                )
15523                .map_err(|error| {
15524                    SidecarError::InvalidState(format!("private key PEM is not utf8: {error}"))
15525                })?,
15526            ),
15527            raw: None,
15528            asymmetric_key_type: javascript_crypto_pkey_type_name(key.id()),
15529            asymmetric_key_details: None,
15530            jwk: None,
15531        },
15532        JavascriptCryptoKeyMaterial::Public(key) => JavascriptSerializedSandboxKeyObject {
15533            kind: String::from("public"),
15534            pem: Some(
15535                String::from_utf8(
15536                    key.public_key_to_pem()
15537                        .map_err(javascript_crypto_openssl_error)?,
15538                )
15539                .map_err(|error| {
15540                    SidecarError::InvalidState(format!("public key PEM is not utf8: {error}"))
15541                })?,
15542            ),
15543            raw: None,
15544            asymmetric_key_type: javascript_crypto_pkey_type_name(key.id()),
15545            asymmetric_key_details: None,
15546            jwk: None,
15547        },
15548        JavascriptCryptoKeyMaterial::Secret(raw) => JavascriptSerializedSandboxKeyObject {
15549            kind: String::from("secret"),
15550            pem: None,
15551            raw: Some(base64::engine::general_purpose::STANDARD.encode(raw)),
15552            asymmetric_key_type: None,
15553            asymmetric_key_details: None,
15554            jwk: None,
15555        },
15556    };
15557    serde_json::to_value(serialized)
15558        .map_err(|error| SidecarError::InvalidState(format!("serialize key object: {error}")))
15559}
15560
15561fn javascript_crypto_pkey_type_name(id: PKeyId) -> Option<String> {
15562    match id {
15563        PKeyId::RSA => Some(String::from("rsa")),
15564        PKeyId::EC => Some(String::from("ec")),
15565        PKeyId::ED25519 => Some(String::from("ed25519")),
15566        PKeyId::ED448 => Some(String::from("ed448")),
15567        PKeyId::X25519 => Some(String::from("x25519")),
15568        PKeyId::X448 => Some(String::from("x448")),
15569        PKeyId::DH => Some(String::from("dh")),
15570        _ => None,
15571    }
15572}
15573
15574fn javascript_crypto_rsa_output_size(
15575    key: &JavascriptCryptoKeyMaterial,
15576) -> Result<usize, SidecarError> {
15577    match key {
15578        JavascriptCryptoKeyMaterial::Private(key) => key
15579            .rsa()
15580            .map(|rsa| rsa.size() as usize)
15581            .map_err(javascript_crypto_openssl_error),
15582        JavascriptCryptoKeyMaterial::Public(key) => key
15583            .rsa()
15584            .map(|rsa| rsa.size() as usize)
15585            .map_err(javascript_crypto_openssl_error),
15586        JavascriptCryptoKeyMaterial::Secret(_) => Err(SidecarError::InvalidState(String::from(
15587            "RSA operations require an asymmetric key",
15588        ))),
15589    }
15590}
15591
15592fn javascript_crypto_parse_serialized_options_arg(
15593    args: &[Value],
15594    index: usize,
15595    label: &str,
15596) -> Result<Option<Value>, SidecarError> {
15597    let Some(raw) = args.get(index).and_then(Value::as_str) else {
15598        return Ok(None);
15599    };
15600    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
15601        SidecarError::InvalidState(format!("{label} must be valid JSON: {error}"))
15602    })?;
15603    if parsed.get("hasOptions").and_then(Value::as_bool) == Some(true) {
15604        Ok(parsed.get("options").cloned())
15605    } else {
15606        Ok(None)
15607    }
15608}
15609
15610fn javascript_crypto_u32_from_bridge_value(
15611    value: &Value,
15612    label: &str,
15613) -> Result<u32, SidecarError> {
15614    if let Some(number) = value.as_u64() {
15615        return u32::try_from(number)
15616            .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")));
15617    }
15618    let bytes = javascript_crypto_decode_bridge_buffer(value, label)?;
15619    if bytes.len() > 4 {
15620        return Err(SidecarError::InvalidState(format!(
15621            "{label} buffer is too large for u32"
15622        )));
15623    }
15624    Ok(bytes
15625        .into_iter()
15626        .fold(0_u32, |acc, byte| (acc << 8) | u32::from(byte)))
15627}
15628
15629fn javascript_crypto_bignum_from_bridge_value(
15630    value: &Value,
15631    label: &str,
15632) -> Result<BigNum, SidecarError> {
15633    if let Some(object) = value.as_object() {
15634        if object.get("__type").and_then(Value::as_str) == Some("bigint") {
15635            let decimal = object.get("value").and_then(Value::as_str).ok_or_else(|| {
15636                SidecarError::InvalidState(format!("{label} bigint is missing a value"))
15637            })?;
15638            return BigNum::from_dec_str(decimal).map_err(javascript_crypto_openssl_error);
15639        }
15640    }
15641    let bytes = javascript_crypto_decode_bridge_buffer(value, label)?;
15642    BigNum::from_slice(&bytes).map_err(javascript_crypto_openssl_error)
15643}
15644
15645fn javascript_crypto_curve_nid(name: &str) -> Result<Nid, SidecarError> {
15646    match name {
15647        "prime256v1" | "P-256" => Ok(Nid::X9_62_PRIME256V1),
15648        "secp384r1" | "P-384" => Ok(Nid::SECP384R1),
15649        "secp521r1" | "P-521" => Ok(Nid::SECP521R1),
15650        "secp256k1" => Ok(Nid::SECP256K1),
15651        other => Err(SidecarError::InvalidState(format!(
15652            "unsupported EC curve {other}"
15653        ))),
15654    }
15655}
15656
15657fn javascript_crypto_named_dh_group(name: &str) -> Result<Dh<Params>, SidecarError> {
15658    match name {
15659        "modp2" => Dh::get_1024_160().map_err(javascript_crypto_openssl_error),
15660        "modp14" | "modp15" | "modp16" | "modp17" | "modp18" => {
15661            Dh::get_2048_256().map_err(javascript_crypto_openssl_error)
15662        }
15663        other => Err(SidecarError::InvalidState(format!(
15664            "unsupported Diffie-Hellman group {other}"
15665        ))),
15666    }
15667}
15668
15669fn javascript_crypto_clone_dh_params(params: &Dh<Params>) -> Result<Dh<Params>, SidecarError> {
15670    Dh::from_pqg(
15671        params
15672            .prime_p()
15673            .to_owned()
15674            .map_err(javascript_crypto_openssl_error)?,
15675        params
15676            .prime_q()
15677            .map(|value| value.to_owned().map_err(javascript_crypto_openssl_error))
15678            .transpose()?,
15679        params
15680            .generator()
15681            .to_owned()
15682            .map_err(javascript_crypto_openssl_error)?,
15683    )
15684    .map_err(javascript_crypto_openssl_error)
15685}
15686
15687fn javascript_crypto_build_dh_params(args: &[Value]) -> Result<Dh<Params>, SidecarError> {
15688    let Some(first) = args.first() else {
15689        return Err(SidecarError::InvalidState(String::from(
15690            "Diffie-Hellman session args are required",
15691        )));
15692    };
15693    if let Some(bits) = first.as_u64() {
15694        let generator = args
15695            .get(1)
15696            .map(|value| javascript_crypto_u32_from_bridge_value(value, "Diffie-Hellman generator"))
15697            .transpose()?
15698            .unwrap_or(2);
15699        return Dh::generate_params(bits as u32, generator)
15700            .map_err(javascript_crypto_openssl_error);
15701    }
15702    let prime = javascript_crypto_bignum_from_bridge_value(first, "Diffie-Hellman prime")?;
15703    let generator = args
15704        .get(1)
15705        .map(|value| javascript_crypto_bignum_from_bridge_value(value, "Diffie-Hellman generator"))
15706        .transpose()?
15707        .unwrap_or(BigNum::from_u32(2).map_err(javascript_crypto_openssl_error)?);
15708    Dh::from_pqg(prime, None, generator).map_err(javascript_crypto_openssl_error)
15709}
15710
15711fn javascript_crypto_call_dh_session(
15712    session: &mut ActiveDhSession,
15713    method: &str,
15714    args: &[Value],
15715) -> Result<(Value, bool), SidecarError> {
15716    match method {
15717        "verifyError" => Ok((Value::Null, false)),
15718        "generateKeys" => {
15719            if session.key_pair.is_none() {
15720                session.key_pair = Some(
15721                    javascript_crypto_clone_dh_params(&session.params)?
15722                        .generate_key()
15723                        .map_err(javascript_crypto_openssl_error)?,
15724                );
15725            }
15726            let public = session
15727                .key_pair
15728                .as_ref()
15729                .expect("dh key pair")
15730                .public_key()
15731                .to_vec();
15732            Ok((javascript_crypto_bridge_buffer_value(&public), true))
15733        }
15734        "computeSecret" => {
15735            if session.key_pair.is_none() {
15736                session.key_pair = Some(
15737                    javascript_crypto_clone_dh_params(&session.params)?
15738                        .generate_key()
15739                        .map_err(javascript_crypto_openssl_error)?,
15740                );
15741            }
15742            let peer = javascript_crypto_bignum_from_bridge_value(
15743                args.first().ok_or_else(|| {
15744                    SidecarError::InvalidState(String::from(
15745                        "computeSecret requires peer public key",
15746                    ))
15747                })?,
15748                "Diffie-Hellman peer public key",
15749            )?;
15750            let secret = session
15751                .key_pair
15752                .as_ref()
15753                .expect("dh key pair")
15754                .compute_key(&peer)
15755                .map_err(javascript_crypto_openssl_error)?;
15756            Ok((javascript_crypto_bridge_buffer_value(&secret), true))
15757        }
15758        "getPrime" => Ok((
15759            javascript_crypto_bridge_buffer_value(&session.params.prime_p().to_vec()),
15760            true,
15761        )),
15762        "getGenerator" => Ok((
15763            javascript_crypto_bridge_buffer_value(&session.params.generator().to_vec()),
15764            true,
15765        )),
15766        "getPublicKey" => {
15767            if session.key_pair.is_none() {
15768                session.key_pair = Some(
15769                    javascript_crypto_clone_dh_params(&session.params)?
15770                        .generate_key()
15771                        .map_err(javascript_crypto_openssl_error)?,
15772                );
15773            }
15774            Ok((
15775                javascript_crypto_bridge_buffer_value(
15776                    &session
15777                        .key_pair
15778                        .as_ref()
15779                        .expect("dh key pair")
15780                        .public_key()
15781                        .to_vec(),
15782                ),
15783                true,
15784            ))
15785        }
15786        "getPrivateKey" => {
15787            if session.key_pair.is_none() {
15788                session.key_pair = Some(
15789                    javascript_crypto_clone_dh_params(&session.params)?
15790                        .generate_key()
15791                        .map_err(javascript_crypto_openssl_error)?,
15792                );
15793            }
15794            Ok((
15795                javascript_crypto_bridge_buffer_value(
15796                    &session
15797                        .key_pair
15798                        .as_ref()
15799                        .expect("dh key pair")
15800                        .private_key()
15801                        .to_vec(),
15802                ),
15803                true,
15804            ))
15805        }
15806        other => Err(SidecarError::InvalidState(format!(
15807            "Unsupported Diffie-Hellman method: {other}"
15808        ))),
15809    }
15810}
15811
15812fn javascript_crypto_call_ecdh_session(
15813    session: &mut ActiveEcdhSession,
15814    method: &str,
15815    args: &[Value],
15816) -> Result<(Value, bool), SidecarError> {
15817    let nid = javascript_crypto_curve_nid(&session.curve)?;
15818    let group = EcGroup::from_curve_name(nid).map_err(javascript_crypto_openssl_error)?;
15819    match method {
15820        "verifyError" => Ok((Value::Null, false)),
15821        "generateKeys" => {
15822            if session.key_pair.is_none() {
15823                session.key_pair =
15824                    Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15825            }
15826            let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
15827            let bytes = session
15828                .key_pair
15829                .as_ref()
15830                .expect("ecdh key pair")
15831                .public_key()
15832                .to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx)
15833                .map_err(javascript_crypto_openssl_error)?;
15834            Ok((javascript_crypto_bridge_buffer_value(&bytes), true))
15835        }
15836        "computeSecret" => {
15837            if session.key_pair.is_none() {
15838                session.key_pair =
15839                    Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15840            }
15841            let peer_bytes = javascript_crypto_decode_bridge_buffer(
15842                args.first().ok_or_else(|| {
15843                    SidecarError::InvalidState(String::from(
15844                        "computeSecret requires peer public key",
15845                    ))
15846                })?,
15847                "ECDH peer public key",
15848            )?;
15849            let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
15850            let peer_point = EcPoint::from_bytes(&group, &peer_bytes, &mut ctx)
15851                .map_err(javascript_crypto_openssl_error)?;
15852            let peer_key = EcKey::from_public_key(&group, &peer_point)
15853                .map_err(javascript_crypto_openssl_error)?;
15854            let private =
15855                PKey::from_ec_key(session.key_pair.as_ref().expect("ecdh key pair").to_owned())
15856                    .map_err(javascript_crypto_openssl_error)?;
15857            let peer = PKey::from_ec_key(peer_key).map_err(javascript_crypto_openssl_error)?;
15858            let mut deriver = Deriver::new(&private).map_err(javascript_crypto_openssl_error)?;
15859            deriver
15860                .set_peer(&peer)
15861                .map_err(javascript_crypto_openssl_error)?;
15862            let secret = deriver
15863                .derive_to_vec()
15864                .map_err(javascript_crypto_openssl_error)?;
15865            Ok((javascript_crypto_bridge_buffer_value(&secret), true))
15866        }
15867        "getPublicKey" => {
15868            if session.key_pair.is_none() {
15869                session.key_pair =
15870                    Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15871            }
15872            let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
15873            let bytes = session
15874                .key_pair
15875                .as_ref()
15876                .expect("ecdh key pair")
15877                .public_key()
15878                .to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx)
15879                .map_err(javascript_crypto_openssl_error)?;
15880            Ok((javascript_crypto_bridge_buffer_value(&bytes), true))
15881        }
15882        "getPrivateKey" => {
15883            if session.key_pair.is_none() {
15884                session.key_pair =
15885                    Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15886            }
15887            Ok((
15888                javascript_crypto_bridge_buffer_value(
15889                    &session
15890                        .key_pair
15891                        .as_ref()
15892                        .expect("ecdh key pair")
15893                        .private_key()
15894                        .to_vec(),
15895                ),
15896                true,
15897            ))
15898        }
15899        other => Err(SidecarError::InvalidState(format!(
15900            "Unsupported Diffie-Hellman method: {other}"
15901        ))),
15902    }
15903}
15904
15905fn javascript_crypto_serialize_encoded_key_value_public(
15906    key: &PKey<Public>,
15907    encoding: Option<&Value>,
15908) -> Result<Value, SidecarError> {
15909    if let Some(encoding) = encoding {
15910        let format = encoding
15911            .get("format")
15912            .and_then(Value::as_str)
15913            .unwrap_or("pem");
15914        return Ok(match format {
15915            "der" => json!({
15916                "kind": "buffer",
15917                "value": base64::engine::general_purpose::STANDARD
15918                    .encode(key.public_key_to_der().map_err(javascript_crypto_openssl_error)?),
15919            }),
15920            _ => json!({
15921                "kind": "string",
15922                "value": String::from_utf8(
15923                    key.public_key_to_pem().map_err(javascript_crypto_openssl_error)?,
15924                )
15925                .map_err(|error| SidecarError::InvalidState(format!("public key PEM utf8: {error}")))?,
15926            }),
15927        });
15928    }
15929    javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Public(
15930        key.to_owned(),
15931    ))
15932}
15933
15934fn javascript_crypto_serialize_encoded_key_value_private(
15935    key: &PKey<Private>,
15936    encoding: Option<&Value>,
15937) -> Result<Value, SidecarError> {
15938    if let Some(encoding) = encoding {
15939        let format = encoding
15940            .get("format")
15941            .and_then(Value::as_str)
15942            .unwrap_or("pem");
15943        return Ok(match format {
15944            "der" => json!({
15945                "kind": "buffer",
15946                "value": base64::engine::general_purpose::STANDARD
15947                    .encode(key.private_key_to_der().map_err(javascript_crypto_openssl_error)?),
15948            }),
15949            _ => json!({
15950                "kind": "string",
15951                "value": String::from_utf8(
15952                    key.private_key_to_pem_pkcs8().map_err(javascript_crypto_openssl_error)?,
15953                )
15954                .map_err(|error| SidecarError::InvalidState(format!("private key PEM utf8: {error}")))?,
15955            }),
15956        });
15957    }
15958    javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Private(
15959        key.to_owned(),
15960    ))
15961}
15962
15963fn javascript_crypto_bridge_buffer_value(bytes: &[u8]) -> Value {
15964    json!({
15965        "__type": "buffer",
15966        "value": base64::engine::general_purpose::STANDARD.encode(bytes),
15967    })
15968}
15969
15970fn javascript_crypto_build_cipher_context(
15971    algorithm: &str,
15972    key: &[u8],
15973    iv: Option<&[u8]>,
15974    decrypt: bool,
15975    options: Option<&Value>,
15976) -> Result<Crypter, SidecarError> {
15977    let cipher = javascript_crypto_cipher_from_name(algorithm)?;
15978    let mode = if decrypt {
15979        Mode::Decrypt
15980    } else {
15981        Mode::Encrypt
15982    };
15983    let mut context =
15984        Crypter::new(cipher, mode, key, iv).map_err(javascript_crypto_openssl_error)?;
15985    if let Some(auto_padding) = options
15986        .and_then(|value| value.get("autoPadding"))
15987        .and_then(Value::as_bool)
15988    {
15989        context.pad(auto_padding);
15990    }
15991    if javascript_crypto_is_aead(algorithm) {
15992        if let Some(aad) = options
15993            .and_then(|value| value.get("aad"))
15994            .and_then(Value::as_str)
15995        {
15996            context
15997                .aad_update(
15998                    &base64::engine::general_purpose::STANDARD
15999                        .decode(aad)
16000                        .map_err(|error| {
16001                            SidecarError::InvalidState(format!(
16002                                "cipher aad contains invalid base64: {error}"
16003                            ))
16004                        })?,
16005                )
16006                .map_err(javascript_crypto_openssl_error)?;
16007        }
16008        if decrypt {
16009            if let Some(auth_tag) = options
16010                .and_then(|value| value.get("authTag"))
16011                .and_then(Value::as_str)
16012            {
16013                let decoded = base64::engine::general_purpose::STANDARD
16014                    .decode(auth_tag)
16015                    .map_err(|error| {
16016                        SidecarError::InvalidState(format!(
16017                            "cipher authTag contains invalid base64: {error}"
16018                        ))
16019                    })?;
16020                context
16021                    .set_tag(&decoded)
16022                    .map_err(javascript_crypto_openssl_error)?;
16023            }
16024        }
16025    }
16026    Ok(context)
16027}
16028
16029fn javascript_crypto_requested_aead_tag_len(
16030    algorithm: &str,
16031    options: Option<&Value>,
16032) -> Result<usize, SidecarError> {
16033    if !javascript_crypto_is_aead(algorithm) {
16034        return Ok(0);
16035    }
16036    let requested = options
16037        .and_then(|value| value.get("authTagLength"))
16038        .and_then(Value::as_u64)
16039        .unwrap_or(javascript_crypto_aead_tag_len(algorithm) as u64);
16040    usize::try_from(requested).map_err(|_| {
16041        SidecarError::InvalidState(String::from("cipher authTagLength must fit within usize"))
16042    })
16043}
16044
16045fn javascript_crypto_cipher_update(
16046    context: &mut Crypter,
16047    data: &[u8],
16048) -> Result<Vec<u8>, SidecarError> {
16049    let mut output = vec![0_u8; data.len() + 32];
16050    let written = context
16051        .update(data, &mut output)
16052        .map_err(javascript_crypto_openssl_error)?;
16053    output.truncate(written);
16054    Ok(output)
16055}
16056
16057fn javascript_crypto_cipher_finalize(context: &mut Crypter) -> Result<Vec<u8>, SidecarError> {
16058    let mut output = vec![0_u8; 32];
16059    let written = context
16060        .finalize(&mut output)
16061        .map_err(javascript_crypto_openssl_error)?;
16062    output.truncate(written);
16063    Ok(output)
16064}
16065
16066fn javascript_crypto_cipher_from_name(name: &str) -> Result<Cipher, SidecarError> {
16067    match name.to_ascii_lowercase().as_str() {
16068        "aes-128-cbc" => Ok(Cipher::aes_128_cbc()),
16069        "aes-192-cbc" => Ok(Cipher::aes_192_cbc()),
16070        "aes-256-cbc" => Ok(Cipher::aes_256_cbc()),
16071        "aes-128-ctr" => Ok(Cipher::aes_128_ctr()),
16072        "aes-192-ctr" => Ok(Cipher::aes_192_ctr()),
16073        "aes-256-ctr" => Ok(Cipher::aes_256_ctr()),
16074        "aes-128-gcm" => Ok(Cipher::aes_128_gcm()),
16075        "aes-192-gcm" => Ok(Cipher::aes_192_gcm()),
16076        "aes-256-gcm" => Ok(Cipher::aes_256_gcm()),
16077        other => Err(SidecarError::InvalidState(format!(
16078            "unsupported crypto cipher algorithm {other}"
16079        ))),
16080    }
16081}
16082
16083fn javascript_crypto_is_aead(algorithm: &str) -> bool {
16084    algorithm.to_ascii_lowercase().ends_with("-gcm")
16085}
16086
16087fn javascript_crypto_aead_tag_len(_algorithm: &str) -> usize {
16088    16
16089}
16090
16091fn javascript_crypto_openssl_error(error: openssl::error::ErrorStack) -> SidecarError {
16092    SidecarError::Execution(format!("crypto operation failed: {error}"))
16093}
16094
16095fn service_javascript_kernel_stdin_sync_rpc(
16096    kernel: &mut SidecarKernel,
16097    process: &mut ActiveProcess,
16098    request: &JavascriptSyncRpcRequest,
16099) -> Result<Value, SidecarError> {
16100    let max_bytes =
16101        javascript_sync_rpc_arg_u64_optional(&request.args, 0, "__kernel_stdin_read max bytes")?
16102            .map(|value| value.clamp(1, DEFAULT_KERNEL_STDIN_READ_MAX_BYTES as u64) as usize)
16103            .unwrap_or(DEFAULT_KERNEL_STDIN_READ_MAX_BYTES);
16104    let timeout_ms =
16105        javascript_sync_rpc_arg_u64_optional(&request.args, 1, "__kernel_stdin_read timeout ms")?
16106            .unwrap_or(DEFAULT_KERNEL_STDIN_READ_TIMEOUT_MS);
16107
16108    match kernel
16109        .fd_read_with_timeout_result(
16110            EXECUTION_DRIVER_NAME,
16111            process.kernel_pid,
16112            0,
16113            max_bytes,
16114            Some(Duration::from_millis(timeout_ms)),
16115        )
16116        .map_err(kernel_error)
16117    {
16118        Ok(Some(chunk)) if !chunk.is_empty() => Ok(json!({
16119            "dataBase64": base64::engine::general_purpose::STANDARD.encode(chunk),
16120        })),
16121        Ok(Some(_)) => Ok(Value::Null),
16122        Ok(None) => Ok(json!({
16123            "done": true,
16124        })),
16125        Err(SidecarError::Kernel(error)) if error.starts_with("EAGAIN:") => Ok(Value::Null),
16126        Err(error) => Err(error),
16127    }
16128}
16129
16130fn service_javascript_pty_set_raw_mode_sync_rpc(
16131    kernel: &mut SidecarKernel,
16132    process: &mut ActiveProcess,
16133    request: &JavascriptSyncRpcRequest,
16134) -> Result<Value, SidecarError> {
16135    let enabled = javascript_sync_rpc_arg_bool(&request.args, 0, "__pty_set_raw_mode enabled")?;
16136    kernel
16137        .pty_set_discipline(
16138            EXECUTION_DRIVER_NAME,
16139            process.kernel_pid,
16140            0,
16141            LineDisciplineConfig {
16142                canonical: Some(!enabled),
16143                echo: Some(!enabled),
16144                isig: Some(!enabled),
16145            },
16146        )
16147        .map_err(kernel_error)?;
16148    Ok(Value::Null)
16149}
16150
16151fn service_javascript_kernel_stdio_write_sync_rpc(
16152    kernel: &mut SidecarKernel,
16153    process: &mut ActiveProcess,
16154    request: &JavascriptSyncRpcRequest,
16155) -> Result<Value, SidecarError> {
16156    let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_stdio_write fd")?;
16157    let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "__kernel_stdio_write chunk")?;
16158
16159    let written = match fd {
16160        1 => kernel
16161            .write_process_stdout(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
16162            .map_err(kernel_error)?,
16163        2 => kernel
16164            .write_process_stderr(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
16165            .map_err(kernel_error)?,
16166        other => {
16167            return Err(SidecarError::InvalidState(format!(
16168                "__kernel_stdio_write only supports fd 1/2, got {other}"
16169            )));
16170        }
16171    };
16172
16173    let event = if fd == 1 {
16174        ActiveExecutionEvent::Stdout(chunk)
16175    } else {
16176        ActiveExecutionEvent::Stderr(chunk)
16177    };
16178    process.queue_pending_execution_event(event)?;
16179
16180    Ok(json!(written))
16181}
16182
16183fn service_javascript_kernel_poll_sync_rpc(
16184    kernel: &mut SidecarKernel,
16185    process: &ActiveProcess,
16186    request: &JavascriptSyncRpcRequest,
16187) -> Result<Value, SidecarError> {
16188    let fd_requests: Vec<KernelPollFdRequest> = serde_json::from_value(
16189        request
16190            .args
16191            .first()
16192            .cloned()
16193            .unwrap_or_else(|| Value::Array(Vec::new())),
16194    )
16195    .map_err(|error| {
16196        SidecarError::InvalidState(format!(
16197            "__kernel_poll fd list must be a JSON array of {{ fd, events }} objects: {error}"
16198        ))
16199    })?;
16200    let timeout_ms =
16201        javascript_sync_rpc_arg_u64_optional(&request.args, 1, "__kernel_poll timeout ms")?
16202            .unwrap_or_default();
16203    let timeout_ms = i32::try_from(timeout_ms).map_err(|_| {
16204        SidecarError::InvalidState(String::from("__kernel_poll timeout ms must fit within i32"))
16205    })?;
16206
16207    let poll_fds = fd_requests
16208        .iter()
16209        .map(|entry| PollFd {
16210            fd: entry.fd,
16211            events: PollEvents::from_bits(entry.events),
16212            revents: PollEvents::empty(),
16213        })
16214        .collect::<Vec<_>>();
16215    let result = kernel
16216        .poll_fds(
16217            EXECUTION_DRIVER_NAME,
16218            process.kernel_pid,
16219            poll_fds,
16220            timeout_ms,
16221        )
16222        .map_err(kernel_error)?;
16223
16224    Ok(json!({
16225        "readyCount": result.ready_count,
16226        "fds": result
16227            .fds
16228            .into_iter()
16229            .map(|entry| KernelPollFdResponse {
16230                fd: entry.fd,
16231                events: entry.events.bits(),
16232                revents: entry.revents.bits(),
16233            })
16234            .collect::<Vec<_>>(),
16235    }))
16236}
16237
16238fn install_kernel_stdin_pipe(kernel: &mut SidecarKernel, pid: u32) -> Result<u32, SidecarError> {
16239    let (read_fd, write_fd) = kernel
16240        .open_pipe(EXECUTION_DRIVER_NAME, pid)
16241        .map_err(kernel_error)?;
16242    kernel
16243        .fd_dup2(EXECUTION_DRIVER_NAME, pid, read_fd, 0)
16244        .map_err(kernel_error)?;
16245    kernel
16246        .fd_close(EXECUTION_DRIVER_NAME, pid, read_fd)
16247        .map_err(kernel_error)?;
16248    Ok(write_fd)
16249}
16250
16251fn javascript_child_process_stdin_mode(request: &JavascriptChildProcessSpawnRequest) -> &str {
16252    request
16253        .options
16254        .stdio
16255        .first()
16256        .map(String::as_str)
16257        .unwrap_or("pipe")
16258}
16259
16260pub(crate) fn write_kernel_process_stdin(
16261    kernel: &mut SidecarKernel,
16262    process: &mut ActiveProcess,
16263    chunk: &[u8],
16264) -> Result<(), SidecarError> {
16265    if process.runtime == GuestRuntimeKind::JavaScript {
16266        return Ok(());
16267    }
16268    let Some(writer_fd) = process.kernel_stdin_writer_fd else {
16269        return Ok(());
16270    };
16271    kernel
16272        .fd_write(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd, chunk)
16273        .map(|_| ())
16274        .map_err(kernel_error)
16275}
16276
16277pub(crate) fn close_kernel_process_stdin(
16278    kernel: &mut SidecarKernel,
16279    process: &mut ActiveProcess,
16280) -> Result<(), SidecarError> {
16281    let Some(writer_fd) = process.kernel_stdin_writer_fd.take() else {
16282        return Ok(());
16283    };
16284    kernel
16285        .fd_close(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd)
16286        .map_err(kernel_error)
16287}
16288
16289fn parse_http_header_collection(
16290    headers: &BTreeMap<String, Value>,
16291    label: &str,
16292) -> Result<HttpHeaderCollection, SidecarError> {
16293    let mut normalized = BTreeMap::<String, Vec<String>>::new();
16294    let mut raw_pairs = Vec::new();
16295
16296    for (raw_name, value) in headers {
16297        let normalized_name = raw_name.to_ascii_lowercase();
16298        let values = match value {
16299            Value::String(text) => vec![text.clone()],
16300            Value::Array(values) => values
16301                .iter()
16302                .map(|entry| {
16303                    entry.as_str().map(str::to_owned).ok_or_else(|| {
16304                        SidecarError::InvalidState(format!(
16305                            "{label} header {raw_name} must contain only strings"
16306                        ))
16307                    })
16308                })
16309                .collect::<Result<Vec<_>, _>>()?,
16310            other => {
16311                return Err(SidecarError::InvalidState(format!(
16312                    "{label} header {raw_name} must be a string or string array, received {other}"
16313                )));
16314            }
16315        };
16316        raw_pairs.extend(
16317            values
16318                .iter()
16319                .cloned()
16320                .map(|entry| (raw_name.clone(), entry)),
16321        );
16322        normalized
16323            .entry(normalized_name)
16324            .or_default()
16325            .extend(values);
16326    }
16327
16328    Ok(HttpHeaderCollection {
16329        normalized,
16330        raw_pairs,
16331    })
16332}
16333
16334fn http_headers_json(headers: &HttpHeaderCollection) -> Value {
16335    let map = headers
16336        .normalized
16337        .iter()
16338        .map(|(name, values)| {
16339            let value = if values.len() == 1 {
16340                Value::String(values[0].clone())
16341            } else {
16342                Value::Array(values.iter().cloned().map(Value::String).collect())
16343            };
16344            (name.clone(), value)
16345        })
16346        .collect::<Map<String, Value>>();
16347    Value::Object(map)
16348}
16349
16350fn http_raw_headers_json(headers: &HttpHeaderCollection) -> Value {
16351    Value::Array(
16352        headers
16353            .raw_pairs
16354            .iter()
16355            .flat_map(|(name, value)| [Value::String(name.clone()), Value::String(value.clone())])
16356            .collect(),
16357    )
16358}
16359
16360fn is_loopback_request_host(host: &str) -> bool {
16361    let bare = host
16362        .strip_prefix('[')
16363        .and_then(|value| value.strip_suffix(']'))
16364        .unwrap_or(host);
16365    matches!(bare, "localhost" | "127.0.0.1" | "::1")
16366}
16367
16368fn serialize_http_loopback_request(
16369    url: &Url,
16370    options: &JavascriptHttpRequestOptions,
16371    headers: &HttpHeaderCollection,
16372) -> Result<String, SidecarError> {
16373    let body_base64 = options
16374        .body
16375        .as_ref()
16376        .map(|body| base64::engine::general_purpose::STANDARD.encode(body.as_bytes()));
16377    serde_json::to_string(&json!({
16378        "method": options.method.clone().unwrap_or_else(|| String::from("GET")),
16379        "url": http_request_target(url),
16380        "headers": http_headers_json(headers),
16381        "rawHeaders": http_raw_headers_json(headers),
16382        "bodyBase64": body_base64,
16383    }))
16384    .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
16385}
16386
16387fn http_request_target(url: &Url) -> String {
16388    let path = if url.path().is_empty() {
16389        "/"
16390    } else {
16391        url.path()
16392    };
16393    format!(
16394        "{path}{}",
16395        url.query()
16396            .map(|query| format!("?{query}"))
16397            .unwrap_or_default()
16398    )
16399}
16400
16401fn find_kernel_http_listener_process(vm: &VmState, port: u16) -> Option<String> {
16402    vm.active_processes
16403        .iter()
16404        .find_map(|(process_id, process)| {
16405            process.tcp_listeners.values().find_map(|listener| {
16406                let socket_id = listener.kernel_socket_id?;
16407                let record = vm.kernel.socket_get(socket_id)?;
16408                let local_addr = record
16409                    .local_address()
16410                    .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
16411                    .unwrap_or_else(|| listener.guest_local_addr());
16412                if local_addr.port() == port && is_vm_local_http_listener_addr(local_addr.ip()) {
16413                    Some(process_id.to_owned())
16414                } else {
16415                    None
16416                }
16417            })
16418        })
16419}
16420
16421fn is_vm_local_http_listener_addr(ip: IpAddr) -> bool {
16422    ip.is_loopback() || ip.is_unspecified()
16423}
16424
16425fn serialize_kernel_http_fetch_request(
16426    port: u16,
16427    path: &str,
16428    options: &JavascriptHttpRequestOptions,
16429    headers: &HttpHeaderCollection,
16430) -> Vec<u8> {
16431    let method = options.method.as_deref().unwrap_or("GET");
16432    let mut lines = vec![format!("{method} {path} HTTP/1.1")];
16433    let mut has_host = false;
16434    let mut has_connection = false;
16435    let mut has_content_length = false;
16436    for (name, values) in &headers.normalized {
16437        match name.as_str() {
16438            "host" => has_host = true,
16439            "connection" => has_connection = true,
16440            "content-length" => has_content_length = true,
16441            _ => {}
16442        }
16443        lines.push(format!("{name}: {}", values.join(", ")));
16444    }
16445    if !has_host {
16446        lines.push(format!("Host: 127.0.0.1:{port}"));
16447    }
16448    if !has_connection {
16449        lines.push(String::from("Connection: close"));
16450    }
16451    let body = options.body.as_deref().unwrap_or("").as_bytes();
16452    if !has_content_length && !body.is_empty() {
16453        lines.push(format!("Content-Length: {}", body.len()));
16454    }
16455    lines.push(String::new());
16456    lines.push(String::new());
16457
16458    let mut request = lines.join("\r\n").into_bytes();
16459    request.extend_from_slice(body);
16460    request
16461}
16462
16463fn parse_kernel_http_fetch_response(
16464    buffer: &[u8],
16465    peer_closed: bool,
16466    url: &str,
16467) -> Result<Option<String>, SidecarError> {
16468    let Some(header_end) = find_http_header_end(buffer) else {
16469        return Ok(None);
16470    };
16471    let header_bytes = &buffer[..header_end];
16472    let head = String::from_utf8_lossy(header_bytes);
16473    let mut lines = head.split("\r\n");
16474    let status_line = lines.next().unwrap_or_default();
16475    let mut status_parts = status_line.splitn(3, ' ');
16476    let version = status_parts.next().unwrap_or_default();
16477    if !version.starts_with("HTTP/") {
16478        return Err(SidecarError::Execution(format!(
16479            "invalid vm.fetch HTTP response status line: {status_line}"
16480        )));
16481    }
16482    let status = status_parts
16483        .next()
16484        .ok_or_else(|| {
16485            SidecarError::Execution(format!(
16486                "invalid vm.fetch HTTP response status line: {status_line}"
16487            ))
16488        })?
16489        .parse::<u16>()
16490        .map_err(|error| {
16491            SidecarError::Execution(format!(
16492                "invalid vm.fetch HTTP response status code in {status_line:?}: {error}"
16493            ))
16494        })?;
16495    let status_text = status_parts.next().unwrap_or_default();
16496    let mut headers = Vec::new();
16497    let mut raw_headers = Vec::new();
16498    let mut content_length = None;
16499    let mut transfer_encoding_values = Vec::new();
16500    for line in lines {
16501        if line.is_empty() {
16502            continue;
16503        }
16504        let Some((name, value)) = line.split_once(':') else {
16505            return Err(SidecarError::Execution(format!(
16506                "invalid vm.fetch HTTP response header line: {line}"
16507            )));
16508        };
16509        let value = value.trim().to_owned();
16510        let normalized = name.to_ascii_lowercase();
16511        if normalized == "content-length" {
16512            content_length = Some(value.parse::<usize>().map_err(|error| {
16513                SidecarError::Execution(format!(
16514                    "invalid vm.fetch Content-Length header {value:?}: {error}"
16515                ))
16516            })?);
16517        } else if normalized == "transfer-encoding" {
16518            transfer_encoding_values.push(value.clone());
16519        }
16520        headers.push(json!([normalized, value.clone()]));
16521        raw_headers.push(Value::String(name.to_owned()));
16522        raw_headers.push(Value::String(value));
16523    }
16524
16525    let body_start = header_end + 4;
16526    let transfer_encoding = transfer_encoding_tokens(&transfer_encoding_values);
16527    let is_chunked = transfer_encoding.iter().any(|token| token == "chunked");
16528    let body = if is_chunked {
16529        if content_length.is_some() {
16530            return Err(SidecarError::Execution(String::from(
16531                "vm.fetch HTTP response cannot include both Transfer-Encoding: chunked and Content-Length",
16532            )));
16533        }
16534        if transfer_encoding.len() != 1 {
16535            return Err(SidecarError::Execution(format!(
16536                "unsupported vm.fetch Transfer-Encoding: {}",
16537                transfer_encoding.join(", ")
16538            )));
16539        }
16540        let Some(decoded) = decode_kernel_http_chunked_body(&buffer[body_start..])? else {
16541            return Ok(None);
16542        };
16543        decoded
16544    } else if !transfer_encoding.is_empty() {
16545        return Err(SidecarError::Execution(format!(
16546            "unsupported vm.fetch Transfer-Encoding: {}",
16547            transfer_encoding.join(", ")
16548        )));
16549    } else if let Some(content_length) = content_length {
16550        let body_end = body_start.saturating_add(content_length);
16551        if buffer.len() < body_end {
16552            return Ok(None);
16553        }
16554        buffer[body_start..body_end].to_vec()
16555    } else if peer_closed {
16556        buffer[body_start..].to_vec()
16557    } else {
16558        return Ok(None);
16559    };
16560
16561    serde_json::to_string(&json!({
16562        "status": status,
16563        "statusText": status_text,
16564        "headers": headers,
16565        "rawHeaders": raw_headers,
16566        "body": base64::engine::general_purpose::STANDARD.encode(&body),
16567        "bodyEncoding": "base64",
16568        "url": url,
16569    }))
16570    .map(Some)
16571    .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
16572}
16573
16574fn find_http_header_end(buffer: &[u8]) -> Option<usize> {
16575    buffer.windows(4).position(|window| window == b"\r\n\r\n")
16576}
16577
16578fn find_crlf(buffer: &[u8], start: usize) -> Option<usize> {
16579    buffer
16580        .get(start..)?
16581        .windows(2)
16582        .position(|window| window == b"\r\n")
16583        .map(|offset| start + offset)
16584}
16585
16586fn transfer_encoding_tokens(values: &[String]) -> Vec<String> {
16587    values
16588        .iter()
16589        .flat_map(|value| value.split(','))
16590        .map(|token| token.trim().to_ascii_lowercase())
16591        .filter(|token| !token.is_empty())
16592        .collect()
16593}
16594
16595fn decode_kernel_http_chunked_body(buffer: &[u8]) -> Result<Option<Vec<u8>>, SidecarError> {
16596    let mut offset = 0;
16597    let mut body = Vec::new();
16598    loop {
16599        let Some(line_end) = find_crlf(buffer, offset) else {
16600            return Ok(None);
16601        };
16602        let size_line = std::str::from_utf8(&buffer[offset..line_end]).map_err(|error| {
16603            SidecarError::Execution(format!(
16604                "invalid vm.fetch chunk size line encoding: {error}"
16605            ))
16606        })?;
16607        let size_part = size_line.split(';').next().unwrap_or_default();
16608        if size_part.is_empty() || !size_part.bytes().all(|byte| byte.is_ascii_hexdigit()) {
16609            return Err(SidecarError::Execution(format!(
16610                "invalid vm.fetch chunk size line: {size_line:?}"
16611            )));
16612        }
16613        let chunk_size = usize::from_str_radix(size_part, 16).map_err(|error| {
16614            SidecarError::Execution(format!(
16615                "invalid vm.fetch chunk size {size_part:?}: {error}"
16616            ))
16617        })?;
16618        let chunk_start = line_end + 2;
16619        let chunk_end = chunk_start
16620            .checked_add(chunk_size)
16621            .ok_or_else(|| SidecarError::Execution(String::from("vm.fetch chunk size overflow")))?;
16622        if chunk_size > 0 {
16623            let chunk_terminator_end = chunk_end.checked_add(2).ok_or_else(|| {
16624                SidecarError::Execution(String::from("vm.fetch chunk terminator overflow"))
16625            })?;
16626            if chunk_terminator_end > buffer.len() {
16627                return Ok(None);
16628            }
16629            if buffer.get(chunk_end..chunk_terminator_end) != Some(b"\r\n") {
16630                return Err(SidecarError::Execution(String::from(
16631                    "invalid vm.fetch chunk terminator",
16632                )));
16633            }
16634            body.extend_from_slice(&buffer[chunk_start..chunk_end]);
16635            offset = chunk_terminator_end;
16636            continue;
16637        }
16638
16639        if buffer.get(chunk_start..chunk_start + 2) == Some(b"\r\n") {
16640            return Ok(Some(body));
16641        }
16642        let Some(trailer_end) = find_http_header_end(&buffer[chunk_start..]) else {
16643            return Ok(None);
16644        };
16645        let trailer_bytes = &buffer[chunk_start..chunk_start + trailer_end];
16646        let trailers = String::from_utf8_lossy(trailer_bytes);
16647        for line in trailers.split("\r\n") {
16648            if line.is_empty() {
16649                continue;
16650            }
16651            if line.starts_with(' ') || line.starts_with('\t') || !line.contains(':') {
16652                return Err(SidecarError::Execution(format!(
16653                    "invalid vm.fetch chunk trailer line: {line}"
16654                )));
16655            }
16656        }
16657        return Ok(Some(body));
16658    }
16659}
16660
16661fn kernel_http_fetch_target_exit_code(error: &SidecarError) -> Option<i32> {
16662    let SidecarError::Execution(message) = error else {
16663        return None;
16664    };
16665    message
16666        .strip_prefix("vm.fetch target exited before responding (exit code ")?
16667        .strip_suffix(')')?
16668        .parse()
16669        .ok()
16670}
16671
16672#[allow(clippy::too_many_arguments)]
16673fn service_host_fetch_target_event<B>(
16674    bridge: &SharedBridge<B>,
16675    vm_id: &str,
16676    dns: &VmDnsConfig,
16677    socket_paths: &JavascriptSocketPathContext,
16678    kernel: &mut SidecarKernel,
16679    process: &mut ActiveProcess,
16680    resource_limits: &ResourceLimits,
16681    wait: Duration,
16682) -> Result<bool, SidecarError>
16683where
16684    B: NativeSidecarBridge + Send + 'static,
16685    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16686{
16687    let Some(event) = process
16688        .execution
16689        .poll_event_blocking(wait)
16690        .map_err(|error| SidecarError::Execution(error.to_string()))?
16691    else {
16692        return Ok(false);
16693    };
16694
16695    match event {
16696        ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
16697            let network_counts = process.network_resource_counts();
16698            let response = service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
16699                bridge,
16700                vm_id,
16701                dns,
16702                socket_paths,
16703                kernel,
16704                process,
16705                sync_request: &request,
16706                resource_limits,
16707                network_counts,
16708            });
16709            match response {
16710                Ok(result) => process
16711                    .execution
16712                    .respond_javascript_sync_rpc_success(request.id, result)
16713                    .or_else(ignore_stale_javascript_sync_rpc_response)?,
16714                Err(error) => process
16715                    .execution
16716                    .respond_javascript_sync_rpc_error(
16717                        request.id,
16718                        javascript_sync_rpc_error_code(&error),
16719                        error.to_string(),
16720                    )
16721                    .or_else(ignore_stale_javascript_sync_rpc_response)?,
16722            }
16723        }
16724        ActiveExecutionEvent::Exited(code) => {
16725            return Err(SidecarError::Execution(format!(
16726                "vm.fetch target exited before responding (exit code {code})"
16727            )));
16728        }
16729        other => {
16730            process.queue_pending_execution_event(other)?;
16731        }
16732    }
16733    Ok(true)
16734}
16735
16736fn drain_host_fetch_target_events<B>(
16737    bridge: &SharedBridge<B>,
16738    vm_id: &str,
16739    vm: &mut VmState,
16740    target_process_id: &str,
16741    socket_paths: &JavascriptSocketPathContext,
16742    resource_limits: &ResourceLimits,
16743) -> Result<(), SidecarError>
16744where
16745    B: NativeSidecarBridge + Send + 'static,
16746    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16747{
16748    for _ in 0..32 {
16749        let dns = vm.dns.clone();
16750        let Some(process) = vm.active_processes.get_mut(target_process_id) else {
16751            break;
16752        };
16753        let serviced = service_host_fetch_target_event(
16754            bridge,
16755            vm_id,
16756            &dns,
16757            socket_paths,
16758            &mut vm.kernel,
16759            process,
16760            resource_limits,
16761            Duration::from_millis(1),
16762        )?;
16763        if !serviced {
16764            break;
16765        }
16766    }
16767    Ok(())
16768}
16769
16770#[allow(clippy::too_many_arguments)]
16771fn dispatch_kernel_http_fetch<B>(
16772    bridge: &SharedBridge<B>,
16773    vm_id: &str,
16774    vm: &mut VmState,
16775    target_process_id: &str,
16776    port: u16,
16777    path: &str,
16778    options: &JavascriptHttpRequestOptions,
16779    headers: &HttpHeaderCollection,
16780    max_fetch_response_bytes: usize,
16781) -> Result<String, SidecarError>
16782where
16783    B: NativeSidecarBridge + Send + 'static,
16784    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16785{
16786    let socket_paths = build_javascript_socket_path_context(vm)?;
16787    let family = JavascriptSocketFamily::Ipv4;
16788    let local_port = allocate_guest_listen_port(
16789        0,
16790        family,
16791        &socket_paths.used_tcp_guest_ports,
16792        socket_paths.listen_policy,
16793    )?;
16794    let resource_limits = vm.kernel.resource_limits().clone();
16795    let network_counts = vm_network_resource_counts(vm);
16796    check_network_resource_limit(
16797        resource_limits.max_sockets,
16798        network_counts.sockets,
16799        2,
16800        "socket",
16801    )?;
16802    check_network_resource_limit(
16803        resource_limits.max_connections,
16804        network_counts.connections,
16805        2,
16806        "connection",
16807    )?;
16808
16809    let kernel_pid = vm
16810        .active_processes
16811        .get(target_process_id)
16812        .ok_or_else(|| {
16813            SidecarError::InvalidState(format!(
16814                "vm.fetch target process disappeared: {target_process_id}"
16815            ))
16816        })?
16817        .kernel_pid;
16818    let socket_id = vm
16819        .kernel
16820        .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, SocketSpec::tcp())
16821        .map_err(kernel_error)?;
16822
16823    let result = dispatch_kernel_http_fetch_with_socket(
16824        bridge,
16825        vm_id,
16826        vm,
16827        target_process_id,
16828        kernel_pid,
16829        socket_id,
16830        local_port,
16831        port,
16832        path,
16833        options,
16834        headers,
16835        &socket_paths,
16836        &resource_limits,
16837        max_fetch_response_bytes,
16838    );
16839    let close_result = vm
16840        .kernel
16841        .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id)
16842        .map_err(kernel_error);
16843    let cleanup_result = if result.is_err() {
16844        drain_host_fetch_target_events(
16845            bridge,
16846            vm_id,
16847            vm,
16848            target_process_id,
16849            &socket_paths,
16850            &resource_limits,
16851        )
16852    } else {
16853        Ok(())
16854    };
16855    match (result, close_result) {
16856        (Ok(response), Ok(())) => cleanup_result.map(|()| response),
16857        (Err(error), _) => Err(error),
16858        (Ok(_), Err(error)) => Err(error),
16859    }
16860}
16861
16862#[allow(clippy::too_many_arguments)]
16863fn dispatch_kernel_http_fetch_with_socket<B>(
16864    bridge: &SharedBridge<B>,
16865    vm_id: &str,
16866    vm: &mut VmState,
16867    target_process_id: &str,
16868    kernel_pid: u32,
16869    socket_id: SocketId,
16870    local_port: u16,
16871    port: u16,
16872    path: &str,
16873    options: &JavascriptHttpRequestOptions,
16874    headers: &HttpHeaderCollection,
16875    socket_paths: &JavascriptSocketPathContext,
16876    resource_limits: &ResourceLimits,
16877    max_fetch_response_bytes: usize,
16878) -> Result<String, SidecarError>
16879where
16880    B: NativeSidecarBridge + Send + 'static,
16881    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16882{
16883    vm.kernel
16884        .socket_bind_inet(
16885            EXECUTION_DRIVER_NAME,
16886            kernel_pid,
16887            socket_id,
16888            InetSocketAddress::new("127.0.0.1", local_port),
16889        )
16890        .map_err(kernel_error)?;
16891    vm.kernel
16892        .socket_connect_inet_loopback(
16893            EXECUTION_DRIVER_NAME,
16894            kernel_pid,
16895            socket_id,
16896            InetSocketAddress::new("127.0.0.1", port),
16897        )
16898        .map_err(kernel_error)?;
16899
16900    let request_bytes = serialize_kernel_http_fetch_request(port, path, options, headers);
16901    vm.kernel
16902        .socket_write(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, &request_bytes)
16903        .map_err(kernel_error)?;
16904
16905    let mut response_buffer = Vec::new();
16906    let mut peer_closed = false;
16907    let url = format!("http://127.0.0.1:{port}{path}");
16908    let deadline = Instant::now() + http_loopback_request_timeout();
16909    loop {
16910        if let Some(response) =
16911            parse_kernel_http_fetch_response(&response_buffer, peer_closed, &url)?
16912        {
16913            ensure_vm_fetch_response_within_limit(&response, "vm.fetch", max_fetch_response_bytes)?;
16914            return Ok(response);
16915        }
16916        if Instant::now() >= deadline {
16917            let preview = String::from_utf8_lossy(&response_buffer);
16918            return Err(SidecarError::Execution(format!(
16919                "vm.fetch timed out waiting for kernel TCP HTTP response ({} buffered bytes: {:?})",
16920                response_buffer.len(),
16921                preview.chars().take(200).collect::<String>()
16922            )));
16923        }
16924
16925        {
16926            let dns = vm.dns.clone();
16927            let process = vm
16928                .active_processes
16929                .get_mut(target_process_id)
16930                .ok_or_else(|| {
16931                    SidecarError::InvalidState(format!(
16932                        "vm.fetch target process disappeared: {target_process_id}"
16933                    ))
16934                })?;
16935            service_host_fetch_target_event(
16936                bridge,
16937                vm_id,
16938                &dns,
16939                socket_paths,
16940                &mut vm.kernel,
16941                process,
16942                resource_limits,
16943                Duration::from_millis(5),
16944            )?;
16945        }
16946
16947        let poll = vm
16948            .kernel
16949            .poll_targets(
16950                EXECUTION_DRIVER_NAME,
16951                kernel_pid,
16952                vec![PollTargetEntry::socket(
16953                    socket_id,
16954                    POLLIN | POLLHUP | POLLERR,
16955                )],
16956                5,
16957            )
16958            .map_err(kernel_error)?;
16959        let revents = poll
16960            .targets
16961            .first()
16962            .map(|entry| entry.revents)
16963            .unwrap_or_else(PollEvents::empty);
16964        if revents.intersects(POLLERR) {
16965            return Err(SidecarError::Execution(String::from(
16966                "vm.fetch kernel TCP socket reported POLLERR",
16967            )));
16968        }
16969        if revents.intersects(POLLIN) {
16970            match vm
16971                .kernel
16972                .socket_read(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, 64 * 1024)
16973            {
16974                Ok(Some(bytes)) if !bytes.is_empty() => {
16975                    response_buffer.extend(bytes);
16976                    ensure_vm_fetch_raw_response_buffer_within_limit(
16977                        response_buffer.len(),
16978                        "vm.fetch",
16979                    )?;
16980                }
16981                Ok(Some(_)) => {}
16982                Ok(None) => peer_closed = true,
16983                Err(error) if error.code() == "EAGAIN" => {}
16984                Err(error) => return Err(kernel_error(error)),
16985            }
16986        }
16987        if revents.intersects(POLLHUP) {
16988            peer_closed = true;
16989        }
16990    }
16991}
16992
16993fn outbound_http_response_json(url: &Url, response: ureq::Response) -> Result<Value, SidecarError> {
16994    let status = response.status();
16995    let status_text = response.status_text().to_owned();
16996    let mut header_pairs = Vec::new();
16997    let mut raw_headers = Vec::new();
16998    for raw_name in response.headers_names() {
16999        for value in response.all(&raw_name) {
17000            header_pairs.push(json!([raw_name.to_ascii_lowercase(), value]));
17001            raw_headers.push(Value::String(raw_name.clone()));
17002            raw_headers.push(Value::String(value.to_owned()));
17003        }
17004    }
17005    let mut reader = response.into_reader();
17006    let mut body = Vec::new();
17007    reader.read_to_end(&mut body).map_err(|error| {
17008        SidecarError::Execution(format!("failed to read HTTP response: {error}"))
17009    })?;
17010    serde_json::to_string(&json!({
17011        "status": status,
17012        "statusText": status_text,
17013        "headers": header_pairs,
17014        "rawHeaders": raw_headers,
17015        "body": base64::engine::general_purpose::STANDARD.encode(body),
17016        "bodyEncoding": "base64",
17017        "url": url.as_str(),
17018    }))
17019    .map(Value::String)
17020    .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
17021}
17022
17023/// Split a ureq resolver `netloc` (`host:port`, with optional `[..]` IPv6
17024/// brackets) into its host and port components. Returns `None` if the port is
17025/// missing or unparseable.
17026fn split_netloc(netloc: &str) -> Option<(&str, u16)> {
17027    let (host, port) = netloc.rsplit_once(':')?;
17028    let port: u16 = port.parse().ok()?;
17029    let host = host
17030        .strip_prefix('[')
17031        .and_then(|rest| rest.strip_suffix(']'))
17032        .unwrap_or(host);
17033    Some((host, port))
17034}
17035
17036fn issue_outbound_http_request(
17037    url: &Url,
17038    options: &JavascriptHttpRequestOptions,
17039    headers: &HttpHeaderCollection,
17040    pinned_addresses: &[IpAddr],
17041) -> Result<Value, SidecarError> {
17042    let method = options.method.as_deref().unwrap_or("GET");
17043    // Pin the underlying resolver to the egress-vetted addresses. ureq performs
17044    // its own DNS resolution for the TCP/TLS connect; without this override an
17045    // https:// request would re-resolve the hostname through the host resolver
17046    // (a rebinding DNS server could then return a private/metadata IP that the
17047    // earlier range check would have rejected). The pinned resolver returns only
17048    // the vetted addresses and refuses any host it was not vetted for, while the
17049    // request URL keeps the original hostname so TLS SNI and the Host header stay
17050    // correct.
17051    let pinned_host = url.host_str().map(str::to_owned);
17052    let pinned: Vec<IpAddr> = pinned_addresses.to_vec();
17053    let resolver = move |netloc: &str| -> std::io::Result<Vec<SocketAddr>> {
17054        let (host, port) = split_netloc(netloc).ok_or_else(|| {
17055            std::io::Error::new(
17056                std::io::ErrorKind::InvalidInput,
17057                format!("invalid network location: {netloc}"),
17058            )
17059        })?;
17060        let expected_host = pinned_host.as_deref();
17061        if expected_host != Some(host) {
17062            return Err(std::io::Error::new(
17063                std::io::ErrorKind::PermissionDenied,
17064                format!(
17065                    "EACCES: outbound HTTP resolver pinned to {expected_host:?}, refusing {host}"
17066                ),
17067            ));
17068        }
17069        if pinned.is_empty() {
17070            return Err(std::io::Error::new(
17071                std::io::ErrorKind::PermissionDenied,
17072                "EACCES: no egress-vetted address available for outbound HTTP request",
17073            ));
17074        }
17075        Ok(pinned.iter().map(|ip| SocketAddr::new(*ip, port)).collect())
17076    };
17077    let mut agent_builder = ureq::AgentBuilder::new()
17078        .resolver(resolver)
17079        .timeout_connect(Duration::from_secs(5))
17080        .timeout_read(Duration::from_secs(15))
17081        .timeout_write(Duration::from_secs(15));
17082    if url.scheme() == "https" {
17083        let tls_options = JavascriptTlsBridgeOptions {
17084            is_server: false,
17085            servername: url.host_str().map(str::to_owned),
17086            alpn_protocols: Some(vec![String::from("http/1.1")]),
17087            reject_unauthorized: options.reject_unauthorized,
17088            ..JavascriptTlsBridgeOptions::default()
17089        };
17090        agent_builder = agent_builder.tls_config(Arc::new(build_client_tls_config(&tls_options)?));
17091    }
17092    let agent = agent_builder.build();
17093    let mut request = agent.request_url(method, url);
17094    for (name, values) in &headers.normalized {
17095        if name == "host" {
17096            continue;
17097        }
17098        let header_value = values.join(", ");
17099        request = request.set(name, &header_value);
17100    }
17101    let response = match options.body.as_deref() {
17102        Some(body) => request.send_string(body),
17103        None => request.call(),
17104    };
17105
17106    match response {
17107        Ok(response) => outbound_http_response_json(url, response),
17108        Err(ureq::Error::Status(_, response)) => outbound_http_response_json(url, response),
17109        Err(ureq::Error::Transport(error)) => Err(SidecarError::Execution(format!(
17110            "ERR_HTTP_REQUEST_FAILED: {error}"
17111        ))),
17112    }
17113}
17114
17115fn wait_for_loopback_http_response<B>(
17116    request: LoopbackHttpResponseWaitRequest<'_, B>,
17117) -> Result<String, SidecarError>
17118where
17119    B: NativeSidecarBridge + Send + 'static,
17120    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17121{
17122    let LoopbackHttpResponseWaitRequest {
17123        bridge,
17124        vm_id,
17125        dns,
17126        socket_paths,
17127        kernel,
17128        process,
17129        resource_limits,
17130        request_key,
17131    } = request;
17132    let deadline = Instant::now() + http_loopback_request_timeout();
17133    loop {
17134        if let Some(response) = process
17135            .pending_http_requests
17136            .get(&request_key)
17137            .and_then(|response| response.clone())
17138        {
17139            process.pending_http_requests.remove(&request_key);
17140            return Ok(response);
17141        }
17142
17143        if Instant::now() >= deadline {
17144            process.pending_http_requests.remove(&request_key);
17145            return Err(SidecarError::Execution(String::from(
17146                "HTTP loopback request timed out waiting for net.http_respond",
17147            )));
17148        }
17149
17150        let Some(event) = process
17151            .execution
17152            .poll_event_blocking(Duration::from_millis(10))
17153            .map_err(|error| SidecarError::Execution(error.to_string()))?
17154        else {
17155            continue;
17156        };
17157
17158        match event {
17159            ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
17160                let network_counts = process.network_resource_counts();
17161                let response = service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
17162                    bridge,
17163                    vm_id,
17164                    dns,
17165                    socket_paths,
17166                    kernel,
17167                    process,
17168                    sync_request: &request,
17169                    resource_limits,
17170                    network_counts,
17171                });
17172                match response {
17173                    Ok(result) => process
17174                        .execution
17175                        .respond_javascript_sync_rpc_success(request.id, result)
17176                        .or_else(ignore_stale_javascript_sync_rpc_response)?,
17177                    Err(error) => process
17178                        .execution
17179                        .respond_javascript_sync_rpc_error(
17180                            request.id,
17181                            javascript_sync_rpc_error_code(&error),
17182                            error.to_string(),
17183                        )
17184                        .or_else(ignore_stale_javascript_sync_rpc_response)?,
17185                }
17186            }
17187            ActiveExecutionEvent::Exited(code) => {
17188                process.pending_http_requests.remove(&request_key);
17189                return Err(SidecarError::Execution(format!(
17190                    "HTTP loopback server exited before responding (exit code {code})"
17191                )));
17192            }
17193            ActiveExecutionEvent::Stdout(_)
17194            | ActiveExecutionEvent::Stderr(_)
17195            | ActiveExecutionEvent::PythonVfsRpcRequest(_)
17196            | ActiveExecutionEvent::SignalState { .. } => {}
17197        }
17198    }
17199}
17200
17201pub(crate) fn dispatch_loopback_http_request<B>(
17202    request: LoopbackHttpDispatchRequest<'_, B>,
17203) -> Result<String, SidecarError>
17204where
17205    B: NativeSidecarBridge + Send + 'static,
17206    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17207{
17208    let LoopbackHttpDispatchRequest {
17209        bridge,
17210        vm_id,
17211        dns,
17212        socket_paths,
17213        kernel,
17214        process,
17215        resource_limits,
17216        server_id,
17217        request_json,
17218    } = request;
17219    let request_id = {
17220        let server = process.http_servers.get_mut(&server_id).ok_or_else(|| {
17221            SidecarError::InvalidState(format!("HTTP target server disappeared: {server_id}"))
17222        })?;
17223        server.next_request_id += 1;
17224        server.next_request_id
17225    };
17226    process
17227        .pending_http_requests
17228        .insert((server_id, request_id), None);
17229    process.execution.send_javascript_stream_event(
17230        "http_request",
17231        json!({
17232            "serverId": server_id,
17233            "requestId": request_id,
17234            "request": request_json,
17235        }),
17236    )?;
17237    wait_for_loopback_http_response(LoopbackHttpResponseWaitRequest {
17238        bridge,
17239        vm_id,
17240        dns,
17241        socket_paths,
17242        kernel,
17243        process,
17244        resource_limits,
17245        request_key: (server_id, request_id),
17246    })
17247}
17248
17249fn ensure_vm_fetch_response_within_limit(
17250    response_json: &str,
17251    operation: &str,
17252    limit: usize,
17253) -> Result<(), SidecarError> {
17254    let size = response_json.len();
17255    if size > limit {
17256        return Err(SidecarError::Execution(format!(
17257            "{operation} payload is {size} bytes, limit is {limit}"
17258        )));
17259    }
17260    Ok(())
17261}
17262
17263fn ensure_vm_fetch_raw_response_buffer_within_limit(
17264    size: usize,
17265    operation: &str,
17266) -> Result<(), SidecarError> {
17267    if size > VM_FETCH_BUFFER_LIMIT_BYTES {
17268        return Err(SidecarError::Execution(format!(
17269            "{operation} raw response buffer is {size} bytes, limit is {VM_FETCH_BUFFER_LIMIT_BYTES}"
17270        )));
17271    }
17272    Ok(())
17273}
17274
17275pub(crate) fn ensure_vm_fetch_response_frame_within_limit(
17276    response: &ResponseFrame,
17277    max_frame_bytes: usize,
17278) -> Result<(), SidecarError> {
17279    let max_frame_bytes = max_frame_bytes.min(VM_FETCH_BUFFER_LIMIT_BYTES);
17280    let frame = crate::protocol::to_generated_protocol_frame(
17281        &crate::protocol::ProtocolFrame::Response(response.clone()),
17282    )
17283    .map_err(|error| SidecarError::FrameTooLarge(error.to_string()))?;
17284    let WireProtocolFrame::ResponseFrame(_) = &frame else {
17285        return Err(SidecarError::FrameTooLarge(String::from(
17286            "vm fetch response converted to non-response wire frame",
17287        )));
17288    };
17289    WireFrameCodec::new(max_frame_bytes)
17290        .encode(&frame)
17291        .map(|_| ())
17292        .map_err(|error| SidecarError::FrameTooLarge(error.to_string()))
17293}
17294
17295fn service_javascript_dns_sync_rpc<B>(
17296    bridge: &SharedBridge<B>,
17297    kernel: &SidecarKernel,
17298    vm_id: &str,
17299    dns: &VmDnsConfig,
17300    request: &JavascriptSyncRpcRequest,
17301) -> Result<Value, SidecarError>
17302where
17303    B: NativeSidecarBridge + Send + 'static,
17304    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17305{
17306    match request.method.as_str() {
17307        "dns.lookup" => {
17308            let payload = request
17309                .args
17310                .first()
17311                .cloned()
17312                .ok_or_else(|| {
17313                    SidecarError::InvalidState(String::from(
17314                        "dns.lookup requires a request payload",
17315                    ))
17316                })
17317                .and_then(|value| {
17318                    serde_json::from_value::<JavascriptDnsLookupRequest>(value).map_err(|error| {
17319                        SidecarError::InvalidState(format!("invalid dns.lookup payload: {error}"))
17320                    })
17321                })?;
17322            let addresses = filter_dns_ip_addrs(
17323                resolve_dns_ip_addrs(
17324                    bridge,
17325                    kernel,
17326                    vm_id,
17327                    dns,
17328                    &payload.hostname,
17329                    DnsLookupPolicy::CheckPermissions,
17330                )?,
17331                payload.family,
17332            )?;
17333            let addresses = filter_dns_safe_ip_addrs(addresses, &payload.hostname)?;
17334            Ok(Value::Array(
17335                addresses
17336                    .into_iter()
17337                    .map(|ip| {
17338                        json!({
17339                            "address": ip.to_string(),
17340                            "family": if ip.is_ipv6() { 6 } else { 4 },
17341                        })
17342                    })
17343                    .collect(),
17344            ))
17345        }
17346        "dns.resolve" | "dns.resolve4" | "dns.resolve6" => {
17347            let payload = request
17348                .args
17349                .first()
17350                .cloned()
17351                .ok_or_else(|| {
17352                    SidecarError::InvalidState(String::from(
17353                        "dns.resolve requires a request payload",
17354                    ))
17355                })
17356                .and_then(|value| {
17357                    serde_json::from_value::<JavascriptDnsResolveRequest>(value).map_err(|error| {
17358                        SidecarError::InvalidState(format!("invalid dns.resolve payload: {error}"))
17359                    })
17360                })?;
17361            let requested_type = match request.method.as_str() {
17362                "dns.resolve4" => String::from("A"),
17363                "dns.resolve6" => String::from("AAAA"),
17364                _ => payload
17365                    .rrtype
17366                    .as_deref()
17367                    .unwrap_or("A")
17368                    .to_ascii_uppercase(),
17369            };
17370            let record_type = parse_dns_record_type(&requested_type)?;
17371            let resolution = resolve_dns_records(
17372                bridge,
17373                kernel,
17374                vm_id,
17375                dns,
17376                &payload.hostname,
17377                record_type,
17378                DnsLookupPolicy::CheckPermissions,
17379            )?;
17380            dns_resolution_to_node_value(&resolution, &requested_type)
17381        }
17382        other => Err(SidecarError::InvalidState(format!(
17383            "unsupported JavaScript dns sync RPC method {other}"
17384        ))),
17385    }
17386}
17387
17388fn service_javascript_dgram_sync_rpc<B>(
17389    request: JavascriptDgramSyncRpcServiceRequest<'_, B>,
17390) -> Result<Value, SidecarError>
17391where
17392    B: NativeSidecarBridge + Send + 'static,
17393    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17394{
17395    let JavascriptDgramSyncRpcServiceRequest {
17396        bridge,
17397        kernel,
17398        vm_id,
17399        dns,
17400        socket_paths,
17401        process,
17402        sync_request: request,
17403        resource_limits,
17404        network_counts,
17405    } = request;
17406    match request.method.as_str() {
17407        "dgram.createSocket" => {
17408            check_network_resource_limit(
17409                resource_limits.max_sockets,
17410                network_counts.sockets,
17411                1,
17412                "socket",
17413            )?;
17414            let payload = request
17415                .args
17416                .first()
17417                .cloned()
17418                .ok_or_else(|| {
17419                    SidecarError::InvalidState(String::from(
17420                        "dgram.createSocket requires a request payload",
17421                    ))
17422                })
17423                .and_then(|value| {
17424                    serde_json::from_value::<JavascriptDgramCreateSocketRequest>(value).map_err(
17425                        |error| {
17426                            SidecarError::InvalidState(format!(
17427                                "invalid dgram.createSocket payload: {error}"
17428                            ))
17429                        },
17430                    )
17431                })?;
17432            let family = JavascriptUdpFamily::from_socket_type(&payload.socket_type)?;
17433            let socket_id = process.allocate_udp_socket_id();
17434            process.udp_sockets.insert(
17435                socket_id.clone(),
17436                ActiveUdpSocket::new(kernel, process.kernel_pid, family)?,
17437            );
17438            Ok(json!({
17439                "socketId": socket_id,
17440                "type": family.socket_type(),
17441            }))
17442        }
17443        "dgram.bind" => {
17444            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.bind socket id")?;
17445            let payload = request
17446                .args
17447                .get(1)
17448                .cloned()
17449                .ok_or_else(|| {
17450                    SidecarError::InvalidState(String::from(
17451                        "dgram.bind requires a request payload",
17452                    ))
17453                })
17454                .and_then(|value| {
17455                    serde_json::from_value::<JavascriptDgramBindRequest>(value).map_err(|error| {
17456                        SidecarError::InvalidState(format!("invalid dgram.bind payload: {error}"))
17457                    })
17458                })?;
17459            let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
17460                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17461            })?;
17462            let local_addr = socket.bind(
17463                kernel,
17464                process.kernel_pid,
17465                payload.address.as_deref(),
17466                payload.port,
17467                socket_paths,
17468            )?;
17469            Ok(json!({
17470                "localAddress": local_addr.ip().to_string(),
17471                "localPort": local_addr.port(),
17472                "family": socket_addr_family(&local_addr),
17473            }))
17474        }
17475        "dgram.send" => {
17476            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.send socket id")?;
17477            let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "dgram.send payload")?;
17478            let payload = request
17479                .args
17480                .get(2)
17481                .cloned()
17482                .ok_or_else(|| {
17483                    SidecarError::InvalidState(String::from(
17484                        "dgram.send requires a request payload",
17485                    ))
17486                })
17487                .and_then(|value| {
17488                    serde_json::from_value::<JavascriptDgramSendRequest>(value).map_err(|error| {
17489                        SidecarError::InvalidState(format!("invalid dgram.send payload: {error}"))
17490                    })
17491                })?;
17492            let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
17493                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17494            })?;
17495            let (written, local_addr) = socket.send_to(ActiveUdpSendToRequest {
17496                bridge,
17497                kernel,
17498                kernel_pid: process.kernel_pid,
17499                vm_id,
17500                dns,
17501                host: payload.address.as_deref().unwrap_or("localhost"),
17502                port: payload.port,
17503                context: socket_paths,
17504                contents: &chunk,
17505            })?;
17506            Ok(json!({
17507                "bytes": written,
17508                "localAddress": local_addr.ip().to_string(),
17509                "localPort": local_addr.port(),
17510                "family": socket_addr_family(&local_addr),
17511            }))
17512        }
17513        "dgram.poll" => {
17514            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.poll socket id")?;
17515            let wait_ms =
17516                javascript_sync_rpc_arg_u64_optional(&request.args, 1, "dgram.poll wait ms")?
17517                    .unwrap_or_default();
17518            let event = {
17519                let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
17520                    SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17521                })?;
17522                socket.poll(kernel, process.kernel_pid, Duration::from_millis(wait_ms))?
17523            };
17524
17525            match event {
17526                Some(JavascriptUdpSocketEvent::Message { data, remote_addr }) => {
17527                    let family = JavascriptSocketFamily::from_ip(remote_addr.ip());
17528                    let guest_remote_port = if is_loopback_ip(remote_addr.ip()) {
17529                        socket_paths
17530                            .guest_udp_port_for_host_port(family, remote_addr.port())
17531                            .unwrap_or(remote_addr.port())
17532                    } else {
17533                        remote_addr.port()
17534                    };
17535                    Ok(json!({
17536                    "type": "message",
17537                    "data": javascript_sync_rpc_bytes_value(&data),
17538                    "remoteAddress": remote_addr.ip().to_string(),
17539                    "remotePort": guest_remote_port,
17540                    "remoteFamily": socket_addr_family(&remote_addr),
17541                    }))
17542                }
17543                Some(JavascriptUdpSocketEvent::Error { code, message }) => Ok(json!({
17544                    "type": "error",
17545                    "code": code,
17546                    "message": message,
17547                })),
17548                None => Ok(Value::Null),
17549            }
17550        }
17551        "dgram.close" => {
17552            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.close socket id")?;
17553            let mut socket = process.udp_sockets.remove(socket_id).ok_or_else(|| {
17554                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17555            })?;
17556            socket.close(kernel, process.kernel_pid);
17557            Ok(Value::Null)
17558        }
17559        "dgram.address" => {
17560            let socket_id =
17561                javascript_sync_rpc_arg_str(&request.args, 0, "dgram.address socket id")?;
17562            let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
17563                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17564            })?;
17565            let local_addr = socket.local_addr().ok_or_else(|| {
17566                SidecarError::Execution(String::from("EBADF: bad file descriptor"))
17567            })?;
17568            javascript_net_json_string(
17569                json!({
17570                    "address": local_addr.ip().to_string(),
17571                    "port": local_addr.port(),
17572                    "family": socket_addr_family(&local_addr),
17573                }),
17574                "dgram.address",
17575            )
17576        }
17577        "dgram.setBufferSize" => {
17578            let socket_id =
17579                javascript_sync_rpc_arg_str(&request.args, 0, "dgram.setBufferSize socket id")?;
17580            let which =
17581                javascript_sync_rpc_arg_str(&request.args, 1, "dgram.setBufferSize buffer kind")?;
17582            let size = javascript_sync_rpc_arg_u64(&request.args, 2, "dgram.setBufferSize size")?;
17583            let size = usize::try_from(size).map_err(|_| {
17584                SidecarError::InvalidState(String::from(
17585                    "dgram.setBufferSize size must fit within usize",
17586                ))
17587            })?;
17588            let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
17589                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17590            })?;
17591            socket.set_buffer_size(which, size)?;
17592            Ok(Value::Null)
17593        }
17594        "dgram.getBufferSize" => {
17595            let socket_id =
17596                javascript_sync_rpc_arg_str(&request.args, 0, "dgram.getBufferSize socket id")?;
17597            let which =
17598                javascript_sync_rpc_arg_str(&request.args, 1, "dgram.getBufferSize buffer kind")?;
17599            let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
17600                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17601            })?;
17602            let size = socket.get_buffer_size(which)?;
17603            Ok(json!(size))
17604        }
17605        other => Err(SidecarError::InvalidState(format!(
17606            "unsupported JavaScript dgram sync RPC method {other}"
17607        ))),
17608    }
17609}
17610
17611#[derive(Debug)]
17612struct ClientHttp2StreamState {
17613    send_stream: Option<h2::SendStream<Bytes>>,
17614}
17615
17616#[derive(Debug)]
17617struct ServerHttp2StreamState {
17618    send_response: Option<ServerHttp2Responder>,
17619    send_stream: Option<h2::SendStream<Bytes>>,
17620}
17621
17622#[derive(Debug)]
17623enum ServerHttp2Responder {
17624    Regular(server::SendResponse<Bytes>),
17625    Pushed(server::SendPushedResponse<Bytes>),
17626}
17627
17628const HTTP2_DEFAULT_WINDOW_SIZE: u32 = 65_535;
17629const HTTP2_POLL_DELAY: Duration = Duration::from_millis(10);
17630
17631fn http2_runtime_snapshot() -> Http2RuntimeSnapshot {
17632    Http2RuntimeSnapshot {
17633        effective_local_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
17634        local_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
17635        remote_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
17636        next_stream_id: 1,
17637        outbound_queue_size: 1,
17638        deflate_dynamic_table_size: 0,
17639        inflate_dynamic_table_size: 0,
17640    }
17641}
17642
17643fn http2_snapshot_json(snapshot: &Http2SessionSnapshot) -> Result<String, SidecarError> {
17644    serde_json::to_string(snapshot)
17645        .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
17646}
17647
17648fn http2_event_value(event: &Http2BridgeEvent) -> Result<Value, SidecarError> {
17649    serde_json::to_string(event)
17650        .map(Value::String)
17651        .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
17652}
17653
17654fn push_http2_server_event(
17655    shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17656    server_id: u64,
17657    event: Http2BridgeEvent,
17658) {
17659    if let Ok(mut state) = shared.lock() {
17660        state
17661            .server_events
17662            .entry(server_id)
17663            .or_default()
17664            .push_back(event);
17665    }
17666}
17667
17668fn push_http2_session_event(
17669    shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17670    session_id: u64,
17671    event: Http2BridgeEvent,
17672) {
17673    if let Ok(mut state) = shared.lock() {
17674        state
17675            .session_events
17676            .entry(session_id)
17677            .or_default()
17678            .push_back(event);
17679    }
17680}
17681
17682fn pop_http2_event(
17683    queue: &mut BTreeMap<u64, VecDeque<Http2BridgeEvent>>,
17684    id: u64,
17685) -> Option<Http2BridgeEvent> {
17686    queue.get_mut(&id).and_then(VecDeque::pop_front)
17687}
17688
17689fn wait_for_http2_event(
17690    shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17691    id: u64,
17692    is_server: bool,
17693    wait_ms: u64,
17694) -> Option<Http2BridgeEvent> {
17695    let deadline = Instant::now() + Duration::from_millis(wait_ms);
17696    loop {
17697        if let Ok(mut state) = shared.lock() {
17698            let queue = if is_server {
17699                &mut state.server_events
17700            } else {
17701                &mut state.session_events
17702            };
17703            if let Some(event) = pop_http2_event(queue, id) {
17704                return Some(event);
17705            }
17706        }
17707        if wait_ms == 0 || Instant::now() >= deadline {
17708            return None;
17709        }
17710        thread::sleep(HTTP2_POLL_DELAY);
17711    }
17712}
17713
17714fn next_http2_session_id(shared: &mut crate::state::Http2SharedState) -> u64 {
17715    shared.next_session_id += 1;
17716    shared.next_session_id
17717}
17718
17719fn next_http2_stream_id(shared: &mut crate::state::Http2SharedState) -> u64 {
17720    shared.next_stream_id += 1;
17721    shared.next_stream_id
17722}
17723
17724fn http2_reason(code: Option<u32>) -> Reason {
17725    code.unwrap_or(Reason::NO_ERROR.into()).into()
17726}
17727
17728fn http2_error_payload(message: impl Into<String>) -> String {
17729    serde_json::to_string(&json!({
17730        "name": "Error",
17731        "code": "ERR_HTTP2_ERROR",
17732        "message": message.into(),
17733    }))
17734    .unwrap_or_else(|_| {
17735        String::from(
17736            "{\"name\":\"Error\",\"code\":\"ERR_HTTP2_ERROR\",\"message\":\"HTTP/2 bridge error\"}",
17737        )
17738    })
17739}
17740
17741fn http2_socket_snapshot(local_addr: SocketAddr, remote_addr: SocketAddr) -> Http2SocketSnapshot {
17742    Http2SocketSnapshot {
17743        encrypted: false,
17744        allow_half_open: false,
17745        local_address: Some(local_addr.ip().to_string()),
17746        local_port: Some(local_addr.port()),
17747        local_family: Some(socket_addr_family(&local_addr).to_string()),
17748        remote_address: Some(remote_addr.ip().to_string()),
17749        remote_port: Some(remote_addr.port()),
17750        remote_family: Some(socket_addr_family(&remote_addr).to_string()),
17751        servername: None,
17752        alpn_protocol: Some(String::from("h2c")),
17753    }
17754}
17755
17756fn http2_wait_result(kind: &str, id: u64) -> Value {
17757    json!({
17758        "kind": kind,
17759        "id": id,
17760    })
17761}
17762
17763fn is_http2_terminal_event(event: &Http2BridgeEvent, is_server: bool, id: u64) -> bool {
17764    if is_server {
17765        event.kind == "serverClose" && event.id == id
17766    } else {
17767        event.kind == "sessionClose" && event.id == id
17768    }
17769}
17770
17771fn dispatch_http2_wait_loop(
17772    process: &ActiveProcess,
17773    id: u64,
17774    is_server: bool,
17775) -> Result<Value, SidecarError> {
17776    loop {
17777        if let Some(event) = wait_for_http2_event(&process.http2.shared, id, is_server, 50) {
17778            let payload = serde_json::to_value(&event).map_err(|error| {
17779                SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}"))
17780            })?;
17781            process
17782                .execution
17783                .send_javascript_stream_event("http2", payload.clone())?;
17784            if is_http2_terminal_event(&event, is_server, id) {
17785                return Ok(payload);
17786            }
17787            continue;
17788        }
17789
17790        let exists = process
17791            .http2
17792            .shared
17793            .lock()
17794            .map(|state| {
17795                if is_server {
17796                    state.servers.contains_key(&id)
17797                } else {
17798                    state.sessions.contains_key(&id)
17799                }
17800            })
17801            .unwrap_or(false);
17802        if !exists {
17803            return Ok(if is_server {
17804                http2_wait_result("serverClose", id)
17805            } else {
17806                http2_wait_result("sessionClose", id)
17807            });
17808        }
17809    }
17810}
17811
17812fn dispatch_http_wait_loop(process: &ActiveProcess, server_id: u64) -> Result<Value, SidecarError> {
17813    loop {
17814        if !process.http_servers.contains_key(&server_id) {
17815            return Ok(json!({
17816                "kind": "serverClose",
17817                "id": server_id,
17818            }));
17819        }
17820        thread::sleep(Duration::from_millis(25));
17821    }
17822}
17823
17824fn http2_settings_from_value(settings: &BTreeMap<String, Value>) -> BTreeMap<String, Value> {
17825    settings.clone()
17826}
17827
17828fn parse_http2_headers_json(
17829    headers_json: &str,
17830    label: &str,
17831) -> Result<BTreeMap<String, Value>, SidecarError> {
17832    serde_json::from_str::<BTreeMap<String, Value>>(headers_json)
17833        .map_err(|error| SidecarError::InvalidState(format!("{label} must be valid JSON: {error}")))
17834}
17835
17836fn apply_http2_header_values(
17837    header_map: &mut HeaderMap,
17838    name: &str,
17839    value: &Value,
17840) -> Result<(), SidecarError> {
17841    let header_name = HeaderName::from_bytes(name.as_bytes()).map_err(|error| {
17842        SidecarError::InvalidState(format!("invalid HTTP/2 header name {name:?}: {error}"))
17843    })?;
17844    match value {
17845        Value::Array(values) => {
17846            for value in values {
17847                apply_http2_header_values(header_map, name, value)?;
17848            }
17849        }
17850        Value::String(text) => {
17851            let value = HeaderValue::from_str(text).map_err(|error| {
17852                SidecarError::InvalidState(format!(
17853                    "invalid HTTP/2 header value for {name}: {error}"
17854                ))
17855            })?;
17856            header_map.append(header_name.clone(), value);
17857        }
17858        Value::Number(number) => {
17859            let value = HeaderValue::from_str(&number.to_string()).map_err(|error| {
17860                SidecarError::InvalidState(format!(
17861                    "invalid HTTP/2 numeric header value for {name}: {error}"
17862                ))
17863            })?;
17864            header_map.append(header_name.clone(), value);
17865        }
17866        Value::Bool(boolean) => {
17867            let value = HeaderValue::from_str(if *boolean { "true" } else { "false" }).map_err(
17868                |error| {
17869                    SidecarError::InvalidState(format!(
17870                        "invalid HTTP/2 boolean header value for {name}: {error}"
17871                    ))
17872                },
17873            )?;
17874            header_map.append(header_name.clone(), value);
17875        }
17876        Value::Null => {}
17877        Value::Object(_) => {
17878            return Err(SidecarError::InvalidState(format!(
17879                "unsupported HTTP/2 header object value for {name}"
17880            )));
17881        }
17882    }
17883    Ok(())
17884}
17885
17886fn build_http2_request(headers_json: &str) -> Result<Request<()>, SidecarError> {
17887    let headers = parse_http2_headers_json(headers_json, "HTTP/2 request headers")?;
17888    let method = headers
17889        .get(":method")
17890        .and_then(Value::as_str)
17891        .unwrap_or("GET");
17892    let path = headers.get(":path").and_then(Value::as_str).unwrap_or("/");
17893    let mut builder = Request::builder()
17894        .method(Method::from_bytes(method.as_bytes()).map_err(|error| {
17895            SidecarError::InvalidState(format!("invalid HTTP/2 method {method:?}: {error}"))
17896        })?)
17897        .uri(path.parse::<Uri>().map_err(|error| {
17898            SidecarError::InvalidState(format!("invalid HTTP/2 path {path:?}: {error}"))
17899        })?);
17900    {
17901        let header_map = builder.headers_mut().expect("request header map");
17902        for (name, value) in &headers {
17903            if name.starts_with(':') {
17904                continue;
17905            }
17906            apply_http2_header_values(header_map, name, value)?;
17907        }
17908    }
17909    builder
17910        .body(())
17911        .map_err(|error| SidecarError::InvalidState(format!("invalid HTTP/2 request: {error}")))
17912}
17913
17914fn build_http2_response(headers_json: &str) -> Result<Response<()>, SidecarError> {
17915    let headers = parse_http2_headers_json(headers_json, "HTTP/2 response headers")?;
17916    let status = headers
17917        .get(":status")
17918        .and_then(Value::as_u64)
17919        .or_else(|| {
17920            headers
17921                .get(":status")
17922                .and_then(Value::as_str)
17923                .and_then(|value| value.parse::<u16>().ok().map(u64::from))
17924        })
17925        .unwrap_or(200);
17926    let mut builder = Response::builder().status(status as u16);
17927    {
17928        let header_map = builder.headers_mut().expect("response header map");
17929        for (name, value) in &headers {
17930            if name.starts_with(':') {
17931                continue;
17932            }
17933            apply_http2_header_values(header_map, name, value)?;
17934        }
17935    }
17936    builder.body(()).map_err(|error| {
17937        SidecarError::InvalidState(format!("invalid HTTP/2 response headers: {error}"))
17938    })
17939}
17940
17941fn serialize_http2_headers_map(
17942    pseudo: BTreeMap<String, Value>,
17943    headers: &HeaderMap,
17944) -> Result<String, SidecarError> {
17945    let mut serialized = pseudo;
17946    for (name, value) in headers {
17947        let name = name.as_str().to_string();
17948        let value = Value::String(
17949            value
17950                .to_str()
17951                .map_err(|error| {
17952                    SidecarError::Execution(format!("invalid HTTP/2 header value: {error}"))
17953                })?
17954                .to_owned(),
17955        );
17956        match serialized.get_mut(&name) {
17957            Some(Value::Array(values)) => values.push(value),
17958            Some(existing) => {
17959                let first = existing.clone();
17960                *existing = Value::Array(vec![first, value]);
17961            }
17962            None => {
17963                serialized.insert(name, value);
17964            }
17965        }
17966    }
17967    serde_json::to_string(&serialized)
17968        .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
17969}
17970
17971fn serialize_http2_request_headers(
17972    request: &Request<h2::RecvStream>,
17973) -> Result<String, SidecarError> {
17974    let mut pseudo = BTreeMap::new();
17975    pseudo.insert(
17976        String::from(":method"),
17977        Value::String(request.method().as_str().to_string()),
17978    );
17979    pseudo.insert(
17980        String::from(":path"),
17981        Value::String(
17982            request
17983                .uri()
17984                .path_and_query()
17985                .map(|value| value.as_str().to_string())
17986                .unwrap_or_else(|| String::from("/")),
17987        ),
17988    );
17989    serialize_http2_headers_map(pseudo, request.headers())
17990}
17991
17992fn serialize_http2_response_headers(
17993    response: &Response<h2::RecvStream>,
17994) -> Result<String, SidecarError> {
17995    let mut pseudo = BTreeMap::new();
17996    pseudo.insert(
17997        String::from(":status"),
17998        Value::Number(serde_json::Number::from(response.status().as_u16())),
17999    );
18000    serialize_http2_headers_map(pseudo, response.headers())
18001}
18002
18003fn remove_http2_session_resources(
18004    shared: &Arc<Mutex<crate::state::Http2SharedState>>,
18005    session_id: u64,
18006) {
18007    if let Ok(mut state) = shared.lock() {
18008        state.sessions.remove(&session_id);
18009        state.session_events.remove(&session_id);
18010        let stream_ids = state
18011            .streams
18012            .iter()
18013            .filter_map(|(stream_id, stream)| {
18014                (stream.session_id == session_id).then_some(*stream_id)
18015            })
18016            .collect::<Vec<_>>();
18017        for stream_id in stream_ids {
18018            state.streams.remove(&stream_id);
18019        }
18020    }
18021}
18022
18023fn spawn_http2_client_session(
18024    shared: Arc<Mutex<crate::state::Http2SharedState>>,
18025    session_id: u64,
18026    remote_addr: SocketAddr,
18027    tls: Option<JavascriptTlsBridgeOptions>,
18028    snapshot: Arc<Mutex<Http2SessionSnapshot>>,
18029    mut command_rx: UnboundedReceiver<Http2SessionCommand>,
18030) {
18031    thread::spawn(move || {
18032        let runtime = match TokioRuntimeBuilder::new_current_thread()
18033            .enable_all()
18034            .build()
18035        {
18036            Ok(runtime) => runtime,
18037            Err(error) => {
18038                push_http2_session_event(
18039                    &shared,
18040                    session_id,
18041                    Http2BridgeEvent {
18042                        kind: String::from("sessionError"),
18043                        id: session_id,
18044                        data: Some(http2_error_payload(error.to_string())),
18045                        ..Http2BridgeEvent::default()
18046                    },
18047                );
18048                remove_http2_session_resources(&shared, session_id);
18049                return;
18050            }
18051        };
18052
18053        runtime.block_on(async move {
18054            let stream = match tokio::net::TcpStream::connect(remote_addr).await {
18055                Ok(stream) => stream,
18056                Err(error) => {
18057                    push_http2_session_event(
18058                        &shared,
18059                        session_id,
18060                        Http2BridgeEvent {
18061                            kind: String::from("sessionError"),
18062                            id: session_id,
18063                            data: Some(http2_error_payload(error.to_string())),
18064                            ..Http2BridgeEvent::default()
18065                        },
18066                    );
18067                    remove_http2_session_resources(&shared, session_id);
18068                    return;
18069                }
18070            };
18071
18072            let local_addr = match stream.local_addr() {
18073                Ok(addr) => addr,
18074                Err(error) => {
18075                    push_http2_session_event(
18076                        &shared,
18077                        session_id,
18078                        Http2BridgeEvent {
18079                            kind: String::from("sessionError"),
18080                            id: session_id,
18081                            data: Some(http2_error_payload(error.to_string())),
18082                            ..Http2BridgeEvent::default()
18083                        },
18084                    );
18085                    remove_http2_session_resources(&shared, session_id);
18086                    return;
18087                }
18088            };
18089
18090            {
18091                let mut snapshot_guard = snapshot.lock().expect("http2 snapshot lock");
18092                snapshot_guard.socket = http2_socket_snapshot(local_addr, remote_addr);
18093                if let Some(options) = tls.as_ref() {
18094                    snapshot_guard.encrypted = true;
18095                    snapshot_guard.alpn_protocol = Some(String::from("h2"));
18096                    snapshot_guard.socket.encrypted = true;
18097                    snapshot_guard.socket.servername = options.servername.clone();
18098                    snapshot_guard.socket.alpn_protocol = Some(String::from("h2"));
18099                }
18100                snapshot_guard.state = http2_runtime_snapshot();
18101            }
18102            if let Ok(snapshot_json) =
18103                http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())
18104            {
18105                push_http2_session_event(
18106                    &shared,
18107                    session_id,
18108                    Http2BridgeEvent {
18109                        kind: String::from("sessionConnect"),
18110                        id: session_id,
18111                        data: Some(snapshot_json),
18112                        ..Http2BridgeEvent::default()
18113                    },
18114                );
18115            }
18116
18117            let io: Pin<Box<dyn Http2AsyncIo>> = if let Some(options) = tls.as_ref() {
18118                let server_name = match ServerName::try_from(
18119                    options
18120                        .servername
18121                        .clone()
18122                        .unwrap_or_else(|| String::from("localhost")),
18123                ) {
18124                    Ok(server_name) => server_name,
18125                    Err(_) => {
18126                        push_http2_session_event(
18127                            &shared,
18128                            session_id,
18129                            Http2BridgeEvent {
18130                                kind: String::from("sessionError"),
18131                                id: session_id,
18132                                data: Some(http2_error_payload("invalid TLS servername")),
18133                                ..Http2BridgeEvent::default()
18134                            },
18135                        );
18136                        remove_http2_session_resources(&shared, session_id);
18137                        return;
18138                    }
18139                };
18140                let connector = match build_client_tls_config(options) {
18141                    Ok(config) => TlsConnector::from(Arc::new(config)),
18142                    Err(error) => {
18143                        push_http2_session_event(
18144                            &shared,
18145                            session_id,
18146                            Http2BridgeEvent {
18147                                kind: String::from("sessionError"),
18148                                id: session_id,
18149                                data: Some(http2_error_payload(error.to_string())),
18150                                ..Http2BridgeEvent::default()
18151                            },
18152                        );
18153                        remove_http2_session_resources(&shared, session_id);
18154                        return;
18155                    }
18156                };
18157                match connector.connect(server_name, stream).await {
18158                    Ok(tls_stream) => Box::pin(tls_stream),
18159                    Err(error) => {
18160                        push_http2_session_event(
18161                            &shared,
18162                            session_id,
18163                            Http2BridgeEvent {
18164                                kind: String::from("sessionError"),
18165                                id: session_id,
18166                                data: Some(http2_error_payload(error.to_string())),
18167                                ..Http2BridgeEvent::default()
18168                            },
18169                        );
18170                        remove_http2_session_resources(&shared, session_id);
18171                        return;
18172                    }
18173                }
18174            } else {
18175                Box::pin(stream)
18176            };
18177
18178            let (mut sender, connection) = match client::handshake(io).await {
18179                Ok(parts) => parts,
18180                Err(error) => {
18181                    push_http2_session_event(
18182                        &shared,
18183                        session_id,
18184                        Http2BridgeEvent {
18185                            kind: String::from("sessionError"),
18186                            id: session_id,
18187                            data: Some(http2_error_payload(error.to_string())),
18188                            ..Http2BridgeEvent::default()
18189                        },
18190                    );
18191                    remove_http2_session_resources(&shared, session_id);
18192                    return;
18193                }
18194            };
18195
18196            let (status_tx, mut status_rx) = unbounded_channel::<Result<(), String>>();
18197            tokio::spawn(async move {
18198                let _ = status_tx.send(connection.await.map_err(|error| error.to_string()));
18199            });
18200
18201            let streams: Arc<Mutex<BTreeMap<u64, ClientHttp2StreamState>>> =
18202                Arc::new(Mutex::new(BTreeMap::new()));
18203
18204            loop {
18205                tokio::select! {
18206                    Some(result) = status_rx.recv() => {
18207                        if let Err(message) = result {
18208                            push_http2_session_event(
18209                                &shared,
18210                                session_id,
18211                                Http2BridgeEvent {
18212                                    kind: String::from("sessionError"),
18213                                    id: session_id,
18214                                    data: Some(http2_error_payload(message)),
18215                                    ..Http2BridgeEvent::default()
18216                                },
18217                            );
18218                        }
18219                        push_http2_session_event(
18220                            &shared,
18221                            session_id,
18222                            Http2BridgeEvent {
18223                                kind: String::from("sessionClose"),
18224                                id: session_id,
18225                                ..Http2BridgeEvent::default()
18226                            },
18227                        );
18228                        remove_http2_session_resources(&shared, session_id);
18229                        break;
18230                    }
18231                    Some(command) = command_rx.recv() => {
18232                        match command {
18233                            Http2SessionCommand::Request { headers_json, options_json, respond_to } => {
18234                                let request = match build_http2_request(&headers_json) {
18235                                    Ok(request) => request,
18236                                    Err(error) => {
18237                                        let _ = respond_to.send(Err(error.to_string()));
18238                                        continue;
18239                                    }
18240                                };
18241                                let options: JavascriptHttp2RequestOptions =
18242                                    serde_json::from_str(&options_json).unwrap_or_default();
18243                                let stream_id = {
18244                                    let mut state = shared.lock().expect("http2 shared state");
18245                                    let stream_id = next_http2_stream_id(&mut state);
18246                                    state.streams.insert(
18247                                        stream_id,
18248                                        ActiveHttp2Stream {
18249                                            session_id,
18250                                            paused: Arc::new(AtomicBool::new(false)),
18251                                        },
18252                                    );
18253                                    stream_id
18254                                };
18255                                match sender.send_request(request, options.end_stream) {
18256                                    Ok((response_future, send_stream)) => {
18257                                        if !options.end_stream {
18258                                            streams
18259                                                .lock()
18260                                                .expect("http2 client streams")
18261                                                .insert(stream_id, ClientHttp2StreamState { send_stream: Some(send_stream) });
18262                                        }
18263                                        let shared_clone = Arc::clone(&shared);
18264                                        let snapshot_clone = Arc::clone(&snapshot);
18265                                        tokio::spawn(async move {
18266                                            match response_future.await {
18267                                                Ok(response) => {
18268                                                    if let Ok(headers_json) = serialize_http2_response_headers(&response) {
18269                                                        push_http2_session_event(
18270                                                            &shared_clone,
18271                                                            session_id,
18272                                                            Http2BridgeEvent {
18273                                                                kind: String::from("clientResponseHeaders"),
18274                                                                id: stream_id,
18275                                                                data: Some(headers_json),
18276                                                                ..Http2BridgeEvent::default()
18277                                                            },
18278                                                        );
18279                                                    }
18280                                                    let mut body = response.into_body();
18281                                                    while let Some(chunk) = body.data().await {
18282                                                        match chunk {
18283                                                            Ok(bytes) => {
18284                                                                let paused = {
18285                                                                    let state = shared_clone.lock().expect("http2 shared state");
18286                                                                    state.streams.get(&stream_id).map(|stream| Arc::clone(&stream.paused))
18287                                                                };
18288                                                                if let Some(paused) = paused {
18289                                                                    while paused.load(Ordering::SeqCst) {
18290                                                                        tokio::time::sleep(HTTP2_POLL_DELAY).await;
18291                                                                    }
18292                                                                }
18293                                                                let _ = body.flow_control().release_capacity(bytes.len());
18294                                                                push_http2_session_event(
18295                                                                    &shared_clone,
18296                                                                    session_id,
18297                                                                    Http2BridgeEvent {
18298                                                                        kind: String::from("clientData"),
18299                                                                        id: stream_id,
18300                                                                        data: Some(base64::engine::general_purpose::STANDARD.encode(bytes)),
18301                                                                        ..Http2BridgeEvent::default()
18302                                                                    },
18303                                                                );
18304                                                            }
18305                                                            Err(error) => {
18306                                                                push_http2_session_event(
18307                                                                    &shared_clone,
18308                                                                    session_id,
18309                                                                    Http2BridgeEvent {
18310                                                                        kind: String::from("clientError"),
18311                                                                        id: stream_id,
18312                                                                        data: Some(http2_error_payload(error.to_string())),
18313                                                                        ..Http2BridgeEvent::default()
18314                                                                    },
18315                                                                );
18316                                                                break;
18317                                                            }
18318                                                        }
18319                                                    }
18320                                                    {
18321                                                        let mut snapshot = snapshot_clone.lock().expect("http2 snapshot lock");
18322                                                        snapshot.state.next_stream_id =
18323                                                            snapshot.state.next_stream_id.saturating_add(2);
18324                                                    }
18325                                                    push_http2_session_event(
18326                                                        &shared_clone,
18327                                                        session_id,
18328                                                        Http2BridgeEvent {
18329                                                            kind: String::from("clientEnd"),
18330                                                            id: stream_id,
18331                                                            ..Http2BridgeEvent::default()
18332                                                        },
18333                                                    );
18334                                                    push_http2_session_event(
18335                                                        &shared_clone,
18336                                                        session_id,
18337                                                        Http2BridgeEvent {
18338                                                            kind: String::from("clientClose"),
18339                                                            id: stream_id,
18340                                                            extra_number: Some(0),
18341                                                            ..Http2BridgeEvent::default()
18342                                                        },
18343                                                    );
18344                                                    if let Ok(mut state) = shared_clone.lock() {
18345                                                        state.streams.remove(&stream_id);
18346                                                    }
18347                                                }
18348                                                Err(error) => {
18349                                                    push_http2_session_event(
18350                                                        &shared_clone,
18351                                                        session_id,
18352                                                        Http2BridgeEvent {
18353                                                            kind: String::from("clientError"),
18354                                                            id: stream_id,
18355                                                            data: Some(http2_error_payload(error.to_string())),
18356                                                            ..Http2BridgeEvent::default()
18357                                                        },
18358                                                    );
18359                                                    push_http2_session_event(
18360                                                        &shared_clone,
18361                                                        session_id,
18362                                                        Http2BridgeEvent {
18363                                                            kind: String::from("clientClose"),
18364                                                            id: stream_id,
18365                                                            extra_number: Some(u32::from(Reason::INTERNAL_ERROR) as u64),
18366                                                            ..Http2BridgeEvent::default()
18367                                                        },
18368                                                    );
18369                                                    if let Ok(mut state) = shared_clone.lock() {
18370                                                        state.streams.remove(&stream_id);
18371                                                    }
18372                                                }
18373                                            }
18374                                        });
18375                                        let _ = respond_to.send(Ok(json!(stream_id)));
18376                                    }
18377                                    Err(error) => {
18378                                        if let Ok(mut state) = shared.lock() {
18379                                            state.streams.remove(&stream_id);
18380                                        }
18381                                        let _ = respond_to.send(Err(error.to_string()));
18382                                    }
18383                                }
18384                            }
18385                            Http2SessionCommand::Settings { settings_json, respond_to } => {
18386                                let settings = serde_json::from_str::<BTreeMap<String, Value>>(&settings_json)
18387                                    .unwrap_or_default();
18388                                {
18389                                    let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18390                                    snapshot.local_settings = http2_settings_from_value(&settings);
18391                                }
18392                                if let Ok(headers_json) = serde_json::to_string(&settings) {
18393                                    push_http2_session_event(
18394                                        &shared,
18395                                        session_id,
18396                                        Http2BridgeEvent {
18397                                            kind: String::from("sessionLocalSettings"),
18398                                            id: session_id,
18399                                            data: Some(headers_json.clone()),
18400                                            ..Http2BridgeEvent::default()
18401                                        },
18402                                    );
18403                                    push_http2_session_event(
18404                                        &shared,
18405                                        session_id,
18406                                        Http2BridgeEvent {
18407                                            kind: String::from("sessionSettingsAck"),
18408                                            id: session_id,
18409                                            ..Http2BridgeEvent::default()
18410                                        },
18411                                    );
18412                                }
18413                                let _ = respond_to.send(Ok(Value::Null));
18414                            }
18415                            Http2SessionCommand::SetLocalWindowSize { size, respond_to } => {
18416                                {
18417                                    let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18418                                    snapshot.state.local_window_size = size;
18419                                    snapshot.state.effective_local_window_size = size;
18420                                }
18421                                let value = snapshot
18422                                    .lock()
18423                                    .ok()
18424                                    .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok())
18425                                    .map(Value::String)
18426                                    .unwrap_or(Value::Null);
18427                                let _ = respond_to.send(Ok(value));
18428                            }
18429                            Http2SessionCommand::Goaway { error_code, last_stream_id, opaque_data, respond_to } => {
18430                                push_http2_session_event(
18431                                    &shared,
18432                                    session_id,
18433                                    Http2BridgeEvent {
18434                                        kind: String::from("sessionGoaway"),
18435                                        id: session_id,
18436                                        data: opaque_data.map(|value| {
18437                                            base64::engine::general_purpose::STANDARD.encode(value)
18438                                        }),
18439                                        extra_number: Some(error_code as u64),
18440                                        flags: Some(last_stream_id as u64),
18441                                        ..Http2BridgeEvent::default()
18442                                    },
18443                                );
18444                                let _ = respond_to.send(Ok(Value::Null));
18445                            }
18446                            Http2SessionCommand::Close { respond_to, .. } => {
18447                                let _ = respond_to.send(Ok(Value::Null));
18448                                push_http2_session_event(
18449                                    &shared,
18450                                    session_id,
18451                                    Http2BridgeEvent {
18452                                        kind: String::from("sessionClose"),
18453                                        id: session_id,
18454                                        ..Http2BridgeEvent::default()
18455                                    },
18456                                );
18457                                remove_http2_session_resources(&shared, session_id);
18458                                break;
18459                            }
18460                            Http2SessionCommand::StreamWrite { stream_id, chunk, end_stream, respond_to } => {
18461                                let result = streams
18462                                    .lock()
18463                                    .expect("http2 client streams")
18464                                    .get_mut(&stream_id)
18465                                    .and_then(|stream| stream.send_stream.as_mut())
18466                                    .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 client stream {stream_id}")))
18467                                    .and_then(|stream| stream.send_data(Bytes::from(chunk), end_stream).map_err(|error| SidecarError::Execution(error.to_string())));
18468                                match result {
18469                                    Ok(()) => {
18470                                        if end_stream {
18471                                            streams.lock().expect("http2 client streams").remove(&stream_id);
18472                                        }
18473                                        let _ = respond_to.send(Ok(Value::Bool(true)));
18474                                    }
18475                                    Err(error) => {
18476                                        let _ = respond_to.send(Err(error.to_string()));
18477                                    }
18478                                }
18479                            }
18480                            Http2SessionCommand::StreamClose { stream_id, error_code, respond_to } => {
18481                                let mut streams = streams.lock().expect("http2 client streams");
18482                                let Some(mut state) = streams.remove(&stream_id) else {
18483                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 client stream {stream_id}")));
18484                                    continue;
18485                                };
18486                                if let Some(stream) = state.send_stream.as_mut() {
18487                                    stream.send_reset(http2_reason(error_code));
18488                                }
18489                                if let Ok(mut state) = shared.lock() {
18490                                    state.streams.remove(&stream_id);
18491                                }
18492                                push_http2_session_event(
18493                                    &shared,
18494                                    session_id,
18495                                    Http2BridgeEvent {
18496                                        kind: String::from("clientClose"),
18497                                        id: stream_id,
18498                                        extra_number: Some(u32::from(http2_reason(error_code)) as u64),
18499                                        ..Http2BridgeEvent::default()
18500                                    },
18501                                );
18502                                let _ = respond_to.send(Ok(Value::Null));
18503                            }
18504                            Http2SessionCommand::StreamRespond { respond_to, .. }
18505                            | Http2SessionCommand::StreamPush { respond_to, .. }
18506                            | Http2SessionCommand::StreamRespondWithFile { respond_to, .. } => {
18507                                let _ = respond_to.send(Err(String::from("HTTP/2 client streams cannot send server responses")));
18508                            }
18509                        }
18510                    }
18511                    else => break,
18512                }
18513            }
18514        });
18515    });
18516}
18517
18518fn spawn_http2_server_session(
18519    shared: Arc<Mutex<crate::state::Http2SharedState>>,
18520    server_id: u64,
18521    session_id: u64,
18522    stream: TcpStream,
18523    tls: Option<JavascriptTlsBridgeOptions>,
18524    snapshot: Arc<Mutex<Http2SessionSnapshot>>,
18525    mut command_rx: UnboundedReceiver<Http2SessionCommand>,
18526) {
18527    thread::spawn(move || {
18528        let runtime = match TokioRuntimeBuilder::new_current_thread()
18529            .enable_all()
18530            .build()
18531        {
18532            Ok(runtime) => runtime,
18533            Err(error) => {
18534                push_http2_server_event(
18535                    &shared,
18536                    server_id,
18537                    Http2BridgeEvent {
18538                        kind: String::from("serverStreamError"),
18539                        id: session_id,
18540                        data: Some(http2_error_payload(error.to_string())),
18541                        ..Http2BridgeEvent::default()
18542                    },
18543                );
18544                remove_http2_session_resources(&shared, session_id);
18545                return;
18546            }
18547        };
18548
18549        runtime.block_on(async move {
18550            if let Err(error) = stream.set_nonblocking(true) {
18551                push_http2_server_event(
18552                    &shared,
18553                    server_id,
18554                    Http2BridgeEvent {
18555                        kind: String::from("serverStreamError"),
18556                        id: session_id,
18557                        data: Some(http2_error_payload(error.to_string())),
18558                        ..Http2BridgeEvent::default()
18559                    },
18560                );
18561                remove_http2_session_resources(&shared, session_id);
18562                return;
18563            }
18564            let stream = match tokio::net::TcpStream::from_std(stream) {
18565                Ok(stream) => stream,
18566                Err(error) => {
18567                    push_http2_server_event(
18568                        &shared,
18569                        server_id,
18570                        Http2BridgeEvent {
18571                            kind: String::from("serverStreamError"),
18572                            id: session_id,
18573                            data: Some(http2_error_payload(error.to_string())),
18574                            ..Http2BridgeEvent::default()
18575                        },
18576                    );
18577                    remove_http2_session_resources(&shared, session_id);
18578                    return;
18579                }
18580            };
18581            let local_addr = match stream.local_addr() {
18582                Ok(addr) => addr,
18583                Err(error) => {
18584                    push_http2_server_event(
18585                        &shared,
18586                        server_id,
18587                        Http2BridgeEvent {
18588                            kind: String::from("serverStreamError"),
18589                            id: session_id,
18590                            data: Some(http2_error_payload(error.to_string())),
18591                            ..Http2BridgeEvent::default()
18592                        },
18593                    );
18594                    remove_http2_session_resources(&shared, session_id);
18595                    return;
18596                }
18597            };
18598            let remote_addr = match stream.peer_addr() {
18599                Ok(addr) => addr,
18600                Err(error) => {
18601                    push_http2_server_event(
18602                        &shared,
18603                        server_id,
18604                        Http2BridgeEvent {
18605                            kind: String::from("serverStreamError"),
18606                            id: session_id,
18607                            data: Some(http2_error_payload(error.to_string())),
18608                            ..Http2BridgeEvent::default()
18609                        },
18610                    );
18611                    remove_http2_session_resources(&shared, session_id);
18612                    return;
18613                }
18614            };
18615            {
18616                let mut snapshot_guard = snapshot.lock().expect("http2 snapshot lock");
18617                snapshot_guard.socket = http2_socket_snapshot(local_addr, remote_addr);
18618                if tls.is_some() {
18619                    snapshot_guard.encrypted = true;
18620                    snapshot_guard.alpn_protocol = Some(String::from("h2"));
18621                    snapshot_guard.socket.encrypted = true;
18622                    snapshot_guard.socket.alpn_protocol = Some(String::from("h2"));
18623                }
18624                snapshot_guard.state = http2_runtime_snapshot();
18625            }
18626            if let Ok(snapshot_json) =
18627                http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())
18628            {
18629                push_http2_server_event(
18630                    &shared,
18631                    server_id,
18632                    Http2BridgeEvent {
18633                        kind: String::from(if tls.is_some() {
18634                            "serverSecureConnection"
18635                        } else {
18636                            "serverConnection"
18637                        }),
18638                        id: server_id,
18639                        data: Some(serde_json::to_string(&http2_socket_snapshot(local_addr, remote_addr)).unwrap_or_default()),
18640                        ..Http2BridgeEvent::default()
18641                    },
18642                );
18643                push_http2_server_event(
18644                    &shared,
18645                    server_id,
18646                    Http2BridgeEvent {
18647                        kind: String::from("serverSession"),
18648                        id: server_id,
18649                        data: Some(snapshot_json),
18650                        extra_number: Some(session_id),
18651                        ..Http2BridgeEvent::default()
18652                    },
18653                );
18654            }
18655
18656            let io: Pin<Box<dyn Http2AsyncIo>> = if let Some(options) = tls.as_ref() {
18657                let acceptor = match build_server_tls_config(options) {
18658                    Ok(config) => TlsAcceptor::from(Arc::new(config)),
18659                    Err(error) => {
18660                        push_http2_server_event(
18661                            &shared,
18662                            server_id,
18663                            Http2BridgeEvent {
18664                                kind: String::from("serverStreamError"),
18665                                id: session_id,
18666                                data: Some(http2_error_payload(error.to_string())),
18667                                ..Http2BridgeEvent::default()
18668                            },
18669                        );
18670                        remove_http2_session_resources(&shared, session_id);
18671                        return;
18672                    }
18673                };
18674                match acceptor.accept(stream).await {
18675                    Ok(tls_stream) => Box::pin(tls_stream),
18676                    Err(error) => {
18677                        push_http2_server_event(
18678                            &shared,
18679                            server_id,
18680                            Http2BridgeEvent {
18681                                kind: String::from("serverStreamError"),
18682                                id: session_id,
18683                                data: Some(http2_error_payload(error.to_string())),
18684                                ..Http2BridgeEvent::default()
18685                            },
18686                        );
18687                        remove_http2_session_resources(&shared, session_id);
18688                        return;
18689                    }
18690                }
18691            } else {
18692                Box::pin(stream)
18693            };
18694
18695            let mut connection = match server::handshake(io).await {
18696                Ok(connection) => connection,
18697                Err(error) => {
18698                    push_http2_server_event(
18699                        &shared,
18700                        server_id,
18701                        Http2BridgeEvent {
18702                            kind: String::from("serverStreamError"),
18703                            id: session_id,
18704                            data: Some(http2_error_payload(error.to_string())),
18705                            ..Http2BridgeEvent::default()
18706                        },
18707                    );
18708                    remove_http2_session_resources(&shared, session_id);
18709                    return;
18710                }
18711            };
18712
18713            let streams: Arc<Mutex<BTreeMap<u64, ServerHttp2StreamState>>> =
18714                Arc::new(Mutex::new(BTreeMap::new()));
18715
18716            loop {
18717                tokio::select! {
18718                    incoming = connection.accept() => {
18719                        match incoming {
18720                            Some(Ok((request, respond))) => {
18721                                let headers_json = match serialize_http2_request_headers(&request) {
18722                                    Ok(headers) => headers,
18723                                    Err(error) => {
18724                                        push_http2_server_event(
18725                                            &shared,
18726                                            server_id,
18727                                            Http2BridgeEvent {
18728                                                kind: String::from("serverStreamError"),
18729                                                id: server_id,
18730                                                data: Some(http2_error_payload(error.to_string())),
18731                                                ..Http2BridgeEvent::default()
18732                                            },
18733                                        );
18734                                        continue;
18735                                    }
18736                                };
18737                                let stream_id = {
18738                                    let mut state = shared.lock().expect("http2 shared state");
18739                                    let stream_id = next_http2_stream_id(&mut state);
18740                                    state.streams.insert(
18741                                        stream_id,
18742                                        ActiveHttp2Stream {
18743                                            session_id,
18744                                            paused: Arc::new(AtomicBool::new(false)),
18745                                        },
18746                                    );
18747                                    stream_id
18748                                };
18749                                streams.lock().expect("http2 server streams").insert(
18750                                    stream_id,
18751                                    ServerHttp2StreamState {
18752                                        send_response: Some(ServerHttp2Responder::Regular(respond)),
18753                                        send_stream: None,
18754                                    },
18755                                );
18756                                let snapshot_json = snapshot
18757                                    .lock()
18758                                    .ok()
18759                                    .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok());
18760                                push_http2_server_event(
18761                                    &shared,
18762                                    server_id,
18763                                    Http2BridgeEvent {
18764                                        kind: String::from("serverStream"),
18765                                        id: server_id,
18766                                        data: Some(stream_id.to_string()),
18767                                        extra: snapshot_json,
18768                                        extra_number: Some(session_id),
18769                                        extra_headers: Some(headers_json),
18770                                        flags: Some(0),
18771                                    },
18772                                );
18773                                let shared_clone = Arc::clone(&shared);
18774                                tokio::spawn(async move {
18775                                    let mut body = request.into_body();
18776                                    while let Some(chunk) = body.data().await {
18777                                        match chunk {
18778                                            Ok(bytes) => {
18779                                                let paused = {
18780                                                    let state = shared_clone.lock().expect("http2 shared state");
18781                                                    state.streams.get(&stream_id).map(|stream| Arc::clone(&stream.paused))
18782                                                };
18783                                                if let Some(paused) = paused {
18784                                                    while paused.load(Ordering::SeqCst) {
18785                                                        tokio::time::sleep(HTTP2_POLL_DELAY).await;
18786                                                    }
18787                                                }
18788                                                let _ = body.flow_control().release_capacity(bytes.len());
18789                                                push_http2_server_event(
18790                                                    &shared_clone,
18791                                                    server_id,
18792                                                    Http2BridgeEvent {
18793                                                        kind: String::from("serverStreamData"),
18794                                                        id: stream_id,
18795                                                        data: Some(base64::engine::general_purpose::STANDARD.encode(bytes)),
18796                                                        ..Http2BridgeEvent::default()
18797                                                    },
18798                                                );
18799                                            }
18800                                            Err(error) => {
18801                                                push_http2_server_event(
18802                                                    &shared_clone,
18803                                                    server_id,
18804                                                    Http2BridgeEvent {
18805                                                        kind: String::from("serverStreamError"),
18806                                                        id: stream_id,
18807                                                        data: Some(http2_error_payload(error.to_string())),
18808                                                        ..Http2BridgeEvent::default()
18809                                                    },
18810                                                );
18811                                                break;
18812                                            }
18813                                        }
18814                                    }
18815                                    push_http2_server_event(
18816                                        &shared_clone,
18817                                        server_id,
18818                                        Http2BridgeEvent {
18819                                            kind: String::from("serverStreamEnd"),
18820                                            id: stream_id,
18821                                            ..Http2BridgeEvent::default()
18822                                        },
18823                                    );
18824                                });
18825                            }
18826                            Some(Err(error)) => {
18827                                push_http2_server_event(
18828                                    &shared,
18829                                    server_id,
18830                                    Http2BridgeEvent {
18831                                        kind: String::from("serverStreamError"),
18832                                        id: server_id,
18833                                        data: Some(http2_error_payload(error.to_string())),
18834                                        ..Http2BridgeEvent::default()
18835                                    },
18836                                );
18837                                break;
18838                            }
18839                            None => {
18840                                push_http2_server_event(
18841                                    &shared,
18842                                    server_id,
18843                                    Http2BridgeEvent {
18844                                        kind: String::from("sessionClose"),
18845                                        id: session_id,
18846                                        ..Http2BridgeEvent::default()
18847                                    },
18848                                );
18849                                remove_http2_session_resources(&shared, session_id);
18850                                break;
18851                            }
18852                        }
18853                    }
18854                    Some(command) = command_rx.recv() => {
18855                        match command {
18856                            Http2SessionCommand::Settings { settings_json, respond_to } => {
18857                                let settings = serde_json::from_str::<BTreeMap<String, Value>>(&settings_json)
18858                                    .unwrap_or_default();
18859                                if let Some(initial_window_size) = settings
18860                                    .get("initialWindowSize")
18861                                    .and_then(Value::as_u64)
18862                                {
18863                                    let _ = connection.set_initial_window_size(initial_window_size as u32);
18864                                }
18865                                {
18866                                    let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18867                                    snapshot.local_settings = http2_settings_from_value(&settings);
18868                                }
18869                                if let Ok(headers_json) = serde_json::to_string(&settings) {
18870                                    push_http2_session_event(
18871                                        &shared,
18872                                        session_id,
18873                                        Http2BridgeEvent {
18874                                            kind: String::from("sessionLocalSettings"),
18875                                            id: session_id,
18876                                            data: Some(headers_json),
18877                                            ..Http2BridgeEvent::default()
18878                                        },
18879                                    );
18880                                }
18881                                let _ = respond_to.send(Ok(Value::Null));
18882                            }
18883                            Http2SessionCommand::SetLocalWindowSize { size, respond_to } => {
18884                                connection.set_target_window_size(size);
18885                                {
18886                                    let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18887                                    snapshot.state.local_window_size = size;
18888                                    snapshot.state.effective_local_window_size = size;
18889                                }
18890                                let value = snapshot
18891                                    .lock()
18892                                    .ok()
18893                                    .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok())
18894                                    .map(Value::String)
18895                                    .unwrap_or(Value::Null);
18896                                let _ = respond_to.send(Ok(value));
18897                            }
18898                            Http2SessionCommand::Goaway { error_code, last_stream_id, opaque_data, respond_to } => {
18899                                connection.abrupt_shutdown(http2_reason(Some(error_code)));
18900                                push_http2_session_event(
18901                                    &shared,
18902                                    session_id,
18903                                    Http2BridgeEvent {
18904                                        kind: String::from("sessionGoaway"),
18905                                        id: session_id,
18906                                        data: opaque_data.map(|value| {
18907                                            base64::engine::general_purpose::STANDARD.encode(value)
18908                                        }),
18909                                        extra_number: Some(error_code as u64),
18910                                        flags: Some(last_stream_id as u64),
18911                                        ..Http2BridgeEvent::default()
18912                                    },
18913                                );
18914                                let _ = respond_to.send(Ok(Value::Null));
18915                            }
18916                            Http2SessionCommand::Close { abrupt, respond_to } => {
18917                                if abrupt {
18918                                    connection.abrupt_shutdown(Reason::NO_ERROR);
18919                                } else {
18920                                    connection.graceful_shutdown();
18921                                }
18922                                let _ = respond_to.send(Ok(Value::Null));
18923                                push_http2_session_event(
18924                                    &shared,
18925                                    session_id,
18926                                    Http2BridgeEvent {
18927                                        kind: String::from("sessionClose"),
18928                                        id: session_id,
18929                                        ..Http2BridgeEvent::default()
18930                                    },
18931                                );
18932                                remove_http2_session_resources(&shared, session_id);
18933                                break;
18934                            }
18935                            Http2SessionCommand::StreamRespond { stream_id, headers_json, respond_to } => {
18936                                let response = match build_http2_response(&headers_json) {
18937                                    Ok(response) => response,
18938                                    Err(error) => {
18939                                        let _ = respond_to.send(Err(error.to_string()));
18940                                        continue;
18941                                    }
18942                                };
18943                                let mut streams = streams.lock().expect("http2 server streams");
18944                                let Some(state) = streams.get_mut(&stream_id) else {
18945                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
18946                                    continue;
18947                                };
18948                                let Some(send_response) = state.send_response.as_mut() else {
18949                                    let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} already responded")));
18950                                    continue;
18951                                };
18952                                match match send_response {
18953                                    ServerHttp2Responder::Regular(send_response) => {
18954                                        send_response.send_response(response, false)
18955                                    }
18956                                    ServerHttp2Responder::Pushed(send_response) => {
18957                                        send_response.send_response(response, false)
18958                                    }
18959                                } {
18960                                    Ok(send_stream) => {
18961                                        state.send_stream = Some(send_stream);
18962                                        state.send_response = None;
18963                                        let _ = respond_to.send(Ok(Value::Null));
18964                                    }
18965                                    Err(error) => {
18966                                        let _ = respond_to.send(Err(error.to_string()));
18967                                    }
18968                                }
18969                            }
18970                            Http2SessionCommand::StreamPush { stream_id, headers_json, respond_to } => {
18971                                let request = match build_http2_request(&headers_json) {
18972                                    Ok(request) => request,
18973                                    Err(error) => {
18974                                        let _ = respond_to.send(Err(error.to_string()));
18975                                        continue;
18976                                    }
18977                                };
18978                                let mut streams_guard = streams.lock().expect("http2 server streams");
18979                                let Some(state) = streams_guard.get_mut(&stream_id) else {
18980                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
18981                                    continue;
18982                                };
18983                                let Some(send_response) = state.send_response.as_mut() else {
18984                                    let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} cannot push after responding")));
18985                                    continue;
18986                                };
18987                                let ServerHttp2Responder::Regular(send_response) = send_response else {
18988                                    let _ = respond_to.send(Err(format!("HTTP/2 pushed stream {stream_id} cannot create nested push promises")));
18989                                    continue;
18990                                };
18991                                match send_response.push_request(request) {
18992                                    Ok(pushed) => {
18993                                        let pushed_stream_id = {
18994                                            let mut state = shared.lock().expect("http2 shared state");
18995                                            let pushed_stream_id = next_http2_stream_id(&mut state);
18996                                            state.streams.insert(
18997                                                pushed_stream_id,
18998                                                ActiveHttp2Stream {
18999                                                    session_id,
19000                                                    paused: Arc::new(AtomicBool::new(false)),
19001                                                },
19002                                            );
19003                                            pushed_stream_id
19004                                        };
19005                                        streams_guard.insert(
19006                                            pushed_stream_id,
19007                                            ServerHttp2StreamState {
19008                                                send_response: Some(ServerHttp2Responder::Pushed(pushed)),
19009                                                send_stream: None,
19010                                            },
19011                                        );
19012                                        let _ = respond_to.send(Ok(json!({
19013                                            "streamId": pushed_stream_id,
19014                                            "headers": headers_json,
19015                                        }).to_string().into()));
19016                                    }
19017                                    Err(error) => {
19018                                        let _ = respond_to.send(Err(error.to_string()));
19019                                    }
19020                                }
19021                            }
19022                            Http2SessionCommand::StreamWrite { stream_id, chunk, end_stream, respond_to } => {
19023                                let mut streams = streams.lock().expect("http2 server streams");
19024                                let Some(state) = streams.get_mut(&stream_id) else {
19025                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
19026                                    continue;
19027                                };
19028                                let Some(send_stream) = state.send_stream.as_mut() else {
19029                                    let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} has not sent response headers")));
19030                                    continue;
19031                                };
19032                                match send_stream.send_data(Bytes::from(chunk), end_stream) {
19033                                    Ok(()) => {
19034                                        if end_stream {
19035                                            streams.remove(&stream_id);
19036                                            if let Ok(mut state) = shared.lock() {
19037                                                state.streams.remove(&stream_id);
19038                                            }
19039                                            push_http2_server_event(
19040                                                &shared,
19041                                                server_id,
19042                                                Http2BridgeEvent {
19043                                                    kind: String::from("serverStreamClose"),
19044                                                    id: stream_id,
19045                                                    extra_number: Some(0),
19046                                                    ..Http2BridgeEvent::default()
19047                                                },
19048                                            );
19049                                        }
19050                                        let _ = respond_to.send(Ok(Value::Bool(true)));
19051                                    }
19052                                    Err(error) => {
19053                                        let _ = respond_to.send(Err(error.to_string()));
19054                                    }
19055                                }
19056                            }
19057                            Http2SessionCommand::StreamClose { stream_id, error_code, respond_to } => {
19058                                let mut streams_guard = streams.lock().expect("http2 server streams");
19059                                let Some(mut state) = streams_guard.remove(&stream_id) else {
19060                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
19061                                    continue;
19062                                };
19063                                let reason = http2_reason(error_code);
19064                                if let Some(send_stream) = state.send_stream.as_mut() {
19065                                    send_stream.send_reset(reason);
19066                                }
19067                                if let Some(send_response) = state.send_response.as_mut() {
19068                                    match send_response {
19069                                        ServerHttp2Responder::Regular(send_response) => {
19070                                            send_response.send_reset(reason)
19071                                        }
19072                                        ServerHttp2Responder::Pushed(send_response) => {
19073                                            send_response.send_reset(reason)
19074                                        }
19075                                    }
19076                                }
19077                                if let Ok(mut shared_guard) = shared.lock() {
19078                                    shared_guard.streams.remove(&stream_id);
19079                                }
19080                                push_http2_server_event(
19081                                    &shared,
19082                                    server_id,
19083                                    Http2BridgeEvent {
19084                                        kind: String::from("serverStreamClose"),
19085                                        id: stream_id,
19086                                        extra_number: Some(u32::from(reason) as u64),
19087                                        ..Http2BridgeEvent::default()
19088                                    },
19089                                );
19090                                let _ = respond_to.send(Ok(Value::Null));
19091                            }
19092                            Http2SessionCommand::StreamRespondWithFile { stream_id, body, headers_json, options_json, respond_to } => {
19093                                let options: JavascriptHttp2FileResponseOptions =
19094                                    serde_json::from_str(&options_json).unwrap_or_default();
19095                                let response = match build_http2_response(&headers_json) {
19096                                    Ok(response) => response,
19097                                    Err(error) => {
19098                                        let _ = respond_to.send(Err(error.to_string()));
19099                                        continue;
19100                                    }
19101                                };
19102                                let offset = usize::try_from(options.offset.unwrap_or_default()).unwrap_or(0);
19103                                let body = if offset >= body.len() {
19104                                    Vec::new()
19105                                } else {
19106                                    let body = &body[offset..];
19107                                    match options.length {
19108                                        Some(length) if length >= 0 => {
19109                                            body[..body.len().min(length as usize)].to_vec()
19110                                        }
19111                                        _ => body.to_vec(),
19112                                    }
19113                                };
19114                                let mut streams_guard = streams.lock().expect("http2 server streams");
19115                                let Some(state) = streams_guard.get_mut(&stream_id) else {
19116                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
19117                                    continue;
19118                                };
19119                                let Some(send_response) = state.send_response.as_mut() else {
19120                                    let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} already responded")));
19121                                    continue;
19122                                };
19123                                match match send_response {
19124                                    ServerHttp2Responder::Regular(send_response) => {
19125                                        send_response.send_response(response, body.is_empty())
19126                                    }
19127                                    ServerHttp2Responder::Pushed(send_response) => {
19128                                        send_response.send_response(response, body.is_empty())
19129                                    }
19130                                } {
19131                                    Ok(mut send_stream) => {
19132                                        state.send_response = None;
19133                                        if body.is_empty() {
19134                                            streams_guard.remove(&stream_id);
19135                                            if let Ok(mut shared_guard) = shared.lock() {
19136                                                shared_guard.streams.remove(&stream_id);
19137                                            }
19138                                        } else {
19139                                            if let Err(error) = send_stream.send_data(Bytes::from(body), true) {
19140                                                let _ = respond_to.send(Err(error.to_string()));
19141                                                continue;
19142                                            }
19143                                            streams_guard.remove(&stream_id);
19144                                            if let Ok(mut shared_guard) = shared.lock() {
19145                                                shared_guard.streams.remove(&stream_id);
19146                                            }
19147                                        }
19148                                        push_http2_server_event(
19149                                            &shared,
19150                                            server_id,
19151                                            Http2BridgeEvent {
19152                                                kind: String::from("serverStreamClose"),
19153                                                id: stream_id,
19154                                                extra_number: Some(0),
19155                                                ..Http2BridgeEvent::default()
19156                                            },
19157                                        );
19158                                        let _ = respond_to.send(Ok(Value::Null));
19159                                    }
19160                                    Err(error) => {
19161                                        let _ = respond_to.send(Err(error.to_string()));
19162                                    }
19163                                }
19164                            }
19165                            Http2SessionCommand::Request { respond_to, .. } => {
19166                                let _ = respond_to.send(Err(String::from("HTTP/2 server sessions cannot initiate client requests")));
19167                            }
19168                        }
19169                    }
19170                    else => break,
19171                }
19172            }
19173        });
19174    });
19175}
19176
19177fn spawn_http2_server_accept_loop(
19178    shared: Arc<Mutex<crate::state::Http2SharedState>>,
19179    server_id: u64,
19180    listener: TcpListener,
19181) {
19182    thread::spawn(move || {
19183        let listener = listener;
19184        loop {
19185            let closed = shared
19186                .lock()
19187                .ok()
19188                .and_then(|state| {
19189                    state
19190                        .servers
19191                        .get(&server_id)
19192                        .map(|server| server.closed.load(Ordering::SeqCst))
19193                })
19194                .unwrap_or(true);
19195            if closed {
19196                break;
19197            }
19198            match listener.accept() {
19199                Ok((stream, _)) => {
19200                    let (command_tx, command_rx) = unbounded_channel();
19201                    let (guest_local_addr, secure, tls) = {
19202                        let state = shared.lock().expect("http2 shared state");
19203                        let server = state.servers.get(&server_id).expect("http2 server state");
19204                        (server.guest_local_addr, server.secure, server.tls.clone())
19205                    };
19206                    let (local_addr, remote_addr) = match (stream.local_addr(), stream.peer_addr())
19207                    {
19208                        (Ok(local_addr), Ok(remote_addr)) => (local_addr, remote_addr),
19209                        _ => continue,
19210                    };
19211                    let session_snapshot = Arc::new(Mutex::new(Http2SessionSnapshot {
19212                        encrypted: secure,
19213                        alpn_protocol: Some(if secure {
19214                            String::from("h2")
19215                        } else {
19216                            String::from("h2c")
19217                        }),
19218                        local_settings: BTreeMap::new(),
19219                        remote_settings: BTreeMap::new(),
19220                        state: http2_runtime_snapshot(),
19221                        socket: Http2SocketSnapshot {
19222                            local_address: Some(guest_local_addr.ip().to_string()),
19223                            local_port: Some(guest_local_addr.port()),
19224                            local_family: Some(socket_addr_family(&guest_local_addr).to_string()),
19225                            remote_address: Some(remote_addr.ip().to_string()),
19226                            remote_port: Some(remote_addr.port()),
19227                            remote_family: Some(socket_addr_family(&remote_addr).to_string()),
19228                            ..http2_socket_snapshot(local_addr, remote_addr)
19229                        },
19230                        ..Http2SessionSnapshot::default()
19231                    }));
19232                    let session_id = {
19233                        let mut state = shared.lock().expect("http2 shared state");
19234                        let session_id = next_http2_session_id(&mut state);
19235                        state
19236                            .sessions
19237                            .insert(session_id, ActiveHttp2Session { command_tx });
19238                        session_id
19239                    };
19240                    spawn_http2_server_session(
19241                        Arc::clone(&shared),
19242                        server_id,
19243                        session_id,
19244                        stream,
19245                        tls,
19246                        session_snapshot,
19247                        command_rx,
19248                    );
19249                }
19250                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
19251                    thread::sleep(HTTP2_POLL_DELAY);
19252                }
19253                Err(error) => {
19254                    push_http2_server_event(
19255                        &shared,
19256                        server_id,
19257                        Http2BridgeEvent {
19258                            kind: String::from("serverStreamError"),
19259                            id: server_id,
19260                            data: Some(http2_error_payload(error.to_string())),
19261                            ..Http2BridgeEvent::default()
19262                        },
19263                    );
19264                    thread::sleep(HTTP2_POLL_DELAY);
19265                }
19266            }
19267        }
19268    });
19269}
19270
19271fn send_http2_command(
19272    session: &ActiveHttp2Session,
19273    command: impl FnOnce(Sender<Result<Value, String>>) -> Http2SessionCommand,
19274) -> Result<Value, SidecarError> {
19275    let (respond_to, response_rx) = mpsc::channel();
19276    session.command_tx.send(command(respond_to)).map_err(|_| {
19277        SidecarError::InvalidState(String::from("HTTP/2 session command channel closed"))
19278    })?;
19279    response_rx
19280        .recv_timeout(Duration::from_secs(30))
19281        .map_err(|_| {
19282            SidecarError::Execution(String::from("timed out waiting for HTTP/2 session command"))
19283        })?
19284        .map_err(SidecarError::Execution)
19285}
19286
19287fn parse_http2_server_listen_payload(
19288    request: &JavascriptSyncRpcRequest,
19289) -> Result<JavascriptHttp2ServerListenRequest, SidecarError> {
19290    let payload_json =
19291        javascript_sync_rpc_arg_str(&request.args, 0, "net.http2_server_listen payload")?;
19292    serde_json::from_str(payload_json).map_err(|error| {
19293        SidecarError::InvalidState(format!(
19294            "net.http2_server_listen payload must be valid JSON: {error}"
19295        ))
19296    })
19297}
19298
19299fn parse_http2_connect_payload(
19300    request: &JavascriptSyncRpcRequest,
19301) -> Result<JavascriptHttp2SessionConnectRequest, SidecarError> {
19302    let payload_json =
19303        javascript_sync_rpc_arg_str(&request.args, 0, "net.http2_session_connect payload")?;
19304    serde_json::from_str(payload_json).map_err(|error| {
19305        SidecarError::InvalidState(format!(
19306            "net.http2_session_connect payload must be valid JSON: {error}"
19307        ))
19308    })
19309}
19310
19311fn http2_session_for_id(
19312    process: &ActiveProcess,
19313    session_id: u64,
19314) -> Result<ActiveHttp2Session, SidecarError> {
19315    let shared = process
19316        .http2
19317        .shared
19318        .lock()
19319        .map_err(|_| SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned")))?;
19320    shared
19321        .sessions
19322        .get(&session_id)
19323        .cloned()
19324        .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 session {session_id}")))
19325}
19326
19327fn http2_stream_for_id(
19328    process: &ActiveProcess,
19329    stream_id: u64,
19330) -> Result<ActiveHttp2Stream, SidecarError> {
19331    let shared = process
19332        .http2
19333        .shared
19334        .lock()
19335        .map_err(|_| SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned")))?;
19336    shared
19337        .streams
19338        .get(&stream_id)
19339        .cloned()
19340        .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 stream {stream_id}")))
19341}
19342
19343fn service_javascript_http2_sync_rpc<B>(
19344    request: JavascriptHttp2SyncRpcServiceRequest<'_, B>,
19345) -> Result<Value, SidecarError>
19346where
19347    B: NativeSidecarBridge + Send + 'static,
19348    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19349{
19350    let JavascriptHttp2SyncRpcServiceRequest {
19351        bridge,
19352        kernel,
19353        vm_id,
19354        dns,
19355        socket_paths,
19356        process,
19357        sync_request: request,
19358        resource_limits,
19359        network_counts,
19360    } = request;
19361    match request.method.as_str() {
19362        "net.http2_server_listen" => {
19363            check_network_resource_limit(
19364                resource_limits.max_sockets,
19365                network_counts.sockets,
19366                1,
19367                "socket",
19368            )?;
19369            let payload = parse_http2_server_listen_payload(request)?;
19370            let (family, bind_host, guest_host) =
19371                normalize_tcp_listen_host(payload.host.as_deref())?;
19372            let requested_port = payload.port.unwrap_or(0);
19373            bridge.require_network_access(
19374                vm_id,
19375                NetworkOperation::Listen,
19376                format_tcp_resource(bind_host, requested_port),
19377            )?;
19378            let port = allocate_guest_listen_port(
19379                requested_port,
19380                family,
19381                &socket_paths.used_tcp_guest_ports,
19382                socket_paths.listen_policy,
19383            )?;
19384            let mut listener =
19385                ActiveTcpListener::bind(bind_host, guest_host, port, payload.backlog)?;
19386            let guest_local_addr = listener.guest_local_addr();
19387            let closed = Arc::new(AtomicBool::new(false));
19388            {
19389                let mut state = process.http2.shared.lock().map_err(|_| {
19390                    SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19391                })?;
19392                state.servers.insert(
19393                    payload.server_id,
19394                    ActiveHttp2Server {
19395                        actual_local_addr: listener.local_addr(),
19396                        guest_local_addr,
19397                        secure: payload.secure,
19398                        tls: payload.tls.clone().map(|mut tls| {
19399                            tls.is_server = payload.secure;
19400                            if payload.secure && tls.alpn_protocols.is_none() {
19401                                tls.alpn_protocols = Some(vec![String::from("h2")]);
19402                            }
19403                            tls
19404                        }),
19405                        closed: Arc::clone(&closed),
19406                    },
19407                );
19408                state.server_events.entry(payload.server_id).or_default();
19409            }
19410            spawn_http2_server_accept_loop(
19411                Arc::clone(&process.http2.shared),
19412                payload.server_id,
19413                listener.listener.take().ok_or_else(|| {
19414                    SidecarError::InvalidState(String::from(
19415                        "HTTP/2 listener missing host TCP socket",
19416                    ))
19417                })?,
19418            );
19419            javascript_net_json_string(
19420                json!({
19421                    "address": {
19422                        "address": guest_local_addr.ip().to_string(),
19423                        "family": socket_addr_family(&guest_local_addr),
19424                        "port": guest_local_addr.port(),
19425                    }
19426                }),
19427                "net.http2_server_listen",
19428            )
19429        }
19430        "net.http2_server_poll" => {
19431            let server_id =
19432                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_poll server id")?;
19433            let wait_ms = javascript_sync_rpc_arg_u64_optional(
19434                &request.args,
19435                1,
19436                "net.http2_server_poll wait ms",
19437            )?
19438            .unwrap_or_default();
19439            match wait_for_http2_event(&process.http2.shared, server_id, true, wait_ms) {
19440                Some(event) => http2_event_value(&event),
19441                None => Ok(Value::Null),
19442            }
19443        }
19444        "net.http2_server_wait" => {
19445            let server_id =
19446                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_wait server id")?;
19447            dispatch_http2_wait_loop(process, server_id, true)
19448        }
19449        "net.http2_server_close" => {
19450            let server_id =
19451                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_close server id")?;
19452            let server = {
19453                let mut state = process.http2.shared.lock().map_err(|_| {
19454                    SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19455                })?;
19456                state.servers.remove(&server_id)
19457            }
19458            .ok_or_else(|| {
19459                SidecarError::InvalidState(format!("unknown HTTP/2 server {server_id}"))
19460            })?;
19461            server.closed.store(true, Ordering::SeqCst);
19462            push_http2_server_event(
19463                &process.http2.shared,
19464                server_id,
19465                Http2BridgeEvent {
19466                    kind: String::from("serverClose"),
19467                    id: server_id,
19468                    ..Http2BridgeEvent::default()
19469                },
19470            );
19471            Ok(Value::Null)
19472        }
19473        "net.http2_server_respond" => {
19474            let server_id = javascript_sync_rpc_arg_u64(
19475                &request.args,
19476                0,
19477                "net.http2_server_respond server id",
19478            )?;
19479            let request_id = javascript_sync_rpc_arg_u64(
19480                &request.args,
19481                1,
19482                "net.http2_server_respond request id",
19483            )?;
19484            let response_json =
19485                javascript_sync_rpc_arg_str(&request.args, 2, "net.http2_server_respond payload")?;
19486            ensure_vm_fetch_response_within_limit(
19487                response_json,
19488                "net.http2_server_respond",
19489                VM_FETCH_BUFFER_LIMIT_BYTES,
19490            )?;
19491            serde_json::from_str::<Value>(response_json).map_err(|error| {
19492                SidecarError::Execution(format!(
19493                    "net.http2_server_respond payload must be valid JSON: {error}"
19494                ))
19495            })?;
19496            let Some(pending) = process
19497                .pending_http_requests
19498                .get_mut(&(server_id, request_id))
19499            else {
19500                return Err(SidecarError::InvalidState(format!(
19501                    "unknown pending HTTP/2 request {request_id} for server {server_id}"
19502                )));
19503            };
19504            *pending = Some(response_json.to_owned());
19505            Ok(Value::Bool(true))
19506        }
19507        "net.http2_session_connect" => {
19508            check_network_resource_limit(
19509                resource_limits.max_sockets,
19510                network_counts.sockets,
19511                1,
19512                "socket",
19513            )?;
19514            check_network_resource_limit(
19515                resource_limits.max_connections,
19516                network_counts.connections,
19517                1,
19518                "connection",
19519            )?;
19520            let payload = parse_http2_connect_payload(request)?;
19521            let authority = payload.authority.clone().unwrap_or_else(|| {
19522                format!(
19523                    "{}://{}:{}",
19524                    payload.protocol.as_deref().unwrap_or("http"),
19525                    payload.host.as_deref().unwrap_or("localhost"),
19526                    payload.port.unwrap_or(80)
19527                )
19528            });
19529            let url = Url::parse(&authority).map_err(|error| {
19530                SidecarError::InvalidState(format!(
19531                    "invalid HTTP/2 authority {authority:?}: {error}"
19532                ))
19533            })?;
19534            let secure = url.scheme() == "https" || payload.protocol.as_deref() == Some("https:");
19535            let host = payload
19536                .host
19537                .as_deref()
19538                .or_else(|| url.host_str())
19539                .unwrap_or("localhost");
19540            let port = payload.port.or_else(|| url.port()).unwrap_or(80);
19541            bridge.require_network_access(
19542                vm_id,
19543                NetworkOperation::Http,
19544                format_tcp_resource(host, port),
19545            )?;
19546            let resolved = {
19547                let shared = process.http2.shared.lock().map_err(|_| {
19548                    SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19549                })?;
19550                shared
19551                    .servers
19552                    .values()
19553                    .find(|server| {
19554                        is_loopback_request_host(host) && server.guest_local_addr.port() == port
19555                    })
19556                    .map(|server| ResolvedTcpConnectAddr {
19557                        actual_addr: server.actual_local_addr,
19558                        guest_remote_addr: server.guest_local_addr,
19559                        use_kernel_loopback: false,
19560                    })
19561            };
19562            let resolved = match resolved {
19563                Some(resolved) => resolved,
19564                None => {
19565                    resolve_tcp_connect_addr(bridge, kernel, vm_id, dns, host, port, socket_paths)?
19566                }
19567            };
19568            let (command_tx, command_rx) = unbounded_channel();
19569            let snapshot = Arc::new(Mutex::new(Http2SessionSnapshot {
19570                encrypted: secure,
19571                alpn_protocol: Some(String::from(if secure { "h2" } else { "h2c" })),
19572                local_settings: http2_settings_from_value(&payload.settings),
19573                remote_settings: BTreeMap::new(),
19574                state: http2_runtime_snapshot(),
19575                socket: Http2SocketSnapshot {
19576                    encrypted: secure,
19577                    remote_address: Some(resolved.guest_remote_addr.ip().to_string()),
19578                    remote_port: Some(resolved.guest_remote_addr.port()),
19579                    remote_family: Some(
19580                        socket_addr_family(&resolved.guest_remote_addr).to_string(),
19581                    ),
19582                    servername: if secure {
19583                        payload
19584                            .tls
19585                            .as_ref()
19586                            .and_then(|tls| tls.servername.clone())
19587                            .or_else(|| Some(host.to_string()))
19588                    } else {
19589                        None
19590                    },
19591                    alpn_protocol: Some(String::from(if secure { "h2" } else { "h2c" })),
19592                    ..Http2SocketSnapshot::default()
19593                },
19594                ..Http2SessionSnapshot::default()
19595            }));
19596            let session_id = {
19597                let mut state = process.http2.shared.lock().map_err(|_| {
19598                    SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19599                })?;
19600                let session_id = next_http2_session_id(&mut state);
19601                state
19602                    .sessions
19603                    .insert(session_id, ActiveHttp2Session { command_tx });
19604                state.session_events.entry(session_id).or_default();
19605                session_id
19606            };
19607            spawn_http2_client_session(
19608                Arc::clone(&process.http2.shared),
19609                session_id,
19610                resolved.actual_addr,
19611                if secure {
19612                    Some(payload.tls.unwrap_or(JavascriptTlsBridgeOptions {
19613                        is_server: false,
19614                        servername: Some(host.to_string()),
19615                        alpn_protocols: Some(vec![String::from("h2")]),
19616                        ..JavascriptTlsBridgeOptions::default()
19617                    }))
19618                } else {
19619                    None
19620                },
19621                Arc::clone(&snapshot),
19622                command_rx,
19623            );
19624            let snapshot_json =
19625                http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())?;
19626            javascript_net_json_string(
19627                json!({
19628                    "sessionId": session_id,
19629                    "state": snapshot_json,
19630                }),
19631                "net.http2_session_connect",
19632            )
19633        }
19634        "net.http2_session_request" => {
19635            let session_id = javascript_sync_rpc_arg_u64(
19636                &request.args,
19637                0,
19638                "net.http2_session_request session id",
19639            )?;
19640            let headers_json =
19641                javascript_sync_rpc_arg_str(&request.args, 1, "net.http2_session_request headers")?;
19642            let options_json =
19643                javascript_sync_rpc_arg_str(&request.args, 2, "net.http2_session_request options")?;
19644            let session = http2_session_for_id(process, session_id)?;
19645            send_http2_command(&session, |respond_to| Http2SessionCommand::Request {
19646                headers_json: headers_json.to_owned(),
19647                options_json: options_json.to_owned(),
19648                respond_to,
19649            })
19650        }
19651        "net.http2_session_settings" => {
19652            let session_id = javascript_sync_rpc_arg_u64(
19653                &request.args,
19654                0,
19655                "net.http2_session_settings session id",
19656            )?;
19657            let settings_json = javascript_sync_rpc_arg_str(
19658                &request.args,
19659                1,
19660                "net.http2_session_settings settings",
19661            )?;
19662            let session = http2_session_for_id(process, session_id)?;
19663            send_http2_command(&session, |respond_to| Http2SessionCommand::Settings {
19664                settings_json: settings_json.to_owned(),
19665                respond_to,
19666            })
19667        }
19668        "net.http2_session_set_local_window_size" => {
19669            let session_id = javascript_sync_rpc_arg_u64(
19670                &request.args,
19671                0,
19672                "net.http2_session_set_local_window_size session id",
19673            )?;
19674            let window_size = javascript_sync_rpc_arg_u64(
19675                &request.args,
19676                1,
19677                "net.http2_session_set_local_window_size window size",
19678            )?;
19679            let session = http2_session_for_id(process, session_id)?;
19680            send_http2_command(&session, |respond_to| {
19681                Http2SessionCommand::SetLocalWindowSize {
19682                    size: window_size as u32,
19683                    respond_to,
19684                }
19685            })
19686        }
19687        "net.http2_session_goaway" => {
19688            let session_id = javascript_sync_rpc_arg_u64(
19689                &request.args,
19690                0,
19691                "net.http2_session_goaway session id",
19692            )?;
19693            let error_code = javascript_sync_rpc_arg_u64(
19694                &request.args,
19695                1,
19696                "net.http2_session_goaway error code",
19697            )?;
19698            let last_stream_id = javascript_sync_rpc_arg_u64(
19699                &request.args,
19700                2,
19701                "net.http2_session_goaway last stream id",
19702            )?;
19703            let opaque_data = request
19704                .args
19705                .get(3)
19706                .and_then(Value::as_str)
19707                .map(|value| {
19708                    base64::engine::general_purpose::STANDARD
19709                        .decode(value)
19710                        .map_err(|error| {
19711                            SidecarError::InvalidState(format!("invalid GOAWAY payload: {error}"))
19712                        })
19713                })
19714                .transpose()?;
19715            let session = http2_session_for_id(process, session_id)?;
19716            send_http2_command(&session, |respond_to| Http2SessionCommand::Goaway {
19717                error_code: error_code as u32,
19718                last_stream_id: last_stream_id as u32,
19719                opaque_data,
19720                respond_to,
19721            })
19722        }
19723        "net.http2_session_close" | "net.http2_session_destroy" => {
19724            let session_id = javascript_sync_rpc_arg_u64(
19725                &request.args,
19726                0,
19727                "net.http2_session_close session id",
19728            )?;
19729            let session = http2_session_for_id(process, session_id)?;
19730            send_http2_command(&session, |respond_to| Http2SessionCommand::Close {
19731                abrupt: request.method == "net.http2_session_destroy",
19732                respond_to,
19733            })
19734        }
19735        "net.http2_session_poll" => {
19736            let session_id =
19737                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_session_poll session id")?;
19738            let wait_ms = javascript_sync_rpc_arg_u64_optional(
19739                &request.args,
19740                1,
19741                "net.http2_session_poll wait ms",
19742            )?
19743            .unwrap_or_default();
19744            match wait_for_http2_event(&process.http2.shared, session_id, false, wait_ms) {
19745                Some(event) => http2_event_value(&event),
19746                None => Ok(Value::Null),
19747            }
19748        }
19749        "net.http2_session_wait" => {
19750            let session_id =
19751                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_session_wait session id")?;
19752            dispatch_http2_wait_loop(process, session_id, false)
19753        }
19754        "net.http2_stream_respond" => {
19755            let stream_id = javascript_sync_rpc_arg_u64(
19756                &request.args,
19757                0,
19758                "net.http2_stream_respond stream id",
19759            )?;
19760            let headers_json =
19761                javascript_sync_rpc_arg_str(&request.args, 1, "net.http2_stream_respond headers")?;
19762            let stream = http2_stream_for_id(process, stream_id)?;
19763            let session = http2_session_for_id(process, stream.session_id)?;
19764            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamRespond {
19765                stream_id,
19766                headers_json: headers_json.to_owned(),
19767                respond_to,
19768            })
19769        }
19770        "net.http2_stream_push_stream" => {
19771            let stream_id = javascript_sync_rpc_arg_u64(
19772                &request.args,
19773                0,
19774                "net.http2_stream_push_stream stream id",
19775            )?;
19776            let headers_json = javascript_sync_rpc_arg_str(
19777                &request.args,
19778                1,
19779                "net.http2_stream_push_stream headers",
19780            )?;
19781            let _options_json = javascript_sync_rpc_arg_str(
19782                &request.args,
19783                2,
19784                "net.http2_stream_push_stream options",
19785            )?;
19786            let stream = http2_stream_for_id(process, stream_id)?;
19787            let session = http2_session_for_id(process, stream.session_id)?;
19788            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamPush {
19789                stream_id,
19790                headers_json: headers_json.to_owned(),
19791                respond_to,
19792            })
19793        }
19794        "net.http2_stream_write" => {
19795            let stream_id =
19796                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_write stream id")?;
19797            let chunk =
19798                javascript_sync_rpc_base64_arg(&request.args, 1, "net.http2_stream_write data")?;
19799            let stream = http2_stream_for_id(process, stream_id)?;
19800            let session = http2_session_for_id(process, stream.session_id)?;
19801            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamWrite {
19802                stream_id,
19803                chunk,
19804                end_stream: false,
19805                respond_to,
19806            })
19807        }
19808        "net.http2_stream_end" => {
19809            let stream_id =
19810                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_end stream id")?;
19811            let chunk = request
19812                .args
19813                .get(1)
19814                .and_then(Value::as_str)
19815                .map(|value| {
19816                    base64::engine::general_purpose::STANDARD
19817                        .decode(value)
19818                        .map_err(|error| {
19819                            SidecarError::InvalidState(format!(
19820                                "invalid HTTP/2 stream payload: {error}"
19821                            ))
19822                        })
19823                })
19824                .transpose()?
19825                .unwrap_or_default();
19826            let stream = http2_stream_for_id(process, stream_id)?;
19827            let session = http2_session_for_id(process, stream.session_id)?;
19828            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamWrite {
19829                stream_id,
19830                chunk,
19831                end_stream: true,
19832                respond_to,
19833            })
19834        }
19835        "net.http2_stream_close" => {
19836            let stream_id =
19837                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_close stream id")?;
19838            let code = javascript_sync_rpc_arg_u64_optional(
19839                &request.args,
19840                1,
19841                "net.http2_stream_close error code",
19842            )?
19843            .map(|value| value as u32);
19844            let stream = http2_stream_for_id(process, stream_id)?;
19845            let session = http2_session_for_id(process, stream.session_id)?;
19846            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamClose {
19847                stream_id,
19848                error_code: code,
19849                respond_to,
19850            })
19851        }
19852        "net.http2_stream_pause" => {
19853            let stream_id =
19854                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_pause stream id")?;
19855            let stream = http2_stream_for_id(process, stream_id)?;
19856            stream.paused.store(true, Ordering::SeqCst);
19857            Ok(Value::Null)
19858        }
19859        "net.http2_stream_resume" => {
19860            let stream_id =
19861                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_resume stream id")?;
19862            let stream = http2_stream_for_id(process, stream_id)?;
19863            stream.paused.store(false, Ordering::SeqCst);
19864            Ok(Value::Null)
19865        }
19866        "net.http2_stream_respond_with_file" => {
19867            let stream_id = javascript_sync_rpc_arg_u64(
19868                &request.args,
19869                0,
19870                "net.http2_stream_respond_with_file stream id",
19871            )?;
19872            let path = javascript_sync_rpc_arg_str(
19873                &request.args,
19874                1,
19875                "net.http2_stream_respond_with_file path",
19876            )?;
19877            let headers_json = javascript_sync_rpc_arg_str(
19878                &request.args,
19879                2,
19880                "net.http2_stream_respond_with_file headers",
19881            )?;
19882            let options_json = javascript_sync_rpc_arg_str(
19883                &request.args,
19884                3,
19885                "net.http2_stream_respond_with_file options",
19886            )?;
19887            let stream = http2_stream_for_id(process, stream_id)?;
19888            let session = http2_session_for_id(process, stream.session_id)?;
19889            let guest_path = resolve_http2_file_response_guest_path(process, path);
19890            let body = kernel.read_file(&guest_path).map_err(kernel_error)?;
19891            send_http2_command(&session, |respond_to| {
19892                Http2SessionCommand::StreamRespondWithFile {
19893                    stream_id,
19894                    body,
19895                    headers_json: headers_json.to_owned(),
19896                    options_json: options_json.to_owned(),
19897                    respond_to,
19898                }
19899            })
19900        }
19901        other => Err(SidecarError::InvalidState(format!(
19902            "unsupported JavaScript HTTP/2 sync RPC method {other}"
19903        ))),
19904    }
19905}
19906
19907const JAVASCRIPT_NET_POLL_MAX_WAIT: Duration = Duration::from_millis(50);
19908const EXITED_PROCESS_SNAPSHOT_RETENTION: Duration = Duration::from_secs(2);
19909
19910fn resolve_http2_file_response_guest_path(process: &ActiveProcess, path: &str) -> String {
19911    if Path::new(path).is_absolute() {
19912        normalize_path(path)
19913    } else {
19914        normalize_path(&format!("{}/{}", process.guest_cwd, path))
19915    }
19916}
19917
19918pub(crate) fn clamp_javascript_net_poll_wait(wait_ms: u64) -> Duration {
19919    // WASM net.poll runs on the sidecar's sync-RPC main thread. Guest-controlled waits
19920    // must stay bounded so one VM cannot stall dispose/shutdown or unrelated VM work.
19921    if wait_ms == 0 {
19922        Duration::ZERO
19923    } else {
19924        Duration::from_millis(wait_ms).min(JAVASCRIPT_NET_POLL_MAX_WAIT)
19925    }
19926}
19927
19928pub(crate) fn service_javascript_net_sync_rpc<B>(
19929    request: JavascriptNetSyncRpcServiceRequest<'_, B>,
19930) -> Result<Value, SidecarError>
19931where
19932    B: NativeSidecarBridge + Send + 'static,
19933    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19934{
19935    let JavascriptNetSyncRpcServiceRequest {
19936        bridge,
19937        vm_id,
19938        dns,
19939        socket_paths,
19940        kernel,
19941        process,
19942        sync_request: request,
19943        resource_limits,
19944        network_counts,
19945    } = request;
19946    match request.method.as_str() {
19947        "net.http_listen" => {
19948            check_network_resource_limit(
19949                resource_limits.max_sockets,
19950                network_counts.sockets,
19951                1,
19952                "socket",
19953            )?;
19954            let payload_json =
19955                javascript_sync_rpc_arg_str(&request.args, 0, "net.http_listen payload")?;
19956            let payload: JavascriptHttpListenRequest =
19957                serde_json::from_str(payload_json).map_err(|error| {
19958                    SidecarError::InvalidState(format!(
19959                        "net.http_listen payload must be valid JSON: {error}"
19960                    ))
19961                })?;
19962            let (family, bind_host, guest_host) =
19963                normalize_tcp_listen_host(payload.hostname.as_deref())?;
19964            let requested_port = payload.port.unwrap_or(0);
19965            bridge.require_network_access(
19966                vm_id,
19967                NetworkOperation::Listen,
19968                format_tcp_resource(bind_host, requested_port),
19969            )?;
19970            let port = allocate_guest_listen_port(
19971                requested_port,
19972                family,
19973                &socket_paths.used_tcp_guest_ports,
19974                socket_paths.listen_policy,
19975            )?;
19976            let mut listener = ActiveTcpListener::bind(
19977                bind_host,
19978                guest_host,
19979                port,
19980                Some(DEFAULT_JAVASCRIPT_NET_BACKLOG),
19981            )?;
19982            let guest_local_addr = listener.guest_local_addr();
19983            process.http_servers.insert(
19984                payload.server_id,
19985                ActiveHttpServer {
19986                    listener: listener.listener.take().ok_or_else(|| {
19987                        SidecarError::InvalidState(String::from(
19988                            "HTTP listener missing host TCP socket",
19989                        ))
19990                    })?,
19991                    guest_local_addr,
19992                    next_request_id: 0,
19993                },
19994            );
19995            serde_json::to_string(&json!({
19996                "address": {
19997                    "address": guest_local_addr.ip().to_string(),
19998                    "family": socket_addr_family(&guest_local_addr),
19999                    "port": guest_local_addr.port(),
20000                }
20001            }))
20002            .map(Value::String)
20003            .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
20004        }
20005        "net.http_close" => {
20006            let server_id =
20007                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_close server id")?;
20008            let server = process.http_servers.remove(&server_id).ok_or_else(|| {
20009                SidecarError::InvalidState(format!("unknown HTTP server {server_id}"))
20010            })?;
20011            drop(server.listener);
20012            process
20013                .pending_http_requests
20014                .retain(|(pending_server_id, _), _| *pending_server_id != server_id);
20015            Ok(Value::Null)
20016        }
20017        "net.http_wait" => {
20018            let server_id =
20019                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_wait server id")?;
20020            dispatch_http_wait_loop(process, server_id)
20021        }
20022        "net.http_respond" => {
20023            let server_id =
20024                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_respond server id")?;
20025            let request_id =
20026                javascript_sync_rpc_arg_u64(&request.args, 1, "net.http_respond request id")?;
20027            let response_json =
20028                javascript_sync_rpc_arg_str(&request.args, 2, "net.http_respond payload")?;
20029            ensure_vm_fetch_response_within_limit(
20030                response_json,
20031                "net.http_respond",
20032                VM_FETCH_BUFFER_LIMIT_BYTES,
20033            )?;
20034            serde_json::from_str::<Value>(response_json).map_err(|error| {
20035                SidecarError::Execution(format!(
20036                    "net.http_respond payload must be valid JSON: {error}"
20037                ))
20038            })?;
20039            let Some(pending) = process
20040                .pending_http_requests
20041                .get_mut(&(server_id, request_id))
20042            else {
20043                return Err(SidecarError::InvalidState(format!(
20044                    "unknown pending HTTP request {request_id} for server {server_id}"
20045                )));
20046            };
20047            *pending = Some(response_json.to_owned());
20048            Ok(Value::Null)
20049        }
20050        "net.reserve_tcp_port" => {
20051            let payload = request
20052                .args
20053                .first()
20054                .cloned()
20055                .ok_or_else(|| {
20056                    SidecarError::InvalidState(String::from(
20057                        "net.reserve_tcp_port requires a request payload",
20058                    ))
20059                })
20060                .and_then(|value| {
20061                    serde_json::from_value::<JavascriptNetReserveTcpPortRequest>(value).map_err(
20062                        |error| {
20063                            SidecarError::InvalidState(format!(
20064                                "invalid net.reserve_tcp_port payload: {error}"
20065                            ))
20066                        },
20067                    )
20068                })?;
20069            let (family, _bind_host, guest_host) =
20070                normalize_tcp_listen_host(payload.host.as_deref())?;
20071            let requested_port = payload.port.unwrap_or(0);
20072            let port = allocate_guest_listen_port(
20073                requested_port,
20074                family,
20075                &socket_paths.used_tcp_guest_ports,
20076                socket_paths.listen_policy,
20077            )?;
20078            let reservation_id = process.allocate_tcp_port_reservation_id();
20079            process
20080                .tcp_port_reservations
20081                .insert(reservation_id.clone(), (family, port));
20082            Ok(json!({
20083                "reservationId": reservation_id,
20084                "localAddress": guest_host,
20085                "localPort": port,
20086                "family": match family {
20087                    JavascriptSocketFamily::Ipv4 => "IPv4",
20088                    JavascriptSocketFamily::Ipv6 => "IPv6",
20089                },
20090            }))
20091        }
20092        "net.release_tcp_port" => {
20093            let reservation_id =
20094                javascript_sync_rpc_arg_str(&request.args, 0, "net.release_tcp_port reservation")?;
20095            process.tcp_port_reservations.remove(reservation_id);
20096            Ok(Value::Null)
20097        }
20098        "net.connect" => {
20099            check_network_resource_limit(
20100                resource_limits.max_sockets,
20101                network_counts.sockets,
20102                1,
20103                "socket",
20104            )?;
20105            check_network_resource_limit(
20106                resource_limits.max_connections,
20107                network_counts.connections,
20108                1,
20109                "connection",
20110            )?;
20111            let payload = request
20112                .args
20113                .first()
20114                .cloned()
20115                .ok_or_else(|| {
20116                    SidecarError::InvalidState(String::from(
20117                        "net.connect requires a request payload",
20118                    ))
20119                })
20120                .and_then(|value| {
20121                    serde_json::from_value::<JavascriptNetConnectRequest>(value).map_err(|error| {
20122                        SidecarError::InvalidState(format!("invalid net.connect payload: {error}"))
20123                    })
20124                })?;
20125            if let Some(path) = payload.path.as_deref() {
20126                let guest_path = normalize_path(path);
20127                let host_path = resolve_guest_socket_host_path(socket_paths, &guest_path);
20128                let socket = ActiveUnixSocket::connect(&host_path, &guest_path)?;
20129                let socket_id = process.allocate_unix_socket_id();
20130                process.unix_sockets.insert(socket_id.clone(), socket);
20131                Ok(json!({
20132                    "socketId": socket_id,
20133                    "remotePath": guest_path,
20134                }))
20135            } else {
20136                let port = payload.port.ok_or_else(|| {
20137                    SidecarError::InvalidState(String::from(
20138                        "net.connect requires either a path or port",
20139                    ))
20140                })?;
20141                let host = payload.host.as_deref().unwrap_or("localhost");
20142                let local_reservation = payload.local_reservation.as_deref().and_then(|id| {
20143                    process
20144                        .tcp_port_reservations
20145                        .remove(id)
20146                        .map(|reservation| (id.to_owned(), reservation))
20147                });
20148                bridge.require_network_access(
20149                    vm_id,
20150                    NetworkOperation::Http,
20151                    format_tcp_resource(host, port),
20152                )?;
20153                if is_loopback_socket_host(host) {
20154                    let families = [JavascriptSocketFamily::Ipv4, JavascriptSocketFamily::Ipv6];
20155                    if let Some((family, target)) = families.iter().find_map(|family| {
20156                        socket_paths
20157                            .http_loopback_target(*family, port)
20158                            .map(|target| (*family, target))
20159                    }) {
20160                        if let Some((reservation_id, reservation)) = local_reservation {
20161                            process
20162                                .tcp_port_reservations
20163                                .insert(reservation_id, reservation);
20164                        }
20165                        let remote_address = match family {
20166                            JavascriptSocketFamily::Ipv4 => "127.0.0.1",
20167                            JavascriptSocketFamily::Ipv6 => "::1",
20168                        };
20169                        return Ok(json!({
20170                            "loopbackHttpTarget": {
20171                                "processId": target.process_id.clone(),
20172                                "serverId": target.server_id,
20173                                "host": remote_address,
20174                                "port": port,
20175                            },
20176                            "localAddress": match family {
20177                                JavascriptSocketFamily::Ipv4 => "127.0.0.1",
20178                                JavascriptSocketFamily::Ipv6 => "::1",
20179                            },
20180                            "localPort": payload.local_port.unwrap_or(0),
20181                            "remoteAddress": remote_address,
20182                            "remotePort": port,
20183                            "remoteFamily": match family {
20184                                JavascriptSocketFamily::Ipv4 => "IPv4",
20185                                JavascriptSocketFamily::Ipv6 => "IPv6",
20186                            },
20187                        }));
20188                    }
20189                }
20190                let connect_result = ActiveTcpSocket::connect(ActiveTcpConnectRequest {
20191                    bridge,
20192                    kernel,
20193                    kernel_pid: process.kernel_pid,
20194                    vm_id,
20195                    dns,
20196                    host,
20197                    port,
20198                    local_address: payload.local_address.as_deref(),
20199                    local_port: payload.local_port,
20200                    local_reservation: local_reservation
20201                        .as_ref()
20202                        .map(|(_, reservation)| *reservation),
20203                    context: socket_paths,
20204                });
20205                if let Err(error) = connect_result {
20206                    if let Some((reservation_id, reservation)) = local_reservation {
20207                        process
20208                            .tcp_port_reservations
20209                            .insert(reservation_id, reservation);
20210                    }
20211                    return Err(error);
20212                }
20213                let socket = connect_result?;
20214                let socket_id = process.allocate_tcp_socket_id();
20215                let local_addr = socket.guest_local_addr;
20216                let remote_addr = socket.guest_remote_addr;
20217                process.tcp_sockets.insert(socket_id.clone(), socket);
20218                Ok(json!({
20219                    "socketId": socket_id,
20220                    "localAddress": local_addr.ip().to_string(),
20221                    "localPort": local_addr.port(),
20222                    "remoteAddress": remote_addr.ip().to_string(),
20223                    "remotePort": remote_addr.port(),
20224                    "remoteFamily": socket_addr_family(&remote_addr),
20225                }))
20226            }
20227        }
20228        "net.listen" => {
20229            check_network_resource_limit(
20230                resource_limits.max_sockets,
20231                network_counts.sockets,
20232                1,
20233                "socket",
20234            )?;
20235            let payload = request
20236                .args
20237                .first()
20238                .cloned()
20239                .ok_or_else(|| {
20240                    SidecarError::InvalidState(String::from(
20241                        "net.listen requires a request payload",
20242                    ))
20243                })
20244                .and_then(|value| match value {
20245                    Value::String(json) => {
20246                        serde_json::from_str::<JavascriptNetListenRequest>(&json).map_err(|error| {
20247                            SidecarError::InvalidState(format!(
20248                                "invalid net.listen payload: {error}"
20249                            ))
20250                        })
20251                    }
20252                    other => serde_json::from_value::<JavascriptNetListenRequest>(other).map_err(
20253                        |error| {
20254                            SidecarError::InvalidState(format!(
20255                                "invalid net.listen payload: {error}"
20256                            ))
20257                        },
20258                    ),
20259                })?;
20260            if let Some(path) = payload.path.as_deref() {
20261                let guest_path = normalize_path(path);
20262                if kernel.exists(&guest_path).map_err(kernel_error)? {
20263                    return Err(sidecar_net_error(std::io::Error::from_raw_os_error(
20264                        libc::EADDRINUSE,
20265                    )));
20266                }
20267
20268                let host_path = resolve_guest_socket_host_path(socket_paths, &guest_path);
20269                let on_host_mount =
20270                    host_mount_path_for_guest_path_from_mounts(&socket_paths.mounts, &guest_path)
20271                        .is_some();
20272                let listener = ActiveUnixListener::bind(&host_path, &guest_path, payload.backlog)?;
20273                if !on_host_mount {
20274                    ensure_kernel_parent_directories(kernel, &guest_path)?;
20275                    kernel
20276                        .write_file(&guest_path, Vec::new())
20277                        .map_err(kernel_error)?;
20278                }
20279                let listener_id = process.allocate_unix_listener_id();
20280                process.unix_listeners.insert(listener_id.clone(), listener);
20281                Ok(json!({
20282                    "serverId": listener_id,
20283                    "path": guest_path,
20284                }))
20285            } else {
20286                let (family, bind_host, guest_host) =
20287                    normalize_tcp_listen_host(payload.host.as_deref())?;
20288                let requested_port = payload.port.unwrap_or(0);
20289                bridge.require_network_access(
20290                    vm_id,
20291                    NetworkOperation::Listen,
20292                    format_tcp_resource(bind_host, requested_port),
20293                )?;
20294                let local_reservation = payload.local_reservation.as_deref().and_then(|id| {
20295                    process
20296                        .tcp_port_reservations
20297                        .remove(id)
20298                        .map(|reservation| (id.to_owned(), reservation))
20299                });
20300                let port = if requested_port != 0
20301                    && local_reservation
20302                        .as_ref()
20303                        .map(|(_, reservation)| *reservation)
20304                        == Some((family, requested_port))
20305                {
20306                    requested_port
20307                } else {
20308                    allocate_guest_listen_port(
20309                        requested_port,
20310                        family,
20311                        &socket_paths.used_tcp_guest_ports,
20312                        socket_paths.listen_policy,
20313                    )?
20314                };
20315                let listener_result = ActiveTcpListener::bind_kernel(
20316                    kernel,
20317                    process.kernel_pid,
20318                    guest_host,
20319                    port,
20320                    payload.backlog,
20321                );
20322                if let Err(error) = listener_result {
20323                    if let Some((reservation_id, reservation)) = local_reservation {
20324                        process
20325                            .tcp_port_reservations
20326                            .insert(reservation_id, reservation);
20327                    }
20328                    return Err(error);
20329                }
20330                let listener = listener_result?;
20331                let listener_id = process.allocate_tcp_listener_id();
20332                let local_addr = listener.guest_local_addr();
20333                process.tcp_listeners.insert(listener_id.clone(), listener);
20334                Ok(json!({
20335                    "serverId": listener_id,
20336                    "localAddress": local_addr.ip().to_string(),
20337                    "localPort": local_addr.port(),
20338                    "family": socket_addr_family(&local_addr),
20339                }))
20340            }
20341        }
20342        "net.poll" => {
20343            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.poll socket id")?;
20344            let wait_ms =
20345                javascript_sync_rpc_arg_u64_optional(&request.args, 1, "net.poll wait ms")?
20346                    .unwrap_or_default();
20347            let wait = clamp_javascript_net_poll_wait(wait_ms);
20348            let event = if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20349                socket.poll(kernel, process.kernel_pid, wait)?
20350            } else if let Some(socket) = process.unix_sockets.get_mut(socket_id) {
20351                socket.poll(wait)?
20352            } else {
20353                return Err(SidecarError::InvalidState(format!(
20354                    "unknown net socket {socket_id}"
20355                )));
20356            };
20357
20358            match event {
20359                Some(JavascriptTcpSocketEvent::Data(chunk)) => Ok(json!({
20360                    "type": "data",
20361                    "data": javascript_sync_rpc_bytes_value(&chunk),
20362                })),
20363                Some(JavascriptTcpSocketEvent::End) => Ok(json!({
20364                    "type": "end",
20365                })),
20366                Some(JavascriptTcpSocketEvent::Error { code, message }) => Ok(json!({
20367                    "type": "error",
20368                    "code": code,
20369                    "message": message,
20370                })),
20371                Some(JavascriptTcpSocketEvent::Close { had_error }) => {
20372                    if let Some(socket) = process.tcp_sockets.remove(socket_id) {
20373                        if let Some(listener_id) = socket.listener_id.as_deref() {
20374                            if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20375                                listener.release_connection(socket_id);
20376                            }
20377                        }
20378                    } else if let Some(socket) = process.unix_sockets.remove(socket_id) {
20379                        if let Some(listener_id) = socket.listener_id.as_deref() {
20380                            if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20381                                listener.release_connection(socket_id);
20382                            }
20383                        }
20384                    }
20385                    Ok(json!({
20386                        "type": "close",
20387                        "hadError": had_error,
20388                    }))
20389                }
20390                None => Ok(Value::Null),
20391            }
20392        }
20393        "net.socket_wait_connect" => {
20394            let socket_id =
20395                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_wait_connect socket id")?;
20396            if let Some(socket) = process.tcp_sockets.get(socket_id) {
20397                javascript_net_json_string(socket.socket_info(), "net.socket_wait_connect")
20398            } else {
20399                let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
20400                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20401                })?;
20402                javascript_net_json_string(socket.socket_info(), "net.socket_wait_connect")
20403            }
20404        }
20405        "net.socket_read" => {
20406            let socket_id =
20407                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_read socket id")?;
20408            if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20409                javascript_net_read_value(socket.poll(
20410                    kernel,
20411                    process.kernel_pid,
20412                    Duration::ZERO,
20413                )?)
20414            } else {
20415                let socket = process.unix_sockets.get_mut(socket_id).ok_or_else(|| {
20416                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20417                })?;
20418                javascript_net_read_value(socket.poll(Duration::ZERO)?)
20419            }
20420        }
20421        "net.socket_set_no_delay" => {
20422            let socket_id =
20423                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_set_no_delay socket id")?;
20424            let enable =
20425                javascript_sync_rpc_arg_bool(&request.args, 1, "net.socket_set_no_delay enabled")?;
20426            if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20427                socket.set_no_delay(enable)?;
20428            } else if !process.unix_sockets.contains_key(socket_id) {
20429                return Err(SidecarError::InvalidState(format!(
20430                    "unknown net socket {socket_id}"
20431                )));
20432            }
20433            Ok(Value::Null)
20434        }
20435        "net.socket_set_keep_alive" => {
20436            let socket_id = javascript_sync_rpc_arg_str(
20437                &request.args,
20438                0,
20439                "net.socket_set_keep_alive socket id",
20440            )?;
20441            let enable = javascript_sync_rpc_arg_bool(
20442                &request.args,
20443                1,
20444                "net.socket_set_keep_alive enabled",
20445            )?;
20446            let initial_delay_secs = javascript_sync_rpc_arg_u64_optional(
20447                &request.args,
20448                2,
20449                "net.socket_set_keep_alive initial delay seconds",
20450            )?;
20451            if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20452                socket.set_keep_alive(enable, initial_delay_secs)?;
20453            } else if !process.unix_sockets.contains_key(socket_id) {
20454                return Err(SidecarError::InvalidState(format!(
20455                    "unknown net socket {socket_id}"
20456                )));
20457            }
20458            Ok(Value::Null)
20459        }
20460        "net.socket_upgrade_tls" => {
20461            let socket_id =
20462                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_upgrade_tls socket id")?;
20463            let options_json =
20464                javascript_sync_rpc_arg_str(&request.args, 1, "net.socket_upgrade_tls options")?;
20465            let options: JavascriptTlsBridgeOptions =
20466                serde_json::from_str(options_json).map_err(|error| {
20467                    SidecarError::InvalidState(format!(
20468                        "net.socket_upgrade_tls options must be valid JSON: {error}"
20469                    ))
20470                })?;
20471            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20472                SidecarError::InvalidState(format!(
20473                    "unknown TCP socket {socket_id} for TLS upgrade"
20474                ))
20475            })?;
20476            socket.upgrade_tls(vm_id, kernel, options)?;
20477            Ok(Value::Null)
20478        }
20479        "net.socket_get_tls_client_hello" => {
20480            let socket_id = javascript_sync_rpc_arg_str(
20481                &request.args,
20482                0,
20483                "net.socket_get_tls_client_hello socket id",
20484            )?;
20485            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20486                SidecarError::InvalidState(format!(
20487                    "unknown TCP socket {socket_id} for TLS client hello query"
20488                ))
20489            })?;
20490            socket.tls_client_hello_json(vm_id, kernel)
20491        }
20492        "net.socket_tls_query" => {
20493            let socket_id =
20494                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_tls_query socket id")?;
20495            let query =
20496                javascript_sync_rpc_arg_str(&request.args, 1, "net.socket_tls_query query")?;
20497            let detailed = request
20498                .args
20499                .get(2)
20500                .and_then(Value::as_bool)
20501                .unwrap_or(false);
20502            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20503                SidecarError::InvalidState(format!("unknown TCP socket {socket_id} for TLS query"))
20504            })?;
20505            socket.tls_query(query, detailed)
20506        }
20507        "net.server_poll" => {
20508            let listener_id =
20509                javascript_sync_rpc_arg_str(&request.args, 0, "net.server_poll listener id")?;
20510            let wait_ms =
20511                javascript_sync_rpc_arg_u64_optional(&request.args, 1, "net.server_poll wait ms")?
20512                    .unwrap_or_default();
20513            let tcp_event = if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20514                Some(listener.poll(kernel, process.kernel_pid, Duration::from_millis(wait_ms))?)
20515            } else {
20516                None
20517            };
20518
20519            if let Some(event) = tcp_event {
20520                return match event {
20521                    Some(JavascriptTcpListenerEvent::Connection(pending)) => {
20522                        let PendingTcpSocket {
20523                            stream,
20524                            kernel_socket_id,
20525                            preallocated,
20526                            guest_local_addr,
20527                            guest_remote_addr,
20528                        } = pending;
20529                        if !preallocated {
20530                            if let Err(error) = check_network_resource_limit(
20531                                resource_limits.max_sockets,
20532                                network_counts.sockets,
20533                                1,
20534                                "socket",
20535                            )
20536                            .and_then(|()| {
20537                                check_network_resource_limit(
20538                                    resource_limits.max_connections,
20539                                    network_counts.connections,
20540                                    1,
20541                                    "connection",
20542                                )
20543                            }) {
20544                                if let Some(stream) = stream {
20545                                    let _ = stream.shutdown(Shutdown::Both);
20546                                }
20547                                return Ok(json!({
20548                                    "type": "error",
20549                                    "code": "EAGAIN",
20550                                    "message": error.to_string(),
20551                                }));
20552                            }
20553                        }
20554                        let socket = if let Some(stream) = stream {
20555                            ActiveTcpSocket::from_stream(
20556                                stream,
20557                                Some(listener_id.to_string()),
20558                                guest_local_addr,
20559                                guest_remote_addr,
20560                            )?
20561                        } else {
20562                            ActiveTcpSocket::from_kernel(
20563                                kernel_socket_id.ok_or_else(|| {
20564                                    SidecarError::InvalidState(String::from(
20565                                        "kernel TCP accept missing socket id",
20566                                    ))
20567                                })?,
20568                                Some(listener_id.to_string()),
20569                                guest_local_addr,
20570                                guest_remote_addr,
20571                            )
20572                        };
20573                        let socket_id = process.allocate_tcp_socket_id();
20574                        if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20575                            listener.register_connection(&socket_id);
20576                        }
20577                        process.tcp_sockets.insert(socket_id.clone(), socket);
20578                        Ok(json!({
20579                            "type": "connection",
20580                            "socketId": socket_id,
20581                            "localAddress": guest_local_addr.ip().to_string(),
20582                            "localPort": guest_local_addr.port(),
20583                            "remoteAddress": guest_remote_addr.ip().to_string(),
20584                            "remotePort": guest_remote_addr.port(),
20585                            "remoteFamily": socket_addr_family(&guest_remote_addr),
20586                        }))
20587                    }
20588                    Some(JavascriptTcpListenerEvent::Error { code, message }) => Ok(json!({
20589                        "type": "error",
20590                        "code": code,
20591                        "message": message,
20592                    })),
20593                    None => Ok(Value::Null),
20594                };
20595            }
20596
20597            let event = {
20598                let listener = process.unix_listeners.get_mut(listener_id).ok_or_else(|| {
20599                    SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20600                })?;
20601                listener.poll(Duration::from_millis(wait_ms))?
20602            };
20603
20604            match event {
20605                Some(JavascriptUnixListenerEvent::Connection(pending)) => {
20606                    if let Err(error) = check_network_resource_limit(
20607                        resource_limits.max_sockets,
20608                        network_counts.sockets,
20609                        1,
20610                        "socket",
20611                    )
20612                    .and_then(|()| {
20613                        check_network_resource_limit(
20614                            resource_limits.max_connections,
20615                            network_counts.connections,
20616                            1,
20617                            "connection",
20618                        )
20619                    }) {
20620                        let _ = pending.stream.shutdown(Shutdown::Both);
20621                        return Ok(json!({
20622                            "type": "error",
20623                            "code": "EAGAIN",
20624                            "message": error.to_string(),
20625                        }));
20626                    }
20627                    let socket = ActiveUnixSocket::from_stream(
20628                        pending.stream,
20629                        Some(listener_id.to_string()),
20630                        pending.local_path.clone(),
20631                        pending.remote_path.clone(),
20632                    )?;
20633                    let socket_id = process.allocate_unix_socket_id();
20634                    if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20635                        listener.register_connection(&socket_id);
20636                    }
20637                    process.unix_sockets.insert(socket_id.clone(), socket);
20638                    Ok(json!({
20639                        "type": "connection",
20640                        "socketId": socket_id,
20641                        "localPath": pending.local_path,
20642                        "remotePath": pending.remote_path,
20643                    }))
20644                }
20645                Some(JavascriptUnixListenerEvent::Error { code, message }) => Ok(json!({
20646                    "type": "error",
20647                    "code": code,
20648                    "message": message,
20649                })),
20650                None => Ok(Value::Null),
20651            }
20652        }
20653        "net.server_accept" => {
20654            let listener_id =
20655                javascript_sync_rpc_arg_str(&request.args, 0, "net.server_accept listener id")?;
20656            if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20657                return match listener.poll(kernel, process.kernel_pid, Duration::ZERO)? {
20658                    Some(JavascriptTcpListenerEvent::Connection(pending)) => {
20659                        let PendingTcpSocket {
20660                            stream,
20661                            kernel_socket_id,
20662                            preallocated,
20663                            guest_local_addr,
20664                            guest_remote_addr,
20665                        } = pending;
20666                        if !preallocated {
20667                            check_network_resource_limit(
20668                                resource_limits.max_sockets,
20669                                network_counts.sockets,
20670                                1,
20671                                "socket",
20672                            )?;
20673                            check_network_resource_limit(
20674                                resource_limits.max_connections,
20675                                network_counts.connections,
20676                                1,
20677                                "connection",
20678                            )?;
20679                        }
20680                        let info = json!({
20681                            "localAddress": guest_local_addr.ip().to_string(),
20682                            "localPort": guest_local_addr.port(),
20683                            "localFamily": socket_addr_family(&guest_local_addr),
20684                            "remoteAddress": guest_remote_addr.ip().to_string(),
20685                            "remotePort": guest_remote_addr.port(),
20686                            "remoteFamily": socket_addr_family(&guest_remote_addr),
20687                        });
20688                        let socket = if let Some(stream) = stream {
20689                            ActiveTcpSocket::from_stream(
20690                                stream,
20691                                Some(listener_id.to_string()),
20692                                guest_local_addr,
20693                                guest_remote_addr,
20694                            )?
20695                        } else {
20696                            ActiveTcpSocket::from_kernel(
20697                                kernel_socket_id.ok_or_else(|| {
20698                                    SidecarError::InvalidState(String::from(
20699                                        "kernel TCP accept missing socket id",
20700                                    ))
20701                                })?,
20702                                Some(listener_id.to_string()),
20703                                guest_local_addr,
20704                                guest_remote_addr,
20705                            )
20706                        };
20707                        let socket_id = process.allocate_tcp_socket_id();
20708                        if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20709                            listener.register_connection(&socket_id);
20710                        }
20711                        process.tcp_sockets.insert(socket_id.clone(), socket);
20712                        javascript_net_json_string(
20713                            json!({
20714                                "socketId": socket_id,
20715                                "info": info,
20716                            }),
20717                            "net.server_accept",
20718                        )
20719                    }
20720                    Some(JavascriptTcpListenerEvent::Error { code, message }) => {
20721                        let detail = code.unwrap_or_else(|| String::from("server accept"));
20722                        Err(SidecarError::Execution(format!("{detail}: {message}")))
20723                    }
20724                    None => Ok(javascript_net_timeout_value()),
20725                };
20726            }
20727
20728            let listener = process.unix_listeners.get_mut(listener_id).ok_or_else(|| {
20729                SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20730            })?;
20731            match listener.poll(Duration::ZERO)? {
20732                Some(JavascriptUnixListenerEvent::Connection(pending)) => {
20733                    check_network_resource_limit(
20734                        resource_limits.max_sockets,
20735                        network_counts.sockets,
20736                        1,
20737                        "socket",
20738                    )?;
20739                    check_network_resource_limit(
20740                        resource_limits.max_connections,
20741                        network_counts.connections,
20742                        1,
20743                        "connection",
20744                    )?;
20745                    let info = json!({
20746                        "localPath": pending.local_path.clone(),
20747                        "remotePath": pending.remote_path.clone(),
20748                    });
20749                    let socket = ActiveUnixSocket::from_stream(
20750                        pending.stream,
20751                        Some(listener_id.to_string()),
20752                        pending.local_path,
20753                        pending.remote_path,
20754                    )?;
20755                    let socket_id = process.allocate_unix_socket_id();
20756                    if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20757                        listener.register_connection(&socket_id);
20758                    }
20759                    process.unix_sockets.insert(socket_id.clone(), socket);
20760                    javascript_net_json_string(
20761                        json!({
20762                            "socketId": socket_id,
20763                            "info": info,
20764                        }),
20765                        "net.server_accept",
20766                    )
20767                }
20768                Some(JavascriptUnixListenerEvent::Error { code, message }) => {
20769                    let detail = code.unwrap_or_else(|| String::from("server accept"));
20770                    Err(SidecarError::Execution(format!("{detail}: {message}")))
20771                }
20772                None => Ok(javascript_net_timeout_value()),
20773            }
20774        }
20775        "net.server_connections" => {
20776            let listener_id = javascript_sync_rpc_arg_str(
20777                &request.args,
20778                0,
20779                "net.server_connections listener id",
20780            )?;
20781            if let Some(listener) = process.tcp_listeners.get(listener_id) {
20782                Ok(json!(listener.active_connection_count()))
20783            } else {
20784                let listener = process.unix_listeners.get(listener_id).ok_or_else(|| {
20785                    SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20786                })?;
20787                Ok(json!(listener.active_connection_count()))
20788            }
20789        }
20790        "net.upgrade_socket_write" => {
20791            let socket_id = javascript_sync_rpc_arg_str(
20792                &request.args,
20793                0,
20794                "net.upgrade_socket_write socket id",
20795            )?;
20796            let chunk =
20797                javascript_sync_rpc_base64_arg(&request.args, 1, "net.upgrade_socket_write chunk")?;
20798            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20799                SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
20800            })?;
20801            socket
20802                .write_all(kernel, process.kernel_pid, &chunk)
20803                .map(|written| json!(written))
20804        }
20805        "net.upgrade_socket_end" => {
20806            let socket_id =
20807                javascript_sync_rpc_arg_str(&request.args, 0, "net.upgrade_socket_end socket id")?;
20808            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20809                SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
20810            })?;
20811            socket.shutdown_write(kernel, process.kernel_pid)?;
20812            Ok(Value::Null)
20813        }
20814        "net.upgrade_socket_destroy" => {
20815            let socket_id = javascript_sync_rpc_arg_str(
20816                &request.args,
20817                0,
20818                "net.upgrade_socket_destroy socket id",
20819            )?;
20820            let socket = process.tcp_sockets.remove(socket_id).ok_or_else(|| {
20821                SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
20822            })?;
20823            if let Some(listener_id) = socket.listener_id.as_deref() {
20824                if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20825                    listener.release_connection(socket_id);
20826                }
20827            }
20828            let _ = socket.close(kernel, process.kernel_pid);
20829            Ok(Value::Null)
20830        }
20831        "net.write" => {
20832            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.write socket id")?;
20833            let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "net.write chunk")?;
20834            if let Some(socket) = process.tcp_sockets.get(socket_id) {
20835                socket
20836                    .write_all(kernel, process.kernel_pid, &chunk)
20837                    .map(|written| json!(written))
20838            } else {
20839                let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
20840                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20841                })?;
20842                socket.write_all(&chunk).map(|written| json!(written))
20843            }
20844        }
20845        "net.shutdown" => {
20846            let socket_id =
20847                javascript_sync_rpc_arg_str(&request.args, 0, "net.shutdown socket id")?;
20848            if let Some(socket) = process.tcp_sockets.get(socket_id) {
20849                socket.shutdown_write(kernel, process.kernel_pid)?;
20850            } else {
20851                let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
20852                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20853                })?;
20854                socket.shutdown_write()?;
20855            }
20856            Ok(Value::Null)
20857        }
20858        "net.destroy" => {
20859            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.destroy socket id")?;
20860            if let Some(socket) = process.tcp_sockets.remove(socket_id) {
20861                if let Some(listener_id) = socket.listener_id.as_deref() {
20862                    if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20863                        listener.release_connection(socket_id);
20864                    }
20865                }
20866                let _ = socket.close(kernel, process.kernel_pid);
20867                Ok(Value::Null)
20868            } else {
20869                let socket = process.unix_sockets.remove(socket_id).ok_or_else(|| {
20870                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20871                })?;
20872                if let Some(listener_id) = socket.listener_id.as_deref() {
20873                    if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20874                        listener.release_connection(socket_id);
20875                    }
20876                }
20877                let _ = socket.close();
20878                Ok(Value::Null)
20879            }
20880        }
20881        "net.server_close" => {
20882            let listener_id =
20883                javascript_sync_rpc_arg_str(&request.args, 0, "net.server_close listener id")?;
20884            if let Some(listener) = process.tcp_listeners.remove(listener_id) {
20885                listener.close(kernel, process.kernel_pid)?;
20886                Ok(Value::Null)
20887            } else {
20888                let listener = process.unix_listeners.remove(listener_id).ok_or_else(|| {
20889                    SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20890                })?;
20891                listener.close()?;
20892                Ok(Value::Null)
20893            }
20894        }
20895        "tls.get_ciphers" => javascript_net_json_string(
20896            Value::Array(
20897                tls_provider()
20898                    .cipher_suites
20899                    .iter()
20900                    .filter_map(|suite| {
20901                        suite
20902                            .suite()
20903                            .as_str()
20904                            .map(|value| Value::String(value.to_owned()))
20905                    })
20906                    .collect(),
20907            ),
20908            "tls.get_ciphers",
20909        ),
20910        _ => Err(SidecarError::InvalidState(format!(
20911            "unsupported JavaScript net sync RPC method {}",
20912            request.method
20913        ))),
20914    }
20915}
20916
20917fn signal_name_for_stream_event(signal: i32) -> Option<&'static str> {
20918    match signal {
20919        libc::SIGHUP => Some("SIGHUP"),
20920        libc::SIGINT => Some("SIGINT"),
20921        libc::SIGUSR1 => Some("SIGUSR1"),
20922        libc::SIGALRM => Some("SIGALRM"),
20923        libc::SIGCONT => Some("SIGCONT"),
20924        libc::SIGTERM => Some("SIGTERM"),
20925        libc::SIGCHLD => Some("SIGCHLD"),
20926        libc::SIGWINCH => Some("SIGWINCH"),
20927        _ => None,
20928    }
20929}
20930
20931pub(crate) fn canonical_signal_name(signal: i32) -> Option<&'static str> {
20932    match signal {
20933        1 => Some("SIGHUP"),
20934        2 => Some("SIGINT"),
20935        3 => Some("SIGQUIT"),
20936        4 => Some("SIGILL"),
20937        5 => Some("SIGTRAP"),
20938        6 => Some("SIGABRT"),
20939        7 => Some("SIGBUS"),
20940        8 => Some("SIGFPE"),
20941        9 => Some("SIGKILL"),
20942        10 => Some("SIGUSR1"),
20943        11 => Some("SIGSEGV"),
20944        12 => Some("SIGUSR2"),
20945        13 => Some("SIGPIPE"),
20946        14 => Some("SIGALRM"),
20947        15 => Some("SIGTERM"),
20948        17 => Some("SIGCHLD"),
20949        18 => Some("SIGCONT"),
20950        19 => Some("SIGSTOP"),
20951        20 => Some("SIGTSTP"),
20952        21 => Some("SIGTTIN"),
20953        22 => Some("SIGTTOU"),
20954        23 => Some("SIGURG"),
20955        24 => Some("SIGXCPU"),
20956        25 => Some("SIGXFSZ"),
20957        26 => Some("SIGVTALRM"),
20958        27 => Some("SIGPROF"),
20959        28 => Some("SIGWINCH"),
20960        29 => Some("SIGIO"),
20961        30 => Some("SIGPWR"),
20962        31 => Some("SIGSYS"),
20963        _ => None,
20964    }
20965}
20966
20967fn dispatch_v8_process_signal(process: &ActiveProcess, signal: i32) -> Result<bool, SidecarError> {
20968    let Some(signal_name) = signal_name_for_stream_event(signal) else {
20969        return Ok(false);
20970    };
20971    process.execution.send_javascript_stream_event(
20972        "signal",
20973        json!({
20974            "signal": signal_name,
20975            "number": signal,
20976            "action": "default",
20977        }),
20978    )?;
20979    Ok(true)
20980}
20981
20982fn dispatch_v8_session_signal_async(session: V8SessionHandle, signal: i32) {
20983    let Some(signal_name) = signal_name_for_stream_event(signal).map(str::to_owned) else {
20984        return;
20985    };
20986    thread::spawn(move || {
20987        thread::sleep(Duration::from_millis(1));
20988        let payload = v8_runtime::json_to_cbor_payload(&json!({
20989            "signal": signal_name,
20990            "number": signal,
20991            "action": "default",
20992        }))
20993        .unwrap_or_default();
20994        let _ = session.send_stream_event("signal", payload);
20995    });
20996}
20997
20998pub(crate) fn parse_signal(signal: &str) -> Result<i32, SidecarError> {
20999    let trimmed = signal.trim();
21000    if trimmed.is_empty() {
21001        return Err(SidecarError::InvalidState(String::from(
21002            "kill_process requires a non-empty signal",
21003        )));
21004    }
21005
21006    if let Ok(value) = trimmed.parse::<i32>() {
21007        return match value {
21008            0..=31 => Ok(value),
21009            _ => Err(SidecarError::InvalidState(format!(
21010                "unsupported kill_process signal {signal}"
21011            ))),
21012        };
21013    }
21014
21015    let upper = trimmed.to_ascii_uppercase();
21016    let normalized = upper.strip_prefix("SIG").unwrap_or(&upper);
21017
21018    signal_number_from_name(normalized).ok_or_else(|| {
21019        SidecarError::InvalidState(format!("unsupported kill_process signal {signal}"))
21020    })
21021}
21022
21023fn signal_number_from_name(signal: &str) -> Option<i32> {
21024    match signal {
21025        "0" => Some(0),
21026        "HUP" => Some(1),
21027        "INT" => Some(2),
21028        "QUIT" => Some(3),
21029        "ILL" => Some(4),
21030        "TRAP" => Some(5),
21031        "ABRT" | "IOT" => Some(6),
21032        "BUS" => Some(7),
21033        "FPE" => Some(8),
21034        "KILL" => Some(9),
21035        "USR1" => Some(10),
21036        "SEGV" => Some(11),
21037        "USR2" => Some(12),
21038        "PIPE" => Some(13),
21039        "ALRM" => Some(14),
21040        "TERM" => Some(15),
21041        "STKFLT" => Some(16),
21042        "CHLD" => Some(17),
21043        "CONT" => Some(18),
21044        "STOP" => Some(19),
21045        "TSTP" => Some(20),
21046        "TTIN" => Some(21),
21047        "TTOU" => Some(22),
21048        "URG" => Some(23),
21049        "XCPU" => Some(24),
21050        "XFSZ" => Some(25),
21051        "VTALRM" => Some(26),
21052        "PROF" => Some(27),
21053        "WINCH" => Some(28),
21054        "IO" | "POLL" => Some(29),
21055        "PWR" => Some(30),
21056        "SYS" => Some(31),
21057        _ => None,
21058    }
21059}
21060
21061pub(crate) fn runtime_child_is_alive(child_pid: u32) -> Result<bool, SidecarError> {
21062    Ok(runtime_child_exit_status(child_pid)?.is_none())
21063}
21064
21065#[cfg(not(target_os = "macos"))]
21066fn runtime_child_exit_status(child_pid: u32) -> Result<Option<i32>, SidecarError> {
21067    if child_pid == 0 {
21068        return Ok(Some(0));
21069    }
21070
21071    let wait_flags = WaitPidFlag::WNOHANG
21072        | WaitPidFlag::WNOWAIT
21073        | WaitPidFlag::WEXITED
21074        | WaitPidFlag::WUNTRACED
21075        | WaitPidFlag::WCONTINUED;
21076    match wait_on_child(WaitId::Pid(Pid::from_raw(child_pid as i32)), wait_flags) {
21077        Ok(WaitStatus::StillAlive)
21078        | Ok(WaitStatus::Stopped(_, _))
21079        | Ok(WaitStatus::Continued(_)) => Ok(None),
21080        Ok(WaitStatus::Exited(_, status)) => Ok(Some(status)),
21081        Ok(WaitStatus::Signaled(_, signal, _)) => Ok(Some(128 + signal as i32)),
21082        #[cfg(any(target_os = "linux", target_os = "android"))]
21083        Ok(WaitStatus::PtraceEvent(_, _, _) | WaitStatus::PtraceSyscall(_)) => Ok(None),
21084        Err(nix::errno::Errno::ECHILD) => Ok(Some(0)),
21085        Err(error) => Err(SidecarError::Execution(format!(
21086            "failed to inspect guest runtime process {child_pid}: {error}"
21087        ))),
21088    }
21089}
21090
21091// macOS nix exposes no `waitid`/`WNOWAIT`, so we poll with `waitpid(WNOHANG)`.
21092// NOTE: unlike Linux's `waitid(WNOWAIT)`, `waitpid` REAPS an exited child rather
21093// than leaving it waitable. That is correct for this poll (the sidecar is the
21094// reaping parent), but a second status query after exit returns ECHILD → treated
21095// as "exited(0)" below.
21096#[cfg(target_os = "macos")]
21097fn runtime_child_exit_status(child_pid: u32) -> Result<Option<i32>, SidecarError> {
21098    if child_pid == 0 {
21099        return Ok(Some(0));
21100    }
21101
21102    match waitpid(Pid::from_raw(child_pid as i32), Some(WaitPidFlag::WNOHANG)) {
21103        Ok(WaitStatus::StillAlive)
21104        | Ok(WaitStatus::Stopped(_, _))
21105        | Ok(WaitStatus::Continued(_)) => Ok(None),
21106        Ok(WaitStatus::Exited(_, status)) => Ok(Some(status)),
21107        Ok(WaitStatus::Signaled(_, signal, _)) => Ok(Some(128 + signal as i32)),
21108        Err(nix::errno::Errno::ECHILD) => Ok(Some(0)),
21109        Err(error) => Err(SidecarError::Execution(format!(
21110            "failed to inspect guest runtime process {child_pid}: {error}"
21111        ))),
21112    }
21113}
21114
21115pub(crate) fn signal_runtime_process(child_pid: u32, signal: i32) -> Result<(), SidecarError> {
21116    if child_pid == 0 {
21117        return Ok(());
21118    }
21119
21120    if !runtime_child_is_alive(child_pid)? {
21121        return Ok(());
21122    }
21123
21124    if signal == 0 {
21125        return Ok(());
21126    }
21127
21128    let parsed = Signal::try_from(signal).map_err(|_| {
21129        SidecarError::InvalidState(format!("unsupported kill_process signal {signal}"))
21130    })?;
21131    let result = send_signal(Pid::from_raw(child_pid as i32), Some(parsed));
21132
21133    match result {
21134        Ok(()) => Ok(()),
21135        Err(nix::errno::Errno::ESRCH) => Ok(()),
21136        Err(error) => Err(SidecarError::Execution(format!(
21137            "failed to signal guest runtime process {child_pid}: {error}"
21138        ))),
21139    }
21140}
21141
21142pub(crate) fn error_code(error: &SidecarError) -> &'static str {
21143    match error {
21144        SidecarError::InvalidState(_) => "invalid_state",
21145        SidecarError::ProtocolVersionMismatch(_) => "protocol_version_mismatch",
21146        SidecarError::BridgeVersionMismatch(_) => "bridge_version_mismatch",
21147        SidecarError::Conflict(_) => "conflict",
21148        SidecarError::Unauthorized(_) => "unauthorized",
21149        SidecarError::Unsupported(_) => "unsupported",
21150        SidecarError::FrameTooLarge(_) => "frame_too_large",
21151        SidecarError::Kernel(_) => "kernel_error",
21152        SidecarError::Plugin(_) => "plugin_error",
21153        SidecarError::Execution(_) => "execution_error",
21154        SidecarError::Bridge(_) => "bridge_error",
21155        SidecarError::Io(_) => "io_error",
21156    }
21157}
21158
21159fn guest_errno_code(message: &str) -> Option<&str> {
21160    const TRUSTED_PREFIXES: &[&str] = &[
21161        "ERR_AGENTOS_NODE_SYNC_RPC",
21162        "ERR_AGENTOS_PYTHON_VFS_RPC",
21163        "ERR_AGENTOS_BRIDGE",
21164    ];
21165
21166    let mut segments = message.split(':').map(str::trim);
21167    let first = segments.next()?;
21168    if is_guest_errno_segment(first) {
21169        return Some(first);
21170    }
21171
21172    if TRUSTED_PREFIXES.contains(&first) {
21173        let second = segments.next()?;
21174        if is_guest_errno_segment(second) {
21175            return Some(second);
21176        }
21177    }
21178
21179    None
21180}
21181
21182fn is_guest_errno_segment(segment: &str) -> bool {
21183    segment.len() >= 2
21184        && segment.starts_with('E')
21185        && !segment.starts_with("ERR_")
21186        && segment[1..]
21187            .bytes()
21188            .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_')
21189}
21190
21191pub(crate) fn javascript_sync_rpc_error_code(error: &SidecarError) -> String {
21192    let message = error.to_string();
21193    if let Some(code) = guest_errno_code(&message) {
21194        return code.to_owned();
21195    }
21196    if message.starts_with("ERR_NATIVE_BINARY_NOT_SUPPORTED:") {
21197        return String::from("ERR_NATIVE_BINARY_NOT_SUPPORTED");
21198    }
21199
21200    let lower = message.to_ascii_lowercase();
21201    if lower.contains("no such file or directory")
21202        || lower.contains("entry not found")
21203        || lower.contains("not found")
21204    {
21205        return String::from("ENOENT");
21206    }
21207    if lower.contains("permission denied") {
21208        return String::from("EACCES");
21209    }
21210    if lower.contains("already exists")
21211        || lower.contains("already registered")
21212        || lower.contains("file exists")
21213    {
21214        return String::from("EEXIST");
21215    }
21216    if lower.contains("invalid argument") {
21217        return String::from("EINVAL");
21218    }
21219
21220    String::from("ERR_AGENTOS_NODE_SYNC_RPC")
21221}
21222
21223pub(crate) fn ignore_stale_javascript_sync_rpc_response(
21224    error: SidecarError,
21225) -> Result<(), SidecarError> {
21226    match error {
21227        SidecarError::Execution(message)
21228            if message.ends_with("is no longer pending")
21229                && message.starts_with("sync RPC request ") =>
21230        {
21231            Ok(())
21232        }
21233        SidecarError::Execution(message) => {
21234            let lower = message.to_ascii_lowercase();
21235            if lower.contains("sync rpc response")
21236                && (lower.contains("broken pipe") || lower.contains("channel closed unexpectedly"))
21237            {
21238                Ok(())
21239            } else {
21240                Err(SidecarError::Execution(message))
21241            }
21242        }
21243        other => Err(other),
21244    }
21245}
21246
21247#[cfg(test)]
21248mod error_code_tests {
21249    use super::{guest_errno_code, javascript_sync_rpc_error_code, SidecarError};
21250
21251    #[test]
21252    fn guest_errno_code_rejects_guest_controlled_errno_segments() {
21253        assert_eq!(guest_errno_code("user said 'EACCES: denied'"), None);
21254        assert_eq!(
21255            guest_errno_code("prefix: user said 'EPERM': more text"),
21256            None
21257        );
21258        assert_eq!(guest_errno_code("ERR_AGENTOS_FAKE: EACCES: denied"), None);
21259    }
21260
21261    #[test]
21262    fn guest_errno_code_accepts_trusted_secure_exec_prefixes() {
21263        assert_eq!(
21264            guest_errno_code("ERR_AGENTOS_NODE_SYNC_RPC: EACCES: permission denied on /foo"),
21265            Some("EACCES")
21266        );
21267        assert_eq!(
21268            guest_errno_code("ERR_AGENTOS_PYTHON_VFS_RPC: ENOENT: missing file"),
21269            Some("ENOENT")
21270        );
21271        assert_eq!(guest_errno_code("EEXIST: already exists"), Some("EEXIST"));
21272    }
21273
21274    #[test]
21275    fn javascript_sync_rpc_error_code_ignores_spoofed_errnos() {
21276        let error = SidecarError::Execution(String::from("user said 'EACCES: denied'"));
21277        assert_eq!(
21278            javascript_sync_rpc_error_code(&error),
21279            "ERR_AGENTOS_NODE_SYNC_RPC"
21280        );
21281    }
21282
21283    #[test]
21284    fn javascript_sync_rpc_error_code_preserves_real_sidecar_errnos() {
21285        let error = SidecarError::Execution(String::from(
21286            "ERR_AGENTOS_NODE_SYNC_RPC: EACCES: permission denied on /foo",
21287        ));
21288        assert_eq!(javascript_sync_rpc_error_code(&error), "EACCES");
21289    }
21290
21291    #[test]
21292    fn javascript_sync_rpc_error_code_maps_file_exists_messages() {
21293        let error = SidecarError::Io(String::from(
21294            "failed to create mapped guest directory /.next/server: File exists (os error 17)",
21295        ));
21296        assert_eq!(javascript_sync_rpc_error_code(&error), "EEXIST");
21297    }
21298
21299    #[test]
21300    fn javascript_sync_rpc_error_code_preserves_native_binary_rejections() {
21301        let error = SidecarError::Execution(String::from(
21302            "ERR_NATIVE_BINARY_NOT_SUPPORTED: refused to execute native ELF guest binary at /tmp/fake-rg inside the VM",
21303        ));
21304        assert_eq!(
21305            javascript_sync_rpc_error_code(&error),
21306            "ERR_NATIVE_BINARY_NOT_SUPPORTED"
21307        );
21308    }
21309}
21310#[cfg(test)]
21311mod ssrf_egress_classifier_tests {
21312    // F-005/006/007 (sec-sidecar T1/T7/T11): the egress classifier must treat the
21313    // unspecified address (0.0.0.0 / ::), CGNAT (100.64.0.0/10), IPv6 spellings of
21314    // restricted IPv4 targets (::a.b.c.d), and reserved/multicast (240/4, 224/4) as
21315    // restricted. 0.0.0.0 routes to 127.0.0.1 on connect(), so leaving it
21316    // unclassified let a guest bypass the loopback port-ownership gate.
21317    //
21318    // These are bounded SAFEGUARD tests: they exercise the classifier and the DNS
21319    // egress filter directly (no network I/O, no Node), so they run fast and
21320    // deterministically. See FAILURES.md#F-005, #F-006, #F-007.
21321    use super::{
21322        filter_dns_safe_ip_addrs, is_loopback_ip, restricted_non_loopback_ip_range, SidecarError,
21323    };
21324    use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
21325
21326    fn assert_restricted(ip: IpAddr, expected_label: &str) {
21327        let classification = restricted_non_loopback_ip_range(ip);
21328        assert!(
21329            classification.is_some(),
21330            "{ip} must be classified as a restricted egress target"
21331        );
21332        let (_cidr, label) = classification.unwrap();
21333        assert_eq!(
21334            label, expected_label,
21335            "{ip} should be labelled {expected_label}, got {label}"
21336        );
21337    }
21338
21339    fn assert_dns_denied(ip: IpAddr, label: &str) {
21340        match filter_dns_safe_ip_addrs(vec![ip], "attacker.example") {
21341            Err(SidecarError::Execution(message)) => assert!(
21342                message.starts_with("EACCES:"),
21343                "{label}: egress filter must deny with EACCES, got: {message}"
21344            ),
21345            other => panic!("{label}: expected EACCES denial, got {other:?}"),
21346        }
21347    }
21348
21349    // F-005 (sec-sidecar T1).
21350    #[test]
21351    fn classifier_denies_unspecified_and_cgnat_targets() {
21352        // 0.0.0.0 (IPv4 unspecified) -> would route to host loopback.
21353        assert_restricted(IpAddr::V4(Ipv4Addr::UNSPECIFIED), "unspecified");
21354        // :: (IPv6 unspecified).
21355        assert_restricted(IpAddr::V6(Ipv6Addr::UNSPECIFIED), "unspecified");
21356
21357        // CGNAT 100.64.0.0/10 spans 100.64.x.x .. 100.127.x.x.
21358        assert_restricted(
21359            IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1)),
21360            "carrier-grade-nat",
21361        );
21362        assert_restricted(
21363            IpAddr::V4(Ipv4Addr::new(100, 127, 255, 254)),
21364            "carrier-grade-nat",
21365        );
21366
21367        // Guard against over-blocking: addresses just outside 100.64/10 stay allowed.
21368        assert!(
21369            restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(100, 63, 255, 255)))
21370                .is_none(),
21371            "100.63.255.255 is outside CGNAT and must remain allowed"
21372        );
21373        assert!(
21374            restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(100, 128, 0, 0))).is_none(),
21375            "100.128.0.0 is outside CGNAT and must remain allowed"
21376        );
21377
21378        // The DNS egress filter must also deny these via EACCES.
21379        assert_dns_denied(IpAddr::V4(Ipv4Addr::UNSPECIFIED), "0.0.0.0 (unspecified)");
21380        assert_dns_denied(IpAddr::V6(Ipv6Addr::UNSPECIFIED), ":: (unspecified)");
21381        assert_dns_denied(
21382            IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1)),
21383            "100.64.0.1 (CGNAT)",
21384        );
21385    }
21386
21387    // F-006 (sec-sidecar T7).
21388    #[test]
21389    fn classifier_denies_ipv6_spelled_metadata_addresses() {
21390        // The IPv4-mapped form (::ffff:169.254.169.254) was already handled; the
21391        // IPv4-compatible form (::169.254.169.254) is the gap this fixes.
21392        let mapped = "::ffff:169.254.169.254".parse::<Ipv6Addr>().unwrap();
21393        assert_restricted(IpAddr::V6(mapped), "link-local");
21394
21395        let compat = "::169.254.169.254".parse::<Ipv6Addr>().unwrap();
21396        assert_restricted(IpAddr::V6(compat), "link-local");
21397
21398        // Other IPv4-compatible private/CGNAT spellings must also be canonicalized.
21399        assert_restricted(
21400            IpAddr::V6("::10.0.0.1".parse::<Ipv6Addr>().unwrap()),
21401            "private",
21402        );
21403        assert_restricted(
21404            IpAddr::V6("::100.64.0.1".parse::<Ipv6Addr>().unwrap()),
21405            "carrier-grade-nat",
21406        );
21407
21408        // Guard against over-blocking: the IPv6 unspecified/loopback addresses
21409        // are not IPv4-compatible host targets, and a public IPv4-compatible
21410        // address must remain allowed.
21411        assert_eq!(
21412            restricted_non_loopback_ip_range(IpAddr::V6(Ipv6Addr::UNSPECIFIED)),
21413            Some(("::/128", "unspecified")),
21414            ":: must classify as unspecified, not via the IPv4-compat path"
21415        );
21416        assert!(
21417            restricted_non_loopback_ip_range(IpAddr::V6(Ipv6Addr::LOCALHOST)).is_none()
21418                || is_loopback_ip(IpAddr::V6(Ipv6Addr::LOCALHOST)),
21419            "::1 must not be classified as a restricted IPv4-compatible target"
21420        );
21421        assert!(
21422            restricted_non_loopback_ip_range(IpAddr::V6("::8.8.8.8".parse::<Ipv6Addr>().unwrap()))
21423                .is_none(),
21424            "::8.8.8.8 (public IPv4-compatible) must remain allowed"
21425        );
21426
21427        // The DNS egress filter must deny the IPv4-compat metadata spelling.
21428        assert_dns_denied(
21429            IpAddr::V6("::169.254.169.254".parse::<Ipv6Addr>().unwrap()),
21430            "::169.254.169.254 (IPv4-compat metadata)",
21431        );
21432    }
21433
21434    // F-007 (sec-sidecar T11).
21435    #[test]
21436    fn classifier_denies_reserved_and_multicast_targets() {
21437        // 224.0.0.0/4 (multicast) and 240.0.0.0/4 (reserved / future use) are not
21438        // legitimate unicast egress targets; a guest connect to them must be
21439        // classified as restricted and denied.
21440        assert_restricted(IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)), "multicast");
21441        assert_restricted(IpAddr::V4(Ipv4Addr::new(239, 255, 255, 255)), "multicast");
21442        assert_restricted(IpAddr::V4(Ipv4Addr::new(240, 0, 0, 1)), "reserved");
21443        // 255.255.255.255 (limited broadcast) falls in 240.0.0.0/4.
21444        assert_restricted(IpAddr::V4(Ipv4Addr::BROADCAST), "reserved");
21445
21446        // IPv4-compatible IPv6 spellings must canonicalize and be denied too.
21447        assert_restricted(
21448            IpAddr::V6("::224.0.0.1".parse::<Ipv6Addr>().unwrap()),
21449            "multicast",
21450        );
21451        assert_restricted(
21452            IpAddr::V6("::240.0.0.1".parse::<Ipv6Addr>().unwrap()),
21453            "reserved",
21454        );
21455
21456        // Guard against over-blocking: addresses just outside 224/4 stay allowed.
21457        assert!(
21458            restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(223, 255, 255, 255)))
21459                .is_none(),
21460            "223.255.255.255 is outside 224/4 and must remain allowed"
21461        );
21462
21463        // The DNS egress filter must also deny these via EACCES.
21464        assert_dns_denied(
21465            IpAddr::V4(Ipv4Addr::new(240, 0, 0, 1)),
21466            "240.0.0.1 (reserved)",
21467        );
21468        assert_dns_denied(
21469            IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)),
21470            "224.0.0.1 (multicast)",
21471        );
21472    }
21473}
21474
21475/// Adversarial coverage for the DNS-rebinding gap (VECTORS.md D.3) on the
21476/// Python/Pyodide `httpRequestSync` outbound HTTP path. The egress range guard
21477/// (`filter_dns_safe_ip_addrs`) runs at resolution time, but `ureq` performs its
21478/// own DNS resolution for the TCP/TLS connect, so a rebinding DNS server could
21479/// previously make the second lookup land on a private/link-local/metadata IP
21480/// the first check rejected. The fix pins `ureq`'s resolver to the vetted
21481/// address set; these tests prove the connect is pinned and refuses any other
21482/// host or an empty (fully-rejected) address set.
21483#[cfg(test)]
21484mod dns_rebinding_pin_tests {
21485    use super::{issue_outbound_http_request, split_netloc, JavascriptHttpRequestOptions};
21486    use std::collections::BTreeMap;
21487    use std::io::{Read, Write};
21488    use std::net::{IpAddr, Ipv4Addr, TcpListener};
21489    use std::thread;
21490    use url::Url;
21491
21492    fn empty_headers() -> super::HttpHeaderCollection {
21493        super::parse_http_header_collection(&BTreeMap::new(), "test headers")
21494            .expect("empty header collection")
21495    }
21496
21497    fn options() -> JavascriptHttpRequestOptions {
21498        JavascriptHttpRequestOptions {
21499            method: Some(String::from("GET")),
21500            headers: BTreeMap::new(),
21501            body: None,
21502            reject_unauthorized: None,
21503        }
21504    }
21505
21506    #[test]
21507    fn split_netloc_handles_hostnames_and_bracketed_ipv6() {
21508        assert_eq!(
21509            split_netloc("attacker.example:80"),
21510            Some(("attacker.example", 80))
21511        );
21512        assert_eq!(split_netloc("[::1]:443"), Some(("::1", 443)));
21513        assert_eq!(split_netloc("10.0.0.1:8080"), Some(("10.0.0.1", 8080)));
21514        assert_eq!(split_netloc("no-port"), None);
21515        assert_eq!(split_netloc("host:notaport"), None);
21516    }
21517
21518    /// A loopback HTTP server stands in for the egress-vetted target. The
21519    /// request URL uses a *different* hostname (`attacker.example`) whose real
21520    /// DNS would resolve elsewhere; pinning forces the connect onto the vetted
21521    /// IP only. If the resolver were unpinned, the request would fail to reach
21522    /// this server (and on a real host could land on a private/metadata IP).
21523    #[test]
21524    fn outbound_http_connect_is_pinned_to_vetted_ip() {
21525        let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).expect("bind loopback server");
21526        let port = listener.local_addr().expect("local addr").port();
21527        let server = thread::spawn(move || {
21528            let (mut stream, _) = listener.accept().expect("accept");
21529            let mut buf = [0u8; 1024];
21530            let _ = stream.read(&mut buf);
21531            stream
21532                .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi")
21533                .expect("write response");
21534            let _ = stream.flush();
21535        });
21536
21537        let url = Url::parse(&format!("http://attacker.example:{port}/")).expect("url");
21538        let pinned = vec![IpAddr::V4(Ipv4Addr::LOCALHOST)];
21539        let result = issue_outbound_http_request(&url, &options(), &empty_headers(), &pinned)
21540            .expect("pinned request should reach the vetted loopback target");
21541        let payload = result.as_str().expect("string payload");
21542        assert!(
21543            payload.contains("\"status\":200"),
21544            "expected 200 from pinned target, got: {payload}"
21545        );
21546        server.join().expect("server thread");
21547    }
21548
21549    /// With no vetted address (every resolved IP was rejected by the range
21550    /// guard, or the literal IP was a blocked range), the pinned resolver must
21551    /// refuse rather than fall back to the host resolver.
21552    #[test]
21553    fn outbound_http_refuses_when_no_vetted_address() {
21554        let url = Url::parse("https://attacker.example/").expect("url");
21555        let error = issue_outbound_http_request(&url, &options(), &empty_headers(), &[])
21556            .expect_err("empty pinned set must be refused");
21557        let message = error.to_string();
21558        assert!(
21559            message.contains("EACCES") || message.contains("ERR_HTTP_REQUEST_FAILED"),
21560            "expected an egress refusal, got: {message}"
21561        );
21562    }
21563}