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_read_sync_rpc, service_javascript_fs_readdir_raw_sync_rpc,
8    service_javascript_fs_sync_rpc, service_javascript_module_sync_rpc,
9};
10use crate::protocol::{
11    CloseStdinRequest, EventFrame, EventPayload, ExecuteRequest, FindBoundUdpRequest,
12    FindListenerRequest, GetProcessSnapshotRequest, GetResourceSnapshotRequest,
13    GetSignalStateRequest, GetZombieTimerCountRequest, GuestKernelCallRequest,
14    GuestKernelResultResponse, GuestRuntimeKind, JavascriptChildProcessSpawnOptions,
15    JavascriptChildProcessSpawnRequest, JavascriptDgramBindRequest,
16    JavascriptDgramCreateSocketRequest, JavascriptDgramSendRequest, JavascriptDnsLookupRequest,
17    JavascriptDnsResolveRequest, JavascriptNetConnectRequest, JavascriptNetListenRequest,
18    JavascriptNetReserveTcpPortRequest, KillProcessRequest, OwnershipScope, ProcessExitedEvent,
19    ProcessOutputEvent, ProcessSnapshotEntry, ProcessSnapshotStatus, PtyResizedResponse,
20    QueueSnapshotEntry, RequestFrame, ResizePtyRequest, ResourceSnapshotResponse, ResponseFrame,
21    ResponsePayload, SidecarRequestPayload, SignalDispositionAction, SignalHandlerRegistration,
22    SocketStateEntry, StreamChannel, VmFetchRequest, VmFetchResponse, WasmPermissionTier,
23    WriteStdinRequest,
24};
25use crate::service::{
26    audit_fields, dirname, emit_security_audit_event, emit_structured_event, javascript_error,
27    kernel_error, log_stale_process_event, normalize_host_path, normalize_path,
28    parse_javascript_child_process_spawn_request, path_is_within_root,
29    process_event_queue_overflow_error, python_error, wasm_error, MAX_PROCESS_EVENT_QUEUE,
30};
31use crate::state::{
32    ActiveCipherSession, ActiveDhSession, ActiveDiffieHellmanSession, ActiveEcdhSession,
33    ActiveExecution, ActiveExecutionEvent, ActiveHttp2Server, ActiveHttp2Session,
34    ActiveHttp2Stream, ActiveHttpServer, ActiveMappedHostFd, ActiveProcess, ActiveSqliteDatabase,
35    ActiveSqliteStatement, ActiveTcpListener, ActiveTcpSocket, ActiveTlsState, ActiveTlsStream,
36    ActiveUdpSocket, ActiveUnixListener, ActiveUnixSocket, BridgeError, ExitedProcessSnapshot,
37    Http2BridgeEvent, Http2RuntimeSnapshot, Http2SessionCommand, Http2SessionSnapshot,
38    Http2SocketSnapshot, JavascriptHttpLoopbackTarget, JavascriptSocketEventPusher,
39    JavascriptSocketFamily, JavascriptSocketPathContext, JavascriptTcpListenerEvent,
40    JavascriptTcpSocketEvent, JavascriptTlsBridgeOptions, JavascriptTlsClientHello,
41    JavascriptTlsDataValue, JavascriptTlsMaterial, JavascriptUdpFamily, JavascriptUdpSocketEvent,
42    JavascriptUnixListenerEvent, KernelSocketReadinessEvent, KernelSocketReadinessRegistry,
43    KernelSocketReadinessTarget, LoopbackTlsPendingWriteHandle, LoopbackTlsPendingWriteState,
44    NetworkResourceCounts, PendingTcpSocket, PendingUnixSocket, ProcNetEntry, ProcessEventEnvelope,
45    PythonHostSocket, ResolvedChildProcessExecution, ResolvedTcpConnectAddr, SharedBridge,
46    SharedSidecarRequestClient, SidecarKernel, SocketQueryKind, ToolExecution, VmDnsConfig,
47    VmListenPolicy, VmState, DEFAULT_JAVASCRIPT_NET_BACKLOG, EXECUTION_DRIVER_NAME,
48    EXECUTION_SANDBOX_ROOT_ENV, JAVASCRIPT_COMMAND, LOOPBACK_EXEMPT_PORTS_ENV,
49    MAPPED_HOST_FD_START, PYTHON_COMMAND, TOOL_DRIVER_NAME,
50    VM_LISTEN_ALLOW_PRIVILEGED_METADATA_KEY, WASM_COMMAND, WASM_STDIO_SYNC_RPC_ENV,
51};
52use crate::tools::{
53    format_tool_failure_output, is_tool_command, normalized_tool_command_name,
54    resolve_tool_command, ToolCommandResolution,
55};
56use crate::wire::{ProtocolFrame as WireProtocolFrame, WireFrameCodec};
57use crate::{DispatchResult, NativeSidecar, NativeSidecarBridge, SidecarError};
58
59use base64::Engine;
60use bytes::Bytes;
61use h2::{client, server, Reason};
62use hickory_resolver::proto::rr::{RData, Record, RecordType};
63use hmac::{Hmac, Mac};
64use http::{HeaderMap, HeaderName, HeaderValue, Method, Request, Response, Uri};
65use md5::Md5;
66use nix::libc;
67use nix::poll::{poll, PollFd as NixPollFd, PollFlags, PollTimeout};
68use nix::sys::signal::{kill as send_signal, Signal};
69use nix::sys::wait::WaitStatus;
70#[cfg(not(target_os = "macos"))]
71use nix::sys::wait::{waitid as wait_on_child, Id as WaitId, WaitPidFlag};
72#[cfg(target_os = "macos")]
73use nix::sys::wait::{waitpid, WaitPidFlag};
74use nix::unistd::Pid;
75use openssl::bn::{BigNum, BigNumContext};
76use openssl::derive::Deriver;
77use openssl::dh::Dh;
78use openssl::ec::{EcGroup, EcKey, EcPoint, PointConversionForm};
79use openssl::hash::MessageDigest;
80use openssl::nid::Nid;
81use openssl::pkey::{Id as PKeyId, PKey, Params, Private, Public};
82use openssl::rand::rand_bytes;
83use openssl::rsa::{Padding, Rsa};
84use openssl::sign::{Signer, Verifier};
85use pbkdf2::pbkdf2_hmac;
86
87use crate::crypto_cipher::{CipherError as AesCipherError, StreamCipherSession};
88use rusqlite::types::ValueRef as SqliteValueRef;
89use rusqlite::{
90    Connection as SqliteConnection, OpenFlags as SqliteOpenFlags, Statement as SqliteStatement,
91};
92use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
93use rustls::crypto::aws_lc_rs;
94use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName};
95use rustls::{
96    ClientConfig, ClientConnection, DigitallySignedStruct, RootCertStore, ServerConfig,
97    ServerConnection, SignatureScheme,
98};
99use scrypt::{scrypt, Params as ScryptParams};
100use secure_exec_bridge::{queue_tracker, LifecycleState};
101use secure_exec_execution::wasm::WasmExecutionError;
102use secure_exec_execution::{
103    javascript::handle_internal_bridge_call_from_host_context, v8_host::V8SessionHandle,
104    v8_runtime, CreateJavascriptContextRequest, CreatePythonContextRequest,
105    CreateWasmContextRequest, GuestModuleReader, GuestRuntimeConfig, JavascriptExecutionEvent,
106    JavascriptExecutionLimits, JavascriptSyncRpcRequest, ModuleFsReader,
107    NodeSignalDispositionAction, NodeSignalHandlerRegistration, PythonExecutionEvent,
108    PythonExecutionLimits, PythonVfsRpcMethod, PythonVfsRpcRequest, PythonVfsRpcResponsePayload,
109    StartJavascriptExecutionRequest, StartPythonExecutionRequest, StartWasmExecutionRequest,
110    WasmExecutionEvent, WasmExecutionLimits, WasmPermissionTier as ExecutionWasmPermissionTier,
111};
112use secure_exec_kernel::dns::{
113    DnsLookupPolicy, DnsRecordResolution, DnsResolutionSource as KernelDnsResolutionSource,
114};
115use secure_exec_kernel::kernel::{KernelProcessHandle, SpawnOptions, VirtualProcessOptions};
116pub(crate) use secure_exec_kernel::network_policy::format_tcp_resource;
117use secure_exec_kernel::network_policy::{
118    is_loopback_ip, loopback_cidr, restricted_non_loopback_ip_range,
119};
120use secure_exec_kernel::permissions::NetworkOperation;
121use secure_exec_kernel::poll::{PollEvents, PollFd, PollTargetEntry, POLLERR, POLLHUP, POLLIN};
122use secure_exec_kernel::process_table::{ProcessStatus, WaitPidFlags, SIGKILL, SIGTERM};
123use secure_exec_kernel::pty::{LineDisciplineConfig, MAX_PTY_BUFFER_BYTES};
124use secure_exec_kernel::resource_accounting::ResourceLimits;
125use secure_exec_kernel::root_fs::RootFilesystemMode;
126use secure_exec_kernel::socket_table::{
127    reset_socket_read_trace, set_socket_read_trace_enabled, socket_read_trace_snapshot,
128    InetSocketAddress, SocketDomain, SocketId, SocketShutdown as KernelSocketShutdown, SocketSpec,
129    SocketState, SocketType,
130};
131use secure_exec_sidecar_core::{
132    apply_process_signal_state_update, bound_udp_snapshot_response, bridge_buffer_value,
133    decode_base64, decode_bridge_buffer_value, decode_encoded_bytes_value, encoded_bytes_value,
134    ensure_vm_fetch_raw_response_buffer_within_limit, ensure_vm_fetch_response_within_limit,
135    listener_snapshot_response, local_endpoint_value, parse_kernel_http_fetch_response,
136    parse_process_signal_state_request, process_killed_response,
137    process_snapshot_entry_from_kernel, process_snapshot_response, process_started_response,
138    remote_endpoint_value, shared_guest_runtime_identity, signal_state_response,
139    socket_addr_family, socket_address_value, stdin_closed_response, stdin_written_response,
140    tcp_socket_info_value, unix_socket_info_value, zombie_timer_count_response,
141    SharedProcessSnapshotEntry, SharedProcessSnapshotStatus, SidecarCoreError,
142    VM_FETCH_BUFFER_LIMIT_BYTES,
143};
144use serde::{Deserialize, Serialize};
145use serde_json::{json, Map, Value};
146use sha1::Sha1;
147use sha2::{digest::Digest, Sha224, Sha256, Sha384, Sha512};
148use socket2::{SockRef, TcpKeepalive};
149use std::collections::VecDeque;
150use std::collections::{BTreeMap, BTreeSet};
151use std::fmt;
152use std::fs;
153use std::io::{Cursor, Read, Write};
154use std::net::{
155    IpAddr, Ipv4Addr, Ipv6Addr, Shutdown, SocketAddr, TcpListener, TcpStream, ToSocketAddrs,
156    UdpSocket,
157};
158use std::os::fd::{AsFd, BorrowedFd};
159use std::os::unix::fs::{MetadataExt, PermissionsExt};
160use std::os::unix::net::{SocketAddr as UnixSocketAddr, UnixListener, UnixStream};
161use std::path::{Path, PathBuf};
162use std::pin::Pin;
163use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
164use std::sync::mpsc::{self, RecvTimeoutError, Sender};
165use std::sync::{Arc, Mutex, OnceLock, Weak};
166use std::thread;
167use std::time::{Duration, Instant};
168use tokio::io::{AsyncRead, AsyncWrite};
169use tokio::runtime::Builder as TokioRuntimeBuilder;
170use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver};
171use tokio_rustls::{TlsAcceptor, TlsConnector};
172use url::Url;
173
174const DEFAULT_KERNEL_STDIN_READ_MAX_BYTES: usize = 64 * 1024;
175const DEFAULT_KERNEL_STDIN_READ_TIMEOUT_MS: u64 = 100;
176const JAVASCRIPT_NET_TIMEOUT_SENTINEL: &str = "__secure_exec_net_timeout__";
177const PYTHON_PYODIDE_GUEST_ROOT: &str = "/__agentos_pyodide";
178const PYTHON_PYODIDE_CACHE_GUEST_ROOT: &str = "/__agentos_pyodide_cache";
179const TCP_SOCKET_POLL_TIMEOUT: Duration = Duration::from_millis(100);
180const TLS_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(5);
181const LOOPBACK_TLS_HANDSHAKE_POLL_TIMEOUT: Duration = Duration::from_millis(1);
182const LOOPBACK_TLS_PENDING_WRITE_BUFFER_BYTES: usize = 4 * 1024 * 1024;
183const LOOPBACK_TLS_PENDING_WRITE_WARNING_BYTES: usize =
184    LOOPBACK_TLS_PENDING_WRITE_BUFFER_BYTES * 4 / 5;
185const HTTP_LOOPBACK_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
186const PROCESS_EXIT_DRAIN_INITIAL_QUIET: Duration = Duration::from_millis(1);
187const PROCESS_EXIT_DRAIN_TRAILING_QUIET: Duration = Duration::from_millis(25);
188
189struct NetTcpTraceCounters {
190    socket_read_calls: AtomicU64,
191    socket_read_zero_wait_calls: AtomicU64,
192    socket_read_data_events: AtomicU64,
193    socket_read_bytes: AtomicU64,
194    socket_read_kernel_us: AtomicU64,
195    socket_read_end_events: AtomicU64,
196    socket_read_eagain: AtomicU64,
197    socket_read_errors: AtomicU64,
198    socket_read_push_attempts: AtomicU64,
199    socket_read_push_sent: AtomicU64,
200    socket_read_push_missing: AtomicU64,
201    socket_read_push_errors: AtomicU64,
202    socket_write_calls: AtomicU64,
203    socket_write_bytes: AtomicU64,
204    socket_write_kernel_us: AtomicU64,
205    socket_write_errors: AtomicU64,
206    server_accept_calls: AtomicU64,
207    server_accept_zero_wait_calls: AtomicU64,
208    server_accept_connections: AtomicU64,
209    server_accept_eagain: AtomicU64,
210    server_accept_errors: AtomicU64,
211    kernel_poll_targets: AtomicU64,
212    kernel_poll_zero_wait_calls: AtomicU64,
213    kernel_poll_wait_us: AtomicU64,
214    kernel_poll_elapsed_us: AtomicU64,
215    kernel_poll_empty: AtomicU64,
216    kernel_poll_ready: AtomicU64,
217    kernel_poll_revents_read: AtomicU64,
218    kernel_poll_revents_hup: AtomicU64,
219    kernel_poll_revents_err: AtomicU64,
220    kernel_poll_revents_bits_or: AtomicU64,
221}
222
223impl NetTcpTraceCounters {
224    const fn new() -> Self {
225        Self {
226            socket_read_calls: AtomicU64::new(0),
227            socket_read_zero_wait_calls: AtomicU64::new(0),
228            socket_read_data_events: AtomicU64::new(0),
229            socket_read_bytes: AtomicU64::new(0),
230            socket_read_kernel_us: AtomicU64::new(0),
231            socket_read_end_events: AtomicU64::new(0),
232            socket_read_eagain: AtomicU64::new(0),
233            socket_read_errors: AtomicU64::new(0),
234            socket_read_push_attempts: AtomicU64::new(0),
235            socket_read_push_sent: AtomicU64::new(0),
236            socket_read_push_missing: AtomicU64::new(0),
237            socket_read_push_errors: AtomicU64::new(0),
238            socket_write_calls: AtomicU64::new(0),
239            socket_write_bytes: AtomicU64::new(0),
240            socket_write_kernel_us: AtomicU64::new(0),
241            socket_write_errors: AtomicU64::new(0),
242            server_accept_calls: AtomicU64::new(0),
243            server_accept_zero_wait_calls: AtomicU64::new(0),
244            server_accept_connections: AtomicU64::new(0),
245            server_accept_eagain: AtomicU64::new(0),
246            server_accept_errors: AtomicU64::new(0),
247            kernel_poll_targets: AtomicU64::new(0),
248            kernel_poll_zero_wait_calls: AtomicU64::new(0),
249            kernel_poll_wait_us: AtomicU64::new(0),
250            kernel_poll_elapsed_us: AtomicU64::new(0),
251            kernel_poll_empty: AtomicU64::new(0),
252            kernel_poll_ready: AtomicU64::new(0),
253            kernel_poll_revents_read: AtomicU64::new(0),
254            kernel_poll_revents_hup: AtomicU64::new(0),
255            kernel_poll_revents_err: AtomicU64::new(0),
256            kernel_poll_revents_bits_or: AtomicU64::new(0),
257        }
258    }
259}
260
261static NET_TCP_TRACE_COUNTERS: NetTcpTraceCounters = NetTcpTraceCounters::new();
262pub(crate) const MAX_PER_PROCESS_STATE_HANDLES: usize = 1024;
263const DEFAULT_SCRYPT_COST: u64 = 16_384;
264const DEFAULT_SCRYPT_BLOCK_SIZE: u32 = 8;
265const DEFAULT_SCRYPT_PARALLELIZATION: u32 = 1;
266const SQLITE_JS_SAFE_INTEGER_MAX: i64 = 9_007_199_254_740_991;
267const HTTP_LOOPBACK_REQUEST_TIMEOUT_MS_ENV: &str =
268    "SECURE_EXEC_TEST_HTTP_LOOPBACK_REQUEST_TIMEOUT_MS";
269
270trait Http2AsyncIo: AsyncRead + AsyncWrite + Unpin + Send {}
271
272impl<T> Http2AsyncIo for T where T: AsyncRead + AsyncWrite + Unpin + Send {}
273
274fn http_loopback_request_timeout() -> Duration {
275    std::env::var(HTTP_LOOPBACK_REQUEST_TIMEOUT_MS_ENV)
276        .ok()
277        .and_then(|value| value.parse::<u64>().ok())
278        .map(Duration::from_millis)
279        .unwrap_or(HTTP_LOOPBACK_REQUEST_TIMEOUT)
280}
281
282/// Block until `fd` is readable or `deadline` passes. Returns whether it became readable.
283///
284/// BLOCKING: parks the calling OS thread in `poll(2)`. The unix/tcp accept and
285/// udp recv callers run on the sidecar's single-thread tokio runtime, so a
286/// non-zero wait stalls the whole event loop for up to `deadline` — the same
287/// stall as the fixed sleeps this replaced, and only acceptable because the
288/// guest net path always polls with wait == 0. Keep deadlines bounded and do
289/// not add wait > 0 callers on paths that service concurrent VM traffic.
290fn wait_fd_readable_until(fd: BorrowedFd<'_>, deadline: Instant) -> bool {
291    let remaining = deadline.saturating_duration_since(Instant::now());
292    if remaining.is_zero() {
293        return false;
294    }
295
296    let timeout_ms = remaining.as_millis().saturating_add(u128::from(
297        !remaining.subsec_nanos().is_multiple_of(1_000_000),
298    ));
299    let timeout =
300        PollTimeout::try_from(timeout_ms.min(i32::MAX as u128)).unwrap_or(PollTimeout::MAX);
301    let mut fds = [NixPollFd::new(fd, PollFlags::POLLIN)];
302    match poll(&mut fds, timeout) {
303        Ok(0) => false,
304        Ok(_) => fds[0]
305            .revents()
306            .unwrap_or_else(PollFlags::empty)
307            .intersects(PollFlags::POLLIN | PollFlags::POLLHUP | PollFlags::POLLERR),
308        Err(_) => true,
309    }
310}
311
312const DEFAULT_ALLOWED_NODE_BUILTINS: &[&str] = &[
313    "assert",
314    "buffer",
315    "console",
316    "child_process",
317    "crypto",
318    "dns",
319    "events",
320    "fs",
321    "http",
322    "http2",
323    "https",
324    "module",
325    "os",
326    "path",
327    "perf_hooks",
328    "querystring",
329    "sqlite",
330    "stream",
331    "string_decoder",
332    "timers",
333    "tls",
334    "tty",
335    "url",
336    "util",
337    "zlib",
338];
339const EXECUTION_REQUEST_TTY_ENV: &str = "AGENTOS_EXEC_TTY";
340
341#[derive(Debug, Clone, Copy, PartialEq, Eq)]
342enum JavascriptCryptoDigestAlgorithm {
343    Md5,
344    Sha1,
345    Sha224,
346    Sha256,
347    Sha384,
348    Sha512,
349}
350
351#[derive(Debug, Default, Deserialize)]
352#[serde(default, rename_all = "camelCase")]
353struct JavascriptScryptOptions {
354    #[serde(alias = "N")]
355    cost: Option<u64>,
356    #[serde(alias = "r")]
357    block_size: Option<u32>,
358    #[serde(alias = "p")]
359    parallelization: Option<u32>,
360}
361
362#[derive(Debug, Deserialize)]
363#[serde(rename_all = "camelCase")]
364struct JavascriptHttpListenRequest {
365    server_id: u64,
366    #[serde(default)]
367    port: Option<u16>,
368    #[serde(default)]
369    hostname: Option<String>,
370}
371
372#[derive(Debug, Default, Deserialize)]
373#[serde(default, rename_all = "camelCase")]
374struct JavascriptHttpRequestOptions {
375    method: Option<String>,
376    headers: BTreeMap<String, Value>,
377    body: Option<String>,
378    reject_unauthorized: Option<bool>,
379}
380
381#[derive(Debug, Default, Deserialize)]
382#[serde(default, rename_all = "camelCase")]
383struct JavascriptHttp2ServerListenRequest {
384    server_id: u64,
385    secure: bool,
386    port: Option<u16>,
387    host: Option<String>,
388    backlog: Option<u32>,
389    timeout: Option<u64>,
390    settings: BTreeMap<String, Value>,
391    tls: Option<JavascriptTlsBridgeOptions>,
392}
393
394#[derive(Debug, Default, Deserialize)]
395#[serde(default, rename_all = "camelCase")]
396struct JavascriptHttp2SessionConnectRequest {
397    authority: Option<String>,
398    protocol: Option<String>,
399    host: Option<String>,
400    port: Option<u16>,
401    settings: BTreeMap<String, Value>,
402    tls: Option<JavascriptTlsBridgeOptions>,
403}
404
405#[derive(Debug, Default, Deserialize)]
406#[serde(default, rename_all = "camelCase")]
407struct JavascriptHttp2RequestOptions {
408    end_stream: bool,
409}
410
411#[derive(Debug, Default, Deserialize)]
412#[serde(default, rename_all = "camelCase")]
413struct JavascriptHttp2FileResponseOptions {
414    offset: Option<u64>,
415    length: Option<i64>,
416}
417
418#[derive(Debug, Clone)]
419struct HttpHeaderCollection {
420    normalized: BTreeMap<String, Vec<String>>,
421    raw_pairs: Vec<(String, String)>,
422}
423
424#[derive(Debug)]
425struct InsecureTlsVerifier {
426    supported_schemes: Vec<SignatureScheme>,
427}
428
429impl ServerCertVerifier for InsecureTlsVerifier {
430    fn verify_server_cert(
431        &self,
432        _end_entity: &CertificateDer<'_>,
433        _intermediates: &[CertificateDer<'_>],
434        _server_name: &ServerName<'_>,
435        _ocsp_response: &[u8],
436        _now: rustls::pki_types::UnixTime,
437    ) -> Result<ServerCertVerified, rustls::Error> {
438        Ok(ServerCertVerified::assertion())
439    }
440
441    fn verify_tls12_signature(
442        &self,
443        _message: &[u8],
444        _cert: &CertificateDer<'_>,
445        _dss: &DigitallySignedStruct,
446    ) -> Result<HandshakeSignatureValid, rustls::Error> {
447        Ok(HandshakeSignatureValid::assertion())
448    }
449
450    fn verify_tls13_signature(
451        &self,
452        _message: &[u8],
453        _cert: &CertificateDer<'_>,
454        _dss: &DigitallySignedStruct,
455    ) -> Result<HandshakeSignatureValid, rustls::Error> {
456        Ok(HandshakeSignatureValid::assertion())
457    }
458
459    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
460        self.supported_schemes.clone()
461    }
462}
463
464impl ActiveProcess {
465    pub(crate) fn new(
466        kernel_pid: u32,
467        kernel_handle: KernelProcessHandle,
468        runtime: GuestRuntimeKind,
469        execution: ActiveExecution,
470    ) -> Self {
471        Self {
472            kernel_pid,
473            kernel_handle,
474            kernel_stdin_writer_fd: None,
475            tty_master_fd: None,
476            runtime,
477            detached: false,
478            execution,
479            guest_cwd: String::from("/"),
480            env: BTreeMap::new(),
481            host_cwd: PathBuf::from("/"),
482            host_write_dirty: false,
483            mapped_host_fds: BTreeMap::new(),
484            next_mapped_host_fd: MAPPED_HOST_FD_START,
485            pending_execution_events: VecDeque::new(),
486            pending_self_signal_exit: None,
487            child_processes: BTreeMap::new(),
488            next_child_process_id: 0,
489            http_servers: BTreeMap::new(),
490            pending_http_requests: BTreeMap::new(),
491            http2: Default::default(),
492            tcp_listeners: BTreeMap::new(),
493            next_tcp_listener_id: 0,
494            tcp_sockets: BTreeMap::new(),
495            next_tcp_socket_id: 0,
496            tcp_port_reservations: BTreeMap::new(),
497            next_tcp_port_reservation_id: 0,
498            unix_listeners: BTreeMap::new(),
499            next_unix_listener_id: 0,
500            unix_sockets: BTreeMap::new(),
501            next_unix_socket_id: 0,
502            udp_sockets: BTreeMap::new(),
503            next_udp_socket_id: 0,
504            python_sockets: BTreeMap::new(),
505            next_python_socket_id: 0,
506            cipher_sessions: BTreeMap::new(),
507            next_cipher_session_id: 0,
508            diffie_hellman_sessions: BTreeMap::new(),
509            next_diffie_hellman_session_id: 0,
510            sqlite_databases: BTreeMap::new(),
511            next_sqlite_database_id: 0,
512            sqlite_statements: BTreeMap::new(),
513            next_sqlite_statement_id: 0,
514            tty_master_owner: None,
515            deferred_kernel_wait_rpc: None,
516            module_resolution_cache: secure_exec_execution::LocalModuleResolutionCache::default(),
517        }
518    }
519
520    pub(crate) fn queue_pending_execution_event(
521        &mut self,
522        event: ActiveExecutionEvent,
523    ) -> Result<(), SidecarError> {
524        if self.pending_execution_events.len() >= MAX_PROCESS_EVENT_QUEUE {
525            return Err(process_event_queue_overflow_error());
526        }
527        self.pending_execution_events.push_back(event);
528        Ok(())
529    }
530
531    pub(crate) fn with_host_cwd(mut self, host_cwd: PathBuf) -> Self {
532        self.host_cwd = host_cwd;
533        self
534    }
535
536    pub(crate) fn mark_host_write_dirty(&mut self) {
537        self.host_write_dirty = true;
538    }
539
540    pub(crate) fn host_write_dirty_recursive(&self) -> bool {
541        self.host_write_dirty
542            || self
543                .child_processes
544                .values()
545                .any(ActiveProcess::host_write_dirty_recursive)
546    }
547
548    pub(crate) fn clean_host_writes_are_observable_recursive(&self) -> bool {
549        matches!(
550            self.execution,
551            ActiveExecution::Javascript(_) | ActiveExecution::Python(_) | ActiveExecution::Wasm(_)
552        ) && self
553            .child_processes
554            .values()
555            .all(ActiveProcess::clean_host_writes_are_observable_recursive)
556    }
557
558    pub(crate) fn with_guest_cwd(mut self, guest_cwd: String) -> Self {
559        self.guest_cwd = guest_cwd;
560        self
561    }
562
563    pub(crate) fn with_env(mut self, env: BTreeMap<String, String>) -> Self {
564        self.env = env;
565        self
566    }
567
568    pub(crate) fn with_kernel_stdin_writer_fd(mut self, fd: u32) -> Self {
569        self.kernel_stdin_writer_fd = Some(fd);
570        self
571    }
572
573    pub(crate) fn with_tty_master_fd(mut self, fd: Option<u32>) -> Self {
574        self.tty_master_fd = fd;
575        self
576    }
577
578    pub(crate) fn with_detached(mut self, detached: bool) -> Self {
579        self.detached = detached;
580        self
581    }
582
583    pub(crate) fn allocate_mapped_host_fd(&mut self, fd: ActiveMappedHostFd) -> u32 {
584        let handle = self.next_mapped_host_fd;
585        self.next_mapped_host_fd = self
586            .next_mapped_host_fd
587            .checked_add(1)
588            .unwrap_or(MAPPED_HOST_FD_START);
589        self.mapped_host_fds.insert(handle, fd);
590        handle
591    }
592
593    pub(crate) fn mapped_host_fd(&self, fd: u32) -> Option<&ActiveMappedHostFd> {
594        self.mapped_host_fds.get(&fd)
595    }
596
597    pub(crate) fn mapped_host_fd_mut(&mut self, fd: u32) -> Option<&mut ActiveMappedHostFd> {
598        self.mapped_host_fds.get_mut(&fd)
599    }
600
601    pub(crate) fn close_mapped_host_fd(&mut self, fd: u32) -> bool {
602        self.mapped_host_fds.remove(&fd).is_some()
603    }
604
605    pub(crate) fn allocate_child_process_id(&mut self) -> String {
606        self.next_child_process_id += 1;
607        format!("child-{}", self.next_child_process_id)
608    }
609
610    fn allocate_tcp_listener_id(&mut self) -> String {
611        self.next_tcp_listener_id += 1;
612        format!("listener-{}", self.next_tcp_listener_id)
613    }
614
615    fn allocate_tcp_socket_id(&mut self) -> String {
616        self.next_tcp_socket_id += 1;
617        format!("socket-{}", self.next_tcp_socket_id)
618    }
619
620    fn allocate_tcp_port_reservation_id(&mut self) -> String {
621        self.next_tcp_port_reservation_id += 1;
622        format!("tcp-port-reservation-{}", self.next_tcp_port_reservation_id)
623    }
624
625    fn allocate_unix_listener_id(&mut self) -> String {
626        self.next_unix_listener_id += 1;
627        format!("unix-listener-{}", self.next_unix_listener_id)
628    }
629
630    fn allocate_unix_socket_id(&mut self) -> String {
631        self.next_unix_socket_id += 1;
632        format!("unix-socket-{}", self.next_unix_socket_id)
633    }
634
635    fn allocate_udp_socket_id(&mut self) -> String {
636        self.next_udp_socket_id += 1;
637        format!("udp-socket-{}", self.next_udp_socket_id)
638    }
639
640    pub(crate) fn network_resource_counts(&self) -> NetworkResourceCounts {
641        let mut counts = NetworkResourceCounts {
642            sockets: self.http_servers.len()
643                + self.tcp_listeners.len()
644                + self.tcp_sockets.len()
645                + self.unix_listeners.len()
646                + self.unix_sockets.len()
647                + self.udp_sockets.len()
648                + self.python_sockets.len(),
649            connections: self.tcp_sockets.len() + self.unix_sockets.len(),
650        };
651        if let Ok(http2) = self.http2.shared.lock() {
652            counts.sockets += http2.servers.len() + http2.sessions.len();
653            counts.connections += http2.sessions.len();
654        }
655
656        for child in self.child_processes.values() {
657            let child_counts = child.network_resource_counts();
658            counts.sockets += child_counts.sockets;
659            counts.connections += child_counts.connections;
660        }
661
662        counts
663    }
664
665    fn sidecar_only_network_resource_counts(&self) -> NetworkResourceCounts {
666        let mut counts = NetworkResourceCounts {
667            sockets: self.http_servers.len()
668                + self
669                    .tcp_listeners
670                    .values()
671                    .filter(|listener| listener.kernel_socket_id.is_none())
672                    .count()
673                + self
674                    .tcp_sockets
675                    .values()
676                    .filter(|socket| socket.kernel_socket_id.is_none())
677                    .count()
678                + self.unix_listeners.len()
679                + self.unix_sockets.len()
680                + self
681                    .udp_sockets
682                    .values()
683                    .filter(|socket| socket.kernel_socket_id.is_none())
684                    .count()
685                + self.python_sockets.len(),
686            connections: self
687                .tcp_sockets
688                .values()
689                .filter(|socket| socket.kernel_socket_id.is_none())
690                .count()
691                + self.unix_sockets.len(),
692        };
693        if let Ok(http2) = self.http2.shared.lock() {
694            counts.sockets += http2.servers.len() + http2.sessions.len();
695            counts.connections += http2.sessions.len();
696        }
697
698        for child in self.child_processes.values() {
699            let child_counts = child.sidecar_only_network_resource_counts();
700            counts.sockets += child_counts.sockets;
701            counts.connections += child_counts.connections;
702        }
703
704        counts
705    }
706}
707
708fn poll_tool_process_event(
709    execution: &ToolExecution,
710) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
711    let event = execution
712        .pending_events
713        .lock()
714        .unwrap_or_else(|poisoned| poisoned.into_inner())
715        .pop_front();
716    if event.is_some() {
717        return Ok(event);
718    }
719    if execution.events_overflowed.load(Ordering::Relaxed) {
720        return Err(process_event_queue_overflow_error());
721    }
722    Ok(None)
723}
724
725fn descendant_pending_execution_event_capacity(
726    root: &ActiveProcess,
727    child_path: &[&str],
728) -> Option<usize> {
729    let mut child = root;
730    for child_process_id in child_path {
731        child = child.child_processes.get(*child_process_id)?;
732    }
733    Some(MAX_PROCESS_EVENT_QUEUE.saturating_sub(child.pending_execution_events.len()))
734}
735
736fn poll_child_execution_after_exit(
737    child: &mut ActiveProcess,
738    wait: Duration,
739) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
740    match child.execution.poll_event_blocking(wait) {
741        Ok(event) => Ok(event),
742        Err(SidecarError::Execution(message))
743            if child.runtime == GuestRuntimeKind::WebAssembly
744                && message == WasmExecutionError::EventChannelClosed.to_string() =>
745        {
746            Ok(None)
747        }
748        Err(error) => Err(error),
749    }
750}
751
752fn closed_javascript_event_channel(message: &str) -> bool {
753    message == "guest JavaScript event channel closed unexpectedly"
754}
755
756fn closed_python_event_channel(message: &str) -> bool {
757    message == "guest Python event channel closed unexpectedly"
758}
759
760fn closed_wasm_event_channel(message: &str) -> bool {
761    message == WasmExecutionError::EventChannelClosed.to_string()
762}
763
764fn missing_vm_error(vm_id: &str) -> SidecarError {
765    SidecarError::InvalidState(format!("VM {vm_id} is no longer active"))
766}
767
768fn missing_process_error(vm_id: &str, process_id: &str) -> SidecarError {
769    SidecarError::InvalidState(format!(
770        "VM {vm_id} no longer has active process {process_id}"
771    ))
772}
773
774/// Map a shared guest-kernel-call dispatcher error into a sidecar error,
775/// preserving POSIX errno codes (`ECODE: message`) as kernel errors so guest
776/// callers observe Linux-faithful failures, mirroring the filesystem path.
777fn guest_kernel_core_error(error: secure_exec_sidecar_core::SidecarCoreError) -> SidecarError {
778    let message = error.to_string();
779    let is_errno = message.split_once(':').is_some_and(|(code, _)| {
780        code.len() >= 2
781            && code.starts_with('E')
782            && code[1..]
783                .bytes()
784                .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_')
785    });
786    if is_errno {
787        SidecarError::Kernel(message)
788    } else {
789        SidecarError::InvalidState(message)
790    }
791}
792
793fn is_broken_pipe_error(error: &SidecarError) -> bool {
794    matches!(error, SidecarError::Execution(message) if message.contains("Broken pipe") || message.contains("os error 32") || message.contains("EPIPE"))
795}
796
797fn javascript_child_process_gone_error(process_id: &str, child_path: &[&str]) -> SidecarError {
798    let child_label = if child_path.is_empty() {
799        process_id.to_owned()
800    } else {
801        format!("{process_id}/{}", child_path.join("/"))
802    };
803    SidecarError::Execution(format!(
804        "ECHILD: child_process {child_label} is no longer available"
805    ))
806}
807
808fn is_javascript_child_process_gone_error(error: &SidecarError) -> bool {
809    matches!(
810        error,
811        SidecarError::Execution(message) if guest_errno_code(message) == Some("ECHILD")
812    )
813}
814
815fn loopback_tls_transport_registry(
816) -> &'static Mutex<BTreeMap<String, Weak<crate::state::LoopbackTlsTransportPair>>> {
817    static REGISTRY: OnceLock<
818        Mutex<BTreeMap<String, Weak<crate::state::LoopbackTlsTransportPair>>>,
819    > = OnceLock::new();
820    REGISTRY.get_or_init(|| Mutex::new(BTreeMap::new()))
821}
822
823#[cfg(test)]
824#[allow(dead_code)]
825pub(crate) fn loopback_tls_registry_len() -> usize {
826    loopback_tls_transport_registry()
827        .lock()
828        .expect("loopback TLS transport registry lock poisoned")
829        .len()
830}
831
832#[cfg(test)]
833pub(crate) fn loopback_tls_registry_contains(key: &str) -> bool {
834    loopback_tls_transport_registry()
835        .lock()
836        .expect("loopback TLS transport registry lock poisoned")
837        .contains_key(key)
838}
839
840fn loopback_tls_transport_key(
841    vm_id: &str,
842    socket_id: SocketId,
843    peer_socket_id: SocketId,
844) -> String {
845    let (lower, higher) = if socket_id <= peer_socket_id {
846        (socket_id, peer_socket_id)
847    } else {
848        (peer_socket_id, socket_id)
849    };
850    format!("{vm_id}:{lower}:{higher}")
851}
852
853fn loopback_tls_endpoint(
854    vm_id: &str,
855    socket_id: SocketId,
856    peer_socket_id: SocketId,
857) -> Result<crate::state::LoopbackTlsEndpoint, SidecarError> {
858    let key = loopback_tls_transport_key(vm_id, socket_id, peer_socket_id);
859    let registry = loopback_tls_transport_registry();
860    let mut transports = registry.lock().map_err(|_| {
861        SidecarError::InvalidState(String::from(
862            "loopback TLS transport registry lock poisoned",
863        ))
864    })?;
865    transports.retain(|_, pair| pair.strong_count() > 0);
866    let pair = transports
867        .get(&key)
868        .and_then(Weak::upgrade)
869        .unwrap_or_else(|| {
870            let pair = Arc::new(crate::state::LoopbackTlsTransportPair {
871                state: Mutex::new(crate::state::LoopbackTlsTransportPairState::default()),
872                ready: std::sync::Condvar::new(),
873            });
874            transports.insert(key.clone(), Arc::downgrade(&pair));
875            pair
876        });
877    Ok(crate::state::LoopbackTlsEndpoint {
878        pair,
879        is_lower_socket: socket_id <= peer_socket_id,
880        poll_timeout: TCP_SOCKET_POLL_TIMEOUT,
881        registry_key: Some(key),
882    })
883}
884
885impl crate::state::LoopbackTlsEndpoint {
886    fn set_poll_timeout(&mut self, timeout: Duration) {
887        self.poll_timeout = timeout;
888    }
889
890    fn interrupt_reader(pair: &Arc<crate::state::LoopbackTlsTransportPair>, is_lower_socket: bool) {
891        if let Ok(mut state) = pair.state.lock() {
892            if is_lower_socket {
893                state.lower_read_interrupt = true;
894            } else {
895                state.higher_read_interrupt = true;
896            }
897            pair.ready.notify_all();
898        }
899    }
900
901    fn shutdown_write(&self) -> Result<(), SidecarError> {
902        let mut state = self.pair.state.lock().map_err(|_| {
903            SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
904        })?;
905        if self.is_lower_socket {
906            state.lower_write_closed = true;
907        } else {
908            state.higher_write_closed = true;
909        }
910        self.pair.ready.notify_all();
911        Ok(())
912    }
913
914    fn close_endpoint(&self) -> Result<(), SidecarError> {
915        let mut state = self.pair.state.lock().map_err(|_| {
916            SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
917        })?;
918        if self.is_lower_socket {
919            state.lower_write_closed = true;
920            state.lower_closed = true;
921        } else {
922            state.higher_write_closed = true;
923            state.higher_closed = true;
924        }
925        self.pair.ready.notify_all();
926        Ok(())
927    }
928}
929
930impl LoopbackTlsPendingWriteHandle {
931    pub(crate) fn new(endpoint: &crate::state::LoopbackTlsEndpoint) -> Self {
932        Self {
933            state: Arc::new(Mutex::new(LoopbackTlsPendingWriteState {
934                buffer: Vec::new(),
935                warned_near_cap: false,
936                flushing: false,
937                defer_shutdown_write: false,
938                failure_message: None,
939            })),
940            tls_handshake_complete: Arc::new(AtomicBool::new(false)),
941            failed: Arc::new(AtomicBool::new(false)),
942            pair: Arc::clone(&endpoint.pair),
943            is_lower_socket: endpoint.is_lower_socket,
944            handshake_started_at: Instant::now(),
945        }
946    }
947
948    fn failure_error(&self) -> SidecarError {
949        let message = self
950            .state
951            .lock()
952            .ok()
953            .and_then(|state| state.failure_message.clone())
954            .unwrap_or_else(|| String::from("loopback TLS pending write failed"));
955        SidecarError::Execution(message)
956    }
957
958    fn mark_failed(&self, message: impl Into<String>) {
959        self.failed.store(true, Ordering::SeqCst);
960        if let Ok(mut state) = self.state.lock() {
961            state.buffer.clear();
962            state.flushing = false;
963            state.defer_shutdown_write = false;
964            state.failure_message = Some(message.into());
965        }
966    }
967
968    fn clear_for_close(&self) {
969        self.mark_failed("loopback TLS socket closed before buffered writes flushed");
970    }
971
972    fn fail_if_pending(&self, message: impl Into<String>) {
973        let has_pending = self
974            .state
975            .lock()
976            .map(|state| !state.buffer.is_empty() || state.flushing || state.defer_shutdown_write)
977            .unwrap_or(true);
978        if has_pending {
979            self.mark_failed(message);
980        }
981    }
982
983    fn should_buffer_write(&self) -> Result<bool, SidecarError> {
984        if self.failed.load(Ordering::SeqCst) {
985            return Err(self.failure_error());
986        }
987        let state = self.state.lock().map_err(|_| {
988            SidecarError::InvalidState(String::from("loopback TLS pending write lock poisoned"))
989        })?;
990        Ok(!self.tls_handshake_complete.load(Ordering::SeqCst)
991            || !state.buffer.is_empty()
992            || state.flushing)
993    }
994
995    pub(crate) fn append_write(&self, contents: &[u8]) -> Result<(), SidecarError> {
996        if self.failed.load(Ordering::SeqCst) {
997            return Err(self.failure_error());
998        }
999        let mut state = self.state.lock().map_err(|_| {
1000            SidecarError::InvalidState(String::from("loopback TLS pending write lock poisoned"))
1001        })?;
1002        let observed = state.buffer.len().saturating_add(contents.len());
1003        if observed > LOOPBACK_TLS_PENDING_WRITE_BUFFER_BYTES {
1004            return Err(SidecarError::Execution(format!(
1005                "loopback TLS pending write buffer exceeded: {observed} bytes > {cap} bytes (limit: loopback TLS pending write buffer)",
1006                cap = LOOPBACK_TLS_PENDING_WRITE_BUFFER_BYTES
1007            )));
1008        }
1009        if !state.warned_near_cap && observed >= LOOPBACK_TLS_PENDING_WRITE_WARNING_BYTES {
1010            state.warned_near_cap = true;
1011            tracing::warn!(
1012                limit = "loopback_tls_pending_write_buffer",
1013                observed_bytes = observed,
1014                cap_bytes = LOOPBACK_TLS_PENDING_WRITE_BUFFER_BYTES,
1015                fill_percent =
1016                    (observed as f64 / LOOPBACK_TLS_PENDING_WRITE_BUFFER_BYTES as f64) * 100.0,
1017                wired = "invariant.loopbackTlsPendingWriteBufferBytes",
1018                "loopback TLS pending write buffer is near capacity"
1019            );
1020        }
1021        state.buffer.extend_from_slice(contents);
1022        Ok(())
1023    }
1024
1025    fn defer_shutdown_write(&self) -> Result<(), SidecarError> {
1026        if self.failed.load(Ordering::SeqCst) {
1027            return Err(self.failure_error());
1028        }
1029        let mut state = self.state.lock().map_err(|_| {
1030            SidecarError::InvalidState(String::from("loopback TLS pending write lock poisoned"))
1031        })?;
1032        state.defer_shutdown_write = true;
1033        Ok(())
1034    }
1035
1036    fn take_buffer_for_flush(&self) -> Result<Option<Vec<u8>>, SidecarError> {
1037        let mut state = self.state.lock().map_err(|_| {
1038            SidecarError::InvalidState(String::from("loopback TLS pending write lock poisoned"))
1039        })?;
1040        if state.buffer.is_empty() {
1041            return Ok(None);
1042        }
1043        state.flushing = true;
1044        Ok(Some(std::mem::take(&mut state.buffer)))
1045    }
1046
1047    fn finish_flush(&self) {
1048        if let Ok(mut state) = self.state.lock() {
1049            state.flushing = false;
1050        }
1051    }
1052
1053    fn take_deferred_shutdown_write(&self) -> Result<bool, SidecarError> {
1054        let mut state = self.state.lock().map_err(|_| {
1055            SidecarError::InvalidState(String::from("loopback TLS pending write lock poisoned"))
1056        })?;
1057        if state.buffer.is_empty() && !state.flushing && state.defer_shutdown_write {
1058            state.defer_shutdown_write = false;
1059            return Ok(true);
1060        }
1061        Ok(false)
1062    }
1063
1064    fn interrupt_own_reader(&self) {
1065        crate::state::LoopbackTlsEndpoint::interrupt_reader(&self.pair, self.is_lower_socket);
1066    }
1067}
1068
1069fn parse_tls_client_hello_from_bytes(
1070    buffer: &[u8],
1071) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
1072    if buffer.is_empty() {
1073        return Ok(None);
1074    }
1075
1076    let mut acceptor = rustls::server::Acceptor::default();
1077    let mut cursor = Cursor::new(buffer);
1078    acceptor.read_tls(&mut cursor).map_err(sidecar_net_error)?;
1079    let Some(accepted) = acceptor.accept().map_err(|(error, _)| {
1080        SidecarError::Execution(format!("failed to parse TLS client hello: {error}"))
1081    })?
1082    else {
1083        return Ok(None);
1084    };
1085    let client_hello = accepted.client_hello();
1086    let alpn_protocols = client_hello.alpn().map(|protocols| {
1087        protocols
1088            .filter_map(|protocol| String::from_utf8(protocol.to_vec()).ok())
1089            .collect::<Vec<_>>()
1090    });
1091    Ok(Some(JavascriptTlsClientHello {
1092        servername: client_hello.server_name().map(str::to_owned),
1093        alpn_protocols,
1094    }))
1095}
1096
1097fn peek_loopback_tls_client_hello(
1098    vm_id: &str,
1099    socket_id: SocketId,
1100    peer_socket_id: SocketId,
1101) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
1102    let key = loopback_tls_transport_key(vm_id, socket_id, peer_socket_id);
1103    let registry = loopback_tls_transport_registry();
1104    let pair = registry
1105        .lock()
1106        .map_err(|_| {
1107            SidecarError::InvalidState(String::from(
1108                "loopback TLS transport registry lock poisoned",
1109            ))
1110        })?
1111        .get(&key)
1112        .and_then(Weak::upgrade);
1113    let Some(pair) = pair else {
1114        return Ok(None);
1115    };
1116    let is_lower_socket = socket_id <= peer_socket_id;
1117    let state = pair.state.lock().map_err(|_| {
1118        SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
1119    })?;
1120    let buffered = if is_lower_socket {
1121        state.higher_to_lower.iter().copied().collect::<Vec<_>>()
1122    } else {
1123        state.lower_to_higher.iter().copied().collect::<Vec<_>>()
1124    };
1125    drop(state);
1126    parse_tls_client_hello_from_bytes(&buffered)
1127}
1128
1129fn wait_for_loopback_peer_socket_id(
1130    kernel: &SidecarKernel,
1131    socket_id: SocketId,
1132) -> Option<SocketId> {
1133    // Keep the total wait budget aligned with the previous ~500ms poll window.
1134    let deadline = Instant::now() + Duration::from_millis(500);
1135    let mut backoff = Duration::from_micros(100);
1136    loop {
1137        if let Some(peer_socket_id) = kernel
1138            .socket_get(socket_id)
1139            .and_then(|record| record.peer_socket_id())
1140        {
1141            return Some(peer_socket_id);
1142        }
1143        let now = Instant::now();
1144        if now >= deadline {
1145            break;
1146        }
1147        std::thread::sleep(backoff.min(deadline.saturating_duration_since(now)));
1148        backoff = (backoff * 2).min(Duration::from_millis(1));
1149    }
1150    None
1151}
1152
1153impl Drop for crate::state::LoopbackTlsEndpoint {
1154    fn drop(&mut self) {
1155        let _ = self.close_endpoint();
1156
1157        // Eagerly prune this endpoint's registry entry once we are the last owner
1158        // of the shared transport pair. Without this, the `Weak` entry survives
1159        // until the next `loopback_tls_endpoint()` call runs its lazy `retain()`,
1160        // so dead entries accumulate under intermittent use. We must NOT remove
1161        // the entry while a peer endpoint still shares the pair, otherwise a later
1162        // connection for the same socket pair would fail to find it and build a
1163        // mismatched fresh pair.
1164        let Some(key) = self.registry_key.take() else {
1165            return;
1166        };
1167        let Ok(mut transports) = loopback_tls_transport_registry().lock() else {
1168            // Lock poisoned: leave the entry for the lazy `retain()` to reclaim.
1169            return;
1170        };
1171        let should_remove = match transports.get(&key) {
1172            // Only prune when the registered entry still points at *our* pair and
1173            // `self` is its last strong owner. During `Drop` `self.pair` is still
1174            // alive, so a strong count of 1 (after dropping the temporary upgrade)
1175            // means no other endpoint references it.
1176            Some(weak) => match weak.upgrade() {
1177                Some(existing) => {
1178                    let same_pair = Arc::ptr_eq(&existing, &self.pair);
1179                    drop(existing);
1180                    same_pair && Arc::strong_count(&self.pair) <= 1
1181                }
1182                None => true,
1183            },
1184            None => false,
1185        };
1186        if should_remove {
1187            transports.remove(&key);
1188        }
1189    }
1190}
1191
1192impl Read for crate::state::LoopbackTlsEndpoint {
1193    fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
1194        let mut state = self
1195            .pair
1196            .state
1197            .lock()
1198            .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
1199
1200        loop {
1201            let (peer_write_closed, peer_closed) = if self.is_lower_socket {
1202                (state.higher_write_closed, state.higher_closed)
1203            } else {
1204                (state.lower_write_closed, state.lower_closed)
1205            };
1206
1207            let incoming = if self.is_lower_socket {
1208                &mut state.higher_to_lower
1209            } else {
1210                &mut state.lower_to_higher
1211            };
1212
1213            if !incoming.is_empty() {
1214                let count = incoming.len().min(buffer.len());
1215                let (head, tail) = incoming.as_slices();
1216                let head_count = head.len().min(count);
1217                buffer[..head_count].copy_from_slice(&head[..head_count]);
1218                if head_count < count {
1219                    buffer[head_count..count].copy_from_slice(&tail[..count - head_count]);
1220                }
1221                incoming.drain(..count);
1222                return Ok(count);
1223            }
1224
1225            if peer_write_closed || peer_closed {
1226                return Ok(0);
1227            }
1228
1229            let read_interrupted = if self.is_lower_socket {
1230                std::mem::take(&mut state.lower_read_interrupt)
1231            } else {
1232                std::mem::take(&mut state.higher_read_interrupt)
1233            };
1234            if read_interrupted {
1235                return Err(std::io::Error::new(
1236                    std::io::ErrorKind::WouldBlock,
1237                    "loopback TLS transport read interrupted",
1238                ));
1239            }
1240
1241            let (next_state, wait_result) = self
1242                .pair
1243                .ready
1244                .wait_timeout(state, self.poll_timeout)
1245                .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
1246            state = next_state;
1247            if wait_result.timed_out() {
1248                return Err(std::io::Error::new(
1249                    std::io::ErrorKind::WouldBlock,
1250                    "loopback TLS transport read timed out",
1251                ));
1252            }
1253        }
1254    }
1255}
1256
1257impl Write for crate::state::LoopbackTlsEndpoint {
1258    fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
1259        let mut state = self
1260            .pair
1261            .state
1262            .lock()
1263            .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
1264
1265        let peer_closed = if self.is_lower_socket {
1266            state.higher_closed
1267        } else {
1268            state.lower_closed
1269        };
1270        let outgoing = if self.is_lower_socket {
1271            &mut state.lower_to_higher
1272        } else {
1273            &mut state.higher_to_lower
1274        };
1275        if peer_closed {
1276            return Err(std::io::Error::new(
1277                std::io::ErrorKind::BrokenPipe,
1278                "loopback TLS peer is closed",
1279            ));
1280        }
1281
1282        outgoing.extend(buffer.iter().copied());
1283        self.pair.ready.notify_all();
1284        Ok(buffer.len())
1285    }
1286
1287    fn flush(&mut self) -> std::io::Result<()> {
1288        Ok(())
1289    }
1290}
1291
1292#[cfg(test)]
1293mod loopback_tls_registry_tests {
1294    use super::{
1295        loopback_tls_endpoint, loopback_tls_registry_contains, loopback_tls_transport_key,
1296    };
1297
1298    // Each test uses a unique vm_id so the process-global registry stays
1299    // partitioned across concurrently running tests.
1300
1301    #[test]
1302    fn dropping_endpoint_removes_its_registry_entry() {
1303        let vm_id = "loopback-tls-drop-removes-entry";
1304        let key = loopback_tls_transport_key(vm_id, 1, 2);
1305
1306        let endpoint = loopback_tls_endpoint(vm_id, 1, 2).expect("create endpoint");
1307        assert!(
1308            loopback_tls_registry_contains(&key),
1309            "registry should contain the key while the endpoint is alive"
1310        );
1311
1312        drop(endpoint);
1313        assert!(
1314            !loopback_tls_registry_contains(&key),
1315            "registry entry must be pruned in the endpoint's Drop, not left for the lazy retain()"
1316        );
1317    }
1318
1319    #[test]
1320    fn registry_entry_survives_until_last_peer_endpoint_drops() {
1321        let vm_id = "loopback-tls-shared-pair";
1322        let key = loopback_tls_transport_key(vm_id, 3, 4);
1323
1324        // Both peers of a loopback connection share the same transport pair.
1325        let lower = loopback_tls_endpoint(vm_id, 3, 4).expect("create lower endpoint");
1326        let higher = loopback_tls_endpoint(vm_id, 4, 3).expect("create higher endpoint");
1327        assert!(loopback_tls_registry_contains(&key));
1328
1329        // Dropping one peer must keep the entry, since the other peer still owns
1330        // the shared pair and a later connection must be able to find it.
1331        drop(lower);
1332        assert!(
1333            loopback_tls_registry_contains(&key),
1334            "entry must survive while a peer endpoint still shares the pair"
1335        );
1336
1337        drop(higher);
1338        assert!(
1339            !loopback_tls_registry_contains(&key),
1340            "entry must be pruned once the last peer endpoint drops"
1341        );
1342    }
1343}
1344
1345// TCP types moved to crate::state
1346
1347struct ActiveTcpConnectRequest<'a, B> {
1348    bridge: &'a SharedBridge<B>,
1349    kernel: &'a mut SidecarKernel,
1350    kernel_pid: u32,
1351    vm_id: &'a str,
1352    dns: &'a VmDnsConfig,
1353    host: &'a str,
1354    port: u16,
1355    local_address: Option<&'a str>,
1356    local_port: Option<u16>,
1357    local_reservation: Option<(JavascriptSocketFamily, u16)>,
1358    context: &'a JavascriptSocketPathContext,
1359}
1360
1361struct ActiveUdpSendToRequest<'a, B> {
1362    bridge: &'a SharedBridge<B>,
1363    kernel: &'a mut SidecarKernel,
1364    kernel_pid: u32,
1365    vm_id: &'a str,
1366    dns: &'a VmDnsConfig,
1367    host: &'a str,
1368    port: u16,
1369    context: &'a JavascriptSocketPathContext,
1370    contents: &'a [u8],
1371}
1372
1373struct UdpRemoteAddrRequest<'a, B> {
1374    bridge: &'a SharedBridge<B>,
1375    kernel: &'a SidecarKernel,
1376    vm_id: &'a str,
1377    dns: &'a VmDnsConfig,
1378    host: &'a str,
1379    port: u16,
1380    family: JavascriptUdpFamily,
1381    context: &'a JavascriptSocketPathContext,
1382}
1383
1384pub(crate) struct JavascriptSyncRpcServiceRequest<'a, B> {
1385    pub(crate) bridge: &'a SharedBridge<B>,
1386    pub(crate) vm_id: &'a str,
1387    pub(crate) dns: &'a VmDnsConfig,
1388    pub(crate) socket_paths: &'a JavascriptSocketPathContext,
1389    pub(crate) kernel: &'a mut SidecarKernel,
1390    pub(crate) kernel_readiness: KernelSocketReadinessRegistry,
1391    pub(crate) process: &'a mut ActiveProcess,
1392    pub(crate) sync_request: &'a JavascriptSyncRpcRequest,
1393    pub(crate) resource_limits: &'a ResourceLimits,
1394    pub(crate) network_counts: NetworkResourceCounts,
1395}
1396
1397pub(crate) enum JavascriptSyncRpcServiceResponse {
1398    Json(Value),
1399    Raw(Vec<u8>),
1400}
1401
1402impl From<Value> for JavascriptSyncRpcServiceResponse {
1403    fn from(value: Value) -> Self {
1404        Self::Json(value)
1405    }
1406}
1407
1408impl JavascriptSyncRpcServiceResponse {
1409    fn as_json(&self) -> Option<&Value> {
1410        match self {
1411            Self::Json(value) => Some(value),
1412            Self::Raw(_) => None,
1413        }
1414    }
1415}
1416
1417pub(crate) struct JavascriptNetSyncRpcServiceRequest<'a, B> {
1418    pub(crate) bridge: &'a SharedBridge<B>,
1419    pub(crate) vm_id: &'a str,
1420    pub(crate) dns: &'a VmDnsConfig,
1421    pub(crate) socket_paths: &'a JavascriptSocketPathContext,
1422    pub(crate) kernel: &'a mut SidecarKernel,
1423    pub(crate) kernel_readiness: KernelSocketReadinessRegistry,
1424    pub(crate) process: &'a mut ActiveProcess,
1425    pub(crate) sync_request: &'a JavascriptSyncRpcRequest,
1426    pub(crate) resource_limits: &'a ResourceLimits,
1427    pub(crate) network_counts: NetworkResourceCounts,
1428}
1429
1430struct LoopbackHttpResponseWaitRequest<'a, B> {
1431    bridge: &'a SharedBridge<B>,
1432    vm_id: &'a str,
1433    dns: &'a VmDnsConfig,
1434    socket_paths: &'a JavascriptSocketPathContext,
1435    kernel: &'a mut SidecarKernel,
1436    kernel_readiness: KernelSocketReadinessRegistry,
1437    process: &'a mut ActiveProcess,
1438    resource_limits: &'a ResourceLimits,
1439    request_key: (u64, u64),
1440}
1441
1442pub(crate) struct LoopbackHttpDispatchRequest<'a, B> {
1443    pub(crate) bridge: &'a SharedBridge<B>,
1444    pub(crate) vm_id: &'a str,
1445    pub(crate) dns: &'a VmDnsConfig,
1446    pub(crate) socket_paths: &'a JavascriptSocketPathContext,
1447    pub(crate) kernel: &'a mut SidecarKernel,
1448    pub(crate) kernel_readiness: KernelSocketReadinessRegistry,
1449    pub(crate) process: &'a mut ActiveProcess,
1450    pub(crate) resource_limits: &'a ResourceLimits,
1451    pub(crate) server_id: u64,
1452    pub(crate) request_json: &'a str,
1453}
1454
1455struct JavascriptDgramSyncRpcServiceRequest<'a, B> {
1456    bridge: &'a SharedBridge<B>,
1457    kernel: &'a mut SidecarKernel,
1458    vm_id: &'a str,
1459    dns: &'a VmDnsConfig,
1460    socket_paths: &'a JavascriptSocketPathContext,
1461    process: &'a mut ActiveProcess,
1462    kernel_readiness: KernelSocketReadinessRegistry,
1463    sync_request: &'a JavascriptSyncRpcRequest,
1464    resource_limits: &'a ResourceLimits,
1465    network_counts: NetworkResourceCounts,
1466}
1467
1468struct JavascriptHttp2SyncRpcServiceRequest<'a, B> {
1469    bridge: &'a SharedBridge<B>,
1470    kernel: &'a mut SidecarKernel,
1471    vm_id: &'a str,
1472    dns: &'a VmDnsConfig,
1473    socket_paths: &'a JavascriptSocketPathContext,
1474    process: &'a mut ActiveProcess,
1475    sync_request: &'a JavascriptSyncRpcRequest,
1476    resource_limits: &'a ResourceLimits,
1477    network_counts: NetworkResourceCounts,
1478}
1479
1480impl ActiveTcpSocket {
1481    fn connect<B>(request: ActiveTcpConnectRequest<'_, B>) -> Result<Self, SidecarError>
1482    where
1483        B: NativeSidecarBridge + Send + 'static,
1484        BridgeError<B>: fmt::Debug + Send + Sync + 'static,
1485    {
1486        let ActiveTcpConnectRequest {
1487            bridge,
1488            kernel,
1489            kernel_pid,
1490            vm_id,
1491            dns,
1492            host,
1493            port,
1494            local_address,
1495            local_port,
1496            local_reservation,
1497            context,
1498        } = request;
1499        let resolved = resolve_tcp_connect_addr(bridge, kernel, vm_id, dns, host, port, context)?;
1500        if resolved.use_kernel_loopback {
1501            let family = JavascriptSocketFamily::from_ip(resolved.guest_remote_addr.ip());
1502            let requested_local_port = local_port.unwrap_or(0);
1503            let local_port = if requested_local_port != 0
1504                && local_reservation == Some((family, requested_local_port))
1505            {
1506                requested_local_port
1507            } else {
1508                allocate_guest_listen_port(
1509                    requested_local_port,
1510                    family,
1511                    &context.used_tcp_guest_ports,
1512                    context.listen_policy,
1513                )?
1514            };
1515            let local_ip = match (family, local_address) {
1516                (JavascriptSocketFamily::Ipv4, Some("0.0.0.0")) => {
1517                    IpAddr::V4(Ipv4Addr::UNSPECIFIED)
1518                }
1519                (JavascriptSocketFamily::Ipv4, Some("127.0.0.1") | Some("localhost") | None) => {
1520                    IpAddr::V4(Ipv4Addr::LOCALHOST)
1521                }
1522                (JavascriptSocketFamily::Ipv6, Some("::")) => IpAddr::V6(Ipv6Addr::UNSPECIFIED),
1523                (JavascriptSocketFamily::Ipv6, Some("::1") | Some("localhost") | None) => {
1524                    IpAddr::V6(Ipv6Addr::LOCALHOST)
1525                }
1526                (JavascriptSocketFamily::Ipv4, Some(other)) => {
1527                    return Err(SidecarError::Execution(format!(
1528                        "EACCES: TCP sockets must bind to loopback or unspecified addresses, got {other}"
1529                    )));
1530                }
1531                (JavascriptSocketFamily::Ipv6, Some(other)) => {
1532                    return Err(SidecarError::Execution(format!(
1533                        "EACCES: TCP sockets must bind to loopback or unspecified addresses, got {other}"
1534                    )));
1535                }
1536            };
1537            let local_addr = SocketAddr::new(local_ip, local_port);
1538            let spec = match family {
1539                JavascriptSocketFamily::Ipv4 => SocketSpec::tcp(),
1540                JavascriptSocketFamily::Ipv6 => {
1541                    SocketSpec::new(SocketDomain::Inet6, SocketType::Stream)
1542                }
1543            };
1544            let socket_id = kernel
1545                .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
1546                .map_err(kernel_error)?;
1547            kernel
1548                .socket_bind_inet(
1549                    EXECUTION_DRIVER_NAME,
1550                    kernel_pid,
1551                    socket_id,
1552                    InetSocketAddress::new(local_ip.to_string(), local_port),
1553                )
1554                .map_err(kernel_error)?;
1555            kernel
1556                .socket_connect_inet_loopback(
1557                    EXECUTION_DRIVER_NAME,
1558                    kernel_pid,
1559                    socket_id,
1560                    InetSocketAddress::new(
1561                        resolved.guest_remote_addr.ip().to_string(),
1562                        resolved.guest_remote_addr.port(),
1563                    ),
1564                )
1565                .map_err(kernel_error)?;
1566            return Ok(Self::from_kernel(
1567                socket_id,
1568                None,
1569                local_addr,
1570                resolved.guest_remote_addr,
1571            ));
1572        }
1573
1574        let stream = TcpStream::connect_timeout(&resolved.actual_addr, Duration::from_secs(30))
1575            .map_err(sidecar_net_error)?;
1576        let guest_local_addr = stream.local_addr().map_err(sidecar_net_error)?;
1577        Self::from_stream(stream, None, guest_local_addr, resolved.guest_remote_addr)
1578    }
1579
1580    fn from_stream(
1581        stream: TcpStream,
1582        listener_id: Option<String>,
1583        guest_local_addr: SocketAddr,
1584        guest_remote_addr: SocketAddr,
1585    ) -> Result<Self, SidecarError> {
1586        let read_stream = stream.try_clone().map_err(sidecar_net_error)?;
1587        read_stream
1588            .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
1589            .map_err(sidecar_net_error)?;
1590        let stream = Arc::new(Mutex::new(stream));
1591        let pending_read_stream = Arc::new(Mutex::new(Some(read_stream)));
1592        let (sender, events) = mpsc::channel();
1593        let tls_mode = Arc::new(AtomicBool::new(false));
1594        let tls_stream = Arc::new(Mutex::new(None));
1595        let tls_state = Arc::new(Mutex::new(None));
1596        let loopback_tls_pending_write = Arc::new(Mutex::new(None));
1597        let saw_local_shutdown = Arc::new(AtomicBool::new(false));
1598        let saw_remote_end = Arc::new(AtomicBool::new(false));
1599        let close_notified = Arc::new(AtomicBool::new(false));
1600
1601        Ok(Self {
1602            stream: Some(stream),
1603            pending_read_stream: Some(pending_read_stream),
1604            events: Some(events),
1605            event_sender: Some(sender),
1606            event_pusher: Arc::new(Mutex::new(None)),
1607            kernel_socket_id: None,
1608            no_delay: false,
1609            keep_alive: false,
1610            keep_alive_initial_delay_secs: None,
1611            guest_local_addr,
1612            guest_remote_addr,
1613            listener_id,
1614            tls_mode,
1615            tls_stream,
1616            tls_state,
1617            loopback_tls_pending_write,
1618            saw_local_shutdown,
1619            saw_remote_end,
1620            close_notified,
1621        })
1622    }
1623
1624    fn from_kernel(
1625        socket_id: SocketId,
1626        listener_id: Option<String>,
1627        guest_local_addr: SocketAddr,
1628        guest_remote_addr: SocketAddr,
1629    ) -> Self {
1630        let (sender, events) = mpsc::channel();
1631        Self {
1632            stream: None,
1633            pending_read_stream: None,
1634            events: Some(events),
1635            event_sender: Some(sender),
1636            event_pusher: Arc::new(Mutex::new(None)),
1637            kernel_socket_id: Some(socket_id),
1638            no_delay: false,
1639            keep_alive: false,
1640            keep_alive_initial_delay_secs: None,
1641            guest_local_addr,
1642            guest_remote_addr,
1643            listener_id,
1644            tls_mode: Arc::new(AtomicBool::new(false)),
1645            tls_stream: Arc::new(Mutex::new(None)),
1646            tls_state: Arc::new(Mutex::new(None)),
1647            loopback_tls_pending_write: Arc::new(Mutex::new(None)),
1648            saw_local_shutdown: Arc::new(AtomicBool::new(false)),
1649            saw_remote_end: Arc::new(AtomicBool::new(false)),
1650            close_notified: Arc::new(AtomicBool::new(false)),
1651        }
1652    }
1653
1654    fn set_event_pusher(&self, session: Option<V8SessionHandle>, socket_id: String) {
1655        let Some(session) = session else {
1656            return;
1657        };
1658        if let Ok(mut pusher) = self.event_pusher.lock() {
1659            *pusher = Some(JavascriptSocketEventPusher { session, socket_id });
1660        }
1661    }
1662
1663    fn poll(
1664        &mut self,
1665        kernel: &mut SidecarKernel,
1666        kernel_pid: u32,
1667        wait: Duration,
1668        trace_enabled: bool,
1669    ) -> Result<Option<JavascriptTcpSocketEvent>, SidecarError> {
1670        if self.tls_mode.load(Ordering::SeqCst) {
1671            self.ensure_tcp_reader()?;
1672            return match self
1673                .events
1674                .as_ref()
1675                .ok_or_else(|| {
1676                    SidecarError::InvalidState(String::from("TCP socket event channel missing"))
1677                })?
1678                .recv_timeout(wait)
1679            {
1680                Ok(event) => Ok(Some(event)),
1681                Err(RecvTimeoutError::Timeout) => Ok(None),
1682                Err(RecvTimeoutError::Disconnected) => Ok(None),
1683            };
1684        }
1685
1686        if let Some(socket_id) = self.kernel_socket_id {
1687            let poll_started = Instant::now();
1688            let result = kernel
1689                .poll_targets(
1690                    EXECUTION_DRIVER_NAME,
1691                    kernel_pid,
1692                    vec![PollTargetEntry::socket(
1693                        socket_id,
1694                        POLLIN | POLLHUP | POLLERR,
1695                    )],
1696                    i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
1697                )
1698                .map_err(kernel_error)?;
1699            let poll_elapsed = poll_started.elapsed();
1700            let revents = result
1701                .targets
1702                .first()
1703                .map(|entry| entry.revents)
1704                .unwrap_or_else(PollEvents::empty);
1705            record_net_tcp_kernel_poll(trace_enabled, wait, poll_elapsed, revents);
1706            if revents.is_empty() {
1707                return Ok(None);
1708            }
1709            if revents.intersects(POLLIN) {
1710                let read_started = Instant::now();
1711                let read_result =
1712                    kernel.socket_read(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, 64 * 1024);
1713                if trace_enabled {
1714                    NET_TCP_TRACE_COUNTERS.socket_read_kernel_us.fetch_add(
1715                        duration_micros_u64(read_started.elapsed()),
1716                        Ordering::Relaxed,
1717                    );
1718                }
1719                return match read_result {
1720                    Ok(Some(bytes)) if !bytes.is_empty() => {
1721                        if trace_enabled {
1722                            NET_TCP_TRACE_COUNTERS
1723                                .socket_read_data_events
1724                                .fetch_add(1, Ordering::Relaxed);
1725                            NET_TCP_TRACE_COUNTERS.socket_read_bytes.fetch_add(
1726                                u64::try_from(bytes.len()).unwrap_or(u64::MAX),
1727                                Ordering::Relaxed,
1728                            );
1729                        }
1730                        Ok(Some(JavascriptTcpSocketEvent::Data(bytes)))
1731                    }
1732                    Ok(Some(_)) => {
1733                        if trace_enabled {
1734                            NET_TCP_TRACE_COUNTERS
1735                                .socket_read_data_events
1736                                .fetch_add(1, Ordering::Relaxed);
1737                        }
1738                        Ok(Some(JavascriptTcpSocketEvent::Data(Vec::new())))
1739                    }
1740                    Ok(None) => {
1741                        if trace_enabled {
1742                            NET_TCP_TRACE_COUNTERS
1743                                .socket_read_end_events
1744                                .fetch_add(1, Ordering::Relaxed);
1745                        }
1746                        self.saw_remote_end.store(true, Ordering::SeqCst);
1747                        Ok(Some(JavascriptTcpSocketEvent::End))
1748                    }
1749                    Err(error) if error.code() == "EAGAIN" => {
1750                        if trace_enabled {
1751                            NET_TCP_TRACE_COUNTERS
1752                                .socket_read_eagain
1753                                .fetch_add(1, Ordering::Relaxed);
1754                        }
1755                        Ok(None)
1756                    }
1757                    Err(error) => {
1758                        if trace_enabled {
1759                            NET_TCP_TRACE_COUNTERS
1760                                .socket_read_errors
1761                                .fetch_add(1, Ordering::Relaxed);
1762                        }
1763                        Ok(Some(JavascriptTcpSocketEvent::Error {
1764                            code: Some(error.code().to_string()),
1765                            message: error.to_string(),
1766                        }))
1767                    }
1768                };
1769            }
1770            if revents.intersects(POLLHUP) {
1771                self.saw_remote_end.store(true, Ordering::SeqCst);
1772                return Ok(Some(JavascriptTcpSocketEvent::End));
1773            }
1774            if revents.intersects(POLLERR) {
1775                return Ok(Some(JavascriptTcpSocketEvent::Error {
1776                    code: Some(String::from("EPIPE")),
1777                    message: String::from("kernel TCP socket reported POLLERR"),
1778                }));
1779            }
1780            return Ok(None);
1781        }
1782
1783        self.ensure_tcp_reader()?;
1784        match self
1785            .events
1786            .as_ref()
1787            .ok_or_else(|| {
1788                SidecarError::InvalidState(String::from("TCP socket event channel missing"))
1789            })?
1790            .recv_timeout(wait)
1791        {
1792            Ok(event) => Ok(Some(event)),
1793            Err(RecvTimeoutError::Timeout) => Ok(None),
1794            Err(RecvTimeoutError::Disconnected) => Ok(None),
1795        }
1796    }
1797
1798    fn ensure_tcp_reader(&self) -> Result<(), SidecarError> {
1799        if self.kernel_socket_id.is_some() {
1800            return Ok(());
1801        }
1802        if self.tls_mode.load(Ordering::SeqCst) {
1803            return Ok(());
1804        }
1805        let read_stream = self
1806            .pending_read_stream
1807            .as_ref()
1808            .ok_or_else(|| {
1809                SidecarError::InvalidState(String::from("TCP socket reader handle missing"))
1810            })?
1811            .lock()
1812            .map_err(|_| {
1813                SidecarError::InvalidState(String::from("TCP socket reader lock poisoned"))
1814            })?
1815            .take();
1816        if let Some(read_stream) = read_stream {
1817            spawn_tcp_socket_reader(
1818                read_stream,
1819                self.event_sender
1820                    .as_ref()
1821                    .ok_or_else(|| {
1822                        SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1823                    })?
1824                    .clone(),
1825                Arc::clone(&self.event_pusher),
1826                Arc::clone(&self.tls_mode),
1827                Arc::clone(&self.saw_local_shutdown),
1828                Arc::clone(&self.saw_remote_end),
1829                Arc::clone(&self.close_notified),
1830            );
1831        }
1832        Ok(())
1833    }
1834
1835    fn socket_info(&self) -> Value {
1836        tcp_socket_info_value(&self.guest_local_addr, &self.guest_remote_addr)
1837    }
1838
1839    fn set_no_delay(&mut self, enable: bool) -> Result<(), SidecarError> {
1840        self.no_delay = enable;
1841        if self.kernel_socket_id.is_some() {
1842            return Ok(());
1843        }
1844        let stream = self
1845            .stream
1846            .as_ref()
1847            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1848            .lock()
1849            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1850        stream.set_nodelay(enable).map_err(sidecar_net_error)
1851    }
1852
1853    fn set_keep_alive(
1854        &mut self,
1855        enable: bool,
1856        initial_delay_secs: Option<u64>,
1857    ) -> Result<(), SidecarError> {
1858        self.keep_alive = enable;
1859        self.keep_alive_initial_delay_secs = initial_delay_secs;
1860        if self.kernel_socket_id.is_some() {
1861            return Ok(());
1862        }
1863        let stream = self
1864            .stream
1865            .as_ref()
1866            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1867            .lock()
1868            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1869        let socket = SockRef::from(&*stream);
1870        socket.set_keepalive(enable).map_err(sidecar_net_error)?;
1871        if enable {
1872            if let Some(delay_secs) = initial_delay_secs.filter(|delay_secs| *delay_secs > 0) {
1873                socket
1874                    .set_tcp_keepalive(
1875                        &TcpKeepalive::new().with_time(Duration::from_secs(delay_secs)),
1876                    )
1877                    .map_err(sidecar_net_error)?;
1878            }
1879        }
1880        Ok(())
1881    }
1882
1883    fn upgrade_tls(
1884        &self,
1885        vm_id: &str,
1886        kernel: &SidecarKernel,
1887        options: JavascriptTlsBridgeOptions,
1888    ) -> Result<(), SidecarError> {
1889        if self.tls_mode.load(Ordering::SeqCst) {
1890            return Ok(());
1891        }
1892
1893        let client_hello = if options.is_server {
1894            self.peek_tls_client_hello(vm_id, kernel)?
1895        } else {
1896            None
1897        };
1898
1899        let (tls_stream, loopback_pending_write) = if let Some(socket_id) = self.kernel_socket_id {
1900            let peer_socket_id = wait_for_loopback_peer_socket_id(kernel, socket_id)
1901                .ok_or_else(|| {
1902                    SidecarError::Execution(format!(
1903                        "ERR_NOT_IMPLEMENTED: kernel-backed loopback socket {socket_id} has no peer for TLS upgrade"
1904                    ))
1905                })?;
1906            let endpoint = loopback_tls_endpoint(vm_id, socket_id, peer_socket_id)?;
1907            let pending_write = LoopbackTlsPendingWriteHandle::new(&endpoint);
1908            let tls_stream = if options.is_server {
1909                ActiveTlsStream::LoopbackServer(build_server_loopback_tls_stream(
1910                    endpoint, &options,
1911                )?)
1912            } else {
1913                ActiveTlsStream::LoopbackClient(build_client_loopback_tls_stream(
1914                    endpoint, &options,
1915                )?)
1916            };
1917            (tls_stream, Some(pending_write))
1918        } else {
1919            self.pending_read_stream
1920                .as_ref()
1921                .ok_or_else(|| {
1922                    SidecarError::InvalidState(String::from("TCP socket reader handle missing"))
1923                })?
1924                .lock()
1925                .map_err(|_| {
1926                    SidecarError::InvalidState(String::from("TCP socket reader lock poisoned"))
1927                })?
1928                .take();
1929            let stream = self
1930                .stream
1931                .as_ref()
1932                .ok_or_else(|| {
1933                    SidecarError::InvalidState(String::from("TCP socket stream missing"))
1934                })?
1935                .lock()
1936                .map_err(|_| {
1937                    SidecarError::InvalidState(String::from("TCP socket lock poisoned"))
1938                })?;
1939            let cloned = stream.try_clone().map_err(sidecar_net_error)?;
1940            drop(stream);
1941
1942            if options.is_server {
1943                (
1944                    ActiveTlsStream::Server(build_server_tls_stream(cloned, &options)?),
1945                    None,
1946                )
1947            } else {
1948                (
1949                    ActiveTlsStream::Client(build_client_tls_stream(cloned, &options)?),
1950                    None,
1951                )
1952            }
1953        };
1954
1955        let tls_state = ActiveTlsState {
1956            client_hello,
1957            local_certificates: tls_local_certificates(&options)?,
1958            session_reused: false,
1959        };
1960
1961        self.tls_mode.store(true, Ordering::SeqCst);
1962        {
1963            let mut state = self
1964                .tls_state
1965                .lock()
1966                .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?;
1967            *state = Some(tls_state);
1968        }
1969        {
1970            let mut stream = self.tls_stream.lock().map_err(|_| {
1971                SidecarError::InvalidState(String::from("TLS stream lock poisoned"))
1972            })?;
1973            *stream = Some(tls_stream);
1974        }
1975        {
1976            let mut pending = self.loopback_tls_pending_write.lock().map_err(|_| {
1977                SidecarError::InvalidState(String::from(
1978                    "loopback TLS pending write handle lock poisoned",
1979                ))
1980            })?;
1981            *pending = loopback_pending_write.clone();
1982        }
1983
1984        spawn_tls_socket_reader(
1985            Arc::clone(&self.tls_stream),
1986            loopback_pending_write,
1987            self.event_sender
1988                .as_ref()
1989                .ok_or_else(|| {
1990                    SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1991                })?
1992                .clone(),
1993            Arc::clone(&self.event_pusher),
1994            Arc::clone(&self.saw_local_shutdown),
1995            Arc::clone(&self.saw_remote_end),
1996            Arc::clone(&self.close_notified),
1997        );
1998        Ok(())
1999    }
2000
2001    fn peek_tls_client_hello(
2002        &self,
2003        vm_id: &str,
2004        kernel: &SidecarKernel,
2005    ) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
2006        if let Some(socket_id) = self.kernel_socket_id {
2007            let Some(peer_socket_id) = kernel
2008                .socket_get(socket_id)
2009                .and_then(|record| record.peer_socket_id())
2010            else {
2011                return Ok(None);
2012            };
2013            return peek_loopback_tls_client_hello(vm_id, socket_id, peer_socket_id);
2014        }
2015
2016        let stream = self
2017            .stream
2018            .as_ref()
2019            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
2020            .lock()
2021            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
2022        let mut buffer = vec![0_u8; 16 * 1024];
2023        let bytes = match stream.peek(&mut buffer) {
2024            Ok(0) => return Ok(None),
2025            Ok(bytes) => bytes,
2026            Err(error)
2027                if matches!(
2028                    error.kind(),
2029                    std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
2030                ) =>
2031            {
2032                return Ok(None);
2033            }
2034            Err(error) => return Err(sidecar_net_error(error)),
2035        };
2036        parse_tls_client_hello_from_bytes(&buffer[..bytes])
2037    }
2038
2039    fn tls_client_hello_json(
2040        &self,
2041        vm_id: &str,
2042        kernel: &SidecarKernel,
2043    ) -> Result<Value, SidecarError> {
2044        if let Some(client_hello) = self
2045            .tls_state
2046            .lock()
2047            .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?
2048            .as_ref()
2049            .and_then(|state| state.client_hello.clone())
2050        {
2051            return javascript_net_json_string(
2052                serde_json::to_value(client_hello).map_err(|error| {
2053                    SidecarError::InvalidState(format!(
2054                        "failed to serialize TLS client hello: {error}"
2055                    ))
2056                })?,
2057                "net.socket_get_tls_client_hello",
2058            );
2059        }
2060
2061        javascript_net_json_string(
2062            serde_json::to_value(
2063                self.peek_tls_client_hello(vm_id, kernel)?
2064                    .unwrap_or_default(),
2065            )
2066            .map_err(|error| {
2067                SidecarError::InvalidState(format!("failed to serialize TLS client hello: {error}"))
2068            })?,
2069            "net.socket_get_tls_client_hello",
2070        )
2071    }
2072
2073    fn tls_query(&self, query: &str, detailed: bool) -> Result<Value, SidecarError> {
2074        let state = self
2075            .tls_state
2076            .lock()
2077            .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?
2078            .clone();
2079        let mut tls_stream = self
2080            .tls_stream
2081            .lock()
2082            .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?;
2083        let Some(stream) = tls_stream.as_mut() else {
2084            return javascript_net_json_string(
2085                tls_bridge_undefined_value(),
2086                "net.socket_tls_query",
2087            );
2088        };
2089
2090        let payload = match query {
2091            "getSession" => tls_bridge_undefined_value(),
2092            "isSessionReused" => Value::Bool(
2093                state
2094                    .as_ref()
2095                    .is_some_and(|tls_state| tls_state.session_reused),
2096            ),
2097            "getPeerCertificate" => {
2098                let certificate = stream
2099                    .peer_certificates()
2100                    .and_then(|certificates| certificates.first())
2101                    .map(|certificate| {
2102                        tls_certificate_bridge_value(certificate.as_ref(), detailed)
2103                    });
2104                certificate.unwrap_or_else(tls_bridge_undefined_value)
2105            }
2106            "getCertificate" => state
2107                .as_ref()
2108                .and_then(|tls_state| tls_state.local_certificates.first())
2109                .map(|certificate| tls_certificate_bridge_value(certificate, detailed))
2110                .unwrap_or_else(tls_bridge_undefined_value),
2111            "getProtocol" => stream
2112                .protocol_version()
2113                .map(tls_protocol_name)
2114                .map(Value::String)
2115                .unwrap_or(Value::Null),
2116            "getCipher" => stream
2117                .negotiated_cipher_suite()
2118                .map(tls_cipher_bridge_value)
2119                .unwrap_or_else(tls_bridge_undefined_value),
2120            other => {
2121                return Err(SidecarError::InvalidState(format!(
2122                    "unsupported TLS query {other}"
2123                )));
2124            }
2125        };
2126        javascript_net_json_string(payload, "net.socket_tls_query")
2127    }
2128
2129    fn write_all(
2130        &self,
2131        kernel: &mut SidecarKernel,
2132        kernel_pid: u32,
2133        contents: &[u8],
2134    ) -> Result<usize, SidecarError> {
2135        if self.tls_mode.load(Ordering::SeqCst) {
2136            let loopback_pending_write = self
2137                .loopback_tls_pending_write
2138                .lock()
2139                .map_err(|_| {
2140                    SidecarError::InvalidState(String::from(
2141                        "loopback TLS pending write handle lock poisoned",
2142                    ))
2143                })?
2144                .clone();
2145            if let Some(pending_write) = loopback_pending_write.as_ref() {
2146                if pending_write.should_buffer_write()? {
2147                    pending_write.append_write(contents)?;
2148                    return Ok(contents.len());
2149                }
2150                pending_write.interrupt_own_reader();
2151            }
2152            let mut tls_stream = self.tls_stream.lock().map_err(|_| {
2153                SidecarError::InvalidState(String::from("TLS stream lock poisoned"))
2154            })?;
2155            let stream = tls_stream.as_mut().ok_or_else(|| {
2156                SidecarError::InvalidState(String::from("TLS stream missing for upgraded socket"))
2157            })?;
2158            stream.write_all(contents)?;
2159            return Ok(contents.len());
2160        }
2161        if let Some(socket_id) = self.kernel_socket_id {
2162            return kernel
2163                .socket_write(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, contents)
2164                .map_err(kernel_error);
2165        }
2166
2167        let mut stream = self
2168            .stream
2169            .as_ref()
2170            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
2171            .lock()
2172            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
2173        stream.write_all(contents).map_err(sidecar_net_error)?;
2174        Ok(contents.len())
2175    }
2176
2177    fn shutdown_write(
2178        &self,
2179        kernel: &mut SidecarKernel,
2180        kernel_pid: u32,
2181    ) -> Result<(), SidecarError> {
2182        if self.tls_mode.load(Ordering::SeqCst) {
2183            let loopback_pending_write = self
2184                .loopback_tls_pending_write
2185                .lock()
2186                .map_err(|_| {
2187                    SidecarError::InvalidState(String::from(
2188                        "loopback TLS pending write handle lock poisoned",
2189                    ))
2190                })?
2191                .clone();
2192            if let Some(pending_write) = loopback_pending_write.as_ref() {
2193                if pending_write.should_buffer_write()? {
2194                    pending_write.defer_shutdown_write()?;
2195                    self.saw_local_shutdown.store(true, Ordering::SeqCst);
2196                    return Ok(());
2197                }
2198                pending_write.interrupt_own_reader();
2199            }
2200            if let Some(stream) = self
2201                .tls_stream
2202                .lock()
2203                .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?
2204                .as_mut()
2205            {
2206                let _ = stream.send_close_notify();
2207                let _ = stream.shutdown_write();
2208            }
2209            if self.kernel_socket_id.is_some() {
2210                self.saw_local_shutdown.store(true, Ordering::SeqCst);
2211                return Ok(());
2212            }
2213        }
2214        if let Some(socket_id) = self.kernel_socket_id {
2215            self.saw_local_shutdown.store(true, Ordering::SeqCst);
2216            match kernel.socket_shutdown(
2217                EXECUTION_DRIVER_NAME,
2218                kernel_pid,
2219                socket_id,
2220                KernelSocketShutdown::Write,
2221            ) {
2222                Ok(()) => {}
2223                Err(error) if error.code() == "ENOENT" => {}
2224                Err(error) => return Err(kernel_error(error)),
2225            }
2226            return Ok(());
2227        }
2228        let stream = self
2229            .stream
2230            .as_ref()
2231            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
2232            .lock()
2233            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
2234        self.saw_local_shutdown.store(true, Ordering::SeqCst);
2235        match stream.shutdown(Shutdown::Write) {
2236            Ok(()) => {}
2237            Err(error) if error.kind() == std::io::ErrorKind::NotConnected => {}
2238            Err(error) => return Err(sidecar_net_error(error)),
2239        }
2240        if self.saw_remote_end.load(Ordering::SeqCst)
2241            && !self.close_notified.swap(true, Ordering::SeqCst)
2242        {
2243            let _ = self
2244                .event_sender
2245                .as_ref()
2246                .ok_or_else(|| {
2247                    SidecarError::InvalidState(String::from("TCP socket event sender missing"))
2248                })?
2249                .send(JavascriptTcpSocketEvent::Close { had_error: false });
2250        }
2251        Ok(())
2252    }
2253
2254    fn close(&self, kernel: &mut SidecarKernel, kernel_pid: u32) -> Result<(), SidecarError> {
2255        if self.tls_mode.load(Ordering::SeqCst) {
2256            if let Some(pending_write) = self
2257                .loopback_tls_pending_write
2258                .lock()
2259                .map_err(|_| {
2260                    SidecarError::InvalidState(String::from(
2261                        "loopback TLS pending write handle lock poisoned",
2262                    ))
2263                })?
2264                .take()
2265            {
2266                pending_write.clear_for_close();
2267                pending_write.interrupt_own_reader();
2268            }
2269            if let Some(stream) = self
2270                .tls_stream
2271                .lock()
2272                .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?
2273                .as_mut()
2274            {
2275                let _ = stream.send_close_notify();
2276                let _ = stream.close();
2277            }
2278        }
2279        if let Some(socket_id) = self.kernel_socket_id {
2280            return close_kernel_socket_idempotent(kernel, kernel_pid, socket_id);
2281        }
2282        let stream = self
2283            .stream
2284            .as_ref()
2285            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
2286            .lock()
2287            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
2288        stream.shutdown(Shutdown::Both).map_err(sidecar_net_error)
2289    }
2290}
2291
2292fn close_kernel_socket_idempotent(
2293    kernel: &mut SidecarKernel,
2294    kernel_pid: u32,
2295    socket_id: SocketId,
2296) -> Result<(), SidecarError> {
2297    match kernel.socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id) {
2298        Ok(()) => Ok(()),
2299        Err(error) if error.code() == "ENOENT" => Ok(()),
2300        Err(error) => Err(kernel_error(error)),
2301    }
2302}
2303
2304fn register_kernel_readiness_target(
2305    registry: &KernelSocketReadinessRegistry,
2306    kernel_socket_id: Option<SocketId>,
2307    session: Option<V8SessionHandle>,
2308    target_id: String,
2309    event: KernelSocketReadinessEvent,
2310) {
2311    let (Some(kernel_socket_id), Some(session)) = (kernel_socket_id, session) else {
2312        return;
2313    };
2314    if let Ok(mut targets) = registry.lock() {
2315        targets.insert(
2316            kernel_socket_id,
2317            KernelSocketReadinessTarget {
2318                session,
2319                target_id,
2320                event,
2321            },
2322        );
2323    }
2324}
2325
2326fn unregister_kernel_readiness_target(
2327    registry: &KernelSocketReadinessRegistry,
2328    kernel_socket_id: Option<SocketId>,
2329) {
2330    let Some(kernel_socket_id) = kernel_socket_id else {
2331        return;
2332    };
2333    if let Ok(mut targets) = registry.lock() {
2334        targets.remove(&kernel_socket_id);
2335    }
2336}
2337
2338fn release_tcp_socket_handle(
2339    process: &mut ActiveProcess,
2340    socket_id: &str,
2341    socket: ActiveTcpSocket,
2342    kernel: &mut SidecarKernel,
2343    kernel_readiness: &KernelSocketReadinessRegistry,
2344) {
2345    unregister_kernel_readiness_target(kernel_readiness, socket.kernel_socket_id);
2346    if let Some(listener_id) = socket.listener_id.as_deref() {
2347        if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
2348            listener.release_connection(socket_id);
2349        }
2350    }
2351    let _ = socket.close(kernel, process.kernel_pid);
2352}
2353
2354fn release_unix_socket_handle(
2355    process: &mut ActiveProcess,
2356    socket_id: &str,
2357    socket: ActiveUnixSocket,
2358) {
2359    if let Some(listener_id) = socket.listener_id.as_deref() {
2360        if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
2361            listener.release_connection(socket_id);
2362        }
2363    }
2364    let _ = socket.close();
2365}
2366
2367impl ActiveTlsStream {
2368    fn is_loopback(&self) -> bool {
2369        matches!(self, Self::LoopbackClient(_) | Self::LoopbackServer(_))
2370    }
2371
2372    fn is_handshaking(&self) -> bool {
2373        match self {
2374            Self::Client(stream) => stream.conn.is_handshaking(),
2375            Self::Server(stream) => stream.conn.is_handshaking(),
2376            Self::LoopbackClient(stream) => stream.conn.is_handshaking(),
2377            Self::LoopbackServer(stream) => stream.conn.is_handshaking(),
2378        }
2379    }
2380
2381    fn set_loopback_poll_timeout(&mut self, timeout: Duration) {
2382        match self {
2383            Self::LoopbackClient(stream) => stream.sock.set_poll_timeout(timeout),
2384            Self::LoopbackServer(stream) => stream.sock.set_poll_timeout(timeout),
2385            Self::Client(_) | Self::Server(_) => {}
2386        }
2387    }
2388
2389    fn write_all(&mut self, contents: &[u8]) -> Result<(), SidecarError> {
2390        match self {
2391            Self::Client(stream) => {
2392                stream.write_all(contents).map_err(sidecar_net_error)?;
2393                stream.flush().map_err(sidecar_net_error)
2394            }
2395            Self::Server(stream) => {
2396                stream.write_all(contents).map_err(sidecar_net_error)?;
2397                stream.flush().map_err(sidecar_net_error)
2398            }
2399            Self::LoopbackClient(stream) => {
2400                stream.write_all(contents).map_err(sidecar_net_error)?;
2401                stream.flush().map_err(sidecar_net_error)
2402            }
2403            Self::LoopbackServer(stream) => {
2404                stream.write_all(contents).map_err(sidecar_net_error)?;
2405                stream.flush().map_err(sidecar_net_error)
2406            }
2407        }
2408    }
2409
2410    fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
2411        match self {
2412            Self::Client(stream) => stream.read(buffer),
2413            Self::Server(stream) => stream.read(buffer),
2414            Self::LoopbackClient(stream) => stream.read(buffer),
2415            Self::LoopbackServer(stream) => stream.read(buffer),
2416        }
2417    }
2418
2419    fn send_close_notify(&mut self) -> Result<(), SidecarError> {
2420        match self {
2421            Self::Client(stream) => {
2422                stream.conn.send_close_notify();
2423                let _ = stream.conn.complete_io(&mut stream.sock);
2424            }
2425            Self::Server(stream) => {
2426                stream.conn.send_close_notify();
2427                let _ = stream.conn.complete_io(&mut stream.sock);
2428            }
2429            Self::LoopbackClient(stream) => {
2430                stream.conn.send_close_notify();
2431                let _ = stream.conn.complete_io(&mut stream.sock);
2432            }
2433            Self::LoopbackServer(stream) => {
2434                stream.conn.send_close_notify();
2435                let _ = stream.conn.complete_io(&mut stream.sock);
2436            }
2437        }
2438        Ok(())
2439    }
2440
2441    fn shutdown_write(&mut self) -> Result<(), SidecarError> {
2442        match self {
2443            Self::Client(stream) => stream
2444                .sock
2445                .shutdown(Shutdown::Write)
2446                .map_err(sidecar_net_error),
2447            Self::Server(stream) => stream
2448                .sock
2449                .shutdown(Shutdown::Write)
2450                .map_err(sidecar_net_error),
2451            Self::LoopbackClient(stream) => stream.sock.shutdown_write(),
2452            Self::LoopbackServer(stream) => stream.sock.shutdown_write(),
2453        }
2454    }
2455
2456    fn close(&mut self) -> Result<(), SidecarError> {
2457        match self {
2458            Self::Client(stream) => stream
2459                .sock
2460                .shutdown(Shutdown::Both)
2461                .map_err(sidecar_net_error),
2462            Self::Server(stream) => stream
2463                .sock
2464                .shutdown(Shutdown::Both)
2465                .map_err(sidecar_net_error),
2466            Self::LoopbackClient(stream) => stream.sock.close_endpoint(),
2467            Self::LoopbackServer(stream) => stream.sock.close_endpoint(),
2468        }
2469    }
2470
2471    fn peer_certificates(&self) -> Option<&[CertificateDer<'static>]> {
2472        match self {
2473            Self::Client(stream) => stream.conn.peer_certificates(),
2474            Self::Server(stream) => stream.conn.peer_certificates(),
2475            Self::LoopbackClient(stream) => stream.conn.peer_certificates(),
2476            Self::LoopbackServer(stream) => stream.conn.peer_certificates(),
2477        }
2478    }
2479
2480    fn negotiated_cipher_suite(&self) -> Option<rustls::SupportedCipherSuite> {
2481        match self {
2482            Self::Client(stream) => stream.conn.negotiated_cipher_suite(),
2483            Self::Server(stream) => stream.conn.negotiated_cipher_suite(),
2484            Self::LoopbackClient(stream) => stream.conn.negotiated_cipher_suite(),
2485            Self::LoopbackServer(stream) => stream.conn.negotiated_cipher_suite(),
2486        }
2487    }
2488
2489    fn protocol_version(&self) -> Option<rustls::ProtocolVersion> {
2490        match self {
2491            Self::Client(stream) => stream.conn.protocol_version(),
2492            Self::Server(stream) => stream.conn.protocol_version(),
2493            Self::LoopbackClient(stream) => stream.conn.protocol_version(),
2494            Self::LoopbackServer(stream) => stream.conn.protocol_version(),
2495        }
2496    }
2497}
2498
2499// ActiveTcpListener moved to crate::state
2500
2501// Unix socket types moved to crate::state
2502
2503impl ActiveUnixSocket {
2504    fn connect(host_path: &Path, guest_path: &str) -> Result<Self, SidecarError> {
2505        let stream = UnixStream::connect(host_path).map_err(sidecar_net_error)?;
2506        Self::from_stream(stream, None, None, Some(guest_path.to_owned()))
2507    }
2508
2509    fn from_stream(
2510        stream: UnixStream,
2511        listener_id: Option<String>,
2512        local_path: Option<String>,
2513        remote_path: Option<String>,
2514    ) -> Result<Self, SidecarError> {
2515        let read_stream = stream.try_clone().map_err(sidecar_net_error)?;
2516        let stream = Arc::new(Mutex::new(stream));
2517        let (sender, events) = mpsc::channel();
2518        let event_pusher = Arc::new(Mutex::new(None));
2519        let saw_local_shutdown = Arc::new(AtomicBool::new(false));
2520        let saw_remote_end = Arc::new(AtomicBool::new(false));
2521        let close_notified = Arc::new(AtomicBool::new(false));
2522        spawn_unix_socket_reader(
2523            read_stream,
2524            sender.clone(),
2525            Arc::clone(&event_pusher),
2526            Arc::clone(&saw_local_shutdown),
2527            Arc::clone(&saw_remote_end),
2528            Arc::clone(&close_notified),
2529        );
2530
2531        Ok(Self {
2532            stream,
2533            events,
2534            event_sender: sender,
2535            event_pusher,
2536            listener_id,
2537            local_path,
2538            remote_path,
2539            saw_local_shutdown,
2540            saw_remote_end,
2541            close_notified,
2542        })
2543    }
2544
2545    fn set_event_pusher(&self, session: Option<V8SessionHandle>, socket_id: String) {
2546        let Some(session) = session else {
2547            return;
2548        };
2549        if let Ok(mut pusher) = self.event_pusher.lock() {
2550            *pusher = Some(JavascriptSocketEventPusher { session, socket_id });
2551        }
2552    }
2553
2554    fn poll(&mut self, wait: Duration) -> Result<Option<JavascriptTcpSocketEvent>, SidecarError> {
2555        match self.events.recv_timeout(wait) {
2556            Ok(event) => Ok(Some(event)),
2557            Err(RecvTimeoutError::Timeout) => Ok(None),
2558            Err(RecvTimeoutError::Disconnected) => Ok(None),
2559        }
2560    }
2561
2562    fn socket_info(&self) -> Value {
2563        unix_socket_info_value(self.local_path.as_deref(), self.remote_path.as_deref())
2564    }
2565
2566    fn write_all(&self, contents: &[u8]) -> Result<usize, SidecarError> {
2567        let mut stream = self
2568            .stream
2569            .lock()
2570            .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
2571        stream.write_all(contents).map_err(sidecar_net_error)?;
2572        Ok(contents.len())
2573    }
2574
2575    fn shutdown_write(&self) -> Result<(), SidecarError> {
2576        let stream = self
2577            .stream
2578            .lock()
2579            .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
2580        self.saw_local_shutdown.store(true, Ordering::SeqCst);
2581        stream
2582            .shutdown(Shutdown::Write)
2583            .map_err(sidecar_net_error)?;
2584        if self.saw_remote_end.load(Ordering::SeqCst)
2585            && !self.close_notified.swap(true, Ordering::SeqCst)
2586        {
2587            let _ = self
2588                .event_sender
2589                .send(JavascriptTcpSocketEvent::Close { had_error: false });
2590        }
2591        Ok(())
2592    }
2593
2594    fn close(&self) -> Result<(), SidecarError> {
2595        let stream = self
2596            .stream
2597            .lock()
2598            .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
2599        stream.shutdown(Shutdown::Both).map_err(sidecar_net_error)
2600    }
2601}
2602
2603// ActiveUnixListener moved to crate::state
2604
2605impl ActiveUnixListener {
2606    fn bind(
2607        host_path: &Path,
2608        guest_path: &str,
2609        backlog: Option<u32>,
2610    ) -> Result<Self, SidecarError> {
2611        if let Some(parent) = host_path.parent() {
2612            fs::create_dir_all(parent).map_err(sidecar_net_error)?;
2613        }
2614        let listener = UnixListener::bind(host_path).map_err(sidecar_net_error)?;
2615        listener.set_nonblocking(true).map_err(sidecar_net_error)?;
2616        Ok(Self {
2617            listener,
2618            path: guest_path.to_owned(),
2619            backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
2620                .expect("default backlog fits within usize"),
2621            active_connection_ids: BTreeSet::new(),
2622        })
2623    }
2624
2625    fn path(&self) -> &str {
2626        &self.path
2627    }
2628
2629    fn poll(
2630        &mut self,
2631        wait: Duration,
2632    ) -> Result<Option<JavascriptUnixListenerEvent>, SidecarError> {
2633        let deadline = Instant::now() + wait;
2634        loop {
2635            match self.listener.accept() {
2636                Ok((stream, remote_addr)) => {
2637                    if self.active_connection_ids.len() >= self.backlog {
2638                        let _ = stream.shutdown(Shutdown::Both);
2639                        if wait.is_zero() || Instant::now() >= deadline {
2640                            return Ok(None);
2641                        }
2642                        continue;
2643                    }
2644
2645                    let local_path = Some(self.path.clone());
2646                    let remote_path = unix_socket_path(&remote_addr);
2647                    return Ok(Some(JavascriptUnixListenerEvent::Connection(
2648                        PendingUnixSocket {
2649                            stream,
2650                            local_path,
2651                            remote_path,
2652                        },
2653                    )));
2654                }
2655                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
2656                    if wait.is_zero() || Instant::now() >= deadline {
2657                        return Ok(None);
2658                    }
2659                    if !wait_fd_readable_until(self.listener.as_fd(), deadline) {
2660                        return Ok(None);
2661                    }
2662                }
2663                Err(error) => {
2664                    return Ok(Some(JavascriptUnixListenerEvent::Error {
2665                        code: io_error_code(&error),
2666                        message: error.to_string(),
2667                    }));
2668                }
2669            }
2670        }
2671    }
2672
2673    fn close(&self) -> Result<(), SidecarError> {
2674        Ok(())
2675    }
2676
2677    fn active_connection_count(&self) -> usize {
2678        self.active_connection_ids.len()
2679    }
2680
2681    fn register_connection(&mut self, socket_id: &str) {
2682        self.active_connection_ids.insert(socket_id.to_string());
2683    }
2684
2685    fn release_connection(&mut self, socket_id: &str) {
2686        self.active_connection_ids.remove(socket_id);
2687    }
2688}
2689
2690impl ActiveTcpListener {
2691    fn bind(
2692        bind_host: &str,
2693        guest_host: &str,
2694        guest_port: u16,
2695        backlog: Option<u32>,
2696    ) -> Result<Self, SidecarError> {
2697        let bind_addr = resolve_tcp_bind_addr(bind_host, 0)?;
2698        let guest_addr = resolve_tcp_bind_addr(guest_host, guest_port)?;
2699        let listener = TcpListener::bind(bind_addr).map_err(sidecar_net_error)?;
2700        listener.set_nonblocking(true).map_err(sidecar_net_error)?;
2701        let local_addr = listener.local_addr().map_err(sidecar_net_error)?;
2702        Ok(Self {
2703            listener: Some(listener),
2704            kernel_socket_id: None,
2705            local_addr: Some(local_addr),
2706            guest_local_addr: guest_addr,
2707            backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
2708                .expect("default backlog fits within usize"),
2709            active_connection_ids: BTreeSet::new(),
2710        })
2711    }
2712
2713    fn bind_kernel(
2714        kernel: &mut SidecarKernel,
2715        kernel_pid: u32,
2716        guest_host: &str,
2717        guest_port: u16,
2718        backlog: Option<u32>,
2719    ) -> Result<Self, SidecarError> {
2720        let guest_addr = resolve_tcp_bind_addr(guest_host, guest_port)?;
2721        let spec = match guest_addr {
2722            SocketAddr::V4(_) => SocketSpec::tcp(),
2723            SocketAddr::V6(_) => SocketSpec::new(SocketDomain::Inet6, SocketType::Stream),
2724        };
2725        let socket_id = kernel
2726            .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
2727            .map_err(kernel_error)?;
2728        kernel
2729            .socket_bind_inet(
2730                EXECUTION_DRIVER_NAME,
2731                kernel_pid,
2732                socket_id,
2733                InetSocketAddress::new(guest_addr.ip().to_string(), guest_addr.port()),
2734            )
2735            .map_err(kernel_error)?;
2736        kernel
2737            .socket_listen(
2738                EXECUTION_DRIVER_NAME,
2739                kernel_pid,
2740                socket_id,
2741                usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
2742                    .expect("default backlog fits within usize"),
2743            )
2744            .map_err(kernel_error)?;
2745        Ok(Self {
2746            listener: None,
2747            kernel_socket_id: Some(socket_id),
2748            local_addr: Some(guest_addr),
2749            guest_local_addr: guest_addr,
2750            backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
2751                .expect("default backlog fits within usize"),
2752            active_connection_ids: BTreeSet::new(),
2753        })
2754    }
2755
2756    pub(crate) fn local_addr(&self) -> SocketAddr {
2757        self.local_addr.unwrap_or(self.guest_local_addr)
2758    }
2759
2760    fn guest_local_addr(&self) -> SocketAddr {
2761        self.guest_local_addr
2762    }
2763
2764    fn poll(
2765        &mut self,
2766        kernel: &mut SidecarKernel,
2767        kernel_pid: u32,
2768        wait: Duration,
2769        trace_enabled: bool,
2770    ) -> Result<Option<JavascriptTcpListenerEvent>, SidecarError> {
2771        if let Some(socket_id) = self.kernel_socket_id {
2772            let poll_started = Instant::now();
2773            let result = kernel
2774                .poll_targets(
2775                    EXECUTION_DRIVER_NAME,
2776                    kernel_pid,
2777                    vec![PollTargetEntry::socket(socket_id, POLLIN)],
2778                    i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
2779                )
2780                .map_err(kernel_error)?;
2781            let poll_elapsed = poll_started.elapsed();
2782            let revents = result
2783                .targets
2784                .first()
2785                .map(|entry| entry.revents)
2786                .unwrap_or_else(PollEvents::empty);
2787            record_net_tcp_kernel_poll(trace_enabled, wait, poll_elapsed, revents);
2788            if revents.is_empty() {
2789                return Ok(None);
2790            }
2791            let accepted_socket_id =
2792                match kernel.socket_accept(EXECUTION_DRIVER_NAME, kernel_pid, socket_id) {
2793                    Ok(accepted_socket_id) => accepted_socket_id,
2794                    Err(error) if error.code() == "EAGAIN" => {
2795                        if trace_enabled {
2796                            NET_TCP_TRACE_COUNTERS
2797                                .server_accept_eagain
2798                                .fetch_add(1, Ordering::Relaxed);
2799                        }
2800                        return Ok(None);
2801                    }
2802                    Err(error) => {
2803                        if trace_enabled {
2804                            NET_TCP_TRACE_COUNTERS
2805                                .server_accept_errors
2806                                .fetch_add(1, Ordering::Relaxed);
2807                        }
2808                        return Ok(Some(JavascriptTcpListenerEvent::Error {
2809                            code: Some(error.code().to_string()),
2810                            message: error.to_string(),
2811                        }));
2812                    }
2813                };
2814            let accepted = kernel.socket_get(accepted_socket_id).ok_or_else(|| {
2815                SidecarError::InvalidState(format!(
2816                    "accepted kernel TCP socket {accepted_socket_id} is missing"
2817                ))
2818            })?;
2819            let local_addr = accepted.local_address().ok_or_else(|| {
2820                SidecarError::InvalidState(format!(
2821                    "accepted kernel TCP socket {accepted_socket_id} missing local address"
2822                ))
2823            })?;
2824            let remote_addr = accepted.peer_address().ok_or_else(|| {
2825                SidecarError::InvalidState(format!(
2826                    "accepted kernel TCP socket {accepted_socket_id} missing peer address"
2827                ))
2828            })?;
2829            if trace_enabled {
2830                NET_TCP_TRACE_COUNTERS
2831                    .server_accept_connections
2832                    .fetch_add(1, Ordering::Relaxed);
2833            }
2834            return Ok(Some(JavascriptTcpListenerEvent::Connection(
2835                PendingTcpSocket {
2836                    stream: None,
2837                    kernel_socket_id: Some(accepted_socket_id),
2838                    preallocated: true,
2839                    guest_local_addr: resolve_tcp_bind_addr(local_addr.host(), local_addr.port())?,
2840                    guest_remote_addr: resolve_tcp_bind_addr(
2841                        remote_addr.host(),
2842                        remote_addr.port(),
2843                    )?,
2844                },
2845            )));
2846        }
2847
2848        let deadline = Instant::now() + wait;
2849        loop {
2850            match self
2851                .listener
2852                .as_ref()
2853                .ok_or_else(|| {
2854                    SidecarError::InvalidState(String::from("TCP listener socket missing"))
2855                })?
2856                .accept()
2857            {
2858                Ok((stream, remote_addr)) => {
2859                    if self.active_connection_ids.len() >= self.backlog {
2860                        let _ = stream.shutdown(Shutdown::Both);
2861                        if wait.is_zero() || Instant::now() >= deadline {
2862                            return Ok(None);
2863                        }
2864                        continue;
2865                    }
2866                    return Ok(Some(JavascriptTcpListenerEvent::Connection(
2867                        PendingTcpSocket {
2868                            stream: Some(stream),
2869                            kernel_socket_id: None,
2870                            preallocated: false,
2871                            guest_local_addr: self.guest_local_addr,
2872                            guest_remote_addr: SocketAddr::new(
2873                                remote_addr.ip(),
2874                                remote_addr.port(),
2875                            ),
2876                        },
2877                    )));
2878                }
2879                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
2880                    if wait.is_zero() || Instant::now() >= deadline {
2881                        return Ok(None);
2882                    }
2883                    if !wait_fd_readable_until(
2884                        self.listener
2885                            .as_ref()
2886                            .expect("TCP listener checked before accept")
2887                            .as_fd(),
2888                        deadline,
2889                    ) {
2890                        return Ok(None);
2891                    }
2892                }
2893                Err(error) => {
2894                    return Ok(Some(JavascriptTcpListenerEvent::Error {
2895                        code: io_error_code(&error),
2896                        message: error.to_string(),
2897                    }));
2898                }
2899            }
2900        }
2901    }
2902
2903    fn close(&self, kernel: &mut SidecarKernel, kernel_pid: u32) -> Result<(), SidecarError> {
2904        if let Some(socket_id) = self.kernel_socket_id {
2905            close_kernel_socket_idempotent(kernel, kernel_pid, socket_id)?;
2906        }
2907        Ok(())
2908    }
2909
2910    fn active_connection_count(&self) -> usize {
2911        self.active_connection_ids.len()
2912    }
2913
2914    fn register_connection(&mut self, socket_id: &str) {
2915        self.active_connection_ids.insert(socket_id.to_string());
2916    }
2917
2918    fn release_connection(&mut self, socket_id: &str) {
2919        self.active_connection_ids.remove(socket_id);
2920    }
2921}
2922
2923// UDP types moved to crate::state
2924
2925impl ActiveUdpSocket {
2926    fn new(
2927        kernel: &mut SidecarKernel,
2928        kernel_pid: u32,
2929        family: JavascriptUdpFamily,
2930    ) -> Result<Self, SidecarError> {
2931        let spec = match family {
2932            JavascriptUdpFamily::Ipv4 => SocketSpec::udp(),
2933            JavascriptUdpFamily::Ipv6 => SocketSpec::new(SocketDomain::Inet6, SocketType::Datagram),
2934        };
2935        let socket_id = kernel
2936            .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
2937            .map_err(kernel_error)?;
2938        Ok(Self {
2939            family,
2940            socket: None,
2941            kernel_socket_id: Some(socket_id),
2942            guest_local_addr: None,
2943            recv_buffer_size: 0,
2944            send_buffer_size: 0,
2945        })
2946    }
2947
2948    fn local_addr(&self) -> Option<SocketAddr> {
2949        self.guest_local_addr
2950    }
2951
2952    fn socket(&self) -> Result<&UdpSocket, SidecarError> {
2953        self.socket
2954            .as_ref()
2955            .ok_or_else(|| SidecarError::Execution(String::from("EBADF: bad file descriptor")))
2956    }
2957
2958    fn bind(
2959        &mut self,
2960        kernel: &mut SidecarKernel,
2961        kernel_pid: u32,
2962        host: Option<&str>,
2963        port: u16,
2964        context: &JavascriptSocketPathContext,
2965    ) -> Result<SocketAddr, SidecarError> {
2966        if self.socket.is_some() || self.guest_local_addr.is_some() {
2967            return Err(SidecarError::Execution(String::from(
2968                "EINVAL: secure-exec dgram socket is already bound",
2969            )));
2970        }
2971
2972        let (bind_host, guest_host, guest_family) = normalize_udp_bind_host(host, self.family)?;
2973        let guest_port = allocate_guest_listen_port(
2974            port,
2975            guest_family,
2976            &context.used_udp_guest_ports,
2977            context.listen_policy,
2978        )?;
2979        let local_addr = resolve_udp_bind_addr(guest_host, guest_port, self.family)?;
2980        if let Some(socket_id) = self.kernel_socket_id {
2981            kernel
2982                .socket_bind_inet(
2983                    EXECUTION_DRIVER_NAME,
2984                    kernel_pid,
2985                    socket_id,
2986                    InetSocketAddress::new(local_addr.ip().to_string(), local_addr.port()),
2987                )
2988                .map_err(kernel_error)?;
2989        } else {
2990            let bind_addr = resolve_udp_bind_addr(bind_host, 0, self.family)?;
2991            let socket = UdpSocket::bind(bind_addr).map_err(sidecar_net_error)?;
2992            socket.set_nonblocking(true).map_err(sidecar_net_error)?;
2993            self.socket = Some(socket);
2994        }
2995        self.guest_local_addr = Some(local_addr);
2996        Ok(local_addr)
2997    }
2998
2999    fn ensure_bound_for_send(
3000        &mut self,
3001        kernel: &mut SidecarKernel,
3002        kernel_pid: u32,
3003        context: &JavascriptSocketPathContext,
3004    ) -> Result<SocketAddr, SidecarError> {
3005        if let Some(local_addr) = self.local_addr() {
3006            return Ok(local_addr);
3007        }
3008
3009        self.bind(kernel, kernel_pid, None, 0, context)
3010    }
3011
3012    fn send_to<B>(
3013        &mut self,
3014        request: ActiveUdpSendToRequest<'_, B>,
3015    ) -> Result<(usize, SocketAddr), SidecarError>
3016    where
3017        B: NativeSidecarBridge + Send + 'static,
3018        BridgeError<B>: fmt::Debug + Send + Sync + 'static,
3019    {
3020        let ActiveUdpSendToRequest {
3021            bridge,
3022            kernel,
3023            kernel_pid,
3024            vm_id,
3025            dns,
3026            host,
3027            port,
3028            context,
3029            contents,
3030        } = request;
3031        let remote_addr = resolve_udp_addr(UdpRemoteAddrRequest {
3032            bridge,
3033            kernel,
3034            vm_id,
3035            dns,
3036            host,
3037            port,
3038            family: self.family,
3039            context,
3040        })?;
3041        let local_addr = self.ensure_bound_for_send(kernel, kernel_pid, context)?;
3042        let written = if let Some(socket_id) = self.kernel_socket_id {
3043            if is_loopback_ip(remote_addr.ip()) && remote_addr.port() == port {
3044                kernel
3045                    .socket_send_to_inet_loopback(
3046                        EXECUTION_DRIVER_NAME,
3047                        kernel_pid,
3048                        socket_id,
3049                        InetSocketAddress::new(remote_addr.ip().to_string(), remote_addr.port()),
3050                        contents,
3051                    )
3052                    .map_err(kernel_error)?
3053            } else {
3054                return Err(SidecarError::Execution(String::from(
3055                    "ERR_NOT_IMPLEMENTED: external UDP datagrams are not yet supported by the kernel-backed V8 bridge",
3056                )));
3057            }
3058        } else {
3059            let socket = self.socket.as_ref().ok_or_else(|| {
3060                SidecarError::InvalidState(String::from("UDP socket is not initialized"))
3061            })?;
3062            socket
3063                .send_to(contents, remote_addr)
3064                .map_err(sidecar_net_error)?
3065        };
3066        Ok((written, local_addr))
3067    }
3068
3069    fn poll(
3070        &self,
3071        kernel: &mut SidecarKernel,
3072        kernel_pid: u32,
3073        wait: Duration,
3074    ) -> Result<Option<JavascriptUdpSocketEvent>, SidecarError> {
3075        if let Some(socket_id) = self.kernel_socket_id {
3076            let result = kernel
3077                .poll_targets(
3078                    EXECUTION_DRIVER_NAME,
3079                    kernel_pid,
3080                    vec![PollTargetEntry::socket(socket_id, POLLIN)],
3081                    i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
3082                )
3083                .map_err(kernel_error)?;
3084            let revents = result
3085                .targets
3086                .first()
3087                .map(|entry| entry.revents)
3088                .unwrap_or_else(PollEvents::empty);
3089            if revents.is_empty() {
3090                return Ok(None);
3091            }
3092            return match kernel.socket_recv_datagram(
3093                EXECUTION_DRIVER_NAME,
3094                kernel_pid,
3095                socket_id,
3096                64 * 1024,
3097            ) {
3098                Ok(Some(datagram)) => {
3099                    let (source_address, payload) = datagram.into_parts();
3100                    let remote_addr = source_address
3101                        .map(|source| {
3102                            resolve_udp_bind_addr(source.host(), source.port(), self.family)
3103                        })
3104                        .transpose()?
3105                        .unwrap_or_else(|| match self.family {
3106                            JavascriptUdpFamily::Ipv4 => {
3107                                SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)
3108                            }
3109                            JavascriptUdpFamily::Ipv6 => {
3110                                SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0)
3111                            }
3112                        });
3113                    Ok(Some(JavascriptUdpSocketEvent::Message {
3114                        data: payload,
3115                        remote_addr,
3116                    }))
3117                }
3118                Ok(None) => Ok(None),
3119                Err(error) if error.code() == "EAGAIN" => Ok(None),
3120                Err(error) => Ok(Some(JavascriptUdpSocketEvent::Error {
3121                    code: Some(error.code().to_string()),
3122                    message: error.to_string(),
3123                })),
3124            };
3125        }
3126        let socket = self.socket()?;
3127        let deadline = Instant::now() + wait;
3128        let mut buffer = vec![0_u8; 64 * 1024];
3129
3130        loop {
3131            match socket.recv_from(&mut buffer) {
3132                Ok((bytes_read, remote_addr)) => {
3133                    return Ok(Some(JavascriptUdpSocketEvent::Message {
3134                        data: buffer[..bytes_read].to_vec(),
3135                        remote_addr,
3136                    }));
3137                }
3138                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
3139                    if wait.is_zero() || Instant::now() >= deadline {
3140                        return Ok(None);
3141                    }
3142                    if !wait_fd_readable_until(socket.as_fd(), deadline) {
3143                        return Ok(None);
3144                    }
3145                }
3146                Err(error) => {
3147                    return Ok(Some(JavascriptUdpSocketEvent::Error {
3148                        code: io_error_code(&error),
3149                        message: error.to_string(),
3150                    }));
3151                }
3152            }
3153        }
3154    }
3155
3156    fn close(&mut self, kernel: &mut SidecarKernel, kernel_pid: u32) {
3157        if let Some(socket_id) = self.kernel_socket_id {
3158            let _ = close_kernel_socket_idempotent(kernel, kernel_pid, socket_id);
3159        }
3160        self.socket.take();
3161        self.guest_local_addr = None;
3162    }
3163
3164    fn set_buffer_size(&mut self, which: &str, size: usize) -> Result<(), SidecarError> {
3165        match which {
3166            "recv" => self.recv_buffer_size = size,
3167            "send" => self.send_buffer_size = size,
3168            other => {
3169                return Err(SidecarError::InvalidState(format!(
3170                    "unsupported UDP buffer size kind {other}"
3171                )));
3172            }
3173        }
3174        if self.kernel_socket_id.is_some() {
3175            return Ok(());
3176        }
3177        let socket = self.socket()?;
3178        let socket = SockRef::from(socket);
3179        match which {
3180            "recv" => socket.set_recv_buffer_size(size).map_err(sidecar_net_error),
3181            "send" => socket.set_send_buffer_size(size).map_err(sidecar_net_error),
3182            other => Err(SidecarError::InvalidState(format!(
3183                "unsupported UDP buffer size kind {other}"
3184            ))),
3185        }
3186    }
3187
3188    fn get_buffer_size(&self, which: &str) -> Result<usize, SidecarError> {
3189        if self.kernel_socket_id.is_some() {
3190            return Ok(match which {
3191                "recv" => self.recv_buffer_size,
3192                "send" => self.send_buffer_size,
3193                other => {
3194                    return Err(SidecarError::InvalidState(format!(
3195                        "unsupported UDP buffer size kind {other}"
3196                    )));
3197                }
3198            });
3199        }
3200        let socket = self.socket()?;
3201        let socket = SockRef::from(socket);
3202        match which {
3203            "recv" => socket.recv_buffer_size().map_err(sidecar_net_error),
3204            "send" => socket.send_buffer_size().map_err(sidecar_net_error),
3205            other => Err(SidecarError::InvalidState(format!(
3206                "unsupported UDP buffer size kind {other}"
3207            ))),
3208        }
3209    }
3210}
3211
3212// ActiveExecution, ActiveExecutionEvent, SocketQueryKind moved to crate::state
3213
3214impl ActiveExecution {
3215    pub(crate) fn uses_shared_v8_runtime(&self) -> bool {
3216        match self {
3217            Self::Javascript(execution) => execution.uses_shared_v8_runtime(),
3218            Self::Python(execution) => execution.uses_shared_v8_runtime(),
3219            Self::Wasm(execution) => execution.uses_shared_v8_runtime(),
3220            Self::Tool(_) => false,
3221        }
3222    }
3223
3224    pub(crate) fn child_pid(&self) -> u32 {
3225        match self {
3226            Self::Javascript(execution) => execution.child_pid(),
3227            Self::Python(execution) => execution.child_pid(),
3228            Self::Wasm(execution) => execution.child_pid(),
3229            Self::Tool(_) => 0,
3230        }
3231    }
3232
3233    pub(crate) fn write_stdin(&mut self, chunk: &[u8]) -> Result<(), SidecarError> {
3234        match self {
3235            Self::Javascript(execution) => execution
3236                .write_stdin(chunk)
3237                .map_err(|error| SidecarError::Execution(error.to_string())),
3238            Self::Python(execution) => execution
3239                .write_stdin(chunk)
3240                .map_err(|error| SidecarError::Execution(error.to_string())),
3241            // Sidecar wasm always runs with kernel-managed stdio
3242            // (AGENTOS_WASI_STDIO_SYNC_RPC=1): the guest reads fd 0 via
3243            // `__kernel_stdin_read`, so skip the V8 `stdin` stream event —
3244            // it is never consumed and would flood the session's deferred
3245            // message queue while the guest blocks in a sync read.
3246            Self::Wasm(execution) => execution
3247                .write_stdin_kernel_only(chunk)
3248                .map_err(|error| SidecarError::Execution(error.to_string())),
3249            Self::Tool(_) => Ok(()),
3250        }
3251    }
3252
3253    pub(crate) fn close_stdin(&mut self) -> Result<(), SidecarError> {
3254        match self {
3255            Self::Javascript(execution) => execution
3256                .close_stdin()
3257                .map_err(|error| SidecarError::Execution(error.to_string())),
3258            Self::Python(execution) => execution
3259                .close_stdin()
3260                .map_err(|error| SidecarError::Execution(error.to_string())),
3261            Self::Wasm(execution) => execution
3262                .close_stdin()
3263                .map_err(|error| SidecarError::Execution(error.to_string())),
3264            Self::Tool(_) => Ok(()),
3265        }
3266    }
3267
3268    pub(crate) fn respond_python_vfs_rpc_success(
3269        &mut self,
3270        id: u64,
3271        payload: PythonVfsRpcResponsePayload,
3272    ) -> Result<(), SidecarError> {
3273        match self {
3274            Self::Python(execution) => execution
3275                .respond_vfs_rpc_success(id, payload)
3276                .map_err(|error| SidecarError::Execution(error.to_string())),
3277            _ => Err(SidecarError::InvalidState(String::from(
3278                "only Python executions can service Python VFS RPC responses",
3279            ))),
3280        }
3281    }
3282
3283    pub(crate) fn respond_python_vfs_rpc_error(
3284        &mut self,
3285        id: u64,
3286        code: impl Into<String>,
3287        message: impl Into<String>,
3288    ) -> Result<(), SidecarError> {
3289        match self {
3290            Self::Python(execution) => execution
3291                .respond_vfs_rpc_error(id, code, message)
3292                .map_err(|error| SidecarError::Execution(error.to_string())),
3293            _ => Err(SidecarError::InvalidState(String::from(
3294                "only Python executions can service Python VFS RPC responses",
3295            ))),
3296        }
3297    }
3298
3299    pub(crate) fn send_javascript_stream_event(
3300        &self,
3301        event_type: &str,
3302        payload: Value,
3303    ) -> Result<(), SidecarError> {
3304        match self {
3305            Self::Javascript(execution) => execution
3306                .send_stream_event(event_type, payload)
3307                .map_err(|error| SidecarError::Execution(error.to_string())),
3308            Self::Wasm(execution) => execution
3309                .send_stream_event(event_type, payload)
3310                .map_err(|error| SidecarError::Execution(error.to_string())),
3311            _ => Err(SidecarError::InvalidState(String::from(
3312                "only embedded V8 executions can receive JavaScript stream events",
3313            ))),
3314        }
3315    }
3316
3317    pub(crate) fn javascript_v8_session_handle(&self) -> Option<V8SessionHandle> {
3318        match self {
3319            Self::Javascript(execution) => Some(execution.v8_session_handle()),
3320            Self::Wasm(execution) => Some(execution.v8_session_handle()),
3321            _ => None,
3322        }
3323    }
3324
3325    pub(crate) fn terminate(&mut self) -> Result<(), SidecarError> {
3326        match self {
3327            Self::Javascript(execution) => execution
3328                .terminate()
3329                .map_err(|error| SidecarError::Execution(error.to_string())),
3330            Self::Python(execution) => execution
3331                .kill()
3332                .map_err(|error| SidecarError::Execution(error.to_string())),
3333            Self::Wasm(execution) => execution
3334                .terminate()
3335                .map_err(|error| SidecarError::Execution(error.to_string())),
3336            Self::Tool(_) => Ok(()),
3337        }
3338    }
3339
3340    pub(crate) fn respond_javascript_sync_rpc_success(
3341        &mut self,
3342        id: u64,
3343        result: Value,
3344    ) -> Result<(), SidecarError> {
3345        match self {
3346            Self::Javascript(execution) => execution
3347                .respond_sync_rpc_success(id, result)
3348                .map_err(|error| SidecarError::Execution(error.to_string())),
3349            Self::Python(execution) => execution
3350                .respond_javascript_sync_rpc_success(id, result)
3351                .map_err(|error| SidecarError::Execution(error.to_string())),
3352            Self::Wasm(execution) => execution
3353                .respond_sync_rpc_success(id, result)
3354                .map_err(|error| SidecarError::Execution(error.to_string())),
3355            _ => Err(SidecarError::InvalidState(String::from(
3356                "only JavaScript, Python, and WebAssembly executions can service JavaScript sync RPC responses",
3357            ))),
3358        }
3359    }
3360
3361    pub(crate) fn respond_javascript_sync_rpc_raw_success(
3362        &mut self,
3363        id: u64,
3364        payload: Vec<u8>,
3365    ) -> Result<(), SidecarError> {
3366        match self {
3367            Self::Javascript(execution) => execution
3368                .respond_sync_rpc_raw_success(id, payload)
3369                .map_err(|error| SidecarError::Execution(error.to_string())),
3370            Self::Wasm(execution) => execution
3371                .respond_sync_rpc_raw_success(id, payload)
3372                .map_err(|error| SidecarError::Execution(error.to_string())),
3373            _ => Err(SidecarError::InvalidState(String::from(
3374                "only embedded V8 executions can service raw JavaScript sync RPC responses",
3375            ))),
3376        }
3377    }
3378
3379    pub(crate) fn respond_javascript_sync_rpc_response(
3380        &mut self,
3381        id: u64,
3382        response: JavascriptSyncRpcServiceResponse,
3383    ) -> Result<(), SidecarError> {
3384        match response {
3385            JavascriptSyncRpcServiceResponse::Json(result) => {
3386                self.respond_javascript_sync_rpc_success(id, result)
3387            }
3388            JavascriptSyncRpcServiceResponse::Raw(payload) => {
3389                self.respond_javascript_sync_rpc_raw_success(id, payload)
3390            }
3391        }
3392    }
3393
3394    pub(crate) fn respond_javascript_sync_rpc_error(
3395        &mut self,
3396        id: u64,
3397        code: impl Into<String>,
3398        message: impl Into<String>,
3399    ) -> Result<(), SidecarError> {
3400        match self {
3401            Self::Javascript(execution) => execution
3402                .respond_sync_rpc_error(id, code, message)
3403                .map_err(|error| SidecarError::Execution(error.to_string())),
3404            Self::Python(execution) => execution
3405                .respond_javascript_sync_rpc_error(id, code, message)
3406                .map_err(|error| SidecarError::Execution(error.to_string())),
3407            Self::Wasm(execution) => execution
3408                .respond_sync_rpc_error(id, code, message)
3409                .map_err(|error| SidecarError::Execution(error.to_string())),
3410            _ => Err(SidecarError::InvalidState(String::from(
3411                "only JavaScript, Python, and WebAssembly executions can service JavaScript sync RPC responses",
3412            ))),
3413        }
3414    }
3415
3416    pub(crate) async fn poll_event(
3417        &mut self,
3418        timeout: Duration,
3419    ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
3420        match self {
3421            Self::Javascript(execution) => execution
3422                .poll_event(timeout)
3423                .await
3424                .map(|event| {
3425                    event.map(|event| match event {
3426                        JavascriptExecutionEvent::Stdout(chunk) => {
3427                            ActiveExecutionEvent::Stdout(chunk)
3428                        }
3429                        JavascriptExecutionEvent::Stderr(chunk) => {
3430                            ActiveExecutionEvent::Stderr(chunk)
3431                        }
3432                        JavascriptExecutionEvent::SyncRpcRequest(request) => {
3433                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
3434                        }
3435                        JavascriptExecutionEvent::SignalState {
3436                            signal,
3437                            registration,
3438                        } => ActiveExecutionEvent::SignalState {
3439                            signal,
3440                            registration: map_node_signal_registration(registration),
3441                        },
3442                        JavascriptExecutionEvent::Exited(code) => {
3443                            ActiveExecutionEvent::Exited(code)
3444                        }
3445                    })
3446                })
3447                .map_err(|error| SidecarError::Execution(error.to_string())),
3448            Self::Python(execution) => execution
3449                .poll_event(timeout)
3450                .await
3451                .map(|event| {
3452                    event.map(|event| match event {
3453                        PythonExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
3454                        PythonExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
3455                        PythonExecutionEvent::JavascriptSyncRpcRequest(request) => {
3456                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
3457                        }
3458                        PythonExecutionEvent::VfsRpcRequest(request) => {
3459                            ActiveExecutionEvent::PythonVfsRpcRequest(request)
3460                        }
3461                        PythonExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
3462                    })
3463                })
3464                .map_err(|error| SidecarError::Execution(error.to_string())),
3465            Self::Wasm(execution) => execution
3466                .poll_event(timeout)
3467                .await
3468                .map(|event| {
3469                    event.map(|event| match event {
3470                        WasmExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
3471                        WasmExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
3472                        WasmExecutionEvent::SyncRpcRequest(request) => {
3473                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
3474                        }
3475                        WasmExecutionEvent::SignalState {
3476                            signal,
3477                            registration,
3478                        } => ActiveExecutionEvent::SignalState {
3479                            signal,
3480                            registration: map_wasm_signal_registration(registration),
3481                        },
3482                        WasmExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
3483                    })
3484                })
3485                .map_err(|error| SidecarError::Execution(error.to_string())),
3486            Self::Tool(execution) => {
3487                let _ = timeout;
3488                poll_tool_process_event(execution)
3489            }
3490        }
3491    }
3492
3493    pub(crate) fn poll_event_blocking(
3494        &mut self,
3495        timeout: Duration,
3496    ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
3497        match self {
3498            Self::Javascript(execution) => execution
3499                .poll_event_blocking(timeout)
3500                .map(|event| {
3501                    event.map(|event| match event {
3502                        JavascriptExecutionEvent::Stdout(chunk) => {
3503                            ActiveExecutionEvent::Stdout(chunk)
3504                        }
3505                        JavascriptExecutionEvent::Stderr(chunk) => {
3506                            ActiveExecutionEvent::Stderr(chunk)
3507                        }
3508                        JavascriptExecutionEvent::SyncRpcRequest(request) => {
3509                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
3510                        }
3511                        JavascriptExecutionEvent::SignalState {
3512                            signal,
3513                            registration,
3514                        } => ActiveExecutionEvent::SignalState {
3515                            signal,
3516                            registration: map_node_signal_registration(registration),
3517                        },
3518                        JavascriptExecutionEvent::Exited(code) => {
3519                            ActiveExecutionEvent::Exited(code)
3520                        }
3521                    })
3522                })
3523                .map_err(|error| SidecarError::Execution(error.to_string())),
3524            Self::Python(execution) => execution
3525                .poll_event_blocking(timeout)
3526                .map(|event| {
3527                    event.map(|event| match event {
3528                        PythonExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
3529                        PythonExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
3530                        PythonExecutionEvent::JavascriptSyncRpcRequest(request) => {
3531                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
3532                        }
3533                        PythonExecutionEvent::VfsRpcRequest(request) => {
3534                            ActiveExecutionEvent::PythonVfsRpcRequest(request)
3535                        }
3536                        PythonExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
3537                    })
3538                })
3539                .map_err(|error| SidecarError::Execution(error.to_string())),
3540            Self::Wasm(execution) => execution
3541                .poll_event_blocking(timeout)
3542                .map(|event| {
3543                    event.map(|event| match event {
3544                        WasmExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
3545                        WasmExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
3546                        WasmExecutionEvent::SyncRpcRequest(request) => {
3547                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
3548                        }
3549                        WasmExecutionEvent::SignalState {
3550                            signal,
3551                            registration,
3552                        } => ActiveExecutionEvent::SignalState {
3553                            signal,
3554                            registration: map_wasm_signal_registration(registration),
3555                        },
3556                        WasmExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
3557                    })
3558                })
3559                .map_err(|error| SidecarError::Execution(error.to_string())),
3560            Self::Tool(execution) => {
3561                let _ = timeout;
3562                poll_tool_process_event(execution)
3563            }
3564        }
3565    }
3566}
3567
3568struct ToolProcessEventRequest {
3569    sidecar_requests: SharedSidecarRequestClient,
3570    connection_id: String,
3571    session_id: String,
3572    vm_id: String,
3573    tool_resolution: ToolCommandResolution,
3574    cancelled: Arc<AtomicBool>,
3575    pending_events: Arc<Mutex<VecDeque<ActiveExecutionEvent>>>,
3576    events_overflowed: Arc<AtomicBool>,
3577}
3578
3579pub(crate) fn send_tool_process_event(
3580    pending_events: &Arc<Mutex<VecDeque<ActiveExecutionEvent>>>,
3581    events_overflowed: &AtomicBool,
3582    event: ActiveExecutionEvent,
3583) -> bool {
3584    let mut pending_events = pending_events
3585        .lock()
3586        .unwrap_or_else(|poisoned| poisoned.into_inner());
3587    if pending_events.len() >= MAX_PROCESS_EVENT_QUEUE {
3588        events_overflowed.store(true, Ordering::Relaxed);
3589        return false;
3590    }
3591    pending_events.push_back(event);
3592    true
3593}
3594
3595fn spawn_tool_process_events(request: ToolProcessEventRequest) {
3596    let ToolProcessEventRequest {
3597        sidecar_requests,
3598        connection_id,
3599        session_id,
3600        vm_id,
3601        tool_resolution,
3602        cancelled,
3603        pending_events,
3604        events_overflowed,
3605    } = request;
3606    std::thread::spawn(move || match tool_resolution {
3607        ToolCommandResolution::Failure(message) => {
3608            if !send_tool_process_event(
3609                &pending_events,
3610                &events_overflowed,
3611                ActiveExecutionEvent::Stderr(format_tool_failure_output(&message)),
3612            ) {
3613                return;
3614            }
3615            let _ = send_tool_process_event(
3616                &pending_events,
3617                &events_overflowed,
3618                ActiveExecutionEvent::Exited(1),
3619            );
3620        }
3621        ToolCommandResolution::Invoke { request, timeout } => {
3622            let response = sidecar_requests.invoke(
3623                OwnershipScope::vm(connection_id.clone(), session_id.clone(), vm_id.clone()),
3624                SidecarRequestPayload::HostCallback(request.clone()),
3625                timeout,
3626            );
3627            if cancelled.load(Ordering::Relaxed) {
3628                return;
3629            }
3630
3631            match response {
3632                Ok(crate::protocol::SidecarResponsePayload::HostCallbackResult(result)) => {
3633                    if let Some(value) = result.result {
3634                        let value: serde_json::Value = serde_json::from_str(&value)
3635                            .unwrap_or(serde_json::Value::String(value));
3636                        let stdout = serde_json::to_vec(&json!({
3637                            "ok": true,
3638                            "result": value,
3639                        }))
3640                        .unwrap_or_else(|error| {
3641                            format_tool_failure_output(&format!(
3642                                "failed to serialize tool result: {error}"
3643                            ))
3644                        });
3645                        if !send_tool_process_event(
3646                            &pending_events,
3647                            &events_overflowed,
3648                            ActiveExecutionEvent::Stdout(stdout),
3649                        ) {
3650                            return;
3651                        }
3652                        let _ = send_tool_process_event(
3653                            &pending_events,
3654                            &events_overflowed,
3655                            ActiveExecutionEvent::Exited(0),
3656                        );
3657                    } else {
3658                        let message = result
3659                            .error
3660                            .unwrap_or_else(|| String::from("tool invocation returned no result"));
3661                        if !send_tool_process_event(
3662                            &pending_events,
3663                            &events_overflowed,
3664                            ActiveExecutionEvent::Stderr(format_tool_failure_output(&message)),
3665                        ) {
3666                            return;
3667                        }
3668                        let _ = send_tool_process_event(
3669                            &pending_events,
3670                            &events_overflowed,
3671                            ActiveExecutionEvent::Exited(1),
3672                        );
3673                    }
3674                }
3675                Ok(_) => {
3676                    if !send_tool_process_event(
3677                        &pending_events,
3678                        &events_overflowed,
3679                        ActiveExecutionEvent::Stderr(format_tool_failure_output(
3680                            "unexpected sidecar tool response",
3681                        )),
3682                    ) {
3683                        return;
3684                    }
3685                    let _ = send_tool_process_event(
3686                        &pending_events,
3687                        &events_overflowed,
3688                        ActiveExecutionEvent::Exited(1),
3689                    );
3690                }
3691                Err(error) => {
3692                    if !send_tool_process_event(
3693                        &pending_events,
3694                        &events_overflowed,
3695                        ActiveExecutionEvent::Stderr(format_tool_failure_output(
3696                            &error.to_string(),
3697                        )),
3698                    ) {
3699                        return;
3700                    }
3701                    let _ = send_tool_process_event(
3702                        &pending_events,
3703                        &events_overflowed,
3704                        ActiveExecutionEvent::Exited(1),
3705                    );
3706                }
3707            }
3708        }
3709    });
3710}
3711
3712impl<B> NativeSidecar<B>
3713where
3714    B: NativeSidecarBridge + Send + 'static,
3715    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
3716{
3717    pub(crate) async fn execute(
3718        &mut self,
3719        request: &RequestFrame,
3720        payload: ExecuteRequest,
3721    ) -> Result<DispatchResult, SidecarError> {
3722        let execute_total_start = Instant::now();
3723        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3724        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3725
3726        let vm = self
3727            .vms
3728            .get_mut(&vm_id)
3729            .ok_or_else(|| missing_vm_error(&vm_id))?;
3730        if vm.active_processes.contains_key(&payload.process_id) {
3731            return Err(SidecarError::InvalidState(format!(
3732                "VM {vm_id} already has an active process with id {}",
3733                payload.process_id
3734            )));
3735        }
3736
3737        if let Some(command) = payload.command.as_deref() {
3738            if let Some(tool_resolution) =
3739                resolve_tool_command(vm, command, &payload.args, payload.cwd.as_deref())?
3740            {
3741                let guest_cwd = payload
3742                    .cwd
3743                    .as_deref()
3744                    .map(normalize_path)
3745                    .unwrap_or_else(|| vm.guest_cwd.clone());
3746                let kernel_handle = vm
3747                    .kernel
3748                    .create_virtual_process(
3749                        EXECUTION_DRIVER_NAME,
3750                        TOOL_DRIVER_NAME,
3751                        command,
3752                        std::iter::once(command.to_owned())
3753                            .chain(payload.args.iter().cloned())
3754                            .collect(),
3755                        VirtualProcessOptions {
3756                            env: vm.guest_env.clone(),
3757                            cwd: Some(guest_cwd.clone()),
3758                            ..VirtualProcessOptions::default()
3759                        },
3760                    )
3761                    .map_err(kernel_error)?;
3762                let kernel_pid = kernel_handle.pid();
3763                let tool_execution = ToolExecution::default();
3764                let cancelled = tool_execution.cancelled.clone();
3765                let pending_events = tool_execution.pending_events.clone();
3766                let events_overflowed = tool_execution.events_overflowed.clone();
3767                vm.active_processes.insert(
3768                    payload.process_id.clone(),
3769                    ActiveProcess::new(
3770                        kernel_pid,
3771                        kernel_handle,
3772                        GuestRuntimeKind::JavaScript,
3773                        ActiveExecution::Tool(tool_execution),
3774                    )
3775                    .with_guest_cwd(guest_cwd.clone())
3776                    .with_host_cwd(resolve_vm_guest_path_to_host(vm, &guest_cwd)),
3777                );
3778                self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy)?;
3779                spawn_tool_process_events(ToolProcessEventRequest {
3780                    sidecar_requests: self.sidecar_requests.clone(),
3781                    connection_id: connection_id.clone(),
3782                    session_id: session_id.clone(),
3783                    vm_id: vm_id.clone(),
3784                    tool_resolution,
3785                    cancelled,
3786                    pending_events,
3787                    events_overflowed,
3788                });
3789
3790                return Ok(DispatchResult {
3791                    response: process_started_response(
3792                        request,
3793                        payload.process_id,
3794                        Some(kernel_pid),
3795                    ),
3796                    events: Vec::new(),
3797                });
3798            }
3799        }
3800
3801        let requested_tty = payload
3802            .env
3803            .get(EXECUTION_REQUEST_TTY_ENV)
3804            .is_some_and(|value| value == "1" || value.eq_ignore_ascii_case("true"));
3805        let phase_start = Instant::now();
3806        let resolved = resolve_execute_request(vm, &payload)?;
3807        record_execute_phase("resolve_execute_request", phase_start.elapsed());
3808        let phase_start = Instant::now();
3809        let mut env = resolved.env.clone();
3810        env.remove(EXECUTION_REQUEST_TTY_ENV);
3811        let sandbox_root = normalize_host_path(&vm.cwd);
3812        env.insert(
3813            String::from(EXECUTION_SANDBOX_ROOT_ENV),
3814            sandbox_root.to_string_lossy().into_owned(),
3815        );
3816        if resolved.runtime == GuestRuntimeKind::JavaScript {
3817            env.insert(
3818                String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
3819                String::from("1"),
3820            );
3821            // A TTY guest-node process reads stdin through the kernel PTY: host
3822            // input is written to the PTY master (write_kernel_process_stdin),
3823            // line discipline runs (echo / VERASE / ICRNL / VEOF), and the
3824            // sidecar drains the cooked bytes from the slave and forwards them
3825            // to the isolate's stream-stdin dispatch
3826            // (forward_tty_slave_input_to_javascript). The in-isolate
3827            // `_kernelStdinRead` bridge stays local; no RPC forwarding is
3828            // needed because the isolate never reads kernel fd 0 itself.
3829        } else if resolved.runtime == GuestRuntimeKind::WebAssembly {
3830            env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
3831        }
3832        let argv = std::iter::once(resolved.entrypoint.clone())
3833            .chain(resolved.execution_args.iter().cloned())
3834            .collect::<Vec<_>>();
3835        record_execute_phase("env_argv_setup", phase_start.elapsed());
3836        let phase_start = Instant::now();
3837        let kernel_handle = vm
3838            .kernel
3839            .spawn_process(
3840                &resolved.command,
3841                argv,
3842                SpawnOptions {
3843                    requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
3844                    cwd: Some(resolved.guest_cwd.clone()),
3845                    ..SpawnOptions::default()
3846                },
3847            )
3848            .map_err(kernel_error)?;
3849        let kernel_pid = kernel_handle.pid();
3850        record_execute_phase("kernel_spawn_process", phase_start.elapsed());
3851        let tty_master_fd = if requested_tty {
3852            let (master_fd, slave_fd, _) = vm
3853                .kernel
3854                .open_pty(EXECUTION_DRIVER_NAME, kernel_pid)
3855                .map_err(kernel_error)?;
3856            vm.kernel
3857                .fd_dup2(EXECUTION_DRIVER_NAME, kernel_pid, slave_fd, 0)
3858                .map_err(kernel_error)?;
3859            vm.kernel
3860                .fd_dup2(EXECUTION_DRIVER_NAME, kernel_pid, slave_fd, 1)
3861                .map_err(kernel_error)?;
3862            vm.kernel
3863                .fd_dup2(EXECUTION_DRIVER_NAME, kernel_pid, slave_fd, 2)
3864                .map_err(kernel_error)?;
3865            vm.kernel
3866                .pty_set_foreground_pgid(EXECUTION_DRIVER_NAME, kernel_pid, master_fd, kernel_pid)
3867                .map_err(kernel_error)?;
3868            if let Some((cols, rows)) = requested_pty_window_size(&env) {
3869                vm.kernel
3870                    .pty_resize(EXECUTION_DRIVER_NAME, kernel_pid, master_fd, cols, rows)
3871                    .map_err(kernel_error)?;
3872            }
3873            Some(master_fd)
3874        } else {
3875            None
3876        };
3877
3878        let (execution, process_env) = match resolved.runtime {
3879            GuestRuntimeKind::JavaScript => {
3880                let phase_start = Instant::now();
3881                let inline_code = load_javascript_entrypoint_source(
3882                    vm,
3883                    &resolved.host_cwd,
3884                    &resolved.entrypoint,
3885                    &env,
3886                );
3887                record_execute_phase("js_load_entrypoint_source", phase_start.elapsed());
3888                let phase_start = Instant::now();
3889                prepare_javascript_shadow(vm, &resolved)?;
3890                record_execute_phase("js_prepare_shadow", phase_start.elapsed());
3891
3892                let phase_start = Instant::now();
3893                let context =
3894                    self.javascript_engine
3895                        .create_context(CreateJavascriptContextRequest {
3896                            vm_id: vm_id.clone(),
3897                            bootstrap_module: None,
3898                            compile_cache_root: Some(self.cache_root.join("node-compile-cache")),
3899                        });
3900                record_execute_phase("js_create_context", phase_start.elapsed());
3901                let phase_start = Instant::now();
3902                let built_reader = build_module_reader(vm, &resolved);
3903                let guest_reader = built_reader.clone().map(|reader| {
3904                    Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader))
3905                        as Box<dyn GuestModuleReader>
3906                });
3907                let module_reader =
3908                    built_reader.map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
3909                record_execute_phase("js_build_module_reader", phase_start.elapsed());
3910                let phase_start = Instant::now();
3911                let execution = self
3912                    .javascript_engine
3913                    .start_execution_with_module_reader(
3914                        StartJavascriptExecutionRequest {
3915                            guest_runtime: guest_runtime_identity(vm, None, None),
3916                            vm_id: vm_id.clone(),
3917                            context_id: context.context_id,
3918                            argv: std::iter::once(resolved.entrypoint.clone())
3919                                .chain(resolved.execution_args.iter().cloned())
3920                                .collect(),
3921                            env: env.clone(),
3922                            cwd: resolved.host_cwd.clone(),
3923                            limits: javascript_execution_limits(vm),
3924                            inline_code,
3925                            wasm_module_bytes: None,
3926                        },
3927                        module_reader,
3928                        guest_reader,
3929                    )
3930                    .map_err(javascript_error)?;
3931                record_execute_phase("js_start_execution", phase_start.elapsed());
3932                (ActiveExecution::Javascript(execution), env.clone())
3933            }
3934            GuestRuntimeKind::Python => {
3935                // The `python` command path (marked by AGENTOS_PYTHON_ARGV) is
3936                // explicit about file mode via AGENTOS_PYTHON_FILE, so a `-c` code
3937                // string that happens to end in `.py` is never mistaken for a path.
3938                // The low-level execute API keeps the `.py`-suffix heuristic.
3939                let python_file_path = if resolved.env.contains_key("AGENTOS_PYTHON_ARGV") {
3940                    resolved.env.get("AGENTOS_PYTHON_FILE").map(PathBuf::from)
3941                } else {
3942                    python_file_entrypoint(&resolved.entrypoint)
3943                };
3944                let pyodide_dist_path = self
3945                    .python_engine
3946                    .bundled_pyodide_dist_path_for_vm(&vm_id)
3947                    .map_err(python_error)?;
3948                let pyodide_cache_path = pyodide_dist_path
3949                    .parent()
3950                    .and_then(Path::parent)
3951                    .unwrap_or(pyodide_dist_path.as_path())
3952                    .join("pyodide-package-cache");
3953                add_runtime_guest_path_mapping(
3954                    &mut env,
3955                    PYTHON_PYODIDE_GUEST_ROOT,
3956                    &pyodide_dist_path,
3957                );
3958                add_runtime_guest_path_mapping(
3959                    &mut env,
3960                    PYTHON_PYODIDE_CACHE_GUEST_ROOT,
3961                    &pyodide_cache_path,
3962                );
3963                add_runtime_host_access_path(
3964                    &mut env,
3965                    "AGENTOS_EXTRA_FS_READ_PATHS",
3966                    &pyodide_dist_path,
3967                    true,
3968                );
3969                add_runtime_host_access_path(
3970                    &mut env,
3971                    "AGENTOS_EXTRA_FS_READ_PATHS",
3972                    &pyodide_cache_path,
3973                    true,
3974                );
3975                add_runtime_host_access_path(
3976                    &mut env,
3977                    "AGENTOS_EXTRA_FS_WRITE_PATHS",
3978                    &pyodide_cache_path,
3979                    false,
3980                );
3981                let context = self
3982                    .python_engine
3983                    .create_context(CreatePythonContextRequest {
3984                        vm_id: vm_id.clone(),
3985                        pyodide_dist_path,
3986                    });
3987                let execution = self
3988                    .python_engine
3989                    .start_execution(StartPythonExecutionRequest {
3990                        vm_id: vm_id.clone(),
3991                        context_id: context.context_id,
3992                        code: resolved.entrypoint.clone(),
3993                        file_path: python_file_path,
3994                        env: env.clone(),
3995                        cwd: resolved.host_cwd.clone(),
3996                        limits: python_execution_limits(vm),
3997                        guest_runtime: guest_runtime_identity(vm, None, None),
3998                    })
3999                    .map_err(python_error)?;
4000                (ActiveExecution::Python(execution), env.clone())
4001            }
4002            GuestRuntimeKind::WebAssembly => {
4003                let wasm_limits = wasm_execution_limits(vm);
4004                let wasm_guest_runtime =
4005                    guest_runtime_identity(vm, Some(u64::from(kernel_pid)), Some(0));
4006                let wasm_permission_tier = resolved.wasm_permission_tier.unwrap_or_else(|| {
4007                    resolve_wasm_permission_tier(
4008                        vm,
4009                        Some(&resolved.command),
4010                        None,
4011                        &resolved.entrypoint,
4012                    )
4013                });
4014                let context = self.wasm_engine.create_context(CreateWasmContextRequest {
4015                    vm_id: vm_id.clone(),
4016                    module_path: Some(resolved.entrypoint.clone()),
4017                });
4018                let execution = self
4019                    .wasm_engine
4020                    .start_execution(StartWasmExecutionRequest {
4021                        vm_id: vm_id.clone(),
4022                        context_id: context.context_id,
4023                        argv: resolved.process_args.clone(),
4024                        env: env.clone(),
4025                        cwd: resolved.host_cwd.clone(),
4026                        permission_tier: execution_wasm_permission_tier(wasm_permission_tier),
4027                        limits: wasm_limits,
4028                        guest_runtime: wasm_guest_runtime,
4029                    })
4030                    .map_err(wasm_error)?;
4031                (ActiveExecution::Wasm(Box::new(execution)), env)
4032            }
4033        };
4034        let child_pid = execution.child_pid();
4035        let phase_start = Instant::now();
4036        let kernel_stdin_writer_fd = if let Some(master_fd) = tty_master_fd {
4037            master_fd
4038        } else {
4039            install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?
4040        };
4041        vm.active_processes.insert(
4042            payload.process_id.clone(),
4043            ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
4044                .with_kernel_stdin_writer_fd(kernel_stdin_writer_fd)
4045                .with_tty_master_fd(tty_master_fd)
4046                .with_guest_cwd(resolved.guest_cwd.clone())
4047                .with_env(process_env)
4048                .with_host_cwd(resolved.host_cwd.clone()),
4049        );
4050        self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy)?;
4051        mark_execute_response_ready(&vm_id, &payload.process_id);
4052        record_execute_phase("process_register_and_lifecycle", phase_start.elapsed());
4053        record_execute_phase("execute_total", execute_total_start.elapsed());
4054
4055        Ok(DispatchResult {
4056            response: process_started_response(
4057                request,
4058                payload.process_id,
4059                Some(if child_pid == 0 {
4060                    kernel_pid
4061                } else {
4062                    child_pid
4063                }),
4064            ),
4065            events: Vec::new(),
4066        })
4067    }
4068
4069    pub(crate) async fn resize_pty(
4070        &mut self,
4071        request: &RequestFrame,
4072        payload: ResizePtyRequest,
4073    ) -> Result<DispatchResult, SidecarError> {
4074        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4075        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4076
4077        let vm = self
4078            .vms
4079            .get_mut(&vm_id)
4080            .ok_or_else(|| missing_vm_error(&vm_id))?;
4081        let process = vm
4082            .active_processes
4083            .get_mut(&payload.process_id)
4084            .ok_or_else(|| {
4085                SidecarError::InvalidState(format!(
4086                    "VM {vm_id} has no active process {}",
4087                    payload.process_id
4088                ))
4089            })?;
4090        let Some(writer_fd) = process.kernel_stdin_writer_fd else {
4091            return Err(SidecarError::InvalidState(format!(
4092                "process {} does not have a PTY",
4093                payload.process_id
4094            )));
4095        };
4096        vm.kernel
4097            .pty_resize(
4098                EXECUTION_DRIVER_NAME,
4099                process.kernel_pid,
4100                writer_fd,
4101                payload.cols,
4102                payload.rows,
4103            )
4104            .map_err(kernel_error)?;
4105
4106        Ok(DispatchResult {
4107            response: self.respond(
4108                request,
4109                ResponsePayload::PtyResized(PtyResizedResponse {
4110                    process_id: payload.process_id,
4111                    cols: payload.cols,
4112                    rows: payload.rows,
4113                }),
4114            ),
4115            events: Vec::new(),
4116        })
4117    }
4118
4119    pub(crate) async fn write_stdin(
4120        &mut self,
4121        request: &RequestFrame,
4122        payload: WriteStdinRequest,
4123    ) -> Result<DispatchResult, SidecarError> {
4124        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4125        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4126
4127        let vm = self
4128            .vms
4129            .get_mut(&vm_id)
4130            .ok_or_else(|| missing_vm_error(&vm_id))?;
4131        let process = vm
4132            .active_processes
4133            .get_mut(&payload.process_id)
4134            .ok_or_else(|| {
4135                SidecarError::InvalidState(format!(
4136                    "VM {vm_id} has no active process {}",
4137                    payload.process_id
4138                ))
4139            })?;
4140        // For a TTY JavaScript process, host stdin must go ONLY to the kernel PTY
4141        // master (so line discipline + echo apply); feeding the in-process local
4142        // stdin bridge as well would double-deliver the input. Non-TTY JS (piped
4143        // stdin) still uses the local bridge; wasm/python always take the
4144        // streaming/no-op `write_stdin` path plus the kernel master write below.
4145        let tty_js =
4146            process.runtime == GuestRuntimeKind::JavaScript && process.tty_master_fd.is_some();
4147        if !tty_js {
4148            process.execution.write_stdin(&payload.chunk)?;
4149        }
4150        write_kernel_process_stdin(&mut vm.kernel, process, &payload.chunk)?;
4151
4152        Ok(DispatchResult {
4153            response: stdin_written_response(
4154                request,
4155                payload.process_id,
4156                payload.chunk.len() as u64,
4157            ),
4158            events: Vec::new(),
4159        })
4160    }
4161
4162    pub(crate) async fn close_stdin(
4163        &mut self,
4164        request: &RequestFrame,
4165        payload: CloseStdinRequest,
4166    ) -> Result<DispatchResult, SidecarError> {
4167        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4168        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4169
4170        let vm = self
4171            .vms
4172            .get_mut(&vm_id)
4173            .ok_or_else(|| missing_vm_error(&vm_id))?;
4174        let process = vm
4175            .active_processes
4176            .get_mut(&payload.process_id)
4177            .ok_or_else(|| {
4178                SidecarError::InvalidState(format!(
4179                    "VM {vm_id} has no active process {}",
4180                    payload.process_id
4181                ))
4182            })?;
4183        process.execution.close_stdin()?;
4184        close_kernel_process_stdin(&mut vm.kernel, process)?;
4185
4186        Ok(DispatchResult {
4187            response: stdin_closed_response(request, payload.process_id),
4188            events: Vec::new(),
4189        })
4190    }
4191
4192    pub(crate) async fn kill_process(
4193        &mut self,
4194        request: &RequestFrame,
4195        payload: KillProcessRequest,
4196    ) -> Result<DispatchResult, SidecarError> {
4197        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4198        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4199        self.kill_process_internal(&vm_id, &payload.process_id, &payload.signal)?;
4200
4201        Ok(DispatchResult {
4202            response: process_killed_response(request, payload.process_id),
4203            events: Vec::new(),
4204        })
4205    }
4206
4207    pub(crate) async fn find_listener(
4208        &mut self,
4209        request: &RequestFrame,
4210        payload: FindListenerRequest,
4211    ) -> Result<DispatchResult, SidecarError> {
4212        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4213        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4214        require_vm_inspection_permission(
4215            &self.bridge,
4216            &vm_id,
4217            "network.inspect",
4218            "network",
4219            &socket_query_resource(SocketQueryKind::TcpListener, &payload),
4220        )?;
4221
4222        let listener =
4223            find_socket_state_entry(self.vms.get(&vm_id), SocketQueryKind::TcpListener, &payload)?;
4224
4225        Ok(DispatchResult {
4226            response: listener_snapshot_response(request, listener),
4227            events: Vec::new(),
4228        })
4229    }
4230
4231    pub(crate) async fn get_process_snapshot(
4232        &mut self,
4233        request: &RequestFrame,
4234        _payload: GetProcessSnapshotRequest,
4235    ) -> Result<DispatchResult, SidecarError> {
4236        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4237        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4238        require_vm_inspection_permission(
4239            &self.bridge,
4240            &vm_id,
4241            "process.inspect",
4242            "process",
4243            "process://snapshot",
4244        )?;
4245
4246        let processes = self
4247            .vms
4248            .get_mut(&vm_id)
4249            .map(|vm| {
4250                prune_exited_process_snapshots(vm);
4251                snapshot_vm_processes(vm)
4252            })
4253            .unwrap_or_default();
4254
4255        Ok(DispatchResult {
4256            response: process_snapshot_response(request, processes),
4257            events: Vec::new(),
4258        })
4259    }
4260
4261    pub(crate) async fn guest_kernel_call(
4262        &mut self,
4263        request: &RequestFrame,
4264        payload: GuestKernelCallRequest,
4265    ) -> Result<DispatchResult, SidecarError> {
4266        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4267        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4268
4269        let vm = self.vms.get_mut(&vm_id).ok_or_else(|| {
4270            SidecarError::InvalidState(format!("VM {vm_id} no longer exists for guest kernel call"))
4271        })?;
4272        let kernel_pid = vm
4273            .active_processes
4274            .get(&payload.execution_id)
4275            .map(|process| process.kernel_pid)
4276            .ok_or_else(|| {
4277                SidecarError::InvalidState(format!(
4278                    "VM {vm_id} has no active process {} for guest kernel call",
4279                    payload.execution_id
4280                ))
4281            })?;
4282
4283        let response = secure_exec_sidecar_core::handle_guest_kernel_call(
4284            &mut vm.kernel,
4285            kernel_pid,
4286            EXECUTION_DRIVER_NAME,
4287            &payload.operation,
4288            &payload.payload,
4289        )
4290        .map_err(guest_kernel_core_error)?;
4291
4292        Ok(DispatchResult {
4293            response: self.respond(
4294                request,
4295                ResponsePayload::GuestKernelResult(GuestKernelResultResponse { payload: response }),
4296            ),
4297            events: Vec::new(),
4298        })
4299    }
4300
4301    pub(crate) async fn get_resource_snapshot(
4302        &mut self,
4303        request: &RequestFrame,
4304        _payload: GetResourceSnapshotRequest,
4305    ) -> Result<DispatchResult, SidecarError> {
4306        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4307        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4308        require_vm_inspection_permission(
4309            &self.bridge,
4310            &vm_id,
4311            "process.inspect",
4312            "process",
4313            "process://resources",
4314        )?;
4315
4316        let snapshot = self
4317            .vms
4318            .get(&vm_id)
4319            .map(|vm| vm.kernel.resource_snapshot())
4320            .unwrap_or_default();
4321        let queue_snapshots = queue_tracker::queue_snapshot()
4322            .into_iter()
4323            .map(|queue| QueueSnapshotEntry {
4324                name: queue.name.as_str().to_owned(),
4325                category: queue.category.as_str().to_owned(),
4326                depth: queue.depth as u64,
4327                high_water: queue.high_water as u64,
4328                capacity: queue.capacity as u64,
4329                fill_percent: queue.fill_percent as u64,
4330            })
4331            .collect();
4332
4333        Ok(DispatchResult {
4334            response: self.respond(
4335                request,
4336                ResponsePayload::ResourceSnapshot(ResourceSnapshotResponse {
4337                    running_processes: snapshot.running_processes as u64,
4338                    exited_processes: snapshot.exited_processes as u64,
4339                    fd_tables: snapshot.fd_tables as u64,
4340                    open_fds: snapshot.open_fds as u64,
4341                    pipes: snapshot.pipes as u64,
4342                    pipe_buffered_bytes: snapshot.pipe_buffered_bytes as u64,
4343                    ptys: snapshot.ptys as u64,
4344                    pty_buffered_input_bytes: snapshot.pty_buffered_input_bytes as u64,
4345                    pty_buffered_output_bytes: snapshot.pty_buffered_output_bytes as u64,
4346                    sockets: snapshot.sockets as u64,
4347                    socket_listeners: snapshot.socket_listeners as u64,
4348                    socket_connections: snapshot.socket_connections as u64,
4349                    socket_buffered_bytes: snapshot.socket_buffered_bytes as u64,
4350                    socket_datagram_queue_len: snapshot.socket_datagram_queue_len as u64,
4351                    queue_snapshots,
4352                }),
4353            ),
4354            events: Vec::new(),
4355        })
4356    }
4357
4358    pub(crate) async fn find_bound_udp(
4359        &mut self,
4360        request: &RequestFrame,
4361        payload: FindBoundUdpRequest,
4362    ) -> Result<DispatchResult, SidecarError> {
4363        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4364        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4365
4366        let lookup_request = FindListenerRequest {
4367            host: payload.host,
4368            port: payload.port,
4369            path: None,
4370        };
4371        require_vm_inspection_permission(
4372            &self.bridge,
4373            &vm_id,
4374            "network.inspect",
4375            "network",
4376            &socket_query_resource(SocketQueryKind::UdpBound, &lookup_request),
4377        )?;
4378        let socket = find_socket_state_entry(
4379            self.vms.get(&vm_id),
4380            SocketQueryKind::UdpBound,
4381            &lookup_request,
4382        )?;
4383
4384        Ok(DispatchResult {
4385            response: bound_udp_snapshot_response(request, socket),
4386            events: Vec::new(),
4387        })
4388    }
4389
4390    pub(crate) async fn vm_fetch(
4391        &mut self,
4392        request: &RequestFrame,
4393        payload: VmFetchRequest,
4394    ) -> Result<DispatchResult, SidecarError> {
4395        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4396        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4397
4398        let vm = self
4399            .vms
4400            .get_mut(&vm_id)
4401            .ok_or_else(|| SidecarError::InvalidState(String::from("unknown sidecar VM")))?;
4402        let target_path = if payload.path.starts_with('/') {
4403            payload.path.clone()
4404        } else {
4405            format!("/{}", payload.path)
4406        };
4407        let request_url = Url::parse(&format!("http://127.0.0.1:{}{target_path}", payload.port))
4408            .map_err(|error| {
4409                SidecarError::InvalidState(format!(
4410                    "invalid vm.fetch target {target_path:?}: {error}"
4411                ))
4412            })?;
4413        let header_values: BTreeMap<String, Value> = serde_json::from_str(&payload.headers_json)
4414            .map_err(|error| {
4415                SidecarError::InvalidState(format!(
4416                    "vm.fetch headers_json must be valid JSON: {error}"
4417                ))
4418            })?;
4419        let options = JavascriptHttpRequestOptions {
4420            method: Some(payload.method),
4421            headers: header_values,
4422            body: payload.body,
4423            reject_unauthorized: None,
4424        };
4425        let headers = parse_http_header_collection(&options.headers, "vm.fetch headers")?;
4426        let target_process_id = find_kernel_http_listener_process(vm, payload.port);
4427        if let Some(target_process_id) = target_process_id {
4428            let max_fetch_response_bytes = vm.limits.http.max_fetch_response_bytes;
4429            let response_json = match dispatch_kernel_http_fetch(
4430                &self.bridge,
4431                &vm_id,
4432                vm,
4433                &target_process_id,
4434                payload.port,
4435                &target_path,
4436                &options,
4437                &headers,
4438                max_fetch_response_bytes,
4439            ) {
4440                Ok(response_json) => response_json,
4441                Err(error) => {
4442                    if let Some(exit_code) = kernel_http_fetch_target_exit_code(&error) {
4443                        let _ = vm;
4444                        self.finish_active_process_exit(&vm_id, &target_process_id, exit_code)?;
4445                    }
4446                    return Err(error);
4447                }
4448            };
4449            let response = self.respond(
4450                request,
4451                ResponsePayload::VmFetchResult(VmFetchResponse { response_json }),
4452            );
4453            ensure_vm_fetch_response_frame_within_limit(&response, self.config.max_frame_bytes)?;
4454
4455            return Ok(DispatchResult {
4456                response,
4457                events: Vec::new(),
4458            });
4459        }
4460
4461        let Some((target_process_id, server_id)) =
4462            vm.active_processes
4463                .iter()
4464                .find_map(|(process_id, process)| {
4465                    process
4466                        .http_servers
4467                        .iter()
4468                        .find(|(_, server)| server.guest_local_addr.port() == payload.port)
4469                        .map(|(server_id, _)| (process_id.clone(), *server_id))
4470                })
4471        else {
4472            return Err(SidecarError::Execution(format!(
4473                "vm.fetch could not find a guest HTTP listener on port {}",
4474                payload.port
4475            )));
4476        };
4477        let socket_paths = build_javascript_socket_path_context(vm)?;
4478        let resource_limits = vm.kernel.resource_limits().clone();
4479        let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness);
4480        let process = vm
4481            .active_processes
4482            .get_mut(&target_process_id)
4483            .ok_or_else(|| {
4484                SidecarError::InvalidState(format!(
4485                    "vm.fetch target process disappeared: {target_process_id}"
4486                ))
4487            })?;
4488        let request_json = serialize_http_loopback_request(&request_url, &options, &headers)?;
4489        let response_json = dispatch_loopback_http_request(LoopbackHttpDispatchRequest {
4490            bridge: &self.bridge,
4491            vm_id: &vm_id,
4492            dns: &vm.dns,
4493            socket_paths: &socket_paths,
4494            kernel: &mut vm.kernel,
4495            kernel_readiness,
4496            process,
4497            resource_limits: &resource_limits,
4498            server_id,
4499            request_json: &request_json,
4500        })?;
4501
4502        let response = self.respond(
4503            request,
4504            ResponsePayload::VmFetchResult(VmFetchResponse { response_json }),
4505        );
4506        ensure_vm_fetch_response_frame_within_limit(&response, self.config.max_frame_bytes)?;
4507
4508        Ok(DispatchResult {
4509            response,
4510            events: Vec::new(),
4511        })
4512    }
4513
4514    pub(crate) async fn get_signal_state(
4515        &mut self,
4516        request: &RequestFrame,
4517        payload: GetSignalStateRequest,
4518    ) -> Result<DispatchResult, SidecarError> {
4519        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4520        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4521
4522        let handlers = self
4523            .vms
4524            .get(&vm_id)
4525            .and_then(|vm| vm.signal_states.get(&payload.process_id))
4526            .cloned()
4527            .unwrap_or_default();
4528
4529        Ok(DispatchResult {
4530            response: signal_state_response(request, payload.process_id, handlers),
4531            events: Vec::new(),
4532        })
4533    }
4534
4535    pub(crate) async fn get_zombie_timer_count(
4536        &mut self,
4537        request: &RequestFrame,
4538        _payload: GetZombieTimerCountRequest,
4539    ) -> Result<DispatchResult, SidecarError> {
4540        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4541        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4542
4543        let count = self
4544            .vms
4545            .get(&vm_id)
4546            .map(|vm| vm.kernel.zombie_timer_count() as u64)
4547            .unwrap_or_default();
4548
4549        Ok(DispatchResult {
4550            response: zombie_timer_count_response(request, count),
4551            events: Vec::new(),
4552        })
4553    }
4554
4555    pub(crate) fn kill_process_internal(
4556        &mut self,
4557        vm_id: &str,
4558        process_id: &str,
4559        signal: &str,
4560    ) -> Result<(), SidecarError> {
4561        let signal_name = signal.to_owned();
4562        let signal = parse_signal(signal)?;
4563        let vm = self
4564            .vms
4565            .get_mut(vm_id)
4566            .ok_or_else(|| SidecarError::InvalidState(format!("unknown sidecar VM {vm_id}")))?;
4567        let process = vm.active_processes.get_mut(process_id).ok_or_else(|| {
4568            SidecarError::InvalidState(format!("VM {vm_id} has no active process {process_id}"))
4569        })?;
4570        let kernel_pid = process.kernel_pid;
4571        if !matches!(signal, 0 | libc::SIGCONT) {
4572            // A guest parked in a deferred kernel-wait sync RPC is blocked in a
4573            // native bridge wait the kill cannot interrupt; answer the parked
4574            // RPC first so the termination can take effect.
4575            flush_parked_kernel_wait_rpc(process);
4576        }
4577
4578        enum KillBehavior {
4579            Tool,
4580            SharedV8StateOnly,
4581            SharedV8Continue,
4582            SharedV8Terminate,
4583            SharedV8DispatchOrTerminate,
4584            Noop,
4585            HostPid(u32),
4586        }
4587
4588        let behavior = match &process.execution {
4589            ActiveExecution::Tool(_) => KillBehavior::Tool,
4590            ActiveExecution::Javascript(execution)
4591                if execution.uses_shared_v8_runtime() && matches!(signal, 0 | libc::SIGSTOP) =>
4592            {
4593                KillBehavior::SharedV8StateOnly
4594            }
4595            ActiveExecution::Javascript(execution)
4596                if execution.uses_shared_v8_runtime() && signal == libc::SIGCONT =>
4597            {
4598                KillBehavior::SharedV8Continue
4599            }
4600            ActiveExecution::Wasm(execution)
4601                if execution.uses_shared_v8_runtime()
4602                    && matches!(signal, 0 | libc::SIGSTOP | libc::SIGCONT) =>
4603            {
4604                KillBehavior::SharedV8StateOnly
4605            }
4606            ActiveExecution::Python(execution)
4607                if execution.uses_shared_v8_runtime()
4608                    && matches!(signal, 0 | libc::SIGSTOP | libc::SIGCONT) =>
4609            {
4610                KillBehavior::SharedV8StateOnly
4611            }
4612            ActiveExecution::Javascript(execution)
4613                if execution.uses_shared_v8_runtime() && signal == SIGKILL =>
4614            {
4615                KillBehavior::SharedV8Terminate
4616            }
4617            ActiveExecution::Wasm(execution)
4618                if execution.uses_shared_v8_runtime() && signal == SIGKILL =>
4619            {
4620                KillBehavior::SharedV8Terminate
4621            }
4622            ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime() => {
4623                KillBehavior::SharedV8DispatchOrTerminate
4624            }
4625            ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime() => {
4626                KillBehavior::SharedV8Terminate
4627            }
4628            ActiveExecution::Python(execution) if execution.uses_shared_v8_runtime() => {
4629                KillBehavior::SharedV8Terminate
4630            }
4631            ActiveExecution::Javascript(execution) if execution.child_pid() == 0 => {
4632                KillBehavior::Noop
4633            }
4634            _ => KillBehavior::HostPid(process.execution.child_pid()),
4635        };
4636
4637        match behavior {
4638            KillBehavior::Tool => {
4639                let ActiveExecution::Tool(execution) = &process.execution else {
4640                    unreachable!("kill behavior must match tool execution");
4641                };
4642                if signal != 0 {
4643                    execution.cancelled.store(true, Ordering::Relaxed);
4644                    process.queue_pending_execution_event(ActiveExecutionEvent::Exited(
4645                        128 + signal,
4646                    ))?;
4647                }
4648            }
4649            KillBehavior::SharedV8StateOnly => {
4650                if matches!(signal, libc::SIGSTOP | libc::SIGCONT) {
4651                    vm.kernel
4652                        .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal)
4653                        .map_err(kernel_error)?;
4654                }
4655            }
4656            KillBehavior::SharedV8Continue => {
4657                vm.kernel
4658                    .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal)
4659                    .map_err(kernel_error)?;
4660                if signal != 0 && !dispatch_v8_process_signal(process, signal)? {
4661                    process.execution.terminate()?;
4662                }
4663            }
4664            KillBehavior::SharedV8Terminate => {
4665                if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
4666                    close_kernel_process_stdin(&mut vm.kernel, process)?;
4667                }
4668                process.execution.terminate()?;
4669                let needs_synthetic_exit = matches!(process.execution, ActiveExecution::Wasm(_))
4670                    || (signal == SIGKILL
4671                        && matches!(process.execution, ActiveExecution::Javascript(_)));
4672                if signal != 0 && needs_synthetic_exit {
4673                    process.queue_pending_execution_event(ActiveExecutionEvent::Exited(
4674                        128 + signal,
4675                    ))?;
4676                }
4677            }
4678            KillBehavior::SharedV8DispatchOrTerminate => {
4679                if signal != 0 && !dispatch_v8_process_signal(process, signal)? {
4680                    process.execution.terminate()?;
4681                }
4682            }
4683            KillBehavior::Noop => {}
4684            KillBehavior::HostPid(pid) => {
4685                if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
4686                    close_kernel_process_stdin(&mut vm.kernel, process)?;
4687                }
4688                signal_runtime_process(pid, signal)?;
4689            }
4690        }
4691        emit_security_audit_event(
4692            &self.bridge,
4693            vm_id,
4694            "security.process.kill",
4695            audit_fields([
4696                (String::from("source"), String::from("control_plane")),
4697                (String::from("source_pid"), String::from("0")),
4698                (String::from("target_pid"), process.kernel_pid.to_string()),
4699                (String::from("process_id"), process_id.to_owned()),
4700                (String::from("signal"), signal_name),
4701                (
4702                    String::from("host_pid"),
4703                    process.execution.child_pid().to_string(),
4704                ),
4705            ]),
4706        );
4707        Ok(())
4708    }
4709
4710    pub async fn pump_process_events(
4711        &mut self,
4712        ownership: &OwnershipScope,
4713    ) -> Result<bool, SidecarError> {
4714        let mut emitted_any = false;
4715
4716        let mut queued_envelopes = Vec::new();
4717        {
4718            let pending_capacity = self.pending_process_event_capacity();
4719            let receiver = self.process_event_receiver.as_mut().ok_or_else(|| {
4720                SidecarError::InvalidState(String::from("process event receiver unavailable"))
4721            })?;
4722            loop {
4723                if queued_envelopes.len() >= pending_capacity {
4724                    if receiver.is_empty() {
4725                        break;
4726                    }
4727                    return Err(process_event_queue_overflow_error());
4728                }
4729                match receiver.try_recv() {
4730                    Ok(envelope) => {
4731                        queued_envelopes.push(envelope);
4732                        emitted_any = true;
4733                    }
4734                    Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
4735                    Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
4736                }
4737            }
4738        }
4739        for envelope in queued_envelopes {
4740            self.queue_pending_process_event(envelope)?;
4741        }
4742
4743        let vm_ids = self.vm_ids_for_scope(ownership)?;
4744        for vm_id in vm_ids {
4745            while let Some(vm) = self.vms.get(&vm_id) {
4746                let connection_id = vm.connection_id.clone();
4747                let session_id = vm.session_id.clone();
4748                let process_ids = self
4749                    .vms
4750                    .get(&vm_id)
4751                    .map(|vm| vm.active_processes.keys().cloned().collect::<Vec<_>>())
4752                    .unwrap_or_default();
4753                let mut emitted_this_pass = false;
4754
4755                for process_id in process_ids {
4756                    if self
4757                        .vms
4758                        .get(&vm_id)
4759                        .is_some_and(|vm| vm.detached_child_processes.contains(&process_id))
4760                    {
4761                        continue;
4762                    }
4763                    enum ProcessPollResult {
4764                        Event(Box<Option<ActiveExecutionEvent>>),
4765                        RecoverClosedChannel,
4766                    }
4767                    let poll_result = {
4768                        let Some(vm) = self.vms.get_mut(&vm_id) else {
4769                            continue;
4770                        };
4771                        let Some(process) = vm.active_processes.get_mut(&process_id) else {
4772                            continue;
4773                        };
4774                        if let Some(event) = process.pending_execution_events.pop_front() {
4775                            ProcessPollResult::Event(Box::new(Some(event)))
4776                        } else {
4777                            match process.execution.poll_event(Duration::ZERO).await {
4778                                Ok(event) => ProcessPollResult::Event(Box::new(event)),
4779                                Err(SidecarError::Execution(message))
4780                                    if (process.runtime == GuestRuntimeKind::JavaScript
4781                                        && closed_javascript_event_channel(&message))
4782                                        || (process.runtime == GuestRuntimeKind::Python
4783                                            && closed_python_event_channel(&message))
4784                                        || (process.runtime == GuestRuntimeKind::WebAssembly
4785                                            && closed_wasm_event_channel(&message)) =>
4786                                {
4787                                    ProcessPollResult::RecoverClosedChannel
4788                                }
4789                                Err(other) => return Err(other),
4790                            }
4791                        }
4792                    };
4793                    let event = match poll_result {
4794                        ProcessPollResult::Event(event) => *event,
4795                        ProcessPollResult::RecoverClosedChannel => {
4796                            self.recover_closed_root_runtime_process_event(&vm_id, &process_id)?
4797                        }
4798                    };
4799
4800                    let Some(event) = event else {
4801                        continue;
4802                    };
4803                    if matches!(&event, ActiveExecutionEvent::Exited(_)) {
4804                        record_execute_response_to_exit_milestone(
4805                            "execute_response_to_exit_event_polled",
4806                            &vm_id,
4807                            &process_id,
4808                        );
4809                    }
4810
4811                    if Self::internal_execution_event(&event) {
4812                        // These events are sidecar work items, not client-facing
4813                        // process events. Handle them immediately so a sibling
4814                        // process can service sync RPCs while another request
4815                        // waits on VM-local networking.
4816                        self.handle_execution_event(&vm_id, &process_id, event)?;
4817                    } else {
4818                        self.queue_pending_process_event(ProcessEventEnvelope {
4819                            connection_id: connection_id.clone(),
4820                            session_id: session_id.clone(),
4821                            vm_id: vm_id.clone(),
4822                            process_id: process_id.clone(),
4823                            event,
4824                        })?;
4825                    }
4826                    emitted_any = true;
4827                    emitted_this_pass = true;
4828                }
4829
4830                if !emitted_this_pass {
4831                    break;
4832                }
4833            }
4834
4835            if self.pump_detached_child_process_events(&vm_id)? {
4836                emitted_any = true;
4837            }
4838        }
4839
4840        Ok(emitted_any)
4841    }
4842
4843    fn internal_execution_event(event: &ActiveExecutionEvent) -> bool {
4844        matches!(
4845            event,
4846            ActiveExecutionEvent::JavascriptSyncRpcRequest(_)
4847                | ActiveExecutionEvent::PythonVfsRpcRequest(_)
4848                | ActiveExecutionEvent::SignalState { .. }
4849        )
4850    }
4851
4852    fn recover_closed_root_runtime_process_event(
4853        &mut self,
4854        vm_id: &str,
4855        process_id: &str,
4856    ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
4857        let Some(vm) = self.vms.get_mut(vm_id) else {
4858            return Ok(None);
4859        };
4860        let Some(process) = vm.active_processes.get(process_id) else {
4861            return Ok(None);
4862        };
4863        if process.execution.uses_shared_v8_runtime() {
4864            return Ok(None);
4865        }
4866        if process.runtime != GuestRuntimeKind::JavaScript
4867            && process.runtime != GuestRuntimeKind::Python
4868            && process.runtime != GuestRuntimeKind::WebAssembly
4869        {
4870            return Ok(None);
4871        }
4872        let runtime_child_pid = process.execution.child_pid();
4873        if runtime_child_pid == 0 {
4874            return Ok(None);
4875        }
4876        if let Some(status) = runtime_child_exit_status(runtime_child_pid)? {
4877            return Ok(Some(ActiveExecutionEvent::Exited(status)));
4878        }
4879        if runtime_child_is_alive(runtime_child_pid)? {
4880            return Ok(None);
4881        }
4882        Ok(Some(ActiveExecutionEvent::Exited(0)))
4883    }
4884
4885    fn active_process_by_path<'a>(
4886        process: &'a ActiveProcess,
4887        child_path: &[&str],
4888    ) -> Option<&'a ActiveProcess> {
4889        let mut current = process;
4890        for child_id in child_path {
4891            current = current.child_processes.get(*child_id)?;
4892        }
4893        Some(current)
4894    }
4895
4896    fn active_process_by_path_mut<'a>(
4897        process: &'a mut ActiveProcess,
4898        child_path: &[&str],
4899    ) -> Option<&'a mut ActiveProcess> {
4900        let mut current = process;
4901        for child_id in child_path {
4902            current = current.child_processes.get_mut(*child_id)?;
4903        }
4904        Some(current)
4905    }
4906
4907    fn active_process_by_owned_path_mut<'a>(
4908        process: &'a mut ActiveProcess,
4909        child_path: &[String],
4910    ) -> Option<&'a mut ActiveProcess> {
4911        let mut current = process;
4912        for child_id in child_path {
4913            current = current.child_processes.get_mut(child_id)?;
4914        }
4915        Some(current)
4916    }
4917
4918    fn active_process_path_by_kernel_pid(
4919        process: &ActiveProcess,
4920        kernel_pid: u32,
4921    ) -> Option<Vec<String>> {
4922        if process.kernel_pid == kernel_pid {
4923            return Some(Vec::new());
4924        }
4925
4926        for (child_id, child) in &process.child_processes {
4927            let Some(mut path) = Self::active_process_path_by_kernel_pid(child, kernel_pid) else {
4928                continue;
4929            };
4930            path.insert(0, child_id.clone());
4931            return Some(path);
4932        }
4933
4934        None
4935    }
4936
4937    fn descendant_parent_process<'a>(
4938        vm: &'a VmState,
4939        process_id: &str,
4940        child_path: &[&str],
4941    ) -> Option<&'a ActiveProcess> {
4942        let root = vm.active_processes.get(process_id)?;
4943        Self::active_process_by_path(root, child_path)
4944    }
4945
4946    fn descendant_parent_process_mut<'a>(
4947        vm: &'a mut VmState,
4948        process_id: &str,
4949        child_path: &[&str],
4950    ) -> Option<&'a mut ActiveProcess> {
4951        let root = vm.active_processes.get_mut(process_id)?;
4952        Self::active_process_by_path_mut(root, child_path)
4953    }
4954
4955    fn child_process_path_label(process_id: &str, child_path: &[&str]) -> String {
4956        if child_path.is_empty() {
4957            process_id.to_owned()
4958        } else {
4959            format!("{process_id}/{}", child_path.join("/"))
4960        }
4961    }
4962
4963    fn adopt_detached_child_processes(
4964        current_process_id: &str,
4965        process: &mut ActiveProcess,
4966    ) -> Vec<(String, ActiveProcess)> {
4967        let mut adopted = Vec::new();
4968        let child_ids = process.child_processes.keys().cloned().collect::<Vec<_>>();
4969        for child_id in child_ids {
4970            let child_process_id = format!("{current_process_id}/{child_id}");
4971            let Some(mut child) = process.child_processes.remove(&child_id) else {
4972                continue;
4973            };
4974            if child.detached {
4975                adopted.push((child_process_id, child));
4976                continue;
4977            }
4978
4979            adopted.extend(Self::adopt_detached_child_processes(
4980                &child_process_id,
4981                &mut child,
4982            ));
4983            process.child_processes.insert(child_id, child);
4984        }
4985        adopted
4986    }
4987
4988    fn child_process_signal_key<'a>(process_id: &'a str, child_path: &[&'a str]) -> &'a str {
4989        child_path.last().copied().unwrap_or(process_id)
4990    }
4991
4992    fn resolve_detached_child_process_path(
4993        vm: &VmState,
4994        detached_process_id: &str,
4995    ) -> Option<(String, Vec<String>)> {
4996        let root_process_id = vm
4997            .active_processes
4998            .keys()
4999            .filter(|candidate| {
5000                detached_process_id == candidate.as_str()
5001                    || detached_process_id
5002                        .strip_prefix(candidate.as_str())
5003                        .is_some_and(|remainder| remainder.starts_with('/'))
5004            })
5005            .max_by_key(|candidate| candidate.len())?
5006            .clone();
5007
5008        let remainder = detached_process_id
5009            .strip_prefix(root_process_id.as_str())
5010            .unwrap_or_default();
5011        if remainder.is_empty() {
5012            return Some((root_process_id, Vec::new()));
5013        }
5014
5015        Some((
5016            root_process_id,
5017            remainder
5018                .trim_start_matches('/')
5019                .split('/')
5020                .map(str::to_owned)
5021                .collect(),
5022        ))
5023    }
5024
5025    fn pump_detached_child_process_events(&mut self, vm_id: &str) -> Result<bool, SidecarError> {
5026        let detached_process_ids = self
5027            .vms
5028            .get(vm_id)
5029            .map(|vm| {
5030                vm.detached_child_processes
5031                    .iter()
5032                    .cloned()
5033                    .collect::<Vec<_>>()
5034            })
5035            .unwrap_or_default();
5036        let mut emitted_any = false;
5037        for detached_process_id in detached_process_ids {
5038            let Some((root_process_id, child_path)) = self
5039                .vms
5040                .get(vm_id)
5041                .and_then(|vm| Self::resolve_detached_child_process_path(vm, &detached_process_id))
5042            else {
5043                if let Some(vm) = self.vms.get_mut(vm_id) {
5044                    vm.detached_child_processes.remove(&detached_process_id);
5045                }
5046                continue;
5047            };
5048            if child_path.is_empty() {
5049                loop {
5050                    enum ProcessPollResult {
5051                        Event(Box<Option<ActiveExecutionEvent>>),
5052                        RecoverClosedChannel,
5053                    }
5054                    let poll_result = {
5055                        let Some(vm) = self.vms.get_mut(vm_id) else {
5056                            break;
5057                        };
5058                        let Some(process) = vm.active_processes.get_mut(&root_process_id) else {
5059                            break;
5060                        };
5061                        if let Some(event) = process.pending_execution_events.pop_front() {
5062                            ProcessPollResult::Event(Box::new(Some(event)))
5063                        } else {
5064                            match process.execution.poll_event_blocking(Duration::ZERO) {
5065                                Ok(event) => ProcessPollResult::Event(Box::new(event)),
5066                                Err(SidecarError::Execution(message))
5067                                    if (process.runtime == GuestRuntimeKind::JavaScript
5068                                        && closed_javascript_event_channel(&message))
5069                                        || (process.runtime == GuestRuntimeKind::Python
5070                                            && closed_python_event_channel(&message))
5071                                        || (process.runtime == GuestRuntimeKind::WebAssembly
5072                                            && closed_wasm_event_channel(&message)) =>
5073                                {
5074                                    ProcessPollResult::RecoverClosedChannel
5075                                }
5076                                Err(error) => return Err(error),
5077                            }
5078                        }
5079                    };
5080                    let event = match poll_result {
5081                        ProcessPollResult::Event(event) => *event,
5082                        ProcessPollResult::RecoverClosedChannel => {
5083                            self.recover_closed_root_runtime_process_event(vm_id, &root_process_id)?
5084                        }
5085                    };
5086                    let Some(event) = event else {
5087                        break;
5088                    };
5089                    if matches!(&event, ActiveExecutionEvent::Exited(_)) {
5090                        record_execute_response_to_exit_milestone(
5091                            "execute_response_to_detached_exit_event_polled",
5092                            vm_id,
5093                            &detached_process_id,
5094                        );
5095                    }
5096                    let Some((connection_id, session_id)) = self
5097                        .vms
5098                        .get(vm_id)
5099                        .map(|vm| (vm.connection_id.clone(), vm.session_id.clone()))
5100                    else {
5101                        break;
5102                    };
5103                    match event {
5104                        ActiveExecutionEvent::Stdout(chunk) => {
5105                            self.queue_pending_process_event(ProcessEventEnvelope {
5106                                connection_id,
5107                                session_id,
5108                                vm_id: vm_id.to_owned(),
5109                                process_id: detached_process_id.clone(),
5110                                event: ActiveExecutionEvent::Stdout(chunk),
5111                            })?;
5112                            emitted_any = true;
5113                        }
5114                        ActiveExecutionEvent::Stderr(chunk) => {
5115                            self.queue_pending_process_event(ProcessEventEnvelope {
5116                                connection_id,
5117                                session_id,
5118                                vm_id: vm_id.to_owned(),
5119                                process_id: detached_process_id.clone(),
5120                                event: ActiveExecutionEvent::Stderr(chunk),
5121                            })?;
5122                            emitted_any = true;
5123                        }
5124                        ActiveExecutionEvent::Exited(exit_code) => {
5125                            if let Some(vm) = self.vms.get_mut(vm_id) {
5126                                vm.detached_child_processes.remove(&detached_process_id);
5127                            }
5128                            self.queue_pending_process_event(ProcessEventEnvelope {
5129                                connection_id,
5130                                session_id,
5131                                vm_id: vm_id.to_owned(),
5132                                process_id: detached_process_id.clone(),
5133                                event: ActiveExecutionEvent::Exited(exit_code),
5134                            })?;
5135                            emitted_any = true;
5136                            break;
5137                        }
5138                        ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
5139                            self.handle_javascript_sync_rpc_request(
5140                                vm_id,
5141                                &root_process_id,
5142                                request,
5143                            )?;
5144                        }
5145                        ActiveExecutionEvent::PythonVfsRpcRequest(request) => {
5146                            self.handle_python_vfs_rpc_request(vm_id, &root_process_id, *request)?;
5147                        }
5148                        ActiveExecutionEvent::SignalState {
5149                            signal,
5150                            registration,
5151                        } => {
5152                            if let Some(vm) = self.vms.get_mut(vm_id) {
5153                                vm.signal_states
5154                                    .entry(root_process_id.clone())
5155                                    .or_default()
5156                                    .insert(signal, registration);
5157                            }
5158                        }
5159                    }
5160                }
5161                continue;
5162            }
5163
5164            let parent_path = child_path[..child_path.len() - 1]
5165                .iter()
5166                .map(String::as_str)
5167                .collect::<Vec<_>>();
5168            let child_process_id = child_path.last().expect("child path cannot be empty");
5169
5170            loop {
5171                let event = match self.poll_descendant_javascript_child_process(
5172                    vm_id,
5173                    &root_process_id,
5174                    &parent_path,
5175                    child_process_id,
5176                    0,
5177                ) {
5178                    Ok(event) => event,
5179                    Err(SidecarError::InvalidState(message))
5180                        if message.contains("unknown child process")
5181                            || message.contains("unknown child process path") =>
5182                    {
5183                        if let Some(vm) = self.vms.get_mut(vm_id) {
5184                            vm.detached_child_processes.remove(&detached_process_id);
5185                        }
5186                        break;
5187                    }
5188                    Err(error) if is_javascript_child_process_gone_error(&error) => {
5189                        if let Some(vm) = self.vms.get_mut(vm_id) {
5190                            vm.detached_child_processes.remove(&detached_process_id);
5191                        }
5192                        break;
5193                    }
5194                    Err(error) => return Err(error),
5195                };
5196
5197                let Some(event_type) = event.get("type").and_then(Value::as_str) else {
5198                    break;
5199                };
5200                let Some((connection_id, session_id)) = self
5201                    .vms
5202                    .get(vm_id)
5203                    .map(|vm| (vm.connection_id.clone(), vm.session_id.clone()))
5204                else {
5205                    break;
5206                };
5207
5208                let envelope = match event_type {
5209                    "stdout" => Some(ProcessEventEnvelope {
5210                        connection_id: connection_id.clone(),
5211                        session_id: session_id.clone(),
5212                        vm_id: vm_id.to_owned(),
5213                        process_id: detached_process_id.clone(),
5214                        event: ActiveExecutionEvent::Stdout(javascript_sync_rpc_bytes_arg(
5215                            &[event.get("data").cloned().unwrap_or(Value::Null)],
5216                            0,
5217                            "detached child_process stdout",
5218                        )?),
5219                    }),
5220                    "stderr" => Some(ProcessEventEnvelope {
5221                        connection_id: connection_id.clone(),
5222                        session_id: session_id.clone(),
5223                        vm_id: vm_id.to_owned(),
5224                        process_id: detached_process_id.clone(),
5225                        event: ActiveExecutionEvent::Stderr(javascript_sync_rpc_bytes_arg(
5226                            &[event.get("data").cloned().unwrap_or(Value::Null)],
5227                            0,
5228                            "detached child_process stderr",
5229                        )?),
5230                    }),
5231                    "exit" => {
5232                        if let Some(vm) = self.vms.get_mut(vm_id) {
5233                            vm.detached_child_processes.remove(&detached_process_id);
5234                        }
5235                        Some(ProcessEventEnvelope {
5236                            connection_id,
5237                            session_id,
5238                            vm_id: vm_id.to_owned(),
5239                            process_id: detached_process_id.clone(),
5240                            event: ActiveExecutionEvent::Exited(
5241                                event
5242                                    .get("exitCode")
5243                                    .and_then(Value::as_i64)
5244                                    .map(|value| value as i32)
5245                                    .unwrap_or(1),
5246                            ),
5247                        })
5248                    }
5249                    _ => None,
5250                };
5251
5252                let Some(envelope) = envelope else {
5253                    break;
5254                };
5255                self.queue_pending_process_event(envelope)?;
5256                emitted_any = true;
5257
5258                if event_type == "exit" {
5259                    break;
5260                }
5261            }
5262        }
5263
5264        Ok(emitted_any)
5265    }
5266    pub(crate) fn drain_queued_descendant_javascript_child_process_events(
5267        &mut self,
5268        vm_id: &str,
5269        process_id: &str,
5270        child_path: &[&str],
5271    ) -> Result<(), SidecarError> {
5272        if child_path.is_empty() {
5273            return Ok(());
5274        }
5275        let target_process_id = Self::child_process_path_label(process_id, child_path);
5276        let mut child_capacity = self
5277            .vms
5278            .get(vm_id)
5279            .and_then(|vm| vm.active_processes.get(process_id))
5280            .and_then(|root| descendant_pending_execution_event_capacity(root, child_path));
5281
5282        let mut deferred = VecDeque::new();
5283        while let Some(envelope) = self.pending_process_events.pop_front() {
5284            if envelope.vm_id == vm_id && envelope.process_id == target_process_id {
5285                if matches!(child_capacity, Some(0)) {
5286                    self.pending_process_events.push_front(envelope);
5287                    while let Some(deferred_envelope) = deferred.pop_back() {
5288                        self.pending_process_events.push_front(deferred_envelope);
5289                    }
5290                    return Err(process_event_queue_overflow_error());
5291                }
5292                if let Some(vm) = self.vms.get_mut(vm_id) {
5293                    if let Some(root) = vm.active_processes.get_mut(process_id) {
5294                        if let Some(child) = Self::active_process_by_path_mut(root, child_path) {
5295                            child.queue_pending_execution_event(envelope.event)?;
5296                            child_capacity = child_capacity.map(|capacity| capacity - 1);
5297                            continue;
5298                        }
5299                    }
5300                }
5301            }
5302            deferred.push_back(envelope);
5303        }
5304        self.pending_process_events = deferred;
5305
5306        let mut queued = Vec::new();
5307        {
5308            let transfer_capacity = self
5309                .pending_process_event_capacity()
5310                .min(child_capacity.unwrap_or(usize::MAX));
5311            let receiver = self.process_event_receiver.as_mut().ok_or_else(|| {
5312                SidecarError::InvalidState(String::from("process event receiver unavailable"))
5313            })?;
5314            loop {
5315                if queued.len() >= transfer_capacity {
5316                    if receiver.is_empty() {
5317                        break;
5318                    }
5319                    return Err(process_event_queue_overflow_error());
5320                }
5321                match receiver.try_recv() {
5322                    Ok(envelope) => queued.push(envelope),
5323                    Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
5324                    Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
5325                }
5326            }
5327        }
5328        for envelope in queued {
5329            if envelope.vm_id == vm_id && envelope.process_id == target_process_id {
5330                if let Some(vm) = self.vms.get_mut(vm_id) {
5331                    if let Some(root) = vm.active_processes.get_mut(process_id) {
5332                        if let Some(child) = Self::active_process_by_path_mut(root, child_path) {
5333                            child.queue_pending_execution_event(envelope.event)?;
5334                            continue;
5335                        }
5336                    }
5337                }
5338            }
5339            self.queue_pending_process_event(envelope)?;
5340        }
5341
5342        Ok(())
5343    }
5344
5345    pub(crate) fn handle_execution_event(
5346        &mut self,
5347        vm_id: &str,
5348        process_id: &str,
5349        event: ActiveExecutionEvent,
5350    ) -> Result<Option<EventFrame>, SidecarError> {
5351        let Some(vm) = self.vms.get(vm_id) else {
5352            log_stale_process_event(&self.bridge, vm_id, process_id, "execution event dispatch");
5353            return Ok(None);
5354        };
5355        if !vm.active_processes.contains_key(process_id) {
5356            log_stale_process_event(&self.bridge, vm_id, process_id, "execution event dispatch");
5357            return Ok(None);
5358        }
5359        let (connection_id, session_id) = { (vm.connection_id.clone(), vm.session_id.clone()) };
5360        let ownership = OwnershipScope::vm(&connection_id, &session_id, vm_id);
5361
5362        if self.capture_extension_process_output_event(vm_id, process_id, &event) {
5363            return Ok(None);
5364        }
5365
5366        match event {
5367            ActiveExecutionEvent::Stdout(chunk) => Ok(Some(EventFrame::new(
5368                ownership,
5369                EventPayload::ProcessOutput(ProcessOutputEvent {
5370                    process_id: process_id.to_owned(),
5371                    channel: StreamChannel::Stdout,
5372                    chunk,
5373                }),
5374            ))),
5375            ActiveExecutionEvent::Stderr(chunk) => Ok(Some(EventFrame::new(
5376                ownership,
5377                EventPayload::ProcessOutput(ProcessOutputEvent {
5378                    process_id: process_id.to_owned(),
5379                    channel: StreamChannel::Stderr,
5380                    chunk,
5381                }),
5382            ))),
5383            ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
5384                self.handle_javascript_sync_rpc_request(vm_id, process_id, request)?;
5385                Ok(None)
5386            }
5387            ActiveExecutionEvent::PythonVfsRpcRequest(request) => {
5388                self.handle_python_vfs_rpc_request(vm_id, process_id, *request)?;
5389                Ok(None)
5390            }
5391            ActiveExecutionEvent::SignalState {
5392                signal,
5393                registration,
5394            } => {
5395                let Some(vm) = self.vms.get_mut(vm_id) else {
5396                    return Ok(None);
5397                };
5398                if !vm.active_processes.contains_key(process_id) {
5399                    return Ok(None);
5400                }
5401                vm.signal_states
5402                    .entry(process_id.to_owned())
5403                    .or_default()
5404                    .insert(signal, registration);
5405                Ok(None)
5406            }
5407            ActiveExecutionEvent::Exited(exit_code) => {
5408                record_execute_response_to_exit_milestone(
5409                    "execute_response_to_exit_event_handle",
5410                    vm_id,
5411                    process_id,
5412                );
5413                record_execute_response_to_exit(vm_id, process_id);
5414                let phase_start = Instant::now();
5415                let became_idle = self
5416                    .finish_active_process_exit(vm_id, process_id, exit_code)?
5417                    .unwrap_or(false);
5418                record_execute_phase("process_exit_cleanup", phase_start.elapsed());
5419
5420                let phase_start = Instant::now();
5421                if became_idle {
5422                    self.bridge.emit_lifecycle(vm_id, LifecycleState::Ready)?;
5423                }
5424                record_execute_phase("process_exit_lifecycle_emit", phase_start.elapsed());
5425
5426                Ok(Some(EventFrame::new(
5427                    ownership,
5428                    EventPayload::ProcessExited(ProcessExitedEvent {
5429                        process_id: process_id.to_owned(),
5430                        exit_code,
5431                    }),
5432                )))
5433            }
5434        }
5435    }
5436
5437    pub(crate) fn finish_active_process_exit(
5438        &mut self,
5439        vm_id: &str,
5440        process_id: &str,
5441        exit_code: i32,
5442    ) -> Result<Option<bool>, SidecarError> {
5443        let Some(vm) = self.vms.get_mut(vm_id) else {
5444            log_stale_process_event(&self.bridge, vm_id, process_id, "process exit cleanup");
5445            return Ok(None);
5446        };
5447        if !vm.active_processes.contains_key(process_id) {
5448            log_stale_process_event(&self.bridge, vm_id, process_id, "process exit cleanup");
5449            return Ok(None);
5450        }
5451
5452        let phase_start = Instant::now();
5453        prune_exited_process_snapshots(vm);
5454        record_execute_phase(
5455            "process_exit_cleanup_prune_snapshots",
5456            phase_start.elapsed(),
5457        );
5458        let phase_start = Instant::now();
5459        let process_table = vm.kernel.list_processes();
5460        record_execute_phase("process_exit_cleanup_list_processes", phase_start.elapsed());
5461        let phase_start = Instant::now();
5462        let Some(mut process) = vm.active_processes.remove(process_id) else {
5463            return Ok(None);
5464        };
5465        record_execute_phase("process_exit_cleanup_remove_active", phase_start.elapsed());
5466        let phase_start = Instant::now();
5467        if let Some(info) = process_table.get(&process.kernel_pid) {
5468            vm.exited_process_snapshots
5469                .push_back(ExitedProcessSnapshot {
5470                    captured_at: Instant::now(),
5471                    process: build_process_snapshot_entry(
5472                        process_id,
5473                        &process,
5474                        info,
5475                        Some(exit_code),
5476                    ),
5477                });
5478        }
5479        record_execute_phase("process_exit_cleanup_build_snapshot", phase_start.elapsed());
5480        let phase_start = Instant::now();
5481        let detached_children = Self::adopt_detached_child_processes(process_id, &mut process);
5482        record_execute_phase("process_exit_cleanup_adopt_detached", phase_start.elapsed());
5483        let phase_start = Instant::now();
5484        let should_sync_host_writes = process.host_write_dirty_recursive()
5485            || !process.clean_host_writes_are_observable_recursive();
5486        if should_sync_host_writes {
5487            sync_process_host_writes_to_kernel(vm, &process)?;
5488        } else {
5489            record_execute_phase(
5490                "process_exit_cleanup_sync_host_writes_clean_skip",
5491                Duration::ZERO,
5492            );
5493        }
5494        record_execute_phase(
5495            "process_exit_cleanup_sync_host_writes",
5496            phase_start.elapsed(),
5497        );
5498        let phase_start = Instant::now();
5499        let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness);
5500        terminate_child_process_tree(&mut vm.kernel, &mut process, &kernel_readiness);
5501        record_execute_phase(
5502            "process_exit_cleanup_terminate_child_tree",
5503            phase_start.elapsed(),
5504        );
5505        let phase_start = Instant::now();
5506        process.kernel_handle.finish(exit_code);
5507        record_execute_phase("process_exit_cleanup_kernel_finish", phase_start.elapsed());
5508        let phase_start = Instant::now();
5509        let _ = vm.kernel.wait_and_reap(process.kernel_pid);
5510        record_execute_phase("process_exit_cleanup_wait_and_reap", phase_start.elapsed());
5511        let phase_start = Instant::now();
5512        vm.signal_states.remove(process_id);
5513        record_execute_phase(
5514            "process_exit_cleanup_signal_state_remove",
5515            phase_start.elapsed(),
5516        );
5517        let phase_start = Instant::now();
5518        for (detached_process_id, detached_child) in detached_children {
5519            vm.detached_child_processes
5520                .insert(detached_process_id.clone());
5521            vm.active_processes
5522                .insert(detached_process_id, detached_child);
5523        }
5524        record_execute_phase(
5525            "process_exit_cleanup_reinsert_detached",
5526            phase_start.elapsed(),
5527        );
5528        let phase_start = Instant::now();
5529        let became_idle = vm.active_processes.is_empty();
5530        record_execute_phase("process_exit_cleanup_became_idle", phase_start.elapsed());
5531        let phase_start = Instant::now();
5532        self.prune_extension_process_resource(process_id);
5533        record_execute_phase("process_exit_cleanup_prune_resource", phase_start.elapsed());
5534
5535        Ok(Some(became_idle))
5536    }
5537
5538    pub(crate) fn drain_process_events_blocking_with_limit(
5539        &mut self,
5540        vm_id: &str,
5541        process_id: &str,
5542        max_events: usize,
5543    ) -> Result<Vec<ActiveExecutionEvent>, SidecarError> {
5544        let mut events = Vec::new();
5545        if max_events == 0 {
5546            return Ok(events);
5547        }
5548        let mut deadline = Instant::now() + PROCESS_EXIT_DRAIN_INITIAL_QUIET;
5549
5550        loop {
5551            if events.len() >= max_events {
5552                break;
5553            }
5554            let event = {
5555                let Some(vm) = self.vms.get_mut(vm_id) else {
5556                    break;
5557                };
5558                let Some(process) = vm.active_processes.get_mut(process_id) else {
5559                    break;
5560                };
5561                if let Some(event) = process.pending_execution_events.pop_front() {
5562                    Some(event)
5563                } else {
5564                    match process.execution.poll_event_blocking(Duration::ZERO) {
5565                        Ok(event) => event,
5566                        Err(SidecarError::Execution(_)) => None,
5567                        Err(other) => return Err(other),
5568                    }
5569                }
5570            };
5571
5572            let Some(event) = event else {
5573                if Instant::now() >= deadline {
5574                    break;
5575                }
5576                let blocking_wait = deadline.saturating_duration_since(Instant::now());
5577                if blocking_wait.is_zero() {
5578                    break;
5579                }
5580                if events.len() >= max_events {
5581                    break;
5582                }
5583                let delayed_event = {
5584                    let Some(vm) = self.vms.get_mut(vm_id) else {
5585                        break;
5586                    };
5587                    let Some(process) = vm.active_processes.get_mut(process_id) else {
5588                        break;
5589                    };
5590                    if let Some(event) = process.pending_execution_events.pop_front() {
5591                        Some(event)
5592                    } else {
5593                        match process.execution.poll_event_blocking(blocking_wait) {
5594                            Ok(event) => event,
5595                            Err(SidecarError::Execution(_)) => None,
5596                            Err(other) => return Err(other),
5597                        }
5598                    }
5599                };
5600                let Some(event) = delayed_event else {
5601                    break;
5602                };
5603                events.push(event);
5604                deadline = Instant::now() + PROCESS_EXIT_DRAIN_TRAILING_QUIET;
5605                continue;
5606            };
5607            events.push(event);
5608            deadline = Instant::now() + PROCESS_EXIT_DRAIN_TRAILING_QUIET;
5609        }
5610
5611        Ok(events)
5612    }
5613
5614    pub(crate) fn handle_python_vfs_rpc_request(
5615        &mut self,
5616        vm_id: &str,
5617        process_id: &str,
5618        request: PythonVfsRpcRequest,
5619    ) -> Result<(), SidecarError> {
5620        match request.method {
5621            PythonVfsRpcMethod::Read
5622            | PythonVfsRpcMethod::Write
5623            | PythonVfsRpcMethod::Stat
5624            | PythonVfsRpcMethod::Lstat
5625            | PythonVfsRpcMethod::ReadDir
5626            | PythonVfsRpcMethod::Mkdir
5627            | PythonVfsRpcMethod::Unlink
5628            | PythonVfsRpcMethod::Rmdir
5629            | PythonVfsRpcMethod::Rename
5630            | PythonVfsRpcMethod::Symlink
5631            | PythonVfsRpcMethod::ReadLink
5632            | PythonVfsRpcMethod::Setattr => {
5633                filesystem_handle_python_vfs_rpc_request(self, vm_id, process_id, request)
5634            }
5635            PythonVfsRpcMethod::HttpRequest => {
5636                self.handle_python_http_rpc_request(vm_id, process_id, request)
5637            }
5638            PythonVfsRpcMethod::DnsLookup => {
5639                self.handle_python_dns_rpc_request(vm_id, process_id, request)
5640            }
5641            PythonVfsRpcMethod::SubprocessRun => {
5642                self.handle_python_subprocess_rpc_request(vm_id, process_id, request)
5643            }
5644            PythonVfsRpcMethod::SocketConnect
5645            | PythonVfsRpcMethod::SocketSend
5646            | PythonVfsRpcMethod::SocketRecv
5647            | PythonVfsRpcMethod::SocketClose
5648            | PythonVfsRpcMethod::UdpCreate
5649            | PythonVfsRpcMethod::UdpSendto
5650            | PythonVfsRpcMethod::UdpRecvfrom => {
5651                self.handle_python_socket_rpc_request(vm_id, process_id, request)
5652            }
5653        }
5654    }
5655
5656    fn handle_python_http_rpc_request(
5657        &mut self,
5658        vm_id: &str,
5659        process_id: &str,
5660        request: PythonVfsRpcRequest,
5661    ) -> Result<(), SidecarError> {
5662        let Some(vm) = self.vms.get(vm_id) else {
5663            return Ok(());
5664        };
5665        if !vm.active_processes.contains_key(process_id) {
5666            return Ok(());
5667        }
5668        let response = (|| {
5669            let url_text = request.url.as_deref().ok_or_else(|| {
5670                SidecarError::InvalidState(String::from("python httpRequest requires a url"))
5671            })?;
5672            let url = Url::parse(url_text)
5673                .map_err(|error| SidecarError::Execution(format!("ERR_INVALID_URL: {error}")))?;
5674            let host = url.host_str().ok_or_else(|| {
5675                SidecarError::Execution(String::from("ERR_INVALID_URL: missing host"))
5676            })?;
5677            let port = url.port_or_known_default().ok_or_else(|| {
5678                SidecarError::Execution(String::from("ERR_INVALID_URL: missing port"))
5679            })?;
5680            self.bridge.require_network_access(
5681                vm_id,
5682                NetworkOperation::Http,
5683                format_tcp_resource(host, port),
5684            )?;
5685            // Pin the outbound connection to the IP addresses that pass the
5686            // egress range guard at resolution time. A literal IP is validated
5687            // directly; a hostname is resolved once here and the resulting
5688            // address set is pinned into the HTTP client's resolver below so a
5689            // rebinding DNS server cannot make the second (TLS/TCP) lookup land
5690            // on a private/link-local/metadata IP that this check rejected.
5691            let pinned_addresses = if let Ok(literal_ip) = host.parse::<IpAddr>() {
5692                filter_dns_safe_ip_addrs(vec![literal_ip], host)?
5693            } else {
5694                filter_dns_safe_ip_addrs(
5695                    resolve_dns_ip_addrs(
5696                        &self.bridge,
5697                        &vm.kernel,
5698                        vm_id,
5699                        &vm.dns,
5700                        host,
5701                        DnsLookupPolicy::SkipPermissions,
5702                    )?,
5703                    host,
5704                )?
5705            };
5706            let mut headers = BTreeMap::new();
5707            for (name, value) in &request.headers {
5708                headers.insert(name.clone(), Value::String(value.clone()));
5709            }
5710            let options = JavascriptHttpRequestOptions {
5711                method: Some(
5712                    request
5713                        .http_method
5714                        .clone()
5715                        .unwrap_or_else(|| String::from("GET")),
5716                ),
5717                headers,
5718                body: request.body_base64.as_deref().map(|body| {
5719                    String::from_utf8(
5720                        base64::engine::general_purpose::STANDARD
5721                            .decode(body)
5722                            .unwrap_or_default(),
5723                    )
5724                    .unwrap_or_default()
5725                }),
5726                reject_unauthorized: None,
5727            };
5728            let headers =
5729                parse_http_header_collection(&options.headers, "python httpRequest headers")?;
5730            let response =
5731                issue_outbound_http_request(&url, &options, &headers, &pinned_addresses)?;
5732            let payload_json = response.as_str().ok_or_else(|| {
5733                SidecarError::Execution(String::from(
5734                    "python httpRequest returned a non-string response payload",
5735                ))
5736            })?;
5737            let payload: Value = serde_json::from_str(payload_json).map_err(|error| {
5738                SidecarError::Execution(format!(
5739                    "python httpRequest response must be valid JSON: {error}"
5740                ))
5741            })?;
5742            let header_map = payload
5743                .get("headers")
5744                .and_then(Value::as_array)
5745                .map(|entries| {
5746                    let mut normalized = BTreeMap::<String, Vec<String>>::new();
5747                    for entry in entries {
5748                        let Some(pair) = entry.as_array() else {
5749                            continue;
5750                        };
5751                        let Some(name) = pair.first().and_then(Value::as_str) else {
5752                            continue;
5753                        };
5754                        let Some(value) = pair.get(1).and_then(Value::as_str) else {
5755                            continue;
5756                        };
5757                        normalized
5758                            .entry(name.to_owned())
5759                            .or_default()
5760                            .push(value.to_owned());
5761                    }
5762                    normalized
5763                })
5764                .unwrap_or_default();
5765            Ok(PythonVfsRpcResponsePayload::Http {
5766                status: payload
5767                    .get("status")
5768                    .and_then(Value::as_u64)
5769                    .map(|value| value as u16)
5770                    .unwrap_or_default(),
5771                reason: payload
5772                    .get("statusText")
5773                    .and_then(Value::as_str)
5774                    .unwrap_or_default()
5775                    .to_owned(),
5776                url: payload
5777                    .get("url")
5778                    .and_then(Value::as_str)
5779                    .unwrap_or(url_text)
5780                    .to_owned(),
5781                headers: header_map,
5782                body_base64: payload
5783                    .get("body")
5784                    .and_then(Value::as_str)
5785                    .unwrap_or_default()
5786                    .to_owned(),
5787            })
5788        })();
5789
5790        self.respond_python_rpc(vm_id, process_id, request.id, response)
5791    }
5792
5793    fn handle_python_dns_rpc_request(
5794        &mut self,
5795        vm_id: &str,
5796        process_id: &str,
5797        request: PythonVfsRpcRequest,
5798    ) -> Result<(), SidecarError> {
5799        let Some(vm) = self.vms.get(vm_id) else {
5800            return Ok(());
5801        };
5802        if !vm.active_processes.contains_key(process_id) {
5803            return Ok(());
5804        }
5805        let response = (|| {
5806            let hostname = request.hostname.as_deref().ok_or_else(|| {
5807                SidecarError::InvalidState(String::from("python dnsLookup requires a hostname"))
5808            })?;
5809            let mut addresses = filter_dns_safe_ip_addrs(
5810                resolve_dns_ip_addrs(
5811                    &self.bridge,
5812                    &vm.kernel,
5813                    vm_id,
5814                    &vm.dns,
5815                    hostname,
5816                    DnsLookupPolicy::CheckPermissions,
5817                )?,
5818                hostname,
5819            )?;
5820            if let Some(family) = request.family {
5821                addresses.retain(|address| {
5822                    matches!((family, address), (4, IpAddr::V4(_)) | (6, IpAddr::V6(_)))
5823                });
5824            }
5825            Ok(PythonVfsRpcResponsePayload::DnsLookup {
5826                addresses: addresses
5827                    .into_iter()
5828                    .map(|address| address.to_string())
5829                    .collect(),
5830            })
5831        })();
5832
5833        self.respond_python_rpc(vm_id, process_id, request.id, response)
5834    }
5835
5836    fn handle_python_subprocess_rpc_request(
5837        &mut self,
5838        vm_id: &str,
5839        process_id: &str,
5840        request: PythonVfsRpcRequest,
5841    ) -> Result<(), SidecarError> {
5842        let command = request.command.clone().ok_or_else(|| {
5843            SidecarError::InvalidState(String::from("python subprocessRun requires a command"))
5844        })?;
5845        let (internal_bootstrap_env, cwd) = {
5846            let Some(vm) = self.vms.get(vm_id) else {
5847                return Ok(());
5848            };
5849            let Some(process) = vm.active_processes.get(process_id) else {
5850                return Ok(());
5851            };
5852            let virtual_home = guest_virtual_home(vm);
5853            let cwd = request.cwd.clone().or_else(|| {
5854                guest_runtime_path_for_host_path(
5855                    &vm.guest_env,
5856                    &virtual_home,
5857                    &vm.host_cwd,
5858                    &process.host_cwd.to_string_lossy(),
5859                )
5860            });
5861            (
5862                sanitize_javascript_child_process_internal_bootstrap_env(&vm.guest_env),
5863                cwd,
5864            )
5865        };
5866        let response = self
5867            .spawn_javascript_child_process_sync(
5868                vm_id,
5869                process_id,
5870                JavascriptChildProcessSpawnRequest {
5871                    command,
5872                    args: request.args.clone(),
5873                    options: JavascriptChildProcessSpawnOptions {
5874                        cwd,
5875                        env: request.env.clone(),
5876                        input: None,
5877                        internal_bootstrap_env,
5878                        shell: request.shell,
5879                        detached: false,
5880                        stdio: vec![
5881                            String::from("pipe"),
5882                            String::from("pipe"),
5883                            String::from("pipe"),
5884                        ],
5885                        timeout: None,
5886                        kill_signal: None,
5887                    },
5888                },
5889                request.max_buffer,
5890            )
5891            .map(|payload| PythonVfsRpcResponsePayload::SubprocessRun {
5892                exit_code: payload
5893                    .get("code")
5894                    .and_then(Value::as_i64)
5895                    .map(|value| value as i32)
5896                    .unwrap_or(1),
5897                stdout: payload
5898                    .get("stdout")
5899                    .and_then(Value::as_str)
5900                    .unwrap_or_default()
5901                    .to_owned(),
5902                stderr: payload
5903                    .get("stderr")
5904                    .and_then(Value::as_str)
5905                    .unwrap_or_default()
5906                    .to_owned(),
5907                max_buffer_exceeded: payload
5908                    .get("maxBufferExceeded")
5909                    .and_then(Value::as_bool)
5910                    .unwrap_or(false),
5911            });
5912
5913        self.respond_python_rpc(vm_id, process_id, request.id, response)
5914    }
5915
5916    fn handle_python_socket_rpc_request(
5917        &mut self,
5918        vm_id: &str,
5919        process_id: &str,
5920        request: PythonVfsRpcRequest,
5921    ) -> Result<(), SidecarError> {
5922        if !self.vms.contains_key(vm_id) {
5923            return Ok(());
5924        }
5925        let response = self.python_socket_op(vm_id, process_id, &request);
5926        self.respond_python_rpc(vm_id, process_id, request.id, response)
5927    }
5928
5929    fn python_socket_op(
5930        &mut self,
5931        vm_id: &str,
5932        process_id: &str,
5933        request: &PythonVfsRpcRequest,
5934    ) -> Result<PythonVfsRpcResponsePayload, SidecarError> {
5935        match request.method {
5936            PythonVfsRpcMethod::SocketConnect => {
5937                let host = python_socket_host(request)?;
5938                let port = python_socket_port(request)?;
5939                self.check_python_socket_limit(vm_id)?;
5940                self.bridge.require_network_access(
5941                    vm_id,
5942                    NetworkOperation::Http,
5943                    format_tcp_resource(&host, port),
5944                )?;
5945                let pinned = self.python_socket_pinned_addrs(vm_id, &host, port)?;
5946                let stream = python_connect_tcp(&pinned, port)?;
5947                stream
5948                    .set_read_timeout(Some(PYTHON_SOCKET_READ_POLL))
5949                    .map_err(python_socket_io_error)?;
5950                // Bound writes too: without a write timeout `write_all` on a
5951                // stalled peer would block the shared event loop indefinitely.
5952                stream
5953                    .set_write_timeout(Some(PYTHON_SOCKET_WRITE_TIMEOUT))
5954                    .map_err(python_socket_io_error)?;
5955                let socket_id =
5956                    self.store_python_socket(vm_id, process_id, PythonHostSocket::Tcp(stream))?;
5957                Ok(PythonVfsRpcResponsePayload::SocketCreated { socket_id })
5958            }
5959            PythonVfsRpcMethod::SocketSend => {
5960                let data = python_socket_payload(request)?;
5961                let socket = self.python_socket_mut(vm_id, process_id, request)?;
5962                let PythonHostSocket::Tcp(stream) = socket else {
5963                    return Err(python_socket_kind_error("send", "TCP"));
5964                };
5965                stream.write_all(&data).map_err(python_socket_io_error)?;
5966                Ok(PythonVfsRpcResponsePayload::SocketSent {
5967                    bytes_sent: data.len(),
5968                })
5969            }
5970            PythonVfsRpcMethod::SocketRecv => {
5971                let max = python_socket_recv_len(request);
5972                let socket = self.python_socket_mut(vm_id, process_id, request)?;
5973                let PythonHostSocket::Tcp(stream) = socket else {
5974                    return Err(python_socket_kind_error("recv", "TCP"));
5975                };
5976                let mut buf = vec![0u8; max];
5977                match stream.read(&mut buf) {
5978                    Ok(0) => Ok(PythonVfsRpcResponsePayload::SocketReceived {
5979                        data_base64: String::new(),
5980                        closed: true,
5981                        timed_out: false,
5982                    }),
5983                    Ok(n) => Ok(PythonVfsRpcResponsePayload::SocketReceived {
5984                        data_base64: base64::engine::general_purpose::STANDARD.encode(&buf[..n]),
5985                        closed: false,
5986                        timed_out: false,
5987                    }),
5988                    Err(error) if python_socket_would_block(&error) => {
5989                        Ok(PythonVfsRpcResponsePayload::SocketReceived {
5990                            data_base64: String::new(),
5991                            closed: false,
5992                            timed_out: true,
5993                        })
5994                    }
5995                    Err(error) => Err(python_socket_io_error(error)),
5996                }
5997            }
5998            PythonVfsRpcMethod::SocketClose => {
5999                self.remove_python_socket(vm_id, process_id, request);
6000                Ok(PythonVfsRpcResponsePayload::Empty)
6001            }
6002            PythonVfsRpcMethod::UdpCreate => {
6003                self.check_python_socket_limit(vm_id)?;
6004                let socket = UdpSocket::bind("0.0.0.0:0").map_err(python_socket_io_error)?;
6005                socket
6006                    .set_read_timeout(Some(PYTHON_SOCKET_READ_POLL))
6007                    .map_err(python_socket_io_error)?;
6008                let socket_id =
6009                    self.store_python_socket(vm_id, process_id, PythonHostSocket::Udp(socket))?;
6010                Ok(PythonVfsRpcResponsePayload::SocketCreated { socket_id })
6011            }
6012            PythonVfsRpcMethod::UdpSendto => {
6013                let host = python_socket_host(request)?;
6014                let port = python_socket_port(request)?;
6015                let data = python_socket_payload(request)?;
6016                self.bridge.require_network_access(
6017                    vm_id,
6018                    NetworkOperation::Http,
6019                    format_tcp_resource(&host, port),
6020                )?;
6021                let pinned = self.python_socket_pinned_addrs(vm_id, &host, port)?;
6022                let target = pinned
6023                    .first()
6024                    .map(|ip| SocketAddr::new(*ip, port))
6025                    .ok_or_else(|| {
6026                        SidecarError::Execution(format!("EAI_NONAME: cannot resolve {host}"))
6027                    })?;
6028                let socket = self.python_socket_mut(vm_id, process_id, request)?;
6029                let PythonHostSocket::Udp(udp) = socket else {
6030                    return Err(python_socket_kind_error("sendto", "UDP"));
6031                };
6032                let sent = udp.send_to(&data, target).map_err(python_socket_io_error)?;
6033                Ok(PythonVfsRpcResponsePayload::SocketSent { bytes_sent: sent })
6034            }
6035            PythonVfsRpcMethod::UdpRecvfrom => {
6036                let max = python_socket_recv_len(request);
6037                let socket = self.python_socket_mut(vm_id, process_id, request)?;
6038                let PythonHostSocket::Udp(udp) = socket else {
6039                    return Err(python_socket_kind_error("recvfrom", "UDP"));
6040                };
6041                let mut buf = vec![0u8; max];
6042                match udp.recv_from(&mut buf) {
6043                    Ok((n, addr)) => Ok(PythonVfsRpcResponsePayload::UdpReceived {
6044                        data_base64: base64::engine::general_purpose::STANDARD.encode(&buf[..n]),
6045                        host: addr.ip().to_string(),
6046                        port: addr.port(),
6047                        timed_out: false,
6048                    }),
6049                    Err(error) if python_socket_would_block(&error) => {
6050                        Ok(PythonVfsRpcResponsePayload::UdpReceived {
6051                            data_base64: String::new(),
6052                            host: String::new(),
6053                            port: 0,
6054                            timed_out: true,
6055                        })
6056                    }
6057                    Err(error) => Err(python_socket_io_error(error)),
6058                }
6059            }
6060            _ => Err(SidecarError::InvalidState(String::from(
6061                "non-socket python RPC reached the socket dispatcher unexpectedly",
6062            ))),
6063        }
6064    }
6065
6066    /// Resolve `host` to the egress-guard-approved IP set, then apply the same
6067    /// loopback-connect gate the JS raw-TCP path uses (`filter_tcp_connect_ip_addrs`)
6068    /// so a rebinding DNS server can't map an allowlisted hostname onto a
6069    /// sidecar-local loopback port the VM policy didn't open.
6070    fn python_socket_pinned_addrs(
6071        &self,
6072        vm_id: &str,
6073        host: &str,
6074        port: u16,
6075    ) -> Result<Vec<IpAddr>, SidecarError> {
6076        let Some(vm) = self.vms.get(vm_id) else {
6077            return Err(SidecarError::InvalidState(String::from(
6078                "python socket op for unknown vm",
6079            )));
6080        };
6081        let context = build_javascript_socket_path_context(vm)?;
6082        if let Ok(literal_ip) = host.parse::<IpAddr>() {
6083            filter_tcp_connect_ip_addrs(vec![literal_ip], host, port, &context)
6084        } else {
6085            filter_tcp_connect_ip_addrs(
6086                resolve_dns_ip_addrs(
6087                    &self.bridge,
6088                    &vm.kernel,
6089                    vm_id,
6090                    &vm.dns,
6091                    host,
6092                    DnsLookupPolicy::SkipPermissions,
6093                )?,
6094                host,
6095                port,
6096                &context,
6097            )
6098        }
6099    }
6100
6101    /// Enforce the VM's `max_sockets` resource limit before opening another
6102    /// host socket for the guest (the registry is otherwise unbounded — a
6103    /// hostile guest could exhaust the sidecar's fds/memory).
6104    fn check_python_socket_limit(&self, vm_id: &str) -> Result<(), SidecarError> {
6105        let Some(vm) = self.vms.get(vm_id) else {
6106            return Ok(());
6107        };
6108        let limit = vm.kernel.resource_limits().max_sockets;
6109        let current = vm_network_resource_counts(vm).sockets;
6110        check_network_resource_limit(limit, current, 1, "socket")
6111    }
6112
6113    fn store_python_socket(
6114        &mut self,
6115        vm_id: &str,
6116        process_id: &str,
6117        socket: PythonHostSocket,
6118    ) -> Result<u64, SidecarError> {
6119        let process = self
6120            .vms
6121            .get_mut(vm_id)
6122            .and_then(|vm| vm.active_processes.get_mut(process_id))
6123            .ok_or_else(|| {
6124                SidecarError::InvalidState(String::from("python socket op for reaped vm/process"))
6125            })?;
6126        let socket_id = process.next_python_socket_id;
6127        process.next_python_socket_id = process.next_python_socket_id.wrapping_add(1);
6128        process.python_sockets.insert(socket_id, socket);
6129        Ok(socket_id)
6130    }
6131
6132    fn python_socket_mut(
6133        &mut self,
6134        vm_id: &str,
6135        process_id: &str,
6136        request: &PythonVfsRpcRequest,
6137    ) -> Result<&mut PythonHostSocket, SidecarError> {
6138        let socket_id = request.socket_id.ok_or_else(|| {
6139            SidecarError::InvalidState(String::from("python socket op requires socketId"))
6140        })?;
6141        self.vms
6142            .get_mut(vm_id)
6143            .and_then(|vm| vm.active_processes.get_mut(process_id))
6144            .and_then(|process| process.python_sockets.get_mut(&socket_id))
6145            .ok_or_else(|| {
6146                SidecarError::Execution(format!("EBADF: unknown python socket {socket_id}"))
6147            })
6148    }
6149
6150    fn remove_python_socket(
6151        &mut self,
6152        vm_id: &str,
6153        process_id: &str,
6154        request: &PythonVfsRpcRequest,
6155    ) {
6156        let Some(socket_id) = request.socket_id else {
6157            return;
6158        };
6159        if let Some(process) = self
6160            .vms
6161            .get_mut(vm_id)
6162            .and_then(|vm| vm.active_processes.get_mut(process_id))
6163        {
6164            process.python_sockets.remove(&socket_id);
6165        }
6166    }
6167
6168    fn respond_python_rpc(
6169        &mut self,
6170        vm_id: &str,
6171        process_id: &str,
6172        request_id: u64,
6173        response: Result<PythonVfsRpcResponsePayload, SidecarError>,
6174    ) -> Result<(), SidecarError> {
6175        let Some(vm) = self.vms.get_mut(vm_id) else {
6176            return Ok(());
6177        };
6178        let Some(process) = vm.active_processes.get_mut(process_id) else {
6179            return Ok(());
6180        };
6181        let result = match response {
6182            Ok(payload) => process
6183                .execution
6184                .respond_python_vfs_rpc_success(request_id, payload),
6185            Err(error) => process.execution.respond_python_vfs_rpc_error(
6186                request_id,
6187                "ERR_AGENTOS_PYTHON_VFS_RPC",
6188                error.to_string(),
6189            ),
6190        };
6191        match result {
6192            Ok(()) => Ok(()),
6193            Err(error) if is_broken_pipe_error(&error) => Ok(()),
6194            Err(error) => Err(error),
6195        }
6196    }
6197
6198    pub(crate) fn resolve_javascript_child_process_execution(
6199        &self,
6200        vm: &VmState,
6201        parent_env: &BTreeMap<String, String>,
6202        parent_guest_cwd: &str,
6203        parent_host_cwd: &Path,
6204        request: &JavascriptChildProcessSpawnRequest,
6205    ) -> Result<ResolvedChildProcessExecution, SidecarError> {
6206        let mut runtime_env = parent_env.clone();
6207        runtime_env.extend(request.options.internal_bootstrap_env.clone());
6208        let (guest_cwd, host_cwd_override) = request
6209            .options
6210            .cwd
6211            .as_deref()
6212            .map(|cwd| {
6213                let normalized_parent_host_cwd = normalize_host_path(parent_host_cwd);
6214                let requested_host_cwd = normalize_host_path(Path::new(cwd));
6215                if path_is_within_root(&requested_host_cwd, &normalized_parent_host_cwd) {
6216                    let relative = requested_host_cwd
6217                        .strip_prefix(&normalized_parent_host_cwd)
6218                        .unwrap_or_else(|_| Path::new(""));
6219                    let relative = relative.to_string_lossy().replace('\\', "/");
6220                    let guest_cwd = if relative.is_empty() {
6221                        parent_guest_cwd.to_owned()
6222                    } else {
6223                        normalize_path(&format!("{parent_guest_cwd}/{relative}"))
6224                    };
6225                    (guest_cwd, Some(requested_host_cwd))
6226                } else if Path::new(cwd).is_relative() {
6227                    (
6228                        normalize_path(&format!("{parent_guest_cwd}/{cwd}")),
6229                        Some(normalize_host_path(&parent_host_cwd.join(cwd))),
6230                    )
6231                } else {
6232                    (normalize_path(cwd), None)
6233                }
6234            })
6235            .unwrap_or_else(|| (parent_guest_cwd.to_owned(), None));
6236        let inherited_host_cwd = (host_cwd_override.is_none() && guest_cwd == parent_guest_cwd)
6237            .then(|| normalize_host_path(parent_host_cwd));
6238        let host_cwd = host_cwd_override
6239            .or(inherited_host_cwd)
6240            .or_else(|| {
6241                host_runtime_path_for_guest_path_with_env(
6242                    vm,
6243                    &runtime_env,
6244                    &guest_cwd,
6245                    parent_host_cwd,
6246                )
6247            })
6248            .unwrap_or_else(|| {
6249                let candidate = PathBuf::from(&guest_cwd);
6250                if guest_cwd == parent_guest_cwd {
6251                    normalize_host_path(parent_host_cwd)
6252                } else if candidate.is_absolute() {
6253                    shadow_path_for_guest(vm, &guest_cwd)
6254                } else {
6255                    vm.host_cwd.clone()
6256                }
6257            });
6258        let mut env = parent_env.clone();
6259        env.extend(request.options.env.clone());
6260        // Child JavaScript executions must resolve their own entrypoint/eval state.
6261        // Reusing the parent's values makes the sidecar load the wrong source file.
6262        env.remove("AGENTOS_GUEST_ENTRYPOINT");
6263        env.remove("AGENTOS_NODE_EVAL");
6264
6265        let (command, process_args) = if request.options.shell {
6266            let tokens = tokenize_shell_free_command(&request.command);
6267            let requires_shell = command_requires_shell(&request.command)
6268                || tokens.first().is_some_and(|command| {
6269                    is_posix_shell_builtin(command) || shell_first_token_requires_shell(command)
6270                });
6271            if requires_shell {
6272                if !vm.command_guest_paths.contains_key("sh") {
6273                    return Err(SidecarError::InvalidState(format!(
6274                        "shell-mode child_process command requires /bin/sh, which is not \
6275                         installed in this VM (install a software package that provides sh, \
6276                         for example @secure-exec/coreutils): {}",
6277                        request.command
6278                    )));
6279                }
6280                (
6281                    String::from("sh"),
6282                    vec![String::from("-c"), request.command.clone()],
6283                )
6284            } else {
6285                let Some((command, args)) = tokens.split_first() else {
6286                    return Err(SidecarError::InvalidState(String::from(
6287                        "child_process shell command must not be empty",
6288                    )));
6289                };
6290                (command.clone(), args.to_vec())
6291            }
6292        } else {
6293            (request.command.clone(), request.args.clone())
6294        };
6295        let process_args = apply_shell_cwd_prefix(&command, process_args, &guest_cwd);
6296        if is_tool_command(vm, &command) {
6297            let command = normalized_tool_command_name(&command).unwrap_or(command);
6298            return Ok(ResolvedChildProcessExecution {
6299                command: command.clone(),
6300                process_args: std::iter::once(command.clone())
6301                    .chain(process_args.iter().cloned())
6302                    .collect(),
6303                runtime: GuestRuntimeKind::JavaScript,
6304                entrypoint: command,
6305                execution_args: process_args,
6306                env,
6307                guest_cwd,
6308                host_cwd,
6309                wasm_permission_tier: None,
6310                tool_command: true,
6311            });
6312        }
6313
6314        if is_path_like_specifier(&command)
6315            && matches!(
6316                Path::new(&command).extension().and_then(|ext| ext.to_str()),
6317                Some("js" | "mjs" | "cjs" | "ts" | "mts" | "cts")
6318            )
6319        {
6320            let guest_entrypoint = if command.starts_with('/') {
6321                normalize_path(&command)
6322            } else if command.starts_with("file:") {
6323                normalize_path(command.trim_start_matches("file:"))
6324            } else {
6325                normalize_path(&format!("{guest_cwd}/{command}"))
6326            };
6327            let host_entrypoint = if command.starts_with("./") || command.starts_with("../") {
6328                normalize_host_path(&host_cwd.join(&command))
6329            } else {
6330                host_runtime_path_for_guest_path_with_env(
6331                    vm,
6332                    &runtime_env,
6333                    &guest_entrypoint,
6334                    parent_host_cwd,
6335                )
6336                .unwrap_or_else(|| {
6337                    let candidate = PathBuf::from(&guest_entrypoint);
6338                    if candidate.is_absolute() {
6339                        candidate
6340                    } else {
6341                        host_cwd.join(&guest_entrypoint)
6342                    }
6343                })
6344            };
6345            env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint);
6346            let guest_entrypoint = env.get("AGENTOS_GUEST_ENTRYPOINT").cloned();
6347            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
6348
6349            return Ok(ResolvedChildProcessExecution {
6350                command: command.clone(),
6351                process_args: std::iter::once(command)
6352                    .chain(process_args.iter().cloned())
6353                    .collect(),
6354                runtime: GuestRuntimeKind::JavaScript,
6355                entrypoint: host_entrypoint.to_string_lossy().into_owned(),
6356                execution_args: process_args,
6357                env,
6358                guest_cwd,
6359                host_cwd,
6360                wasm_permission_tier: None,
6361                tool_command: false,
6362            });
6363        }
6364
6365        if is_node_runtime_command(&command) {
6366            if let Some(cli) = resolve_host_node_cli_entrypoint(&command) {
6367                env.insert(
6368                    String::from("AGENTOS_NODE_EVAL"),
6369                    build_host_node_cli_eval(&cli),
6370                );
6371                prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
6372                add_runtime_guest_path_mapping(&mut env, &cli.guest_root, &cli.package_root);
6373                add_runtime_host_access_path(
6374                    &mut env,
6375                    "AGENTOS_EXTRA_FS_READ_PATHS",
6376                    &cli.package_root,
6377                    true,
6378                );
6379
6380                return Ok(ResolvedChildProcessExecution {
6381                    command: command.clone(),
6382                    process_args: std::iter::once(command.clone())
6383                        .chain(process_args.iter().cloned())
6384                        .collect(),
6385                    runtime: GuestRuntimeKind::JavaScript,
6386                    entrypoint: String::from("-e"),
6387                    execution_args: std::iter::once(cli.guest_entrypoint.clone())
6388                        .chain(process_args.iter().cloned())
6389                        .collect(),
6390                    env,
6391                    guest_cwd,
6392                    host_cwd,
6393                    wasm_permission_tier: None,
6394                    tool_command: false,
6395                });
6396            }
6397
6398            if process_args.is_empty() {
6399                env.insert(String::from("AGENTOS_NODE_EVAL"), String::new());
6400                prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
6401
6402                return Ok(ResolvedChildProcessExecution {
6403                    command: command.clone(),
6404                    process_args: vec![command.clone()],
6405                    runtime: GuestRuntimeKind::JavaScript,
6406                    entrypoint: String::from("-e"),
6407                    execution_args: Vec::new(),
6408                    env,
6409                    guest_cwd,
6410                    host_cwd,
6411                    wasm_permission_tier: None,
6412                    tool_command: false,
6413                });
6414            }
6415
6416            if let Some((entrypoint, execution_args)) =
6417                resolve_special_node_cli_invocation(&process_args, &mut env)
6418            {
6419                prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
6420
6421                return Ok(ResolvedChildProcessExecution {
6422                    command: command.clone(),
6423                    process_args: std::iter::once(command.clone())
6424                        .chain(process_args.iter().cloned())
6425                        .collect(),
6426                    runtime: GuestRuntimeKind::JavaScript,
6427                    entrypoint,
6428                    execution_args,
6429                    env,
6430                    guest_cwd,
6431                    host_cwd,
6432                    wasm_permission_tier: None,
6433                    tool_command: false,
6434                });
6435            }
6436
6437            let Some(entrypoint_specifier) = process_args.first() else {
6438                return Err(SidecarError::InvalidState(format!(
6439                    "{command} child_process spawn requires an entrypoint"
6440                )));
6441            };
6442
6443            let (entrypoint, execution_args) = if is_path_like_specifier(entrypoint_specifier) {
6444                let guest_entrypoint = if entrypoint_specifier.starts_with('/') {
6445                    normalize_path(entrypoint_specifier)
6446                } else if entrypoint_specifier.starts_with("file:") {
6447                    normalize_path(entrypoint_specifier.trim_start_matches("file:"))
6448                } else {
6449                    normalize_path(&format!("{guest_cwd}/{entrypoint_specifier}"))
6450                };
6451                let host_entrypoint = if entrypoint_specifier.starts_with("./")
6452                    || entrypoint_specifier.starts_with("../")
6453                {
6454                    normalize_host_path(&host_cwd.join(entrypoint_specifier))
6455                } else {
6456                    host_runtime_path_for_guest_path_with_env(
6457                        vm,
6458                        &runtime_env,
6459                        &guest_entrypoint,
6460                        parent_host_cwd,
6461                    )
6462                    .unwrap_or_else(|| {
6463                        let candidate = PathBuf::from(&guest_entrypoint);
6464                        if candidate.is_absolute() {
6465                            candidate
6466                        } else {
6467                            host_cwd.join(&guest_entrypoint)
6468                        }
6469                    })
6470                };
6471                env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint);
6472                (
6473                    host_entrypoint.to_string_lossy().into_owned(),
6474                    process_args.iter().skip(1).cloned().collect(),
6475                )
6476            } else {
6477                (
6478                    entrypoint_specifier.clone(),
6479                    process_args.iter().skip(1).cloned().collect(),
6480                )
6481            };
6482            let guest_entrypoint = env.get("AGENTOS_GUEST_ENTRYPOINT").cloned();
6483            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
6484
6485            return Ok(ResolvedChildProcessExecution {
6486                command: command.clone(),
6487                process_args: std::iter::once(command)
6488                    .chain(process_args.iter().cloned())
6489                    .collect(),
6490                runtime: GuestRuntimeKind::JavaScript,
6491                entrypoint,
6492                execution_args,
6493                env,
6494                guest_cwd,
6495                host_cwd,
6496                wasm_permission_tier: None,
6497                tool_command: false,
6498            });
6499        }
6500
6501        if is_python_runtime_command(&command) {
6502            return resolve_python_command_execution(
6503                vm,
6504                &command,
6505                &process_args,
6506                env,
6507                guest_cwd,
6508                host_cwd,
6509            );
6510        }
6511
6512        let guest_entrypoint = resolve_guest_command_entrypoint(
6513            vm,
6514            &guest_cwd,
6515            &command,
6516            env.get("PATH").map(String::as_str),
6517        )
6518        .ok_or_else(|| SidecarError::InvalidState(format!("command not found: {command}")))?;
6519        let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint);
6520        let wasm_permission_tier = vm.command_permissions.get(&command).copied().or_else(|| {
6521            Path::new(&guest_entrypoint)
6522                .file_name()
6523                .and_then(|name| name.to_str())
6524                .and_then(|name| vm.command_permissions.get(name).copied())
6525        });
6526        if let Some((javascript_guest_entrypoint, javascript_host_entrypoint)) =
6527            resolve_javascript_command_entrypoint(vm, &guest_entrypoint, &host_entrypoint)
6528        {
6529            prepare_guest_runtime_env(
6530                vm,
6531                &mut env,
6532                &guest_cwd,
6533                &host_cwd,
6534                Some(javascript_guest_entrypoint),
6535            )?;
6536
6537            return Ok(ResolvedChildProcessExecution {
6538                command: command.clone(),
6539                process_args: std::iter::once(command)
6540                    .chain(process_args.iter().cloned())
6541                    .collect(),
6542                runtime: GuestRuntimeKind::JavaScript,
6543                entrypoint: javascript_host_entrypoint.to_string_lossy().into_owned(),
6544                execution_args: process_args,
6545                env,
6546                guest_cwd,
6547                host_cwd,
6548                wasm_permission_tier: None,
6549                tool_command: false,
6550            });
6551        }
6552        prepare_guest_runtime_env(
6553            vm,
6554            &mut env,
6555            &guest_cwd,
6556            &host_cwd,
6557            Some(guest_entrypoint.clone()),
6558        )?;
6559
6560        Ok(ResolvedChildProcessExecution {
6561            command: command.clone(),
6562            process_args: std::iter::once(command)
6563                .chain(process_args.iter().cloned())
6564                .collect(),
6565            runtime: GuestRuntimeKind::WebAssembly,
6566            entrypoint: host_entrypoint.to_string_lossy().into_owned(),
6567            execution_args: process_args,
6568            env,
6569            guest_cwd,
6570            host_cwd,
6571            wasm_permission_tier,
6572            tool_command: false,
6573        })
6574    }
6575
6576    pub(crate) fn spawn_javascript_child_process(
6577        &mut self,
6578        vm_id: &str,
6579        process_id: &str,
6580        request: JavascriptChildProcessSpawnRequest,
6581    ) -> Result<Value, SidecarError> {
6582        let total_start = Instant::now();
6583        let phase_start = Instant::now();
6584        let resolved = {
6585            let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?;
6586            let parent = vm
6587                .active_processes
6588                .get(process_id)
6589                .ok_or_else(|| missing_process_error(vm_id, process_id))?;
6590            self.resolve_javascript_child_process_execution(
6591                vm,
6592                &parent.env,
6593                &parent.guest_cwd,
6594                &parent.host_cwd,
6595                &request,
6596            )?
6597        };
6598        record_execute_phase("child_process_resolve_execution", phase_start.elapsed());
6599        let (parent_kernel_pid, child_process_id) = {
6600            let vm = self
6601                .vms
6602                .get_mut(vm_id)
6603                .ok_or_else(|| missing_vm_error(vm_id))?;
6604            let process = vm
6605                .active_processes
6606                .get_mut(process_id)
6607                .ok_or_else(|| missing_process_error(vm_id, process_id))?;
6608            (process.kernel_pid, process.allocate_child_process_id())
6609        };
6610        let sidecar_requests = self.sidecar_requests.clone();
6611        let vm = self
6612            .vms
6613            .get_mut(vm_id)
6614            .ok_or_else(|| missing_vm_error(vm_id))?;
6615        let phase_start = Instant::now();
6616        let (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd) = if resolved
6617            .tool_command
6618        {
6619            let tool_resolution = resolve_tool_command(
6620                vm,
6621                &resolved.command,
6622                &resolved.execution_args,
6623                Some(&resolved.guest_cwd),
6624            )?
6625            .ok_or_else(|| {
6626                SidecarError::InvalidState(format!(
6627                    "tool command no longer resolves: {}",
6628                    resolved.command
6629                ))
6630            })?;
6631            let kernel_handle = vm
6632                .kernel
6633                .create_virtual_process(
6634                    EXECUTION_DRIVER_NAME,
6635                    TOOL_DRIVER_NAME,
6636                    &resolved.command,
6637                    resolved.process_args.clone(),
6638                    VirtualProcessOptions {
6639                        parent_pid: Some(parent_kernel_pid),
6640                        env: resolved.env.clone(),
6641                        cwd: Some(resolved.guest_cwd.clone()),
6642                    },
6643                )
6644                .map_err(kernel_error)?;
6645            let kernel_pid = kernel_handle.pid();
6646            let tool_execution = ToolExecution::default();
6647            let cancelled = tool_execution.cancelled.clone();
6648            let pending_events = tool_execution.pending_events.clone();
6649            let events_overflowed = tool_execution.events_overflowed.clone();
6650            spawn_tool_process_events(ToolProcessEventRequest {
6651                sidecar_requests: sidecar_requests.clone(),
6652                connection_id: vm.connection_id.clone(),
6653                session_id: vm.session_id.clone(),
6654                vm_id: vm_id.to_owned(),
6655                tool_resolution,
6656                cancelled,
6657                pending_events,
6658                events_overflowed,
6659            });
6660            (
6661                kernel_pid,
6662                kernel_handle,
6663                ActiveExecution::Tool(tool_execution),
6664                None,
6665            )
6666        } else {
6667            let kernel_command = match resolved.runtime {
6668                GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND,
6669                GuestRuntimeKind::WebAssembly => WASM_COMMAND,
6670                GuestRuntimeKind::Python => PYTHON_COMMAND,
6671            };
6672            let kernel_handle = vm
6673                .kernel
6674                .spawn_process(
6675                    kernel_command,
6676                    resolved.process_args.clone(),
6677                    SpawnOptions {
6678                        requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
6679                        parent_pid: Some(parent_kernel_pid),
6680                        env: resolved.env.clone(),
6681                        cwd: Some(resolved.guest_cwd.clone()),
6682                    },
6683                )
6684                .map_err(kernel_error)?;
6685            let kernel_pid = kernel_handle.pid();
6686            if request.options.detached {
6687                vm.kernel
6688                    .setsid(EXECUTION_DRIVER_NAME, kernel_pid)
6689                    .map_err(kernel_error)?;
6690            }
6691            let mut execution_env = resolved.env.clone();
6692            execution_env.insert(
6693                String::from(EXECUTION_SANDBOX_ROOT_ENV),
6694                normalize_host_path(&vm.cwd).to_string_lossy().into_owned(),
6695            );
6696
6697            let execution = match resolved.runtime {
6698                GuestRuntimeKind::JavaScript => {
6699                    execution_env.extend(sanitize_javascript_child_process_internal_bootstrap_env(
6700                        &request.options.internal_bootstrap_env,
6701                    ));
6702                    execution_env.insert(
6703                        String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
6704                        String::from("1"),
6705                    );
6706                    let context =
6707                        self.javascript_engine
6708                            .create_context(CreateJavascriptContextRequest {
6709                                vm_id: vm_id.to_owned(),
6710                                bootstrap_module: None,
6711                                compile_cache_root: Some(
6712                                    self.cache_root.join("node-compile-cache"),
6713                                ),
6714                            });
6715                    let inline_code = load_javascript_entrypoint_source(
6716                        vm,
6717                        &resolved.host_cwd,
6718                        &resolved.entrypoint,
6719                        &execution_env,
6720                    );
6721                    prepare_javascript_shadow(vm, &resolved)?;
6722
6723                    let built_reader = build_module_reader(vm, &resolved);
6724                    let guest_reader = built_reader.clone().map(|reader| {
6725                        Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader))
6726                            as Box<dyn GuestModuleReader>
6727                    });
6728                    let module_reader = built_reader
6729                        .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
6730                    let execution = self
6731                        .javascript_engine
6732                        .start_execution_with_module_reader(
6733                            StartJavascriptExecutionRequest {
6734                                guest_runtime: guest_runtime_identity(
6735                                    vm,
6736                                    Some(u64::from(kernel_pid)),
6737                                    Some(u64::from(parent_kernel_pid)),
6738                                ),
6739                                vm_id: vm_id.to_owned(),
6740                                context_id: context.context_id,
6741                                argv: std::iter::once(resolved.entrypoint.clone())
6742                                    .chain(resolved.execution_args.clone())
6743                                    .collect(),
6744                                env: execution_env,
6745                                cwd: resolved.host_cwd.clone(),
6746                                limits: javascript_execution_limits(vm),
6747                                inline_code,
6748                                wasm_module_bytes: None,
6749                            },
6750                            module_reader,
6751                            guest_reader,
6752                        )
6753                        .map_err(javascript_error)?;
6754                    ActiveExecution::Javascript(execution)
6755                }
6756                GuestRuntimeKind::WebAssembly => {
6757                    execution_env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
6758                    let wasm_limits = wasm_execution_limits(vm);
6759                    let wasm_guest_runtime = guest_runtime_identity(
6760                        vm,
6761                        Some(u64::from(kernel_pid)),
6762                        Some(u64::from(parent_kernel_pid)),
6763                    );
6764                    let context = self.wasm_engine.create_context(CreateWasmContextRequest {
6765                        vm_id: vm_id.to_owned(),
6766                        module_path: Some(resolved.entrypoint.clone()),
6767                    });
6768                    let execution = self
6769                        .wasm_engine
6770                        .start_execution(StartWasmExecutionRequest {
6771                            vm_id: vm_id.to_owned(),
6772                            context_id: context.context_id,
6773                            argv: resolved.process_args.clone(),
6774                            env: execution_env,
6775                            cwd: resolved.host_cwd.clone(),
6776                            permission_tier: execution_wasm_permission_tier(
6777                                resolved
6778                                    .wasm_permission_tier
6779                                    .unwrap_or(WasmPermissionTier::Full),
6780                            ),
6781                            limits: wasm_limits,
6782                            guest_runtime: wasm_guest_runtime,
6783                        })
6784                        .map_err(wasm_error)?;
6785                    ActiveExecution::Wasm(Box::new(execution))
6786                }
6787                GuestRuntimeKind::Python => {
6788                    // Nested `python` child_process: set up the Pyodide context the
6789                    // same way the top-level execute path does, so a guest shell or
6790                    // node parent can spawn `python` exactly like `node`.
6791                    let python_file_path = if execution_env.contains_key("AGENTOS_PYTHON_ARGV") {
6792                        execution_env.get("AGENTOS_PYTHON_FILE").map(PathBuf::from)
6793                    } else {
6794                        python_file_entrypoint(&resolved.entrypoint)
6795                    };
6796                    let pyodide_dist_path = self
6797                        .python_engine
6798                        .bundled_pyodide_dist_path_for_vm(vm_id)
6799                        .map_err(python_error)?;
6800                    let pyodide_cache_path = pyodide_dist_path
6801                        .parent()
6802                        .and_then(Path::parent)
6803                        .unwrap_or(pyodide_dist_path.as_path())
6804                        .join("pyodide-package-cache");
6805                    add_runtime_guest_path_mapping(
6806                        &mut execution_env,
6807                        PYTHON_PYODIDE_GUEST_ROOT,
6808                        &pyodide_dist_path,
6809                    );
6810                    add_runtime_guest_path_mapping(
6811                        &mut execution_env,
6812                        PYTHON_PYODIDE_CACHE_GUEST_ROOT,
6813                        &pyodide_cache_path,
6814                    );
6815                    add_runtime_host_access_path(
6816                        &mut execution_env,
6817                        "AGENTOS_EXTRA_FS_READ_PATHS",
6818                        &pyodide_dist_path,
6819                        true,
6820                    );
6821                    add_runtime_host_access_path(
6822                        &mut execution_env,
6823                        "AGENTOS_EXTRA_FS_READ_PATHS",
6824                        &pyodide_cache_path,
6825                        true,
6826                    );
6827                    add_runtime_host_access_path(
6828                        &mut execution_env,
6829                        "AGENTOS_EXTRA_FS_WRITE_PATHS",
6830                        &pyodide_cache_path,
6831                        false,
6832                    );
6833                    let context = self
6834                        .python_engine
6835                        .create_context(CreatePythonContextRequest {
6836                            vm_id: vm_id.to_owned(),
6837                            pyodide_dist_path,
6838                        });
6839                    let execution = self
6840                        .python_engine
6841                        .start_execution(StartPythonExecutionRequest {
6842                            vm_id: vm_id.to_owned(),
6843                            context_id: context.context_id,
6844                            code: resolved.entrypoint.clone(),
6845                            file_path: python_file_path,
6846                            env: execution_env,
6847                            cwd: resolved.host_cwd.clone(),
6848                            limits: python_execution_limits(vm),
6849                            guest_runtime: guest_runtime_identity(
6850                                vm,
6851                                Some(u64::from(kernel_pid)),
6852                                Some(u64::from(parent_kernel_pid)),
6853                            ),
6854                        })
6855                        .map_err(python_error)?;
6856                    ActiveExecution::Python(execution)
6857                }
6858            };
6859            let kernel_stdin_writer_fd = match javascript_child_process_stdin_mode(&request) {
6860                "pipe" => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
6861                "ignore" => {
6862                    vm.kernel
6863                        .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, 0)
6864                        .map_err(kernel_error)?;
6865                    None
6866                }
6867                "inherit" => None,
6868                _ => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
6869            };
6870            (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd)
6871        };
6872        record_execute_phase(
6873            "child_process_spawn_and_start_execution",
6874            phase_start.elapsed(),
6875        );
6876
6877        let phase_start = Instant::now();
6878        // Shared-terminal detection: when the child's kernel fd 1 is a PTY (the
6879        // slave inherited from a TTY shell), record who owns the host-facing
6880        // master so the child's stdio writes surface through master drains
6881        // instead of child stdout events (see `tty_master_owner`).
6882        let child_fd1_is_tty = vm
6883            .kernel
6884            .isatty(EXECUTION_DRIVER_NAME, kernel_pid, 1)
6885            .unwrap_or(false);
6886        let process = vm
6887            .active_processes
6888            .get_mut(process_id)
6889            .ok_or_else(|| missing_process_error(vm_id, process_id))?;
6890        let inherited_tty_master_owner = if child_fd1_is_tty {
6891            process
6892                .tty_master_fd
6893                .map(|master_fd| (process.kernel_pid, master_fd))
6894                .or(process.tty_master_owner)
6895        } else {
6896            None
6897        };
6898        process.child_processes.insert(
6899            child_process_id.clone(),
6900            ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
6901                .with_detached(request.options.detached)
6902                .with_guest_cwd(resolved.guest_cwd.clone())
6903                .with_env(resolved.env.clone())
6904                .with_host_cwd(resolved.host_cwd.clone()),
6905        );
6906        {
6907            let child = process
6908                .child_processes
6909                .get_mut(&child_process_id)
6910                .ok_or_else(|| {
6911                    SidecarError::InvalidState(format!(
6912                        "child process {child_process_id} disappeared during spawn"
6913                    ))
6914                })?;
6915            child.tty_master_owner = inherited_tty_master_owner;
6916            if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd {
6917                child.kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd);
6918            }
6919        }
6920        record_execute_phase("child_process_register", phase_start.elapsed());
6921        record_execute_phase("child_process_spawn_total", total_start.elapsed());
6922        Ok(json!({
6923            "childId": child_process_id,
6924            "pid": kernel_pid,
6925            "command": resolved.command,
6926            "args": resolved.process_args,
6927        }))
6928    }
6929
6930    pub(crate) fn spawn_javascript_child_process_sync(
6931        &mut self,
6932        vm_id: &str,
6933        process_id: &str,
6934        request: JavascriptChildProcessSpawnRequest,
6935        max_buffer: Option<usize>,
6936    ) -> Result<Value, SidecarError> {
6937        let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?;
6938        let timeout_deadline = request
6939            .options
6940            .timeout
6941            .map(|timeout_ms| Instant::now() + Duration::from_millis(timeout_ms));
6942        let timeout_signal = request
6943            .options
6944            .kill_signal
6945            .clone()
6946            .unwrap_or_else(|| String::from("SIGTERM"));
6947        let spawned = self.spawn_javascript_child_process(vm_id, process_id, request)?;
6948        let child_process_id = spawned
6949            .get("childId")
6950            .and_then(Value::as_str)
6951            .ok_or_else(|| {
6952                SidecarError::InvalidState(String::from(
6953                    "child_process.spawn_sync response is missing childId",
6954                ))
6955            })?
6956            .to_owned();
6957
6958        if let Some(input) = sync_input.as_deref() {
6959            self.write_javascript_child_process_stdin(vm_id, process_id, &child_process_id, input)?;
6960        }
6961        self.close_javascript_child_process_stdin(vm_id, process_id, &child_process_id)?;
6962
6963        let max_buffer = max_buffer.unwrap_or(1024 * 1024);
6964        let mut stdout = Vec::new();
6965        let mut stderr = Vec::new();
6966        let mut max_buffer_exceeded = false;
6967        let mut kill_sent = false;
6968        let mut timed_out = false;
6969
6970        let exit_code = loop {
6971            let wait_ms = if let Some(deadline) = timeout_deadline {
6972                let now = Instant::now();
6973                if now >= deadline {
6974                    if !kill_sent {
6975                        timed_out = true;
6976                        self.kill_javascript_child_process(
6977                            vm_id,
6978                            process_id,
6979                            &child_process_id,
6980                            &timeout_signal,
6981                        )?;
6982                        kill_sent = true;
6983                    }
6984                    0
6985                } else {
6986                    u64::try_from(deadline.saturating_duration_since(now).as_millis().min(50))
6987                        .unwrap_or(50)
6988                }
6989            } else {
6990                50
6991            };
6992            let event =
6993                self.poll_javascript_child_process(vm_id, process_id, &child_process_id, wait_ms)?;
6994            if event.is_null() {
6995                continue;
6996            }
6997
6998            match event.get("type").and_then(Value::as_str) {
6999                Some("stdout") => {
7000                    let chunk = javascript_sync_rpc_bytes_arg(
7001                        &[event.get("data").cloned().unwrap_or(Value::Null)],
7002                        0,
7003                        "child_process.spawn_sync stdout",
7004                    )?;
7005                    stdout.extend_from_slice(&chunk);
7006                    if stdout.len() > max_buffer && !kill_sent {
7007                        max_buffer_exceeded = true;
7008                        self.kill_javascript_child_process(
7009                            vm_id,
7010                            process_id,
7011                            &child_process_id,
7012                            "SIGTERM",
7013                        )?;
7014                        kill_sent = true;
7015                    }
7016                }
7017                Some("stderr") => {
7018                    let chunk = javascript_sync_rpc_bytes_arg(
7019                        &[event.get("data").cloned().unwrap_or(Value::Null)],
7020                        0,
7021                        "child_process.spawn_sync stderr",
7022                    )?;
7023                    stderr.extend_from_slice(&chunk);
7024                    if stderr.len() > max_buffer && !kill_sent {
7025                        max_buffer_exceeded = true;
7026                        self.kill_javascript_child_process(
7027                            vm_id,
7028                            process_id,
7029                            &child_process_id,
7030                            "SIGTERM",
7031                        )?;
7032                        kill_sent = true;
7033                    }
7034                }
7035                Some("exit") => {
7036                    break event
7037                        .get("exitCode")
7038                        .and_then(Value::as_i64)
7039                        .map(|value| value as i32)
7040                        .unwrap_or(1);
7041                }
7042                _ => {}
7043            }
7044        };
7045
7046        Ok(json!({
7047            "stdout": String::from_utf8_lossy(&stdout),
7048            "stderr": String::from_utf8_lossy(&stderr),
7049            "code": exit_code,
7050            "signal": if timed_out { Value::String(timeout_signal) } else { Value::Null },
7051            "timedOut": timed_out,
7052            "maxBufferExceeded": max_buffer_exceeded,
7053        }))
7054    }
7055
7056    fn spawn_descendant_javascript_child_process(
7057        &mut self,
7058        vm_id: &str,
7059        process_id: &str,
7060        current_process_path: &[&str],
7061        request: JavascriptChildProcessSpawnRequest,
7062    ) -> Result<Value, SidecarError> {
7063        let total_start = Instant::now();
7064        let current_process_label =
7065            Self::child_process_path_label(process_id, current_process_path);
7066        let phase_start = Instant::now();
7067        let (resolved, parent_kernel_pid) = {
7068            let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?;
7069            let root = vm
7070                .active_processes
7071                .get(process_id)
7072                .ok_or_else(|| missing_process_error(vm_id, process_id))?;
7073            let parent =
7074                Self::active_process_by_path(root, current_process_path).ok_or_else(|| {
7075                    SidecarError::InvalidState(format!(
7076                        "unknown child process path {current_process_label} during nested spawn"
7077                    ))
7078                })?;
7079            (
7080                self.resolve_javascript_child_process_execution(
7081                    vm,
7082                    &parent.env,
7083                    &parent.guest_cwd,
7084                    &parent.host_cwd,
7085                    &request,
7086                )?,
7087                parent.kernel_pid,
7088            )
7089        };
7090        record_execute_phase("child_process_resolve_execution", phase_start.elapsed());
7091
7092        let sidecar_requests = self.sidecar_requests.clone();
7093        let vm = self
7094            .vms
7095            .get_mut(vm_id)
7096            .ok_or_else(|| missing_vm_error(vm_id))?;
7097        let phase_start = Instant::now();
7098        let child_process_id = {
7099            let root = vm
7100                .active_processes
7101                .get_mut(process_id)
7102                .ok_or_else(|| missing_process_error(vm_id, process_id))?;
7103            let parent =
7104                Self::active_process_by_path_mut(root, current_process_path).ok_or_else(|| {
7105                    SidecarError::InvalidState(format!(
7106                        "unknown child process path {current_process_label} during nested spawn"
7107                    ))
7108                })?;
7109            parent.allocate_child_process_id()
7110        };
7111        let mut child_path = current_process_path.to_vec();
7112        child_path.push(child_process_id.as_str());
7113        let (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd) = if resolved
7114            .tool_command
7115        {
7116            let tool_resolution = resolve_tool_command(
7117                vm,
7118                &resolved.command,
7119                &resolved.execution_args,
7120                Some(&resolved.guest_cwd),
7121            )?
7122            .ok_or_else(|| {
7123                SidecarError::InvalidState(format!(
7124                    "tool command no longer resolves: {}",
7125                    resolved.command
7126                ))
7127            })?;
7128            let kernel_handle = vm
7129                .kernel
7130                .create_virtual_process(
7131                    EXECUTION_DRIVER_NAME,
7132                    TOOL_DRIVER_NAME,
7133                    &resolved.command,
7134                    resolved.process_args.clone(),
7135                    VirtualProcessOptions {
7136                        parent_pid: Some(parent_kernel_pid),
7137                        env: resolved.env.clone(),
7138                        cwd: Some(resolved.guest_cwd.clone()),
7139                    },
7140                )
7141                .map_err(kernel_error)?;
7142            let kernel_pid = kernel_handle.pid();
7143            let tool_execution = ToolExecution::default();
7144            let cancelled = tool_execution.cancelled.clone();
7145            let pending_events = tool_execution.pending_events.clone();
7146            let events_overflowed = tool_execution.events_overflowed.clone();
7147            spawn_tool_process_events(ToolProcessEventRequest {
7148                sidecar_requests: sidecar_requests.clone(),
7149                connection_id: vm.connection_id.clone(),
7150                session_id: vm.session_id.clone(),
7151                vm_id: vm_id.to_owned(),
7152                tool_resolution,
7153                cancelled,
7154                pending_events,
7155                events_overflowed,
7156            });
7157            (
7158                kernel_pid,
7159                kernel_handle,
7160                ActiveExecution::Tool(tool_execution),
7161                None,
7162            )
7163        } else {
7164            let kernel_command = match resolved.runtime {
7165                GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND,
7166                GuestRuntimeKind::WebAssembly => WASM_COMMAND,
7167                GuestRuntimeKind::Python => PYTHON_COMMAND,
7168            };
7169            let kernel_handle = vm
7170                .kernel
7171                .spawn_process(
7172                    kernel_command,
7173                    resolved.process_args.clone(),
7174                    SpawnOptions {
7175                        requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
7176                        parent_pid: Some(parent_kernel_pid),
7177                        env: resolved.env.clone(),
7178                        cwd: Some(resolved.guest_cwd.clone()),
7179                    },
7180                )
7181                .map_err(kernel_error)?;
7182            let kernel_pid = kernel_handle.pid();
7183            if request.options.detached {
7184                vm.kernel
7185                    .setsid(EXECUTION_DRIVER_NAME, kernel_pid)
7186                    .map_err(kernel_error)?;
7187            }
7188            let mut execution_env = resolved.env.clone();
7189            execution_env.insert(
7190                String::from(EXECUTION_SANDBOX_ROOT_ENV),
7191                normalize_host_path(&vm.cwd).to_string_lossy().into_owned(),
7192            );
7193            let execution = match resolved.runtime {
7194                GuestRuntimeKind::JavaScript => {
7195                    execution_env.extend(sanitize_javascript_child_process_internal_bootstrap_env(
7196                        &request.options.internal_bootstrap_env,
7197                    ));
7198                    execution_env.insert(
7199                        String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
7200                        String::from("1"),
7201                    );
7202                    let context =
7203                        self.javascript_engine
7204                            .create_context(CreateJavascriptContextRequest {
7205                                vm_id: vm_id.to_owned(),
7206                                bootstrap_module: None,
7207                                compile_cache_root: Some(
7208                                    self.cache_root.join("node-compile-cache"),
7209                                ),
7210                            });
7211                    let inline_code = load_javascript_entrypoint_source(
7212                        vm,
7213                        &resolved.host_cwd,
7214                        &resolved.entrypoint,
7215                        &execution_env,
7216                    );
7217                    prepare_javascript_shadow(vm, &resolved)?;
7218
7219                    let built_reader = build_module_reader(vm, &resolved);
7220                    let guest_reader = built_reader.clone().map(|reader| {
7221                        Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader))
7222                            as Box<dyn GuestModuleReader>
7223                    });
7224                    let module_reader = built_reader
7225                        .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
7226                    let execution = self
7227                        .javascript_engine
7228                        .start_execution_with_module_reader(
7229                            StartJavascriptExecutionRequest {
7230                                guest_runtime: guest_runtime_identity(
7231                                    vm,
7232                                    Some(u64::from(kernel_pid)),
7233                                    Some(u64::from(parent_kernel_pid)),
7234                                ),
7235                                vm_id: vm_id.to_owned(),
7236                                context_id: context.context_id,
7237                                argv: std::iter::once(resolved.entrypoint.clone())
7238                                    .chain(resolved.execution_args.clone())
7239                                    .collect(),
7240                                env: execution_env,
7241                                cwd: resolved.host_cwd.clone(),
7242                                limits: javascript_execution_limits(vm),
7243                                inline_code,
7244                                wasm_module_bytes: None,
7245                            },
7246                            module_reader,
7247                            guest_reader,
7248                        )
7249                        .map_err(javascript_error)?;
7250                    ActiveExecution::Javascript(execution)
7251                }
7252                GuestRuntimeKind::WebAssembly => {
7253                    execution_env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
7254                    let wasm_limits = wasm_execution_limits(vm);
7255                    let wasm_guest_runtime = guest_runtime_identity(
7256                        vm,
7257                        Some(u64::from(kernel_pid)),
7258                        Some(u64::from(parent_kernel_pid)),
7259                    );
7260                    let context = self.wasm_engine.create_context(CreateWasmContextRequest {
7261                        vm_id: vm_id.to_owned(),
7262                        module_path: Some(resolved.entrypoint.clone()),
7263                    });
7264                    let execution = self
7265                        .wasm_engine
7266                        .start_execution(StartWasmExecutionRequest {
7267                            vm_id: vm_id.to_owned(),
7268                            context_id: context.context_id,
7269                            argv: resolved.process_args.clone(),
7270                            env: execution_env,
7271                            cwd: resolved.host_cwd.clone(),
7272                            permission_tier: execution_wasm_permission_tier(
7273                                resolved
7274                                    .wasm_permission_tier
7275                                    .unwrap_or(WasmPermissionTier::Full),
7276                            ),
7277                            limits: wasm_limits,
7278                            guest_runtime: wasm_guest_runtime,
7279                        })
7280                        .map_err(wasm_error)?;
7281                    ActiveExecution::Wasm(Box::new(execution))
7282                }
7283                GuestRuntimeKind::Python => {
7284                    // Nested `python` child_process: set up the Pyodide context the
7285                    // same way the top-level execute path does, so a guest shell or
7286                    // node parent can spawn `python` exactly like `node`.
7287                    let python_file_path = if execution_env.contains_key("AGENTOS_PYTHON_ARGV") {
7288                        execution_env.get("AGENTOS_PYTHON_FILE").map(PathBuf::from)
7289                    } else {
7290                        python_file_entrypoint(&resolved.entrypoint)
7291                    };
7292                    let pyodide_dist_path = self
7293                        .python_engine
7294                        .bundled_pyodide_dist_path_for_vm(vm_id)
7295                        .map_err(python_error)?;
7296                    let pyodide_cache_path = pyodide_dist_path
7297                        .parent()
7298                        .and_then(Path::parent)
7299                        .unwrap_or(pyodide_dist_path.as_path())
7300                        .join("pyodide-package-cache");
7301                    add_runtime_guest_path_mapping(
7302                        &mut execution_env,
7303                        PYTHON_PYODIDE_GUEST_ROOT,
7304                        &pyodide_dist_path,
7305                    );
7306                    add_runtime_guest_path_mapping(
7307                        &mut execution_env,
7308                        PYTHON_PYODIDE_CACHE_GUEST_ROOT,
7309                        &pyodide_cache_path,
7310                    );
7311                    add_runtime_host_access_path(
7312                        &mut execution_env,
7313                        "AGENTOS_EXTRA_FS_READ_PATHS",
7314                        &pyodide_dist_path,
7315                        true,
7316                    );
7317                    add_runtime_host_access_path(
7318                        &mut execution_env,
7319                        "AGENTOS_EXTRA_FS_READ_PATHS",
7320                        &pyodide_cache_path,
7321                        true,
7322                    );
7323                    add_runtime_host_access_path(
7324                        &mut execution_env,
7325                        "AGENTOS_EXTRA_FS_WRITE_PATHS",
7326                        &pyodide_cache_path,
7327                        false,
7328                    );
7329                    let context = self
7330                        .python_engine
7331                        .create_context(CreatePythonContextRequest {
7332                            vm_id: vm_id.to_owned(),
7333                            pyodide_dist_path,
7334                        });
7335                    let execution = self
7336                        .python_engine
7337                        .start_execution(StartPythonExecutionRequest {
7338                            vm_id: vm_id.to_owned(),
7339                            context_id: context.context_id,
7340                            code: resolved.entrypoint.clone(),
7341                            file_path: python_file_path,
7342                            env: execution_env,
7343                            cwd: resolved.host_cwd.clone(),
7344                            limits: python_execution_limits(vm),
7345                            guest_runtime: guest_runtime_identity(
7346                                vm,
7347                                Some(u64::from(kernel_pid)),
7348                                Some(u64::from(parent_kernel_pid)),
7349                            ),
7350                        })
7351                        .map_err(python_error)?;
7352                    ActiveExecution::Python(execution)
7353                }
7354            };
7355            let kernel_stdin_writer_fd = match javascript_child_process_stdin_mode(&request) {
7356                "pipe" => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
7357                "ignore" => {
7358                    vm.kernel
7359                        .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, 0)
7360                        .map_err(kernel_error)?;
7361                    None
7362                }
7363                "inherit" => None,
7364                _ => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
7365            };
7366            (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd)
7367        };
7368        record_execute_phase(
7369            "child_process_spawn_and_start_execution",
7370            phase_start.elapsed(),
7371        );
7372
7373        let phase_start = Instant::now();
7374        let child_fd1_is_tty = vm
7375            .kernel
7376            .isatty(EXECUTION_DRIVER_NAME, kernel_pid, 1)
7377            .unwrap_or(false);
7378        let root = vm
7379            .active_processes
7380            .get_mut(process_id)
7381            .ok_or_else(|| missing_process_error(vm_id, process_id))?;
7382        let parent =
7383            Self::active_process_by_path_mut(root, current_process_path).ok_or_else(|| {
7384                SidecarError::InvalidState(format!(
7385                    "unknown child process path {current_process_label} during nested spawn"
7386                ))
7387            })?;
7388        let inherited_tty_master_owner = if child_fd1_is_tty {
7389            parent
7390                .tty_master_fd
7391                .map(|master_fd| (parent.kernel_pid, master_fd))
7392                .or(parent.tty_master_owner)
7393        } else {
7394            None
7395        };
7396        parent.child_processes.insert(
7397            child_process_id.clone(),
7398            ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
7399                .with_detached(request.options.detached)
7400                .with_guest_cwd(resolved.guest_cwd.clone())
7401                .with_env(resolved.env.clone())
7402                .with_host_cwd(resolved.host_cwd.clone()),
7403        );
7404        {
7405            let child = parent
7406                .child_processes
7407                .get_mut(&child_process_id)
7408                .ok_or_else(|| {
7409                    SidecarError::InvalidState(format!(
7410                        "child process {child_process_id} disappeared during nested spawn"
7411                    ))
7412                })?;
7413            child.tty_master_owner = inherited_tty_master_owner;
7414            if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd {
7415                child.kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd);
7416            }
7417        }
7418        record_execute_phase("child_process_register", phase_start.elapsed());
7419        record_execute_phase("child_process_spawn_total", total_start.elapsed());
7420        Ok(json!({
7421            "childId": child_process_id,
7422            "pid": kernel_pid,
7423            "command": resolved.command,
7424            "args": resolved.process_args,
7425        }))
7426    }
7427
7428    fn spawn_descendant_javascript_child_process_sync(
7429        &mut self,
7430        vm_id: &str,
7431        process_id: &str,
7432        current_process_path: &[&str],
7433        request: JavascriptChildProcessSpawnRequest,
7434        max_buffer: Option<usize>,
7435    ) -> Result<Value, SidecarError> {
7436        let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?;
7437        let timeout_deadline = request
7438            .options
7439            .timeout
7440            .map(|timeout_ms| Instant::now() + Duration::from_millis(timeout_ms));
7441        let timeout_signal = request
7442            .options
7443            .kill_signal
7444            .clone()
7445            .unwrap_or_else(|| String::from("SIGTERM"));
7446        let spawned = self.spawn_descendant_javascript_child_process(
7447            vm_id,
7448            process_id,
7449            current_process_path,
7450            request,
7451        )?;
7452        let child_process_id = spawned
7453            .get("childId")
7454            .and_then(Value::as_str)
7455            .ok_or_else(|| {
7456                SidecarError::InvalidState(String::from(
7457                    "child_process.spawn_sync response is missing childId",
7458                ))
7459            })?
7460            .to_owned();
7461
7462        if let Some(input) = sync_input.as_deref() {
7463            self.write_descendant_javascript_child_process_stdin(
7464                vm_id,
7465                process_id,
7466                current_process_path,
7467                &child_process_id,
7468                input,
7469            )?;
7470        }
7471        self.close_descendant_javascript_child_process_stdin(
7472            vm_id,
7473            process_id,
7474            current_process_path,
7475            &child_process_id,
7476        )?;
7477
7478        let max_buffer = max_buffer.unwrap_or(1024 * 1024);
7479        let mut stdout = Vec::new();
7480        let mut stderr = Vec::new();
7481        let mut max_buffer_exceeded = false;
7482        let mut kill_sent = false;
7483        let mut timed_out = false;
7484
7485        let exit_code = loop {
7486            let wait_ms = if let Some(deadline) = timeout_deadline {
7487                let now = Instant::now();
7488                if now >= deadline {
7489                    if !kill_sent {
7490                        timed_out = true;
7491                        self.kill_descendant_javascript_child_process(
7492                            vm_id,
7493                            process_id,
7494                            current_process_path,
7495                            &child_process_id,
7496                            &timeout_signal,
7497                        )?;
7498                        kill_sent = true;
7499                    }
7500                    0
7501                } else {
7502                    u64::try_from(deadline.saturating_duration_since(now).as_millis().min(50))
7503                        .unwrap_or(50)
7504                }
7505            } else {
7506                50
7507            };
7508            let event = self.poll_descendant_javascript_child_process(
7509                vm_id,
7510                process_id,
7511                current_process_path,
7512                &child_process_id,
7513                wait_ms,
7514            )?;
7515            if event.is_null() {
7516                continue;
7517            }
7518
7519            match event.get("type").and_then(Value::as_str) {
7520                Some("stdout") => {
7521                    let chunk = javascript_sync_rpc_bytes_arg(
7522                        &[event.get("data").cloned().unwrap_or(Value::Null)],
7523                        0,
7524                        "child_process.spawn_sync stdout",
7525                    )?;
7526                    stdout.extend_from_slice(&chunk);
7527                    if stdout.len() > max_buffer && !kill_sent {
7528                        max_buffer_exceeded = true;
7529                        self.kill_descendant_javascript_child_process(
7530                            vm_id,
7531                            process_id,
7532                            current_process_path,
7533                            &child_process_id,
7534                            "SIGTERM",
7535                        )?;
7536                        kill_sent = true;
7537                    }
7538                }
7539                Some("stderr") => {
7540                    let chunk = javascript_sync_rpc_bytes_arg(
7541                        &[event.get("data").cloned().unwrap_or(Value::Null)],
7542                        0,
7543                        "child_process.spawn_sync stderr",
7544                    )?;
7545                    stderr.extend_from_slice(&chunk);
7546                    if stderr.len() > max_buffer && !kill_sent {
7547                        max_buffer_exceeded = true;
7548                        self.kill_descendant_javascript_child_process(
7549                            vm_id,
7550                            process_id,
7551                            current_process_path,
7552                            &child_process_id,
7553                            "SIGTERM",
7554                        )?;
7555                        kill_sent = true;
7556                    }
7557                }
7558                Some("exit") => {
7559                    break event
7560                        .get("exitCode")
7561                        .and_then(Value::as_i64)
7562                        .map(|value| value as i32)
7563                        .unwrap_or(1);
7564                }
7565                _ => {}
7566            }
7567        };
7568
7569        Ok(json!({
7570            "stdout": String::from_utf8_lossy(&stdout),
7571            "stderr": String::from_utf8_lossy(&stderr),
7572            "code": exit_code,
7573            "signal": if timed_out { Value::String(timeout_signal) } else { Value::Null },
7574            "timedOut": timed_out,
7575            "maxBufferExceeded": max_buffer_exceeded,
7576        }))
7577    }
7578
7579    fn handle_descendant_javascript_child_process_rpc(
7580        &mut self,
7581        vm_id: &str,
7582        process_id: &str,
7583        current_process_path: &[&str],
7584        request: &JavascriptSyncRpcRequest,
7585    ) -> Result<Value, SidecarError> {
7586        match request.method.as_str() {
7587            "child_process.spawn" => {
7588                let Some(vm) = self.vms.get(vm_id) else {
7589                    return Ok(Value::Null);
7590                };
7591                let (payload, _) = parse_javascript_child_process_spawn_request(vm, &request.args)?;
7592                self.spawn_descendant_javascript_child_process(
7593                    vm_id,
7594                    process_id,
7595                    current_process_path,
7596                    payload,
7597                )
7598            }
7599            "child_process.spawn_sync" => {
7600                let Some(vm) = self.vms.get(vm_id) else {
7601                    return Ok(Value::Null);
7602                };
7603                let (payload, max_buffer) =
7604                    parse_javascript_child_process_spawn_request(vm, &request.args)?;
7605                self.spawn_descendant_javascript_child_process_sync(
7606                    vm_id,
7607                    process_id,
7608                    current_process_path,
7609                    payload,
7610                    max_buffer,
7611                )
7612            }
7613            "child_process.poll" => {
7614                let child_process_id =
7615                    javascript_sync_rpc_arg_str(&request.args, 0, "child_process.poll child id")?;
7616                let wait_ms = javascript_sync_rpc_arg_u64_optional(
7617                    &request.args,
7618                    1,
7619                    "child_process.poll wait ms",
7620                )?
7621                .unwrap_or_default();
7622                self.poll_descendant_javascript_child_process(
7623                    vm_id,
7624                    process_id,
7625                    current_process_path,
7626                    child_process_id,
7627                    wait_ms,
7628                )
7629            }
7630            "child_process.write_stdin" => {
7631                let child_process_id = javascript_sync_rpc_arg_str(
7632                    &request.args,
7633                    0,
7634                    "child_process.write_stdin child id",
7635                )?;
7636                let chunk = javascript_sync_rpc_bytes_arg(
7637                    &request.args,
7638                    1,
7639                    "child_process.write_stdin chunk",
7640                )?;
7641                self.write_descendant_javascript_child_process_stdin(
7642                    vm_id,
7643                    process_id,
7644                    current_process_path,
7645                    child_process_id,
7646                    &chunk,
7647                )?;
7648                Ok(Value::Null)
7649            }
7650            "child_process.close_stdin" => {
7651                let child_process_id = javascript_sync_rpc_arg_str(
7652                    &request.args,
7653                    0,
7654                    "child_process.close_stdin child id",
7655                )?;
7656                self.close_descendant_javascript_child_process_stdin(
7657                    vm_id,
7658                    process_id,
7659                    current_process_path,
7660                    child_process_id,
7661                )?;
7662                Ok(Value::Null)
7663            }
7664            "child_process.kill" => {
7665                let child_process_id =
7666                    javascript_sync_rpc_arg_str(&request.args, 0, "child_process.kill child id")?;
7667                let signal =
7668                    javascript_sync_rpc_arg_str(&request.args, 1, "child_process.kill signal")?;
7669                self.kill_descendant_javascript_child_process(
7670                    vm_id,
7671                    process_id,
7672                    current_process_path,
7673                    child_process_id,
7674                    signal,
7675                )?;
7676                Ok(Value::Null)
7677            }
7678            _ => Err(SidecarError::InvalidState(format!(
7679                "unsupported nested child process RPC method {}",
7680                request.method
7681            ))),
7682        }
7683    }
7684
7685    /// Deferred servicing for a CHILD's `__kernel_stdin_read` / `__kernel_poll`
7686    /// inside the child-event pump: probe readiness with a zero timeout, reply
7687    /// when ready / expired / non-blocking, otherwise park the RPC on the child
7688    /// (reply-by-token). The pump loop re-checks the parked RPC every
7689    /// iteration, so the dispatch loop never blocks in a kernel wait on the
7690    /// child's behalf. Returns false when the RPC must be serviced inline
7691    /// (non-TTY JavaScript local stdin bridge).
7692    fn service_child_kernel_wait_rpc(
7693        &mut self,
7694        vm_id: &str,
7695        process_id: &str,
7696        current_process_path: &[&str],
7697        child_process_id: &str,
7698        request: &JavascriptSyncRpcRequest,
7699    ) -> Result<bool, SidecarError> {
7700        let Some(vm) = self.vms.get_mut(vm_id) else {
7701            return Ok(true);
7702        };
7703        let kernel = &mut vm.kernel;
7704        let Some(root) = vm.active_processes.get_mut(process_id) else {
7705            return Ok(true);
7706        };
7707        let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
7708            return Ok(true);
7709        };
7710        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
7711            return Ok(true);
7712        };
7713        if request.method == "__kernel_stdin_read"
7714            && matches!(child.execution, ActiveExecution::Javascript(_))
7715            && child.tty_master_fd.is_none()
7716        {
7717            return Ok(false);
7718        }
7719        let now = Instant::now();
7720        let requested_timeout_ms = match request.method.as_str() {
7721            "__kernel_stdin_read" => parse_kernel_stdin_read_args(request)?.1,
7722            _ => u64::try_from(parse_kernel_poll_args(request)?.1).unwrap_or(0),
7723        };
7724        let deadline = match &child.deferred_kernel_wait_rpc {
7725            Some((parked, parked_deadline)) if parked.id == request.id => *parked_deadline,
7726            _ => now + Duration::from_millis(requested_timeout_ms),
7727        };
7728        let kernel_pid = child.kernel_pid;
7729        let probe = match request.method.as_str() {
7730            "__kernel_stdin_read" => {
7731                let (max_bytes, _) = parse_kernel_stdin_read_args(request)?;
7732                kernel_stdin_read_response(kernel, kernel_pid, max_bytes, Duration::ZERO)
7733            }
7734            _ => {
7735                let (fd_requests, _) = parse_kernel_poll_args(request)?;
7736                kernel_poll_response(kernel, kernel_pid, &fd_requests, 0)
7737            }
7738        };
7739        let probe = match probe {
7740            Ok(value) => value,
7741            Err(error) => {
7742                child.deferred_kernel_wait_rpc = None;
7743                child
7744                    .execution
7745                    .respond_javascript_sync_rpc_error(
7746                        request.id,
7747                        javascript_sync_rpc_error_code(&error),
7748                        error.to_string(),
7749                    )
7750                    .or_else(ignore_stale_javascript_sync_rpc_response)?;
7751                return Ok(true);
7752            }
7753        };
7754        let ready = match request.method.as_str() {
7755            "__kernel_stdin_read" => !probe.is_null(),
7756            _ => probe.get("readyCount").and_then(Value::as_u64).unwrap_or(0) > 0,
7757        };
7758        if ready || requested_timeout_ms == 0 || now >= deadline {
7759            child.deferred_kernel_wait_rpc = None;
7760            child
7761                .execution
7762                .respond_javascript_sync_rpc_response(request.id, probe.into())
7763                .or_else(ignore_stale_javascript_sync_rpc_response)?;
7764            return Ok(true);
7765        }
7766        child.deferred_kernel_wait_rpc = Some((request.clone(), deadline));
7767        Ok(true)
7768    }
7769
7770    /// Service `__kernel_stdio_write` for a process writing to the SHARED
7771    /// terminal (`tty_master_owner` set): write through the process's own PTY
7772    /// slave (line discipline applies), then drain the master and surface the
7773    /// drained bytes as the OWNER's ordered output stream — the single
7774    /// host-facing path. No child stdout event is queued, so nothing gets
7775    /// relayed (and re-rendered) by the parent shell.
7776    pub(crate) fn service_shared_tty_stdio_write(
7777        &mut self,
7778        vm_id: &str,
7779        writer_kernel_pid: u32,
7780        owner: (u32, u32),
7781        request: &JavascriptSyncRpcRequest,
7782    ) -> Result<Value, SidecarError> {
7783        let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_stdio_write fd")?;
7784        let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "__kernel_stdio_write chunk")?;
7785        if fd != 1 && fd != 2 {
7786            return Err(SidecarError::InvalidState(format!(
7787                "__kernel_stdio_write only supports fd 1/2, got {fd}"
7788            )));
7789        }
7790        let Some(vm) = self.vms.get_mut(vm_id) else {
7791            return Ok(json!(chunk.len()));
7792        };
7793        let written = if fd == 1 {
7794            vm.kernel
7795                .write_process_stdout(EXECUTION_DRIVER_NAME, writer_kernel_pid, &chunk)
7796                .map_err(kernel_error)?
7797        } else {
7798            vm.kernel
7799                .write_process_stderr(EXECUTION_DRIVER_NAME, writer_kernel_pid, &chunk)
7800                .map_err(kernel_error)?
7801        };
7802        let (owner_pid, master_fd) = owner;
7803        let mut drained: Vec<u8> = Vec::new();
7804        loop {
7805            match vm.kernel.fd_read_with_timeout_result(
7806                EXECUTION_DRIVER_NAME,
7807                owner_pid,
7808                master_fd,
7809                MAX_PTY_BUFFER_BYTES,
7810                Some(Duration::ZERO),
7811            ) {
7812                Ok(Some(bytes)) if !bytes.is_empty() => drained.extend(bytes),
7813                Ok(_) => break,
7814                Err(error) if error.code() == "EAGAIN" => break,
7815                Err(error) => return Err(kernel_error(error)),
7816            }
7817        }
7818        if !drained.is_empty() {
7819            if let Some(owner_process) = vm
7820                .active_processes
7821                .values_mut()
7822                .find(|process| process.kernel_pid == owner_pid)
7823            {
7824                owner_process
7825                    .queue_pending_execution_event(ActiveExecutionEvent::Stdout(drained))?;
7826            }
7827        }
7828        Ok(json!(written))
7829    }
7830
7831    /// Re-check a child's parked kernel-wait RPC (see
7832    /// `service_child_kernel_wait_rpc`); called once per pump-loop iteration.
7833    fn recheck_child_deferred_kernel_wait_rpc(
7834        &mut self,
7835        vm_id: &str,
7836        process_id: &str,
7837        current_process_path: &[&str],
7838        child_process_id: &str,
7839    ) -> Result<(), SidecarError> {
7840        let parked = {
7841            let Some(vm) = self.vms.get_mut(vm_id) else {
7842                return Ok(());
7843            };
7844            let Some(root) = vm.active_processes.get_mut(process_id) else {
7845                return Ok(());
7846            };
7847            let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
7848                return Ok(());
7849            };
7850            let Some(child) = parent.child_processes.get_mut(child_process_id) else {
7851                return Ok(());
7852            };
7853            child
7854                .deferred_kernel_wait_rpc
7855                .as_ref()
7856                .map(|(request, _)| request.clone())
7857        };
7858        if let Some(request) = parked {
7859            let _ = self.service_child_kernel_wait_rpc(
7860                vm_id,
7861                process_id,
7862                current_process_path,
7863                child_process_id,
7864                &request,
7865            )?;
7866        }
7867        Ok(())
7868    }
7869
7870    fn poll_descendant_javascript_child_process(
7871        &mut self,
7872        vm_id: &str,
7873        process_id: &str,
7874        current_process_path: &[&str],
7875        child_process_id: &str,
7876        wait_ms: u64,
7877    ) -> Result<Value, SidecarError> {
7878        let mut child_path = current_process_path.to_vec();
7879        child_path.push(child_process_id);
7880        let child_gone_error = || javascript_child_process_gone_error(process_id, &child_path);
7881        let deadline = Instant::now() + Duration::from_millis(wait_ms);
7882        let mut polled_once = false;
7883
7884        loop {
7885            self.drain_queued_descendant_javascript_child_process_events(
7886                vm_id,
7887                process_id,
7888                &child_path,
7889            )?;
7890            self.recheck_child_deferred_kernel_wait_rpc(
7891                vm_id,
7892                process_id,
7893                current_process_path,
7894                child_process_id,
7895            )?;
7896            enum ChildPollResult {
7897                Event(Box<Option<ActiveExecutionEvent>>),
7898                RecoverRuntimeExit,
7899                Timeout,
7900            }
7901            let wait = if wait_ms == 0 {
7902                Duration::ZERO
7903            } else {
7904                deadline.saturating_duration_since(Instant::now())
7905            };
7906            let poll_result = {
7907                let Some(vm) = self.vms.get_mut(vm_id) else {
7908                    return Ok(Value::Null);
7909                };
7910                let Some(parent) =
7911                    Self::descendant_parent_process_mut(vm, process_id, current_process_path)
7912                else {
7913                    return Err(child_gone_error());
7914                };
7915                let Some(child) = parent.child_processes.get_mut(child_process_id) else {
7916                    return Err(child_gone_error());
7917                };
7918                if let Some(event) = child.pending_execution_events.pop_front() {
7919                    ChildPollResult::Event(Box::new(Some(event)))
7920                } else if polled_once && wait.is_zero() {
7921                    ChildPollResult::Timeout
7922                } else {
7923                    polled_once = true;
7924                    match child.execution.poll_event_blocking(wait) {
7925                        Ok(Some(event)) => ChildPollResult::Event(Box::new(Some(event))),
7926                        Ok(None) => ChildPollResult::RecoverRuntimeExit,
7927                        Err(SidecarError::Execution(message))
7928                            if (child.runtime == GuestRuntimeKind::JavaScript
7929                                && closed_javascript_event_channel(&message))
7930                                || (child.runtime == GuestRuntimeKind::Python
7931                                    && closed_python_event_channel(&message))
7932                                || (child.runtime == GuestRuntimeKind::WebAssembly
7933                                    && closed_wasm_event_channel(&message)) =>
7934                        {
7935                            ChildPollResult::RecoverRuntimeExit
7936                        }
7937                        Err(error) => return Err(error),
7938                    }
7939                }
7940            };
7941            let event = match poll_result {
7942                ChildPollResult::Event(event) => *event,
7943                ChildPollResult::Timeout => return Ok(Value::Null),
7944                ChildPollResult::RecoverRuntimeExit => self
7945                    .recover_descendant_runtime_child_process_event(
7946                        vm_id,
7947                        process_id,
7948                        current_process_path,
7949                        child_process_id,
7950                        wait.as_millis().try_into().unwrap_or(u64::MAX),
7951                    )?,
7952            };
7953
7954            let Some(event) = event else {
7955                return Ok(Value::Null);
7956            };
7957
7958            match event {
7959                ActiveExecutionEvent::Stdout(chunk) => {
7960                    return Ok(json!({
7961                        "type": "stdout",
7962                        "data": javascript_sync_rpc_bytes_value(&chunk),
7963                    }));
7964                }
7965                ActiveExecutionEvent::Stderr(chunk) => {
7966                    return Ok(json!({
7967                        "type": "stderr",
7968                        "data": javascript_sync_rpc_bytes_value(&chunk),
7969                    }));
7970                }
7971                ActiveExecutionEvent::Exited(exit_code) => {
7972                    let cleanup_start = Instant::now();
7973                    let had_trailing_events = {
7974                        let Some(vm) = self.vms.get_mut(vm_id) else {
7975                            return Ok(Value::Null);
7976                        };
7977                        let Some(parent) = Self::descendant_parent_process_mut(
7978                            vm,
7979                            process_id,
7980                            current_process_path,
7981                        ) else {
7982                            return Ok(Value::Null);
7983                        };
7984                        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
7985                            return Ok(Value::Null);
7986                        };
7987                        let mut quiet_deadline = Instant::now() + PROCESS_EXIT_DRAIN_INITIAL_QUIET;
7988                        loop {
7989                            let wait = quiet_deadline.saturating_duration_since(Instant::now());
7990                            let next = poll_child_execution_after_exit(child, wait)?;
7991                            let Some(next) = next else {
7992                                break;
7993                            };
7994                            if matches!(next, ActiveExecutionEvent::Exited(_)) {
7995                                continue;
7996                            }
7997                            child.queue_pending_execution_event(next)?;
7998                            quiet_deadline = Instant::now() + PROCESS_EXIT_DRAIN_TRAILING_QUIET;
7999                        }
8000                        if !child.pending_execution_events.is_empty() {
8001                            child.queue_pending_execution_event(ActiveExecutionEvent::Exited(
8002                                exit_code,
8003                            ))?;
8004                            true
8005                        } else {
8006                            false
8007                        }
8008                    };
8009                    if had_trailing_events {
8010                        continue;
8011                    }
8012
8013                    let parent_signal_key =
8014                        Self::child_process_signal_key(process_id, current_process_path);
8015                    let Some(vm) = self.vms.get_mut(vm_id) else {
8016                        return Ok(Value::Null);
8017                    };
8018                    let signal_name = {
8019                        let Some(parent) = Self::descendant_parent_process_mut(
8020                            vm,
8021                            process_id,
8022                            current_process_path,
8023                        ) else {
8024                            return Ok(Value::Null);
8025                        };
8026                        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
8027                            return Ok(Value::Null);
8028                        };
8029                        child.pending_self_signal_exit.take().and_then(|signal| {
8030                            if exit_code == 128 + signal {
8031                                canonical_signal_name(signal).map(str::to_owned)
8032                            } else {
8033                                None
8034                            }
8035                        })
8036                    };
8037                    let (parent_runtime_pid, parent_v8_signal_session, should_signal_parent) = {
8038                        let Some(parent) =
8039                            Self::descendant_parent_process(vm, process_id, current_process_path)
8040                        else {
8041                            return Ok(Value::Null);
8042                        };
8043                        (
8044                            parent.execution.child_pid(),
8045                            parent.execution.javascript_v8_session_handle().filter(|_| {
8046                                matches!(
8047                                    &parent.execution,
8048                                    ActiveExecution::Javascript(execution)
8049                                        if execution.uses_shared_v8_runtime()
8050                                )
8051                            }),
8052                            vm.signal_states
8053                                .get(parent_signal_key)
8054                                .and_then(|handlers| handlers.get(&(libc::SIGCHLD as u32)))
8055                                .is_some_and(|registration| {
8056                                    registration.action != SignalDispositionAction::Default
8057                                }),
8058                        )
8059                    };
8060                    let Some(parent) =
8061                        Self::descendant_parent_process_mut(vm, process_id, current_process_path)
8062                    else {
8063                        return Ok(Value::Null);
8064                    };
8065                    let Some(mut child) = parent.child_processes.remove(child_process_id) else {
8066                        return Ok(Value::Null);
8067                    };
8068                    let child_process_label =
8069                        Self::child_process_path_label(process_id, &child_path);
8070                    let detached_children =
8071                        Self::adopt_detached_child_processes(&child_process_label, &mut child);
8072                    sync_process_host_writes_to_kernel(vm, &child)?;
8073                    let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness);
8074                    terminate_child_process_tree(&mut vm.kernel, &mut child, &kernel_readiness);
8075                    child.kernel_handle.finish(exit_code);
8076                    let _ = vm.kernel.wait_and_reap(child.kernel_pid);
8077                    vm.signal_states.remove(child_process_id);
8078                    for (detached_process_id, detached_child) in detached_children {
8079                        vm.detached_child_processes
8080                            .insert(detached_process_id.clone());
8081                        vm.active_processes
8082                            .insert(detached_process_id, detached_child);
8083                    }
8084                    if should_signal_parent {
8085                        if let Some(session) = parent_v8_signal_session {
8086                            dispatch_v8_session_signal_async(session, libc::SIGCHLD);
8087                        } else {
8088                            signal_runtime_process(parent_runtime_pid, libc::SIGCHLD)?;
8089                        }
8090                    }
8091                    let mut payload = Map::new();
8092                    payload.insert(String::from("type"), Value::String(String::from("exit")));
8093                    payload.insert(String::from("exitCode"), Value::from(exit_code));
8094                    if let Some(signal_name) = signal_name {
8095                        payload.insert(String::from("signal"), Value::String(signal_name));
8096                    }
8097                    record_execute_phase("child_process_exit_cleanup", cleanup_start.elapsed());
8098                    return Ok(Value::Object(payload));
8099                }
8100                ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
8101                    let mut current_child_path = current_process_path.to_vec();
8102                    current_child_path.push(child_process_id);
8103                    if matches!(
8104                        request.method.as_str(),
8105                        "__kernel_stdin_read" | "__kernel_poll"
8106                    ) && self.service_child_kernel_wait_rpc(
8107                        vm_id,
8108                        process_id,
8109                        current_process_path,
8110                        child_process_id,
8111                        &request,
8112                    )? {
8113                        // Replied immediately or parked on the child; the pump
8114                        // loop re-checks parked RPCs every iteration.
8115                        continue;
8116                    }
8117                    if request.method == "__kernel_stdio_write" {
8118                        let shared_tty = {
8119                            let Some(vm) = self.vms.get_mut(vm_id) else {
8120                                return Ok(Value::Null);
8121                            };
8122                            let Some(root) = vm.active_processes.get_mut(process_id) else {
8123                                return Ok(Value::Null);
8124                            };
8125                            let Some(parent) =
8126                                Self::active_process_by_path_mut(root, current_process_path)
8127                            else {
8128                                return Ok(Value::Null);
8129                            };
8130                            parent
8131                                .child_processes
8132                                .get(child_process_id)
8133                                .and_then(|child| {
8134                                    child
8135                                        .tty_master_owner
8136                                        .map(|owner| (child.kernel_pid, owner))
8137                                })
8138                        };
8139                        if let Some((child_kernel_pid, owner)) = shared_tty {
8140                            let response = self.service_shared_tty_stdio_write(
8141                                vm_id,
8142                                child_kernel_pid,
8143                                owner,
8144                                &request,
8145                            );
8146                            let Some(vm) = self.vms.get_mut(vm_id) else {
8147                                return Ok(Value::Null);
8148                            };
8149                            let Some(root) = vm.active_processes.get_mut(process_id) else {
8150                                return Ok(Value::Null);
8151                            };
8152                            let Some(parent) =
8153                                Self::active_process_by_path_mut(root, current_process_path)
8154                            else {
8155                                return Ok(Value::Null);
8156                            };
8157                            let Some(child) = parent.child_processes.get_mut(child_process_id)
8158                            else {
8159                                return Ok(Value::Null);
8160                            };
8161                            match response {
8162                                Ok(result) => child
8163                                    .execution
8164                                    .respond_javascript_sync_rpc_response(request.id, result.into())
8165                                    .or_else(ignore_stale_javascript_sync_rpc_response)?,
8166                                Err(error) => child
8167                                    .execution
8168                                    .respond_javascript_sync_rpc_error(
8169                                        request.id,
8170                                        javascript_sync_rpc_error_code(&error),
8171                                        error.to_string(),
8172                                    )
8173                                    .or_else(ignore_stale_javascript_sync_rpc_response)?,
8174                            }
8175                            continue;
8176                        }
8177                    }
8178                    let response = if request.method == "process.signal_state" {
8179                        let (signal, registration) =
8180                            parse_process_signal_state_request(&request.args)
8181                                .map_err(|error| SidecarError::InvalidState(error.to_string()))?;
8182                        let Some(vm) = self.vms.get_mut(vm_id) else {
8183                            return Ok(Value::Null);
8184                        };
8185                        let signal_key =
8186                            Self::child_process_signal_key(process_id, &current_child_path)
8187                                .to_owned();
8188                        apply_process_signal_state_update(
8189                            &mut vm.signal_states,
8190                            &signal_key,
8191                            signal,
8192                            registration,
8193                        );
8194                        Ok(Value::Null.into())
8195                    } else if request.method == "process.kill" {
8196                        self.handle_descendant_process_kill_rpc(
8197                            vm_id,
8198                            process_id,
8199                            current_process_path,
8200                            child_process_id,
8201                            &request,
8202                        )
8203                        .map(Into::into)
8204                    } else if request.method.starts_with("child_process.") {
8205                        self.handle_descendant_javascript_child_process_rpc(
8206                            vm_id,
8207                            process_id,
8208                            &current_child_path,
8209                            &request,
8210                        )
8211                        .map(Into::into)
8212                    } else {
8213                        let Some(vm) = self.vms.get_mut(vm_id) else {
8214                            return Ok(Value::Null);
8215                        };
8216                        let resource_limits = vm.kernel.resource_limits().clone();
8217                        let network_counts = vm_network_resource_counts(vm);
8218                        let socket_paths = build_javascript_socket_path_context(vm)?;
8219                        let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness);
8220                        let Some(root) = vm.active_processes.get_mut(process_id) else {
8221                            return Ok(Value::Null);
8222                        };
8223                        let Some(parent) =
8224                            Self::active_process_by_path_mut(root, current_process_path)
8225                        else {
8226                            return Ok(Value::Null);
8227                        };
8228                        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
8229                            return Ok(Value::Null);
8230                        };
8231                        service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
8232                            bridge: &self.bridge,
8233                            vm_id,
8234                            dns: &vm.dns,
8235                            socket_paths: &socket_paths,
8236                            kernel: &mut vm.kernel,
8237                            kernel_readiness,
8238                            process: child,
8239                            sync_request: &request,
8240                            resource_limits: &resource_limits,
8241                            network_counts,
8242                        })
8243                    };
8244
8245                    let Some(vm) = self.vms.get_mut(vm_id) else {
8246                        return Ok(Value::Null);
8247                    };
8248                    let Some(parent) =
8249                        Self::descendant_parent_process_mut(vm, process_id, current_process_path)
8250                    else {
8251                        return Ok(Value::Null);
8252                    };
8253                    let Some(child) = parent.child_processes.get_mut(child_process_id) else {
8254                        return Ok(Value::Null);
8255                    };
8256                    let parent_signal_event = response
8257                        .as_ref()
8258                        .ok()
8259                        .and_then(JavascriptSyncRpcServiceResponse::as_json)
8260                        .and_then(|result| {
8261                        let target_path_label =
8262                            Self::child_process_path_label(process_id, current_process_path);
8263                        if request.method != "process.kill"
8264                            || result.get("action").and_then(Value::as_str) != Some("user")
8265                            || result.get("targetProcessPath").and_then(Value::as_str)
8266                                != Some(target_path_label.as_str())
8267                        {
8268                            return None;
8269                        }
8270                        Some(json!({
8271                            "type": "signal",
8272                            "signal": result.get("signal").and_then(Value::as_str).unwrap_or_default(),
8273                            "number": result.get("number").and_then(Value::as_i64).unwrap_or_default(),
8274                        }))
8275                    });
8276                    match response {
8277                        Ok(result) => child
8278                            .execution
8279                            .respond_javascript_sync_rpc_response(request.id, result)
8280                            .or_else(ignore_stale_javascript_sync_rpc_response)?,
8281                        Err(error) => child
8282                            .execution
8283                            .respond_javascript_sync_rpc_error(
8284                                request.id,
8285                                javascript_sync_rpc_error_code(&error),
8286                                error.to_string(),
8287                            )
8288                            .or_else(ignore_stale_javascript_sync_rpc_response)?,
8289                    }
8290                    if let Some(event) = parent_signal_event {
8291                        return Ok(event);
8292                    }
8293                }
8294                ActiveExecutionEvent::PythonVfsRpcRequest(request) => {
8295                    // The kernel-VFS bridge is wired for top-level Python
8296                    // executions; a nested Python child (spawned by a JS/Python
8297                    // parent) cannot service VFS RPCs through this child-event
8298                    // path. Respond with a recoverable error instead of aborting
8299                    // the child, so its runner falls back to the in-isolate FS
8300                    // for the nested process — top-level Python keeps the full
8301                    // VFS root.
8302                    let Some(vm) = self.vms.get_mut(vm_id) else {
8303                        return Ok(Value::Null);
8304                    };
8305                    let Some(parent) =
8306                        Self::descendant_parent_process_mut(vm, process_id, current_process_path)
8307                    else {
8308                        return Ok(Value::Null);
8309                    };
8310                    let Some(child) = parent.child_processes.get_mut(child_process_id) else {
8311                        return Ok(Value::Null);
8312                    };
8313                    // Best-effort: deliver the "unavailable" error so the child's
8314                    // pending VFS RPC resolves and its runner falls back to the
8315                    // in-isolate FS. If delivery fails the child has already gone
8316                    // away (broken pipe / no-longer-pending), so dropping the
8317                    // result is correct here — there is nothing left to hang.
8318                    let _ = child.execution.respond_python_vfs_rpc_error(
8319                        request.id,
8320                        "ERR_AGENTOS_PYTHON_VFS_UNAVAILABLE",
8321                        "python VFS is not available for nested child processes",
8322                    );
8323                }
8324                ActiveExecutionEvent::SignalState {
8325                    signal,
8326                    registration,
8327                } => {
8328                    let Some(vm) = self.vms.get_mut(vm_id) else {
8329                        return Ok(Value::Null);
8330                    };
8331                    let signal_key =
8332                        Self::child_process_signal_key(process_id, &child_path).to_owned();
8333                    apply_process_signal_state_update(
8334                        &mut vm.signal_states,
8335                        &signal_key,
8336                        signal,
8337                        registration.clone(),
8338                    );
8339                    return Ok(json!({
8340                        "type": "signal_state",
8341                        "signal": signal,
8342                        "registration": registration,
8343                    }));
8344                }
8345            }
8346        }
8347    }
8348
8349    fn recover_descendant_runtime_child_process_event(
8350        &mut self,
8351        vm_id: &str,
8352        process_id: &str,
8353        current_process_path: &[&str],
8354        child_process_id: &str,
8355        wait_ms: u64,
8356    ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
8357        let (
8358            parent_kernel_pid,
8359            child_kernel_pid,
8360            child_runtime_pid,
8361            child_runtime,
8362            child_shared_runtime,
8363        ) = {
8364            let mut child_path = current_process_path.to_vec();
8365            child_path.push(child_process_id);
8366            let Some(vm) = self.vms.get_mut(vm_id) else {
8367                return Ok(None);
8368            };
8369            let Some(parent) =
8370                Self::descendant_parent_process_mut(vm, process_id, current_process_path)
8371            else {
8372                return Err(javascript_child_process_gone_error(process_id, &child_path));
8373            };
8374            let Some(child) = parent.child_processes.get_mut(child_process_id) else {
8375                return Err(javascript_child_process_gone_error(process_id, &child_path));
8376            };
8377            (
8378                parent.kernel_pid,
8379                child.kernel_pid,
8380                child.execution.child_pid(),
8381                child.runtime.clone(),
8382                child.execution.uses_shared_v8_runtime(),
8383            )
8384        };
8385        if child_runtime != GuestRuntimeKind::JavaScript
8386            && child_runtime != GuestRuntimeKind::Python
8387            && child_runtime != GuestRuntimeKind::WebAssembly
8388        {
8389            return Ok(None);
8390        }
8391        let wait_deadline = Instant::now() + Duration::from_millis(wait_ms.min(25));
8392        loop {
8393            let Some(vm) = self.vms.get_mut(vm_id) else {
8394                return Ok(None);
8395            };
8396            if let Some(process_info) = vm.kernel.list_processes().get(&child_kernel_pid) {
8397                if process_info.status == ProcessStatus::Exited {
8398                    return Ok(Some(ActiveExecutionEvent::Exited(
8399                        process_info.exit_code.unwrap_or(0),
8400                    )));
8401                }
8402            }
8403            if let Some(wait_result) = vm
8404                .kernel
8405                .waitpid_with_options(
8406                    EXECUTION_DRIVER_NAME,
8407                    parent_kernel_pid,
8408                    child_kernel_pid as i32,
8409                    WaitPidFlags::WNOHANG,
8410                )
8411                .map_err(kernel_error)?
8412            {
8413                return Ok(Some(ActiveExecutionEvent::Exited(wait_result.status)));
8414            }
8415
8416            if !child_shared_runtime && child_runtime_pid != 0 {
8417                if let Some(status) = runtime_child_exit_status(child_runtime_pid)? {
8418                    return Ok(Some(ActiveExecutionEvent::Exited(status)));
8419                }
8420                if !runtime_child_is_alive(child_runtime_pid)? {
8421                    return Ok(Some(ActiveExecutionEvent::Exited(0)));
8422                }
8423            }
8424            if Instant::now() >= wait_deadline {
8425                return Ok(None);
8426            }
8427            std::thread::sleep(Duration::from_millis(5));
8428        }
8429    }
8430
8431    fn write_descendant_javascript_child_process_stdin(
8432        &mut self,
8433        vm_id: &str,
8434        process_id: &str,
8435        current_process_path: &[&str],
8436        child_process_id: &str,
8437        chunk: &[u8],
8438    ) -> Result<(), SidecarError> {
8439        let mut child_path = current_process_path.to_vec();
8440        child_path.push(child_process_id);
8441        let Some(vm) = self.vms.get_mut(vm_id) else {
8442            return Err(javascript_child_process_gone_error(process_id, &child_path));
8443        };
8444        let Some(root) = vm.active_processes.get_mut(process_id) else {
8445            return Err(javascript_child_process_gone_error(process_id, &child_path));
8446        };
8447        let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
8448            return Err(javascript_child_process_gone_error(process_id, &child_path));
8449        };
8450        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
8451            return Err(javascript_child_process_gone_error(process_id, &child_path));
8452        };
8453        if let Err(error) = child.execution.write_stdin(chunk) {
8454            if is_broken_pipe_error(&error) {
8455                return Ok(());
8456            }
8457            return Err(error);
8458        }
8459        write_kernel_process_stdin(&mut vm.kernel, child, chunk)
8460    }
8461
8462    fn close_descendant_javascript_child_process_stdin(
8463        &mut self,
8464        vm_id: &str,
8465        process_id: &str,
8466        current_process_path: &[&str],
8467        child_process_id: &str,
8468    ) -> Result<(), SidecarError> {
8469        let mut child_path = current_process_path.to_vec();
8470        child_path.push(child_process_id);
8471        let Some(vm) = self.vms.get_mut(vm_id) else {
8472            return Err(javascript_child_process_gone_error(process_id, &child_path));
8473        };
8474        let Some(root) = vm.active_processes.get_mut(process_id) else {
8475            return Err(javascript_child_process_gone_error(process_id, &child_path));
8476        };
8477        let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
8478            return Err(javascript_child_process_gone_error(process_id, &child_path));
8479        };
8480        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
8481            return Err(javascript_child_process_gone_error(process_id, &child_path));
8482        };
8483        child.execution.close_stdin()?;
8484        close_kernel_process_stdin(&mut vm.kernel, child)
8485    }
8486
8487    fn kill_descendant_javascript_child_process(
8488        &mut self,
8489        vm_id: &str,
8490        process_id: &str,
8491        current_process_path: &[&str],
8492        child_process_id: &str,
8493        signal: &str,
8494    ) -> Result<(), SidecarError> {
8495        let signal_name = signal.to_owned();
8496        let signal = parse_signal(signal)?;
8497        let Some(vm) = self.vms.get_mut(vm_id) else {
8498            return Ok(());
8499        };
8500        let Some(root) = vm.active_processes.get_mut(process_id) else {
8501            return Ok(());
8502        };
8503        let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
8504            return Ok(());
8505        };
8506        let source_pid = parent.kernel_pid;
8507        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
8508            return Ok(());
8509        };
8510        terminate_tracked_child_process_for_signal(&mut vm.kernel, child, signal)?;
8511        let child_process_label = if current_process_path.is_empty() {
8512            child_process_id.to_owned()
8513        } else {
8514            format!("{}/{}", current_process_path.join("/"), child_process_id)
8515        };
8516        emit_security_audit_event(
8517            &self.bridge,
8518            vm_id,
8519            "security.process.kill",
8520            audit_fields([
8521                (String::from("source"), String::from("guest_child_process")),
8522                (String::from("source_pid"), source_pid.to_string()),
8523                (String::from("target_pid"), child.kernel_pid.to_string()),
8524                (String::from("process_id"), process_id.to_owned()),
8525                (String::from("child_process_id"), child_process_label),
8526                (String::from("signal"), signal_name),
8527            ]),
8528        );
8529        Ok(())
8530    }
8531
8532    fn handle_descendant_process_kill_rpc(
8533        &mut self,
8534        vm_id: &str,
8535        process_id: &str,
8536        current_process_path: &[&str],
8537        child_process_id: &str,
8538        request: &JavascriptSyncRpcRequest,
8539    ) -> Result<Value, SidecarError> {
8540        let target_pid = javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?;
8541        let signal_name = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?;
8542        let signal = parse_signal(signal_name)?;
8543
8544        let mut source_path = current_process_path.to_vec();
8545        source_path.push(child_process_id);
8546
8547        if signal != 0 && target_pid < 0 {
8548            let pgid = target_pid.unsigned_abs();
8549            let caller_kernel_pid = {
8550                let Some(vm) = self.vms.get(vm_id) else {
8551                    return Err(SidecarError::InvalidState(String::from(
8552                        "ESRCH: unknown VM during process.kill",
8553                    )));
8554                };
8555                let Some(root) = vm.active_processes.get(process_id) else {
8556                    return Err(SidecarError::InvalidState(format!(
8557                        "ESRCH: unknown process {process_id} during process.kill",
8558                    )));
8559                };
8560                let Some(source) = Self::active_process_by_path(root, &source_path) else {
8561                    return Err(SidecarError::InvalidState(format!(
8562                        "ESRCH: unknown child process {child_process_id} during process.kill",
8563                    )));
8564                };
8565                source.kernel_pid
8566            };
8567            let caller_is_member =
8568                self.signal_vm_process_group(vm_id, caller_kernel_pid, pgid, signal_name)?;
8569            if !caller_is_member {
8570                return Ok(Value::Null);
8571            }
8572            let Some(vm) = self.vms.get_mut(vm_id) else {
8573                return Ok(Value::Null);
8574            };
8575            let Some(root) = vm.active_processes.get_mut(process_id) else {
8576                return Ok(Value::Null);
8577            };
8578            let Some(source) = Self::active_process_by_path_mut(root, &source_path) else {
8579                return Ok(Value::Null);
8580            };
8581            source.pending_self_signal_exit = None;
8582            if !matches!(
8583                canonical_signal_name(signal),
8584                Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
8585            ) {
8586                source.pending_self_signal_exit = Some(signal);
8587            }
8588            return Ok(json!({
8589                "self": true,
8590                "action": "default",
8591            }));
8592        }
8593
8594        let Some(vm) = self.vms.get_mut(vm_id) else {
8595            return Err(SidecarError::InvalidState(String::from(
8596                "ESRCH: unknown VM during process.kill",
8597            )));
8598        };
8599
8600        if signal == 0 {
8601            vm.kernel
8602                .signal_process(EXECUTION_DRIVER_NAME, target_pid, signal)
8603                .map_err(kernel_error)?;
8604            return Ok(Value::Null);
8605        }
8606
8607        let target_kernel_pid = u32::try_from(target_pid).map_err(|_| {
8608            SidecarError::InvalidState(format!("EINVAL: invalid process pid {target_pid}"))
8609        })?;
8610        let (source_pid, located_target_path) = {
8611            let Some(root) = vm.active_processes.get(process_id) else {
8612                return Err(SidecarError::InvalidState(format!(
8613                    "ESRCH: unknown process {process_id} during process.kill",
8614                )));
8615            };
8616            let Some(source) = Self::active_process_by_path(root, &source_path) else {
8617                return Err(SidecarError::InvalidState(format!(
8618                    "ESRCH: unknown child process {child_process_id} during process.kill",
8619                )));
8620            };
8621            vm.kernel
8622                .signal_process(EXECUTION_DRIVER_NAME, target_pid, 0)
8623                .map_err(kernel_error)?;
8624            (
8625                source.kernel_pid,
8626                Self::active_process_path_by_kernel_pid(root, target_kernel_pid),
8627            )
8628        };
8629        let Some(target_path) = located_target_path else {
8630            // The target is alive but not part of this root's process tree.
8631            // Resolve it VM-wide so cross-tree pids and untracked kernel
8632            // processes still receive the signal.
8633            self.signal_vm_kernel_pid(vm_id, target_kernel_pid, signal_name)?;
8634            return Ok(Value::Null);
8635        };
8636        let Some(vm) = self.vms.get_mut(vm_id) else {
8637            return Err(SidecarError::InvalidState(String::from(
8638                "ESRCH: unknown VM during process.kill",
8639            )));
8640        };
8641
8642        if source_pid == target_kernel_pid {
8643            let Some(root) = vm.active_processes.get_mut(process_id) else {
8644                return Ok(Value::Null);
8645            };
8646            let Some(source) = Self::active_process_by_path_mut(root, &source_path) else {
8647                return Ok(Value::Null);
8648            };
8649            source.pending_self_signal_exit = None;
8650            if !matches!(
8651                canonical_signal_name(signal),
8652                Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
8653            ) {
8654                source.pending_self_signal_exit = Some(signal);
8655            }
8656            return Ok(json!({
8657                "self": true,
8658                "action": "default",
8659            }));
8660        }
8661
8662        let signal_key = target_path.last().map(String::as_str).unwrap_or(process_id);
8663        let registration = vm
8664            .signal_states
8665            .get(signal_key)
8666            .and_then(|handlers| handlers.get(&(signal as u32)))
8667            .cloned();
8668
8669        let action = match registration
8670            .as_ref()
8671            .map(|registration| &registration.action)
8672        {
8673            Some(SignalDispositionAction::Ignore) => "ignore",
8674            Some(SignalDispositionAction::User) => {
8675                let Some(root) = vm.active_processes.get_mut(process_id) else {
8676                    return Ok(Value::Null);
8677                };
8678                let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path)
8679                else {
8680                    return Err(SidecarError::InvalidState(format!(
8681                        "ESRCH: unknown process pid {target_pid}"
8682                    )));
8683                };
8684                if let Some(session) = target.execution.javascript_v8_session_handle().filter(
8685                    |_| matches!(&target.execution, ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime())
8686                        || matches!(&target.execution, ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime()),
8687                ) {
8688                    dispatch_v8_session_signal_async(session, signal);
8689                } else if !dispatch_v8_process_signal(target, signal)? {
8690                    return Err(SidecarError::InvalidState(format!(
8691                        "unsupported guest signal delivery for pid {target_pid}"
8692                    )));
8693                }
8694                "user"
8695            }
8696            Some(SignalDispositionAction::Default) | None
8697                if matches!(
8698                    canonical_signal_name(signal),
8699                    Some("SIGWINCH" | "SIGCHLD" | "SIGURG")
8700                ) =>
8701            {
8702                "ignore"
8703            }
8704            Some(SignalDispositionAction::Default) | None => {
8705                let Some(root) = vm.active_processes.get_mut(process_id) else {
8706                    return Ok(Value::Null);
8707                };
8708                let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path)
8709                else {
8710                    return Err(SidecarError::InvalidState(format!(
8711                        "ESRCH: unknown process pid {target_pid}"
8712                    )));
8713                };
8714                apply_active_process_default_signal(&mut vm.kernel, target, signal)?;
8715                "default"
8716            }
8717        };
8718
8719        let target_path_label = Self::child_process_path_label(
8720            process_id,
8721            &target_path.iter().map(String::as_str).collect::<Vec<_>>(),
8722        );
8723        emit_security_audit_event(
8724            &self.bridge,
8725            vm_id,
8726            "security.process.kill",
8727            audit_fields([
8728                (String::from("source"), String::from("guest_process")),
8729                (String::from("source_pid"), source_pid.to_string()),
8730                (String::from("target_pid"), target_pid.to_string()),
8731                (String::from("process_id"), process_id.to_owned()),
8732                (
8733                    String::from("target_process_path"),
8734                    target_path_label.clone(),
8735                ),
8736                (String::from("signal"), signal_name.to_owned()),
8737            ]),
8738        );
8739
8740        Ok(json!({
8741            "self": false,
8742            "action": action,
8743            "signal": signal_name,
8744            "number": signal,
8745            "targetProcessPath": target_path_label,
8746        }))
8747    }
8748
8749    pub(crate) fn poll_javascript_child_process(
8750        &mut self,
8751        vm_id: &str,
8752        process_id: &str,
8753        child_process_id: &str,
8754        wait_ms: u64,
8755    ) -> Result<Value, SidecarError> {
8756        self.poll_descendant_javascript_child_process(
8757            vm_id,
8758            process_id,
8759            &[],
8760            child_process_id,
8761            wait_ms,
8762        )
8763    }
8764
8765    pub(crate) fn write_javascript_child_process_stdin(
8766        &mut self,
8767        vm_id: &str,
8768        process_id: &str,
8769        child_process_id: &str,
8770        chunk: &[u8],
8771    ) -> Result<(), SidecarError> {
8772        let Some(vm) = self.vms.get_mut(vm_id) else {
8773            return Err(javascript_child_process_gone_error(
8774                process_id,
8775                &[child_process_id],
8776            ));
8777        };
8778        let Some(child) = vm
8779            .active_processes
8780            .get_mut(process_id)
8781            .ok_or_else(|| missing_process_error(vm_id, process_id))?
8782            .child_processes
8783            .get_mut(child_process_id)
8784        else {
8785            return Err(javascript_child_process_gone_error(
8786                process_id,
8787                &[child_process_id],
8788            ));
8789        };
8790        if let Err(error) = child.execution.write_stdin(chunk) {
8791            if is_broken_pipe_error(&error) {
8792                return Ok(());
8793            }
8794            return Err(error);
8795        }
8796        write_kernel_process_stdin(&mut vm.kernel, child, chunk)
8797    }
8798
8799    pub(crate) fn close_javascript_child_process_stdin(
8800        &mut self,
8801        vm_id: &str,
8802        process_id: &str,
8803        child_process_id: &str,
8804    ) -> Result<(), SidecarError> {
8805        let Some(vm) = self.vms.get_mut(vm_id) else {
8806            return Err(javascript_child_process_gone_error(
8807                process_id,
8808                &[child_process_id],
8809            ));
8810        };
8811        let Some(child) = vm
8812            .active_processes
8813            .get_mut(process_id)
8814            .ok_or_else(|| missing_process_error(vm_id, process_id))?
8815            .child_processes
8816            .get_mut(child_process_id)
8817        else {
8818            return Err(javascript_child_process_gone_error(
8819                process_id,
8820                &[child_process_id],
8821            ));
8822        };
8823        child.execution.close_stdin()?;
8824        close_kernel_process_stdin(&mut vm.kernel, child)
8825    }
8826
8827    pub(crate) fn kill_javascript_child_process(
8828        &mut self,
8829        vm_id: &str,
8830        process_id: &str,
8831        child_process_id: &str,
8832        signal: &str,
8833    ) -> Result<(), SidecarError> {
8834        let signal_name = signal.to_owned();
8835        let signal = parse_signal(signal)?;
8836        let Some(vm) = self.vms.get_mut(vm_id) else {
8837            return Ok(());
8838        };
8839        let process = vm
8840            .active_processes
8841            .get_mut(process_id)
8842            .ok_or_else(|| missing_process_error(vm_id, process_id))?;
8843        let source_pid = process.kernel_pid;
8844        let child = process
8845            .child_processes
8846            .get_mut(child_process_id)
8847            .ok_or_else(|| {
8848                SidecarError::InvalidState(format!(
8849                    "unknown child process {child_process_id} during kill"
8850                ))
8851            })?;
8852        terminate_tracked_child_process_for_signal(&mut vm.kernel, child, signal)?;
8853        emit_security_audit_event(
8854            &self.bridge,
8855            vm_id,
8856            "security.process.kill",
8857            audit_fields([
8858                (String::from("source"), String::from("guest_child_process")),
8859                (String::from("source_pid"), source_pid.to_string()),
8860                (String::from("target_pid"), child.kernel_pid.to_string()),
8861                (String::from("process_id"), process_id.to_owned()),
8862                (
8863                    String::from("child_process_id"),
8864                    child_process_id.to_owned(),
8865                ),
8866                (String::from("signal"), signal_name),
8867            ]),
8868        );
8869        Ok(())
8870    }
8871
8872    /// Delivers a signal to one kernel pid inside a VM, resolving the target
8873    /// through the active-process tree first so tracked sidecar executions get
8874    /// the same termination handling as a direct `child_process.kill`.
8875    /// Untracked kernel processes (for example WASM subprocess trees) receive
8876    /// the signal through the kernel process table directly.
8877    pub(crate) fn signal_vm_kernel_pid(
8878        &mut self,
8879        vm_id: &str,
8880        target_kernel_pid: u32,
8881        signal_name: &str,
8882    ) -> Result<(), SidecarError> {
8883        let signal = parse_signal(signal_name)?;
8884        let located = {
8885            let Some(vm) = self.vms.get(vm_id) else {
8886                return Err(SidecarError::InvalidState(String::from(
8887                    "ESRCH: unknown VM during process.kill",
8888                )));
8889            };
8890            let alive = vm
8891                .kernel
8892                .list_processes()
8893                .get(&target_kernel_pid)
8894                .is_some_and(|info| info.status != ProcessStatus::Exited);
8895            if !alive {
8896                return Err(SidecarError::InvalidState(format!(
8897                    "ESRCH: no such process {target_kernel_pid}"
8898                )));
8899            }
8900            vm.active_processes.iter().find_map(|(process_id, root)| {
8901                Self::active_process_path_by_kernel_pid(root, target_kernel_pid)
8902                    .map(|path| (process_id.clone(), path))
8903            })
8904        };
8905
8906        match located {
8907            Some((process_id, path)) if path.is_empty() => {
8908                self.kill_process_internal(vm_id, &process_id, signal_name)
8909            }
8910            Some((process_id, path)) => {
8911                let Some(vm) = self.vms.get_mut(vm_id) else {
8912                    return Ok(());
8913                };
8914                let Some(root) = vm.active_processes.get_mut(&process_id) else {
8915                    return Ok(());
8916                };
8917                let Some(target) = Self::active_process_by_owned_path_mut(root, &path) else {
8918                    return Err(SidecarError::InvalidState(format!(
8919                        "ESRCH: no such process {target_kernel_pid}"
8920                    )));
8921                };
8922                terminate_tracked_child_process_for_signal(&mut vm.kernel, target, signal)?;
8923                emit_security_audit_event(
8924                    &self.bridge,
8925                    vm_id,
8926                    "security.process.kill",
8927                    audit_fields([
8928                        (String::from("source"), String::from("guest_process")),
8929                        (String::from("target_pid"), target_kernel_pid.to_string()),
8930                        (String::from("process_id"), process_id),
8931                        (String::from("signal"), signal_name.to_owned()),
8932                    ]),
8933                );
8934                Ok(())
8935            }
8936            None => {
8937                let Some(vm) = self.vms.get_mut(vm_id) else {
8938                    return Ok(());
8939                };
8940                let target_pid = i32::try_from(target_kernel_pid).map_err(|_| {
8941                    SidecarError::InvalidState(format!(
8942                        "EINVAL: invalid process pid {target_kernel_pid}"
8943                    ))
8944                })?;
8945                vm.kernel
8946                    .signal_process(EXECUTION_DRIVER_NAME, target_pid, signal)
8947                    .map_err(kernel_error)?;
8948                emit_security_audit_event(
8949                    &self.bridge,
8950                    vm_id,
8951                    "security.process.kill",
8952                    audit_fields([
8953                        (String::from("source"), String::from("guest_process")),
8954                        (String::from("target_pid"), target_kernel_pid.to_string()),
8955                        (String::from("signal"), signal_name.to_owned()),
8956                    ]),
8957                );
8958                Ok(())
8959            }
8960        }
8961    }
8962
8963    /// Delivers a signal to every live member of a VM process group, matching
8964    /// Linux `kill(-pgid, sig)` semantics. Returns whether the caller itself
8965    /// is a member of the group so entry points can apply self-signal
8966    /// delivery; the caller is intentionally skipped here.
8967    pub(crate) fn signal_vm_process_group(
8968        &mut self,
8969        vm_id: &str,
8970        caller_kernel_pid: u32,
8971        pgid: u32,
8972        signal_name: &str,
8973    ) -> Result<bool, SidecarError> {
8974        parse_signal(signal_name)?;
8975        let members = {
8976            let Some(vm) = self.vms.get(vm_id) else {
8977                return Err(SidecarError::InvalidState(String::from(
8978                    "ESRCH: unknown VM during process.kill",
8979                )));
8980            };
8981            vm.kernel
8982                .list_processes()
8983                .into_iter()
8984                .filter(|(_, info)| info.pgid == pgid && info.status != ProcessStatus::Exited)
8985                .map(|(pid, _)| pid)
8986                .collect::<Vec<_>>()
8987        };
8988        if members.is_empty() {
8989            return Err(SidecarError::InvalidState(format!(
8990                "ESRCH: no such process group {pgid}"
8991            )));
8992        }
8993
8994        let mut caller_is_member = false;
8995        for member_pid in members {
8996            if member_pid == caller_kernel_pid {
8997                caller_is_member = true;
8998                continue;
8999            }
9000            match self.signal_vm_kernel_pid(vm_id, member_pid, signal_name) {
9001                Ok(()) => {}
9002                // Group members can exit while the group is being signaled. A
9003                // vanished member is not an error for the group kill overall.
9004                Err(error) if sidecar_error_is_esrch(&error) => {}
9005                Err(error) => return Err(error),
9006            }
9007        }
9008        Ok(caller_is_member)
9009    }
9010}
9011
9012/// Applies a kill signal to a tracked child execution. Shared-runtime
9013/// executions for lethal signals are terminated directly with a synthetic
9014/// signal exit so child polls observe a prompt close; everything else routes
9015/// through the kernel process table.
9016fn terminate_tracked_child_process_for_signal(
9017    kernel: &mut SidecarKernel,
9018    child: &mut ActiveProcess,
9019    signal: i32,
9020) -> Result<(), SidecarError> {
9021    let should_terminate_shared_runtime = child.execution.uses_shared_v8_runtime()
9022        && signal != 0
9023        && !matches!(
9024            signal,
9025            libc::SIGHUP
9026                | libc::SIGINT
9027                | libc::SIGTERM
9028                | libc::SIGCHLD
9029                | libc::SIGWINCH
9030                | libc::SIGSTOP
9031                | libc::SIGCONT
9032        );
9033    if should_terminate_shared_runtime {
9034        child.execution.terminate()?;
9035        child.pending_self_signal_exit = Some(signal);
9036        child.queue_pending_execution_event(ActiveExecutionEvent::Exited(128 + signal))?;
9037    } else {
9038        kernel
9039            .kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, signal)
9040            .map_err(kernel_error)?;
9041    }
9042    Ok(())
9043}
9044
9045fn sidecar_error_is_esrch(error: &SidecarError) -> bool {
9046    error.to_string().contains("ESRCH")
9047}
9048
9049fn apply_active_process_default_signal(
9050    kernel: &mut SidecarKernel,
9051    process: &mut ActiveProcess,
9052    signal: i32,
9053) -> Result<(), SidecarError> {
9054    if matches!(signal, libc::SIGSTOP | libc::SIGCONT) {
9055        return kernel
9056            .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal)
9057            .map_err(kernel_error);
9058    }
9059
9060    if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
9061        close_kernel_process_stdin(kernel, process)?;
9062    }
9063
9064    if process.execution.uses_shared_v8_runtime() {
9065        process.execution.terminate()?;
9066        if signal != 0 && matches!(process.execution, ActiveExecution::Wasm(_)) {
9067            process.queue_pending_execution_event(ActiveExecutionEvent::Exited(128 + signal))?;
9068        }
9069        return Ok(());
9070    }
9071
9072    kernel
9073        .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal)
9074        .map_err(kernel_error)
9075}
9076
9077fn map_wasm_signal_registration(
9078    registration: secure_exec_execution::wasm::WasmSignalHandlerRegistration,
9079) -> SignalHandlerRegistration {
9080    SignalHandlerRegistration {
9081        action: match registration.action {
9082            secure_exec_execution::wasm::WasmSignalDispositionAction::Default => {
9083                crate::protocol::SignalDispositionAction::Default
9084            }
9085            secure_exec_execution::wasm::WasmSignalDispositionAction::Ignore => {
9086                crate::protocol::SignalDispositionAction::Ignore
9087            }
9088            secure_exec_execution::wasm::WasmSignalDispositionAction::User => {
9089                crate::protocol::SignalDispositionAction::User
9090            }
9091        },
9092        mask: registration.mask,
9093        flags: registration.flags,
9094    }
9095}
9096
9097fn map_node_signal_registration(
9098    registration: NodeSignalHandlerRegistration,
9099) -> SignalHandlerRegistration {
9100    SignalHandlerRegistration {
9101        action: match registration.action {
9102            NodeSignalDispositionAction::Default => SignalDispositionAction::Default,
9103            NodeSignalDispositionAction::Ignore => SignalDispositionAction::Ignore,
9104            NodeSignalDispositionAction::User => SignalDispositionAction::User,
9105        },
9106        mask: registration.mask,
9107        flags: registration.flags,
9108    }
9109}
9110
9111fn javascript_child_process_sync_input_bytes(
9112    value: Option<&Value>,
9113) -> Result<Option<Vec<u8>>, SidecarError> {
9114    let Some(value) = value else {
9115        return Ok(None);
9116    };
9117
9118    match value {
9119        Value::Null => Ok(None),
9120        Value::String(text) => Ok(Some(text.as_bytes().to_vec())),
9121        other => javascript_sync_rpc_bytes_arg(
9122            std::slice::from_ref(other),
9123            0,
9124            "child_process.spawn_sync input",
9125        )
9126        .map(Some),
9127    }
9128}
9129
9130// bridge_permissions moved to crate::bridge
9131
9132// reconcile_mounts, resolve_cwd moved to crate::vm
9133
9134fn resolve_execute_request(
9135    vm: &VmState,
9136    payload: &ExecuteRequest,
9137) -> Result<ResolvedChildProcessExecution, SidecarError> {
9138    let payload_env: BTreeMap<String, String> = payload
9139        .env
9140        .iter()
9141        .map(|(k, v)| (k.clone(), v.clone()))
9142        .collect();
9143    if let Some(command) = payload.command.as_deref() {
9144        return resolve_command_execution(
9145            vm,
9146            command,
9147            &payload.args,
9148            &payload_env,
9149            payload.cwd.as_deref(),
9150            payload.wasm_permission_tier,
9151        );
9152    }
9153
9154    let runtime = payload.runtime.clone().ok_or_else(|| {
9155        SidecarError::InvalidState(String::from("execute requires either command or runtime"))
9156    })?;
9157    let entrypoint = payload.entrypoint.clone().ok_or_else(|| {
9158        SidecarError::InvalidState(String::from(
9159            "execute requires either command or entrypoint",
9160        ))
9161    })?;
9162    let (guest_cwd, host_cwd, allow_host_path_overrides) =
9163        resolve_execution_cwds(vm, payload.cwd.as_deref());
9164    let mut env = vm.guest_env.clone();
9165    env.extend(payload_env.clone());
9166
9167    let requested_host_entrypoint = resolve_host_entrypoint_within_vm_host_cwd(vm, &entrypoint);
9168    if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
9169        let requested_cwd = payload.cwd.as_deref().unwrap_or(guest_cwd.as_str());
9170        return Err(SidecarError::InvalidState(format!(
9171            "execution cwd {requested_cwd} is outside sandbox root {}",
9172            vm.host_cwd.to_string_lossy()
9173        )));
9174    }
9175    let host_entrypoint_override = allow_host_path_overrides
9176        .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, &entrypoint))
9177        .flatten();
9178
9179    let guest_entrypoint = host_entrypoint_override
9180        .as_ref()
9181        .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
9182        .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, &entrypoint));
9183    prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
9184
9185    Ok(ResolvedChildProcessExecution {
9186        command: match runtime {
9187            GuestRuntimeKind::JavaScript => String::from(JAVASCRIPT_COMMAND),
9188            GuestRuntimeKind::Python => String::from(PYTHON_COMMAND),
9189            GuestRuntimeKind::WebAssembly => String::from(WASM_COMMAND),
9190        },
9191        process_args: std::iter::once(entrypoint.clone())
9192            .chain(payload.args.iter().cloned())
9193            .collect(),
9194        runtime,
9195        entrypoint: host_entrypoint_override
9196            .map(|(_, host_entrypoint)| host_entrypoint)
9197            .unwrap_or(entrypoint),
9198        execution_args: payload.args.clone(),
9199        env,
9200        guest_cwd,
9201        host_cwd,
9202        wasm_permission_tier: payload.wasm_permission_tier,
9203        tool_command: false,
9204    })
9205}
9206
9207fn resolve_command_execution(
9208    vm: &VmState,
9209    command: &str,
9210    args: &[String],
9211    extra_env: &BTreeMap<String, String>,
9212    cwd: Option<&str>,
9213    explicit_wasm_permission_tier: Option<WasmPermissionTier>,
9214) -> Result<ResolvedChildProcessExecution, SidecarError> {
9215    let (guest_cwd, host_cwd, allow_host_path_overrides) = resolve_execution_cwds(vm, cwd);
9216    let mut env = vm.guest_env.clone();
9217    env.extend(extra_env.clone());
9218    let args = apply_shell_cwd_prefix(command, args.to_vec(), &guest_cwd);
9219
9220    if is_tool_command(vm, command) {
9221        let command = normalized_tool_command_name(command).unwrap_or_else(|| command.to_owned());
9222        return Ok(ResolvedChildProcessExecution {
9223            command: command.clone(),
9224            process_args: std::iter::once(command.clone())
9225                .chain(args.iter().cloned())
9226                .collect(),
9227            runtime: GuestRuntimeKind::JavaScript,
9228            entrypoint: command,
9229            execution_args: args,
9230            env,
9231            guest_cwd,
9232            host_cwd,
9233            wasm_permission_tier: None,
9234            tool_command: true,
9235        });
9236    }
9237
9238    if is_python_runtime_command(command) {
9239        return resolve_python_command_execution(vm, command, &args, env, guest_cwd, host_cwd);
9240    }
9241
9242    if is_node_runtime_command(command) {
9243        if let Some(cli) = resolve_host_node_cli_entrypoint(command) {
9244            env.insert(
9245                String::from("AGENTOS_NODE_EVAL"),
9246                build_host_node_cli_eval(&cli),
9247            );
9248            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
9249            add_runtime_guest_path_mapping(&mut env, &cli.guest_root, &cli.package_root);
9250            add_runtime_host_access_path(
9251                &mut env,
9252                "AGENTOS_EXTRA_FS_READ_PATHS",
9253                &cli.package_root,
9254                true,
9255            );
9256
9257            return Ok(ResolvedChildProcessExecution {
9258                command: String::from(JAVASCRIPT_COMMAND),
9259                process_args: std::iter::once(command.to_owned())
9260                    .chain(args.iter().cloned())
9261                    .collect(),
9262                runtime: GuestRuntimeKind::JavaScript,
9263                entrypoint: String::from("-e"),
9264                execution_args: std::iter::once(cli.guest_entrypoint.clone())
9265                    .chain(args.iter().cloned())
9266                    .collect(),
9267                env,
9268                guest_cwd,
9269                host_cwd,
9270                wasm_permission_tier: None,
9271                tool_command: false,
9272            });
9273        }
9274
9275        if args.is_empty() {
9276            env.insert(String::from("AGENTOS_NODE_EVAL"), String::new());
9277            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
9278
9279            return Ok(ResolvedChildProcessExecution {
9280                command: String::from(JAVASCRIPT_COMMAND),
9281                process_args: vec![command.to_owned()],
9282                runtime: GuestRuntimeKind::JavaScript,
9283                entrypoint: String::from("-e"),
9284                execution_args: Vec::new(),
9285                env,
9286                guest_cwd,
9287                host_cwd,
9288                wasm_permission_tier: None,
9289                tool_command: false,
9290            });
9291        }
9292
9293        if let Some((entrypoint, execution_args)) =
9294            resolve_special_node_cli_invocation(&args, &mut env)
9295        {
9296            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
9297
9298            return Ok(ResolvedChildProcessExecution {
9299                command: String::from(JAVASCRIPT_COMMAND),
9300                process_args: std::iter::once(command.to_owned())
9301                    .chain(args.iter().cloned())
9302                    .collect(),
9303                runtime: GuestRuntimeKind::JavaScript,
9304                entrypoint,
9305                execution_args,
9306                env,
9307                guest_cwd,
9308                host_cwd,
9309                wasm_permission_tier: None,
9310                tool_command: false,
9311            });
9312        }
9313
9314        let Some(entrypoint_specifier) = args.first() else {
9315            return Err(SidecarError::InvalidState(format!(
9316                "{command} execution requires an entrypoint"
9317            )));
9318        };
9319
9320        let (entrypoint, execution_args, guest_entrypoint) = {
9321            let requested_host_entrypoint =
9322                resolve_host_entrypoint_within_vm_host_cwd(vm, entrypoint_specifier);
9323            if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
9324                let requested_cwd = cwd.unwrap_or(guest_cwd.as_str());
9325                return Err(SidecarError::InvalidState(format!(
9326                    "execution cwd {requested_cwd} is outside sandbox root {}",
9327                    vm.host_cwd.to_string_lossy()
9328                )));
9329            }
9330            let host_entrypoint_override = allow_host_path_overrides
9331                .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, entrypoint_specifier))
9332                .flatten();
9333            let guest_entrypoint = host_entrypoint_override
9334                .as_ref()
9335                .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
9336                .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, entrypoint_specifier));
9337            let entrypoint = host_entrypoint_override.map_or_else(
9338                || {
9339                    guest_entrypoint.as_ref().map_or_else(
9340                        || entrypoint_specifier.clone(),
9341                        |guest_entrypoint| {
9342                            resolve_vm_guest_path_to_host(vm, guest_entrypoint)
9343                                .to_string_lossy()
9344                                .into_owned()
9345                        },
9346                    )
9347                },
9348                |(_, host_entrypoint)| host_entrypoint,
9349            );
9350            (
9351                entrypoint,
9352                args.iter().skip(1).cloned().collect(),
9353                guest_entrypoint,
9354            )
9355        };
9356
9357        prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
9358
9359        return Ok(ResolvedChildProcessExecution {
9360            command: String::from(JAVASCRIPT_COMMAND),
9361            process_args: std::iter::once(command.to_owned())
9362                .chain(args.iter().cloned())
9363                .collect(),
9364            runtime: GuestRuntimeKind::JavaScript,
9365            entrypoint,
9366            execution_args,
9367            env,
9368            guest_cwd,
9369            host_cwd,
9370            wasm_permission_tier: None,
9371            tool_command: false,
9372        });
9373    }
9374
9375    if command.ends_with(".js") || command.ends_with(".mjs") || command.ends_with(".cjs") {
9376        let requested_host_entrypoint = resolve_host_entrypoint_within_vm_host_cwd(vm, command);
9377        if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
9378            let requested_cwd = cwd.unwrap_or(guest_cwd.as_str());
9379            return Err(SidecarError::InvalidState(format!(
9380                "execution cwd {requested_cwd} is outside sandbox root {}",
9381                vm.host_cwd.to_string_lossy()
9382            )));
9383        }
9384        let host_entrypoint_override = allow_host_path_overrides
9385            .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, command))
9386            .flatten();
9387        let guest_entrypoint = host_entrypoint_override
9388            .as_ref()
9389            .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
9390            .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, command));
9391        let entrypoint = host_entrypoint_override.map_or_else(
9392            || {
9393                guest_entrypoint.as_ref().map_or_else(
9394                    || command.to_owned(),
9395                    |guest_entrypoint| {
9396                        resolve_vm_guest_path_to_host(vm, guest_entrypoint)
9397                            .to_string_lossy()
9398                            .into_owned()
9399                    },
9400                )
9401            },
9402            |(_, host_entrypoint)| host_entrypoint,
9403        );
9404        prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
9405
9406        return Ok(ResolvedChildProcessExecution {
9407            command: String::from(JAVASCRIPT_COMMAND),
9408            process_args: std::iter::once(command.to_owned())
9409                .chain(args.iter().cloned())
9410                .collect(),
9411            runtime: GuestRuntimeKind::JavaScript,
9412            entrypoint,
9413            execution_args: args.to_vec(),
9414            env,
9415            guest_cwd,
9416            host_cwd,
9417            wasm_permission_tier: None,
9418            tool_command: false,
9419        });
9420    }
9421
9422    let guest_entrypoint = resolve_guest_command_entrypoint(
9423        vm,
9424        &guest_cwd,
9425        command,
9426        env.get("PATH").map(String::as_str),
9427    )
9428    .ok_or_else(|| {
9429        SidecarError::InvalidState(format!(
9430            "command not found on native sidecar path: {command}"
9431        ))
9432    })?;
9433    let wasm_permission_tier = explicit_wasm_permission_tier
9434        .or_else(|| vm.command_permissions.get(command).copied())
9435        .or_else(|| {
9436            Path::new(&guest_entrypoint)
9437                .file_name()
9438                .and_then(|name| name.to_str())
9439                .and_then(|name| vm.command_permissions.get(name).copied())
9440        });
9441
9442    let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint);
9443    if let Some((javascript_guest_entrypoint, javascript_host_entrypoint)) =
9444        resolve_javascript_command_entrypoint(vm, &guest_entrypoint, &host_entrypoint)
9445    {
9446        prepare_guest_runtime_env(
9447            vm,
9448            &mut env,
9449            &guest_cwd,
9450            &host_cwd,
9451            Some(javascript_guest_entrypoint),
9452        )?;
9453
9454        return Ok(ResolvedChildProcessExecution {
9455            command: command.to_owned(),
9456            process_args: std::iter::once(command.to_owned())
9457                .chain(args.iter().cloned())
9458                .collect(),
9459            runtime: GuestRuntimeKind::JavaScript,
9460            entrypoint: javascript_host_entrypoint.to_string_lossy().into_owned(),
9461            execution_args: args.to_vec(),
9462            env,
9463            guest_cwd,
9464            host_cwd,
9465            wasm_permission_tier: None,
9466            tool_command: false,
9467        });
9468    }
9469    prepare_guest_runtime_env(
9470        vm,
9471        &mut env,
9472        &guest_cwd,
9473        &host_cwd,
9474        Some(guest_entrypoint.clone()),
9475    )?;
9476
9477    Ok(ResolvedChildProcessExecution {
9478        command: command.to_owned(),
9479        process_args: std::iter::once(command.to_owned())
9480            .chain(args.iter().cloned())
9481            .collect(),
9482        runtime: GuestRuntimeKind::WebAssembly,
9483        entrypoint: host_entrypoint.to_string_lossy().into_owned(),
9484        execution_args: args.to_vec(),
9485        env,
9486        guest_cwd,
9487        host_cwd,
9488        wasm_permission_tier,
9489        tool_command: false,
9490    })
9491}
9492
9493const MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH: usize = 4;
9494
9495fn resolve_javascript_command_entrypoint(
9496    vm: &VmState,
9497    guest_entrypoint: &str,
9498    host_entrypoint: &Path,
9499) -> Option<(String, PathBuf)> {
9500    resolve_javascript_command_entrypoint_inner(
9501        vm,
9502        guest_entrypoint,
9503        host_entrypoint,
9504        MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH,
9505    )
9506}
9507
9508fn resolve_javascript_command_entrypoint_inner(
9509    vm: &VmState,
9510    guest_entrypoint: &str,
9511    host_entrypoint: &Path,
9512    redirects_remaining: usize,
9513) -> Option<(String, PathBuf)> {
9514    if redirects_remaining > 0 {
9515        let symlink_target = fs::symlink_metadata(host_entrypoint)
9516            .ok()
9517            .filter(|metadata| metadata.file_type().is_symlink())
9518            .and_then(|_| fs::read_link(host_entrypoint).ok());
9519        if let Some(symlink_target) = symlink_target {
9520            let guest_parent = Path::new(guest_entrypoint)
9521                .parent()
9522                .and_then(|path| path.to_str())
9523                .unwrap_or("/");
9524            let symlink_guest_entrypoint = if symlink_target.is_absolute() {
9525                normalize_path(&symlink_target.to_string_lossy())
9526            } else {
9527                normalize_path(&format!(
9528                    "{guest_parent}/{}",
9529                    symlink_target.to_string_lossy().replace('\\', "/")
9530                ))
9531            };
9532            let symlink_host_entrypoint =
9533                resolve_vm_guest_path_to_host(vm, &symlink_guest_entrypoint);
9534            return resolve_javascript_command_entrypoint_inner(
9535                vm,
9536                &symlink_guest_entrypoint,
9537                &symlink_host_entrypoint,
9538                redirects_remaining - 1,
9539            );
9540        }
9541    }
9542
9543    let script = load_executable_script_preview(host_entrypoint)?;
9544    let interpreter = parse_script_interpreter_name(&script);
9545
9546    if interpreter.is_none() && is_probable_javascript_entrypoint(host_entrypoint, &script) {
9547        return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf()));
9548    }
9549
9550    let interpreter = interpreter?;
9551    if interpreter == "node" {
9552        return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf()));
9553    }
9554
9555    if redirects_remaining == 0 || !matches!(interpreter.as_str(), "sh" | "bash" | "dash") {
9556        return None;
9557    }
9558
9559    let shim_target = parse_node_shell_shim_target(&script)?;
9560    let guest_parent = Path::new(guest_entrypoint)
9561        .parent()
9562        .and_then(|path| path.to_str())
9563        .unwrap_or("/");
9564    let shim_guest_entrypoint = normalize_path(&format!("{guest_parent}/{shim_target}"));
9565    let shim_host_entrypoint = resolve_vm_guest_path_to_host(vm, &shim_guest_entrypoint);
9566    resolve_javascript_command_entrypoint_inner(
9567        vm,
9568        &shim_guest_entrypoint,
9569        &shim_host_entrypoint,
9570        redirects_remaining - 1,
9571    )
9572}
9573
9574fn load_executable_script_preview(path: &Path) -> Option<String> {
9575    let bytes = fs::read(path).ok()?;
9576    let preview_len = bytes.len().min(16 * 1024);
9577    Some(String::from_utf8_lossy(&bytes[..preview_len]).into_owned())
9578}
9579
9580fn parse_script_interpreter_name(script: &str) -> Option<String> {
9581    let shebang = script.lines().next()?.strip_prefix("#!")?.trim();
9582    let mut tokens = shebang.split_whitespace();
9583    let command = tokens.next()?;
9584    let command_name = Path::new(command).file_name()?.to_str()?;
9585    if command_name == "env" {
9586        for token in tokens {
9587            if token.starts_with('-') {
9588                continue;
9589            }
9590            return Path::new(token)
9591                .file_name()
9592                .and_then(|name| name.to_str())
9593                .map(ToOwned::to_owned);
9594        }
9595        return None;
9596    }
9597
9598    Some(command_name.to_owned())
9599}
9600
9601fn parse_node_shell_shim_target(script: &str) -> Option<String> {
9602    for line in script.lines() {
9603        let trimmed = line.trim();
9604        if !trimmed.starts_with("exec ") {
9605            continue;
9606        }
9607
9608        let mut remaining = trimmed;
9609        while let Some(start) = remaining.find("\"$basedir/") {
9610            let after_prefix = &remaining[start + "\"$basedir/".len()..];
9611            let end = after_prefix.find('"')?;
9612            let candidate = &after_prefix[..end];
9613            remaining = &after_prefix[end + 1..];
9614
9615            if candidate.is_empty() || candidate == "node" || candidate.ends_with("/node") {
9616                continue;
9617            }
9618
9619            return Some(candidate.to_owned());
9620        }
9621    }
9622
9623    None
9624}
9625
9626fn is_probable_javascript_entrypoint(path: &Path, script: &str) -> bool {
9627    let extension = path
9628        .extension()
9629        .and_then(|value| value.to_str())
9630        .unwrap_or_default();
9631    if matches!(extension, "js" | "cjs" | "mjs") {
9632        return true;
9633    }
9634
9635    if !path
9636        .components()
9637        .any(|component| component.as_os_str() == "node_modules")
9638    {
9639        return false;
9640    }
9641
9642    let preview = script.trim_start_matches('\u{feff}').trim_start();
9643    !preview.is_empty()
9644        && !preview.starts_with("#!")
9645        && (preview.starts_with("\"use strict\"")
9646            || preview.starts_with("'use strict'")
9647            || preview.starts_with("import ")
9648            || preview.starts_with("export ")
9649            || preview.starts_with("const ")
9650            || preview.starts_with("let ")
9651            || preview.starts_with("var ")
9652            || preview.starts_with("Object.defineProperty(exports")
9653            || preview.starts_with("module.exports")
9654            || preview.starts_with("require("))
9655}
9656
9657fn resolve_guest_execution_cwd(vm: &VmState, value: Option<&str>) -> String {
9658    value
9659        .map(normalize_path)
9660        .unwrap_or_else(|| vm.guest_cwd.clone())
9661}
9662
9663fn resolve_execution_cwds(vm: &VmState, value: Option<&str>) -> (String, PathBuf, bool) {
9664    if let Some(raw_cwd) = value {
9665        let normalized_vm_host_cwd = normalize_host_path(&vm.host_cwd);
9666        let requested_host_cwd = normalize_host_path(Path::new(raw_cwd));
9667        if path_is_within_root(&requested_host_cwd, &normalized_vm_host_cwd) {
9668            let relative = requested_host_cwd
9669                .strip_prefix(&normalized_vm_host_cwd)
9670                .unwrap_or_else(|_| Path::new(""));
9671            let relative = relative.to_string_lossy().replace('\\', "/");
9672            let guest_cwd = if relative.is_empty() {
9673                String::from("/")
9674            } else {
9675                normalize_path(&format!("/{relative}"))
9676            };
9677            return (guest_cwd, requested_host_cwd, true);
9678        }
9679    }
9680
9681    let guest_cwd = resolve_guest_execution_cwd(vm, value);
9682    let host_cwd = if value.is_none() {
9683        vm.host_cwd.clone()
9684    } else {
9685        resolve_vm_guest_path_to_host(vm, &guest_cwd)
9686    };
9687    (guest_cwd, host_cwd, value.is_none())
9688}
9689
9690fn resolve_vm_guest_path_to_host(vm: &VmState, guest_path: &str) -> PathBuf {
9691    host_mount_path_for_guest_path(vm, guest_path)
9692        .unwrap_or_else(|| shadow_path_for_guest(vm, guest_path))
9693}
9694
9695fn shadow_path_for_guest(vm: &VmState, guest_path: &str) -> PathBuf {
9696    let normalized = normalize_path(guest_path);
9697    let relative = normalized.trim_start_matches('/');
9698    if relative.is_empty() {
9699        return vm.cwd.clone();
9700    }
9701    vm.cwd.join(relative)
9702}
9703
9704fn apply_shell_cwd_prefix(command: &str, mut args: Vec<String>, guest_cwd: &str) -> Vec<String> {
9705    if guest_cwd == "/" || !is_shell_command(command) {
9706        return args;
9707    }
9708
9709    let Some(flag) = args.first() else {
9710        return args;
9711    };
9712    if !matches!(flag.as_str(), "-c" | "-lc") || args.len() < 2 {
9713        return args;
9714    }
9715
9716    let command_text = args[1].clone();
9717    let quoted_cwd = shell_single_quote(guest_cwd);
9718    args[1] = format!("cd {quoted_cwd} && {command_text}");
9719    args
9720}
9721
9722fn is_shell_command(command: &str) -> bool {
9723    Path::new(command)
9724        .file_name()
9725        .and_then(|name| name.to_str())
9726        .unwrap_or(command)
9727        .trim_end_matches(".exe")
9728        .eq("sh")
9729        || Path::new(command)
9730            .file_name()
9731            .and_then(|name| name.to_str())
9732            .unwrap_or(command)
9733            .trim_end_matches(".exe")
9734            .eq("bash")
9735}
9736
9737fn shell_single_quote(value: &str) -> String {
9738    if value.is_empty() {
9739        return String::from("''");
9740    }
9741    format!("'{}'", value.replace('\'', "'\"'\"'"))
9742}
9743
9744pub(crate) fn sync_active_process_host_writes_to_kernel(
9745    vm: &mut VmState,
9746) -> Result<(), SidecarError> {
9747    if vm.root_filesystem_mode != RootFilesystemMode::ReadOnly {
9748        let shadow_root = vm.cwd.clone();
9749        sync_host_directory_tree_to_kernel(vm, &shadow_root, "/")?;
9750    }
9751
9752    let normalized_vm_root = normalize_host_path(&vm.cwd);
9753    let extra_roots = collect_active_process_host_sync_roots(vm, &normalized_vm_root);
9754    for (host_cwd, guest_cwd) in extra_roots {
9755        sync_host_directory_tree_to_kernel(vm, &host_cwd, &guest_cwd)?;
9756    }
9757
9758    Ok(())
9759}
9760
9761fn collect_active_process_host_sync_roots(
9762    vm: &VmState,
9763    normalized_vm_root: &Path,
9764) -> Vec<(PathBuf, String)> {
9765    let mut roots = Vec::new();
9766    let mut seen = BTreeSet::new();
9767
9768    for process in vm.active_processes.values() {
9769        collect_process_host_sync_roots(process, normalized_vm_root, &mut seen, &mut roots);
9770    }
9771
9772    roots
9773}
9774
9775fn collect_process_host_sync_roots(
9776    process: &ActiveProcess,
9777    normalized_vm_root: &Path,
9778    seen: &mut BTreeSet<(PathBuf, String)>,
9779    roots: &mut Vec<(PathBuf, String)>,
9780) {
9781    let normalized_host_cwd = normalize_host_path(&process.host_cwd);
9782    if !path_is_within_root(&normalized_host_cwd, normalized_vm_root) {
9783        let guest_cwd = normalize_path(&process.guest_cwd);
9784        if seen.insert((normalized_host_cwd.clone(), guest_cwd.clone())) {
9785            roots.push((normalized_host_cwd, guest_cwd));
9786        }
9787    }
9788
9789    for child in process.child_processes.values() {
9790        collect_process_host_sync_roots(child, normalized_vm_root, seen, roots);
9791    }
9792}
9793
9794fn sync_process_host_writes_to_kernel(
9795    vm: &mut VmState,
9796    process: &ActiveProcess,
9797) -> Result<(), SidecarError> {
9798    if vm.root_filesystem_mode != RootFilesystemMode::ReadOnly {
9799        let shadow_root = vm.cwd.clone();
9800        sync_host_directory_tree_to_kernel(vm, &shadow_root, "/")?;
9801    }
9802
9803    if !path_is_within_root(
9804        &normalize_host_path(&process.host_cwd),
9805        &normalize_host_path(&vm.cwd),
9806    ) {
9807        sync_host_directory_tree_to_kernel(vm, &process.host_cwd, &process.guest_cwd)?;
9808    }
9809
9810    Ok(())
9811}
9812
9813fn host_sync_root_is_filesystem_root(host_root: &Path) -> bool {
9814    normalize_host_path(host_root) == Path::new("/")
9815}
9816
9817fn sync_host_directory_tree_to_kernel(
9818    vm: &mut VmState,
9819    host_root: &Path,
9820    guest_root: &str,
9821) -> Result<(), SidecarError> {
9822    let normalized_host_root = normalize_host_path(host_root);
9823    let normalized_guest_root = normalize_path(guest_root);
9824    if host_sync_root_is_filesystem_root(host_root) {
9825        // A process tracked with host cwd "/" would pull the entire host
9826        // filesystem into the kernel VFS (until the size/inode caps fire).
9827        // No sanctioned flow shadows the host root wholesale; host access is
9828        // scoped through mounts.
9829        tracing::warn!("skipping host shadow sync rooted at the host filesystem root");
9830        return Ok(());
9831    }
9832    let mut synced_file_times = BTreeMap::new();
9833    sync_host_directory_tree_to_kernel_inner(
9834        vm,
9835        &normalized_host_root,
9836        &normalized_host_root,
9837        &normalized_guest_root,
9838        &mut synced_file_times,
9839    )
9840}
9841
9842fn sync_host_directory_tree_to_kernel_inner(
9843    vm: &mut VmState,
9844    host_root: &Path,
9845    current_host_dir: &Path,
9846    guest_root: &str,
9847    synced_file_times: &mut BTreeMap<(u64, u64), (u64, u64)>,
9848) -> Result<(), SidecarError> {
9849    let entries = match fs::read_dir(current_host_dir) {
9850        Ok(entries) => entries,
9851        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
9852        Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => {
9853            // Host dirs the sidecar user cannot read (e.g. root-owned
9854            // /lost+found under a host-root mount) are skipped rather than
9855            // failing the whole shadow sync; the guest just won't see them.
9856            tracing::warn!(
9857                path = %current_host_dir.display(),
9858                "skipping unreadable host shadow directory"
9859            );
9860            return Ok(());
9861        }
9862        Err(error) => {
9863            return Err(SidecarError::Io(format!(
9864                "failed to read host shadow directory {}: {error}",
9865                current_host_dir.display()
9866            )));
9867        }
9868    };
9869
9870    for entry in entries {
9871        let entry = entry.map_err(|error| {
9872            SidecarError::Io(format!(
9873                "failed to read host shadow entry in {}: {error}",
9874                current_host_dir.display()
9875            ))
9876        })?;
9877        let host_path = entry.path();
9878        let file_type = entry.file_type().map_err(|error| {
9879            SidecarError::Io(format!(
9880                "failed to stat host shadow entry {}: {error}",
9881                host_path.display()
9882            ))
9883        })?;
9884        let relative_path = host_path
9885            .strip_prefix(host_root)
9886            .map_err(|error| {
9887                SidecarError::InvalidState(format!(
9888                    "failed to relativize host shadow path {} against {}: {error}",
9889                    host_path.display(),
9890                    host_root.display()
9891                ))
9892            })?
9893            .to_string_lossy()
9894            .replace('\\', "/");
9895        let guest_path = if guest_root == "/" {
9896            normalize_path(&format!("/{relative_path}"))
9897        } else {
9898            normalize_path(&format!(
9899                "{}/{}",
9900                guest_root.trim_end_matches('/'),
9901                relative_path
9902            ))
9903        };
9904
9905        if should_skip_shadow_sync_path(vm, &guest_path) {
9906            continue;
9907        }
9908
9909        if file_type.is_dir() {
9910            let metadata = entry.metadata().map_err(|error| {
9911                SidecarError::Io(format!(
9912                    "failed to read host shadow metadata {}: {error}",
9913                    host_path.display()
9914                ))
9915            })?;
9916            if !is_shadow_bootstrap_dir(&guest_path)
9917                && !vm.kernel.exists(&guest_path).unwrap_or(false)
9918            {
9919                vm.kernel.mkdir(&guest_path, true).map_err(|error| {
9920                    SidecarError::InvalidState(format!(
9921                        "failed to sync host shadow directory {} to guest {}: {}",
9922                        host_path.display(),
9923                        guest_path,
9924                        kernel_error(error)
9925                    ))
9926                })?;
9927                vm.kernel
9928                    .chmod(&guest_path, host_shadow_mode(&metadata))
9929                    .map_err(|error| {
9930                        SidecarError::InvalidState(format!(
9931                            "failed to sync host shadow directory mode {} to guest {}: {}",
9932                            host_path.display(),
9933                            guest_path,
9934                            kernel_error(error)
9935                        ))
9936                    })?;
9937            }
9938            sync_host_directory_tree_to_kernel_inner(
9939                vm,
9940                host_root,
9941                &host_path,
9942                guest_root,
9943                synced_file_times,
9944            )?;
9945            continue;
9946        }
9947
9948        if file_type.is_file() {
9949            let metadata = entry.metadata().map_err(|error| {
9950                SidecarError::Io(format!(
9951                    "failed to read host shadow metadata {}: {error}",
9952                    host_path.display()
9953                ))
9954            })?;
9955            let timestamp_key = (metadata.dev(), metadata.ino());
9956            let (atime_ms, mtime_ms) =
9957                *synced_file_times.entry(timestamp_key).or_insert_with(|| {
9958                    (
9959                        metadata_time_ms(metadata.atime(), metadata.atime_nsec()),
9960                        metadata_time_ms(metadata.mtime(), metadata.mtime_nsec()),
9961                    )
9962                });
9963            let desired_mode = host_shadow_mode(&metadata);
9964            // Fast path: skip the expensive re-read + re-write when the kernel already
9965            // holds a copy of this shadow file that matches on size, mode, and mtime.
9966            //
9967            // Every read-side fs op (exists/stat/readFile/...) triggers a full
9968            // shadow-tree reconciliation walk. Without this skip the walk re-reads every
9969            // file's bytes from the host and re-writes them into the kernel VFS on every
9970            // op -- O(whole tree) per op, and super-linear as the VM's shadow grows,
9971            // which is a dominant source of session-creation/runtime latency on
9972            // populated VMs.
9973            //
9974            // This is a (size, mode, mtime) quick-check, the same heuristic rsync uses
9975            // by default. It needs no separate cache to invalidate -- it compares against
9976            // the kernel's own stat, so a kernel reset (e.g. a layer swap) or any host
9977            // change that moves size/mode/mtime forces a resync. Limitation: mtime is
9978            // compared at the millisecond granularity the kernel stores (utimes truncates
9979            // to ms), so a host-side rewrite that preserves byte length AND mode AND lands
9980            // in the same wall-clock millisecond can be skipped and leave stale bytes.
9981            // That window is sub-millisecond same-length edits; if it ever matters here,
9982            // upgrade this to a content digest (or full-precision mtime) for files whose
9983            // mtime is within the last few ms of `now`.
9984            if let Ok(existing) = vm.kernel.lstat(&guest_path) {
9985                if !existing.is_directory
9986                    && !existing.is_symbolic_link
9987                    && existing.size == metadata.len()
9988                    && (existing.mode & 0o7777) == (desired_mode & 0o7777)
9989                    && existing.mtime_ms == mtime_ms
9990                {
9991                    continue;
9992                }
9993            }
9994            let bytes = match read_host_shadow_file(&host_path, desired_mode) {
9995                Ok(bytes) => bytes,
9996                // The host entry vanished between the walk and the read
9997                // (short-lived files churn constantly — editor swap files,
9998                // temp files). Skipping matches native semantics; failing
9999                // here would poison EVERY subsequent fs op on the VM.
10000                Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
10001                // Same tolerance for entries the sidecar user cannot read
10002                // (root-owned files under a host-root mount): skip them
10003                // rather than poisoning the whole sync.
10004                Err(error)
10005                    if error.kind() == std::io::ErrorKind::PermissionDenied
10006                        || error.raw_os_error() == Some(libc::EPERM) =>
10007                {
10008                    tracing::warn!(
10009                        path = %host_path.display(),
10010                        "skipping unreadable host shadow file"
10011                    );
10012                    continue;
10013                }
10014                Err(error) => {
10015                    return Err(SidecarError::Io(format!(
10016                        "failed to read host shadow file {}: {error}",
10017                        host_path.display()
10018                    )));
10019                }
10020            };
10021            match vm.kernel.write_file(&guest_path, bytes) {
10022                Ok(()) => {}
10023                // ENOENT here means the guest-side path cannot currently
10024                // receive the write (e.g. it is a symlink whose target was
10025                // just unlinked by the guest — vim's swap-file dance). The
10026                // entry is mid-churn; skip it rather than failing the VM.
10027                Err(error) if error.code() == "ENOENT" => continue,
10028                Err(error) => {
10029                    return Err(SidecarError::InvalidState(format!(
10030                        "failed to sync host shadow file {} to guest {}: {}",
10031                        host_path.display(),
10032                        guest_path,
10033                        kernel_error(error)
10034                    )));
10035                }
10036            }
10037            vm.kernel
10038                .chmod(&guest_path, desired_mode)
10039                .map_err(|error| {
10040                    SidecarError::InvalidState(format!(
10041                        "failed to sync host shadow file mode {} to guest {}: {}",
10042                        host_path.display(),
10043                        guest_path,
10044                        kernel_error(error)
10045                    ))
10046                })?;
10047            vm.kernel
10048                .utimes(&guest_path, atime_ms, mtime_ms)
10049                .map_err(|error| {
10050                    SidecarError::InvalidState(format!(
10051                        "failed to sync host shadow file times {} to guest {}: {}",
10052                        host_path.display(),
10053                        guest_path,
10054                        kernel_error(error)
10055                    ))
10056                })?;
10057            continue;
10058        }
10059
10060        if file_type.is_symlink() {
10061            let target = match fs::read_link(&host_path) {
10062                Ok(target) => target,
10063                Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
10064                Err(error) => {
10065                    return Err(SidecarError::Io(format!(
10066                        "failed to read host shadow symlink {}: {error}",
10067                        host_path.display()
10068                    )));
10069                }
10070            };
10071            replace_kernel_symlink(vm, &guest_path, &target.to_string_lossy())?;
10072        }
10073    }
10074
10075    Ok(())
10076}
10077
10078fn replace_kernel_symlink(
10079    vm: &mut VmState,
10080    guest_path: &str,
10081    target: &str,
10082) -> Result<(), SidecarError> {
10083    if vm.kernel.symlink(target, guest_path).is_ok() {
10084        return Ok(());
10085    }
10086
10087    if let Ok(existing_target) = vm.kernel.read_link(guest_path) {
10088        if existing_target == target {
10089            return Ok(());
10090        }
10091    }
10092
10093    let _ = vm.kernel.remove_file(guest_path);
10094    let _ = vm.kernel.remove_dir(guest_path);
10095    vm.kernel
10096        .symlink(target, guest_path)
10097        .map_err(kernel_error)?;
10098    Ok(())
10099}
10100
10101fn host_shadow_mode(metadata: &fs::Metadata) -> u32 {
10102    metadata.permissions().mode() & 0o7777
10103}
10104
10105/// Reads a shadow-root file back into the kernel even when guest-visible mode
10106/// bits make it unreadable for the host user. The sidecar is the kernel for
10107/// this tree, so guest permission bits (for example a 0o200 write-only file
10108/// produced by `chmod` plus a shell append redirect) must not break the
10109/// exit-time shadow sync. The original mode is restored after the read.
10110fn read_host_shadow_file(host_path: &Path, mode: u32) -> std::io::Result<Vec<u8>> {
10111    match fs::read(host_path) {
10112        Ok(bytes) => Ok(bytes),
10113        Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => {
10114            fs::set_permissions(host_path, fs::Permissions::from_mode(mode | 0o400))?;
10115            let result = fs::read(host_path);
10116            fs::set_permissions(host_path, fs::Permissions::from_mode(mode))?;
10117            result
10118        }
10119        Err(error) => Err(error),
10120    }
10121}
10122
10123fn metadata_time_ms(seconds: i64, nanos: i64) -> u64 {
10124    let seconds = seconds.max(0) as u64;
10125    let nanos = nanos.max(0) as u64;
10126    seconds
10127        .saturating_mul(1_000)
10128        .saturating_add(nanos / 1_000_000)
10129}
10130
10131fn is_shadow_bootstrap_dir(path: &str) -> bool {
10132    matches!(
10133        path,
10134        "/dev"
10135            | "/proc"
10136            | "/tmp"
10137            | "/bin"
10138            | "/lib"
10139            | "/sbin"
10140            | "/boot"
10141            | "/etc"
10142            | "/root"
10143            | "/run"
10144            | "/srv"
10145            | "/sys"
10146            | "/opt"
10147            | "/mnt"
10148            | "/media"
10149            | "/home"
10150            | "/home/agentos"
10151            | "/usr"
10152            | "/usr/bin"
10153            | "/usr/games"
10154            | "/usr/include"
10155            | "/usr/lib"
10156            | "/usr/libexec"
10157            | "/usr/man"
10158            | "/usr/local"
10159            | "/usr/local/bin"
10160            | "/usr/sbin"
10161            | "/usr/share"
10162            | "/usr/share/man"
10163            | "/var"
10164            | "/var/cache"
10165            | "/var/empty"
10166            | "/var/lib"
10167            | "/var/lock"
10168            | "/var/log"
10169            | "/var/run"
10170            | "/var/spool"
10171            | "/var/tmp"
10172            | "/etc/agentos"
10173            | "/workspace"
10174    )
10175}
10176
10177#[cfg(test)]
10178mod shadow_sync_tests {
10179    use super::{is_protected_agentos_shadow_sync_path, is_shadow_bootstrap_dir};
10180
10181    #[test]
10182    fn shadow_bootstrap_sync_skips_virtual_home_tree() {
10183        assert!(is_shadow_bootstrap_dir("/home"));
10184        assert!(is_shadow_bootstrap_dir("/home/agentos"));
10185    }
10186
10187    #[test]
10188    fn protected_agentos_paths_are_not_shadow_synced() {
10189        assert!(is_protected_agentos_shadow_sync_path("/etc/agentos"));
10190        assert!(is_protected_agentos_shadow_sync_path(
10191            "/etc/agentos/instructions.md"
10192        ));
10193        assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos-copy"));
10194        assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos.md"));
10195    }
10196}
10197
10198fn is_kernel_owned_shadow_sync_path(path: &str) -> bool {
10199    matches!(path, "/dev" | "/proc" | "/sys")
10200        || path.starts_with("/dev/")
10201        || path.starts_with("/proc/")
10202        || path.starts_with("/sys/")
10203}
10204
10205pub(crate) fn is_protected_agentos_shadow_sync_path(path: &str) -> bool {
10206    path == "/etc/agentos" || path.starts_with("/etc/agentos/")
10207}
10208
10209fn should_skip_shadow_sync_path(vm: &VmState, guest_path: &str) -> bool {
10210    is_kernel_owned_shadow_sync_path(guest_path)
10211        || is_protected_agentos_shadow_sync_path(guest_path)
10212        || host_mount_path_for_guest_path_from_mounts(&vm.configuration.mounts, guest_path)
10213            .is_some()
10214}
10215
10216fn resolve_path_like_guest_specifier(cwd: &str, specifier: &str) -> String {
10217    if specifier.starts_with("file://") {
10218        normalize_path(specifier.trim_start_matches("file://"))
10219    } else if specifier.starts_with("file:") {
10220        normalize_path(specifier.trim_start_matches("file:"))
10221    } else if specifier.starts_with('/') {
10222        normalize_path(specifier)
10223    } else {
10224        normalize_path(&format!("{cwd}/{specifier}"))
10225    }
10226}
10227
10228fn guest_entrypoint_for_specifier(cwd: &str, specifier: &str) -> Option<String> {
10229    is_path_like_specifier(specifier).then(|| resolve_path_like_guest_specifier(cwd, specifier))
10230}
10231
10232fn is_node_runtime_command(command: &str) -> bool {
10233    matches!(command, "node" | "npm" | "npx")
10234        || Path::new(command)
10235            .file_name()
10236            .and_then(|name| name.to_str())
10237            .is_some_and(|name| matches!(name, "node" | "npm" | "npx"))
10238}
10239
10240fn python_command_base_name(command: &str) -> &str {
10241    Path::new(command)
10242        .file_name()
10243        .and_then(|name| name.to_str())
10244        .unwrap_or(command)
10245}
10246
10247/// `python` / `python3` (and `pip` / `pip3`, which map to `python -m pip`) are
10248/// served by the embedded Pyodide runtime, mirroring how `node` is served by the
10249/// embedded V8 runtime.
10250fn is_python_runtime_command(command: &str) -> bool {
10251    matches!(
10252        python_command_base_name(command),
10253        "python" | "python3" | "pip" | "pip3"
10254    )
10255}
10256
10257/// Parse a `python` / `pip` command line into a Pyodide execution. Supports the
10258/// CPython program selectors `-c CODE`, `-m MODULE`, a `SCRIPT` path, `-` /
10259/// piped stdin programs, and a bare interpreter (interactive REPL). The chosen
10260/// mode plus `sys.argv` are forwarded to the runner as `AGENTOS_PYTHON_*` control
10261/// env, which the runner consumes and never exposes in the guest `os.environ`.
10262fn resolve_python_command_execution(
10263    vm: &VmState,
10264    command: &str,
10265    args: &[String],
10266    mut env: BTreeMap<String, String>,
10267    guest_cwd: String,
10268    host_cwd: PathBuf,
10269) -> Result<ResolvedChildProcessExecution, SidecarError> {
10270    let base_name = python_command_base_name(command);
10271    let is_pip = matches!(base_name, "pip" | "pip3");
10272
10273    let mut entrypoint = String::new();
10274    let mut argv: Vec<String> = Vec::new();
10275    let mut module: Option<String> = None;
10276    let mut stdin_program = false;
10277    let mut interactive = false;
10278    let mut guest_entrypoint: Option<String> = None;
10279
10280    if is_pip {
10281        module = Some(String::from("pip"));
10282        argv.push(String::from("pip"));
10283        argv.extend(args.iter().cloned());
10284    } else {
10285        // Skip the value-less interpreter flags we can safely ignore so they do
10286        // not get mistaken for a script path.
10287        let mut idx = 0;
10288        while let Some(flag) = args.get(idx) {
10289            match flag.as_str() {
10290                "-B" | "-E" | "-I" | "-O" | "-OO" | "-q" | "-s" | "-S" | "-u" | "-v" | "-b"
10291                | "-d" | "-x" => idx += 1,
10292                _ => break,
10293            }
10294        }
10295        let rest = &args[idx..];
10296        match rest.first().map(String::as_str) {
10297            Some("-c") => {
10298                entrypoint = rest.get(1).cloned().ok_or_else(|| {
10299                    SidecarError::InvalidState(String::from("argument expected for the -c option"))
10300                })?;
10301                argv.push(String::from("-c"));
10302                argv.extend(rest.iter().skip(2).cloned());
10303            }
10304            Some("-m") => {
10305                let name = rest.get(1).cloned().ok_or_else(|| {
10306                    SidecarError::InvalidState(String::from("argument expected for the -m option"))
10307                })?;
10308                module = Some(name);
10309                argv.push(String::from("-m"));
10310                argv.extend(rest.iter().skip(2).cloned());
10311            }
10312            Some("-") => {
10313                stdin_program = true;
10314                argv.push(String::from("-"));
10315                argv.extend(rest.iter().skip(1).cloned());
10316            }
10317            Some(spec) if !spec.starts_with('-') => {
10318                let resolved_guest = guest_entrypoint_for_specifier(&guest_cwd, spec)
10319                    .unwrap_or_else(|| spec.to_string());
10320                entrypoint = resolved_guest.clone();
10321                env.insert(String::from("AGENTOS_PYTHON_FILE"), resolved_guest.clone());
10322                guest_entrypoint = Some(resolved_guest);
10323                argv.push(spec.to_string());
10324                argv.extend(rest.iter().skip(1).cloned());
10325            }
10326            Some(other) => {
10327                return Err(SidecarError::InvalidState(format!(
10328                    "unsupported python option: {other}"
10329                )));
10330            }
10331            None => {
10332                interactive = true;
10333                argv.push(String::new());
10334            }
10335        }
10336    }
10337
10338    env.insert(
10339        String::from("AGENTOS_PYTHON_ARGV"),
10340        serde_json::to_string(&argv).unwrap_or_else(|_| String::from("[]")),
10341    );
10342    if let Some(module) = &module {
10343        env.insert(String::from("AGENTOS_PYTHON_MODULE"), module.clone());
10344    }
10345    if stdin_program {
10346        env.insert(
10347            String::from("AGENTOS_PYTHON_STDIN_PROGRAM"),
10348            String::from("1"),
10349        );
10350    }
10351    if interactive {
10352        env.insert(
10353            String::from("AGENTOS_PYTHON_INTERACTIVE"),
10354            String::from("1"),
10355        );
10356    }
10357
10358    prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
10359
10360    Ok(ResolvedChildProcessExecution {
10361        command: String::from(PYTHON_COMMAND),
10362        process_args: std::iter::once(command.to_owned())
10363            .chain(args.iter().cloned())
10364            .collect(),
10365        runtime: GuestRuntimeKind::Python,
10366        entrypoint,
10367        execution_args: args.to_vec(),
10368        env,
10369        guest_cwd,
10370        host_cwd,
10371        wasm_permission_tier: None,
10372        tool_command: false,
10373    })
10374}
10375
10376fn resolve_special_node_cli_invocation(
10377    args: &[String],
10378    env: &mut BTreeMap<String, String>,
10379) -> Option<(String, Vec<String>)> {
10380    let first = args.first()?;
10381    match first.as_str() {
10382        "-e" | "--eval" => {
10383            env.insert(
10384                String::from("AGENTOS_NODE_EVAL"),
10385                args.get(1).cloned().unwrap_or_default(),
10386            );
10387            Some((first.clone(), args.iter().skip(2).cloned().collect()))
10388        }
10389        "-v" | "--version" => {
10390            env.insert(
10391                String::from("AGENTOS_NODE_EVAL"),
10392                String::from("console.log(process.version);"),
10393            );
10394            Some((String::from("-e"), args.to_vec()))
10395        }
10396        _ => None,
10397    }
10398}
10399
10400fn node_runtime_command_name(command: &str) -> Option<&str> {
10401    let name = Path::new(command)
10402        .file_name()
10403        .and_then(|name| name.to_str())?;
10404    matches!(name, "node" | "npm" | "npx").then_some(name)
10405}
10406
10407struct ResolvedHostNodeCliEntrypoint {
10408    command_name: String,
10409    guest_root: String,
10410    guest_entrypoint: String,
10411    package_root: PathBuf,
10412}
10413
10414fn resolve_host_node_cli_entrypoint(command: &str) -> Option<ResolvedHostNodeCliEntrypoint> {
10415    let command_name = node_runtime_command_name(command)?;
10416    if !matches!(command_name, "npm" | "npx") {
10417        return None;
10418    }
10419
10420    let path = std::env::var_os("PATH")?;
10421    for root in std::env::split_paths(&path) {
10422        let candidate = root.join(command_name);
10423        if !candidate.is_file() {
10424            continue;
10425        }
10426        let entrypoint = candidate.canonicalize().ok().unwrap_or(candidate);
10427        let package_root = entrypoint.parent()?.parent()?.to_path_buf();
10428        let guest_root = format!("/__secure_exec/node-runtime/{command_name}");
10429        let relative_entrypoint = entrypoint.strip_prefix(&package_root).ok()?;
10430        let guest_entrypoint = normalize_path(&format!(
10431            "{guest_root}/{}",
10432            relative_entrypoint.to_string_lossy().replace('\\', "/")
10433        ));
10434        return Some(ResolvedHostNodeCliEntrypoint {
10435            command_name: command_name.to_owned(),
10436            guest_root,
10437            guest_entrypoint,
10438            package_root,
10439        });
10440    }
10441
10442    None
10443}
10444
10445fn build_host_node_cli_eval(cli: &ResolvedHostNodeCliEntrypoint) -> String {
10446    let guest_npm_main = normalize_path(&format!("{}/lib/npm.js", cli.guest_root));
10447    let guest_npm_cli = normalize_path(&format!("{}/bin/npm-cli.js", cli.guest_root));
10448    let guest_package_json = normalize_path(&format!("{}/package.json", cli.guest_root));
10449    let guest_display_module = normalize_path(&format!("{}/lib/utils/display.js", cli.guest_root));
10450    let guest_log_file_module =
10451        normalize_path(&format!("{}/lib/utils/log-file.js", cli.guest_root));
10452    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); } } }";
10453    let display_stub = format!(
10454        "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 }};",
10455        display_module = serde_json::to_string(&guest_display_module)
10456            .unwrap_or_else(|_| format!("\"{guest_display_module}\"")),
10457        log_file_module = serde_json::to_string(&guest_log_file_module)
10458            .unwrap_or_else(|_| format!("\"{guest_log_file_module}\"")),
10459    );
10460    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)); }";
10461    match cli.command_name.as_str() {
10462        "npx" => format!(
10463            "{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); }});",
10464            debug_preamble = debug_preamble,
10465            display_stub = display_stub,
10466            registry_fetch_stub = registry_fetch_stub.replace(
10467                "__AGENTOS_NPM_MAIN__",
10468                &serde_json::to_string(&guest_npm_main)
10469                    .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
10470            ),
10471            npm_main = serde_json::to_string(&guest_npm_main)
10472                .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
10473            npm_cli = serde_json::to_string(&guest_npm_cli)
10474                .unwrap_or_else(|_| format!("\"{guest_npm_cli}\"")),
10475            package_json = serde_json::to_string(&guest_package_json)
10476                .unwrap_or_else(|_| format!("\"{guest_package_json}\"")),
10477        ),
10478        _ => format!(
10479            "{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); }});",
10480            debug_preamble = debug_preamble,
10481            display_stub = display_stub,
10482            registry_fetch_stub = registry_fetch_stub.replace(
10483                "__AGENTOS_NPM_MAIN__",
10484                &serde_json::to_string(&guest_npm_main)
10485                    .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
10486            ),
10487            npm_main = serde_json::to_string(&guest_npm_main)
10488                .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
10489            package_json = serde_json::to_string(&guest_package_json)
10490                .unwrap_or_else(|_| format!("\"{guest_package_json}\"")),
10491        ),
10492    }
10493}
10494
10495fn resolve_guest_command_entrypoint(
10496    vm: &VmState,
10497    guest_cwd: &str,
10498    command: &str,
10499    path_env: Option<&str>,
10500) -> Option<String> {
10501    if !is_path_like_specifier(command) {
10502        if let Some(entrypoint) = vm.command_guest_paths.get(command) {
10503            return Some(entrypoint.clone());
10504        }
10505
10506        for search_dir in guest_command_search_dirs(vm, guest_cwd, path_env) {
10507            let candidate = normalize_path(&format!("{search_dir}/{command}"));
10508            if let Some(entrypoint) = resolve_guest_command_path_candidate(vm, &candidate) {
10509                return Some(entrypoint);
10510            }
10511        }
10512
10513        return None;
10514    }
10515
10516    let normalized = resolve_path_like_guest_specifier(guest_cwd, command);
10517    resolve_guest_command_path_candidate(vm, &normalized).or_else(|| {
10518        // Some guest shells materialize PATH lookups into absolute candidate paths.
10519        // If that path points into a searched directory but does not exist, fall
10520        // back to the command basename so the sidecar can remap VM command packages.
10521        let parent_dir = Path::new(&normalized).parent()?.to_str()?;
10522        if !guest_command_search_dirs(vm, guest_cwd, path_env)
10523            .iter()
10524            .any(|search_dir| normalize_path(search_dir) == normalize_path(parent_dir))
10525        {
10526            return None;
10527        }
10528
10529        let file_name = Path::new(&normalized).file_name()?.to_str()?;
10530        vm.command_guest_paths.get(file_name).cloned()
10531    })
10532}
10533
10534fn guest_command_search_dirs(vm: &VmState, guest_cwd: &str, path_env: Option<&str>) -> Vec<String> {
10535    let mut search_dirs = Vec::new();
10536    let mut seen = BTreeSet::new();
10537
10538    if let Some(path) = path_env.or_else(|| vm.guest_env.get("PATH").map(String::as_str)) {
10539        for segment in path.split(':') {
10540            let trimmed = segment.trim();
10541            if trimmed.is_empty() {
10542                continue;
10543            }
10544            let normalized = if trimmed.starts_with('/') {
10545                normalize_path(trimmed)
10546            } else {
10547                normalize_path(&format!("{guest_cwd}/{trimmed}"))
10548            };
10549            if seen.insert(normalized.clone()) {
10550                search_dirs.push(normalized);
10551            }
10552        }
10553    }
10554
10555    for fallback in ["/bin", "/usr/bin", "/usr/local/bin"] {
10556        let normalized = String::from(fallback);
10557        if seen.insert(normalized.clone()) {
10558            search_dirs.push(normalized);
10559        }
10560    }
10561
10562    search_dirs
10563}
10564
10565fn resolve_guest_command_path_candidate(vm: &VmState, candidate: &str) -> Option<String> {
10566    if candidate.starts_with("/bin/")
10567        || candidate.starts_with("/usr/bin/")
10568        || candidate.starts_with("/usr/local/bin/")
10569        || candidate.starts_with("/__secure_exec/commands/")
10570    {
10571        if let Some(file_name) = Path::new(candidate)
10572            .file_name()
10573            .and_then(|name| name.to_str())
10574        {
10575            if let Some(guest_entrypoint) = vm.command_guest_paths.get(file_name) {
10576                return Some(guest_entrypoint.clone());
10577            }
10578        }
10579    }
10580
10581    if vm
10582        .kernel
10583        .exists(candidate)
10584        .ok()
10585        .is_some_and(|exists| exists)
10586    {
10587        return Some(normalize_path(candidate));
10588    }
10589
10590    resolve_vm_guest_path_to_host(vm, candidate)
10591        .is_file()
10592        .then(|| normalize_path(candidate))
10593}
10594
10595fn resolve_host_entrypoint_within_vm_host_cwd(
10596    vm: &VmState,
10597    specifier: &str,
10598) -> Option<(String, String)> {
10599    let candidate = Path::new(specifier);
10600    if !candidate.is_absolute() {
10601        return None;
10602    }
10603
10604    let normalized_entrypoint = normalize_host_path(candidate);
10605    let normalized_host_cwd = normalize_host_path(&vm.host_cwd);
10606    if !path_is_within_root(&normalized_entrypoint, &normalized_host_cwd) {
10607        return None;
10608    }
10609
10610    let relative = normalized_entrypoint
10611        .strip_prefix(&normalized_host_cwd)
10612        .ok()?
10613        .to_string_lossy()
10614        .replace('\\', "/");
10615    let guest_entrypoint = if relative.is_empty() {
10616        String::from("/")
10617    } else {
10618        normalize_path(&format!("/{relative}"))
10619    };
10620    Some((
10621        guest_entrypoint,
10622        normalized_entrypoint.to_string_lossy().into_owned(),
10623    ))
10624}
10625
10626fn prepare_guest_runtime_env(
10627    vm: &VmState,
10628    env: &mut BTreeMap<String, String>,
10629    guest_cwd: &str,
10630    host_cwd: &Path,
10631    guest_entrypoint: Option<String>,
10632) -> Result<(), SidecarError> {
10633    let user = vm.kernel.user_profile();
10634    let path_mappings = runtime_guest_path_mappings(vm);
10635    let read_paths = expand_host_access_paths(
10636        std::iter::once(vm.cwd.clone())
10637            .chain(
10638                path_mappings
10639                    .iter()
10640                    .map(|mapping| PathBuf::from(&mapping.host_path)),
10641            )
10642            .chain(std::iter::once(host_cwd.to_path_buf()))
10643            .collect::<Vec<_>>()
10644            .as_slice(),
10645    );
10646    let write_paths = dedupe_host_paths(
10647        std::iter::once(vm.cwd.clone())
10648            .chain(std::iter::once(host_cwd.to_path_buf()))
10649            .chain(runtime_guest_writable_host_paths(vm))
10650            .collect::<Vec<_>>()
10651            .as_slice(),
10652    );
10653    let allowed_node_builtins = configured_allowed_node_builtins(vm);
10654    let loopback_exempt_ports = configured_loopback_exempt_ports(vm);
10655
10656    env.insert(
10657        String::from("AGENTOS_GUEST_PATH_MAPPINGS"),
10658        serde_json::to_string(&path_mappings).map_err(|error| {
10659            SidecarError::InvalidState(format!("failed to encode guest path mappings: {error}"))
10660        })?,
10661    );
10662    env.entry(String::from(EXECUTION_SANDBOX_ROOT_ENV))
10663        .or_insert_with(|| normalize_host_path(&vm.cwd).to_string_lossy().into_owned());
10664    env.insert(
10665        String::from("AGENTOS_EXTRA_FS_READ_PATHS"),
10666        serde_json::to_string(
10667            &read_paths
10668                .iter()
10669                .map(|path| path.to_string_lossy().into_owned())
10670                .collect::<Vec<_>>(),
10671        )
10672        .map_err(|error| {
10673            SidecarError::InvalidState(format!("failed to encode read paths: {error}"))
10674        })?,
10675    );
10676    env.insert(
10677        String::from("AGENTOS_EXTRA_FS_WRITE_PATHS"),
10678        serde_json::to_string(
10679            &write_paths
10680                .iter()
10681                .map(|path| path.to_string_lossy().into_owned())
10682                .collect::<Vec<_>>(),
10683        )
10684        .map_err(|error| {
10685            SidecarError::InvalidState(format!("failed to encode write paths: {error}"))
10686        })?,
10687    );
10688    env.insert(
10689        String::from("AGENTOS_ALLOWED_NODE_BUILTINS"),
10690        serde_json::to_string(&allowed_node_builtins).map_err(|error| {
10691            SidecarError::InvalidState(format!("failed to encode allowed builtins: {error}"))
10692        })?,
10693    );
10694    // The guest JS host platform drives subtractive global scrubbing in the
10695    // per-execution runtime shim (see prepend_v8_runtime_shim).
10696    env.insert(
10697        String::from("AGENTOS_JS_PLATFORM"),
10698        js_runtime_platform_env(vm).to_owned(),
10699    );
10700    // Module-resolution mode (omitted when full Node resolution / the default).
10701    if let Some(resolution) = js_runtime_module_resolution_env(vm) {
10702        env.insert(
10703            String::from("AGENTOS_JS_MODULE_RESOLUTION"),
10704            resolution.to_owned(),
10705        );
10706    }
10707    // Builtin allow-list gate for the live resolver. Present only when builtins
10708    // should be restricted (non-node platform => deny all; node + explicit
10709    // allow-list => exactly those). Absent => unrestricted (node default).
10710    if let Some(allowlist) = js_runtime_enforced_builtins(vm) {
10711        env.insert(
10712            String::from("AGENTOS_JS_BUILTIN_ALLOWLIST"),
10713            serde_json::to_string(&allowlist).map_err(|error| {
10714                SidecarError::InvalidState(format!(
10715                    "failed to encode jsRuntime builtin allow-list: {error}"
10716                ))
10717            })?,
10718        );
10719    }
10720    // Virtual OS identity (os.cpus/totalmem/freemem/homedir/userInfo/...) now
10721    // rides the typed `guest_runtime` (see `guest_runtime_identity`), exposed to
10722    // the guest as the `__agentOSVirtualOs` structured global by the runtime
10723    // shim — no longer the `AGENTOS_VIRTUAL_OS_*` env vars.
10724    // Virtual process uid/gid now ride the typed `guest_runtime` identity
10725    // (see `guest_runtime_identity`), not the `AGENTOS_VIRTUAL_PROCESS_*` env.
10726    env.entry(String::from("HOME"))
10727        .or_insert_with(|| user.homedir.clone());
10728    env.entry(String::from("USER"))
10729        .or_insert_with(|| user.username.clone());
10730    env.entry(String::from("LOGNAME"))
10731        .or_insert_with(|| user.username.clone());
10732    env.entry(String::from("SHELL"))
10733        .or_insert_with(|| user.shell.clone());
10734    env.entry(String::from("PATH")).or_insert_with(|| {
10735        vm.guest_env
10736            .get("PATH")
10737            .cloned()
10738            .unwrap_or_else(|| crate::vm::DEFAULT_GUEST_PATH_ENV.to_owned())
10739    });
10740    env.entry(String::from("TMPDIR"))
10741        .or_insert_with(|| String::from("/tmp"));
10742    env.insert(String::from("PWD"), guest_cwd.to_owned());
10743    if !loopback_exempt_ports.is_empty() {
10744        env.insert(
10745            String::from(LOOPBACK_EXEMPT_PORTS_ENV),
10746            serde_json::to_string(&loopback_exempt_ports).map_err(|error| {
10747                SidecarError::InvalidState(format!("failed to encode loopback exemptions: {error}"))
10748            })?,
10749        );
10750    }
10751    if let Some(guest_entrypoint) = guest_entrypoint {
10752        env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint);
10753    }
10754    Ok(())
10755}
10756
10757/// Build the typed per-execution JavaScript limits from the per-VM `VmLimits`
10758/// (sourced from `CreateVmConfig` on the BARE wire). These ride the execution
10759/// request, not `AGENTOS_*` env vars — see the env-vs-wire rule in
10760/// `crates/sidecar/CLAUDE.md`.
10761fn javascript_execution_limits(vm: &VmState) -> JavascriptExecutionLimits {
10762    JavascriptExecutionLimits {
10763        v8_heap_limit_mb: vm.limits.js_runtime.v8_heap_limit_mb,
10764        sync_rpc_wait_timeout_ms: vm.limits.js_runtime.sync_rpc_wait_timeout_ms,
10765    }
10766}
10767
10768/// Build the typed per-execution guest-runtime identity (virtual `process.*`)
10769/// from kernel state. Replaces the `AGENTOS_VIRTUAL_PROCESS_{UID,GID,PID,PPID}`
10770/// env round-trip: the runtime shim reads these from `guest_runtime`, not env.
10771/// `uid`/`gid` come from the VM user profile (applied to every guest);
10772/// `pid`/`ppid` are per-process and only set for paths that assigned them.
10773fn guest_runtime_identity(
10774    vm: &VmState,
10775    virtual_pid: Option<u64>,
10776    virtual_ppid: Option<u64>,
10777) -> GuestRuntimeConfig {
10778    let user = vm.kernel.user_profile();
10779    let resource_limits = vm.kernel.resource_limits();
10780    let identity = shared_guest_runtime_identity(&user, resource_limits, virtual_pid, virtual_ppid);
10781    GuestRuntimeConfig {
10782        virtual_uid: Some(identity.virtual_uid),
10783        virtual_gid: Some(identity.virtual_gid),
10784        virtual_pid: identity.virtual_pid,
10785        virtual_ppid: identity.virtual_ppid,
10786        virtual_exec_path: None,
10787        os_cpu_count: Some(identity.os_cpu_count),
10788        os_totalmem: Some(identity.os_totalmem),
10789        os_freemem: Some(identity.os_freemem),
10790        os_homedir: Some(identity.os_homedir),
10791        os_hostname: Some(identity.os_hostname),
10792        os_tmpdir: Some(identity.os_tmpdir),
10793        os_type: Some(identity.os_type),
10794        os_release: Some(identity.os_release),
10795        os_version: Some(identity.os_version),
10796        os_machine: Some(identity.os_machine),
10797        os_shell: Some(identity.os_shell),
10798        os_user: Some(identity.os_user),
10799        high_resolution_time: vm
10800            .configuration
10801            .js_runtime
10802            .as_ref()
10803            .is_some_and(|cfg| cfg.high_resolution_time.unwrap_or(false)),
10804        // Userland bundle to bake into the per-sidecar snapshot, supplied by the
10805        // (trusted) client via jsRuntime.snapshotUserlandCode. The agent-os layer
10806        // sets this to the agent SDK bundle for snapshot-enabled agents; `None`
10807        // keeps the bridge-only snapshot.
10808        snapshot_userland_code: vm
10809            .configuration
10810            .js_runtime
10811            .as_ref()
10812            .and_then(|cfg| cfg.snapshot_userland_code.clone()),
10813    }
10814}
10815
10816/// The guest's virtual home directory, sourced from the VM user profile (the
10817/// same value carried to the guest as `os.homedir()` via `guest_runtime`). Used
10818/// by sidecar-internal `~`-path resolution; falls back to `/root` for a
10819/// non-absolute profile value.
10820fn guest_virtual_home(vm: &VmState) -> String {
10821    let homedir = vm.kernel.user_profile().homedir;
10822    if homedir.starts_with('/') {
10823        homedir
10824    } else {
10825        String::from("/root")
10826    }
10827}
10828
10829/// Build the typed per-execution Python limits from the per-VM `VmLimits`.
10830fn python_execution_limits(vm: &VmState) -> PythonExecutionLimits {
10831    PythonExecutionLimits {
10832        output_buffer_max_bytes: Some(vm.limits.python.output_buffer_max_bytes),
10833        execution_timeout_ms: Some(vm.limits.python.execution_timeout_ms),
10834        max_old_space_mb: Some(vm.limits.python.max_old_space_mb),
10835        vfs_rpc_timeout_ms: Some(vm.limits.python.vfs_rpc_timeout_ms),
10836    }
10837}
10838
10839/// Build the typed per-execution WebAssembly limits from the per-VM kernel
10840/// `ResourceLimits`. Replaces the old `apply_wasm_limit_env` env round-trip;
10841/// notably this is the path that finally enforces the stack cap that the
10842/// `AGENTOS_WASM_MAX_STACK_BYTES` env knob set but no reader consumed.
10843fn wasm_execution_limits(vm: &VmState) -> WasmExecutionLimits {
10844    let resource_limits = vm.kernel.resource_limits();
10845    WasmExecutionLimits {
10846        max_fuel: resource_limits.max_wasm_fuel,
10847        max_memory_bytes: resource_limits.max_wasm_memory_bytes,
10848        max_stack_bytes: resource_limits
10849            .max_wasm_stack_bytes
10850            .map(|value| value as u64),
10851    }
10852}
10853
10854/// The guest JavaScript host platform configured for this VM, defaulting to
10855/// full Node.js emulation when no `jsRuntime` config was supplied at create.
10856fn js_runtime_platform(vm: &VmState) -> vm_config::JsRuntimePlatform {
10857    vm.configuration
10858        .js_runtime
10859        .as_ref()
10860        .map(|cfg| cfg.platform)
10861        .unwrap_or(vm_config::JsRuntimePlatform::Node)
10862}
10863
10864/// Lowercase wire name for the configured platform, mirroring the serde
10865/// representation of `vm_config::JsRuntimePlatform`.
10866fn js_runtime_platform_env(vm: &VmState) -> &'static str {
10867    match js_runtime_platform(vm) {
10868        vm_config::JsRuntimePlatform::Node => "node",
10869        vm_config::JsRuntimePlatform::Browser => "browser",
10870        vm_config::JsRuntimePlatform::Neutral => "neutral",
10871        vm_config::JsRuntimePlatform::Bare => "bare",
10872    }
10873}
10874
10875/// Wire name for the configured module-resolution mode, or `None` when it is the
10876/// full-Node default (which the live resolver also assumes when the env is unset).
10877fn js_runtime_module_resolution_env(vm: &VmState) -> Option<&'static str> {
10878    let resolution = vm
10879        .configuration
10880        .js_runtime
10881        .as_ref()
10882        .map(|cfg| cfg.module_resolution)
10883        .unwrap_or(vm_config::JsModuleResolution::Node);
10884    match resolution {
10885        vm_config::JsModuleResolution::Node => None,
10886        vm_config::JsModuleResolution::Relative => Some("relative"),
10887        vm_config::JsModuleResolution::None => Some("none"),
10888    }
10889}
10890
10891/// The builtin allow-list the live resolver should enforce, or `None` to leave
10892/// builtins unrestricted (full Node default — preserving today's behavior).
10893/// Non-node platforms enforce an empty list (deny all builtins).
10894fn js_runtime_enforced_builtins(vm: &VmState) -> Option<Vec<String>> {
10895    if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node {
10896        return Some(Vec::new());
10897    }
10898    vm.configuration
10899        .js_runtime
10900        .as_ref()
10901        .and_then(|cfg| cfg.allowed_builtins.clone())
10902}
10903
10904fn configured_allowed_node_builtins(vm: &VmState) -> Vec<String> {
10905    // Non-node platforms expose no Node builtin modules at all.
10906    if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node {
10907        return Vec::new();
10908    }
10909    // Under the node platform an explicit allow-list wins — including an explicit
10910    // empty list, which means deny all. Absence falls back to the engine default.
10911    let configured = match vm
10912        .configuration
10913        .js_runtime
10914        .as_ref()
10915        .and_then(|cfg| cfg.allowed_builtins.as_ref())
10916    {
10917        Some(list) => list.clone(),
10918        None => DEFAULT_ALLOWED_NODE_BUILTINS
10919            .iter()
10920            .map(|value| (*value).to_owned())
10921            .collect::<Vec<_>>(),
10922    };
10923    dedupe_strings(&configured)
10924}
10925
10926fn configured_loopback_exempt_ports(vm: &VmState) -> Vec<String> {
10927    if !vm.configuration.loopback_exempt_ports.is_empty() {
10928        return vm
10929            .configuration
10930            .loopback_exempt_ports
10931            .iter()
10932            .map(ToString::to_string)
10933            .collect();
10934    }
10935
10936    vm.create_loopback_exempt_ports
10937        .iter()
10938        .map(ToString::to_string)
10939        .collect()
10940}
10941
10942/// Extract the `hostPath` string from a mount plugin's JSON-encoded config.
10943fn mount_config_host_path(config: &str) -> Option<String> {
10944    serde_json::from_str::<Value>(config)
10945        .ok()?
10946        .get("hostPath")
10947        .and_then(Value::as_str)
10948        .map(str::to_owned)
10949}
10950
10951fn runtime_guest_writable_host_paths(vm: &VmState) -> Vec<PathBuf> {
10952    vm.configuration
10953        .mounts
10954        .iter()
10955        .filter(|mount| !mount.read_only)
10956        .filter_map(|mount| {
10957            ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
10958                .then(|| mount_config_host_path(&mount.plugin.config))
10959                .flatten()
10960                .map(PathBuf::from)
10961        })
10962        .collect()
10963}
10964
10965fn runtime_guest_path_mappings(vm: &VmState) -> Vec<RuntimeGuestPathMapping> {
10966    let mut mappings = vm
10967        .configuration
10968        .mounts
10969        .iter()
10970        .filter_map(|mount| {
10971            ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
10972                .then(|| {
10973                    mount_config_host_path(&mount.plugin.config).map(|host_path| {
10974                        RuntimeGuestPathMapping {
10975                            guest_path: normalize_path(&mount.guest_path),
10976                            host_path,
10977                            read_only: mount.read_only,
10978                        }
10979                    })
10980                })
10981                .flatten()
10982        })
10983        .collect::<Vec<_>>();
10984    let mut command_root_mappings = vm
10985        .command_guest_paths
10986        .values()
10987        .filter_map(|guest_path| {
10988            Path::new(guest_path)
10989                .parent()
10990                .and_then(|parent| parent.to_str())
10991                .map(normalize_path)
10992        })
10993        .collect::<BTreeSet<_>>()
10994        .into_iter()
10995        .map(|guest_path| RuntimeGuestPathMapping {
10996            host_path: resolve_vm_guest_path_to_host(vm, &guest_path)
10997                .to_string_lossy()
10998                .into_owned(),
10999            guest_path,
11000            read_only: false,
11001        })
11002        .collect::<Vec<_>>();
11003    mappings.append(&mut command_root_mappings);
11004    let mut extra_node_modules_roots = mappings
11005        .iter()
11006        .filter(|mapping| mapping.guest_path.starts_with("/root/node_modules/"))
11007        .filter_map(|mapping| {
11008            host_node_modules_root(Path::new(&mapping.host_path)).map(|host_root| {
11009                RuntimeGuestPathMapping {
11010                    guest_path: String::from("/root/node_modules"),
11011                    host_path: host_root.to_string_lossy().into_owned(),
11012                    read_only: mapping.read_only,
11013                }
11014            })
11015        })
11016        .collect::<Vec<_>>();
11017    mappings.append(&mut extra_node_modules_roots);
11018    mappings.push(RuntimeGuestPathMapping {
11019        guest_path: String::from("/"),
11020        host_path: vm.cwd.to_string_lossy().into_owned(),
11021        read_only: false,
11022    });
11023    mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.guest_path.len()));
11024    mappings.dedup_by(|left, right| {
11025        left.guest_path == right.guest_path && left.host_path == right.host_path
11026    });
11027    mappings
11028}
11029
11030/// Build a `Send`-able, read-only VFS module reader over the VM's read-only
11031/// `host_dir`/`module_access` mounts (and the derived `/root/node_modules` root
11032/// for nested mounts). When present, the V8 bridge thread resolves modules
11033/// inline against this reader — concurrently with the service loop — so a large
11034/// cold-start module graph never serializes behind / starves an in-flight ACP
11035/// `session/new` bootstrap on the single service-loop thread. The reader reads
11036/// the same mounted tree the guest sees (anchored `openat2`, escaping-symlink
11037/// refusal), never the host-direct path translator. Returns `None` when the VM
11038/// has no usable read-only mount, so resolution falls back to the service-loop
11039/// kernel reader.
11040fn build_module_reader(
11041    vm: &VmState,
11042    resolved: &ResolvedChildProcessExecution,
11043) -> Option<crate::plugins::host_dir::HostDirModuleReader> {
11044    let mut pairs: Vec<(String, PathBuf)> = vm
11045        .configuration
11046        .mounts
11047        .iter()
11048        .filter(|mount| mount.read_only)
11049        .filter(|mount| (mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
11050        .filter_map(|mount| {
11051            mount_config_host_path(&mount.plugin.config)
11052                .map(|host_path| (normalize_path(&mount.guest_path), PathBuf::from(host_path)))
11053        })
11054        .collect();
11055
11056    let guest_entrypoint = resolved
11057        .env
11058        .get("AGENTOS_GUEST_ENTRYPOINT")
11059        .map(|path| normalize_path(path));
11060    if let Some(guest_entrypoint) = guest_entrypoint.as_deref() {
11061        let entrypoint_in_read_only_mount = pairs.iter().any(|(guest_path, _)| {
11062            guest_entrypoint == guest_path
11063                || guest_entrypoint.starts_with(&format!("{guest_path}/"))
11064        });
11065        if !entrypoint_in_read_only_mount {
11066            return None;
11067        }
11068    }
11069
11070    // Mirror runtime_guest_path_mappings: a mount nested under
11071    // `/root/node_modules/<pkg>` implies a `/root/node_modules` root the resolver
11072    // walks, so expose that root too (e.g. software-package mounts).
11073    let extra_roots: Vec<(String, PathBuf)> = pairs
11074        .iter()
11075        .filter(|(guest_path, _)| guest_path.starts_with("/root/node_modules/"))
11076        .filter_map(|(_, host_path)| {
11077            host_node_modules_root(host_path).map(|root| (String::from("/root/node_modules"), root))
11078        })
11079        .collect();
11080    pairs.extend(extra_roots);
11081
11082    crate::plugins::host_dir::HostDirModuleReader::from_mounts(pairs)
11083}
11084
11085fn host_node_modules_root(path: &Path) -> Option<PathBuf> {
11086    if let Some(root) = path
11087        .ancestors()
11088        .filter(|candidate| {
11089            candidate.file_name().and_then(|name| name.to_str()) == Some("node_modules")
11090        })
11091        .last()
11092        .map(Path::to_path_buf)
11093    {
11094        return Some(root);
11095    }
11096
11097    fs::canonicalize(path)
11098        .ok()?
11099        .ancestors()
11100        .filter(|candidate| {
11101            candidate.file_name().and_then(|name| name.to_str()) == Some("node_modules")
11102        })
11103        .last()
11104        .map(Path::to_path_buf)
11105}
11106
11107#[cfg(test)]
11108mod runtime_guest_path_mapping_tests {
11109    use super::{host_node_modules_root, javascript_sync_rpc_option_bool};
11110    use serde_json::json;
11111    use std::fs;
11112    use std::time::{SystemTime, UNIX_EPOCH};
11113
11114    #[test]
11115    fn host_node_modules_root_prefers_workspace_root_over_pnpm_package_node_modules() {
11116        let unique = SystemTime::now()
11117            .duration_since(UNIX_EPOCH)
11118            .expect("clock should be monotonic")
11119            .as_nanos();
11120        let temp = std::env::temp_dir().join(format!("secure-exec-sidecar-node-modules-{unique}"));
11121        let workspace_node_modules = temp.join("node_modules");
11122        let package_root = workspace_node_modules
11123            .join(".pnpm")
11124            .join("example@1.0.0")
11125            .join("node_modules")
11126            .join("@scope")
11127            .join("pkg");
11128        fs::create_dir_all(&package_root).expect("package root should be created");
11129
11130        let resolved =
11131            host_node_modules_root(&package_root).expect("node_modules root should resolve");
11132
11133        assert_eq!(resolved, workspace_node_modules);
11134
11135        fs::remove_dir_all(&temp).expect("temp tree should be removed");
11136    }
11137
11138    #[test]
11139    fn host_node_modules_root_preserves_symlinked_workspace_node_modules_path() {
11140        let unique = SystemTime::now()
11141            .duration_since(UNIX_EPOCH)
11142            .expect("clock should be monotonic")
11143            .as_nanos();
11144        let temp =
11145            std::env::temp_dir().join(format!("secure-exec-sidecar-node-modules-symlink-{unique}"));
11146        let workspace_node_modules = temp.join("node_modules");
11147        let package_link = workspace_node_modules.join("@scope").join("pkg");
11148        let real_package = temp.join("registry").join("agent").join("pkg");
11149        fs::create_dir_all(package_link.parent().expect("package parent should exist"))
11150            .expect("scoped parent should be created");
11151        fs::create_dir_all(&real_package).expect("real package root should be created");
11152        std::os::unix::fs::symlink(&real_package, &package_link)
11153            .expect("package symlink should be created");
11154
11155        let resolved =
11156            host_node_modules_root(&package_link).expect("node_modules root should resolve");
11157
11158        assert_eq!(resolved, workspace_node_modules);
11159
11160        fs::remove_dir_all(&temp).expect("temp tree should be removed");
11161    }
11162
11163    #[test]
11164    fn javascript_sync_rpc_option_bool_accepts_boolean_recursive_argument() {
11165        assert_eq!(
11166            javascript_sync_rpc_option_bool(&[json!("/workspace"), json!(true)], 1, "recursive"),
11167            Some(true)
11168        );
11169        assert_eq!(
11170            javascript_sync_rpc_option_bool(
11171                &[json!("/workspace"), json!({ "recursive": false })],
11172                1,
11173                "recursive"
11174            ),
11175            Some(false)
11176        );
11177    }
11178}
11179
11180#[cfg(test)]
11181mod kernel_poll_sync_rpc_tests {
11182    use super::{
11183        service_javascript_kernel_poll_sync_rpc, ActiveExecution, ActiveProcess,
11184        JavascriptSyncRpcRequest, KernelPollFdResponse, SidecarKernel, ToolExecution,
11185        EXECUTION_DRIVER_NAME, JAVASCRIPT_COMMAND,
11186    };
11187    use secure_exec_kernel::command_registry::CommandDriver;
11188    use secure_exec_kernel::kernel::{KernelVmConfig, SpawnOptions};
11189    use secure_exec_kernel::mount_table::MountTable;
11190    use secure_exec_kernel::permissions::Permissions;
11191    use secure_exec_kernel::poll::{POLLHUP, POLLIN};
11192    use secure_exec_kernel::vfs::MemoryFileSystem;
11193    use serde_json::{json, Value};
11194    use std::collections::HashMap;
11195    #[test]
11196    fn javascript_kernel_poll_sync_rpc_reports_multiple_kernel_fds() {
11197        let mut config = KernelVmConfig::new("vm-js-kernel-poll");
11198        config.permissions = Permissions::allow_all();
11199        let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config);
11200        kernel
11201            .register_driver(CommandDriver::new(
11202                EXECUTION_DRIVER_NAME,
11203                [JAVASCRIPT_COMMAND],
11204            ))
11205            .expect("register execution driver");
11206
11207        let kernel_handle = kernel
11208            .spawn_process(
11209                JAVASCRIPT_COMMAND,
11210                Vec::new(),
11211                SpawnOptions {
11212                    requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
11213                    ..SpawnOptions::default()
11214                },
11215            )
11216            .expect("spawn javascript kernel process");
11217        let pid = kernel_handle.pid();
11218
11219        let (stdin_read_fd, stdin_write_fd) = kernel
11220            .open_pipe(EXECUTION_DRIVER_NAME, pid)
11221            .expect("open kernel stdin pipe");
11222        kernel
11223            .fd_dup2(EXECUTION_DRIVER_NAME, pid, stdin_read_fd, 0)
11224            .expect("dup stdin pipe onto fd 0");
11225        kernel
11226            .fd_close(EXECUTION_DRIVER_NAME, pid, stdin_read_fd)
11227            .expect("close original stdin read fd");
11228
11229        let process = ActiveProcess::new(
11230            pid,
11231            kernel_handle,
11232            super::GuestRuntimeKind::JavaScript,
11233            ActiveExecution::Tool(ToolExecution::default()),
11234        );
11235
11236        kernel
11237            .fd_write(EXECUTION_DRIVER_NAME, pid, stdin_write_fd, b"poll-ready")
11238            .expect("write kernel stdin payload");
11239        kernel
11240            .fd_close(EXECUTION_DRIVER_NAME, pid, stdin_write_fd)
11241            .expect("close kernel stdin writer");
11242
11243        let response = service_javascript_kernel_poll_sync_rpc(
11244            &mut kernel,
11245            &process,
11246            &JavascriptSyncRpcRequest {
11247                id: 1,
11248                method: String::from("__kernel_poll"),
11249                raw_bytes_args: HashMap::new(),
11250                args: vec![
11251                    json!([
11252                        { "fd": 0, "events": POLLIN.bits() },
11253                        { "fd": 1, "events": POLLIN.bits() }
11254                    ]),
11255                    json!(250),
11256                ],
11257            },
11258        )
11259        .expect("poll kernel fds");
11260
11261        assert_eq!(response["readyCount"], Value::from(1));
11262        let fds: Vec<KernelPollFdResponse> =
11263            serde_json::from_value(response["fds"].clone()).expect("kernel poll fd response");
11264        assert_eq!(
11265            fds,
11266            vec![
11267                KernelPollFdResponse {
11268                    fd: 0,
11269                    events: POLLIN.bits(),
11270                    revents: (POLLIN | POLLHUP).bits(),
11271                },
11272                KernelPollFdResponse {
11273                    fd: 1,
11274                    events: POLLIN.bits(),
11275                    revents: 0,
11276                },
11277            ]
11278        );
11279
11280        process.kernel_handle.finish(0);
11281        kernel.waitpid(pid).expect("wait javascript kernel process");
11282    }
11283}
11284
11285fn dedupe_strings(values: &[String]) -> Vec<String> {
11286    let mut seen = BTreeSet::new();
11287    let mut deduped = Vec::new();
11288    for value in values {
11289        if seen.insert(value.clone()) {
11290            deduped.push(value.clone());
11291        }
11292    }
11293    deduped
11294}
11295
11296fn dedupe_host_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
11297    let mut seen = BTreeSet::new();
11298    let mut deduped = Vec::new();
11299    for path in paths {
11300        let normalized = normalize_host_path(path);
11301        let key = normalized.to_string_lossy().into_owned();
11302        if seen.insert(key) {
11303            deduped.push(normalized);
11304        }
11305    }
11306    deduped
11307}
11308
11309fn expand_host_access_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
11310    let mut expanded = Vec::new();
11311    let mut seen = BTreeSet::new();
11312
11313    let mut add_path = |candidate: PathBuf| {
11314        let normalized = normalize_host_path(&candidate);
11315        let key = normalized.to_string_lossy().into_owned();
11316        if seen.insert(key) {
11317            expanded.push(normalized);
11318        }
11319    };
11320
11321    for host_path in paths {
11322        add_path(host_path.clone());
11323        if let Ok(realpath) = fs::canonicalize(host_path) {
11324            add_path(realpath);
11325        }
11326
11327        if host_path.file_name().and_then(|name| name.to_str()) != Some("node_modules") {
11328            continue;
11329        }
11330
11331        let mut current = host_path.parent();
11332        while let Some(parent) = current {
11333            let candidate = parent.join("node_modules");
11334            if candidate.exists() {
11335                add_path(candidate.clone());
11336                if let Ok(realpath) = fs::canonicalize(&candidate) {
11337                    add_path(realpath);
11338                }
11339            }
11340            current = parent.parent();
11341        }
11342    }
11343
11344    expanded
11345}
11346
11347fn prepare_javascript_shadow(
11348    vm: &mut VmState,
11349    resolved: &ResolvedChildProcessExecution,
11350) -> Result<(), SidecarError> {
11351    let guest_entrypoint = resolved
11352        .env
11353        .get("AGENTOS_GUEST_ENTRYPOINT")
11354        .cloned()
11355        // An absolute `entrypoint` may be a host path that lives inside the VM's
11356        // host cwd (callers can pass a fully-qualified host path). The guest sees
11357        // it at its translated guest path (host_cwd -> guest_cwd), so the shadow
11358        // must be keyed by that guest path rather than the raw host path. Falling
11359        // back to the host path here would materialize the file at the wrong guest
11360        // location and the runtime's `require()` would fail with "Cannot find
11361        // module".
11362        .or_else(|| {
11363            resolve_host_entrypoint_within_vm_host_cwd(vm, &resolved.entrypoint)
11364                .map(|(guest_entrypoint, _)| guest_entrypoint)
11365        })
11366        .or_else(|| {
11367            resolved
11368                .entrypoint
11369                .starts_with('/')
11370                .then(|| normalize_path(&resolved.entrypoint))
11371        });
11372    let Some(guest_entrypoint) = guest_entrypoint else {
11373        return Ok(());
11374    };
11375    if host_mount_path_for_guest_path(vm, &guest_entrypoint).is_some() {
11376        return Ok(());
11377    }
11378    if vm.kernel.lstat(&guest_entrypoint).is_err() {
11379        let host_entrypoint = {
11380            let candidate = Path::new(&resolved.entrypoint);
11381            if candidate.is_absolute() {
11382                candidate.to_path_buf()
11383            } else {
11384                resolved.host_cwd.join(candidate)
11385            }
11386        };
11387        if host_entrypoint.exists() {
11388            materialize_host_path_to_shadow(vm, &guest_entrypoint, &host_entrypoint)?;
11389            // The shadow write only stages the file on the host side; the runtime
11390            // resolves modules against the kernel VFS, so the staged entrypoint
11391            // must be synced into the kernel before execution starts (otherwise
11392            // `require()` reports "Cannot find module").
11393            return sync_shadow_entrypoint_into_kernel(vm, &guest_entrypoint);
11394        }
11395    }
11396    materialize_guest_path_to_shadow(vm, &guest_entrypoint)
11397}
11398
11399/// Sync a freshly-staged shadow entrypoint into the kernel VFS so the runtime's
11400/// kernel-backed module resolver can read it. Mirrors the host->kernel file sync
11401/// used by the broader shadow reconciliation, but scoped to the single
11402/// entrypoint we just materialized.
11403fn sync_shadow_entrypoint_into_kernel(
11404    vm: &mut VmState,
11405    guest_entrypoint: &str,
11406) -> Result<(), SidecarError> {
11407    if vm.kernel.exists(guest_entrypoint).unwrap_or(false) {
11408        return Ok(());
11409    }
11410    let shadow_path = shadow_path_for_guest(vm, guest_entrypoint);
11411    let bytes = match fs::read(&shadow_path) {
11412        Ok(bytes) => bytes,
11413        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
11414        Err(error) => {
11415            return Err(SidecarError::Io(format!(
11416                "failed to read staged shadow entrypoint {}: {error}",
11417                shadow_path.display()
11418            )));
11419        }
11420    };
11421    if let Some(parent) = guest_parent_path(guest_entrypoint) {
11422        if !vm.kernel.exists(&parent).unwrap_or(false) {
11423            vm.kernel.mkdir(&parent, true).map_err(kernel_error)?;
11424        }
11425    }
11426    vm.kernel
11427        .write_file(guest_entrypoint, bytes)
11428        .map_err(kernel_error)?;
11429    Ok(())
11430}
11431
11432fn guest_parent_path(guest_path: &str) -> Option<String> {
11433    let parent = Path::new(guest_path).parent()?;
11434    let parent = parent.to_string_lossy();
11435    if parent.is_empty() || parent == "/" {
11436        None
11437    } else {
11438        Some(parent.into_owned())
11439    }
11440}
11441
11442fn materialize_host_path_to_shadow(
11443    vm: &VmState,
11444    guest_path: &str,
11445    host_path: &Path,
11446) -> Result<(), SidecarError> {
11447    let shadow_path = shadow_path_for_guest(vm, guest_path);
11448    let metadata = fs::symlink_metadata(host_path)
11449        .map_err(|error| SidecarError::Io(format!("failed to stat host entrypoint: {error}")))?;
11450
11451    if metadata.file_type().is_symlink() {
11452        if let Some(parent) = shadow_path.parent() {
11453            fs::create_dir_all(parent).map_err(|error| {
11454                SidecarError::Io(format!("failed to create shadow symlink parent: {error}"))
11455            })?;
11456        }
11457        let _ = fs::remove_file(&shadow_path);
11458        let _ = fs::remove_dir_all(&shadow_path);
11459        let target = fs::read_link(host_path)
11460            .map_err(|error| SidecarError::Io(format!("failed to read host symlink: {error}")))?;
11461        std::os::unix::fs::symlink(&target, &shadow_path)
11462            .map_err(|error| SidecarError::Io(format!("failed to mirror host symlink: {error}")))?;
11463        return Ok(());
11464    }
11465
11466    if metadata.is_dir() {
11467        fs::create_dir_all(&shadow_path).map_err(|error| {
11468            SidecarError::Io(format!("failed to create shadow directory: {error}"))
11469        })?;
11470        fs::set_permissions(
11471            &shadow_path,
11472            fs::Permissions::from_mode(metadata.permissions().mode() & 0o7777),
11473        )
11474        .map_err(|error| {
11475            SidecarError::Io(format!(
11476                "failed to set shadow directory mode on {}: {error}",
11477                shadow_path.display()
11478            ))
11479        })?;
11480        return Ok(());
11481    }
11482
11483    if let Some(parent) = shadow_path.parent() {
11484        fs::create_dir_all(parent).map_err(|error| {
11485            SidecarError::Io(format!("failed to create shadow parent: {error}"))
11486        })?;
11487    }
11488    let bytes = fs::read(host_path)
11489        .map_err(|error| SidecarError::Io(format!("failed to read host entrypoint: {error}")))?;
11490    fs::write(&shadow_path, bytes).map_err(|error| {
11491        SidecarError::Io(format!(
11492            "failed to mirror host file into shadow root: {error}"
11493        ))
11494    })?;
11495    fs::set_permissions(
11496        &shadow_path,
11497        fs::Permissions::from_mode(metadata.permissions().mode() & 0o7777),
11498    )
11499    .map_err(|error| {
11500        SidecarError::Io(format!(
11501            "failed to set shadow file mode on {}: {error}",
11502            shadow_path.display()
11503        ))
11504    })?;
11505    Ok(())
11506}
11507
11508fn materialize_guest_path_to_shadow(
11509    vm: &mut VmState,
11510    guest_path: &str,
11511) -> Result<(), SidecarError> {
11512    let stat = vm.kernel.lstat(guest_path).map_err(kernel_error)?;
11513    let shadow_path = shadow_path_for_guest(vm, guest_path);
11514
11515    if stat.is_symbolic_link {
11516        if let Some(parent) = shadow_path.parent() {
11517            fs::create_dir_all(parent).map_err(|error| {
11518                SidecarError::Io(format!("failed to create shadow symlink parent: {error}"))
11519            })?;
11520        }
11521        let _ = fs::remove_file(&shadow_path);
11522        let _ = fs::remove_dir_all(&shadow_path);
11523        let target = vm.kernel.read_link(guest_path).map_err(kernel_error)?;
11524        std::os::unix::fs::symlink(&target, &shadow_path)
11525            .map_err(|error| SidecarError::Io(format!("failed to mirror symlink: {error}")))?;
11526        return Ok(());
11527    }
11528
11529    if stat.is_directory {
11530        fs::create_dir_all(&shadow_path).map_err(|error| {
11531            SidecarError::Io(format!("failed to create shadow directory: {error}"))
11532        })?;
11533        fs::set_permissions(&shadow_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err(
11534            |error| {
11535                SidecarError::Io(format!(
11536                    "failed to set shadow directory mode on {}: {error}",
11537                    shadow_path.display()
11538                ))
11539            },
11540        )?;
11541        return Ok(());
11542    }
11543
11544    if let Some(parent) = shadow_path.parent() {
11545        fs::create_dir_all(parent).map_err(|error| {
11546            SidecarError::Io(format!("failed to create shadow parent: {error}"))
11547        })?;
11548    }
11549    let bytes = vm.kernel.read_file(guest_path).map_err(kernel_error)?;
11550    fs::write(&shadow_path, bytes).map_err(|error| {
11551        SidecarError::Io(format!(
11552            "failed to mirror guest file into shadow root: {error}"
11553        ))
11554    })?;
11555    fs::set_permissions(&shadow_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err(
11556        |error| {
11557            SidecarError::Io(format!(
11558                "failed to set shadow file mode on {}: {error}",
11559                shadow_path.display()
11560            ))
11561        },
11562    )?;
11563    Ok(())
11564}
11565
11566fn load_javascript_entrypoint_source(
11567    vm: &mut VmState,
11568    host_cwd: &Path,
11569    entrypoint: &str,
11570    env: &BTreeMap<String, String>,
11571) -> Option<String> {
11572    let mut read_guest_file = |path: &str| {
11573        vm.kernel
11574            .read_file(path)
11575            .ok()
11576            .and_then(|bytes| String::from_utf8(bytes).ok())
11577    };
11578
11579    if let Some(source) = env
11580        .get("AGENTOS_GUEST_ENTRYPOINT")
11581        .filter(|path| path.starts_with('/'))
11582        .and_then(|path| read_guest_file(path))
11583    {
11584        return Some(source);
11585    }
11586
11587    if entrypoint.starts_with('/') {
11588        if let Some(source) = read_guest_file(entrypoint) {
11589            return Some(source);
11590        }
11591    }
11592
11593    let host_entrypoint = if Path::new(entrypoint).is_absolute() {
11594        PathBuf::from(entrypoint)
11595    } else {
11596        host_cwd.join(entrypoint)
11597    };
11598    let normalized_entrypoint = normalize_host_path(&host_entrypoint);
11599    let sandbox_root = normalize_host_path(&vm.cwd);
11600    let host_cwd = normalize_host_path(&vm.host_cwd);
11601    if !path_is_within_root(&normalized_entrypoint, &sandbox_root)
11602        && !path_is_within_root(&normalized_entrypoint, &host_cwd)
11603    {
11604        return None;
11605    }
11606
11607    fs::read_to_string(&normalized_entrypoint).ok()
11608}
11609
11610fn emit_dns_resolution_event<B>(
11611    bridge: &SharedBridge<B>,
11612    vm_id: &str,
11613    hostname: &str,
11614    source: KernelDnsResolutionSource,
11615    addresses: &[IpAddr],
11616    dns: &VmDnsConfig,
11617) where
11618    B: NativeSidecarBridge + Send + 'static,
11619    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11620{
11621    let _ = emit_structured_event(
11622        bridge,
11623        vm_id,
11624        "network.dns.resolved",
11625        audit_fields([
11626            ("hostname", hostname.to_owned()),
11627            ("source", source.as_str().to_owned()),
11628            (
11629                "addresses",
11630                addresses
11631                    .iter()
11632                    .map(ToString::to_string)
11633                    .collect::<Vec<_>>()
11634                    .join(","),
11635            ),
11636            ("address_count", addresses.len().to_string()),
11637            ("resolver_count", dns.name_servers.len().to_string()),
11638            (
11639                "resolvers",
11640                dns.name_servers
11641                    .iter()
11642                    .map(ToString::to_string)
11643                    .collect::<Vec<_>>()
11644                    .join(","),
11645            ),
11646        ]),
11647    );
11648}
11649
11650fn emit_dns_record_resolution_event<B>(
11651    bridge: &SharedBridge<B>,
11652    vm_id: &str,
11653    hostname: &str,
11654    resolution: &DnsRecordResolution,
11655    dns: &VmDnsConfig,
11656) where
11657    B: NativeSidecarBridge + Send + 'static,
11658    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11659{
11660    if let Some(addresses) = dns_resolution_ip_addrs(resolution.records()) {
11661        emit_dns_resolution_event(
11662            bridge,
11663            vm_id,
11664            hostname,
11665            resolution.source(),
11666            &addresses,
11667            dns,
11668        );
11669        return;
11670    }
11671
11672    let _ = emit_structured_event(
11673        bridge,
11674        vm_id,
11675        "network.dns.resolved",
11676        audit_fields([
11677            ("hostname", hostname.to_owned()),
11678            ("source", resolution.source().as_str().to_owned()),
11679            (
11680                "addresses",
11681                resolution
11682                    .records()
11683                    .iter()
11684                    .map(summarize_dns_record)
11685                    .collect::<Vec<_>>()
11686                    .join(","),
11687            ),
11688            ("address_count", resolution.records().len().to_string()),
11689            ("resolver_count", dns.name_servers.len().to_string()),
11690            (
11691                "resolvers",
11692                dns.name_servers
11693                    .iter()
11694                    .map(ToString::to_string)
11695                    .collect::<Vec<_>>()
11696                    .join(","),
11697            ),
11698        ]),
11699    );
11700}
11701
11702fn emit_dns_resolution_failure_event<B>(
11703    bridge: &SharedBridge<B>,
11704    vm_id: &str,
11705    hostname: &str,
11706    dns: &VmDnsConfig,
11707    error: &SidecarError,
11708) where
11709    B: NativeSidecarBridge + Send + 'static,
11710    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11711{
11712    let _ = emit_structured_event(
11713        bridge,
11714        vm_id,
11715        "network.dns.resolve_failed",
11716        audit_fields([
11717            ("hostname", hostname.to_owned()),
11718            ("reason", error.to_string()),
11719            ("resolver_count", dns.name_servers.len().to_string()),
11720            (
11721                "resolvers",
11722                dns.name_servers
11723                    .iter()
11724                    .map(ToString::to_string)
11725                    .collect::<Vec<_>>()
11726                    .join(","),
11727            ),
11728        ]),
11729    );
11730}
11731
11732fn parse_dns_record_type(rrtype: &str) -> Result<RecordType, SidecarError> {
11733    match rrtype {
11734        "A" => Ok(RecordType::A),
11735        "AAAA" => Ok(RecordType::AAAA),
11736        "MX" => Ok(RecordType::MX),
11737        "TXT" => Ok(RecordType::TXT),
11738        "SRV" => Ok(RecordType::SRV),
11739        "CNAME" => Ok(RecordType::CNAME),
11740        "PTR" => Ok(RecordType::PTR),
11741        "NS" => Ok(RecordType::NS),
11742        "SOA" => Ok(RecordType::SOA),
11743        "NAPTR" => Ok(RecordType::NAPTR),
11744        "CAA" => Ok(RecordType::CAA),
11745        "ANY" => Ok(RecordType::ANY),
11746        other => Err(SidecarError::Execution(format!(
11747            "ERR_NOT_IMPLEMENTED: dns rrtype {other} is not supported by the secure-exec dns bridge"
11748        ))),
11749    }
11750}
11751
11752fn dns_resolution_to_node_value(
11753    resolution: &DnsRecordResolution,
11754    requested_type: &str,
11755) -> Result<Value, SidecarError> {
11756    let safe_ips = dns_resolution_safe_ip_set(resolution.records(), resolution.hostname())?;
11757    match requested_type {
11758        "A" | "AAAA" => Ok(Value::Array(
11759            resolution
11760                .records()
11761                .iter()
11762                .filter_map(|record| dns_record_ip_string(record, &safe_ips))
11763                .map(Value::String)
11764                .collect(),
11765        )),
11766        "MX" => Ok(Value::Array(
11767            resolution
11768                .records()
11769                .iter()
11770                .filter_map(|record| match record.data() {
11771                    RData::MX(mx) => Some(json!({
11772                        "priority": mx.preference,
11773                        "exchange": normalize_dns_name_for_node(&mx.exchange),
11774                        "type": "MX",
11775                    })),
11776                    _ => None,
11777                })
11778                .collect(),
11779        )),
11780        "TXT" => Ok(Value::Array(
11781            resolution
11782                .records()
11783                .iter()
11784                .filter_map(|record| match record.data() {
11785                    RData::TXT(txt) => Some(Value::Array(
11786                        txt.txt_data
11787                            .iter()
11788                            .map(|entry| Value::String(String::from_utf8_lossy(entry).into_owned()))
11789                            .collect(),
11790                    )),
11791                    _ => None,
11792                })
11793                .collect(),
11794        )),
11795        "SRV" => Ok(Value::Array(
11796            resolution
11797                .records()
11798                .iter()
11799                .filter_map(|record| match record.data() {
11800                    RData::SRV(srv) => Some(json!({
11801                        "priority": srv.priority,
11802                        "weight": srv.weight,
11803                        "port": srv.port,
11804                        "name": normalize_dns_name_for_node(&srv.target),
11805                        "type": "SRV",
11806                    })),
11807                    _ => None,
11808                })
11809                .collect(),
11810        )),
11811        "CNAME" => Ok(Value::Array(
11812            resolution
11813                .records()
11814                .iter()
11815                .filter_map(|record| match record.data() {
11816                    RData::CNAME(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
11817                    _ => None,
11818                })
11819                .collect(),
11820        )),
11821        "PTR" => Ok(Value::Array(
11822            resolution
11823                .records()
11824                .iter()
11825                .filter_map(|record| match record.data() {
11826                    RData::PTR(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
11827                    _ => None,
11828                })
11829                .collect(),
11830        )),
11831        "NS" => Ok(Value::Array(
11832            resolution
11833                .records()
11834                .iter()
11835                .filter_map(|record| match record.data() {
11836                    RData::NS(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
11837                    _ => None,
11838                })
11839                .collect(),
11840        )),
11841        "SOA" => resolution
11842            .records()
11843            .iter()
11844            .find_map(|record| match record.data() {
11845                RData::SOA(soa) => Some(json!({
11846                    "nsname": normalize_dns_name_for_node(&soa.mname),
11847                    "hostmaster": normalize_dns_name_for_node(&soa.rname),
11848                    "serial": soa.serial,
11849                    "refresh": soa.refresh,
11850                    "retry": soa.retry,
11851                    "expire": soa.expire,
11852                    "minttl": soa.minimum,
11853                })),
11854                _ => None,
11855            })
11856            .ok_or_else(|| {
11857                SidecarError::Execution(String::from("failed to resolve DNS SOA record"))
11858            }),
11859        "NAPTR" => Ok(Value::Array(
11860            resolution
11861                .records()
11862                .iter()
11863                .filter_map(|record| match record.data() {
11864                    RData::NAPTR(naptr) => Some(json!({
11865                        "flags": String::from_utf8_lossy(&naptr.flags).into_owned(),
11866                        "service": String::from_utf8_lossy(&naptr.services).into_owned(),
11867                        "regexp": String::from_utf8_lossy(&naptr.regexp).into_owned(),
11868                        "replacement": normalize_dns_name_for_node(&naptr.replacement),
11869                        "order": naptr.order,
11870                        "preference": naptr.preference,
11871                    })),
11872                    _ => None,
11873                })
11874                .collect(),
11875        )),
11876        "CAA" => Ok(Value::Array(
11877            resolution
11878                .records()
11879                .iter()
11880                .filter_map(|record| match record.data() {
11881                    RData::CAA(caa) => {
11882                        let mut value = serde_json::Map::new();
11883                        value.insert(
11884                            "critical".to_owned(),
11885                            Value::from(u8::from(caa.issuer_critical)),
11886                        );
11887                        value.insert("type".to_owned(), Value::String(String::from("CAA")));
11888                        if caa.tag.eq_ignore_ascii_case("iodef") {
11889                            value.insert(
11890                                "iodef".to_owned(),
11891                                Value::String(
11892                                    caa.value_as_iodef()
11893                                        .map(|url| url.to_string())
11894                                        .unwrap_or_else(|_| {
11895                                            String::from_utf8_lossy(&caa.value).into_owned()
11896                                        }),
11897                                ),
11898                            );
11899                        } else if let Ok((issuer, _params)) = caa.value_as_issue() {
11900                            let field = if caa.tag.eq_ignore_ascii_case("issuewild") {
11901                                "issuewild"
11902                            } else {
11903                                "issue"
11904                            };
11905                            value.insert(
11906                                field.to_owned(),
11907                                Value::String(
11908                                    issuer.as_ref().map(ToString::to_string).unwrap_or_else(|| {
11909                                        String::from_utf8_lossy(&caa.value).into_owned()
11910                                    }),
11911                                ),
11912                            );
11913                        } else {
11914                            value.insert(
11915                                caa.tag.to_ascii_lowercase(),
11916                                Value::String(String::from_utf8_lossy(&caa.value).into_owned()),
11917                            );
11918                        }
11919                        Some(Value::Object(value))
11920                    }
11921                    _ => None,
11922                })
11923                .collect(),
11924        )),
11925        "ANY" => Ok(Value::Array(
11926            resolution
11927                .records()
11928                .iter()
11929                .filter_map(|record| dns_any_record_to_value(record, &safe_ips))
11930                .collect(),
11931        )),
11932        other => Err(SidecarError::Execution(format!(
11933            "ERR_NOT_IMPLEMENTED: dns rrtype {other} is not supported by the secure-exec dns bridge"
11934        ))),
11935    }
11936}
11937
11938fn dns_resolution_safe_ip_set(
11939    records: &[Record],
11940    hostname: &str,
11941) -> Result<BTreeSet<IpAddr>, SidecarError> {
11942    let ips = records
11943        .iter()
11944        .filter_map(dns_record_ip_addr)
11945        .collect::<Vec<_>>();
11946    if ips.is_empty() {
11947        return Ok(BTreeSet::new());
11948    }
11949    Ok(filter_dns_safe_ip_addrs(ips, hostname)?
11950        .into_iter()
11951        .collect())
11952}
11953
11954fn dns_resolution_ip_addrs(records: &[Record]) -> Option<Vec<IpAddr>> {
11955    let ips = records
11956        .iter()
11957        .filter_map(dns_record_ip_addr)
11958        .collect::<Vec<_>>();
11959    if ips.is_empty() {
11960        return None;
11961    }
11962    Some(ips)
11963}
11964
11965fn dns_record_ip_addr(record: &Record) -> Option<IpAddr> {
11966    match record.data() {
11967        RData::A(address) => Some(IpAddr::V4(**address)),
11968        RData::AAAA(address) => Some(IpAddr::V6(**address)),
11969        _ => None,
11970    }
11971}
11972
11973fn dns_record_ip_string(record: &Record, safe_ips: &BTreeSet<IpAddr>) -> Option<String> {
11974    let ip = dns_record_ip_addr(record)?;
11975    safe_ips.contains(&ip).then(|| ip.to_string())
11976}
11977
11978fn dns_any_record_to_value(record: &Record, safe_ips: &BTreeSet<IpAddr>) -> Option<Value> {
11979    let value = match record.data() {
11980        RData::A(_) | RData::AAAA(_) => json!({
11981            "address": dns_record_ip_string(record, safe_ips)?,
11982            "ttl": record.ttl(),
11983            "type": record.record_type().to_string(),
11984        }),
11985        RData::MX(mx) => json!({
11986            "exchange": normalize_dns_name_for_node(&mx.exchange),
11987            "priority": mx.preference,
11988            "type": "MX",
11989        }),
11990        RData::TXT(txt) => json!({
11991            "entries": txt
11992                .txt_data
11993                .iter()
11994                .map(|entry| String::from_utf8_lossy(entry).into_owned())
11995                .collect::<Vec<_>>(),
11996            "type": "TXT",
11997        }),
11998        RData::SRV(srv) => json!({
11999            "name": normalize_dns_name_for_node(&srv.target),
12000            "port": srv.port,
12001            "priority": srv.priority,
12002            "weight": srv.weight,
12003            "type": "SRV",
12004        }),
12005        RData::CNAME(name) => json!({
12006            "value": normalize_dns_name_for_node(&name.0),
12007            "type": "CNAME",
12008        }),
12009        RData::PTR(name) => json!({
12010            "value": normalize_dns_name_for_node(&name.0),
12011            "type": "PTR",
12012        }),
12013        RData::NS(name) => json!({
12014            "value": normalize_dns_name_for_node(&name.0),
12015            "type": "NS",
12016        }),
12017        RData::SOA(soa) => json!({
12018            "nsname": normalize_dns_name_for_node(&soa.mname),
12019            "hostmaster": normalize_dns_name_for_node(&soa.rname),
12020            "serial": soa.serial,
12021            "refresh": soa.refresh,
12022            "retry": soa.retry,
12023            "expire": soa.expire,
12024            "minttl": soa.minimum,
12025            "type": "SOA",
12026        }),
12027        RData::NAPTR(naptr) => json!({
12028            "flags": String::from_utf8_lossy(&naptr.flags).into_owned(),
12029            "service": String::from_utf8_lossy(&naptr.services).into_owned(),
12030            "regexp": String::from_utf8_lossy(&naptr.regexp).into_owned(),
12031            "replacement": normalize_dns_name_for_node(&naptr.replacement),
12032            "order": naptr.order,
12033            "preference": naptr.preference,
12034            "type": "NAPTR",
12035        }),
12036        RData::CAA(caa) => {
12037            let mut value = serde_json::Map::new();
12038            value.insert(
12039                "critical".to_owned(),
12040                Value::from(u8::from(caa.issuer_critical)),
12041            );
12042            value.insert("type".to_owned(), Value::String(String::from("CAA")));
12043            if caa.tag.eq_ignore_ascii_case("iodef") {
12044                value.insert(
12045                    "iodef".to_owned(),
12046                    Value::String(
12047                        caa.value_as_iodef()
12048                            .map(|url| url.to_string())
12049                            .unwrap_or_else(|_| String::from_utf8_lossy(&caa.value).into_owned()),
12050                    ),
12051                );
12052            } else if let Ok((issuer, _params)) = caa.value_as_issue() {
12053                let field = if caa.tag.eq_ignore_ascii_case("issuewild") {
12054                    "issuewild"
12055                } else {
12056                    "issue"
12057                };
12058                value.insert(
12059                    field.to_owned(),
12060                    Value::String(
12061                        issuer
12062                            .as_ref()
12063                            .map(ToString::to_string)
12064                            .unwrap_or_else(|| String::from_utf8_lossy(&caa.value).into_owned()),
12065                    ),
12066                );
12067            }
12068            Value::Object(value)
12069        }
12070        _ => return None,
12071    };
12072    Some(value)
12073}
12074
12075fn normalize_dns_name_for_node(name: &impl ToString) -> String {
12076    name.to_string().trim_end_matches('.').to_owned()
12077}
12078
12079fn summarize_dns_record(record: &Record) -> String {
12080    match record.data() {
12081        RData::A(_) | RData::AAAA(_) => record.data().to_string(),
12082        _ => format!("{} {}", record.record_type(), record.data()),
12083    }
12084}
12085
12086// build_root_filesystem, convert_root_lower_descriptor, convert_root_filesystem_entry,
12087// root_snapshot_entry moved to crate::bootstrap
12088
12089// apply_root_filesystem_entry, ensure_parent_directories moved to crate::bootstrap
12090
12091// ProcNetEntry moved to crate::state
12092
12093fn find_socket_state_entry(
12094    vm: Option<&VmState>,
12095    kind: SocketQueryKind,
12096    request: &FindListenerRequest,
12097) -> Result<Option<SocketStateEntry>, SidecarError> {
12098    let vm = vm.ok_or_else(|| SidecarError::InvalidState(String::from("unknown sidecar VM")))?;
12099
12100    for (process_id, process) in &vm.active_processes {
12101        if let Some(path) = request.path.as_deref() {
12102            if matches!(kind, SocketQueryKind::TcpListener) {
12103                for listener in process.unix_listeners.values() {
12104                    if listener.path() != path {
12105                        continue;
12106                    }
12107                    return Ok(Some(SocketStateEntry {
12108                        process_id: process_id.to_owned(),
12109                        host: None,
12110                        port: None,
12111                        path: Some(path.to_owned()),
12112                    }));
12113                }
12114            }
12115        }
12116
12117        if request.path.is_none() {
12118            if let Some(entry) =
12119                find_kernel_socket_state_entry(&vm.kernel, process_id, process, kind, request)?
12120            {
12121                return Ok(Some(entry));
12122            }
12123
12124            match kind {
12125                SocketQueryKind::TcpListener => {
12126                    for server in process.http_servers.values() {
12127                        let local_addr = server.guest_local_addr;
12128                        let local_host = local_addr.ip().to_string();
12129                        if !socket_host_matches(request.host.as_deref(), &local_host) {
12130                            continue;
12131                        }
12132                        if let Some(port) = request.port {
12133                            if local_addr.port() != port {
12134                                continue;
12135                            }
12136                        }
12137                        return Ok(Some(SocketStateEntry {
12138                            process_id: process_id.to_owned(),
12139                            host: Some(local_host),
12140                            port: Some(local_addr.port()),
12141                            path: None,
12142                        }));
12143                    }
12144
12145                    for listener in process.tcp_listeners.values() {
12146                        if listener.kernel_socket_id.is_some() {
12147                            continue;
12148                        }
12149                        let local_addr = listener.guest_local_addr();
12150                        let local_host = local_addr.ip().to_string();
12151                        if !socket_host_matches(request.host.as_deref(), &local_host) {
12152                            continue;
12153                        }
12154                        if let Some(port) = request.port {
12155                            if local_addr.port() != port {
12156                                continue;
12157                            }
12158                        }
12159                        return Ok(Some(SocketStateEntry {
12160                            process_id: process_id.to_owned(),
12161                            host: Some(local_host),
12162                            port: Some(local_addr.port()),
12163                            path: None,
12164                        }));
12165                    }
12166                }
12167                SocketQueryKind::UdpBound => {
12168                    for socket in process.udp_sockets.values() {
12169                        if socket.kernel_socket_id.is_some() {
12170                            continue;
12171                        }
12172                        let Some(local_addr) = socket.local_addr() else {
12173                            continue;
12174                        };
12175                        let local_host = local_addr.ip().to_string();
12176                        if !socket_host_matches(request.host.as_deref(), &local_host) {
12177                            continue;
12178                        }
12179                        if let Some(port) = request.port {
12180                            if local_addr.port() != port {
12181                                continue;
12182                            }
12183                        }
12184                        return Ok(Some(SocketStateEntry {
12185                            process_id: process_id.to_owned(),
12186                            host: Some(local_host),
12187                            port: Some(local_addr.port()),
12188                            path: None,
12189                        }));
12190                    }
12191                }
12192            }
12193        }
12194
12195        let child_pid = process.execution.child_pid();
12196        let inodes = socket_inodes_for_pid(child_pid)?;
12197        if inodes.is_empty() {
12198            continue;
12199        }
12200
12201        if let Some(path) = request.path.as_deref() {
12202            if let Some(listener) = find_unix_socket_for_pid(child_pid, &inodes, path, process_id)?
12203            {
12204                return Ok(Some(listener));
12205            }
12206            continue;
12207        }
12208
12209        let table_paths = match kind {
12210            SocketQueryKind::TcpListener => [
12211                format!("/proc/{child_pid}/net/tcp"),
12212                format!("/proc/{child_pid}/net/tcp6"),
12213            ],
12214            SocketQueryKind::UdpBound => [
12215                format!("/proc/{child_pid}/net/udp"),
12216                format!("/proc/{child_pid}/net/udp6"),
12217            ],
12218        };
12219        for table_path in table_paths {
12220            if let Some(entry) = find_inet_socket_for_pid(
12221                &table_path,
12222                &inodes,
12223                kind,
12224                request.host.as_deref(),
12225                request.port,
12226                process_id,
12227            )? {
12228                return Ok(Some(entry));
12229            }
12230        }
12231    }
12232
12233    Ok(None)
12234}
12235
12236fn require_vm_inspection_permission<B>(
12237    bridge: &SharedBridge<B>,
12238    vm_id: &str,
12239    capability: &str,
12240    domain: &str,
12241    resource: &str,
12242) -> Result<(), SidecarError>
12243where
12244    B: NativeSidecarBridge + Send + 'static,
12245    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
12246{
12247    let decision = bridge.static_permission_decision(vm_id, capability, domain, Some(resource));
12248    if decision.as_ref().is_some_and(|decision| decision.allow) {
12249        return Ok(());
12250    }
12251
12252    let reason = decision
12253        .and_then(|decision| decision.reason)
12254        .unwrap_or_else(|| format!("{capability} permission required"));
12255    Err(SidecarError::Execution(format!(
12256        "EACCES: permission denied, {resource}: {reason}"
12257    )))
12258}
12259
12260fn socket_query_resource(kind: SocketQueryKind, request: &FindListenerRequest) -> String {
12261    if let Some(path) = request.path.as_deref() {
12262        return format!("unix://{path}");
12263    }
12264
12265    let host = request.host.as_deref().unwrap_or("*");
12266    let port = request
12267        .port
12268        .map_or_else(|| String::from("*"), |port| port.to_string());
12269    match kind {
12270        SocketQueryKind::TcpListener => format!("tcp://{host}:{port}"),
12271        SocketQueryKind::UdpBound => format!("udp://{host}:{port}"),
12272    }
12273}
12274
12275fn snapshot_vm_processes(vm: &VmState) -> Vec<ProcessSnapshotEntry> {
12276    let process_table = vm.kernel.list_processes();
12277    snapshot_vm_processes_inner(vm, &process_table)
12278}
12279
12280fn snapshot_vm_processes_inner(
12281    vm: &VmState,
12282    process_table: &BTreeMap<u32, secure_exec_kernel::process_table::ProcessInfo>,
12283) -> Vec<ProcessSnapshotEntry> {
12284    let mut entries = Vec::new();
12285
12286    for (process_id, process) in &vm.active_processes {
12287        collect_process_snapshot_entries(process_id, process, process_table, &mut entries);
12288    }
12289
12290    for exited in &vm.exited_process_snapshots {
12291        entries.push(exited.process.clone());
12292    }
12293
12294    entries
12295}
12296
12297fn prune_exited_process_snapshots(vm: &mut VmState) {
12298    let cutoff = Instant::now() - EXITED_PROCESS_SNAPSHOT_RETENTION;
12299    while vm
12300        .exited_process_snapshots
12301        .front()
12302        .is_some_and(|snapshot| snapshot.captured_at < cutoff)
12303    {
12304        vm.exited_process_snapshots.pop_front();
12305    }
12306}
12307
12308fn build_process_snapshot_entry(
12309    process_id: &str,
12310    process: &ActiveProcess,
12311    info: &secure_exec_kernel::process_table::ProcessInfo,
12312    exit_code: Option<i32>,
12313) -> ProcessSnapshotEntry {
12314    wire_process_snapshot_entry_from_shared(process_snapshot_entry_from_kernel(
12315        process_id,
12316        info,
12317        process.guest_cwd.clone(),
12318        exit_code,
12319    ))
12320}
12321
12322fn wire_process_snapshot_entry_from_shared(
12323    entry: SharedProcessSnapshotEntry,
12324) -> ProcessSnapshotEntry {
12325    ProcessSnapshotEntry {
12326        process_id: entry.process_id,
12327        pid: entry.pid,
12328        ppid: entry.ppid,
12329        pgid: entry.pgid,
12330        sid: entry.sid,
12331        driver: entry.driver,
12332        command: entry.command,
12333        args: entry.args,
12334        cwd: entry.cwd,
12335        status: match entry.status {
12336            SharedProcessSnapshotStatus::Running => ProcessSnapshotStatus::Running,
12337            SharedProcessSnapshotStatus::Stopped => ProcessSnapshotStatus::Stopped,
12338            SharedProcessSnapshotStatus::Exited => ProcessSnapshotStatus::Exited,
12339        },
12340        exit_code: entry.exit_code,
12341    }
12342}
12343
12344fn collect_process_snapshot_entries(
12345    process_id: &str,
12346    process: &ActiveProcess,
12347    process_table: &BTreeMap<u32, secure_exec_kernel::process_table::ProcessInfo>,
12348    entries: &mut Vec<ProcessSnapshotEntry>,
12349) {
12350    if let Some(info) = process_table.get(&process.kernel_pid) {
12351        entries.push(build_process_snapshot_entry(
12352            process_id, process, info, None,
12353        ));
12354    }
12355
12356    for (child_id, child) in &process.child_processes {
12357        let child_process_id = format!("{process_id}/{child_id}");
12358        collect_process_snapshot_entries(&child_process_id, child, process_table, entries);
12359    }
12360}
12361
12362fn find_kernel_socket_state_entry(
12363    kernel: &SidecarKernel,
12364    process_id: &str,
12365    process: &ActiveProcess,
12366    kind: SocketQueryKind,
12367    request: &FindListenerRequest,
12368) -> Result<Option<SocketStateEntry>, SidecarError> {
12369    let entry = match kind {
12370        SocketQueryKind::TcpListener => process
12371            .tcp_listeners
12372            .values()
12373            .filter_map(|listener| listener.kernel_socket_id)
12374            .find_map(|socket_id| {
12375                kernel_socket_state_entry(kernel, process_id, socket_id, kind, request)
12376            }),
12377        SocketQueryKind::UdpBound => process
12378            .udp_sockets
12379            .values()
12380            .filter_map(|socket| socket.kernel_socket_id)
12381            .find_map(|socket_id| {
12382                kernel_socket_state_entry(kernel, process_id, socket_id, kind, request)
12383            }),
12384    };
12385
12386    if entry.is_some() {
12387        return Ok(entry);
12388    }
12389
12390    for child in process.child_processes.values() {
12391        if let Some(entry) =
12392            find_kernel_socket_state_entry(kernel, process_id, child, kind, request)?
12393        {
12394            return Ok(Some(entry));
12395        }
12396    }
12397
12398    Ok(None)
12399}
12400
12401fn kernel_socket_state_entry(
12402    kernel: &SidecarKernel,
12403    process_id: &str,
12404    socket_id: SocketId,
12405    kind: SocketQueryKind,
12406    request: &FindListenerRequest,
12407) -> Option<SocketStateEntry> {
12408    let record = kernel.socket_get(socket_id)?;
12409    let local_address = record.local_address()?;
12410    match kind {
12411        SocketQueryKind::TcpListener if record.state() == SocketState::Listening => {}
12412        SocketQueryKind::TcpListener => return None,
12413        SocketQueryKind::UdpBound => {}
12414    }
12415
12416    if !socket_host_matches(request.host.as_deref(), local_address.host()) {
12417        return None;
12418    }
12419    if request
12420        .port
12421        .is_some_and(|port| local_address.port() != port)
12422    {
12423        return None;
12424    }
12425
12426    Some(SocketStateEntry {
12427        process_id: process_id.to_owned(),
12428        host: Some(local_address.host().to_owned()),
12429        port: Some(local_address.port()),
12430        path: None,
12431    })
12432}
12433
12434fn socket_inodes_for_pid(pid: u32) -> Result<BTreeSet<u64>, SidecarError> {
12435    let fd_dir = PathBuf::from(format!("/proc/{pid}/fd"));
12436    let entries = match fs::read_dir(&fd_dir) {
12437        Ok(entries) => entries,
12438        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(BTreeSet::new()),
12439        Err(error) => {
12440            return Err(SidecarError::Io(format!(
12441                "failed to read socket descriptors for process {pid}: {error}"
12442            )));
12443        }
12444    };
12445
12446    let mut inodes = BTreeSet::new();
12447    for entry in entries {
12448        let entry = entry.map_err(|error| {
12449            SidecarError::Io(format!(
12450                "failed to inspect fd entry for process {pid}: {error}"
12451            ))
12452        })?;
12453        let target = match fs::read_link(entry.path()) {
12454            Ok(target) => target,
12455            Err(_) => continue,
12456        };
12457        if let Some(inode) = parse_socket_inode(&target) {
12458            inodes.insert(inode);
12459        }
12460    }
12461
12462    Ok(inodes)
12463}
12464
12465fn parse_socket_inode(target: &Path) -> Option<u64> {
12466    let value = target.to_string_lossy();
12467    let trimmed = value.strip_prefix("socket:[")?.strip_suffix(']')?;
12468    trimmed.parse().ok()
12469}
12470
12471fn unix_socket_path(addr: &UnixSocketAddr) -> Option<String> {
12472    addr.as_pathname()
12473        .map(|path| path.to_string_lossy().into_owned())
12474}
12475
12476fn find_unix_socket_for_pid(
12477    pid: u32,
12478    inodes: &BTreeSet<u64>,
12479    path: &str,
12480    process_id: &str,
12481) -> Result<Option<SocketStateEntry>, SidecarError> {
12482    let table_path = format!("/proc/{pid}/net/unix");
12483    let contents = match fs::read_to_string(&table_path) {
12484        Ok(contents) => contents,
12485        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
12486        Err(error) => {
12487            return Err(SidecarError::Io(format!(
12488                "failed to inspect unix sockets for process {pid}: {error}"
12489            )));
12490        }
12491    };
12492
12493    for line in contents.lines().skip(1) {
12494        let columns = line.split_whitespace().collect::<Vec<_>>();
12495        if columns.len() < 8 {
12496            continue;
12497        }
12498        let Ok(inode) = columns[6].parse::<u64>() else {
12499            continue;
12500        };
12501        if !inodes.contains(&inode) || columns[7] != path {
12502            continue;
12503        }
12504        return Ok(Some(SocketStateEntry {
12505            process_id: process_id.to_owned(),
12506            host: None,
12507            port: None,
12508            path: Some(path.to_owned()),
12509        }));
12510    }
12511
12512    Ok(None)
12513}
12514
12515fn find_inet_socket_for_pid(
12516    table_path: &str,
12517    inodes: &BTreeSet<u64>,
12518    kind: SocketQueryKind,
12519    requested_host: Option<&str>,
12520    requested_port: Option<u16>,
12521    process_id: &str,
12522) -> Result<Option<SocketStateEntry>, SidecarError> {
12523    for entry in parse_proc_net_entries(table_path)? {
12524        if !inodes.contains(&entry.inode) {
12525            continue;
12526        }
12527        if matches!(kind, SocketQueryKind::TcpListener) && entry.state != "0A" {
12528            continue;
12529        }
12530        if !socket_host_matches(requested_host, &entry.local_host) {
12531            continue;
12532        }
12533        if let Some(port) = requested_port {
12534            if entry.local_port != port {
12535                continue;
12536            }
12537        }
12538        return Ok(Some(SocketStateEntry {
12539            process_id: process_id.to_owned(),
12540            host: Some(entry.local_host),
12541            port: Some(entry.local_port),
12542            path: None,
12543        }));
12544    }
12545
12546    Ok(None)
12547}
12548
12549fn is_unspecified_socket_host(host: &str) -> bool {
12550    host == "0.0.0.0" || host == "::"
12551}
12552
12553fn is_loopback_socket_host(host: &str) -> bool {
12554    host == "127.0.0.1" || host == "::1" || host.eq_ignore_ascii_case("localhost")
12555}
12556
12557pub(crate) fn vm_network_resource_counts(vm: &VmState) -> NetworkResourceCounts {
12558    let snapshot = vm.kernel.resource_snapshot();
12559    let mut counts = NetworkResourceCounts {
12560        sockets: snapshot.sockets,
12561        connections: snapshot.socket_connections,
12562    };
12563    for process in vm.active_processes.values() {
12564        let process_counts = process.sidecar_only_network_resource_counts();
12565        counts.sockets += process_counts.sockets;
12566        counts.connections += process_counts.connections;
12567    }
12568    counts
12569}
12570
12571#[allow(clippy::too_many_arguments)]
12572fn collect_javascript_socket_port_state(
12573    kernel: &SidecarKernel,
12574    process_id: &str,
12575    process: &ActiveProcess,
12576    tcp_guest_to_host: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
12577    http_loopback_targets: &mut BTreeMap<
12578        (JavascriptSocketFamily, u16),
12579        JavascriptHttpLoopbackTarget,
12580    >,
12581    udp_guest_to_host: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
12582    udp_host_to_guest: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
12583    used_tcp_ports: &mut BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
12584    used_udp_ports: &mut BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
12585) {
12586    for (family, port) in process.tcp_port_reservations.values() {
12587        used_tcp_ports.entry(*family).or_default().insert(*port);
12588    }
12589
12590    let mut record_tcp_listener = |guest_addr: SocketAddr, host_port: u16| {
12591        let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
12592        used_tcp_ports
12593            .entry(family)
12594            .or_default()
12595            .insert(guest_addr.port());
12596        // VM-local loopback connects should also resolve listeners bound to
12597        // unspecified guest addresses like 0.0.0.0/::.
12598        tcp_guest_to_host.insert((family, guest_addr.port()), host_port);
12599    };
12600
12601    for listener in process.tcp_listeners.values() {
12602        let local_addr = listener
12603            .kernel_socket_id
12604            .and_then(|socket_id| kernel.socket_get(socket_id))
12605            .and_then(|record| record.local_address().cloned())
12606            .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
12607            .unwrap_or_else(|| listener.guest_local_addr());
12608        record_tcp_listener(local_addr, local_addr.port());
12609    }
12610
12611    for (server_id, server) in &process.http_servers {
12612        let host_port = match server.listener.local_addr() {
12613            Ok(addr) => addr.port(),
12614            Err(_) => continue,
12615        };
12616        record_tcp_listener(server.guest_local_addr, host_port);
12617        let family = JavascriptSocketFamily::from_ip(server.guest_local_addr.ip());
12618        http_loopback_targets.insert(
12619            (family, server.guest_local_addr.port()),
12620            JavascriptHttpLoopbackTarget {
12621                process_id: process_id.to_owned(),
12622                server_id: *server_id,
12623            },
12624        );
12625    }
12626
12627    if let Ok(http2) = process.http2.shared.lock() {
12628        for server in http2.servers.values() {
12629            record_tcp_listener(server.guest_local_addr, server.actual_local_addr.port());
12630        }
12631    }
12632
12633    for socket in process.tcp_sockets.values() {
12634        let guest_addr = socket
12635            .kernel_socket_id
12636            .and_then(|socket_id| kernel.socket_get(socket_id))
12637            .and_then(|record| record.local_address().cloned())
12638            .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
12639            .unwrap_or(socket.guest_local_addr);
12640        let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
12641        used_tcp_ports
12642            .entry(family)
12643            .or_default()
12644            .insert(guest_addr.port());
12645    }
12646
12647    for socket in process.udp_sockets.values() {
12648        let guest_addr = socket
12649            .kernel_socket_id
12650            .and_then(|socket_id| kernel.socket_get(socket_id))
12651            .and_then(|record| record.local_address().cloned())
12652            .and_then(|address| {
12653                resolve_udp_bind_addr(address.host(), address.port(), socket.family).ok()
12654            })
12655            .or_else(|| socket.local_addr());
12656        let Some(guest_addr) = guest_addr else {
12657            continue;
12658        };
12659        let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
12660        used_udp_ports
12661            .entry(family)
12662            .or_default()
12663            .insert(guest_addr.port());
12664        if let Some(host_addr) = socket
12665            .socket
12666            .as_ref()
12667            .and_then(|socket| socket.local_addr().ok())
12668        {
12669            if is_loopback_ip(guest_addr.ip()) {
12670                udp_guest_to_host.insert((family, guest_addr.port()), host_addr.port());
12671                udp_host_to_guest.insert((family, host_addr.port()), guest_addr.port());
12672            }
12673        } else if socket.kernel_socket_id.is_some() && is_loopback_ip(guest_addr.ip()) {
12674            udp_guest_to_host.insert((family, guest_addr.port()), guest_addr.port());
12675            udp_host_to_guest.insert((family, guest_addr.port()), guest_addr.port());
12676        }
12677    }
12678
12679    for (child_process_id, child) in &process.child_processes {
12680        let child_id = format!("{process_id}/{child_process_id}");
12681        collect_javascript_socket_port_state(
12682            kernel,
12683            &child_id,
12684            child,
12685            tcp_guest_to_host,
12686            http_loopback_targets,
12687            udp_guest_to_host,
12688            udp_host_to_guest,
12689            used_tcp_ports,
12690            used_udp_ports,
12691        );
12692    }
12693}
12694
12695pub(crate) fn build_javascript_socket_path_context(
12696    vm: &VmState,
12697) -> Result<JavascriptSocketPathContext, SidecarError> {
12698    let mut loopback_exempt_ports = vm.create_loopback_exempt_ports.clone();
12699    loopback_exempt_ports.extend(vm.configuration.loopback_exempt_ports.iter().copied());
12700    let mut tcp_loopback_guest_to_host_ports = BTreeMap::new();
12701    let mut http_loopback_targets = BTreeMap::new();
12702    let mut udp_loopback_guest_to_host_ports = BTreeMap::new();
12703    let mut udp_loopback_host_to_guest_ports = BTreeMap::new();
12704    let mut used_tcp_guest_ports = BTreeMap::new();
12705    let mut used_udp_guest_ports = BTreeMap::new();
12706    for (process_id, process) in &vm.active_processes {
12707        collect_javascript_socket_port_state(
12708            &vm.kernel,
12709            process_id,
12710            process,
12711            &mut tcp_loopback_guest_to_host_ports,
12712            &mut http_loopback_targets,
12713            &mut udp_loopback_guest_to_host_ports,
12714            &mut udp_loopback_host_to_guest_ports,
12715            &mut used_tcp_guest_ports,
12716            &mut used_udp_guest_ports,
12717        );
12718    }
12719    Ok(JavascriptSocketPathContext {
12720        sandbox_root: vm.cwd.clone(),
12721        mounts: vm.configuration.mounts.clone(),
12722        listen_policy: vm.listen_policy,
12723        loopback_exempt_ports,
12724        tcp_loopback_guest_to_host_ports,
12725        http_loopback_targets,
12726        udp_loopback_guest_to_host_ports,
12727        udp_loopback_host_to_guest_ports,
12728        used_tcp_guest_ports,
12729        used_udp_guest_ports,
12730    })
12731}
12732
12733fn check_network_resource_limit(
12734    limit: Option<usize>,
12735    current: usize,
12736    additional: usize,
12737    label: &str,
12738) -> Result<(), SidecarError> {
12739    if let Some(limit) = limit {
12740        if current.saturating_add(additional) > limit {
12741            return Err(SidecarError::Execution(format!(
12742                "EAGAIN: maximum {label} count reached"
12743            )));
12744        }
12745    }
12746    Ok(())
12747}
12748
12749fn normalize_tcp_listen_host(
12750    host: Option<&str>,
12751) -> Result<(JavascriptSocketFamily, &'static str, &'static str), SidecarError> {
12752    match host.unwrap_or("127.0.0.1") {
12753        "127.0.0.1" | "localhost" => Ok((JavascriptSocketFamily::Ipv4, "127.0.0.1", "127.0.0.1")),
12754        "::1" => Ok((JavascriptSocketFamily::Ipv6, "::1", "::1")),
12755        "0.0.0.0" => Ok((JavascriptSocketFamily::Ipv4, "127.0.0.1", "0.0.0.0")),
12756        "::" => Ok((JavascriptSocketFamily::Ipv6, "::1", "::")),
12757        other => Err(SidecarError::Execution(format!(
12758            "EACCES: TCP listeners must bind to loopback or unspecified addresses, got {other}"
12759        ))),
12760    }
12761}
12762
12763fn normalize_udp_bind_host(
12764    host: Option<&str>,
12765    family: JavascriptUdpFamily,
12766) -> Result<(&'static str, &'static str, JavascriptSocketFamily), SidecarError> {
12767    match (family, host) {
12768        (JavascriptUdpFamily::Ipv4, None) | (JavascriptUdpFamily::Ipv4, Some("0.0.0.0")) => {
12769            Ok(("127.0.0.1", "0.0.0.0", JavascriptSocketFamily::Ipv4))
12770        }
12771        (JavascriptUdpFamily::Ipv4, Some("127.0.0.1"))
12772        | (JavascriptUdpFamily::Ipv4, Some("localhost")) => {
12773            Ok(("127.0.0.1", "127.0.0.1", JavascriptSocketFamily::Ipv4))
12774        }
12775        (JavascriptUdpFamily::Ipv6, None) | (JavascriptUdpFamily::Ipv6, Some("::")) => {
12776            Ok(("::1", "::", JavascriptSocketFamily::Ipv6))
12777        }
12778        (JavascriptUdpFamily::Ipv6, Some("::1"))
12779        | (JavascriptUdpFamily::Ipv6, Some("localhost")) => {
12780            Ok(("::1", "::1", JavascriptSocketFamily::Ipv6))
12781        }
12782        (JavascriptUdpFamily::Ipv4, Some(other)) => Err(SidecarError::Execution(format!(
12783            "EACCES: udp4 sockets must bind to 127.0.0.1 or 0.0.0.0, got {other}"
12784        ))),
12785        (JavascriptUdpFamily::Ipv6, Some(other)) => Err(SidecarError::Execution(format!(
12786            "EACCES: udp6 sockets must bind to ::1 or ::, got {other}"
12787        ))),
12788    }
12789}
12790
12791fn allocate_guest_listen_port(
12792    requested_port: u16,
12793    family: JavascriptSocketFamily,
12794    used_ports: &BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
12795    policy: VmListenPolicy,
12796) -> Result<u16, SidecarError> {
12797    let is_allowed = |port: u16| {
12798        port >= policy.port_min
12799            && port <= policy.port_max
12800            && (policy.allow_privileged || port >= 1024)
12801    };
12802    let used = used_ports.get(&family);
12803
12804    if requested_port != 0 {
12805        if !is_allowed(requested_port) {
12806            let reason = if requested_port < 1024 && !policy.allow_privileged {
12807                format!(
12808                    "EACCES: privileged listen port {requested_port} requires {}=true",
12809                    VM_LISTEN_ALLOW_PRIVILEGED_METADATA_KEY
12810                )
12811            } else {
12812                format!(
12813                    "EACCES: listen port {requested_port} is outside the allowed range {}-{}",
12814                    policy.port_min, policy.port_max
12815                )
12816            };
12817            return Err(SidecarError::Execution(reason));
12818        }
12819        if used.is_some_and(|ports| ports.contains(&requested_port)) {
12820            return Err(sidecar_net_error(std::io::Error::from_raw_os_error(
12821                libc::EADDRINUSE,
12822            )));
12823        }
12824        return Ok(requested_port);
12825    }
12826
12827    let allocation_start = policy
12828        .port_min
12829        .max(if policy.allow_privileged { 1 } else { 1024 });
12830    for candidate in allocation_start..=policy.port_max {
12831        if used.is_some_and(|ports| ports.contains(&candidate)) {
12832            continue;
12833        }
12834        return Ok(candidate);
12835    }
12836
12837    Err(sidecar_net_error(std::io::Error::from_raw_os_error(
12838        libc::EADDRINUSE,
12839    )))
12840}
12841
12842fn socket_host_matches(requested: Option<&str>, actual: &str) -> bool {
12843    match requested {
12844        None => true,
12845        Some(requested) if requested == actual => true,
12846        Some(requested)
12847            if is_unspecified_socket_host(requested) && is_unspecified_socket_host(actual) =>
12848        {
12849            true
12850        }
12851        Some(requested) if is_unspecified_socket_host(requested) => is_loopback_socket_host(actual),
12852        Some(requested) if requested.eq_ignore_ascii_case("localhost") => {
12853            is_loopback_socket_host(actual)
12854        }
12855        _ => false,
12856    }
12857}
12858
12859fn parse_proc_net_entries(table_path: &str) -> Result<Vec<ProcNetEntry>, SidecarError> {
12860    let contents = match fs::read_to_string(table_path) {
12861        Ok(contents) => contents,
12862        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
12863        Err(error) => {
12864            return Err(SidecarError::Io(format!(
12865                "failed to inspect socket table {table_path}: {error}"
12866            )));
12867        }
12868    };
12869
12870    let mut entries = Vec::new();
12871    for line in contents.lines().skip(1) {
12872        let columns = line.split_whitespace().collect::<Vec<_>>();
12873        if columns.len() < 10 {
12874            continue;
12875        }
12876        let Some((host, port)) = parse_proc_ip_port(columns[1]) else {
12877            continue;
12878        };
12879        let Ok(inode) = columns[9].parse::<u64>() else {
12880            continue;
12881        };
12882        entries.push(ProcNetEntry {
12883            local_host: host,
12884            local_port: port,
12885            state: columns[3].to_owned(),
12886            inode,
12887        });
12888    }
12889
12890    Ok(entries)
12891}
12892
12893fn parse_proc_ip_port(value: &str) -> Option<(String, u16)> {
12894    let (raw_ip, raw_port) = value.split_once(':')?;
12895    let port = u16::from_str_radix(raw_port, 16).ok()?;
12896    let host = match raw_ip.len() {
12897        8 => {
12898            let raw = u32::from_str_radix(raw_ip, 16).ok()?;
12899            Ipv4Addr::from(raw.to_le_bytes()).to_string()
12900        }
12901        32 => {
12902            let mut bytes = [0_u8; 16];
12903            for (index, chunk) in raw_ip.as_bytes().chunks(8).enumerate() {
12904                let word = u32::from_str_radix(std::str::from_utf8(chunk).ok()?, 16).ok()?;
12905                bytes[index * 4..(index + 1) * 4].copy_from_slice(&word.to_le_bytes());
12906            }
12907            Ipv6Addr::from(bytes).to_string()
12908        }
12909        _ => return None,
12910    };
12911    Some((host, port))
12912}
12913
12914fn python_file_entrypoint(entrypoint: &str) -> Option<PathBuf> {
12915    let path = Path::new(entrypoint);
12916    (path.extension().and_then(|extension| extension.to_str()) == Some("py"))
12917        .then(|| path.to_path_buf())
12918}
12919
12920fn add_runtime_guest_path_mapping(
12921    env: &mut BTreeMap<String, String>,
12922    guest_path: &str,
12923    host_path: &Path,
12924) {
12925    let mut mappings = env
12926        .get("AGENTOS_GUEST_PATH_MAPPINGS")
12927        .and_then(|value| serde_json::from_str::<Vec<Value>>(value).ok())
12928        .unwrap_or_default();
12929    mappings.retain(|mapping| {
12930        mapping
12931            .get("guestPath")
12932            .and_then(Value::as_str)
12933            .map(|existing| normalize_path(existing) != normalize_path(guest_path))
12934            .unwrap_or(true)
12935    });
12936    mappings.push(json!({
12937        "guestPath": normalize_path(guest_path),
12938        "hostPath": host_path.display().to_string(),
12939    }));
12940    if let Ok(serialized) = serde_json::to_string(&mappings) {
12941        env.insert(String::from("AGENTOS_GUEST_PATH_MAPPINGS"), serialized);
12942    }
12943}
12944
12945fn add_runtime_host_access_path(
12946    env: &mut BTreeMap<String, String>,
12947    key: &str,
12948    host_path: &Path,
12949    expand: bool,
12950) {
12951    let existing = env
12952        .get(key)
12953        .and_then(|value| serde_json::from_str::<Vec<String>>(value).ok())
12954        .unwrap_or_default()
12955        .into_iter()
12956        .map(PathBuf::from)
12957        .collect::<Vec<_>>();
12958    let mut paths = existing;
12959    paths.push(host_path.to_path_buf());
12960    let normalized = if expand {
12961        expand_host_access_paths(&paths)
12962    } else {
12963        dedupe_host_paths(&paths)
12964    };
12965    let serialized = normalized
12966        .iter()
12967        .map(|path| path.to_string_lossy().into_owned())
12968        .collect::<Vec<_>>();
12969    if let Ok(serialized) = serde_json::to_string(&serialized) {
12970        env.insert(key.to_owned(), serialized);
12971    }
12972}
12973
12974// discover_command_guest_paths moved to crate::bootstrap
12975
12976fn is_path_like_specifier(specifier: &str) -> bool {
12977    specifier.starts_with('/')
12978        || specifier.starts_with("./")
12979        || specifier.starts_with("../")
12980        || specifier.starts_with("file:")
12981}
12982
12983fn execution_wasm_permission_tier(tier: WasmPermissionTier) -> ExecutionWasmPermissionTier {
12984    match tier {
12985        WasmPermissionTier::Full => ExecutionWasmPermissionTier::Full,
12986        WasmPermissionTier::ReadWrite => ExecutionWasmPermissionTier::ReadWrite,
12987        WasmPermissionTier::ReadOnly => ExecutionWasmPermissionTier::ReadOnly,
12988        WasmPermissionTier::Isolated => ExecutionWasmPermissionTier::Isolated,
12989    }
12990}
12991
12992fn resolve_wasm_permission_tier(
12993    vm: &VmState,
12994    command_name: Option<&str>,
12995    explicit_tier: Option<WasmPermissionTier>,
12996    entrypoint: &str,
12997) -> WasmPermissionTier {
12998    explicit_tier
12999        .or_else(|| command_name.and_then(|command| vm.command_permissions.get(command).copied()))
13000        .or_else(|| {
13001            Path::new(entrypoint)
13002                .file_name()
13003                .and_then(|name| name.to_str())
13004                .and_then(|command| vm.command_permissions.get(command).copied())
13005        })
13006        .unwrap_or(WasmPermissionTier::Full)
13007}
13008
13009fn tokenize_shell_free_command(command: &str) -> Vec<String> {
13010    command
13011        .split_whitespace()
13012        .filter(|segment| !segment.is_empty())
13013        .map(str::to_owned)
13014        .collect()
13015}
13016
13017fn is_posix_shell_builtin(command: &str) -> bool {
13018    matches!(
13019        command,
13020        "." | ":"
13021            | "break"
13022            | "cd"
13023            | "continue"
13024            | "eval"
13025            | "exec"
13026            | "exit"
13027            | "export"
13028            | "readonly"
13029            | "return"
13030            | "set"
13031            | "shift"
13032            | "times"
13033            | "trap"
13034            | "umask"
13035            | "unset"
13036    )
13037}
13038
13039/// Single-token checks for shell-mode commands whose first word forces a real
13040/// shell even when the command string has no shell metacharacters. This is not
13041/// a parser: env-assignment prefixes (`FOO=bar cmd`) and shell reserved words
13042/// have no meaning outside `sh`, so whitespace-tokenizing them would silently
13043/// run the wrong program.
13044fn shell_first_token_requires_shell(token: &str) -> bool {
13045    token.contains('=') || is_shell_reserved_word(token)
13046}
13047
13048fn is_shell_reserved_word(token: &str) -> bool {
13049    matches!(
13050        token,
13051        "if" | "then"
13052            | "elif"
13053            | "else"
13054            | "fi"
13055            | "for"
13056            | "in"
13057            | "do"
13058            | "done"
13059            | "while"
13060            | "until"
13061            | "case"
13062            | "esac"
13063            | "{"
13064            | "}"
13065            | "!"
13066    )
13067}
13068
13069fn command_requires_shell(command: &str) -> bool {
13070    command.chars().any(|ch| {
13071        matches!(
13072            ch,
13073            '|' | '&'
13074                | ';'
13075                | '<'
13076                | '>'
13077                | '('
13078                | ')'
13079                | '$'
13080                | '`'
13081                | '*'
13082                | '?'
13083                | '['
13084                | ']'
13085                | '{'
13086                | '}'
13087                | '~'
13088                | '\''
13089                | '"'
13090                | '\\'
13091                | '\n'
13092        )
13093    })
13094}
13095
13096fn host_mount_path_for_guest_path(vm: &VmState, guest_path: &str) -> Option<PathBuf> {
13097    let normalized = normalize_path(guest_path);
13098
13099    let mut mounts = vm
13100        .configuration
13101        .mounts
13102        .iter()
13103        .filter_map(|mount| {
13104            ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
13105                .then(|| {
13106                    mount_config_host_path(&mount.plugin.config)
13107                        .map(|host_path| (mount.guest_path.as_str(), host_path))
13108                })
13109                .flatten()
13110        })
13111        .collect::<Vec<_>>();
13112    mounts.sort_by_key(|mount| std::cmp::Reverse(mount.0.len()));
13113
13114    for (guest_root, host_root) in mounts {
13115        if normalized != guest_root && !normalized.starts_with(&format!("{guest_root}/")) {
13116            continue;
13117        }
13118
13119        let suffix = normalized
13120            .strip_prefix(guest_root)
13121            .unwrap_or_default()
13122            .trim_start_matches('/');
13123        let mut path = PathBuf::from(host_root);
13124        if !suffix.is_empty() {
13125            path.push(suffix);
13126        }
13127        return Some(path);
13128    }
13129
13130    None
13131}
13132
13133fn host_runtime_path_for_guest_path_with_env(
13134    vm: &VmState,
13135    runtime_env: &BTreeMap<String, String>,
13136    guest_path: &str,
13137    default_host_cwd: &Path,
13138) -> Option<PathBuf> {
13139    if let Some(path) = host_mount_path_for_guest_path(vm, guest_path) {
13140        return Some(path);
13141    }
13142    if let Some(path) = host_path_from_runtime_guest_mappings(runtime_env, guest_path) {
13143        return Some(path);
13144    }
13145
13146    let normalized = normalize_path(guest_path);
13147    let virtual_home = guest_virtual_home(vm);
13148
13149    if normalized == virtual_home || normalized.starts_with(&format!("{virtual_home}/")) {
13150        let suffix = normalized
13151            .strip_prefix(&virtual_home)
13152            .unwrap_or_default()
13153            .trim_start_matches('/');
13154        let mut host_path = default_host_cwd.to_path_buf();
13155        if !suffix.is_empty() {
13156            host_path.push(suffix);
13157        }
13158        return Some(host_path);
13159    }
13160
13161    None
13162}
13163
13164#[derive(Deserialize, Serialize)]
13165struct RuntimeGuestPathMapping {
13166    #[serde(rename = "guestPath")]
13167    guest_path: String,
13168    #[serde(rename = "hostPath")]
13169    host_path: String,
13170    #[serde(rename = "readOnly", default)]
13171    read_only: bool,
13172}
13173
13174pub(crate) fn host_path_from_runtime_guest_mappings(
13175    runtime_env: &BTreeMap<String, String>,
13176    guest_path: &str,
13177) -> Option<PathBuf> {
13178    let mappings = runtime_env
13179        .get("AGENTOS_GUEST_PATH_MAPPINGS")
13180        .and_then(|value| serde_json::from_str::<Vec<RuntimeGuestPathMapping>>(value).ok())?;
13181    let normalized = normalize_path(guest_path);
13182
13183    let mut sorted_mappings = mappings
13184        .into_iter()
13185        .filter_map(|mapping| {
13186            (!mapping.guest_path.is_empty() && !mapping.host_path.is_empty()).then_some((
13187                normalize_path(&mapping.guest_path),
13188                PathBuf::from(mapping.host_path),
13189            ))
13190        })
13191        .collect::<Vec<_>>();
13192    sorted_mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.0.len()));
13193
13194    for (guest_root, mut host_root) in sorted_mappings {
13195        if guest_root != "/"
13196            && normalized != guest_root
13197            && !normalized.starts_with(&format!("{guest_root}/"))
13198        {
13199            continue;
13200        }
13201        if guest_root == "/" && !normalized.starts_with('/') {
13202            continue;
13203        }
13204
13205        if host_root.is_relative() {
13206            host_root = std::env::current_dir().ok()?.join(host_root);
13207        }
13208
13209        let suffix = if guest_root == "/" {
13210            normalized.trim_start_matches('/')
13211        } else {
13212            normalized
13213                .strip_prefix(&guest_root)
13214                .unwrap_or_default()
13215                .trim_start_matches('/')
13216        };
13217        if !suffix.is_empty() {
13218            host_root.push(suffix);
13219        }
13220        return Some(host_root);
13221    }
13222
13223    None
13224}
13225
13226fn guest_runtime_path_for_host_path(
13227    runtime_env: &BTreeMap<String, String>,
13228    virtual_home: &str,
13229    cwd: &Path,
13230    host_path: &str,
13231) -> Option<String> {
13232    let resolved = if host_path.starts_with("file://") {
13233        PathBuf::from(host_path.trim_start_matches("file://"))
13234    } else if host_path.starts_with("file:") {
13235        PathBuf::from(host_path.trim_start_matches("file:"))
13236    } else {
13237        let candidate = PathBuf::from(host_path);
13238        if candidate.is_absolute() {
13239            candidate
13240        } else if host_path.starts_with("./") || host_path.starts_with("../") {
13241            cwd.join(candidate)
13242        } else {
13243            return None;
13244        }
13245    };
13246    let normalized = normalize_host_path(&resolved);
13247
13248    if let Some(path) = guest_path_from_runtime_host_mappings(runtime_env, &normalized) {
13249        return Some(path);
13250    }
13251
13252    let normalized_cwd = normalize_host_path(cwd);
13253    if !path_is_within_root(&normalized, &normalized_cwd) {
13254        return None;
13255    }
13256
13257    let virtual_home = if virtual_home.starts_with('/') {
13258        virtual_home.to_string()
13259    } else {
13260        String::from("/root")
13261    };
13262    let suffix = normalized
13263        .strip_prefix(&normalized_cwd)
13264        .ok()?
13265        .to_string_lossy()
13266        .replace('\\', "/")
13267        .trim_start_matches('/')
13268        .to_owned();
13269
13270    Some(if suffix.is_empty() {
13271        virtual_home
13272    } else {
13273        normalize_path(&format!("{virtual_home}/{suffix}"))
13274    })
13275}
13276
13277fn guest_path_from_runtime_host_mappings(
13278    runtime_env: &BTreeMap<String, String>,
13279    host_path: &Path,
13280) -> Option<String> {
13281    let mappings = runtime_env
13282        .get("AGENTOS_GUEST_PATH_MAPPINGS")
13283        .and_then(|value| serde_json::from_str::<Vec<RuntimeGuestPathMapping>>(value).ok())?;
13284    let normalized = normalize_host_path(host_path);
13285
13286    let mut sorted_mappings = mappings
13287        .into_iter()
13288        .filter_map(|mapping| {
13289            (!mapping.guest_path.is_empty() && !mapping.host_path.is_empty()).then_some((
13290                normalize_path(&mapping.guest_path),
13291                normalize_host_path(Path::new(&mapping.host_path)),
13292            ))
13293        })
13294        .collect::<Vec<_>>();
13295    sorted_mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.1.as_os_str().len()));
13296
13297    for (guest_root, host_root) in sorted_mappings {
13298        if !path_is_within_root(&normalized, &host_root) {
13299            continue;
13300        }
13301        let suffix = normalized
13302            .strip_prefix(&host_root)
13303            .ok()?
13304            .to_string_lossy()
13305            .replace('\\', "/")
13306            .trim_start_matches('/')
13307            .to_owned();
13308
13309        return Some(if suffix.is_empty() {
13310            guest_root
13311        } else if guest_root == "/" {
13312            normalize_path(&format!("/{suffix}"))
13313        } else {
13314            normalize_path(&format!("{guest_root}/{suffix}"))
13315        });
13316    }
13317
13318    None
13319}
13320
13321fn host_mount_path_for_guest_path_from_mounts(
13322    mounts: &[crate::protocol::MountDescriptor],
13323    guest_path: &str,
13324) -> Option<PathBuf> {
13325    let normalized = normalize_path(guest_path);
13326
13327    let mut host_mounts = mounts
13328        .iter()
13329        .filter_map(|mount| {
13330            ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
13331                .then(|| {
13332                    mount_config_host_path(&mount.plugin.config)
13333                        .map(|host_path| (mount.guest_path.as_str(), host_path))
13334                })
13335                .flatten()
13336        })
13337        .collect::<Vec<_>>();
13338    host_mounts.sort_by_key(|mount| std::cmp::Reverse(mount.0.len()));
13339
13340    for (guest_root, host_root) in host_mounts {
13341        if normalized != guest_root && !normalized.starts_with(&format!("{guest_root}/")) {
13342            continue;
13343        }
13344
13345        let suffix = normalized
13346            .strip_prefix(guest_root)
13347            .unwrap_or_default()
13348            .trim_start_matches('/');
13349        let mut path = PathBuf::from(host_root);
13350        if !suffix.is_empty() {
13351            path.push(suffix);
13352        }
13353        return Some(path);
13354    }
13355
13356    None
13357}
13358
13359#[cfg(test)]
13360mod host_mount_path_for_guest_path_from_mounts_tests {
13361    use super::host_mount_path_for_guest_path_from_mounts;
13362    use crate::protocol::{MountDescriptor, MountPluginDescriptor};
13363    use serde_json::json;
13364    use std::path::PathBuf;
13365
13366    #[test]
13367    fn resolves_module_access_mount_paths() {
13368        let mounts = vec![MountDescriptor {
13369            guest_path: String::from("/root/node_modules"),
13370            read_only: true,
13371            plugin: MountPluginDescriptor {
13372                id: String::from("module_access"),
13373                config: json!({
13374                    "hostPath": "/tmp/workspace/node_modules",
13375                })
13376                .to_string(),
13377            },
13378        }];
13379
13380        let resolved =
13381            host_mount_path_for_guest_path_from_mounts(&mounts, "/root/node_modules/pkg/index.js")
13382                .expect("module_access mount should resolve");
13383
13384        assert_eq!(
13385            resolved,
13386            PathBuf::from("/tmp/workspace/node_modules/pkg/index.js")
13387        );
13388    }
13389}
13390
13391fn resolve_guest_socket_host_path(
13392    context: &JavascriptSocketPathContext,
13393    guest_path: &str,
13394) -> PathBuf {
13395    if let Some(path) = host_mount_path_for_guest_path_from_mounts(&context.mounts, guest_path) {
13396        return path;
13397    }
13398
13399    let normalized = normalize_path(guest_path);
13400    let mut host_path = context.sandbox_root.clone();
13401    let suffix = normalized.trim_start_matches('/');
13402    if !suffix.is_empty() {
13403        host_path.push(suffix);
13404    }
13405    host_path
13406}
13407
13408fn ensure_kernel_parent_directories(
13409    kernel: &mut SidecarKernel,
13410    path: &str,
13411) -> Result<(), SidecarError> {
13412    let parent = dirname(path);
13413    if parent != "/" && !kernel.exists(&parent).map_err(kernel_error)? {
13414        kernel.mkdir(&parent, true).map_err(kernel_error)?;
13415    }
13416    Ok(())
13417}
13418
13419// JavascriptChildProcessSpawnOptions, JavascriptChildProcessSpawnRequest moved to crate::protocol
13420// ResolvedChildProcessExecution moved to crate::state
13421
13422pub(crate) fn sanitize_javascript_child_process_internal_bootstrap_env(
13423    env: &BTreeMap<String, String>,
13424) -> BTreeMap<String, String> {
13425    const ALLOWED_KEYS: &[&str] = &[
13426        "AGENTOS_ALLOWED_NODE_BUILTINS",
13427        "AGENTOS_GUEST_PATH_MAPPINGS",
13428        "AGENTOS_LOOPBACK_EXEMPT_PORTS",
13429        "AGENTOS_VIRTUAL_PROCESS_EXEC_PATH",
13430        "AGENTOS_VIRTUAL_PROCESS_UID",
13431        "AGENTOS_VIRTUAL_PROCESS_GID",
13432        "AGENTOS_VIRTUAL_PROCESS_VERSION",
13433    ];
13434
13435    env.iter()
13436        .filter(|(key, _)| {
13437            ALLOWED_KEYS.contains(&key.as_str()) || key.starts_with("AGENTOS_VIRTUAL_OS_")
13438        })
13439        .map(|(key, value)| (key.clone(), value.clone()))
13440        .collect()
13441}
13442
13443// Network request types moved to crate::protocol
13444
13445// VmDnsConfig, DnsResolutionSource moved to crate::state
13446
13447fn resolve_tcp_bind_addr(host: &str, port: u16) -> Result<SocketAddr, SidecarError> {
13448    (host, port)
13449        .to_socket_addrs()
13450        .map_err(sidecar_net_error)?
13451        .next()
13452        .ok_or_else(|| {
13453            SidecarError::Execution(format!("failed to resolve TCP bind address {host}:{port}"))
13454        })
13455}
13456
13457pub(crate) fn format_dns_resource(hostname: &str) -> String {
13458    format!("dns://{hostname}")
13459}
13460
13461// --- Guest Python socket bridge helpers ------------------------------------
13462
13463/// Host-socket read timeout for one `recv`/`recvfrom` RPC. Kept short so the
13464/// synchronous RPC returns promptly (data, or a `timed_out` flag the Python
13465/// shim re-polls on) and never stalls the shared sidecar event loop.
13466// Short so a recv/recvfrom RPC holds the shared event loop only briefly; the
13467// guest shim adds a capped backoff between polls to bound the poll rate.
13468const PYTHON_SOCKET_READ_POLL: Duration = Duration::from_millis(25);
13469const PYTHON_SOCKET_CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
13470const PYTHON_SOCKET_WRITE_TIMEOUT: Duration = Duration::from_secs(30);
13471const PYTHON_SOCKET_DEFAULT_RECV: usize = 65536;
13472const PYTHON_SOCKET_MAX_RECV: usize = 4 * 1024 * 1024;
13473
13474fn python_socket_host(request: &PythonVfsRpcRequest) -> Result<String, SidecarError> {
13475    request
13476        .hostname
13477        .clone()
13478        .ok_or_else(|| SidecarError::InvalidState(String::from("python socket op requires a host")))
13479}
13480
13481fn python_socket_port(request: &PythonVfsRpcRequest) -> Result<u16, SidecarError> {
13482    request
13483        .port
13484        .ok_or_else(|| SidecarError::InvalidState(String::from("python socket op requires a port")))
13485}
13486
13487fn python_socket_payload(request: &PythonVfsRpcRequest) -> Result<Vec<u8>, SidecarError> {
13488    let Some(body) = request.body_base64.as_deref() else {
13489        return Ok(Vec::new());
13490    };
13491    base64::engine::general_purpose::STANDARD
13492        .decode(body)
13493        .map_err(|error| {
13494            SidecarError::InvalidState(format!("invalid base64 python socket payload: {error}"))
13495        })
13496}
13497
13498fn python_socket_recv_len(request: &PythonVfsRpcRequest) -> usize {
13499    request
13500        .max_buffer
13501        .unwrap_or(PYTHON_SOCKET_DEFAULT_RECV)
13502        .clamp(1, PYTHON_SOCKET_MAX_RECV)
13503}
13504
13505fn python_connect_tcp(addrs: &[IpAddr], port: u16) -> Result<TcpStream, SidecarError> {
13506    let mut last_error: Option<String> = None;
13507    for ip in addrs {
13508        let addr = SocketAddr::new(*ip, port);
13509        match TcpStream::connect_timeout(&addr, PYTHON_SOCKET_CONNECT_TIMEOUT) {
13510            Ok(stream) => return Ok(stream),
13511            Err(error) => last_error = Some(error.to_string()),
13512        }
13513    }
13514    Err(SidecarError::Execution(format!(
13515        "ECONNREFUSED: {}",
13516        last_error.unwrap_or_else(|| String::from("no resolved addresses"))
13517    )))
13518}
13519
13520fn python_socket_would_block(error: &std::io::Error) -> bool {
13521    matches!(
13522        error.kind(),
13523        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
13524    )
13525}
13526
13527fn python_socket_io_error(error: std::io::Error) -> SidecarError {
13528    SidecarError::Execution(format!("EIO: python socket: {error}"))
13529}
13530
13531fn python_socket_kind_error(op: &str, expected: &str) -> SidecarError {
13532    SidecarError::Execution(format!(
13533        "EOPNOTSUPP: python socket {op} requires a {expected} socket"
13534    ))
13535}
13536
13537fn blocked_dns_resolution_error(
13538    resource: &str,
13539    ip: IpAddr,
13540    cidr: &str,
13541    label: &str,
13542) -> SidecarError {
13543    SidecarError::Execution(format!(
13544        "EACCES: blocked outbound network access to {resource}: {ip} is within restricted {label} range {cidr}"
13545    ))
13546}
13547
13548fn blocked_loopback_connect_error(resource: &str, ip: IpAddr, port: u16) -> SidecarError {
13549    SidecarError::Execution(format!(
13550        "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}",
13551        loopback_cidr(ip)
13552    ))
13553}
13554
13555fn filter_dns_safe_ip_addrs(
13556    addresses: Vec<IpAddr>,
13557    hostname: &str,
13558) -> Result<Vec<IpAddr>, SidecarError> {
13559    let resource = format_dns_resource(hostname);
13560    let mut allowed = Vec::new();
13561    let mut blocked = None;
13562
13563    for ip in addresses {
13564        if let Some((cidr, label)) = restricted_non_loopback_ip_range(ip) {
13565            blocked.get_or_insert((ip, cidr, label));
13566            continue;
13567        }
13568        allowed.push(ip);
13569    }
13570
13571    if allowed.is_empty() {
13572        let (ip, cidr, label) = blocked.expect("blocked DNS results should capture a reason");
13573        return Err(blocked_dns_resolution_error(&resource, ip, cidr, label));
13574    }
13575
13576    Ok(allowed)
13577}
13578
13579fn loopback_connect_allowed(context: &JavascriptSocketPathContext, port: u16) -> bool {
13580    context.loopback_port_allowed(port)
13581}
13582
13583fn filter_tcp_connect_ip_addrs(
13584    addresses: Vec<IpAddr>,
13585    host: &str,
13586    port: u16,
13587    context: &JavascriptSocketPathContext,
13588) -> Result<Vec<IpAddr>, SidecarError> {
13589    let resource = format_tcp_resource(host, port);
13590    let mut allowed = Vec::new();
13591    let mut blocked = None;
13592
13593    for ip in addresses {
13594        if let Some((cidr, label)) = restricted_non_loopback_ip_range(ip) {
13595            blocked.get_or_insert_with(|| blocked_dns_resolution_error(&resource, ip, cidr, label));
13596            continue;
13597        }
13598        if is_loopback_ip(ip) && !loopback_connect_allowed(context, port) {
13599            blocked.get_or_insert_with(|| blocked_loopback_connect_error(&resource, ip, port));
13600            continue;
13601        }
13602        allowed.push(ip);
13603    }
13604
13605    if allowed.is_empty() {
13606        return Err(blocked.expect("blocked TCP connect results should capture a reason"));
13607    }
13608
13609    Ok(allowed)
13610}
13611
13612fn resolve_tcp_connect_addr<B>(
13613    bridge: &SharedBridge<B>,
13614    kernel: &SidecarKernel,
13615    vm_id: &str,
13616    dns: &VmDnsConfig,
13617    host: &str,
13618    port: u16,
13619    context: &JavascriptSocketPathContext,
13620) -> Result<ResolvedTcpConnectAddr, SidecarError>
13621where
13622    B: NativeSidecarBridge + Send + 'static,
13623    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
13624{
13625    let allowed = filter_tcp_connect_ip_addrs(
13626        resolve_dns_ip_addrs(
13627            bridge,
13628            kernel,
13629            vm_id,
13630            dns,
13631            host,
13632            DnsLookupPolicy::SkipPermissions,
13633        )?,
13634        host,
13635        port,
13636        context,
13637    )?;
13638    let ip = allowed
13639        .iter()
13640        .copied()
13641        .find(|candidate| {
13642            let family = JavascriptSocketFamily::from_ip(*candidate);
13643            context.translate_tcp_loopback_port(family, port).is_some()
13644        })
13645        // We do not implement Happy Eyeballs yet, so prefer IPv4 over a
13646        // verbatim IPv6-first DNS answer for general outbound TCP connects.
13647        .or_else(|| allowed.iter().copied().find(IpAddr::is_ipv4))
13648        .or_else(|| allowed.first().copied())
13649        .ok_or_else(|| {
13650            SidecarError::Execution(format!("failed to resolve TCP address {host}:{port}"))
13651        })?;
13652    let family = JavascriptSocketFamily::from_ip(ip);
13653    let translated_loopback_port = context.translate_tcp_loopback_port(family, port);
13654    let use_kernel_loopback = is_loopback_ip(ip) && translated_loopback_port == Some(port);
13655    let actual_port = if is_loopback_ip(ip) {
13656        translated_loopback_port.unwrap_or(port)
13657    } else {
13658        port
13659    };
13660    Ok(ResolvedTcpConnectAddr {
13661        actual_addr: SocketAddr::new(ip, actual_port),
13662        guest_remote_addr: SocketAddr::new(ip, port),
13663        use_kernel_loopback,
13664    })
13665}
13666
13667fn resolve_dns_ip_addrs<B>(
13668    bridge: &SharedBridge<B>,
13669    kernel: &SidecarKernel,
13670    vm_id: &str,
13671    dns: &VmDnsConfig,
13672    hostname: &str,
13673    policy: DnsLookupPolicy,
13674) -> Result<Vec<IpAddr>, SidecarError>
13675where
13676    B: NativeSidecarBridge + Send + 'static,
13677    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
13678{
13679    let resolution = match kernel.resolve_dns(hostname, policy) {
13680        Ok(resolution) => resolution,
13681        Err(error) => {
13682            let sidecar_error = kernel_error(error.clone());
13683            if error.code() != "EACCES" {
13684                emit_dns_resolution_failure_event(bridge, vm_id, hostname, dns, &sidecar_error);
13685            }
13686            return Err(sidecar_error);
13687        }
13688    };
13689    emit_dns_resolution_event(
13690        bridge,
13691        vm_id,
13692        hostname,
13693        resolution.source(),
13694        resolution.addresses(),
13695        dns,
13696    );
13697    Ok(resolution.addresses().to_vec())
13698}
13699
13700fn resolve_dns_records<B>(
13701    bridge: &SharedBridge<B>,
13702    kernel: &SidecarKernel,
13703    vm_id: &str,
13704    dns: &VmDnsConfig,
13705    hostname: &str,
13706    record_type: RecordType,
13707    policy: DnsLookupPolicy,
13708) -> Result<DnsRecordResolution, SidecarError>
13709where
13710    B: NativeSidecarBridge + Send + 'static,
13711    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
13712{
13713    let resolution = match kernel.resolve_dns_records(hostname, record_type, policy) {
13714        Ok(resolution) => resolution,
13715        Err(error) => {
13716            let sidecar_error = kernel_error(error.clone());
13717            if error.code() != "EACCES" {
13718                emit_dns_resolution_failure_event(bridge, vm_id, hostname, dns, &sidecar_error);
13719            }
13720            return Err(sidecar_error);
13721        }
13722    };
13723    emit_dns_record_resolution_event(bridge, vm_id, hostname, &resolution, dns);
13724    Ok(resolution)
13725}
13726
13727fn filter_dns_ip_addrs(
13728    addresses: Vec<IpAddr>,
13729    family: Option<u8>,
13730) -> Result<Vec<IpAddr>, SidecarError> {
13731    let filtered: Vec<_> = match family.unwrap_or(0) {
13732        0 => addresses,
13733        4 => addresses
13734            .into_iter()
13735            .filter(|ip| matches!(ip, IpAddr::V4(_)))
13736            .collect(),
13737        6 => addresses
13738            .into_iter()
13739            .filter(|ip| matches!(ip, IpAddr::V6(_)))
13740            .collect(),
13741        other => {
13742            return Err(SidecarError::InvalidState(format!(
13743                "unsupported dns family {other}"
13744            )));
13745        }
13746    };
13747
13748    if filtered.is_empty() {
13749        return Err(SidecarError::Execution(String::from(
13750            "failed to resolve DNS address for requested family",
13751        )));
13752    }
13753
13754    Ok(filtered)
13755}
13756
13757fn resolve_udp_bind_addr(
13758    host: &str,
13759    port: u16,
13760    family: JavascriptUdpFamily,
13761) -> Result<SocketAddr, SidecarError> {
13762    (host, port)
13763        .to_socket_addrs()
13764        .map_err(sidecar_net_error)?
13765        .find(|addr| family.matches_addr(addr))
13766        .ok_or_else(|| {
13767            SidecarError::Execution(format!(
13768                "failed to resolve {} UDP bind address {host}:{port}",
13769                family.socket_type()
13770            ))
13771        })
13772}
13773
13774fn resolve_udp_addr<B>(request: UdpRemoteAddrRequest<'_, B>) -> Result<SocketAddr, SidecarError>
13775where
13776    B: NativeSidecarBridge + Send + 'static,
13777    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
13778{
13779    let UdpRemoteAddrRequest {
13780        bridge,
13781        kernel,
13782        vm_id,
13783        dns,
13784        host,
13785        port,
13786        family,
13787        context,
13788    } = request;
13789    resolve_dns_ip_addrs(
13790        bridge,
13791        kernel,
13792        vm_id,
13793        dns,
13794        host,
13795        DnsLookupPolicy::SkipPermissions,
13796    )?
13797    .into_iter()
13798    .map(|ip| {
13799        let family_key = JavascriptSocketFamily::from_ip(ip);
13800        let actual_port = if is_loopback_ip(ip) {
13801            context
13802                .translate_udp_loopback_port(family_key, port)
13803                .unwrap_or(port)
13804        } else {
13805            port
13806        };
13807        SocketAddr::new(ip, actual_port)
13808    })
13809    .find(|addr| family.matches_addr(addr))
13810    .ok_or_else(|| {
13811        SidecarError::Execution(format!(
13812            "failed to resolve {} UDP address {host}:{port}",
13813            family.socket_type()
13814        ))
13815    })
13816}
13817
13818fn javascript_net_timeout_value() -> Value {
13819    Value::String(String::from(JAVASCRIPT_NET_TIMEOUT_SENTINEL))
13820}
13821
13822fn javascript_net_json_string(value: Value, label: &str) -> Result<Value, SidecarError> {
13823    serde_json::to_string(&value)
13824        .map(Value::String)
13825        .map_err(|error| {
13826            SidecarError::InvalidState(format!("failed to serialize {label} payload: {error}"))
13827        })
13828}
13829
13830fn javascript_net_read_value(
13831    event: Option<JavascriptTcpSocketEvent>,
13832) -> Result<Value, SidecarError> {
13833    match event {
13834        Some(JavascriptTcpSocketEvent::Data(chunk)) => Ok(Value::String(
13835            base64::engine::general_purpose::STANDARD.encode(chunk),
13836        )),
13837        Some(JavascriptTcpSocketEvent::End | JavascriptTcpSocketEvent::Close { .. }) => {
13838            Ok(Value::Null)
13839        }
13840        Some(JavascriptTcpSocketEvent::Error { code, message }) => {
13841            let detail = code.unwrap_or_else(|| String::from("socket read"));
13842            Err(SidecarError::Execution(format!("{detail}: {message}")))
13843        }
13844        None => Ok(javascript_net_timeout_value()),
13845    }
13846}
13847
13848fn io_error_code(error: &std::io::Error) -> Option<String> {
13849    match error.raw_os_error() {
13850        Some(libc::EADDRINUSE) => Some(String::from("EADDRINUSE")),
13851        Some(libc::EADDRNOTAVAIL) => Some(String::from("EADDRNOTAVAIL")),
13852        Some(libc::ECONNREFUSED) => Some(String::from("ECONNREFUSED")),
13853        Some(libc::ECONNRESET) => Some(String::from("ECONNRESET")),
13854        Some(libc::EINVAL) => Some(String::from("EINVAL")),
13855        Some(libc::EPIPE) => Some(String::from("EPIPE")),
13856        Some(libc::ETIMEDOUT) => Some(String::from("ETIMEDOUT")),
13857        Some(libc::EHOSTUNREACH) => Some(String::from("EHOSTUNREACH")),
13858        Some(libc::ENETUNREACH) => Some(String::from("ENETUNREACH")),
13859        _ => None,
13860    }
13861}
13862
13863fn sidecar_net_error(error: std::io::Error) -> SidecarError {
13864    let message = match io_error_code(&error) {
13865        Some(code) => format!("{code}: {error}"),
13866        None => error.to_string(),
13867    };
13868    SidecarError::Execution(message)
13869}
13870
13871fn tls_provider() -> Arc<rustls::crypto::CryptoProvider> {
13872    Arc::new(aws_lc_rs::default_provider())
13873}
13874
13875fn tls_local_certificates(
13876    options: &JavascriptTlsBridgeOptions,
13877) -> Result<Vec<Vec<u8>>, SidecarError> {
13878    let Some(certificates) = options.cert.as_ref() else {
13879        return Ok(Vec::new());
13880    };
13881    tls_material_entries(certificates)
13882}
13883
13884fn tls_material_entries(material: &JavascriptTlsMaterial) -> Result<Vec<Vec<u8>>, SidecarError> {
13885    match material {
13886        JavascriptTlsMaterial::Single(entry) => tls_data_value(entry).map(|value| vec![value]),
13887        JavascriptTlsMaterial::Many(entries) => entries.iter().map(tls_data_value).collect(),
13888    }
13889}
13890
13891fn tls_data_value(value: &JavascriptTlsDataValue) -> Result<Vec<u8>, SidecarError> {
13892    match value {
13893        JavascriptTlsDataValue::Buffer { data } => base64::engine::general_purpose::STANDARD
13894            .decode(data)
13895            .map_err(|error| {
13896                SidecarError::InvalidState(format!("TLS material contains invalid base64: {error}"))
13897            }),
13898        JavascriptTlsDataValue::String { data } => Ok(data.as_bytes().to_vec()),
13899    }
13900}
13901
13902fn tls_certificates_from_material(
13903    material: &JavascriptTlsMaterial,
13904) -> Result<Vec<CertificateDer<'static>>, SidecarError> {
13905    let mut certificates = Vec::new();
13906    for entry in tls_material_entries(material)? {
13907        let mut reader = std::io::BufReader::new(Cursor::new(entry.clone()));
13908        let parsed = rustls_pemfile::certs(&mut reader)
13909            .collect::<Result<Vec<_>, _>>()
13910            .map_err(sidecar_net_error)?;
13911        if parsed.is_empty() {
13912            certificates.push(CertificateDer::from(entry));
13913        } else {
13914            certificates.extend(parsed);
13915        }
13916    }
13917    if certificates.is_empty() {
13918        return Err(SidecarError::InvalidState(String::from(
13919            "TLS certificate material did not contain any certificates",
13920        )));
13921    }
13922    Ok(certificates)
13923}
13924
13925fn tls_private_key_from_material(
13926    material: &JavascriptTlsMaterial,
13927) -> Result<PrivateKeyDer<'static>, SidecarError> {
13928    for entry in tls_material_entries(material)? {
13929        let mut reader = std::io::BufReader::new(Cursor::new(entry));
13930        if let Some(key) = rustls_pemfile::private_key(&mut reader).map_err(sidecar_net_error)? {
13931            return Ok(key);
13932        }
13933    }
13934    Err(SidecarError::InvalidState(String::from(
13935        "TLS private key material did not contain a supported key",
13936    )))
13937}
13938
13939fn tls_root_store(options: &JavascriptTlsBridgeOptions) -> Result<RootCertStore, SidecarError> {
13940    let mut roots = RootCertStore::empty();
13941    if let Some(ca) = options.ca.as_ref() {
13942        for certificate in tls_certificates_from_material(ca)? {
13943            roots.add(certificate).map_err(|error| {
13944                SidecarError::InvalidState(format!("failed to add TLS CA certificate: {error}"))
13945            })?;
13946        }
13947        return Ok(roots);
13948    }
13949
13950    for certificate in rustls_native_certs::load_native_certs().certs {
13951        roots.add(certificate).map_err(|error| {
13952            SidecarError::InvalidState(format!(
13953                "failed to add native TLS certificate to root store: {error}"
13954            ))
13955        })?;
13956    }
13957    Ok(roots)
13958}
13959
13960fn build_client_tls_stream(
13961    stream: TcpStream,
13962    options: &JavascriptTlsBridgeOptions,
13963) -> Result<rustls::StreamOwned<ClientConnection, TcpStream>, SidecarError> {
13964    let config = build_client_tls_config(options)?;
13965    let server_name = options
13966        .servername
13967        .clone()
13968        .unwrap_or_else(|| String::from("localhost"));
13969    let server_name = ServerName::try_from(server_name)
13970        .map_err(|_| SidecarError::InvalidState(String::from("invalid TLS servername")))?;
13971    stream
13972        .set_read_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
13973        .map_err(sidecar_net_error)?;
13974    stream
13975        .set_write_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
13976        .map_err(sidecar_net_error)?;
13977    let mut tls_stream = rustls::StreamOwned::new(
13978        ClientConnection::new(Arc::new(config), server_name).map_err(|error| {
13979            SidecarError::Execution(format!("failed to start TLS client: {error}"))
13980        })?,
13981        stream,
13982    );
13983    while tls_stream.conn.is_handshaking() {
13984        tls_stream
13985            .conn
13986            .complete_io(&mut tls_stream.sock)
13987            .map_err(sidecar_net_error)?;
13988    }
13989    tls_stream
13990        .sock
13991        .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
13992        .map_err(sidecar_net_error)?;
13993    tls_stream
13994        .sock
13995        .set_write_timeout(None)
13996        .map_err(sidecar_net_error)?;
13997    Ok(tls_stream)
13998}
13999
14000fn build_client_loopback_tls_stream(
14001    transport: crate::state::LoopbackTlsEndpoint,
14002    options: &JavascriptTlsBridgeOptions,
14003) -> Result<rustls::StreamOwned<ClientConnection, crate::state::LoopbackTlsEndpoint>, SidecarError>
14004{
14005    let config = build_client_tls_config(options)?;
14006    let server_name = options
14007        .servername
14008        .clone()
14009        .unwrap_or_else(|| String::from("localhost"));
14010    let server_name = ServerName::try_from(server_name)
14011        .map_err(|_| SidecarError::InvalidState(String::from("invalid TLS servername")))?;
14012    let mut tls_stream = rustls::StreamOwned::new(
14013        ClientConnection::new(Arc::new(config), server_name).map_err(|error| {
14014            SidecarError::Execution(format!("failed to start TLS client: {error}"))
14015        })?,
14016        transport,
14017    );
14018    match tls_stream.conn.complete_io(&mut tls_stream.sock) {
14019        Ok(_) => {}
14020        Err(error)
14021            if matches!(
14022                error.kind(),
14023                std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
14024            ) => {}
14025        Err(error) => return Err(sidecar_net_error(error)),
14026    }
14027    Ok(tls_stream)
14028}
14029
14030fn build_client_tls_config(
14031    options: &JavascriptTlsBridgeOptions,
14032) -> Result<ClientConfig, SidecarError> {
14033    let provider = tls_provider();
14034    let builder = ClientConfig::builder_with_provider(provider.clone())
14035        .with_safe_default_protocol_versions()
14036        .map_err(|error| {
14037            SidecarError::InvalidState(format!("invalid TLS protocol config: {error}"))
14038        })?;
14039
14040    let mut config = if options.reject_unauthorized == Some(false) {
14041        let verifier = Arc::new(InsecureTlsVerifier {
14042            supported_schemes: provider
14043                .signature_verification_algorithms
14044                .supported_schemes(),
14045        });
14046        builder
14047            .dangerous()
14048            .with_custom_certificate_verifier(verifier)
14049            .with_no_client_auth()
14050    } else {
14051        builder
14052            .with_root_certificates(tls_root_store(options)?)
14053            .with_no_client_auth()
14054    };
14055
14056    if let Some(protocols) = options.alpn_protocols.as_ref() {
14057        config.alpn_protocols = protocols
14058            .iter()
14059            .map(|protocol| protocol.as_bytes().to_vec())
14060            .collect();
14061    }
14062    Ok(config)
14063}
14064
14065fn build_server_tls_stream(
14066    stream: TcpStream,
14067    options: &JavascriptTlsBridgeOptions,
14068) -> Result<rustls::StreamOwned<ServerConnection, TcpStream>, SidecarError> {
14069    let config = build_server_tls_config(options)?;
14070    stream
14071        .set_read_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
14072        .map_err(sidecar_net_error)?;
14073    stream
14074        .set_write_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
14075        .map_err(sidecar_net_error)?;
14076    let mut tls_stream = rustls::StreamOwned::new(
14077        ServerConnection::new(Arc::new(config)).map_err(|error| {
14078            SidecarError::Execution(format!("failed to start TLS server: {error}"))
14079        })?,
14080        stream,
14081    );
14082    while tls_stream.conn.is_handshaking() {
14083        tls_stream
14084            .conn
14085            .complete_io(&mut tls_stream.sock)
14086            .map_err(sidecar_net_error)?;
14087    }
14088    tls_stream
14089        .sock
14090        .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
14091        .map_err(sidecar_net_error)?;
14092    tls_stream
14093        .sock
14094        .set_write_timeout(None)
14095        .map_err(sidecar_net_error)?;
14096    Ok(tls_stream)
14097}
14098
14099fn build_server_loopback_tls_stream(
14100    transport: crate::state::LoopbackTlsEndpoint,
14101    options: &JavascriptTlsBridgeOptions,
14102) -> Result<rustls::StreamOwned<ServerConnection, crate::state::LoopbackTlsEndpoint>, SidecarError>
14103{
14104    let config = build_server_tls_config(options)?;
14105    Ok(rustls::StreamOwned::new(
14106        ServerConnection::new(Arc::new(config)).map_err(|error| {
14107            SidecarError::Execution(format!("failed to start TLS server: {error}"))
14108        })?,
14109        transport,
14110    ))
14111}
14112
14113fn build_server_tls_config(
14114    options: &JavascriptTlsBridgeOptions,
14115) -> Result<ServerConfig, SidecarError> {
14116    let certificates = tls_certificates_from_material(options.cert.as_ref().ok_or_else(|| {
14117        SidecarError::InvalidState(String::from("TLS server upgrade requires a certificate"))
14118    })?)?;
14119    let key = tls_private_key_from_material(options.key.as_ref().ok_or_else(|| {
14120        SidecarError::InvalidState(String::from("TLS server upgrade requires a private key"))
14121    })?)?;
14122
14123    let mut config = ServerConfig::builder_with_provider(tls_provider())
14124        .with_safe_default_protocol_versions()
14125        .map_err(|error| {
14126            SidecarError::InvalidState(format!("invalid TLS protocol config: {error}"))
14127        })?
14128        .with_no_client_auth()
14129        .with_single_cert(certificates, key)
14130        .map_err(|error| {
14131            SidecarError::InvalidState(format!("invalid TLS server config: {error}"))
14132        })?;
14133
14134    if let Some(protocols) = options.alpn_protocols.as_ref() {
14135        config.alpn_protocols = protocols
14136            .iter()
14137            .map(|protocol| protocol.as_bytes().to_vec())
14138            .collect();
14139    }
14140    Ok(config)
14141}
14142
14143fn tls_protocol_name(version: rustls::ProtocolVersion) -> String {
14144    match version {
14145        rustls::ProtocolVersion::TLSv1_2 => String::from("TLSv1.2"),
14146        rustls::ProtocolVersion::TLSv1_3 => String::from("TLSv1.3"),
14147        other => other
14148            .as_str()
14149            .map(str::to_owned)
14150            .unwrap_or_else(|| format!("{other:?}")),
14151    }
14152}
14153
14154fn tls_cipher_bridge_value(suite: rustls::SupportedCipherSuite) -> Value {
14155    tls_bridge_object(vec![
14156        (
14157            "name",
14158            suite
14159                .suite()
14160                .as_str()
14161                .map(|value| Value::String(value.to_owned()))
14162                .unwrap_or(Value::Null),
14163        ),
14164        (
14165            "standardName",
14166            suite
14167                .suite()
14168                .as_str()
14169                .map(|value| Value::String(value.to_owned()))
14170                .unwrap_or(Value::Null),
14171        ),
14172        (
14173            "version",
14174            Value::String(if suite.tls13().is_some() {
14175                String::from("TLSv1.3")
14176            } else {
14177                String::from("TLSv1.2")
14178            }),
14179        ),
14180    ])
14181}
14182
14183fn tls_certificate_bridge_value(certificate: &[u8], detailed: bool) -> Value {
14184    let mut fields = vec![("raw", tls_bridge_buffer_value(certificate))];
14185    if detailed {
14186        fields.push(("issuerCertificate", tls_bridge_undefined_value()));
14187    }
14188    tls_bridge_object(fields)
14189}
14190
14191fn tls_bridge_buffer_value(bytes: &[u8]) -> Value {
14192    json!({
14193        "type": "buffer",
14194        "data": base64::engine::general_purpose::STANDARD.encode(bytes),
14195    })
14196}
14197
14198fn tls_bridge_object(entries: Vec<(&str, Value)>) -> Value {
14199    let value = entries
14200        .into_iter()
14201        .map(|(key, value)| (key.to_owned(), value))
14202        .collect::<serde_json::Map<String, Value>>();
14203    json!({
14204        "type": "object",
14205        "id": 1,
14206        "value": value,
14207    })
14208}
14209
14210fn tls_bridge_undefined_value() -> Value {
14211    json!({
14212        "type": "undefined",
14213    })
14214}
14215
14216fn spawn_tcp_socket_reader(
14217    stream: TcpStream,
14218    sender: Sender<JavascriptTcpSocketEvent>,
14219    event_pusher: Arc<Mutex<Option<JavascriptSocketEventPusher>>>,
14220    tls_mode: Arc<AtomicBool>,
14221    saw_local_shutdown: Arc<AtomicBool>,
14222    saw_remote_end: Arc<AtomicBool>,
14223    close_notified: Arc<AtomicBool>,
14224) {
14225    thread::spawn(move || {
14226        let mut stream = stream;
14227        let mut buffer = vec![0_u8; 64 * 1024];
14228        loop {
14229            if tls_mode.load(Ordering::SeqCst) {
14230                break;
14231            }
14232            match stream.read(&mut buffer) {
14233                Ok(0) => {
14234                    saw_remote_end.store(true, Ordering::SeqCst);
14235                    let _ = sender.send(JavascriptTcpSocketEvent::End);
14236                    push_socket_event(&event_pusher, "end");
14237                    if saw_local_shutdown.load(Ordering::SeqCst)
14238                        && !close_notified.swap(true, Ordering::SeqCst)
14239                    {
14240                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
14241                        push_socket_event(&event_pusher, "close");
14242                    }
14243                    break;
14244                }
14245                Ok(bytes_read) => {
14246                    if sender
14247                        .send(JavascriptTcpSocketEvent::Data(
14248                            buffer[..bytes_read].to_vec(),
14249                        ))
14250                        .is_err()
14251                    {
14252                        break;
14253                    }
14254                    push_socket_event(&event_pusher, "data");
14255                }
14256                Err(error)
14257                    if matches!(
14258                        error.kind(),
14259                        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
14260                    ) =>
14261                {
14262                    continue;
14263                }
14264                Err(error) => {
14265                    let code = io_error_code(&error);
14266                    let _ = sender.send(JavascriptTcpSocketEvent::Error {
14267                        code,
14268                        message: error.to_string(),
14269                    });
14270                    push_socket_event(&event_pusher, "error");
14271                    if !close_notified.swap(true, Ordering::SeqCst) {
14272                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
14273                        push_socket_event(&event_pusher, "close");
14274                    }
14275                    break;
14276                }
14277            }
14278        }
14279    });
14280}
14281
14282fn push_socket_event(
14283    event_pusher: &Arc<Mutex<Option<JavascriptSocketEventPusher>>>,
14284    event: &'static str,
14285) {
14286    NET_TCP_TRACE_COUNTERS
14287        .socket_read_push_attempts
14288        .fetch_add(1, Ordering::Relaxed);
14289    let target = event_pusher.lock().ok().and_then(|guard| guard.clone());
14290    let Some(target) = target else {
14291        NET_TCP_TRACE_COUNTERS
14292            .socket_read_push_missing
14293            .fetch_add(1, Ordering::Relaxed);
14294        return;
14295    };
14296    let payload = v8_runtime::json_to_cbor_payload(&json!({
14297        "socketId": target.socket_id,
14298        "event": event,
14299    }))
14300    .unwrap_or_default();
14301    match target.session.send_stream_event("net_socket", payload) {
14302        Ok(()) => {
14303            NET_TCP_TRACE_COUNTERS
14304                .socket_read_push_sent
14305                .fetch_add(1, Ordering::Relaxed);
14306        }
14307        Err(_) => {
14308            NET_TCP_TRACE_COUNTERS
14309                .socket_read_push_errors
14310                .fetch_add(1, Ordering::Relaxed);
14311        }
14312    }
14313}
14314
14315fn send_tls_socket_error_and_close(
14316    sender: &Sender<JavascriptTcpSocketEvent>,
14317    event_pusher: &Arc<Mutex<Option<JavascriptSocketEventPusher>>>,
14318    close_notified: &Arc<AtomicBool>,
14319    code: Option<String>,
14320    message: String,
14321) {
14322    let _ = sender.send(JavascriptTcpSocketEvent::Error { code, message });
14323    push_socket_event(event_pusher, "error");
14324    if !close_notified.swap(true, Ordering::SeqCst) {
14325        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
14326        push_socket_event(event_pusher, "close");
14327    }
14328}
14329
14330fn fail_loopback_tls_pending_reader(
14331    pending_write: &LoopbackTlsPendingWriteHandle,
14332    sender: &Sender<JavascriptTcpSocketEvent>,
14333    event_pusher: &Arc<Mutex<Option<JavascriptSocketEventPusher>>>,
14334    close_notified: &Arc<AtomicBool>,
14335    code: Option<String>,
14336    message: String,
14337) {
14338    pending_write.mark_failed(message.clone());
14339    send_tls_socket_error_and_close(sender, event_pusher, close_notified, code, message);
14340}
14341
14342fn flush_loopback_tls_pending_writes(
14343    tls_stream: &Arc<Mutex<Option<ActiveTlsStream>>>,
14344    pending_write: &LoopbackTlsPendingWriteHandle,
14345    sender: &Sender<JavascriptTcpSocketEvent>,
14346    event_pusher: &Arc<Mutex<Option<JavascriptSocketEventPusher>>>,
14347    close_notified: &Arc<AtomicBool>,
14348) -> bool {
14349    loop {
14350        let bytes = match pending_write.take_buffer_for_flush() {
14351            Ok(bytes) => bytes,
14352            Err(error) => {
14353                fail_loopback_tls_pending_reader(
14354                    pending_write,
14355                    sender,
14356                    event_pusher,
14357                    close_notified,
14358                    Some(String::from("EIO")),
14359                    error.to_string(),
14360                );
14361                return false;
14362            }
14363        };
14364
14365        if let Some(bytes) = bytes {
14366            let write_result = {
14367                let mut guard = match tls_stream.lock() {
14368                    Ok(guard) => guard,
14369                    Err(_) => {
14370                        pending_write.finish_flush();
14371                        fail_loopback_tls_pending_reader(
14372                            pending_write,
14373                            sender,
14374                            event_pusher,
14375                            close_notified,
14376                            Some(String::from("EIO")),
14377                            String::from("TLS stream lock poisoned while flushing pending writes"),
14378                        );
14379                        return false;
14380                    }
14381                };
14382                let Some(stream) = guard.as_mut() else {
14383                    pending_write.finish_flush();
14384                    fail_loopback_tls_pending_reader(
14385                        pending_write,
14386                        sender,
14387                        event_pusher,
14388                        close_notified,
14389                        Some(String::from("EIO")),
14390                        String::from("TLS stream missing while flushing pending writes"),
14391                    );
14392                    return false;
14393                };
14394                stream.write_all(&bytes)
14395            };
14396            pending_write.finish_flush();
14397            if let Err(error) = write_result {
14398                fail_loopback_tls_pending_reader(
14399                    pending_write,
14400                    sender,
14401                    event_pusher,
14402                    close_notified,
14403                    Some(String::from("EIO")),
14404                    format!("loopback TLS pending write flush failed: {error}"),
14405                );
14406                return false;
14407            }
14408            continue;
14409        }
14410
14411        let should_shutdown = match pending_write.take_deferred_shutdown_write() {
14412            Ok(should_shutdown) => should_shutdown,
14413            Err(error) => {
14414                fail_loopback_tls_pending_reader(
14415                    pending_write,
14416                    sender,
14417                    event_pusher,
14418                    close_notified,
14419                    Some(String::from("EIO")),
14420                    error.to_string(),
14421                );
14422                return false;
14423            }
14424        };
14425        if should_shutdown {
14426            let shutdown_result = {
14427                let mut guard = match tls_stream.lock() {
14428                    Ok(guard) => guard,
14429                    Err(_) => {
14430                        fail_loopback_tls_pending_reader(
14431                            pending_write,
14432                            sender,
14433                            event_pusher,
14434                            close_notified,
14435                            Some(String::from("EIO")),
14436                            String::from("TLS stream lock poisoned during deferred shutdown"),
14437                        );
14438                        return false;
14439                    }
14440                };
14441                let Some(stream) = guard.as_mut() else {
14442                    fail_loopback_tls_pending_reader(
14443                        pending_write,
14444                        sender,
14445                        event_pusher,
14446                        close_notified,
14447                        Some(String::from("EIO")),
14448                        String::from("TLS stream missing during deferred shutdown"),
14449                    );
14450                    return false;
14451                };
14452                stream
14453                    .send_close_notify()
14454                    .and_then(|_| stream.shutdown_write())
14455            };
14456            if let Err(error) = shutdown_result {
14457                fail_loopback_tls_pending_reader(
14458                    pending_write,
14459                    sender,
14460                    event_pusher,
14461                    close_notified,
14462                    Some(String::from("EIO")),
14463                    format!("loopback TLS deferred shutdown failed: {error}"),
14464                );
14465                return false;
14466            }
14467            continue;
14468        }
14469
14470        return true;
14471    }
14472}
14473
14474fn spawn_tls_socket_reader(
14475    tls_stream: Arc<Mutex<Option<ActiveTlsStream>>>,
14476    loopback_pending_write: Option<LoopbackTlsPendingWriteHandle>,
14477    sender: Sender<JavascriptTcpSocketEvent>,
14478    event_pusher: Arc<Mutex<Option<JavascriptSocketEventPusher>>>,
14479    saw_local_shutdown: Arc<AtomicBool>,
14480    saw_remote_end: Arc<AtomicBool>,
14481    close_notified: Arc<AtomicBool>,
14482) {
14483    thread::spawn(move || {
14484        let mut buffer = vec![0_u8; 64 * 1024];
14485        loop {
14486            if let Some(pending_write) = loopback_pending_write.as_ref() {
14487                let is_handshaking = {
14488                    let mut guard = match tls_stream.lock() {
14489                        Ok(guard) => guard,
14490                        Err(_) => {
14491                            pending_write.mark_failed("TLS stream lock poisoned");
14492                            return;
14493                        }
14494                    };
14495                    let Some(stream) = guard.as_mut() else {
14496                        pending_write.mark_failed("TLS stream missing");
14497                        return;
14498                    };
14499                    if stream.is_loopback() {
14500                        let is_handshaking = stream.is_handshaking();
14501                        stream.set_loopback_poll_timeout(LOOPBACK_TLS_HANDSHAKE_POLL_TIMEOUT);
14502                        is_handshaking
14503                    } else {
14504                        false
14505                    }
14506                };
14507
14508                if is_handshaking {
14509                    if pending_write.handshake_started_at.elapsed() >= TLS_HANDSHAKE_TIMEOUT {
14510                        fail_loopback_tls_pending_reader(
14511                            pending_write,
14512                            &sender,
14513                            &event_pusher,
14514                            &close_notified,
14515                            Some(String::from("ETIMEDOUT")),
14516                            format!(
14517                                "loopback TLS handshake timed out after {}ms",
14518                                TLS_HANDSHAKE_TIMEOUT.as_millis()
14519                            ),
14520                        );
14521                        return;
14522                    }
14523                } else {
14524                    pending_write
14525                        .tls_handshake_complete
14526                        .store(true, Ordering::SeqCst);
14527                    if !flush_loopback_tls_pending_writes(
14528                        &tls_stream,
14529                        pending_write,
14530                        &sender,
14531                        &event_pusher,
14532                        &close_notified,
14533                    ) {
14534                        return;
14535                    }
14536                }
14537            }
14538
14539            let read_result = {
14540                let mut guard = match tls_stream.lock() {
14541                    Ok(guard) => guard,
14542                    Err(_) => {
14543                        if let Some(pending_write) = loopback_pending_write.as_ref() {
14544                            pending_write.mark_failed("TLS stream lock poisoned");
14545                        }
14546                        return;
14547                    }
14548                };
14549                let Some(stream) = guard.as_mut() else {
14550                    if let Some(pending_write) = loopback_pending_write.as_ref() {
14551                        pending_write.mark_failed("TLS stream missing");
14552                    }
14553                    return;
14554                };
14555                stream.read(&mut buffer)
14556            };
14557
14558            match read_result {
14559                Ok(0) => {
14560                    if let Some(pending_write) = loopback_pending_write.as_ref() {
14561                        pending_write.fail_if_pending(
14562                            "loopback TLS peer closed before buffered writes flushed",
14563                        );
14564                    }
14565                    saw_remote_end.store(true, Ordering::SeqCst);
14566                    let _ = sender.send(JavascriptTcpSocketEvent::End);
14567                    push_socket_event(&event_pusher, "end");
14568                    if saw_local_shutdown.load(Ordering::SeqCst)
14569                        && !close_notified.swap(true, Ordering::SeqCst)
14570                    {
14571                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
14572                        push_socket_event(&event_pusher, "close");
14573                    }
14574                    break;
14575                }
14576                Ok(bytes_read) => {
14577                    if sender
14578                        .send(JavascriptTcpSocketEvent::Data(
14579                            buffer[..bytes_read].to_vec(),
14580                        ))
14581                        .is_err()
14582                    {
14583                        break;
14584                    }
14585                    push_socket_event(&event_pusher, "data");
14586                }
14587                Err(error)
14588                    if matches!(
14589                        error.kind(),
14590                        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
14591                    ) =>
14592                {
14593                    // The TLS reader and writer share one rustls stream mutex. Yield after
14594                    // timed-out reads so request writes can acquire the lock promptly.
14595                    std::thread::sleep(Duration::from_millis(1));
14596                    continue;
14597                }
14598                Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => {
14599                    if let Some(pending_write) = loopback_pending_write.as_ref() {
14600                        pending_write.fail_if_pending(
14601                            "loopback TLS peer closed before buffered writes flushed",
14602                        );
14603                    }
14604                    saw_remote_end.store(true, Ordering::SeqCst);
14605                    let _ = sender.send(JavascriptTcpSocketEvent::End);
14606                    push_socket_event(&event_pusher, "end");
14607                    if saw_local_shutdown.load(Ordering::SeqCst)
14608                        && !close_notified.swap(true, Ordering::SeqCst)
14609                    {
14610                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
14611                        push_socket_event(&event_pusher, "close");
14612                    }
14613                    break;
14614                }
14615                Err(error) => {
14616                    if let Some(pending_write) = loopback_pending_write.as_ref() {
14617                        pending_write.mark_failed(format!(
14618                            "loopback TLS reader failed before buffered writes flushed: {error}"
14619                        ));
14620                    }
14621                    let code = io_error_code(&error);
14622                    let _ = sender.send(JavascriptTcpSocketEvent::Error {
14623                        code,
14624                        message: error.to_string(),
14625                    });
14626                    push_socket_event(&event_pusher, "error");
14627                    if !close_notified.swap(true, Ordering::SeqCst) {
14628                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
14629                        push_socket_event(&event_pusher, "close");
14630                    }
14631                    break;
14632                }
14633            }
14634        }
14635    });
14636}
14637
14638fn spawn_unix_socket_reader(
14639    stream: UnixStream,
14640    sender: Sender<JavascriptTcpSocketEvent>,
14641    event_pusher: Arc<Mutex<Option<JavascriptSocketEventPusher>>>,
14642    saw_local_shutdown: Arc<AtomicBool>,
14643    saw_remote_end: Arc<AtomicBool>,
14644    close_notified: Arc<AtomicBool>,
14645) {
14646    thread::spawn(move || {
14647        let mut stream = stream;
14648        let mut buffer = vec![0_u8; 64 * 1024];
14649        loop {
14650            match stream.read(&mut buffer) {
14651                Ok(0) => {
14652                    saw_remote_end.store(true, Ordering::SeqCst);
14653                    let _ = sender.send(JavascriptTcpSocketEvent::End);
14654                    push_socket_event(&event_pusher, "end");
14655                    if saw_local_shutdown.load(Ordering::SeqCst)
14656                        && !close_notified.swap(true, Ordering::SeqCst)
14657                    {
14658                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
14659                        push_socket_event(&event_pusher, "close");
14660                    }
14661                    break;
14662                }
14663                Ok(bytes_read) => {
14664                    if sender
14665                        .send(JavascriptTcpSocketEvent::Data(
14666                            buffer[..bytes_read].to_vec(),
14667                        ))
14668                        .is_err()
14669                    {
14670                        break;
14671                    }
14672                    push_socket_event(&event_pusher, "data");
14673                }
14674                Err(error) => {
14675                    let code = io_error_code(&error);
14676                    let _ = sender.send(JavascriptTcpSocketEvent::Error {
14677                        code,
14678                        message: error.to_string(),
14679                    });
14680                    push_socket_event(&event_pusher, "error");
14681                    if !close_notified.swap(true, Ordering::SeqCst) {
14682                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
14683                        push_socket_event(&event_pusher, "close");
14684                    }
14685                    break;
14686                }
14687            }
14688        }
14689    });
14690}
14691
14692/// Unblock a guest thread parked in a deferred `__kernel_stdin_read` /
14693/// `__kernel_poll` sync RPC. Isolate termination cannot interrupt the native
14694/// bridge wait, so teardown must answer the parked RPC BEFORE dropping the
14695/// execution (drop joins the guest thread) or cleanup deadlocks against it.
14696fn flush_parked_kernel_wait_rpc(process: &mut ActiveProcess) {
14697    if let Some((request, _)) = process.deferred_kernel_wait_rpc.take() {
14698        let _ = process
14699            .execution
14700            .respond_javascript_sync_rpc_error(request.id, "EINTR", "process teardown")
14701            .or_else(ignore_stale_javascript_sync_rpc_response);
14702    }
14703}
14704
14705fn terminate_child_process_tree(
14706    kernel: &mut SidecarKernel,
14707    process: &mut ActiveProcess,
14708    kernel_readiness: &KernelSocketReadinessRegistry,
14709) {
14710    flush_parked_kernel_wait_rpc(process);
14711    let sqlite_database_ids = process.sqlite_databases.keys().copied().collect::<Vec<_>>();
14712    for database_id in sqlite_database_ids {
14713        let _ = close_sqlite_database(kernel, process, database_id);
14714    }
14715    process.sqlite_statements.clear();
14716    process.http_servers.clear();
14717    process.pending_http_requests.clear();
14718    if let Ok(mut http2) = process.http2.shared.lock() {
14719        let sessions = http2.sessions.values().cloned().collect::<Vec<_>>();
14720        http2.server_events.clear();
14721        http2.session_events.clear();
14722        http2.streams.clear();
14723        http2.servers.clear();
14724        http2.sessions.clear();
14725        http2.event_session = None;
14726        drop(http2);
14727        for session in sessions {
14728            let (respond_to, _rx) = mpsc::channel();
14729            let _ = session.command_tx.send(Http2SessionCommand::Close {
14730                abrupt: true,
14731                respond_to,
14732            });
14733        }
14734    }
14735
14736    let listener_ids = process.tcp_listeners.keys().cloned().collect::<Vec<_>>();
14737    for listener_id in listener_ids {
14738        if let Some(listener) = process.tcp_listeners.remove(&listener_id) {
14739            unregister_kernel_readiness_target(kernel_readiness, listener.kernel_socket_id);
14740            let _ = listener.close(kernel, process.kernel_pid);
14741        }
14742    }
14743
14744    let sockets = process.tcp_sockets.keys().cloned().collect::<Vec<_>>();
14745    for socket_id in sockets {
14746        if let Some(socket) = process.tcp_sockets.remove(&socket_id) {
14747            unregister_kernel_readiness_target(kernel_readiness, socket.kernel_socket_id);
14748            let _ = socket.close(kernel, process.kernel_pid);
14749        }
14750    }
14751
14752    let unix_listener_ids = process.unix_listeners.keys().cloned().collect::<Vec<_>>();
14753    for listener_id in unix_listener_ids {
14754        if let Some(listener) = process.unix_listeners.remove(&listener_id) {
14755            let _ = listener.close();
14756        }
14757    }
14758
14759    let unix_sockets = process.unix_sockets.keys().cloned().collect::<Vec<_>>();
14760    for socket_id in unix_sockets {
14761        if let Some(socket) = process.unix_sockets.remove(&socket_id) {
14762            let _ = socket.close();
14763        }
14764    }
14765
14766    let udp_socket_ids = process.udp_sockets.keys().cloned().collect::<Vec<_>>();
14767    for socket_id in udp_socket_ids {
14768        if let Some(mut socket) = process.udp_sockets.remove(&socket_id) {
14769            unregister_kernel_readiness_target(kernel_readiness, socket.kernel_socket_id);
14770            socket.close(kernel, process.kernel_pid);
14771        }
14772    }
14773
14774    let child_ids = process.child_processes.keys().cloned().collect::<Vec<_>>();
14775    for child_id in child_ids {
14776        let Some(mut child) = process.child_processes.remove(&child_id) else {
14777            continue;
14778        };
14779        terminate_child_process_tree(kernel, &mut child, kernel_readiness);
14780        let _ = kernel.kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, SIGTERM);
14781        let _ = signal_runtime_process(child.execution.child_pid(), SIGTERM);
14782        child.kernel_handle.finish(0);
14783        let _ = kernel.wait_and_reap(child.kernel_pid);
14784    }
14785}
14786
14787fn service_javascript_sqlite_sync_rpc(
14788    kernel: &mut SidecarKernel,
14789    process: &mut ActiveProcess,
14790    request: &JavascriptSyncRpcRequest,
14791) -> Result<Value, SidecarError> {
14792    match request.method.as_str() {
14793        "sqlite.constants" => Ok(json!({})),
14794        "sqlite.open" => sqlite_open_database(kernel, process, request),
14795        "sqlite.close" => {
14796            let database_id =
14797                javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.close database id")?;
14798            close_sqlite_database(kernel, process, database_id)?;
14799            Ok(Value::Null)
14800        }
14801        "sqlite.exec" => sqlite_exec_database(kernel, process, request),
14802        "sqlite.query" => sqlite_query_database(process, request),
14803        "sqlite.prepare" => sqlite_prepare_statement(process, request),
14804        "sqlite.location" => {
14805            let database_id =
14806                javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.location database id")?;
14807            let database = sqlite_database(process, database_id)?;
14808            Ok(database
14809                .vm_path
14810                .as_ref()
14811                .map(|path| Value::String(path.clone()))
14812                .unwrap_or(Value::Null))
14813        }
14814        "sqlite.checkpoint" => {
14815            let database_id =
14816                javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.checkpoint database id")?;
14817            let kernel_pid = process.kernel_pid;
14818            let database = sqlite_database_mut(process, database_id)?;
14819            sqlite_sync_database(kernel, kernel_pid, database)?;
14820            Ok(Value::Null)
14821        }
14822        "sqlite.statement.run" => sqlite_run_statement(kernel, process, request),
14823        "sqlite.statement.get" => sqlite_get_statement(process, request),
14824        "sqlite.statement.all" | "sqlite.statement.iterate" => {
14825            sqlite_all_statement(process, request)
14826        }
14827        "sqlite.statement.columns" => sqlite_statement_columns(process, request),
14828        "sqlite.statement.setReturnArrays" => {
14829            let statement_id = javascript_sync_rpc_arg_u64(
14830                &request.args,
14831                0,
14832                "sqlite.statement.setReturnArrays statement id",
14833            )?;
14834            let enabled = javascript_sync_rpc_arg_bool(
14835                &request.args,
14836                1,
14837                "sqlite.statement.setReturnArrays enabled",
14838            )?;
14839            sqlite_statement_mut(process, statement_id)?.return_arrays = enabled;
14840            Ok(Value::Null)
14841        }
14842        "sqlite.statement.setReadBigInts" => {
14843            let statement_id = javascript_sync_rpc_arg_u64(
14844                &request.args,
14845                0,
14846                "sqlite.statement.setReadBigInts statement id",
14847            )?;
14848            let enabled = javascript_sync_rpc_arg_bool(
14849                &request.args,
14850                1,
14851                "sqlite.statement.setReadBigInts enabled",
14852            )?;
14853            sqlite_statement_mut(process, statement_id)?.read_bigints = enabled;
14854            Ok(Value::Null)
14855        }
14856        "sqlite.statement.setAllowBareNamedParameters" => {
14857            let statement_id = javascript_sync_rpc_arg_u64(
14858                &request.args,
14859                0,
14860                "sqlite.statement.setAllowBareNamedParameters statement id",
14861            )?;
14862            let enabled = javascript_sync_rpc_arg_bool(
14863                &request.args,
14864                1,
14865                "sqlite.statement.setAllowBareNamedParameters enabled",
14866            )?;
14867            sqlite_statement_mut(process, statement_id)?.allow_bare_named_parameters = enabled;
14868            Ok(Value::Null)
14869        }
14870        "sqlite.statement.setAllowUnknownNamedParameters" => {
14871            let statement_id = javascript_sync_rpc_arg_u64(
14872                &request.args,
14873                0,
14874                "sqlite.statement.setAllowUnknownNamedParameters statement id",
14875            )?;
14876            let enabled = javascript_sync_rpc_arg_bool(
14877                &request.args,
14878                1,
14879                "sqlite.statement.setAllowUnknownNamedParameters enabled",
14880            )?;
14881            sqlite_statement_mut(process, statement_id)?.allow_unknown_named_parameters = enabled;
14882            Ok(Value::Null)
14883        }
14884        "sqlite.statement.finalize" => {
14885            let statement_id = javascript_sync_rpc_arg_u64(
14886                &request.args,
14887                0,
14888                "sqlite.statement.finalize statement id",
14889            )?;
14890            process
14891                .sqlite_statements
14892                .remove(&statement_id)
14893                .ok_or_else(|| {
14894                    SidecarError::InvalidState(format!(
14895                        "sqlite statement handle not found: {statement_id}"
14896                    ))
14897                })?;
14898            Ok(Value::Null)
14899        }
14900        other => Err(SidecarError::InvalidState(format!(
14901            "unsupported JavaScript sqlite sync RPC method {other}"
14902        ))),
14903    }
14904}
14905
14906fn sqlite_open_database(
14907    kernel: &mut SidecarKernel,
14908    process: &mut ActiveProcess,
14909    request: &JavascriptSyncRpcRequest,
14910) -> Result<Value, SidecarError> {
14911    ensure_per_process_state_handle_capacity(process.sqlite_databases.len(), "sqlite database")?;
14912    let path = request.args.first().and_then(Value::as_str);
14913    let vm_path = path.filter(|value| !value.is_empty() && *value != ":memory:");
14914    let options = request.args.get(1);
14915    let read_only = sqlite_option_bool(options, "readOnly").unwrap_or(false);
14916    let create = sqlite_option_bool(options, "create").unwrap_or(!read_only);
14917    let timeout_ms = sqlite_option_u64(options, "timeout");
14918
14919    process.next_sqlite_database_id += 1;
14920    let database_id = process.next_sqlite_database_id;
14921
14922    let host_path = if vm_path.is_some() {
14923        Some(
14924            std::env::temp_dir()
14925                .join(format!(
14926                    "secure-exec-sidecar-sqlite-{}-{database_id}",
14927                    process.kernel_pid
14928                ))
14929                .join("database.sqlite"),
14930        )
14931    } else {
14932        None
14933    };
14934
14935    if let Some(host_path) = host_path.as_ref() {
14936        if let Some(parent) = host_path.parent() {
14937            fs::create_dir_all(parent).map_err(|error| {
14938                SidecarError::Io(format!(
14939                    "failed to prepare sqlite temp directory {}: {error}",
14940                    parent.display()
14941                ))
14942            })?;
14943        }
14944    }
14945
14946    if let (Some(vm_path), Some(host_path)) = (vm_path, host_path.as_ref()) {
14947        if kernel
14948            .exists_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, vm_path)
14949            .map_err(kernel_error)?
14950        {
14951            let contents = kernel
14952                .read_file_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, vm_path)
14953                .map_err(kernel_error)?;
14954            fs::write(host_path, contents).map_err(|error| {
14955                SidecarError::Io(format!(
14956                    "failed to materialize sqlite database {}: {error}",
14957                    host_path.display()
14958                ))
14959            })?;
14960        } else if read_only && !create {
14961            return Err(SidecarError::InvalidState(format!(
14962                "sqlite database does not exist: {vm_path}"
14963            )));
14964        }
14965    }
14966
14967    let target = host_path
14968        .as_ref()
14969        .map(|path| path.to_string_lossy().into_owned())
14970        .unwrap_or_else(|| String::from(":memory:"));
14971    let mut flags = if read_only {
14972        SqliteOpenFlags::SQLITE_OPEN_READ_ONLY
14973    } else {
14974        SqliteOpenFlags::SQLITE_OPEN_READ_WRITE
14975    };
14976    if create && !read_only {
14977        flags |= SqliteOpenFlags::SQLITE_OPEN_CREATE;
14978    }
14979
14980    let connection = SqliteConnection::open_with_flags(&target, flags).map_err(|error| {
14981        SidecarError::InvalidState(format!(
14982            "sqlite database open failed for {}: {error}",
14983            vm_path.unwrap_or(":memory:")
14984        ))
14985    })?;
14986    if let Some(timeout_ms) = timeout_ms {
14987        connection
14988            .busy_timeout(Duration::from_millis(timeout_ms))
14989            .map_err(sqlite_error)?;
14990    }
14991    if host_path.is_some() && !read_only {
14992        let _ = connection.pragma_update(None, "journal_mode", "WAL");
14993    }
14994
14995    process.sqlite_databases.insert(
14996        database_id,
14997        ActiveSqliteDatabase {
14998            connection,
14999            host_path,
15000            vm_path: vm_path.map(String::from),
15001            dirty: false,
15002            transaction_depth: 0,
15003            read_only,
15004        },
15005    );
15006
15007    Ok(json!(database_id))
15008}
15009
15010fn sqlite_exec_database(
15011    kernel: &mut SidecarKernel,
15012    process: &mut ActiveProcess,
15013    request: &JavascriptSyncRpcRequest,
15014) -> Result<Value, SidecarError> {
15015    let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.exec database id")?;
15016    let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.exec sql")?;
15017    let kernel_pid = process.kernel_pid;
15018    let database = sqlite_database_mut(process, database_id)?;
15019    let before = database.connection.total_changes();
15020    database
15021        .connection
15022        .execute_batch(sql)
15023        .map_err(sqlite_error)?;
15024    mark_sqlite_mutation(database, sql);
15025    sqlite_sync_database(kernel, kernel_pid, database)?;
15026    Ok(json!(database
15027        .connection
15028        .total_changes()
15029        .saturating_sub(before)))
15030}
15031
15032fn sqlite_query_database(
15033    process: &mut ActiveProcess,
15034    request: &JavascriptSyncRpcRequest,
15035) -> Result<Value, SidecarError> {
15036    let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.query database id")?;
15037    let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.query sql")?;
15038    let params = request.args.get(2);
15039    let options = request.args.get(3);
15040    let return_arrays = sqlite_option_bool(options, "returnArrays").unwrap_or(false);
15041    let read_bigints = sqlite_option_bool(options, "readBigInts").unwrap_or(false);
15042    let database = sqlite_database_mut(process, database_id)?;
15043    sqlite_query_rows(
15044        &mut database.connection,
15045        sql,
15046        params,
15047        return_arrays,
15048        read_bigints,
15049        true,
15050        false,
15051    )
15052}
15053
15054fn sqlite_prepare_statement(
15055    process: &mut ActiveProcess,
15056    request: &JavascriptSyncRpcRequest,
15057) -> Result<Value, SidecarError> {
15058    ensure_per_process_state_handle_capacity(process.sqlite_statements.len(), "sqlite statement")?;
15059    let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.prepare database id")?;
15060    let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.prepare sql")?;
15061    let _ = sqlite_database(process, database_id)?;
15062    process.next_sqlite_statement_id += 1;
15063    let statement_id = process.next_sqlite_statement_id;
15064    process.sqlite_statements.insert(
15065        statement_id,
15066        ActiveSqliteStatement {
15067            database_id,
15068            sql: sql.to_owned(),
15069            return_arrays: false,
15070            read_bigints: false,
15071            allow_bare_named_parameters: false,
15072            allow_unknown_named_parameters: false,
15073        },
15074    );
15075    Ok(json!(statement_id))
15076}
15077
15078fn sqlite_run_statement(
15079    kernel: &mut SidecarKernel,
15080    process: &mut ActiveProcess,
15081    request: &JavascriptSyncRpcRequest,
15082) -> Result<Value, SidecarError> {
15083    let statement_id =
15084        javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.run statement id")?;
15085    let params = request.args.get(1);
15086    let statement_state = sqlite_statement(process, statement_id)?.clone();
15087    let kernel_pid = process.kernel_pid;
15088    let database = sqlite_database_mut(process, statement_state.database_id)?;
15089    let before = database.connection.total_changes();
15090    {
15091        let mut statement = database
15092            .connection
15093            .prepare(&statement_state.sql)
15094            .map_err(sqlite_error)?;
15095        bind_sqlite_parameters(
15096            &mut statement,
15097            params,
15098            statement_state.allow_bare_named_parameters,
15099            statement_state.allow_unknown_named_parameters,
15100        )?;
15101        statement.raw_execute().map_err(sqlite_error)?;
15102    }
15103    let changes = database.connection.total_changes().saturating_sub(before);
15104    let last_insert_rowid = database.connection.last_insert_rowid();
15105    mark_sqlite_mutation(database, &statement_state.sql);
15106    sqlite_sync_database(kernel, kernel_pid, database)?;
15107    let result = json!({
15108        "changes": changes,
15109        "lastInsertRowid": encode_sqlite_integer(last_insert_rowid, true),
15110    });
15111    Ok(result)
15112}
15113
15114fn sqlite_get_statement(
15115    process: &mut ActiveProcess,
15116    request: &JavascriptSyncRpcRequest,
15117) -> Result<Value, SidecarError> {
15118    let statement_id =
15119        javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.get statement id")?;
15120    let params = request.args.get(1);
15121    let statement_state = sqlite_statement(process, statement_id)?.clone();
15122    let database = sqlite_database_mut(process, statement_state.database_id)?;
15123    let rows = sqlite_query_rows(
15124        &mut database.connection,
15125        &statement_state.sql,
15126        params,
15127        statement_state.return_arrays,
15128        statement_state.read_bigints,
15129        statement_state.allow_bare_named_parameters,
15130        statement_state.allow_unknown_named_parameters,
15131    )?;
15132    Ok(rows
15133        .as_array()
15134        .and_then(|rows| rows.first().cloned())
15135        .unwrap_or(Value::Null))
15136}
15137
15138fn sqlite_all_statement(
15139    process: &mut ActiveProcess,
15140    request: &JavascriptSyncRpcRequest,
15141) -> Result<Value, SidecarError> {
15142    let statement_id =
15143        javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.all statement id")?;
15144    let params = request.args.get(1);
15145    let statement_state = sqlite_statement(process, statement_id)?.clone();
15146    let database = sqlite_database_mut(process, statement_state.database_id)?;
15147    sqlite_query_rows(
15148        &mut database.connection,
15149        &statement_state.sql,
15150        params,
15151        statement_state.return_arrays,
15152        statement_state.read_bigints,
15153        statement_state.allow_bare_named_parameters,
15154        statement_state.allow_unknown_named_parameters,
15155    )
15156}
15157
15158fn sqlite_statement_columns(
15159    process: &mut ActiveProcess,
15160    request: &JavascriptSyncRpcRequest,
15161) -> Result<Value, SidecarError> {
15162    let statement_id =
15163        javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.columns statement id")?;
15164    let statement_state = sqlite_statement(process, statement_id)?.clone();
15165    let database = sqlite_database_mut(process, statement_state.database_id)?;
15166    let statement = database
15167        .connection
15168        .prepare(&statement_state.sql)
15169        .map_err(sqlite_error)?;
15170    Ok(Value::Array(
15171        statement
15172            .column_names()
15173            .iter()
15174            .map(|name| json!({ "name": name }))
15175            .collect(),
15176    ))
15177}
15178
15179fn sqlite_query_rows(
15180    connection: &mut SqliteConnection,
15181    sql: &str,
15182    params: Option<&Value>,
15183    return_arrays: bool,
15184    read_bigints: bool,
15185    allow_bare_named_parameters: bool,
15186    allow_unknown_named_parameters: bool,
15187) -> Result<Value, SidecarError> {
15188    let mut statement = connection.prepare(sql).map_err(sqlite_error)?;
15189    let column_names = statement
15190        .column_names()
15191        .iter()
15192        .map(|name| (*name).to_owned())
15193        .collect::<Vec<_>>();
15194    let column_count = statement.column_count();
15195    bind_sqlite_parameters(
15196        &mut statement,
15197        params,
15198        allow_bare_named_parameters,
15199        allow_unknown_named_parameters,
15200    )?;
15201    let mut rows = statement.raw_query();
15202    let mut encoded_rows = Vec::new();
15203    while let Some(row) = rows.next().map_err(sqlite_error)? {
15204        encoded_rows.push(encode_sqlite_row(
15205            row,
15206            &column_names,
15207            column_count,
15208            return_arrays,
15209            read_bigints,
15210        )?);
15211    }
15212    Ok(Value::Array(encoded_rows))
15213}
15214
15215fn encode_sqlite_row(
15216    row: &rusqlite::Row<'_>,
15217    column_names: &[String],
15218    column_count: usize,
15219    return_arrays: bool,
15220    read_bigints: bool,
15221) -> Result<Value, SidecarError> {
15222    if return_arrays {
15223        let mut values = Vec::with_capacity(column_count);
15224        for index in 0..column_count {
15225            values.push(encode_sqlite_value_ref(
15226                row.get_ref(index).map_err(sqlite_error)?,
15227                read_bigints,
15228            )?);
15229        }
15230        return Ok(Value::Array(values));
15231    }
15232
15233    let mut object = Map::with_capacity(column_count);
15234    for (index, name) in column_names.iter().enumerate() {
15235        object.insert(
15236            name.clone(),
15237            encode_sqlite_value_ref(row.get_ref(index).map_err(sqlite_error)?, read_bigints)?,
15238        );
15239    }
15240    Ok(Value::Object(object))
15241}
15242
15243fn encode_sqlite_value_ref(
15244    value: SqliteValueRef<'_>,
15245    read_bigints: bool,
15246) -> Result<Value, SidecarError> {
15247    Ok(match value {
15248        SqliteValueRef::Null => Value::Null,
15249        SqliteValueRef::Integer(number) => encode_sqlite_integer(number, read_bigints),
15250        SqliteValueRef::Real(number) => json!(number),
15251        SqliteValueRef::Text(text) => Value::String(String::from_utf8_lossy(text).into_owned()),
15252        SqliteValueRef::Blob(bytes) => json!({
15253            "__agentosSqliteType": "uint8array",
15254            "value": base64::engine::general_purpose::STANDARD.encode(bytes),
15255        }),
15256    })
15257}
15258
15259fn encode_sqlite_integer(number: i64, read_bigints: bool) -> Value {
15260    if read_bigints || number.abs() > SQLITE_JS_SAFE_INTEGER_MAX {
15261        json!({
15262            "__agentosSqliteType": "bigint",
15263            "value": number.to_string(),
15264        })
15265    } else {
15266        json!(number)
15267    }
15268}
15269
15270fn bind_sqlite_parameters(
15271    statement: &mut SqliteStatement<'_>,
15272    params: Option<&Value>,
15273    allow_bare_named_parameters: bool,
15274    allow_unknown_named_parameters: bool,
15275) -> Result<(), SidecarError> {
15276    let Some(params) = params else {
15277        return Ok(());
15278    };
15279    match params {
15280        Value::Null => Ok(()),
15281        Value::Array(values) => {
15282            for (index, value) in values.iter().enumerate() {
15283                statement
15284                    .raw_bind_parameter(index + 1, decode_sqlite_parameter(value)?)
15285                    .map_err(sqlite_error)?;
15286            }
15287            Ok(())
15288        }
15289        Value::Object(map)
15290            if map
15291                .get("__agentosSqliteType")
15292                .and_then(Value::as_str)
15293                .is_none() =>
15294        {
15295            for (key, value) in map {
15296                let index =
15297                    resolve_sqlite_parameter_index(statement, key, allow_bare_named_parameters)?;
15298                let Some(index) = index else {
15299                    if allow_unknown_named_parameters {
15300                        continue;
15301                    }
15302                    return Err(SidecarError::InvalidState(format!(
15303                        "sqlite named parameter not found: {key}"
15304                    )));
15305                };
15306                statement
15307                    .raw_bind_parameter(index, decode_sqlite_parameter(value)?)
15308                    .map_err(sqlite_error)?;
15309            }
15310            Ok(())
15311        }
15312        other => statement
15313            .raw_bind_parameter(1, decode_sqlite_parameter(other)?)
15314            .map_err(sqlite_error),
15315    }
15316}
15317
15318fn resolve_sqlite_parameter_index(
15319    statement: &mut SqliteStatement<'_>,
15320    key: &str,
15321    allow_bare_named_parameters: bool,
15322) -> Result<Option<usize>, SidecarError> {
15323    let mut candidates = vec![key.to_owned()];
15324    if allow_bare_named_parameters
15325        && !key.starts_with(':')
15326        && !key.starts_with('@')
15327        && !key.starts_with('$')
15328    {
15329        candidates.push(format!(":{key}"));
15330        candidates.push(format!("@{key}"));
15331        candidates.push(format!("${key}"));
15332    }
15333    for candidate in candidates {
15334        if let Some(index) = statement
15335            .parameter_index(&candidate)
15336            .map_err(sqlite_error)?
15337        {
15338            return Ok(Some(index));
15339        }
15340    }
15341    Ok(None)
15342}
15343
15344fn decode_sqlite_parameter(value: &Value) -> Result<rusqlite::types::Value, SidecarError> {
15345    Ok(match value {
15346        Value::Null => rusqlite::types::Value::Null,
15347        Value::Bool(value) => rusqlite::types::Value::Integer(i64::from(*value)),
15348        Value::Number(value) => match (value.as_i64(), value.as_f64()) {
15349            (Some(integer), _) => rusqlite::types::Value::Integer(integer),
15350            (_, Some(real)) => rusqlite::types::Value::Real(real),
15351            _ => {
15352                return Err(SidecarError::InvalidState(String::from(
15353                    "sqlite parameter number is not representable",
15354                )));
15355            }
15356        },
15357        Value::String(value) => rusqlite::types::Value::Text(value.clone()),
15358        Value::Array(_) => {
15359            return Err(SidecarError::InvalidState(String::from(
15360                "sqlite parameters do not support nested arrays",
15361            )));
15362        }
15363        Value::Object(map) => match map.get("__agentosSqliteType").and_then(Value::as_str) {
15364            Some("bigint") => rusqlite::types::Value::Integer(
15365                map.get("value")
15366                    .and_then(Value::as_str)
15367                    .ok_or_else(|| {
15368                        SidecarError::InvalidState(String::from(
15369                            "sqlite bigint parameter missing string value",
15370                        ))
15371                    })?
15372                    .parse::<i64>()
15373                    .map_err(|error| {
15374                        SidecarError::InvalidState(format!(
15375                            "sqlite bigint parameter is not a signed 64-bit integer: {error}"
15376                        ))
15377                    })?,
15378            ),
15379            Some("uint8array") => rusqlite::types::Value::Blob(
15380                base64::engine::general_purpose::STANDARD
15381                    .decode(map.get("value").and_then(Value::as_str).ok_or_else(|| {
15382                        SidecarError::InvalidState(String::from(
15383                            "sqlite blob parameter missing base64 value",
15384                        ))
15385                    })?)
15386                    .map_err(|error| {
15387                        SidecarError::InvalidState(format!(
15388                            "sqlite blob parameter contains invalid base64: {error}"
15389                        ))
15390                    })?,
15391            ),
15392            Some(other) => {
15393                return Err(SidecarError::InvalidState(format!(
15394                    "unsupported sqlite tagged parameter type {other}"
15395                )));
15396            }
15397            None => {
15398                return Err(SidecarError::InvalidState(String::from(
15399                    "sqlite named parameter objects must be passed as the top-level params object",
15400                )));
15401            }
15402        },
15403    })
15404}
15405
15406fn close_sqlite_database(
15407    kernel: &mut SidecarKernel,
15408    process: &mut ActiveProcess,
15409    database_id: u64,
15410) -> Result<(), SidecarError> {
15411    let mut database = process
15412        .sqlite_databases
15413        .remove(&database_id)
15414        .ok_or_else(|| {
15415            SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
15416        })?;
15417    process
15418        .sqlite_statements
15419        .retain(|_, statement| statement.database_id != database_id);
15420    sqlite_sync_database(kernel, process.kernel_pid, &mut database)?;
15421    let host_path = database.host_path.clone();
15422    drop(database);
15423    cleanup_sqlite_host_artifacts(host_path.as_deref())?;
15424    Ok(())
15425}
15426
15427fn ensure_per_process_state_handle_capacity(len: usize, label: &str) -> Result<(), SidecarError> {
15428    if len >= MAX_PER_PROCESS_STATE_HANDLES {
15429        return Err(SidecarError::InvalidState(format!(
15430            "{label} handle limit exceeded: limit is {MAX_PER_PROCESS_STATE_HANDLES}"
15431        )));
15432    }
15433    Ok(())
15434}
15435
15436fn sqlite_sync_database(
15437    kernel: &mut SidecarKernel,
15438    kernel_pid: u32,
15439    database: &mut ActiveSqliteDatabase,
15440) -> Result<(), SidecarError> {
15441    if !database.dirty
15442        || database.transaction_depth > 0
15443        || database.read_only
15444        || database.host_path.is_none()
15445        || database.vm_path.is_none()
15446    {
15447        return Ok(());
15448    }
15449
15450    let _ = database
15451        .connection
15452        .execute_batch("PRAGMA wal_checkpoint(TRUNCATE)");
15453    let host_path = database.host_path.as_ref().expect("sqlite host path");
15454    if !host_path.exists() {
15455        return Ok(());
15456    }
15457    ensure_vm_parent_dir(
15458        kernel,
15459        kernel_pid,
15460        database.vm_path.as_deref().expect("sqlite vm path"),
15461    )?;
15462    let contents = fs::read(host_path).map_err(|error| {
15463        SidecarError::Io(format!(
15464            "failed to read sqlite temp database {}: {error}",
15465            host_path.display()
15466        ))
15467    })?;
15468    kernel
15469        .write_file_for_process(
15470            EXECUTION_DRIVER_NAME,
15471            kernel_pid,
15472            database.vm_path.as_deref().expect("sqlite vm path"),
15473            contents,
15474            None,
15475        )
15476        .map_err(kernel_error)?;
15477    database.dirty = false;
15478    Ok(())
15479}
15480
15481fn cleanup_sqlite_host_artifacts(host_path: Option<&Path>) -> Result<(), SidecarError> {
15482    let Some(host_path) = host_path else {
15483        return Ok(());
15484    };
15485    let parent = host_path.parent().map(PathBuf::from);
15486    for suffix in ["", "-wal", "-shm"] {
15487        let path = PathBuf::from(format!("{}{}", host_path.display(), suffix));
15488        if path.exists() {
15489            fs::remove_file(&path).map_err(|error| {
15490                SidecarError::Io(format!(
15491                    "failed to remove sqlite temp artifact {}: {error}",
15492                    path.display()
15493                ))
15494            })?;
15495        }
15496    }
15497    if let Some(parent) = parent {
15498        let _ = fs::remove_dir_all(parent);
15499    }
15500    Ok(())
15501}
15502
15503fn ensure_vm_parent_dir(
15504    kernel: &mut SidecarKernel,
15505    kernel_pid: u32,
15506    path: &str,
15507) -> Result<(), SidecarError> {
15508    let parent = dirname(path);
15509    if parent == "/" || parent == "." {
15510        return Ok(());
15511    }
15512    let mut current = String::new();
15513    for segment in parent.split('/').filter(|segment| !segment.is_empty()) {
15514        current.push('/');
15515        current.push_str(segment);
15516        if !kernel
15517            .exists_for_process(EXECUTION_DRIVER_NAME, kernel_pid, &current)
15518            .map_err(kernel_error)?
15519        {
15520            kernel
15521                .mkdir_for_process(EXECUTION_DRIVER_NAME, kernel_pid, &current, false, None)
15522                .map_err(kernel_error)?;
15523        }
15524    }
15525    Ok(())
15526}
15527
15528fn sqlite_database(
15529    process: &ActiveProcess,
15530    database_id: u64,
15531) -> Result<&ActiveSqliteDatabase, SidecarError> {
15532    process.sqlite_databases.get(&database_id).ok_or_else(|| {
15533        SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
15534    })
15535}
15536
15537fn sqlite_database_mut(
15538    process: &mut ActiveProcess,
15539    database_id: u64,
15540) -> Result<&mut ActiveSqliteDatabase, SidecarError> {
15541    process
15542        .sqlite_databases
15543        .get_mut(&database_id)
15544        .ok_or_else(|| {
15545            SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
15546        })
15547}
15548
15549fn sqlite_statement(
15550    process: &ActiveProcess,
15551    statement_id: u64,
15552) -> Result<&ActiveSqliteStatement, SidecarError> {
15553    process.sqlite_statements.get(&statement_id).ok_or_else(|| {
15554        SidecarError::InvalidState(format!("sqlite statement handle not found: {statement_id}"))
15555    })
15556}
15557
15558fn sqlite_statement_mut(
15559    process: &mut ActiveProcess,
15560    statement_id: u64,
15561) -> Result<&mut ActiveSqliteStatement, SidecarError> {
15562    process
15563        .sqlite_statements
15564        .get_mut(&statement_id)
15565        .ok_or_else(|| {
15566            SidecarError::InvalidState(format!("sqlite statement handle not found: {statement_id}"))
15567        })
15568}
15569
15570fn mark_sqlite_mutation(database: &mut ActiveSqliteDatabase, sql: &str) {
15571    let normalized = sql.trim_start().to_ascii_lowercase();
15572    if normalized.starts_with("begin") || normalized.starts_with("savepoint") {
15573        database.dirty = true;
15574        database.transaction_depth += 1;
15575        return;
15576    }
15577    if normalized.starts_with("commit") || normalized.starts_with("release savepoint") {
15578        database.dirty = true;
15579        database.transaction_depth = database.transaction_depth.saturating_sub(1);
15580        return;
15581    }
15582    if normalized.starts_with("rollback") && !normalized.starts_with("rollback to") {
15583        database.dirty = true;
15584        database.transaction_depth = database.transaction_depth.saturating_sub(1);
15585        return;
15586    }
15587    if normalized.starts_with("insert")
15588        || normalized.starts_with("update")
15589        || normalized.starts_with("delete")
15590        || normalized.starts_with("replace")
15591        || normalized.starts_with("create")
15592        || normalized.starts_with("alter")
15593        || normalized.starts_with("drop")
15594        || normalized.starts_with("vacuum")
15595        || normalized.starts_with("reindex")
15596        || normalized.starts_with("analyze")
15597        || normalized.starts_with("attach")
15598        || normalized.starts_with("detach")
15599        || normalized.starts_with("pragma")
15600    {
15601        database.dirty = true;
15602    }
15603}
15604
15605fn sqlite_option_bool(options: Option<&Value>, key: &str) -> Option<bool> {
15606    options
15607        .and_then(|value| value.get(key))
15608        .and_then(Value::as_bool)
15609}
15610
15611fn sqlite_option_u64(options: Option<&Value>, key: &str) -> Option<u64> {
15612    options
15613        .and_then(|value| value.get(key))
15614        .and_then(Value::as_u64)
15615}
15616
15617fn sqlite_error(error: rusqlite::Error) -> SidecarError {
15618    SidecarError::InvalidState(format!("sqlite error: {error}"))
15619}
15620
15621pub(crate) fn javascript_sync_rpc_arg_str<'a>(
15622    args: &'a [Value],
15623    index: usize,
15624    label: &str,
15625) -> Result<&'a str, SidecarError> {
15626    args.get(index)
15627        .and_then(Value::as_str)
15628        .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a string argument")))
15629}
15630
15631pub(crate) fn javascript_sync_rpc_arg_bool(
15632    args: &[Value],
15633    index: usize,
15634    label: &str,
15635) -> Result<bool, SidecarError> {
15636    args.get(index)
15637        .and_then(Value::as_bool)
15638        .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a boolean argument")))
15639}
15640
15641pub(crate) fn javascript_sync_rpc_encoding(args: &[Value]) -> Option<String> {
15642    args.get(1).and_then(|value| {
15643        value.as_str().map(str::to_owned).or_else(|| {
15644            value
15645                .get("encoding")
15646                .and_then(Value::as_str)
15647                .map(str::to_owned)
15648        })
15649    })
15650}
15651
15652pub(crate) fn javascript_sync_rpc_option_bool(
15653    args: &[Value],
15654    index: usize,
15655    key: &str,
15656) -> Option<bool> {
15657    let value = args.get(index)?;
15658    if key == "recursive" {
15659        if let Some(boolean) = value.as_bool() {
15660            return Some(boolean);
15661        }
15662    }
15663    value.get(key).and_then(Value::as_bool)
15664}
15665
15666pub(crate) fn javascript_sync_rpc_option_u32(
15667    args: &[Value],
15668    index: usize,
15669    key: &str,
15670) -> Result<Option<u32>, SidecarError> {
15671    let Some(value) = args.get(index).and_then(|value| {
15672        if value.is_object() {
15673            value.get(key)
15674        } else if key == "mode" && value.is_number() {
15675            Some(value)
15676        } else {
15677            None
15678        }
15679    }) else {
15680        return Ok(None);
15681    };
15682    if value.is_null() {
15683        return Ok(None);
15684    }
15685
15686    let numeric = value
15687        .as_u64()
15688        .or_else(|| {
15689            value
15690                .as_f64()
15691                .filter(|number| number.is_finite() && *number >= 0.0)
15692                .map(|number| number as u64)
15693        })
15694        .ok_or_else(|| SidecarError::InvalidState(format!("{key} must be numeric")))?;
15695
15696    u32::try_from(numeric)
15697        .map(Some)
15698        .map_err(|_| SidecarError::InvalidState(format!("{key} must fit within u32")))
15699}
15700
15701pub(crate) fn javascript_sync_rpc_arg_u32(
15702    args: &[Value],
15703    index: usize,
15704    label: &str,
15705) -> Result<u32, SidecarError> {
15706    let value = javascript_sync_rpc_arg_u64(args, index, label)?;
15707    u32::try_from(value)
15708        .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")))
15709}
15710
15711pub(crate) fn javascript_sync_rpc_arg_i32(
15712    args: &[Value],
15713    index: usize,
15714    label: &str,
15715) -> Result<i32, SidecarError> {
15716    let Some(value) = args.get(index) else {
15717        return Err(SidecarError::InvalidState(format!("{label} is required")));
15718    };
15719
15720    let numeric = value
15721        .as_i64()
15722        .or_else(|| {
15723            value
15724                .as_f64()
15725                .filter(|number| number.is_finite())
15726                .map(|number| number as i64)
15727        })
15728        .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a numeric argument")))?;
15729
15730    i32::try_from(numeric)
15731        .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within i32")))
15732}
15733
15734pub(crate) fn javascript_sync_rpc_arg_u32_optional(
15735    args: &[Value],
15736    index: usize,
15737    label: &str,
15738) -> Result<Option<u32>, SidecarError> {
15739    javascript_sync_rpc_arg_u64_optional(args, index, label)?
15740        .map(|value| {
15741            u32::try_from(value)
15742                .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")))
15743        })
15744        .transpose()
15745}
15746
15747pub(crate) fn javascript_sync_rpc_arg_u64(
15748    args: &[Value],
15749    index: usize,
15750    label: &str,
15751) -> Result<u64, SidecarError> {
15752    let Some(value) = args.get(index) else {
15753        return Err(SidecarError::InvalidState(format!("{label} is required")));
15754    };
15755
15756    value
15757        .as_u64()
15758        .or_else(|| {
15759            value
15760                .as_f64()
15761                .filter(|number| number.is_finite() && *number >= 0.0)
15762                .map(|number| number as u64)
15763        })
15764        .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a numeric argument")))
15765}
15766
15767pub(crate) fn javascript_sync_rpc_arg_u64_optional(
15768    args: &[Value],
15769    index: usize,
15770    label: &str,
15771) -> Result<Option<u64>, SidecarError> {
15772    let Some(value) = args.get(index) else {
15773        return Ok(None);
15774    };
15775    if value.is_null() {
15776        return Ok(None);
15777    }
15778    javascript_sync_rpc_arg_u64(args, index, label).map(Some)
15779}
15780
15781pub(crate) fn javascript_sync_rpc_bytes_arg(
15782    args: &[Value],
15783    index: usize,
15784    label: &str,
15785) -> Result<Vec<u8>, SidecarError> {
15786    let Some(value) = args.get(index) else {
15787        return Err(SidecarError::InvalidState(format!("{label} is required")));
15788    };
15789
15790    if let Some(text) = value.as_str() {
15791        return Ok(text.as_bytes().to_vec());
15792    }
15793
15794    decode_encoded_bytes_value(value)
15795        .map_err(|error| SidecarError::InvalidState(format!("{label} {error}")))
15796}
15797
15798pub(crate) fn javascript_sync_rpc_bytes_value(bytes: &[u8]) -> Value {
15799    encoded_bytes_value(bytes)
15800}
15801
15802#[derive(Debug, Deserialize)]
15803pub(crate) struct KernelPollFdRequest {
15804    fd: u32,
15805    events: u16,
15806}
15807
15808#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
15809struct KernelPollFdResponse {
15810    fd: u32,
15811    events: u16,
15812    revents: u16,
15813}
15814
15815fn javascript_sync_rpc_base64_arg(
15816    args: &[Value],
15817    index: usize,
15818    label: &str,
15819) -> Result<Vec<u8>, SidecarError> {
15820    let value = javascript_sync_rpc_arg_str(args, index, label)?;
15821    decode_base64(value).map_err(|error| SidecarError::InvalidState(format!("{label} {error}")))
15822}
15823
15824// ── Sync-RPC round-trip counting (opt-in via AGENTOS_SYNC_RPC_TRACE=1) ──
15825// Each guest fs/module/net sync RPC funnels through service_javascript_sync_rpc,
15826// so this is the one place to measure the kernel-VFS "syscall storm" that makes
15827// metadata-heavy phases (resourceLoader.reload, createAgentSession) 40-90x slower
15828// in the VM than on bare node. Emits a perf log line every 200 calls with the
15829// running per-method breakdown.
15830static SYNC_RPC_STATS: std::sync::OnceLock<
15831    std::sync::Mutex<std::collections::BTreeMap<String, u64>>,
15832> = std::sync::OnceLock::new();
15833
15834#[derive(Default)]
15835struct ExecutePhaseStats {
15836    calls: u64,
15837    total_ns: u128,
15838    max_ns: u128,
15839}
15840
15841static EXECUTE_PHASES: OnceLock<Mutex<BTreeMap<String, ExecutePhaseStats>>> = OnceLock::new();
15842static EXECUTE_LIFETIMES: OnceLock<Mutex<BTreeMap<String, Instant>>> = OnceLock::new();
15843static EXECUTE_EXIT_EVENT_QUEUED: OnceLock<Mutex<BTreeMap<String, Instant>>> = OnceLock::new();
15844
15845fn execute_phases_enabled() -> bool {
15846    std::env::var("AGENTOS_EXECUTE_PHASES").as_deref() == Ok("1")
15847}
15848
15849fn execute_phase_key(vm_id: &str, process_id: &str) -> String {
15850    format!("{vm_id}/{process_id}")
15851}
15852
15853pub(crate) fn record_execute_phase(stage: &str, elapsed: Duration) {
15854    if !execute_phases_enabled() {
15855        return;
15856    }
15857    let phases = EXECUTE_PHASES.get_or_init(|| Mutex::new(BTreeMap::new()));
15858    let Ok(mut phases) = phases.lock() else {
15859        return;
15860    };
15861    let stats = phases.entry(stage.to_string()).or_default();
15862    stats.calls += 1;
15863    let elapsed_ns = elapsed.as_nanos();
15864    stats.total_ns += elapsed_ns;
15865    stats.max_ns = stats.max_ns.max(elapsed_ns);
15866
15867    let Some(path) = std::env::var_os("AGENTOS_EXECUTE_PHASES_FILE") else {
15868        return;
15869    };
15870    let mut output = String::new();
15871    for (stage, stats) in phases.iter() {
15872        let total_us = stats.total_ns / 1_000;
15873        let avg_us = if stats.calls == 0 {
15874            0
15875        } else {
15876            total_us / u128::from(stats.calls)
15877        };
15878        let max_us = stats.max_ns / 1_000;
15879        output.push_str(&format!(
15880            "stage={stage} calls={} total_us={total_us} avg_us={avg_us} max_us={max_us}\n",
15881            stats.calls
15882        ));
15883    }
15884    let _ = fs::write(path, output);
15885}
15886
15887fn mark_execute_response_ready(vm_id: &str, process_id: &str) {
15888    if !execute_phases_enabled() {
15889        return;
15890    }
15891    let lifetimes = EXECUTE_LIFETIMES.get_or_init(|| Mutex::new(BTreeMap::new()));
15892    if let Ok(mut lifetimes) = lifetimes.lock() {
15893        lifetimes.insert(execute_phase_key(vm_id, process_id), Instant::now());
15894    }
15895}
15896
15897pub(crate) fn mark_execute_exit_event_queued(vm_id: &str, process_id: &str) {
15898    if !execute_phases_enabled() {
15899        return;
15900    }
15901    let queued = EXECUTE_EXIT_EVENT_QUEUED.get_or_init(|| Mutex::new(BTreeMap::new()));
15902    if let Ok(mut queued) = queued.lock() {
15903        let key = execute_phase_key(vm_id, process_id);
15904        if let std::collections::btree_map::Entry::Vacant(entry) = queued.entry(key) {
15905            record_execute_response_to_exit_milestone(
15906                "execute_response_to_exit_event_queued",
15907                vm_id,
15908                process_id,
15909            );
15910            entry.insert(Instant::now());
15911        }
15912    }
15913}
15914
15915pub(crate) fn record_execute_exit_event_queue_wait(stage: &str, vm_id: &str, process_id: &str) {
15916    if !execute_phases_enabled() {
15917        return;
15918    }
15919    let Some(queued) = EXECUTE_EXIT_EVENT_QUEUED.get() else {
15920        return;
15921    };
15922    let Ok(mut queued) = queued.lock() else {
15923        return;
15924    };
15925    if let Some(started) = queued.remove(&execute_phase_key(vm_id, process_id)) {
15926        record_execute_phase(stage, started.elapsed());
15927    }
15928}
15929
15930pub(crate) fn record_execute_response_to_exit_milestone(
15931    stage: &str,
15932    vm_id: &str,
15933    process_id: &str,
15934) {
15935    if !execute_phases_enabled() {
15936        return;
15937    }
15938    let Some(lifetimes) = EXECUTE_LIFETIMES.get() else {
15939        return;
15940    };
15941    let Ok(lifetimes) = lifetimes.lock() else {
15942        return;
15943    };
15944    if let Some(started) = lifetimes.get(&execute_phase_key(vm_id, process_id)) {
15945        record_execute_phase(stage, started.elapsed());
15946    }
15947}
15948
15949fn record_execute_response_to_exit(vm_id: &str, process_id: &str) {
15950    if !execute_phases_enabled() {
15951        return;
15952    }
15953    let Some(lifetimes) = EXECUTE_LIFETIMES.get() else {
15954        return;
15955    };
15956    let Ok(mut lifetimes) = lifetimes.lock() else {
15957        return;
15958    };
15959    if let Some(started) = lifetimes.remove(&execute_phase_key(vm_id, process_id)) {
15960        record_execute_phase("execute_response_to_exit_event", started.elapsed());
15961    }
15962}
15963
15964fn sync_rpc_trace_enabled() -> bool {
15965    std::env::var("AGENTOS_SYNC_RPC_TRACE").as_deref() == Ok("1")
15966}
15967
15968fn record_sync_rpc(method: &str) {
15969    let stats =
15970        SYNC_RPC_STATS.get_or_init(|| std::sync::Mutex::new(std::collections::BTreeMap::new()));
15971    let Ok(mut map) = stats.lock() else {
15972        return;
15973    };
15974    *map.entry(method.to_string()).or_insert(0) += 1;
15975    let total: u64 = map.values().sum();
15976    if total == 1 || total.is_multiple_of(50) {
15977        let mut top: Vec<(&String, &u64)> = map.iter().collect();
15978        top.sort_by(|a, b| b.1.cmp(a.1));
15979        let breakdown = top
15980            .iter()
15981            .take(8)
15982            .map(|(m, c)| format!("{m}={c}"))
15983            .collect::<Vec<_>>()
15984            .join(" ");
15985        tracing::info!(target: "secure_exec_sidecar::perf", total, %breakdown, "sync_rpc count");
15986    }
15987}
15988
15989fn net_tcp_trace_enabled(env: &BTreeMap<String, String>) -> bool {
15990    env.get("AGENTOS_NET_BRIDGE_TRACE").map(String::as_str) == Some("1")
15991}
15992
15993fn duration_micros_u64(duration: Duration) -> u64 {
15994    u64::try_from(duration.as_micros()).unwrap_or(u64::MAX)
15995}
15996
15997fn net_tcp_trace_reset() {
15998    reset_socket_read_trace();
15999    set_socket_read_trace_enabled(true);
16000    for counter in [
16001        &NET_TCP_TRACE_COUNTERS.socket_read_calls,
16002        &NET_TCP_TRACE_COUNTERS.socket_read_zero_wait_calls,
16003        &NET_TCP_TRACE_COUNTERS.socket_read_data_events,
16004        &NET_TCP_TRACE_COUNTERS.socket_read_bytes,
16005        &NET_TCP_TRACE_COUNTERS.socket_read_kernel_us,
16006        &NET_TCP_TRACE_COUNTERS.socket_read_end_events,
16007        &NET_TCP_TRACE_COUNTERS.socket_read_eagain,
16008        &NET_TCP_TRACE_COUNTERS.socket_read_errors,
16009        &NET_TCP_TRACE_COUNTERS.socket_read_push_attempts,
16010        &NET_TCP_TRACE_COUNTERS.socket_read_push_sent,
16011        &NET_TCP_TRACE_COUNTERS.socket_read_push_missing,
16012        &NET_TCP_TRACE_COUNTERS.socket_read_push_errors,
16013        &NET_TCP_TRACE_COUNTERS.socket_write_calls,
16014        &NET_TCP_TRACE_COUNTERS.socket_write_bytes,
16015        &NET_TCP_TRACE_COUNTERS.socket_write_kernel_us,
16016        &NET_TCP_TRACE_COUNTERS.socket_write_errors,
16017        &NET_TCP_TRACE_COUNTERS.server_accept_calls,
16018        &NET_TCP_TRACE_COUNTERS.server_accept_zero_wait_calls,
16019        &NET_TCP_TRACE_COUNTERS.server_accept_connections,
16020        &NET_TCP_TRACE_COUNTERS.server_accept_eagain,
16021        &NET_TCP_TRACE_COUNTERS.server_accept_errors,
16022        &NET_TCP_TRACE_COUNTERS.kernel_poll_targets,
16023        &NET_TCP_TRACE_COUNTERS.kernel_poll_zero_wait_calls,
16024        &NET_TCP_TRACE_COUNTERS.kernel_poll_wait_us,
16025        &NET_TCP_TRACE_COUNTERS.kernel_poll_elapsed_us,
16026        &NET_TCP_TRACE_COUNTERS.kernel_poll_empty,
16027        &NET_TCP_TRACE_COUNTERS.kernel_poll_ready,
16028        &NET_TCP_TRACE_COUNTERS.kernel_poll_revents_read,
16029        &NET_TCP_TRACE_COUNTERS.kernel_poll_revents_hup,
16030        &NET_TCP_TRACE_COUNTERS.kernel_poll_revents_err,
16031        &NET_TCP_TRACE_COUNTERS.kernel_poll_revents_bits_or,
16032    ] {
16033        counter.store(0, Ordering::Relaxed);
16034    }
16035}
16036
16037fn net_tcp_trace_snapshot() -> Value {
16038    let load = |counter: &AtomicU64| counter.load(Ordering::Relaxed);
16039    let socket_read = socket_read_trace_snapshot();
16040    json!({
16041        "socketReadCalls": load(&NET_TCP_TRACE_COUNTERS.socket_read_calls),
16042        "socketReadZeroWaitCalls": load(&NET_TCP_TRACE_COUNTERS.socket_read_zero_wait_calls),
16043        "socketReadDataEvents": load(&NET_TCP_TRACE_COUNTERS.socket_read_data_events),
16044        "socketReadBytes": load(&NET_TCP_TRACE_COUNTERS.socket_read_bytes),
16045        "socketReadKernelUs": load(&NET_TCP_TRACE_COUNTERS.socket_read_kernel_us),
16046        "socketReadRecordCloneCalls": socket_read.socket_record_clone_calls,
16047        "socketReadRecordCloneUs": socket_read.socket_record_clone_us,
16048        "socketReadRecvCalls": socket_read.read_recv_calls,
16049        "socketReadRecvBytes": socket_read.read_recv_bytes,
16050        "socketReadRecvChunks": socket_read.read_recv_chunks,
16051        "socketReadRecvCopyUs": socket_read.read_recv_copy_us,
16052        "socketReadEndEvents": load(&NET_TCP_TRACE_COUNTERS.socket_read_end_events),
16053        "socketReadEagain": load(&NET_TCP_TRACE_COUNTERS.socket_read_eagain),
16054        "socketReadErrors": load(&NET_TCP_TRACE_COUNTERS.socket_read_errors),
16055        "socketReadPushAttempts": load(&NET_TCP_TRACE_COUNTERS.socket_read_push_attempts),
16056        "socketReadPushSent": load(&NET_TCP_TRACE_COUNTERS.socket_read_push_sent),
16057        "socketReadPushMissing": load(&NET_TCP_TRACE_COUNTERS.socket_read_push_missing),
16058        "socketReadPushErrors": load(&NET_TCP_TRACE_COUNTERS.socket_read_push_errors),
16059        "socketWriteCalls": load(&NET_TCP_TRACE_COUNTERS.socket_write_calls),
16060        "socketWriteBytes": load(&NET_TCP_TRACE_COUNTERS.socket_write_bytes),
16061        "socketWriteKernelUs": load(&NET_TCP_TRACE_COUNTERS.socket_write_kernel_us),
16062        "socketWriteErrors": load(&NET_TCP_TRACE_COUNTERS.socket_write_errors),
16063        "serverAcceptCalls": load(&NET_TCP_TRACE_COUNTERS.server_accept_calls),
16064        "serverAcceptZeroWaitCalls": load(&NET_TCP_TRACE_COUNTERS.server_accept_zero_wait_calls),
16065        "serverAcceptConnections": load(&NET_TCP_TRACE_COUNTERS.server_accept_connections),
16066        "serverAcceptEagain": load(&NET_TCP_TRACE_COUNTERS.server_accept_eagain),
16067        "serverAcceptErrors": load(&NET_TCP_TRACE_COUNTERS.server_accept_errors),
16068        "kernelPollTargets": load(&NET_TCP_TRACE_COUNTERS.kernel_poll_targets),
16069        "kernelPollZeroWaitCalls": load(&NET_TCP_TRACE_COUNTERS.kernel_poll_zero_wait_calls),
16070        "kernelPollWaitUs": load(&NET_TCP_TRACE_COUNTERS.kernel_poll_wait_us),
16071        "kernelPollElapsedUs": load(&NET_TCP_TRACE_COUNTERS.kernel_poll_elapsed_us),
16072        "kernelPollEmpty": load(&NET_TCP_TRACE_COUNTERS.kernel_poll_empty),
16073        "kernelPollReady": load(&NET_TCP_TRACE_COUNTERS.kernel_poll_ready),
16074        "kernelPollReventsRead": load(&NET_TCP_TRACE_COUNTERS.kernel_poll_revents_read),
16075        "kernelPollReventsHup": load(&NET_TCP_TRACE_COUNTERS.kernel_poll_revents_hup),
16076        "kernelPollReventsErr": load(&NET_TCP_TRACE_COUNTERS.kernel_poll_revents_err),
16077        "kernelPollReventsBitsOr": load(&NET_TCP_TRACE_COUNTERS.kernel_poll_revents_bits_or),
16078    })
16079}
16080
16081fn record_net_tcp_kernel_poll(
16082    enabled: bool,
16083    wait: Duration,
16084    elapsed: Duration,
16085    revents: PollEvents,
16086) {
16087    if !enabled {
16088        return;
16089    }
16090    NET_TCP_TRACE_COUNTERS
16091        .kernel_poll_targets
16092        .fetch_add(1, Ordering::Relaxed);
16093    if wait.is_zero() {
16094        NET_TCP_TRACE_COUNTERS
16095            .kernel_poll_zero_wait_calls
16096            .fetch_add(1, Ordering::Relaxed);
16097    }
16098    NET_TCP_TRACE_COUNTERS
16099        .kernel_poll_wait_us
16100        .fetch_add(duration_micros_u64(wait), Ordering::Relaxed);
16101    NET_TCP_TRACE_COUNTERS
16102        .kernel_poll_elapsed_us
16103        .fetch_add(duration_micros_u64(elapsed), Ordering::Relaxed);
16104    if revents.is_empty() {
16105        NET_TCP_TRACE_COUNTERS
16106            .kernel_poll_empty
16107            .fetch_add(1, Ordering::Relaxed);
16108    } else {
16109        NET_TCP_TRACE_COUNTERS
16110            .kernel_poll_ready
16111            .fetch_add(1, Ordering::Relaxed);
16112        NET_TCP_TRACE_COUNTERS
16113            .kernel_poll_revents_bits_or
16114            .fetch_or(u64::from(revents.bits()), Ordering::Relaxed);
16115    }
16116    if revents.intersects(POLLIN) {
16117        NET_TCP_TRACE_COUNTERS
16118            .kernel_poll_revents_read
16119            .fetch_add(1, Ordering::Relaxed);
16120    }
16121    if revents.intersects(POLLHUP) {
16122        NET_TCP_TRACE_COUNTERS
16123            .kernel_poll_revents_hup
16124            .fetch_add(1, Ordering::Relaxed);
16125    }
16126    if revents.intersects(POLLERR) {
16127        NET_TCP_TRACE_COUNTERS
16128            .kernel_poll_revents_err
16129            .fetch_add(1, Ordering::Relaxed);
16130    }
16131}
16132
16133pub(crate) fn service_javascript_sync_rpc<B>(
16134    request: JavascriptSyncRpcServiceRequest<'_, B>,
16135) -> Result<JavascriptSyncRpcServiceResponse, SidecarError>
16136where
16137    B: NativeSidecarBridge + Send + 'static,
16138    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16139{
16140    if sync_rpc_trace_enabled() {
16141        record_sync_rpc(request.sync_request.method.as_str());
16142    }
16143    let JavascriptSyncRpcServiceRequest {
16144        bridge,
16145        vm_id,
16146        dns,
16147        socket_paths,
16148        kernel,
16149        kernel_readiness,
16150        process,
16151        sync_request: request,
16152        resource_limits,
16153        network_counts,
16154    } = request;
16155    if request.raw_bytes_args.contains_key(&usize::MAX) && request.method == "fs.readSync" {
16156        let kernel_pid = process.kernel_pid;
16157        let bytes = service_javascript_fs_read_sync_rpc(kernel, process, kernel_pid, request)?;
16158        return Ok(JavascriptSyncRpcServiceResponse::Raw(bytes));
16159    }
16160    if request.method == "fs.readdirSync" {
16161        let kernel_pid = process.kernel_pid;
16162        let bytes =
16163            service_javascript_fs_readdir_raw_sync_rpc(kernel, process, kernel_pid, request)?;
16164        return Ok(JavascriptSyncRpcServiceResponse::Raw(bytes));
16165    }
16166    let response = match request.method.as_str() {
16167        "__bench.noop" => Ok(Value::Null),
16168        "__bench.net_tcp_metrics_reset" => {
16169            net_tcp_trace_reset();
16170            Ok(Value::Null)
16171        }
16172        "__bench.net_tcp_metrics_snapshot" => Ok(net_tcp_trace_snapshot()),
16173        // Module resolution / loading / format detection read the kernel VFS so
16174        // the resolver sees exactly what the guest and `kernel.readFile()` see.
16175        "_resolveModule"
16176        | "_resolveModuleSync"
16177        | "__resolve_module"
16178        | "_batchResolveModules"
16179        | "__batch_resolve_modules"
16180        | "_loadFile"
16181        | "_loadFileSync"
16182        | "__load_file"
16183        | "_moduleFormat"
16184        | "__module_format" => service_javascript_module_sync_rpc(kernel, process, request),
16185        // Polyfills are static guest expressions, not VFS reads.
16186        "_loadPolyfill" | "__load_polyfill" => {
16187            service_javascript_internal_bridge_sync_rpc(process, request)
16188        }
16189        "__kernel_stdin_read" => {
16190            // A TTY (PTY-backed) JavaScript process must read its stdin from the
16191            // kernel PTY slave (fd 0) so cooked-mode line discipline (echo,
16192            // VERASE/VKILL/VWERASE, ICRNL, VEOF) applies exactly as it does for
16193            // wasm/python. Non-TTY JS keeps using the in-process local stdin
16194            // bridge (piped stdin fed via process.execution.write_stdin).
16195            let js_local_bridge = matches!(process.execution, ActiveExecution::Javascript(_))
16196                && process.tty_master_fd.is_none();
16197            if js_local_bridge {
16198                match &process.execution {
16199                    ActiveExecution::Javascript(execution) => execution
16200                        .read_kernel_stdin_sync_rpc(request)
16201                        .map_err(|error| SidecarError::Execution(error.to_string())),
16202                    _ => unreachable!("js_local_bridge implies a JavaScript execution"),
16203                }
16204            } else {
16205                service_javascript_kernel_stdin_sync_rpc(kernel, process, request)
16206            }
16207        }
16208        "__kernel_stdio_write" => {
16209            service_javascript_kernel_stdio_write_sync_rpc(kernel, process, request)
16210        }
16211        "__kernel_isatty" => service_javascript_kernel_isatty_sync_rpc(kernel, process, request),
16212        "__kernel_tty_size" => {
16213            service_javascript_kernel_tty_size_sync_rpc(kernel, process, request)
16214        }
16215        "__kernel_poll" => service_javascript_kernel_poll_sync_rpc(kernel, process, request),
16216        "__pty_set_raw_mode" => {
16217            service_javascript_pty_set_raw_mode_sync_rpc(kernel, process, request)
16218        }
16219        "crypto.hashDigest"
16220        | "crypto.hmacDigest"
16221        | "crypto.pbkdf2"
16222        | "crypto.scrypt"
16223        | "crypto.cipheriv"
16224        | "crypto.decipheriv"
16225        | "crypto.cipherivCreate"
16226        | "crypto.cipherivUpdate"
16227        | "crypto.cipherivFinal"
16228        | "crypto.sign"
16229        | "crypto.verify"
16230        | "crypto.asymmetricOp"
16231        | "crypto.createKeyObject"
16232        | "crypto.generateKeyPairSync"
16233        | "crypto.generateKeySync"
16234        | "crypto.generatePrimeSync"
16235        | "crypto.diffieHellman"
16236        | "crypto.diffieHellmanGroup"
16237        | "crypto.diffieHellmanSessionCreate"
16238        | "crypto.diffieHellmanSessionCall"
16239        | "crypto.diffieHellmanSessionDestroy"
16240        | "crypto.subtle" => service_javascript_crypto_sync_rpc(process, request),
16241        "dns.lookup" | "dns.resolve" | "dns.resolve4" | "dns.resolve6" => {
16242            service_javascript_dns_sync_rpc(bridge, kernel, vm_id, dns, request)
16243        }
16244        "net.http_listen" | "net.http_close" | "net.http_wait" | "net.http_respond" => {
16245            return service_javascript_net_sync_rpc_response(JavascriptNetSyncRpcServiceRequest {
16246                bridge,
16247                vm_id,
16248                dns,
16249                socket_paths,
16250                kernel,
16251                kernel_readiness: Arc::clone(&kernel_readiness),
16252                process,
16253                sync_request: request,
16254                resource_limits,
16255                network_counts,
16256            })
16257        }
16258        "net.http2_server_listen"
16259        | "net.http2_server_poll"
16260        | "net.http2_server_close"
16261        | "net.http2_server_respond"
16262        | "net.http2_server_wait"
16263        | "net.http2_session_connect"
16264        | "net.http2_session_request"
16265        | "net.http2_session_settings"
16266        | "net.http2_session_set_local_window_size"
16267        | "net.http2_session_goaway"
16268        | "net.http2_session_close"
16269        | "net.http2_session_destroy"
16270        | "net.http2_session_poll"
16271        | "net.http2_session_wait"
16272        | "net.http2_stream_respond"
16273        | "net.http2_stream_push_stream"
16274        | "net.http2_stream_write"
16275        | "net.http2_stream_end"
16276        | "net.http2_stream_close"
16277        | "net.http2_stream_pause"
16278        | "net.http2_stream_resume"
16279        | "net.http2_stream_respond_with_file" => {
16280            service_javascript_http2_sync_rpc(JavascriptHttp2SyncRpcServiceRequest {
16281                bridge,
16282                kernel,
16283                vm_id,
16284                dns,
16285                socket_paths,
16286                process,
16287                sync_request: request,
16288                resource_limits,
16289                network_counts,
16290            })
16291        }
16292        "net.connect"
16293        | "net.reserve_tcp_port"
16294        | "net.release_tcp_port"
16295        | "net.listen"
16296        | "net.poll"
16297        | "net.socket_wait_connect"
16298        | "net.socket_read"
16299        | "net.socket_set_no_delay"
16300        | "net.socket_set_keep_alive"
16301        | "net.socket_upgrade_tls"
16302        | "net.socket_get_tls_client_hello"
16303        | "net.socket_tls_query"
16304        | "net.server_poll"
16305        | "net.server_accept"
16306        | "net.server_connections"
16307        | "net.upgrade_socket_write"
16308        | "net.upgrade_socket_end"
16309        | "net.upgrade_socket_destroy"
16310        | "net.write"
16311        | "net.shutdown"
16312        | "net.destroy"
16313        | "net.server_close"
16314        | "tls.get_ciphers" => {
16315            return service_javascript_net_sync_rpc_response(JavascriptNetSyncRpcServiceRequest {
16316                bridge,
16317                vm_id,
16318                dns,
16319                socket_paths,
16320                kernel,
16321                kernel_readiness: Arc::clone(&kernel_readiness),
16322                process,
16323                sync_request: request,
16324                resource_limits,
16325                network_counts,
16326            })
16327        }
16328        "dgram.createSocket"
16329        | "dgram.bind"
16330        | "dgram.send"
16331        | "dgram.poll"
16332        | "dgram.close"
16333        | "dgram.address"
16334        | "dgram.setBufferSize"
16335        | "dgram.getBufferSize" => {
16336            service_javascript_dgram_sync_rpc(JavascriptDgramSyncRpcServiceRequest {
16337                bridge,
16338                kernel,
16339                vm_id,
16340                dns,
16341                socket_paths,
16342                process,
16343                kernel_readiness,
16344                sync_request: request,
16345                resource_limits,
16346                network_counts,
16347            })
16348        }
16349        "sqlite.constants"
16350        | "sqlite.open"
16351        | "sqlite.close"
16352        | "sqlite.exec"
16353        | "sqlite.query"
16354        | "sqlite.prepare"
16355        | "sqlite.location"
16356        | "sqlite.checkpoint"
16357        | "sqlite.statement.run"
16358        | "sqlite.statement.get"
16359        | "sqlite.statement.all"
16360        | "sqlite.statement.iterate"
16361        | "sqlite.statement.columns"
16362        | "sqlite.statement.setReturnArrays"
16363        | "sqlite.statement.setReadBigInts"
16364        | "sqlite.statement.setAllowBareNamedParameters"
16365        | "sqlite.statement.setAllowUnknownNamedParameters"
16366        | "sqlite.statement.finalize" => {
16367            service_javascript_sqlite_sync_rpc(kernel, process, request)
16368        }
16369        "process.kill" => {
16370            let target_pid =
16371                javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?;
16372            let signal = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?;
16373            let parsed_signal = parse_signal(signal)?;
16374            if parsed_signal == 0 {
16375                kernel
16376                    .signal_process(EXECUTION_DRIVER_NAME, target_pid, parsed_signal)
16377                    .map_err(kernel_error)?;
16378                return Ok(Value::Null.into());
16379            }
16380            let process_pid = i32::try_from(process.kernel_pid)
16381                .map_err(|_| SidecarError::InvalidState("process pid exceeds i32".into()))?;
16382            if target_pid != process_pid {
16383                return Err(SidecarError::InvalidState(format!(
16384                    "unknown process pid {target_pid}"
16385                )));
16386            }
16387            process.pending_self_signal_exit = None;
16388            if parsed_signal != 0
16389                && !matches!(
16390                    canonical_signal_name(parsed_signal),
16391                    Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
16392                )
16393            {
16394                process.pending_self_signal_exit = Some(parsed_signal);
16395            }
16396            Ok(json!({
16397                "self": true,
16398                "action": "default",
16399            }))
16400        }
16401        "process.umask" => {
16402            let new_mask = javascript_sync_rpc_arg_u32_optional(&request.args, 0, "process umask")?;
16403            kernel
16404                .umask(EXECUTION_DRIVER_NAME, process.kernel_pid, new_mask)
16405                .map(|mask| json!(mask))
16406                .map_err(kernel_error)
16407        }
16408        "fs.chmodSync" | "fs.promises.chmod" => {
16409            let response =
16410                service_javascript_fs_sync_rpc(kernel, process, process.kernel_pid, request)?;
16411            mirror_process_chmod_to_host(process, request)?;
16412            Ok(response)
16413        }
16414        _ => service_javascript_fs_sync_rpc(kernel, process, process.kernel_pid, request),
16415    }?;
16416    Ok(response.into())
16417}
16418
16419fn service_javascript_internal_bridge_sync_rpc(
16420    process: &ActiveProcess,
16421    request: &JavascriptSyncRpcRequest,
16422) -> Result<Value, SidecarError> {
16423    // Module resolution / loading / format now reads the kernel VFS via
16424    // `service_javascript_module_sync_rpc`. This host-context path only handles
16425    // polyfills, which are static guest expressions independent of the FS.
16426    let method = match request.method.as_str() {
16427        "_loadPolyfill" | "__load_polyfill" => "_loadPolyfill",
16428        other => {
16429            return Err(SidecarError::InvalidState(format!(
16430                "unsupported JavaScript internal bridge method {other}"
16431            )));
16432        }
16433    };
16434
16435    handle_internal_bridge_call_from_host_context(
16436        &process.host_cwd,
16437        &process.guest_cwd,
16438        &process.env,
16439        method,
16440        &request.args,
16441    )
16442    .ok_or_else(|| {
16443        SidecarError::InvalidState(format!(
16444            "JavaScript internal bridge method {method} returned no value"
16445        ))
16446    })
16447}
16448
16449fn mirror_process_chmod_to_host(
16450    process: &ActiveProcess,
16451    request: &JavascriptSyncRpcRequest,
16452) -> Result<(), SidecarError> {
16453    let guest_path = javascript_sync_rpc_arg_str(&request.args, 0, "filesystem chmod path")?;
16454    let mode = javascript_sync_rpc_arg_u32(&request.args, 1, "filesystem chmod mode")? & 0o7777;
16455    let Some(host_path) = resolve_process_guest_path_to_host(process, guest_path) else {
16456        return Ok(());
16457    };
16458    if !host_path.exists() {
16459        return Ok(());
16460    }
16461    fs::set_permissions(&host_path, fs::Permissions::from_mode(mode)).map_err(|error| {
16462        SidecarError::Io(format!(
16463            "failed to mirror chmod to host path {}: {error}",
16464            host_path.display()
16465        ))
16466    })
16467}
16468
16469fn resolve_process_guest_path_to_host(
16470    process: &ActiveProcess,
16471    guest_path: &str,
16472) -> Option<PathBuf> {
16473    let normalized_guest_path = if guest_path.starts_with('/') {
16474        normalize_path(guest_path)
16475    } else {
16476        normalize_path(&format!(
16477            "{}/{}",
16478            process.guest_cwd.trim_end_matches('/'),
16479            guest_path
16480        ))
16481    };
16482    if let Some(host_path) =
16483        host_path_from_runtime_guest_mappings(&process.env, &normalized_guest_path)
16484    {
16485        return Some(host_path);
16486    }
16487    let normalized_guest_cwd = normalize_path(&process.guest_cwd);
16488    let mut host_root = normalize_host_path(&process.host_cwd);
16489    for _ in normalized_guest_cwd
16490        .trim_start_matches('/')
16491        .split('/')
16492        .filter(|segment| !segment.is_empty())
16493    {
16494        host_root = host_root.parent()?.to_path_buf();
16495    }
16496    if normalized_guest_path == "/" {
16497        Some(host_root)
16498    } else {
16499        Some(host_root.join(normalized_guest_path.trim_start_matches('/')))
16500    }
16501}
16502
16503pub(crate) fn service_javascript_crypto_sync_rpc(
16504    process: &mut ActiveProcess,
16505    request: &JavascriptSyncRpcRequest,
16506) -> Result<Value, SidecarError> {
16507    match request.method.as_str() {
16508        "crypto.hashDigest" => {
16509            let algorithm = javascript_crypto_digest_algorithm(
16510                &request.args,
16511                0,
16512                "crypto.hashDigest algorithm",
16513            )?;
16514            let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.hashDigest data")?;
16515            Ok(Value::String(
16516                base64::engine::general_purpose::STANDARD.encode(algorithm.digest(&data)),
16517            ))
16518        }
16519        "crypto.hmacDigest" => {
16520            let algorithm = javascript_crypto_digest_algorithm(
16521                &request.args,
16522                0,
16523                "crypto.hmacDigest algorithm",
16524            )?;
16525            let key = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.hmacDigest key")?;
16526            let data = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.hmacDigest data")?;
16527            Ok(Value::String(
16528                base64::engine::general_purpose::STANDARD.encode(algorithm.hmac(&key, &data)?),
16529            ))
16530        }
16531        "crypto.pbkdf2" => {
16532            let password =
16533                javascript_sync_rpc_base64_arg(&request.args, 0, "crypto.pbkdf2 password")?;
16534            let salt = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.pbkdf2 salt")?;
16535            let iterations =
16536                javascript_sync_rpc_arg_u32(&request.args, 2, "crypto.pbkdf2 iterations")?;
16537            if iterations == 0 {
16538                return Err(SidecarError::InvalidState(String::from(
16539                    "crypto.pbkdf2 iterations must be greater than zero",
16540                )));
16541            }
16542            let key_len = usize::try_from(javascript_sync_rpc_arg_u64(
16543                &request.args,
16544                3,
16545                "crypto.pbkdf2 key length",
16546            )?)
16547            .map_err(|_| {
16548                SidecarError::InvalidState(String::from(
16549                    "crypto.pbkdf2 key length must fit within usize",
16550                ))
16551            })?;
16552            let algorithm =
16553                javascript_crypto_digest_algorithm(&request.args, 4, "crypto.pbkdf2 digest")?;
16554            let mut output = vec![0u8; key_len];
16555            algorithm.pbkdf2(&password, &salt, iterations, &mut output);
16556            Ok(Value::String(
16557                base64::engine::general_purpose::STANDARD.encode(output),
16558            ))
16559        }
16560        "crypto.scrypt" => {
16561            let password =
16562                javascript_sync_rpc_base64_arg(&request.args, 0, "crypto.scrypt password")?;
16563            let salt = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.scrypt salt")?;
16564            let key_len = usize::try_from(javascript_sync_rpc_arg_u64(
16565                &request.args,
16566                2,
16567                "crypto.scrypt key length",
16568            )?)
16569            .map_err(|_| {
16570                SidecarError::InvalidState(String::from(
16571                    "crypto.scrypt key length must fit within usize",
16572                ))
16573            })?;
16574            let options_json =
16575                javascript_sync_rpc_arg_str(&request.args, 3, "crypto.scrypt options")?;
16576            let options: JavascriptScryptOptions =
16577                serde_json::from_str(options_json).map_err(|error| {
16578                    SidecarError::InvalidState(format!(
16579                        "crypto.scrypt options must be valid JSON: {error}"
16580                    ))
16581                })?;
16582            let cost = options.cost.unwrap_or(DEFAULT_SCRYPT_COST);
16583            if cost == 0 || !cost.is_power_of_two() {
16584                return Err(SidecarError::InvalidState(String::from(
16585                    "crypto.scrypt cost must be a positive power of two",
16586                )));
16587            }
16588            let log_n = u8::try_from(cost.ilog2()).map_err(|_| {
16589                SidecarError::InvalidState(String::from(
16590                    "crypto.scrypt cost exceeds supported parameter range",
16591                ))
16592            })?;
16593            let params = ScryptParams::new(
16594                log_n,
16595                options.block_size.unwrap_or(DEFAULT_SCRYPT_BLOCK_SIZE),
16596                options
16597                    .parallelization
16598                    .unwrap_or(DEFAULT_SCRYPT_PARALLELIZATION),
16599                key_len,
16600            )
16601            .map_err(|error| {
16602                SidecarError::InvalidState(format!("crypto.scrypt options are invalid: {error}"))
16603            })?;
16604            let mut output = vec![0u8; key_len];
16605            scrypt(&password, &salt, &params, &mut output).map_err(|error| {
16606                SidecarError::Execution(format!("crypto.scrypt failed: {error}"))
16607            })?;
16608            Ok(Value::String(
16609                base64::engine::general_purpose::STANDARD.encode(output),
16610            ))
16611        }
16612        "crypto.cipheriv" => service_javascript_crypto_cipheriv_sync_rpc(request),
16613        "crypto.decipheriv" => service_javascript_crypto_decipheriv_sync_rpc(request),
16614        "crypto.cipherivCreate" => {
16615            service_javascript_crypto_cipheriv_create_sync_rpc(process, request)
16616        }
16617        "crypto.cipherivUpdate" => {
16618            service_javascript_crypto_cipheriv_update_sync_rpc(process, request)
16619        }
16620        "crypto.cipherivFinal" => {
16621            service_javascript_crypto_cipheriv_final_sync_rpc(process, request)
16622        }
16623        "crypto.sign" => service_javascript_crypto_sign_sync_rpc(request),
16624        "crypto.verify" => service_javascript_crypto_verify_sync_rpc(request),
16625        "crypto.asymmetricOp" => service_javascript_crypto_asymmetric_op_sync_rpc(request),
16626        "crypto.createKeyObject" => service_javascript_crypto_create_key_object_sync_rpc(request),
16627        "crypto.generateKeyPairSync" => {
16628            service_javascript_crypto_generate_key_pair_sync_rpc(request)
16629        }
16630        "crypto.generateKeySync" => service_javascript_crypto_generate_key_sync_rpc(request),
16631        "crypto.generatePrimeSync" => service_javascript_crypto_generate_prime_sync_rpc(request),
16632        "crypto.diffieHellman" => service_javascript_crypto_diffie_hellman_sync_rpc(request),
16633        "crypto.diffieHellmanGroup" => {
16634            service_javascript_crypto_diffie_hellman_group_sync_rpc(request)
16635        }
16636        "crypto.diffieHellmanSessionCreate" => {
16637            service_javascript_crypto_diffie_hellman_session_create_sync_rpc(process, request)
16638        }
16639        "crypto.diffieHellmanSessionCall" => {
16640            service_javascript_crypto_diffie_hellman_session_call_sync_rpc(process, request)
16641        }
16642        "crypto.diffieHellmanSessionDestroy" => {
16643            service_javascript_crypto_diffie_hellman_session_destroy_sync_rpc(process, request)
16644        }
16645        "crypto.subtle" => service_javascript_crypto_subtle_sync_rpc(request),
16646        _ => Err(SidecarError::InvalidState(format!(
16647            "unsupported JavaScript crypto sync RPC method {}",
16648            request.method
16649        ))),
16650    }
16651}
16652
16653fn javascript_crypto_digest_algorithm(
16654    args: &[Value],
16655    index: usize,
16656    label: &str,
16657) -> Result<JavascriptCryptoDigestAlgorithm, SidecarError> {
16658    JavascriptCryptoDigestAlgorithm::parse(javascript_sync_rpc_arg_str(args, index, label)?)
16659}
16660
16661impl JavascriptCryptoDigestAlgorithm {
16662    fn parse(value: &str) -> Result<Self, SidecarError> {
16663        match value.trim().to_ascii_lowercase().replace('-', "").as_str() {
16664            "md5" => Ok(Self::Md5),
16665            "sha1" => Ok(Self::Sha1),
16666            "sha224" => Ok(Self::Sha224),
16667            "sha256" => Ok(Self::Sha256),
16668            "sha384" => Ok(Self::Sha384),
16669            "sha512" => Ok(Self::Sha512),
16670            _ => Err(SidecarError::InvalidState(format!(
16671                "unsupported crypto digest algorithm {value}"
16672            ))),
16673        }
16674    }
16675
16676    fn digest(self, data: &[u8]) -> Vec<u8> {
16677        match self {
16678            Self::Md5 => Md5::digest(data).to_vec(),
16679            Self::Sha1 => Sha1::digest(data).to_vec(),
16680            Self::Sha224 => Sha224::digest(data).to_vec(),
16681            Self::Sha256 => Sha256::digest(data).to_vec(),
16682            Self::Sha384 => Sha384::digest(data).to_vec(),
16683            Self::Sha512 => Sha512::digest(data).to_vec(),
16684        }
16685    }
16686
16687    fn hmac(self, key: &[u8], data: &[u8]) -> Result<Vec<u8>, SidecarError> {
16688        match self {
16689            Self::Md5 => {
16690                let mut mac = Hmac::<Md5>::new_from_slice(key).map_err(|error| {
16691                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
16692                })?;
16693                mac.update(data);
16694                Ok(mac.finalize().into_bytes().to_vec())
16695            }
16696            Self::Sha1 => {
16697                let mut mac = Hmac::<Sha1>::new_from_slice(key).map_err(|error| {
16698                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
16699                })?;
16700                mac.update(data);
16701                Ok(mac.finalize().into_bytes().to_vec())
16702            }
16703            Self::Sha224 => {
16704                let mut mac = Hmac::<Sha224>::new_from_slice(key).map_err(|error| {
16705                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
16706                })?;
16707                mac.update(data);
16708                Ok(mac.finalize().into_bytes().to_vec())
16709            }
16710            Self::Sha256 => {
16711                let mut mac = Hmac::<Sha256>::new_from_slice(key).map_err(|error| {
16712                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
16713                })?;
16714                mac.update(data);
16715                Ok(mac.finalize().into_bytes().to_vec())
16716            }
16717            Self::Sha384 => {
16718                let mut mac = Hmac::<Sha384>::new_from_slice(key).map_err(|error| {
16719                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
16720                })?;
16721                mac.update(data);
16722                Ok(mac.finalize().into_bytes().to_vec())
16723            }
16724            Self::Sha512 => {
16725                let mut mac = Hmac::<Sha512>::new_from_slice(key).map_err(|error| {
16726                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
16727                })?;
16728                mac.update(data);
16729                Ok(mac.finalize().into_bytes().to_vec())
16730            }
16731        }
16732    }
16733
16734    fn pbkdf2(self, password: &[u8], salt: &[u8], iterations: u32, output: &mut [u8]) {
16735        match self {
16736            Self::Md5 => pbkdf2_hmac::<Md5>(password, salt, iterations, output),
16737            Self::Sha1 => pbkdf2_hmac::<Sha1>(password, salt, iterations, output),
16738            Self::Sha224 => pbkdf2_hmac::<Sha224>(password, salt, iterations, output),
16739            Self::Sha256 => pbkdf2_hmac::<Sha256>(password, salt, iterations, output),
16740            Self::Sha384 => pbkdf2_hmac::<Sha384>(password, salt, iterations, output),
16741            Self::Sha512 => pbkdf2_hmac::<Sha512>(password, salt, iterations, output),
16742        }
16743    }
16744}
16745
16746#[derive(Debug, Clone)]
16747enum JavascriptCryptoKeyMaterial {
16748    Private(PKey<Private>),
16749    Public(PKey<Public>),
16750    Secret(Vec<u8>),
16751}
16752
16753#[derive(Debug, Clone, Deserialize, Serialize)]
16754struct JavascriptSerializedSandboxKeyObject {
16755    #[serde(rename = "type")]
16756    kind: String,
16757    #[serde(skip_serializing_if = "Option::is_none")]
16758    pem: Option<String>,
16759    #[serde(skip_serializing_if = "Option::is_none")]
16760    raw: Option<String>,
16761    #[serde(skip_serializing_if = "Option::is_none", rename = "asymmetricKeyType")]
16762    asymmetric_key_type: Option<String>,
16763    #[serde(
16764        skip_serializing_if = "Option::is_none",
16765        rename = "asymmetricKeyDetails"
16766    )]
16767    asymmetric_key_details: Option<Map<String, Value>>,
16768    #[serde(skip_serializing_if = "Option::is_none")]
16769    jwk: Option<Value>,
16770}
16771
16772#[derive(Debug, Clone)]
16773struct JavascriptDirectKeyInput {
16774    key: JavascriptCryptoKeyMaterial,
16775    padding: Option<Padding>,
16776}
16777
16778fn service_javascript_crypto_cipheriv_sync_rpc(
16779    request: &JavascriptSyncRpcRequest,
16780) -> Result<Value, SidecarError> {
16781    service_javascript_crypto_cipheriv_inner(request, false)
16782}
16783
16784fn service_javascript_crypto_decipheriv_sync_rpc(
16785    request: &JavascriptSyncRpcRequest,
16786) -> Result<Value, SidecarError> {
16787    service_javascript_crypto_cipheriv_inner(request, true)
16788}
16789
16790fn service_javascript_crypto_cipheriv_create_sync_rpc(
16791    process: &mut ActiveProcess,
16792    request: &JavascriptSyncRpcRequest,
16793) -> Result<Value, SidecarError> {
16794    ensure_per_process_state_handle_capacity(process.cipher_sessions.len(), "cipher session")?;
16795    let mode = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.cipherivCreate mode")?;
16796    let decrypt = mode == "decipher";
16797    let algorithm =
16798        javascript_sync_rpc_arg_str(&request.args, 1, "crypto.cipherivCreate algorithm")?;
16799    let key = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.cipherivCreate key")?;
16800    let iv = javascript_sync_rpc_base64_arg_optional(&request.args, 3, "crypto.cipherivCreate iv")?;
16801    let options =
16802        javascript_sync_rpc_json_arg_optional(&request.args, 4, "crypto.cipherivCreate options")?;
16803    let context = javascript_crypto_build_cipher_session(
16804        algorithm,
16805        &key,
16806        iv.as_deref(),
16807        decrypt,
16808        options.as_ref(),
16809    )?;
16810    process.next_cipher_session_id += 1;
16811    let session_id = process.next_cipher_session_id;
16812    process
16813        .cipher_sessions
16814        .insert(session_id, ActiveCipherSession { context });
16815    Ok(json!(session_id))
16816}
16817
16818fn service_javascript_crypto_cipheriv_update_sync_rpc(
16819    process: &mut ActiveProcess,
16820    request: &JavascriptSyncRpcRequest,
16821) -> Result<Value, SidecarError> {
16822    let session_id =
16823        javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.cipherivUpdate session id")?;
16824    let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.cipherivUpdate data")?;
16825    let session = process
16826        .cipher_sessions
16827        .get_mut(&session_id)
16828        .ok_or_else(|| {
16829            SidecarError::InvalidState(format!("Cipher session {session_id} not found"))
16830        })?;
16831    let result = javascript_crypto_cipher_update(&mut session.context, &data)?;
16832    Ok(Value::String(
16833        base64::engine::general_purpose::STANDARD.encode(result),
16834    ))
16835}
16836
16837fn service_javascript_crypto_cipheriv_final_sync_rpc(
16838    process: &mut ActiveProcess,
16839    request: &JavascriptSyncRpcRequest,
16840) -> Result<Value, SidecarError> {
16841    let session_id =
16842        javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.cipherivFinal session id")?;
16843    let session = process.cipher_sessions.remove(&session_id).ok_or_else(|| {
16844        SidecarError::InvalidState(format!("Cipher session {session_id} not found"))
16845    })?;
16846    let outcome = session
16847        .context
16848        .finalize()
16849        .map_err(javascript_crypto_cipher_error)?;
16850    let mut response = Map::new();
16851    response.insert(
16852        String::from("data"),
16853        Value::String(base64::engine::general_purpose::STANDARD.encode(outcome.data)),
16854    );
16855    if let Some(auth_tag) = outcome.auth_tag {
16856        response.insert(
16857            String::from("authTag"),
16858            Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
16859        );
16860    }
16861    Ok(Value::String(serde_json::to_string(&response).map_err(
16862        |error| SidecarError::InvalidState(format!("serialize cipher final response: {error}")),
16863    )?))
16864}
16865
16866fn service_javascript_crypto_sign_sync_rpc(
16867    request: &JavascriptSyncRpcRequest,
16868) -> Result<Value, SidecarError> {
16869    let algorithm = request.args.first().and_then(Value::as_str);
16870    let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.sign data")?;
16871    let key_json = javascript_sync_rpc_arg_str(&request.args, 2, "crypto.sign key")?;
16872    let key_input =
16873        javascript_crypto_parse_direct_key_input(key_json, Some("private"), "crypto.sign key")?;
16874    let private_key = javascript_crypto_expect_private_key(key_input.key, "crypto.sign key")?;
16875    let mut signer = javascript_crypto_new_signer(algorithm, &private_key)?;
16876    if let Some(padding) = key_input.padding {
16877        signer
16878            .set_rsa_padding(padding)
16879            .map_err(javascript_crypto_openssl_error)?;
16880    }
16881    signer
16882        .update(&data)
16883        .map_err(javascript_crypto_openssl_error)?;
16884    Ok(Value::String(
16885        base64::engine::general_purpose::STANDARD.encode(
16886            signer
16887                .sign_to_vec()
16888                .map_err(javascript_crypto_openssl_error)?,
16889        ),
16890    ))
16891}
16892
16893fn service_javascript_crypto_verify_sync_rpc(
16894    request: &JavascriptSyncRpcRequest,
16895) -> Result<Value, SidecarError> {
16896    let algorithm = request.args.first().and_then(Value::as_str);
16897    let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.verify data")?;
16898    let key_json = javascript_sync_rpc_arg_str(&request.args, 2, "crypto.verify key")?;
16899    let signature = javascript_sync_rpc_base64_arg(&request.args, 3, "crypto.verify signature")?;
16900    let key_input =
16901        javascript_crypto_parse_direct_key_input(key_json, Some("public"), "crypto.verify key")?;
16902    let public_key = javascript_crypto_expect_public_key(key_input.key, "crypto.verify key")?;
16903    let mut verifier = javascript_crypto_new_verifier(algorithm, &public_key)?;
16904    if let Some(padding) = key_input.padding {
16905        verifier
16906            .set_rsa_padding(padding)
16907            .map_err(javascript_crypto_openssl_error)?;
16908    }
16909    verifier
16910        .update(&data)
16911        .map_err(javascript_crypto_openssl_error)?;
16912    Ok(json!(verifier
16913        .verify(&signature)
16914        .map_err(javascript_crypto_openssl_error)?))
16915}
16916
16917fn service_javascript_crypto_asymmetric_op_sync_rpc(
16918    request: &JavascriptSyncRpcRequest,
16919) -> Result<Value, SidecarError> {
16920    let operation = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.asymmetricOp operation")?;
16921    let key_json = javascript_sync_rpc_arg_str(&request.args, 1, "crypto.asymmetricOp key")?;
16922    let data = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.asymmetricOp data")?;
16923    let expect_kind = match operation {
16924        "publicEncrypt" | "publicDecrypt" => Some("public"),
16925        "privateEncrypt" | "privateDecrypt" => Some("private"),
16926        other => {
16927            return Err(SidecarError::InvalidState(format!(
16928                "Unsupported asymmetric crypto operation: {other}"
16929            )));
16930        }
16931    };
16932    let key_input =
16933        javascript_crypto_parse_direct_key_input(key_json, expect_kind, "crypto.asymmetricOp key")?;
16934    let padding = key_input.padding.unwrap_or(Padding::PKCS1);
16935    let mut output = vec![0_u8; javascript_crypto_rsa_output_size(&key_input.key)?];
16936    let written = match (operation, key_input.key) {
16937        ("publicEncrypt", JavascriptCryptoKeyMaterial::Public(key))
16938        | ("publicDecrypt", JavascriptCryptoKeyMaterial::Public(key)) => {
16939            let rsa = key.rsa().map_err(javascript_crypto_openssl_error)?;
16940            if operation == "publicEncrypt" {
16941                rsa.public_encrypt(&data, &mut output, padding)
16942                    .map_err(javascript_crypto_openssl_error)?
16943            } else {
16944                rsa.public_decrypt(&data, &mut output, padding)
16945                    .map_err(javascript_crypto_openssl_error)?
16946            }
16947        }
16948        ("privateEncrypt", JavascriptCryptoKeyMaterial::Private(key))
16949        | ("privateDecrypt", JavascriptCryptoKeyMaterial::Private(key)) => {
16950            let rsa = key.rsa().map_err(javascript_crypto_openssl_error)?;
16951            if operation == "privateEncrypt" {
16952                rsa.private_encrypt(&data, &mut output, padding)
16953                    .map_err(javascript_crypto_openssl_error)?
16954            } else {
16955                rsa.private_decrypt(&data, &mut output, padding)
16956                    .map_err(javascript_crypto_openssl_error)?
16957            }
16958        }
16959        _ => {
16960            return Err(SidecarError::InvalidState(format!(
16961                "{operation} requires an RSA {} key",
16962                expect_kind.unwrap_or("asymmetric")
16963            )));
16964        }
16965    };
16966    output.truncate(written);
16967    Ok(Value::String(
16968        base64::engine::general_purpose::STANDARD.encode(output),
16969    ))
16970}
16971
16972fn service_javascript_crypto_create_key_object_sync_rpc(
16973    request: &JavascriptSyncRpcRequest,
16974) -> Result<Value, SidecarError> {
16975    let operation =
16976        javascript_sync_rpc_arg_str(&request.args, 0, "crypto.createKeyObject operation")?;
16977    let key_json = javascript_sync_rpc_arg_str(&request.args, 1, "crypto.createKeyObject key")?;
16978    let expected = match operation {
16979        "createPrivateKey" => Some("private"),
16980        "createPublicKey" => Some("public"),
16981        other => {
16982            return Err(SidecarError::InvalidState(format!(
16983                "Unsupported key creation operation: {other}"
16984            )));
16985        }
16986    };
16987    let key_input =
16988        javascript_crypto_parse_direct_key_input(key_json, expected, "crypto.createKeyObject key")?;
16989    Ok(Value::String(
16990        serde_json::to_string(&javascript_crypto_serialize_sandbox_key_object(
16991            &key_input.key,
16992        )?)
16993        .map_err(|error| {
16994            SidecarError::InvalidState(format!("serialize crypto key object: {error}"))
16995        })?,
16996    ))
16997}
16998
16999fn service_javascript_crypto_generate_key_pair_sync_rpc(
17000    request: &JavascriptSyncRpcRequest,
17001) -> Result<Value, SidecarError> {
17002    let key_type =
17003        javascript_sync_rpc_arg_str(&request.args, 0, "crypto.generateKeyPairSync type")?;
17004    let options = javascript_crypto_parse_serialized_options_arg(
17005        &request.args,
17006        1,
17007        "crypto.generateKeyPairSync options",
17008    )?
17009    .unwrap_or(Value::Object(Map::new()));
17010    let public_encoding = options.get("publicKeyEncoding").cloned();
17011    let private_encoding = options.get("privateKeyEncoding").cloned();
17012
17013    let private_key = match key_type {
17014        "rsa" => {
17015            let bits = options
17016                .get("modulusLength")
17017                .and_then(Value::as_u64)
17018                .unwrap_or(2048) as u32;
17019            let exponent = options
17020                .get("publicExponent")
17021                .map(|value| javascript_crypto_u32_from_bridge_value(value, "rsa publicExponent"))
17022                .transpose()?
17023                .unwrap_or(65_537);
17024            let exponent = BigNum::from_u32(exponent).map_err(javascript_crypto_openssl_error)?;
17025            let rsa =
17026                Rsa::generate_with_e(bits, &exponent).map_err(javascript_crypto_openssl_error)?;
17027            PKey::from_rsa(rsa).map_err(javascript_crypto_openssl_error)?
17028        }
17029        "ec" => {
17030            let named_curve = options
17031                .get("namedCurve")
17032                .and_then(Value::as_str)
17033                .ok_or_else(|| {
17034                    SidecarError::InvalidState(String::from(
17035                        "crypto.generateKeyPairSync ec requires namedCurve",
17036                    ))
17037                })?;
17038            let group = EcGroup::from_curve_name(javascript_crypto_curve_nid(named_curve)?)
17039                .map_err(javascript_crypto_openssl_error)?;
17040            let key = EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?;
17041            PKey::from_ec_key(key).map_err(javascript_crypto_openssl_error)?
17042        }
17043        "ed25519" => PKey::generate_ed25519().map_err(javascript_crypto_openssl_error)?,
17044        "x25519" => PKey::generate_x25519().map_err(javascript_crypto_openssl_error)?,
17045        other => {
17046            return Err(SidecarError::InvalidState(format!(
17047                "unsupported crypto key pair type {other}"
17048            )));
17049        }
17050    };
17051    let public_key = PKey::public_key_from_pem(
17052        &private_key
17053            .public_key_to_pem()
17054            .map_err(javascript_crypto_openssl_error)?,
17055    )
17056    .map_err(javascript_crypto_openssl_error)?;
17057    let response = if public_encoding.is_some() || private_encoding.is_some() {
17058        json!({
17059            "publicKey": javascript_crypto_serialize_encoded_key_value_public(&public_key, public_encoding.as_ref())?,
17060            "privateKey": javascript_crypto_serialize_encoded_key_value_private(&private_key, private_encoding.as_ref())?,
17061        })
17062    } else {
17063        json!({
17064            "publicKey": javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Public(public_key))?,
17065            "privateKey": javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Private(private_key))?,
17066        })
17067    };
17068    Ok(Value::String(serde_json::to_string(&response).map_err(
17069        |error| SidecarError::InvalidState(format!("serialize generated key pair: {error}")),
17070    )?))
17071}
17072
17073fn service_javascript_crypto_generate_key_sync_rpc(
17074    request: &JavascriptSyncRpcRequest,
17075) -> Result<Value, SidecarError> {
17076    let key_type = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.generateKeySync type")?;
17077    let options = javascript_crypto_parse_serialized_options_arg(
17078        &request.args,
17079        1,
17080        "crypto.generateKeySync options",
17081    )?
17082    .unwrap_or(Value::Object(Map::new()));
17083    let bit_length = options
17084        .get("length")
17085        .and_then(Value::as_u64)
17086        .ok_or_else(|| {
17087            SidecarError::InvalidState(String::from(
17088                "crypto.generateKeySync options.length is required",
17089            ))
17090        })? as usize;
17091    let mut raw = vec![0_u8; bit_length.div_ceil(8)];
17092    rand_bytes(&mut raw).map_err(javascript_crypto_openssl_error)?;
17093    let serialized = match key_type {
17094        "hmac" => javascript_crypto_serialize_sandbox_key_object(
17095            &JavascriptCryptoKeyMaterial::Secret(raw),
17096        )?,
17097        "aes" => javascript_crypto_serialize_sandbox_key_object(
17098            &JavascriptCryptoKeyMaterial::Secret(raw),
17099        )?,
17100        other => {
17101            return Err(SidecarError::InvalidState(format!(
17102                "unsupported crypto.generateKeySync type {other}"
17103            )));
17104        }
17105    };
17106    Ok(Value::String(serde_json::to_string(&serialized).map_err(
17107        |error| SidecarError::InvalidState(format!("serialize generated key: {error}")),
17108    )?))
17109}
17110
17111fn service_javascript_crypto_generate_prime_sync_rpc(
17112    request: &JavascriptSyncRpcRequest,
17113) -> Result<Value, SidecarError> {
17114    let bits =
17115        javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.generatePrimeSync size")? as i32;
17116    let options = javascript_crypto_parse_serialized_options_arg(
17117        &request.args,
17118        1,
17119        "crypto.generatePrimeSync options",
17120    )?
17121    .unwrap_or(Value::Object(Map::new()));
17122    let safe = options
17123        .get("safe")
17124        .and_then(Value::as_bool)
17125        .unwrap_or(false);
17126    let add = options
17127        .get("add")
17128        .map(|value| javascript_crypto_bignum_from_bridge_value(value, "prime add"))
17129        .transpose()?;
17130    let rem = options
17131        .get("rem")
17132        .map(|value| javascript_crypto_bignum_from_bridge_value(value, "prime rem"))
17133        .transpose()?;
17134    let mut prime = BigNum::new().map_err(javascript_crypto_openssl_error)?;
17135    prime
17136        .generate_prime(bits, safe, add.as_deref(), rem.as_deref())
17137        .map_err(javascript_crypto_openssl_error)?;
17138    let payload = if options
17139        .get("bigint")
17140        .and_then(Value::as_bool)
17141        .unwrap_or(false)
17142    {
17143        json!({
17144            "__type": "bigint",
17145            "value": prime.to_dec_str().map_err(javascript_crypto_openssl_error)?.to_string(),
17146        })
17147    } else {
17148        json!({
17149            "__type": "buffer",
17150            "value": base64::engine::general_purpose::STANDARD.encode(prime.to_vec()),
17151        })
17152    };
17153    Ok(Value::String(serde_json::to_string(&payload).map_err(
17154        |error| SidecarError::InvalidState(format!("serialize generated prime: {error}")),
17155    )?))
17156}
17157
17158fn service_javascript_crypto_diffie_hellman_sync_rpc(
17159    request: &JavascriptSyncRpcRequest,
17160) -> Result<Value, SidecarError> {
17161    let options = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.diffieHellman options")?;
17162    let parsed: Value = serde_json::from_str(options).map_err(|error| {
17163        SidecarError::InvalidState(format!(
17164            "crypto.diffieHellman options must be valid JSON: {error}"
17165        ))
17166    })?;
17167    let private_key = javascript_crypto_parse_key_material_value(
17168        parsed.get("privateKey").ok_or_else(|| {
17169            SidecarError::InvalidState(String::from("crypto.diffieHellman missing privateKey"))
17170        })?,
17171        Some("private"),
17172        "crypto.diffieHellman privateKey",
17173    )?;
17174    let public_key = javascript_crypto_parse_key_material_value(
17175        parsed.get("publicKey").ok_or_else(|| {
17176            SidecarError::InvalidState(String::from("crypto.diffieHellman missing publicKey"))
17177        })?,
17178        Some("public"),
17179        "crypto.diffieHellman publicKey",
17180    )?;
17181    let private_key =
17182        javascript_crypto_expect_private_key(private_key, "crypto.diffieHellman privateKey")?;
17183    let public_key =
17184        javascript_crypto_expect_public_key(public_key, "crypto.diffieHellman publicKey")?;
17185    let mut deriver = Deriver::new(&private_key).map_err(javascript_crypto_openssl_error)?;
17186    deriver
17187        .set_peer(&public_key)
17188        .map_err(javascript_crypto_openssl_error)?;
17189    let secret = deriver
17190        .derive_to_vec()
17191        .map_err(javascript_crypto_openssl_error)?;
17192    Ok(Value::String(
17193        serde_json::to_string(&json!({
17194            "__type": "buffer",
17195            "value": base64::engine::general_purpose::STANDARD.encode(secret),
17196        }))
17197        .map_err(|error| {
17198            SidecarError::InvalidState(format!("serialize derived secret: {error}"))
17199        })?,
17200    ))
17201}
17202
17203fn service_javascript_crypto_diffie_hellman_group_sync_rpc(
17204    request: &JavascriptSyncRpcRequest,
17205) -> Result<Value, SidecarError> {
17206    let name = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.diffieHellmanGroup name")?;
17207    let params = javascript_crypto_named_dh_group(name)?;
17208    let response = json!({
17209        "prime": {
17210            "__type": "buffer",
17211            "value": base64::engine::general_purpose::STANDARD.encode(params.prime_p().to_vec()),
17212        },
17213        "generator": {
17214            "__type": "buffer",
17215            "value": base64::engine::general_purpose::STANDARD.encode(params.generator().to_vec()),
17216        },
17217    });
17218    Ok(Value::String(serde_json::to_string(&response).map_err(
17219        |error| {
17220            SidecarError::InvalidState(format!("serialize diffieHellmanGroup response: {error}"))
17221        },
17222    )?))
17223}
17224
17225fn service_javascript_crypto_diffie_hellman_session_create_sync_rpc(
17226    process: &mut ActiveProcess,
17227    request: &JavascriptSyncRpcRequest,
17228) -> Result<Value, SidecarError> {
17229    ensure_per_process_state_handle_capacity(
17230        process.diffie_hellman_sessions.len(),
17231        "diffie-hellman session",
17232    )?;
17233    let raw = javascript_sync_rpc_arg_str(
17234        &request.args,
17235        0,
17236        "crypto.diffieHellmanSessionCreate request",
17237    )?;
17238    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
17239        SidecarError::InvalidState(format!(
17240            "crypto.diffieHellmanSessionCreate request must be valid JSON: {error}"
17241        ))
17242    })?;
17243    let session = match parsed.get("type").and_then(Value::as_str) {
17244        Some("group") => {
17245            let name = parsed.get("name").and_then(Value::as_str).ok_or_else(|| {
17246                SidecarError::InvalidState(String::from(
17247                    "crypto.diffieHellmanSessionCreate group requires name",
17248                ))
17249            })?;
17250            ActiveDiffieHellmanSession::Dh(ActiveDhSession {
17251                params: javascript_crypto_named_dh_group(name)?,
17252                key_pair: None,
17253            })
17254        }
17255        Some("dh") => {
17256            let args = parsed
17257                .get("args")
17258                .and_then(Value::as_array)
17259                .ok_or_else(|| {
17260                    SidecarError::InvalidState(String::from(
17261                        "crypto.diffieHellmanSessionCreate dh requires args",
17262                    ))
17263                })?;
17264            let params = javascript_crypto_build_dh_params(args)?;
17265            ActiveDiffieHellmanSession::Dh(ActiveDhSession {
17266                params,
17267                key_pair: None,
17268            })
17269        }
17270        Some("ecdh") => {
17271            let curve = parsed.get("name").and_then(Value::as_str).ok_or_else(|| {
17272                SidecarError::InvalidState(String::from(
17273                    "crypto.diffieHellmanSessionCreate ecdh requires name",
17274                ))
17275            })?;
17276            ActiveDiffieHellmanSession::Ecdh(ActiveEcdhSession {
17277                curve: curve.to_string(),
17278                key_pair: None,
17279            })
17280        }
17281        other => {
17282            return Err(SidecarError::InvalidState(format!(
17283                "Unsupported Diffie-Hellman session type: {}",
17284                other.unwrap_or("<missing>")
17285            )));
17286        }
17287    };
17288    process.next_diffie_hellman_session_id += 1;
17289    let session_id = process.next_diffie_hellman_session_id;
17290    process.diffie_hellman_sessions.insert(session_id, session);
17291    Ok(json!(session_id))
17292}
17293
17294fn service_javascript_crypto_diffie_hellman_session_call_sync_rpc(
17295    process: &mut ActiveProcess,
17296    request: &JavascriptSyncRpcRequest,
17297) -> Result<Value, SidecarError> {
17298    let session_id = javascript_sync_rpc_arg_u64(
17299        &request.args,
17300        0,
17301        "crypto.diffieHellmanSessionCall session id",
17302    )?;
17303    let raw =
17304        javascript_sync_rpc_arg_str(&request.args, 1, "crypto.diffieHellmanSessionCall request")?;
17305    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
17306        SidecarError::InvalidState(format!(
17307            "crypto.diffieHellmanSessionCall request must be valid JSON: {error}"
17308        ))
17309    })?;
17310    let method = parsed
17311        .get("method")
17312        .and_then(Value::as_str)
17313        .ok_or_else(|| {
17314            SidecarError::InvalidState(String::from(
17315                "crypto.diffieHellmanSessionCall request missing method",
17316            ))
17317        })?;
17318    let args = parsed
17319        .get("args")
17320        .and_then(Value::as_array)
17321        .cloned()
17322        .unwrap_or_default();
17323    let session = process
17324        .diffie_hellman_sessions
17325        .get_mut(&session_id)
17326        .ok_or_else(|| {
17327            SidecarError::InvalidState(format!("Diffie-Hellman session {session_id} not found"))
17328        })?;
17329    let (result, has_result) = match session {
17330        ActiveDiffieHellmanSession::Dh(session) => {
17331            javascript_crypto_call_dh_session(session, method, &args)?
17332        }
17333        ActiveDiffieHellmanSession::Ecdh(session) => {
17334            javascript_crypto_call_ecdh_session(session, method, &args)?
17335        }
17336    };
17337    Ok(Value::String(
17338        serde_json::to_string(&json!({
17339            "result": result,
17340            "hasResult": has_result,
17341        }))
17342        .map_err(|error| {
17343            SidecarError::InvalidState(format!("serialize diffie session result: {error}"))
17344        })?,
17345    ))
17346}
17347
17348fn service_javascript_crypto_diffie_hellman_session_destroy_sync_rpc(
17349    process: &mut ActiveProcess,
17350    request: &JavascriptSyncRpcRequest,
17351) -> Result<Value, SidecarError> {
17352    let session_id = javascript_sync_rpc_arg_u64(
17353        &request.args,
17354        0,
17355        "crypto.diffieHellmanSessionDestroy session id",
17356    )?;
17357    process
17358        .diffie_hellman_sessions
17359        .remove(&session_id)
17360        .ok_or_else(|| {
17361            SidecarError::InvalidState(format!("Diffie-Hellman session {session_id} not found"))
17362        })?;
17363    Ok(Value::Null)
17364}
17365
17366fn service_javascript_crypto_subtle_sync_rpc(
17367    request: &JavascriptSyncRpcRequest,
17368) -> Result<Value, SidecarError> {
17369    let raw = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.subtle request")?;
17370    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
17371        SidecarError::InvalidState(format!("crypto.subtle request must be valid JSON: {error}"))
17372    })?;
17373    let op = parsed.get("op").and_then(Value::as_str).ok_or_else(|| {
17374        SidecarError::InvalidState(String::from("crypto.subtle request missing op"))
17375    })?;
17376    match op {
17377        "digest" => {
17378            let algorithm = parsed
17379                .get("algorithm")
17380                .and_then(Value::as_str)
17381                .ok_or_else(|| {
17382                    SidecarError::InvalidState(String::from(
17383                        "crypto.subtle.digest missing algorithm",
17384                    ))
17385                })?;
17386            let data = parsed.get("data").and_then(Value::as_str).ok_or_else(|| {
17387                SidecarError::InvalidState(String::from("crypto.subtle.digest missing data"))
17388            })?;
17389            let bytes = base64::engine::general_purpose::STANDARD
17390                .decode(data)
17391                .map_err(|error| {
17392                    SidecarError::InvalidState(format!("crypto.subtle.digest data base64: {error}"))
17393                })?;
17394            let digest = JavascriptCryptoDigestAlgorithm::parse(algorithm)?.digest(&bytes);
17395            Ok(Value::String(
17396                serde_json::to_string(&json!({
17397                    "data": base64::engine::general_purpose::STANDARD.encode(digest),
17398                }))
17399                .map_err(|error| {
17400                    SidecarError::InvalidState(format!("serialize crypto.subtle digest: {error}"))
17401                })?,
17402            ))
17403        }
17404        "generateKey" => {
17405            let algorithm = parsed.get("algorithm").ok_or_else(|| {
17406                SidecarError::InvalidState(String::from(
17407                    "crypto.subtle.generateKey missing algorithm",
17408                ))
17409            })?;
17410            let name =
17411                javascript_crypto_subtle_algorithm_name(algorithm, "crypto.subtle.generateKey")?;
17412            if !matches!(name, "AES-GCM" | "AES-CBC" | "AES-CTR" | "AES-KW") {
17413                return Err(SidecarError::InvalidState(format!(
17414                    "Unsupported key algorithm: {name}"
17415                )));
17416            }
17417            let length_bits = algorithm
17418                .get("length")
17419                .and_then(Value::as_u64)
17420                .ok_or_else(|| {
17421                    SidecarError::InvalidState(String::from(
17422                        "crypto.subtle.generateKey AES algorithm requires length",
17423                    ))
17424                })?;
17425            if length_bits % 8 != 0 {
17426                return Err(SidecarError::InvalidState(String::from(
17427                    "crypto.subtle.generateKey length must be byte-aligned",
17428                )));
17429            }
17430            let length_bytes = usize::try_from(length_bits / 8).map_err(|_| {
17431                SidecarError::InvalidState(String::from(
17432                    "crypto.subtle.generateKey length is too large",
17433                ))
17434            })?;
17435            let mut raw = vec![0_u8; length_bytes];
17436            rand_bytes(&mut raw).map_err(javascript_crypto_openssl_error)?;
17437            let key = javascript_crypto_serialize_subtle_secret_key(
17438                &raw,
17439                javascript_crypto_normalize_subtle_secret_algorithm(algorithm.clone(), &raw)?,
17440                parsed
17441                    .get("extractable")
17442                    .and_then(Value::as_bool)
17443                    .unwrap_or(false),
17444                parsed.get("usages").cloned().unwrap_or_else(|| json!([])),
17445            )?;
17446            Ok(Value::String(
17447                serde_json::to_string(&json!({ "key": key })).map_err(|error| {
17448                    SidecarError::InvalidState(format!(
17449                        "serialize crypto.subtle generated key: {error}"
17450                    ))
17451                })?,
17452            ))
17453        }
17454        "importKey" => {
17455            let format = parsed
17456                .get("format")
17457                .and_then(Value::as_str)
17458                .ok_or_else(|| {
17459                    SidecarError::InvalidState(String::from(
17460                        "crypto.subtle.importKey missing format",
17461                    ))
17462                })?;
17463            if format != "raw" {
17464                return Err(SidecarError::InvalidState(format!(
17465                    "Unsupported import format: {format}"
17466                )));
17467            }
17468            let key_data = parsed
17469                .get("keyData")
17470                .and_then(Value::as_str)
17471                .ok_or_else(|| {
17472                    SidecarError::InvalidState(String::from(
17473                        "crypto.subtle.importKey missing keyData",
17474                    ))
17475                })?;
17476            let raw = base64::engine::general_purpose::STANDARD
17477                .decode(key_data)
17478                .map_err(|error| {
17479                    SidecarError::InvalidState(format!(
17480                        "crypto.subtle.importKey keyData base64: {error}"
17481                    ))
17482                })?;
17483            let algorithm = parsed.get("algorithm").ok_or_else(|| {
17484                SidecarError::InvalidState(String::from(
17485                    "crypto.subtle.importKey missing algorithm",
17486                ))
17487            })?;
17488            let key = javascript_crypto_serialize_subtle_secret_key(
17489                &raw,
17490                javascript_crypto_normalize_subtle_secret_algorithm(algorithm.clone(), &raw)?,
17491                parsed
17492                    .get("extractable")
17493                    .and_then(Value::as_bool)
17494                    .unwrap_or(false),
17495                parsed.get("usages").cloned().unwrap_or_else(|| json!([])),
17496            )?;
17497            Ok(Value::String(
17498                serde_json::to_string(&json!({ "key": key })).map_err(|error| {
17499                    SidecarError::InvalidState(format!(
17500                        "serialize crypto.subtle imported key: {error}"
17501                    ))
17502                })?,
17503            ))
17504        }
17505        "exportKey" => {
17506            let format = parsed
17507                .get("format")
17508                .and_then(Value::as_str)
17509                .ok_or_else(|| {
17510                    SidecarError::InvalidState(String::from(
17511                        "crypto.subtle.exportKey missing format",
17512                    ))
17513                })?;
17514            if format != "raw" {
17515                return Err(SidecarError::InvalidState(format!(
17516                    "Unsupported export format: {format}"
17517                )));
17518            }
17519            let raw = javascript_crypto_subtle_key_raw(
17520                parsed.get("key").ok_or_else(|| {
17521                    SidecarError::InvalidState(String::from("crypto.subtle.exportKey missing key"))
17522                })?,
17523                "crypto.subtle.exportKey key",
17524            )?;
17525            Ok(Value::String(
17526                serde_json::to_string(&json!({
17527                    "data": base64::engine::general_purpose::STANDARD.encode(raw),
17528                }))
17529                .map_err(|error| {
17530                    SidecarError::InvalidState(format!("serialize crypto.subtle export: {error}"))
17531                })?,
17532            ))
17533        }
17534        "encrypt" | "decrypt" => service_javascript_crypto_subtle_aes_crypt_sync_rpc(op, &parsed),
17535        _ => Err(SidecarError::InvalidState(format!(
17536            "Unsupported subtle operation: {op}"
17537        ))),
17538    }
17539}
17540
17541fn javascript_crypto_subtle_algorithm_name<'a>(
17542    algorithm: &'a Value,
17543    label: &str,
17544) -> Result<&'a str, SidecarError> {
17545    if let Some(name) = algorithm.as_str() {
17546        return Ok(name);
17547    }
17548    algorithm
17549        .get("name")
17550        .and_then(Value::as_str)
17551        .ok_or_else(|| SidecarError::InvalidState(format!("{label} algorithm missing name")))
17552}
17553
17554fn javascript_crypto_normalize_subtle_secret_algorithm(
17555    algorithm: Value,
17556    raw: &[u8],
17557) -> Result<Value, SidecarError> {
17558    let mut object = match algorithm {
17559        Value::String(name) => {
17560            let mut object = Map::new();
17561            object.insert(String::from("name"), Value::String(name));
17562            object
17563        }
17564        Value::Object(object) => object,
17565        _ => {
17566            return Err(SidecarError::InvalidState(String::from(
17567                "crypto.subtle secret algorithm must be a string or object",
17568            )));
17569        }
17570    };
17571    let name = object
17572        .get("name")
17573        .and_then(Value::as_str)
17574        .ok_or_else(|| {
17575            SidecarError::InvalidState(String::from("crypto.subtle secret algorithm missing name"))
17576        })?
17577        .to_string();
17578    if matches!(name.as_str(), "AES-GCM" | "AES-CBC" | "AES-CTR" | "AES-KW")
17579        && !object.contains_key("length")
17580    {
17581        object.insert(String::from("length"), json!(raw.len() * 8));
17582    }
17583    Ok(Value::Object(object))
17584}
17585
17586fn javascript_crypto_serialize_subtle_secret_key(
17587    raw: &[u8],
17588    algorithm: Value,
17589    extractable: bool,
17590    usages: Value,
17591) -> Result<Value, SidecarError> {
17592    let raw_base64 = base64::engine::general_purpose::STANDARD.encode(raw);
17593    let source_key_object_data = javascript_crypto_serialize_sandbox_key_object(
17594        &JavascriptCryptoKeyMaterial::Secret(raw.to_vec()),
17595    )?;
17596    Ok(json!({
17597        "type": "secret",
17598        "algorithm": algorithm,
17599        "extractable": extractable,
17600        "usages": usages,
17601        "_raw": raw_base64,
17602        "_sourceKeyObjectData": source_key_object_data,
17603    }))
17604}
17605
17606fn javascript_crypto_subtle_key_raw(key: &Value, label: &str) -> Result<Vec<u8>, SidecarError> {
17607    let raw = key.get("_raw").and_then(Value::as_str).ok_or_else(|| {
17608        SidecarError::InvalidState(format!("{label} must be a raw secret CryptoKey"))
17609    })?;
17610    base64::engine::general_purpose::STANDARD
17611        .decode(raw)
17612        .map_err(|error| SidecarError::InvalidState(format!("{label} raw base64: {error}")))
17613}
17614
17615fn service_javascript_crypto_subtle_aes_crypt_sync_rpc(
17616    op: &str,
17617    parsed: &Value,
17618) -> Result<Value, SidecarError> {
17619    let algorithm = parsed.get("algorithm").ok_or_else(|| {
17620        SidecarError::InvalidState(format!("crypto.subtle.{op} missing algorithm"))
17621    })?;
17622    let name = javascript_crypto_subtle_algorithm_name(algorithm, &format!("crypto.subtle.{op}"))?;
17623    if name != "AES-GCM" {
17624        return Err(SidecarError::InvalidState(format!(
17625            "Unsupported subtle AES operation algorithm: {name}"
17626        )));
17627    }
17628    let key = javascript_crypto_subtle_key_raw(
17629        parsed
17630            .get("key")
17631            .ok_or_else(|| SidecarError::InvalidState(format!("crypto.subtle.{op} missing key")))?,
17632        &format!("crypto.subtle.{op} key"),
17633    )?;
17634    let iv = algorithm.get("iv").and_then(Value::as_str).ok_or_else(|| {
17635        SidecarError::InvalidState(format!("crypto.subtle.{op} AES-GCM missing iv"))
17636    })?;
17637    let iv = base64::engine::general_purpose::STANDARD
17638        .decode(iv)
17639        .map_err(|error| {
17640            SidecarError::InvalidState(format!("crypto.subtle.{op} iv base64: {error}"))
17641        })?;
17642    let data = parsed
17643        .get("data")
17644        .and_then(Value::as_str)
17645        .ok_or_else(|| SidecarError::InvalidState(format!("crypto.subtle.{op} missing data")))?;
17646    let mut data = base64::engine::general_purpose::STANDARD
17647        .decode(data)
17648        .map_err(|error| {
17649            SidecarError::InvalidState(format!("crypto.subtle.{op} data base64: {error}"))
17650        })?;
17651    let tag_len = javascript_crypto_subtle_aes_gcm_tag_len(algorithm)?;
17652    let mut options = Map::new();
17653    options.insert(String::from("authTagLength"), json!(tag_len));
17654    if let Some(additional_data) = algorithm.get("additionalData").and_then(Value::as_str) {
17655        options.insert(
17656            String::from("aad"),
17657            Value::String(additional_data.to_string()),
17658        );
17659    }
17660    let decrypt = op == "decrypt";
17661    if decrypt {
17662        if data.len() < tag_len {
17663            return Err(SidecarError::InvalidState(String::from(
17664                "crypto.subtle.decrypt AES-GCM data shorter than auth tag",
17665            )));
17666        }
17667        let auth_tag = data.split_off(data.len() - tag_len);
17668        options.insert(
17669            String::from("authTag"),
17670            Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
17671        );
17672    }
17673    let cipher_name = format!("aes-{}-gcm", key.len() * 8);
17674    let mut session = javascript_crypto_build_cipher_session(
17675        &cipher_name,
17676        &key,
17677        Some(&iv),
17678        decrypt,
17679        Some(&Value::Object(options)),
17680    )?;
17681    let mut output = javascript_crypto_cipher_update(&mut session, &data)?;
17682    let outcome = session.finalize().map_err(javascript_crypto_cipher_error)?;
17683    output.extend(outcome.data);
17684    if !decrypt {
17685        if let Some(auth_tag) = outcome.auth_tag {
17686            output.extend(auth_tag);
17687        }
17688    }
17689    Ok(Value::String(
17690        serde_json::to_string(&json!({
17691            "data": base64::engine::general_purpose::STANDARD.encode(output),
17692        }))
17693        .map_err(|error| {
17694            SidecarError::InvalidState(format!("serialize crypto.subtle {op}: {error}"))
17695        })?,
17696    ))
17697}
17698
17699fn javascript_crypto_subtle_aes_gcm_tag_len(algorithm: &Value) -> Result<usize, SidecarError> {
17700    let tag_bits = algorithm
17701        .get("tagLength")
17702        .and_then(Value::as_u64)
17703        .unwrap_or(128);
17704    if !tag_bits.is_multiple_of(8) {
17705        return Err(SidecarError::InvalidState(String::from(
17706            "crypto.subtle AES-GCM tagLength must be byte-aligned",
17707        )));
17708    }
17709    usize::try_from(tag_bits / 8).map_err(|_| {
17710        SidecarError::InvalidState(String::from("crypto.subtle AES-GCM tagLength too large"))
17711    })
17712}
17713
17714fn service_javascript_crypto_cipheriv_inner(
17715    request: &JavascriptSyncRpcRequest,
17716    decrypt: bool,
17717) -> Result<Value, SidecarError> {
17718    let label = if decrypt {
17719        "crypto.decipheriv"
17720    } else {
17721        "crypto.cipheriv"
17722    };
17723    let algorithm = javascript_sync_rpc_arg_str(&request.args, 0, &format!("{label} algorithm"))?;
17724    let key = javascript_sync_rpc_base64_arg(&request.args, 1, &format!("{label} key"))?;
17725    let iv = javascript_sync_rpc_base64_arg_optional(&request.args, 2, &format!("{label} iv"))?;
17726    let data = javascript_sync_rpc_base64_arg(&request.args, 3, &format!("{label} data"))?;
17727    let options =
17728        javascript_sync_rpc_json_arg_optional(&request.args, 4, &format!("{label} options"))?;
17729    let mut session = javascript_crypto_build_cipher_session(
17730        algorithm,
17731        &key,
17732        iv.as_deref(),
17733        decrypt,
17734        options.as_ref(),
17735    )?;
17736    let payload = javascript_crypto_cipher_update(&mut session, &data)?;
17737    let outcome = session.finalize().map_err(javascript_crypto_cipher_error)?;
17738    if decrypt {
17739        let mut output = payload;
17740        output.extend(outcome.data);
17741        return Ok(Value::String(
17742            base64::engine::general_purpose::STANDARD.encode(output),
17743        ));
17744    }
17745
17746    let mut response = Map::new();
17747    let mut encrypted = payload;
17748    encrypted.extend(outcome.data);
17749    response.insert(
17750        String::from("data"),
17751        Value::String(base64::engine::general_purpose::STANDARD.encode(encrypted)),
17752    );
17753    if let Some(auth_tag) = outcome.auth_tag {
17754        response.insert(
17755            String::from("authTag"),
17756            Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
17757        );
17758    }
17759    Ok(Value::String(serde_json::to_string(&response).map_err(
17760        |error| SidecarError::InvalidState(format!("serialize {label} response: {error}")),
17761    )?))
17762}
17763
17764fn javascript_sync_rpc_base64_arg_optional(
17765    args: &[Value],
17766    index: usize,
17767    label: &str,
17768) -> Result<Option<Vec<u8>>, SidecarError> {
17769    if args.get(index).is_none() || args[index].is_null() {
17770        return Ok(None);
17771    }
17772    javascript_sync_rpc_base64_arg(args, index, label).map(Some)
17773}
17774
17775fn javascript_sync_rpc_json_arg_optional(
17776    args: &[Value],
17777    index: usize,
17778    label: &str,
17779) -> Result<Option<Value>, SidecarError> {
17780    if args.get(index).is_none() || args[index].is_null() {
17781        return Ok(None);
17782    }
17783    let raw = javascript_sync_rpc_arg_str(args, index, label)?;
17784    serde_json::from_str(raw)
17785        .map(Some)
17786        .map_err(|error| SidecarError::InvalidState(format!("{label} must be valid JSON: {error}")))
17787}
17788
17789fn javascript_crypto_parse_direct_key_input(
17790    raw: &str,
17791    expected: Option<&str>,
17792    label: &str,
17793) -> Result<JavascriptDirectKeyInput, SidecarError> {
17794    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
17795        SidecarError::InvalidState(format!("{label} must be valid JSON: {error}"))
17796    })?;
17797    let padding = match parsed.as_object().and_then(|value| value.get("padding")) {
17798        Some(value) => javascript_crypto_padding_from_value(value)?,
17799        None => None,
17800    };
17801    Ok(JavascriptDirectKeyInput {
17802        key: javascript_crypto_parse_key_material_value(&parsed, expected, label)?,
17803        padding,
17804    })
17805}
17806
17807fn javascript_crypto_parse_key_material_value(
17808    value: &Value,
17809    expected: Option<&str>,
17810    label: &str,
17811) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
17812    if let Some(object) = value.as_object() {
17813        if object.get("__type").and_then(Value::as_str) == Some("keyObject") {
17814            let serialized = object.get("value").ok_or_else(|| {
17815                SidecarError::InvalidState(format!("{label} keyObject is missing a value"))
17816            })?;
17817            return javascript_crypto_parse_serialized_key_object(serialized, expected, label);
17818        }
17819        if object.contains_key("type") && (object.contains_key("pem") || object.contains_key("raw"))
17820        {
17821            return javascript_crypto_parse_serialized_key_object(value, expected, label);
17822        }
17823        if let Some(source) = object.get("key") {
17824            return javascript_crypto_parse_key_source(
17825                source,
17826                object.get("format").and_then(Value::as_str),
17827                object.get("type").and_then(Value::as_str),
17828                expected,
17829                label,
17830            );
17831        }
17832    }
17833    javascript_crypto_parse_key_source(value, None, None, expected, label)
17834}
17835
17836fn javascript_crypto_parse_key_source(
17837    source: &Value,
17838    format: Option<&str>,
17839    kind: Option<&str>,
17840    expected: Option<&str>,
17841    label: &str,
17842) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
17843    match source {
17844        Value::String(pem) => javascript_crypto_parse_key_from_pem(pem.as_bytes(), expected, label),
17845        Value::Object(object) if object.get("__type").and_then(Value::as_str) == Some("buffer") => {
17846            let data = javascript_crypto_decode_bridge_buffer(source, label)?;
17847            javascript_crypto_parse_key_from_bytes(&data, format, kind, expected, label)
17848        }
17849        Value::Object(_) => {
17850            if format == Some("jwk") {
17851                return Err(SidecarError::InvalidState(format!(
17852                    "{label} jwk inputs are not supported yet"
17853                )));
17854            }
17855            Err(SidecarError::InvalidState(format!(
17856                "{label} has an unsupported key shape"
17857            )))
17858        }
17859        _ => Err(SidecarError::InvalidState(format!(
17860            "{label} has an unsupported key value"
17861        ))),
17862    }
17863}
17864
17865fn javascript_crypto_parse_key_from_pem(
17866    pem: &[u8],
17867    expected: Option<&str>,
17868    label: &str,
17869) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
17870    match expected {
17871        Some("private") => PKey::private_key_from_pem(pem)
17872            .map(JavascriptCryptoKeyMaterial::Private)
17873            .map_err(|error| {
17874                SidecarError::InvalidState(format!("{label} private key is invalid: {error}"))
17875            }),
17876        Some("public") => PKey::public_key_from_pem(pem)
17877            .map(JavascriptCryptoKeyMaterial::Public)
17878            .map_err(|error| {
17879                SidecarError::InvalidState(format!("{label} public key is invalid: {error}"))
17880            }),
17881        _ => PKey::private_key_from_pem(pem)
17882            .map(JavascriptCryptoKeyMaterial::Private)
17883            .or_else(|_| PKey::public_key_from_pem(pem).map(JavascriptCryptoKeyMaterial::Public))
17884            .map_err(|error| {
17885                SidecarError::InvalidState(format!("{label} PEM key is invalid: {error}"))
17886            }),
17887    }
17888}
17889
17890fn javascript_crypto_parse_key_from_bytes(
17891    der: &[u8],
17892    format: Option<&str>,
17893    kind: Option<&str>,
17894    expected: Option<&str>,
17895    label: &str,
17896) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
17897    match (format.unwrap_or("der"), kind.or(expected)) {
17898        ("der", Some("pkcs8")) | ("der", Some("private")) => PKey::private_key_from_der(der)
17899            .map(JavascriptCryptoKeyMaterial::Private)
17900            .map_err(|error| {
17901                SidecarError::InvalidState(format!("{label} private key DER is invalid: {error}"))
17902            }),
17903        ("der", Some("spki")) | ("der", Some("public")) => PKey::public_key_from_der(der)
17904            .map(JavascriptCryptoKeyMaterial::Public)
17905            .map_err(|error| {
17906                SidecarError::InvalidState(format!("{label} public key DER is invalid: {error}"))
17907            }),
17908        _ => Err(SidecarError::InvalidState(format!(
17909            "{label} unsupported key bytes format"
17910        ))),
17911    }
17912}
17913
17914fn javascript_crypto_parse_serialized_key_object(
17915    value: &Value,
17916    expected: Option<&str>,
17917    label: &str,
17918) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
17919    let serialized: JavascriptSerializedSandboxKeyObject = serde_json::from_value(value.clone())
17920        .map_err(|error| {
17921            SidecarError::InvalidState(format!("{label} keyObject is invalid: {error}"))
17922        })?;
17923    match serialized.kind.as_str() {
17924        "secret" => {
17925            if expected == Some("public") || expected == Some("private") {
17926                return Err(SidecarError::InvalidState(format!(
17927                    "{label} expected an asymmetric key"
17928                )));
17929            }
17930            Ok(JavascriptCryptoKeyMaterial::Secret(
17931                base64::engine::general_purpose::STANDARD
17932                    .decode(serialized.raw.unwrap_or_default())
17933                    .map_err(|error| {
17934                        SidecarError::InvalidState(format!(
17935                            "{label} secret key contains invalid base64: {error}"
17936                        ))
17937                    })?,
17938            ))
17939        }
17940        "private" => {
17941            let pem = serialized.pem.ok_or_else(|| {
17942                SidecarError::InvalidState(format!("{label} private keyObject is missing pem"))
17943            })?;
17944            javascript_crypto_parse_key_from_pem(pem.as_bytes(), Some("private"), label)
17945        }
17946        "public" => {
17947            let pem = serialized.pem.ok_or_else(|| {
17948                SidecarError::InvalidState(format!("{label} public keyObject is missing pem"))
17949            })?;
17950            javascript_crypto_parse_key_from_pem(pem.as_bytes(), Some("public"), label)
17951        }
17952        other => Err(SidecarError::InvalidState(format!(
17953            "{label} has unsupported keyObject type {other}"
17954        ))),
17955    }
17956}
17957
17958fn javascript_crypto_expect_private_key(
17959    key: JavascriptCryptoKeyMaterial,
17960    label: &str,
17961) -> Result<PKey<Private>, SidecarError> {
17962    match key {
17963        JavascriptCryptoKeyMaterial::Private(key) => Ok(key),
17964        _ => Err(SidecarError::InvalidState(format!(
17965            "{label} requires a private key"
17966        ))),
17967    }
17968}
17969
17970fn javascript_crypto_expect_public_key(
17971    key: JavascriptCryptoKeyMaterial,
17972    label: &str,
17973) -> Result<PKey<Public>, SidecarError> {
17974    match key {
17975        JavascriptCryptoKeyMaterial::Public(key) => Ok(key),
17976        JavascriptCryptoKeyMaterial::Private(key) => {
17977            let pem = key
17978                .public_key_to_pem()
17979                .map_err(javascript_crypto_openssl_error)?;
17980            PKey::public_key_from_pem(&pem).map_err(javascript_crypto_openssl_error)
17981        }
17982        _ => Err(SidecarError::InvalidState(format!(
17983            "{label} requires a public key"
17984        ))),
17985    }
17986}
17987
17988fn javascript_crypto_new_signer<'a>(
17989    algorithm: Option<&'a str>,
17990    key: &'a PKey<Private>,
17991) -> Result<Signer<'a>, SidecarError> {
17992    if matches!(key.id(), PKeyId::ED25519 | PKeyId::ED448) || algorithm.is_none() {
17993        return Signer::new_without_digest(key).map_err(javascript_crypto_openssl_error);
17994    }
17995    Signer::new(
17996        javascript_crypto_message_digest_from_name(algorithm.ok_or_else(|| {
17997            SidecarError::InvalidState(String::from("crypto.sign requires a digest algorithm"))
17998        })?)?,
17999        key,
18000    )
18001    .map_err(javascript_crypto_openssl_error)
18002}
18003
18004fn javascript_crypto_new_verifier<'a>(
18005    algorithm: Option<&'a str>,
18006    key: &'a PKey<Public>,
18007) -> Result<Verifier<'a>, SidecarError> {
18008    if matches!(key.id(), PKeyId::ED25519 | PKeyId::ED448) || algorithm.is_none() {
18009        return Verifier::new_without_digest(key).map_err(javascript_crypto_openssl_error);
18010    }
18011    Verifier::new(
18012        javascript_crypto_message_digest_from_name(algorithm.ok_or_else(|| {
18013            SidecarError::InvalidState(String::from("crypto.verify requires a digest algorithm"))
18014        })?)?,
18015        key,
18016    )
18017    .map_err(javascript_crypto_openssl_error)
18018}
18019
18020fn javascript_crypto_message_digest_from_name(name: &str) -> Result<MessageDigest, SidecarError> {
18021    match name.trim().to_ascii_lowercase().replace('-', "").as_str() {
18022        "md5" => Ok(MessageDigest::md5()),
18023        "sha1" => Ok(MessageDigest::sha1()),
18024        "sha256" => Ok(MessageDigest::sha256()),
18025        "sha384" => Ok(MessageDigest::sha384()),
18026        "sha512" => Ok(MessageDigest::sha512()),
18027        other => Err(SidecarError::InvalidState(format!(
18028            "unsupported crypto digest algorithm {other}"
18029        ))),
18030    }
18031}
18032
18033fn javascript_crypto_padding_from_value(value: &Value) -> Result<Option<Padding>, SidecarError> {
18034    let Some(number) = value.as_i64() else {
18035        return Ok(None);
18036    };
18037    let padding = match number {
18038        1 => Padding::PKCS1,
18039        3 => Padding::NONE,
18040        4 => Padding::PKCS1_OAEP,
18041        6 => Padding::PKCS1_PSS,
18042        other => {
18043            return Err(SidecarError::InvalidState(format!(
18044                "unsupported RSA padding constant {other}"
18045            )));
18046        }
18047    };
18048    Ok(Some(padding))
18049}
18050
18051fn javascript_crypto_decode_bridge_buffer(
18052    value: &Value,
18053    label: &str,
18054) -> Result<Vec<u8>, SidecarError> {
18055    decode_bridge_buffer_value(value)
18056        .map_err(|error| SidecarError::InvalidState(format!("{label} {error}")))
18057}
18058
18059fn javascript_crypto_serialize_sandbox_key_object(
18060    key: &JavascriptCryptoKeyMaterial,
18061) -> Result<Value, SidecarError> {
18062    let serialized = match key {
18063        JavascriptCryptoKeyMaterial::Private(key) => JavascriptSerializedSandboxKeyObject {
18064            kind: String::from("private"),
18065            pem: Some(
18066                String::from_utf8(
18067                    key.private_key_to_pem_pkcs8()
18068                        .map_err(javascript_crypto_openssl_error)?,
18069                )
18070                .map_err(|error| {
18071                    SidecarError::InvalidState(format!("private key PEM is not utf8: {error}"))
18072                })?,
18073            ),
18074            raw: None,
18075            asymmetric_key_type: javascript_crypto_pkey_type_name(key.id()),
18076            asymmetric_key_details: None,
18077            jwk: None,
18078        },
18079        JavascriptCryptoKeyMaterial::Public(key) => JavascriptSerializedSandboxKeyObject {
18080            kind: String::from("public"),
18081            pem: Some(
18082                String::from_utf8(
18083                    key.public_key_to_pem()
18084                        .map_err(javascript_crypto_openssl_error)?,
18085                )
18086                .map_err(|error| {
18087                    SidecarError::InvalidState(format!("public key PEM is not utf8: {error}"))
18088                })?,
18089            ),
18090            raw: None,
18091            asymmetric_key_type: javascript_crypto_pkey_type_name(key.id()),
18092            asymmetric_key_details: None,
18093            jwk: None,
18094        },
18095        JavascriptCryptoKeyMaterial::Secret(raw) => JavascriptSerializedSandboxKeyObject {
18096            kind: String::from("secret"),
18097            pem: None,
18098            raw: Some(base64::engine::general_purpose::STANDARD.encode(raw)),
18099            asymmetric_key_type: None,
18100            asymmetric_key_details: None,
18101            jwk: None,
18102        },
18103    };
18104    serde_json::to_value(serialized)
18105        .map_err(|error| SidecarError::InvalidState(format!("serialize key object: {error}")))
18106}
18107
18108fn javascript_crypto_pkey_type_name(id: PKeyId) -> Option<String> {
18109    match id {
18110        PKeyId::RSA => Some(String::from("rsa")),
18111        PKeyId::EC => Some(String::from("ec")),
18112        PKeyId::ED25519 => Some(String::from("ed25519")),
18113        PKeyId::ED448 => Some(String::from("ed448")),
18114        PKeyId::X25519 => Some(String::from("x25519")),
18115        PKeyId::X448 => Some(String::from("x448")),
18116        PKeyId::DH => Some(String::from("dh")),
18117        _ => None,
18118    }
18119}
18120
18121fn javascript_crypto_rsa_output_size(
18122    key: &JavascriptCryptoKeyMaterial,
18123) -> Result<usize, SidecarError> {
18124    match key {
18125        JavascriptCryptoKeyMaterial::Private(key) => key
18126            .rsa()
18127            .map(|rsa| rsa.size() as usize)
18128            .map_err(javascript_crypto_openssl_error),
18129        JavascriptCryptoKeyMaterial::Public(key) => key
18130            .rsa()
18131            .map(|rsa| rsa.size() as usize)
18132            .map_err(javascript_crypto_openssl_error),
18133        JavascriptCryptoKeyMaterial::Secret(_) => Err(SidecarError::InvalidState(String::from(
18134            "RSA operations require an asymmetric key",
18135        ))),
18136    }
18137}
18138
18139fn javascript_crypto_parse_serialized_options_arg(
18140    args: &[Value],
18141    index: usize,
18142    label: &str,
18143) -> Result<Option<Value>, SidecarError> {
18144    let Some(raw) = args.get(index).and_then(Value::as_str) else {
18145        return Ok(None);
18146    };
18147    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
18148        SidecarError::InvalidState(format!("{label} must be valid JSON: {error}"))
18149    })?;
18150    if parsed.get("hasOptions").and_then(Value::as_bool) == Some(true) {
18151        Ok(parsed.get("options").cloned())
18152    } else {
18153        Ok(None)
18154    }
18155}
18156
18157fn javascript_crypto_u32_from_bridge_value(
18158    value: &Value,
18159    label: &str,
18160) -> Result<u32, SidecarError> {
18161    if let Some(number) = value.as_u64() {
18162        return u32::try_from(number)
18163            .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")));
18164    }
18165    let bytes = javascript_crypto_decode_bridge_buffer(value, label)?;
18166    if bytes.len() > 4 {
18167        return Err(SidecarError::InvalidState(format!(
18168            "{label} buffer is too large for u32"
18169        )));
18170    }
18171    Ok(bytes
18172        .into_iter()
18173        .fold(0_u32, |acc, byte| (acc << 8) | u32::from(byte)))
18174}
18175
18176fn javascript_crypto_bignum_from_bridge_value(
18177    value: &Value,
18178    label: &str,
18179) -> Result<BigNum, SidecarError> {
18180    if let Some(object) = value.as_object() {
18181        if object.get("__type").and_then(Value::as_str) == Some("bigint") {
18182            let decimal = object.get("value").and_then(Value::as_str).ok_or_else(|| {
18183                SidecarError::InvalidState(format!("{label} bigint is missing a value"))
18184            })?;
18185            return BigNum::from_dec_str(decimal).map_err(javascript_crypto_openssl_error);
18186        }
18187    }
18188    let bytes = javascript_crypto_decode_bridge_buffer(value, label)?;
18189    BigNum::from_slice(&bytes).map_err(javascript_crypto_openssl_error)
18190}
18191
18192fn javascript_crypto_curve_nid(name: &str) -> Result<Nid, SidecarError> {
18193    match name {
18194        "prime256v1" | "P-256" => Ok(Nid::X9_62_PRIME256V1),
18195        "secp384r1" | "P-384" => Ok(Nid::SECP384R1),
18196        "secp521r1" | "P-521" => Ok(Nid::SECP521R1),
18197        "secp256k1" => Ok(Nid::SECP256K1),
18198        other => Err(SidecarError::InvalidState(format!(
18199            "unsupported EC curve {other}"
18200        ))),
18201    }
18202}
18203
18204fn javascript_crypto_named_dh_group(name: &str) -> Result<Dh<Params>, SidecarError> {
18205    match name {
18206        "modp2" => Dh::get_1024_160().map_err(javascript_crypto_openssl_error),
18207        "modp14" | "modp15" | "modp16" | "modp17" | "modp18" => {
18208            Dh::get_2048_256().map_err(javascript_crypto_openssl_error)
18209        }
18210        other => Err(SidecarError::InvalidState(format!(
18211            "unsupported Diffie-Hellman group {other}"
18212        ))),
18213    }
18214}
18215
18216fn javascript_crypto_clone_dh_params(params: &Dh<Params>) -> Result<Dh<Params>, SidecarError> {
18217    Dh::from_pqg(
18218        params
18219            .prime_p()
18220            .to_owned()
18221            .map_err(javascript_crypto_openssl_error)?,
18222        params
18223            .prime_q()
18224            .map(|value| value.to_owned().map_err(javascript_crypto_openssl_error))
18225            .transpose()?,
18226        params
18227            .generator()
18228            .to_owned()
18229            .map_err(javascript_crypto_openssl_error)?,
18230    )
18231    .map_err(javascript_crypto_openssl_error)
18232}
18233
18234fn javascript_crypto_build_dh_params(args: &[Value]) -> Result<Dh<Params>, SidecarError> {
18235    let Some(first) = args.first() else {
18236        return Err(SidecarError::InvalidState(String::from(
18237            "Diffie-Hellman session args are required",
18238        )));
18239    };
18240    if let Some(bits) = first.as_u64() {
18241        let generator = args
18242            .get(1)
18243            .map(|value| javascript_crypto_u32_from_bridge_value(value, "Diffie-Hellman generator"))
18244            .transpose()?
18245            .unwrap_or(2);
18246        return Dh::generate_params(bits as u32, generator)
18247            .map_err(javascript_crypto_openssl_error);
18248    }
18249    let prime = javascript_crypto_bignum_from_bridge_value(first, "Diffie-Hellman prime")?;
18250    let generator = args
18251        .get(1)
18252        .map(|value| javascript_crypto_bignum_from_bridge_value(value, "Diffie-Hellman generator"))
18253        .transpose()?
18254        .unwrap_or(BigNum::from_u32(2).map_err(javascript_crypto_openssl_error)?);
18255    Dh::from_pqg(prime, None, generator).map_err(javascript_crypto_openssl_error)
18256}
18257
18258fn javascript_crypto_call_dh_session(
18259    session: &mut ActiveDhSession,
18260    method: &str,
18261    args: &[Value],
18262) -> Result<(Value, bool), SidecarError> {
18263    match method {
18264        "verifyError" => Ok((Value::Null, false)),
18265        "generateKeys" => {
18266            if session.key_pair.is_none() {
18267                session.key_pair = Some(
18268                    javascript_crypto_clone_dh_params(&session.params)?
18269                        .generate_key()
18270                        .map_err(javascript_crypto_openssl_error)?,
18271                );
18272            }
18273            let public = session
18274                .key_pair
18275                .as_ref()
18276                .expect("dh key pair")
18277                .public_key()
18278                .to_vec();
18279            Ok((javascript_crypto_bridge_buffer_value(&public), true))
18280        }
18281        "computeSecret" => {
18282            if session.key_pair.is_none() {
18283                session.key_pair = Some(
18284                    javascript_crypto_clone_dh_params(&session.params)?
18285                        .generate_key()
18286                        .map_err(javascript_crypto_openssl_error)?,
18287                );
18288            }
18289            let peer = javascript_crypto_bignum_from_bridge_value(
18290                args.first().ok_or_else(|| {
18291                    SidecarError::InvalidState(String::from(
18292                        "computeSecret requires peer public key",
18293                    ))
18294                })?,
18295                "Diffie-Hellman peer public key",
18296            )?;
18297            let private_key = session
18298                .key_pair
18299                .as_ref()
18300                .expect("dh key pair")
18301                .private_key();
18302            let mut secret = BigNum::new().map_err(javascript_crypto_openssl_error)?;
18303            let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
18304            secret
18305                .mod_exp(&peer, private_key, session.params.prime_p(), &mut ctx)
18306                .map_err(javascript_crypto_openssl_error)?;
18307            Ok((
18308                javascript_crypto_bridge_buffer_value(
18309                    &secret
18310                        .to_vec_padded(session.params.prime_p().num_bytes())
18311                        .map_err(javascript_crypto_openssl_error)?,
18312                ),
18313                true,
18314            ))
18315        }
18316        "getPrime" => Ok((
18317            javascript_crypto_bridge_buffer_value(&session.params.prime_p().to_vec()),
18318            true,
18319        )),
18320        "getGenerator" => Ok((
18321            javascript_crypto_bridge_buffer_value(&session.params.generator().to_vec()),
18322            true,
18323        )),
18324        "getPublicKey" => {
18325            if session.key_pair.is_none() {
18326                session.key_pair = Some(
18327                    javascript_crypto_clone_dh_params(&session.params)?
18328                        .generate_key()
18329                        .map_err(javascript_crypto_openssl_error)?,
18330                );
18331            }
18332            Ok((
18333                javascript_crypto_bridge_buffer_value(
18334                    &session
18335                        .key_pair
18336                        .as_ref()
18337                        .expect("dh key pair")
18338                        .public_key()
18339                        .to_vec(),
18340                ),
18341                true,
18342            ))
18343        }
18344        "getPrivateKey" => {
18345            if session.key_pair.is_none() {
18346                session.key_pair = Some(
18347                    javascript_crypto_clone_dh_params(&session.params)?
18348                        .generate_key()
18349                        .map_err(javascript_crypto_openssl_error)?,
18350                );
18351            }
18352            Ok((
18353                javascript_crypto_bridge_buffer_value(
18354                    &session
18355                        .key_pair
18356                        .as_ref()
18357                        .expect("dh key pair")
18358                        .private_key()
18359                        .to_vec(),
18360                ),
18361                true,
18362            ))
18363        }
18364        "setPrivateKey" => {
18365            let private_key = javascript_crypto_bignum_from_bridge_value(
18366                args.first().ok_or_else(|| {
18367                    SidecarError::InvalidState(String::from("setPrivateKey requires private key"))
18368                })?,
18369                "Diffie-Hellman private key",
18370            )?;
18371            let mut public_key = BigNum::new().map_err(javascript_crypto_openssl_error)?;
18372            let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
18373            public_key
18374                .mod_exp(
18375                    session.params.generator(),
18376                    &private_key,
18377                    session.params.prime_p(),
18378                    &mut ctx,
18379                )
18380                .map_err(javascript_crypto_openssl_error)?;
18381            session.key_pair = Some(
18382                javascript_crypto_clone_dh_params(&session.params)?
18383                    .set_key(public_key, private_key)
18384                    .map_err(javascript_crypto_openssl_error)?,
18385            );
18386            Ok((Value::Null, false))
18387        }
18388        "setPublicKey" => {
18389            let public_key = javascript_crypto_bignum_from_bridge_value(
18390                args.first().ok_or_else(|| {
18391                    SidecarError::InvalidState(String::from("setPublicKey requires public key"))
18392                })?,
18393                "Diffie-Hellman public key",
18394            )?;
18395            let private_key = session
18396                .key_pair
18397                .as_ref()
18398                .ok_or_else(|| {
18399                    SidecarError::InvalidState(String::from(
18400                        "setPublicKey requires private key to be set first",
18401                    ))
18402                })?
18403                .private_key()
18404                .to_owned()
18405                .map_err(javascript_crypto_openssl_error)?;
18406            session.key_pair = Some(
18407                javascript_crypto_clone_dh_params(&session.params)?
18408                    .set_key(public_key, private_key)
18409                    .map_err(javascript_crypto_openssl_error)?,
18410            );
18411            Ok((Value::Null, false))
18412        }
18413        other => Err(SidecarError::InvalidState(format!(
18414            "Unsupported Diffie-Hellman method: {other}"
18415        ))),
18416    }
18417}
18418
18419fn javascript_crypto_call_ecdh_session(
18420    session: &mut ActiveEcdhSession,
18421    method: &str,
18422    args: &[Value],
18423) -> Result<(Value, bool), SidecarError> {
18424    let nid = javascript_crypto_curve_nid(&session.curve)?;
18425    let group = EcGroup::from_curve_name(nid).map_err(javascript_crypto_openssl_error)?;
18426    match method {
18427        "verifyError" => Ok((Value::Null, false)),
18428        "generateKeys" => {
18429            if session.key_pair.is_none() {
18430                session.key_pair =
18431                    Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
18432            }
18433            let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
18434            let bytes = session
18435                .key_pair
18436                .as_ref()
18437                .expect("ecdh key pair")
18438                .public_key()
18439                .to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx)
18440                .map_err(javascript_crypto_openssl_error)?;
18441            Ok((javascript_crypto_bridge_buffer_value(&bytes), true))
18442        }
18443        "computeSecret" => {
18444            if session.key_pair.is_none() {
18445                session.key_pair =
18446                    Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
18447            }
18448            let peer_bytes = javascript_crypto_decode_bridge_buffer(
18449                args.first().ok_or_else(|| {
18450                    SidecarError::InvalidState(String::from(
18451                        "computeSecret requires peer public key",
18452                    ))
18453                })?,
18454                "ECDH peer public key",
18455            )?;
18456            let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
18457            let peer_point = EcPoint::from_bytes(&group, &peer_bytes, &mut ctx)
18458                .map_err(javascript_crypto_openssl_error)?;
18459            let peer_key = EcKey::from_public_key(&group, &peer_point)
18460                .map_err(javascript_crypto_openssl_error)?;
18461            let private =
18462                PKey::from_ec_key(session.key_pair.as_ref().expect("ecdh key pair").to_owned())
18463                    .map_err(javascript_crypto_openssl_error)?;
18464            let peer = PKey::from_ec_key(peer_key).map_err(javascript_crypto_openssl_error)?;
18465            let mut deriver = Deriver::new(&private).map_err(javascript_crypto_openssl_error)?;
18466            deriver
18467                .set_peer(&peer)
18468                .map_err(javascript_crypto_openssl_error)?;
18469            let secret = deriver
18470                .derive_to_vec()
18471                .map_err(javascript_crypto_openssl_error)?;
18472            Ok((javascript_crypto_bridge_buffer_value(&secret), true))
18473        }
18474        "getPublicKey" => {
18475            if session.key_pair.is_none() {
18476                session.key_pair =
18477                    Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
18478            }
18479            let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
18480            let bytes = session
18481                .key_pair
18482                .as_ref()
18483                .expect("ecdh key pair")
18484                .public_key()
18485                .to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx)
18486                .map_err(javascript_crypto_openssl_error)?;
18487            Ok((javascript_crypto_bridge_buffer_value(&bytes), true))
18488        }
18489        "getPrivateKey" => {
18490            if session.key_pair.is_none() {
18491                session.key_pair =
18492                    Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
18493            }
18494            Ok((
18495                javascript_crypto_bridge_buffer_value(
18496                    &session
18497                        .key_pair
18498                        .as_ref()
18499                        .expect("ecdh key pair")
18500                        .private_key()
18501                        .to_vec(),
18502                ),
18503                true,
18504            ))
18505        }
18506        "setPrivateKey" => {
18507            let private_key = javascript_crypto_bignum_from_bridge_value(
18508                args.first().ok_or_else(|| {
18509                    SidecarError::InvalidState(String::from("setPrivateKey requires private key"))
18510                })?,
18511                "ECDH private key",
18512            )?;
18513            let ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
18514            let mut public_key = EcPoint::new(&group).map_err(javascript_crypto_openssl_error)?;
18515            public_key
18516                .mul_generator(&group, &private_key, &ctx)
18517                .map_err(javascript_crypto_openssl_error)?;
18518            session.key_pair = Some(
18519                EcKey::from_private_components(&group, &private_key, &public_key)
18520                    .map_err(javascript_crypto_openssl_error)?,
18521            );
18522            Ok((Value::Null, false))
18523        }
18524        "setPublicKey" => {
18525            let public_key_bytes = javascript_crypto_decode_bridge_buffer(
18526                args.first().ok_or_else(|| {
18527                    SidecarError::InvalidState(String::from("setPublicKey requires public key"))
18528                })?,
18529                "ECDH public key",
18530            )?;
18531            let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
18532            let public_key = EcPoint::from_bytes(&group, &public_key_bytes, &mut ctx)
18533                .map_err(javascript_crypto_openssl_error)?;
18534            let private_key = session
18535                .key_pair
18536                .as_ref()
18537                .ok_or_else(|| {
18538                    SidecarError::InvalidState(String::from(
18539                        "setPublicKey requires private key to be set first",
18540                    ))
18541                })?
18542                .private_key()
18543                .to_owned()
18544                .map_err(javascript_crypto_openssl_error)?;
18545            session.key_pair = Some(
18546                EcKey::from_private_components(&group, &private_key, &public_key)
18547                    .map_err(javascript_crypto_openssl_error)?,
18548            );
18549            Ok((Value::Null, false))
18550        }
18551        other => Err(SidecarError::InvalidState(format!(
18552            "Unsupported Diffie-Hellman method: {other}"
18553        ))),
18554    }
18555}
18556
18557fn javascript_crypto_serialize_encoded_key_value_public(
18558    key: &PKey<Public>,
18559    encoding: Option<&Value>,
18560) -> Result<Value, SidecarError> {
18561    if let Some(encoding) = encoding {
18562        let format = encoding
18563            .get("format")
18564            .and_then(Value::as_str)
18565            .unwrap_or("pem");
18566        return Ok(match format {
18567            "der" => json!({
18568                "kind": "buffer",
18569                "value": base64::engine::general_purpose::STANDARD
18570                    .encode(key.public_key_to_der().map_err(javascript_crypto_openssl_error)?),
18571            }),
18572            _ => json!({
18573                "kind": "string",
18574                "value": String::from_utf8(
18575                    key.public_key_to_pem().map_err(javascript_crypto_openssl_error)?,
18576                )
18577                .map_err(|error| SidecarError::InvalidState(format!("public key PEM utf8: {error}")))?,
18578            }),
18579        });
18580    }
18581    javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Public(
18582        key.to_owned(),
18583    ))
18584}
18585
18586fn javascript_crypto_serialize_encoded_key_value_private(
18587    key: &PKey<Private>,
18588    encoding: Option<&Value>,
18589) -> Result<Value, SidecarError> {
18590    if let Some(encoding) = encoding {
18591        let format = encoding
18592            .get("format")
18593            .and_then(Value::as_str)
18594            .unwrap_or("pem");
18595        return Ok(match format {
18596            "der" => json!({
18597                "kind": "buffer",
18598                "value": base64::engine::general_purpose::STANDARD
18599                    .encode(key.private_key_to_der().map_err(javascript_crypto_openssl_error)?),
18600            }),
18601            _ => json!({
18602                "kind": "string",
18603                "value": String::from_utf8(
18604                    key.private_key_to_pem_pkcs8().map_err(javascript_crypto_openssl_error)?,
18605                )
18606                .map_err(|error| SidecarError::InvalidState(format!("private key PEM utf8: {error}")))?,
18607            }),
18608        });
18609    }
18610    javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Private(
18611        key.to_owned(),
18612    ))
18613}
18614
18615fn javascript_crypto_bridge_buffer_value(bytes: &[u8]) -> Value {
18616    bridge_buffer_value(bytes)
18617}
18618
18619fn javascript_crypto_cipher_error(error: AesCipherError) -> SidecarError {
18620    SidecarError::InvalidState(error.0)
18621}
18622
18623fn javascript_crypto_decode_cipher_option_b64(
18624    options: Option<&Value>,
18625    field: &str,
18626) -> Result<Option<Vec<u8>>, SidecarError> {
18627    let Some(encoded) = options
18628        .and_then(|value| value.get(field))
18629        .and_then(Value::as_str)
18630    else {
18631        return Ok(None);
18632    };
18633    base64::engine::general_purpose::STANDARD
18634        .decode(encoded)
18635        .map(Some)
18636        .map_err(|error| {
18637            SidecarError::InvalidState(format!("cipher {field} contains invalid base64: {error}"))
18638        })
18639}
18640
18641fn javascript_crypto_build_cipher_session(
18642    algorithm: &str,
18643    key: &[u8],
18644    iv: Option<&[u8]>,
18645    decrypt: bool,
18646    options: Option<&Value>,
18647) -> Result<StreamCipherSession, SidecarError> {
18648    let pad = options
18649        .and_then(|value| value.get("autoPadding"))
18650        .and_then(Value::as_bool)
18651        .unwrap_or(true);
18652    let aad = if javascript_crypto_is_aead(algorithm) {
18653        javascript_crypto_decode_cipher_option_b64(options, "aad")?
18654    } else {
18655        None
18656    };
18657    let auth_tag = if decrypt && javascript_crypto_is_aead(algorithm) {
18658        javascript_crypto_decode_cipher_option_b64(options, "authTag")?
18659    } else {
18660        None
18661    };
18662    let tag_len = javascript_crypto_requested_aead_tag_len(algorithm, options)?;
18663    StreamCipherSession::new(
18664        algorithm,
18665        key,
18666        iv,
18667        decrypt,
18668        pad,
18669        aad.as_deref(),
18670        auth_tag.as_deref(),
18671        tag_len,
18672    )
18673    .map_err(javascript_crypto_cipher_error)
18674}
18675
18676fn javascript_crypto_requested_aead_tag_len(
18677    algorithm: &str,
18678    options: Option<&Value>,
18679) -> Result<usize, SidecarError> {
18680    if !javascript_crypto_is_aead(algorithm) {
18681        return Ok(0);
18682    }
18683    let requested = options
18684        .and_then(|value| value.get("authTagLength"))
18685        .and_then(Value::as_u64)
18686        .unwrap_or(javascript_crypto_aead_tag_len(algorithm) as u64);
18687    usize::try_from(requested).map_err(|_| {
18688        SidecarError::InvalidState(String::from("cipher authTagLength must fit within usize"))
18689    })
18690}
18691
18692fn javascript_crypto_cipher_update(
18693    session: &mut StreamCipherSession,
18694    data: &[u8],
18695) -> Result<Vec<u8>, SidecarError> {
18696    session.update(data).map_err(javascript_crypto_cipher_error)
18697}
18698
18699fn javascript_crypto_is_aead(algorithm: &str) -> bool {
18700    crate::crypto_cipher::is_aead(algorithm)
18701}
18702
18703fn javascript_crypto_aead_tag_len(_algorithm: &str) -> usize {
18704    crate::crypto_cipher::default_aead_tag_len()
18705}
18706
18707fn javascript_crypto_openssl_error(error: openssl::error::ErrorStack) -> SidecarError {
18708    SidecarError::Execution(format!("crypto operation failed: {error}"))
18709}
18710
18711fn service_javascript_kernel_stdin_sync_rpc(
18712    kernel: &mut SidecarKernel,
18713    process: &mut ActiveProcess,
18714    request: &JavascriptSyncRpcRequest,
18715) -> Result<Value, SidecarError> {
18716    let (max_bytes, timeout_ms) = parse_kernel_stdin_read_args(request)?;
18717    kernel_stdin_read_response(
18718        kernel,
18719        process.kernel_pid,
18720        max_bytes,
18721        Duration::from_millis(timeout_ms),
18722    )
18723}
18724
18725/// Parse `__kernel_stdin_read` args: (max bytes, requested timeout ms).
18726pub(crate) fn parse_kernel_stdin_read_args(
18727    request: &JavascriptSyncRpcRequest,
18728) -> Result<(usize, u64), SidecarError> {
18729    let max_bytes =
18730        javascript_sync_rpc_arg_u64_optional(&request.args, 0, "__kernel_stdin_read max bytes")?
18731            .map(|value| value.clamp(1, DEFAULT_KERNEL_STDIN_READ_MAX_BYTES as u64) as usize)
18732            .unwrap_or(DEFAULT_KERNEL_STDIN_READ_MAX_BYTES);
18733    let timeout_ms =
18734        javascript_sync_rpc_arg_u64_optional(&request.args, 1, "__kernel_stdin_read timeout ms")?
18735            .unwrap_or(DEFAULT_KERNEL_STDIN_READ_TIMEOUT_MS);
18736    Ok((max_bytes, timeout_ms))
18737}
18738
18739/// One bounded stdin read against the kernel. `Duration::ZERO` = non-blocking
18740/// probe (deferred servicing re-checks readiness with this before replying).
18741pub(crate) fn kernel_stdin_read_response(
18742    kernel: &mut SidecarKernel,
18743    kernel_pid: u32,
18744    max_bytes: usize,
18745    timeout: Duration,
18746) -> Result<Value, SidecarError> {
18747    match kernel
18748        .fd_read_with_timeout_result(
18749            EXECUTION_DRIVER_NAME,
18750            kernel_pid,
18751            0,
18752            max_bytes,
18753            Some(timeout),
18754        )
18755        .map_err(kernel_error)
18756    {
18757        Ok(Some(chunk)) if !chunk.is_empty() => Ok(json!({
18758            "dataBase64": base64::engine::general_purpose::STANDARD.encode(chunk),
18759        })),
18760        Ok(Some(_)) => Ok(Value::Null),
18761        Ok(None) => Ok(json!({
18762            "done": true,
18763        })),
18764        Err(SidecarError::Kernel(error)) if error.starts_with("EAGAIN:") => Ok(Value::Null),
18765        Err(error) => Err(error),
18766    }
18767}
18768
18769fn service_javascript_pty_set_raw_mode_sync_rpc(
18770    kernel: &mut SidecarKernel,
18771    process: &mut ActiveProcess,
18772    request: &JavascriptSyncRpcRequest,
18773) -> Result<Value, SidecarError> {
18774    let enabled = javascript_sync_rpc_arg_bool(&request.args, 0, "__pty_set_raw_mode enabled")?;
18775    kernel
18776        .pty_set_discipline(
18777            EXECUTION_DRIVER_NAME,
18778            process.kernel_pid,
18779            0,
18780            // Match cfmakeraw: raw mode also disables output post-processing
18781            // (OPOST/ONLCR) so a full-screen app (vim) that drives its own
18782            // cursor/CRLF is not mangled by the line discipline; cooked mode
18783            // re-enables it so the line shell gets LF->CRLF.
18784            LineDisciplineConfig {
18785                canonical: Some(!enabled),
18786                echo: Some(!enabled),
18787                isig: Some(!enabled),
18788                opost: Some(!enabled),
18789                onlcr: Some(!enabled),
18790            },
18791        )
18792        .map_err(kernel_error)?;
18793    Ok(Value::Null)
18794}
18795
18796fn service_javascript_kernel_isatty_sync_rpc(
18797    kernel: &mut SidecarKernel,
18798    process: &ActiveProcess,
18799    request: &JavascriptSyncRpcRequest,
18800) -> Result<Value, SidecarError> {
18801    let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_isatty fd")?;
18802    let is_tty = kernel
18803        .isatty(EXECUTION_DRIVER_NAME, process.kernel_pid, fd)
18804        .map_err(kernel_error)?;
18805    Ok(json!(is_tty))
18806}
18807
18808fn service_javascript_kernel_tty_size_sync_rpc(
18809    kernel: &mut SidecarKernel,
18810    process: &ActiveProcess,
18811    request: &JavascriptSyncRpcRequest,
18812) -> Result<Value, SidecarError> {
18813    let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_tty_size fd")?;
18814    let size = kernel
18815        .pty_window_size(EXECUTION_DRIVER_NAME, process.kernel_pid, fd)
18816        .map_err(kernel_error)?;
18817    Ok(json!({
18818        "cols": size.cols,
18819        "rows": size.rows,
18820    }))
18821}
18822
18823// TODO(double-print): The recovered pre-convergence work (94e935d0) added a
18824// guard here because in THAT model the sidecar owned a per-process
18825// `tty_master_fd`, drained the PTY master into `Stdout`/`ProcessOutput` events,
18826// and therefore had to suppress a TTY child's own `process_output` event to
18827// avoid surfacing the same bytes twice (once via the child's Stdout event, once
18828// via the drained master).
18829//
18830// The current "real terminal client" model on main is different and does NOT
18831// exhibit that bug as far as I can determine:
18832//   * `tty_master_fd` here is only a LOCAL used to derive
18833//     `kernel_stdin_writer_fd`; there is no `ActiveProcess::tty_master_fd`
18834//     field and no `drain_tty_master_output` — the sidecar never surfaces the
18835//     PTY master as host `ProcessOutput` at all. The in-guest terminal client
18836//     reads the master itself.
18837//   * A wasm/python CHILD of a cooked-TTY JS shell is spawned WITHOUT the
18838//     parent's PTY slave dup2'd onto its fds (the child spawn path does no
18839//     `open_pty`/slave dup2), so the child's stdout does not flow through the
18840//     parent's master. It is surfaced exactly once, via the `Stdout` event
18841//     queued below.
18842// So there is no second surfacing path to suppress, and porting 94e935d0's
18843// master-drain guard would guard against a drain that no longer exists.
18844//
18845// UNRESOLVED: I could not exercise the live JS-shell-spawns-wasm-child scenario
18846// end-to-end to positively confirm the host sees exactly one copy; the analysis
18847// above is from the code paths only. If a real double-print is later observed
18848// for a TTY child, the fix belongs here (or at the `ProcessOutput` emission in
18849// `event_frame_for_execution_event`): suppress this child `Stdout` event when
18850// the same bytes already reach the host through a PTY master owned by an
18851// ancestor. Deliberately NOT fabricating that guard now, per instructions.
18852/// A TTY in raw mode (no echo, no canonical) — like cfmakeraw. Full-screen apps
18853/// (vim) run raw and drive their own cursor/CRLF, so their output must be passed
18854/// through untouched, NOT round-tripped through the slave->process_output->master
18855/// path (which buffers/reorders escape sequences and corrupts the screen).
18856fn tty_is_raw_mode(kernel: &SidecarKernel, process: &ActiveProcess) -> bool {
18857    let Some(master_fd) = process.tty_master_fd else {
18858        return false;
18859    };
18860    match kernel.tcgetattr(EXECUTION_DRIVER_NAME, process.kernel_pid, master_fd) {
18861        Ok(termios) => !termios.echo && !termios.icanon,
18862        Err(_) => false,
18863    }
18864}
18865
18866/// Non-blocking drain of the PTY master output buffer for a TTY process.
18867///
18868/// For a TTY (PTY-backed) process the master output buffer is the single
18869/// ordered output stream: it carries cooked-mode echo plus ONLCR-processed
18870/// guest output, already merged FIFO. A zero-timeout master read returns the
18871/// whole current buffer (so echo and guest output stay grouped) or EAGAIN when
18872/// empty, which is mapped to `Ok(None)`. Returns `Ok(None)` for non-TTY
18873/// processes (no master fd).
18874pub(crate) fn drain_tty_master_output(
18875    kernel: &mut SidecarKernel,
18876    process: &mut ActiveProcess,
18877) -> Result<Option<Vec<u8>>, SidecarError> {
18878    let Some(master_fd) = process.tty_master_fd else {
18879        return Ok(None);
18880    };
18881    match kernel.fd_read_with_timeout_result(
18882        EXECUTION_DRIVER_NAME,
18883        process.kernel_pid,
18884        master_fd,
18885        MAX_PTY_BUFFER_BYTES,
18886        Some(Duration::ZERO),
18887    ) {
18888        Ok(Some(bytes)) if !bytes.is_empty() => Ok(Some(bytes)),
18889        Ok(_) => Ok(None),
18890        Err(error) if error.code() == "EAGAIN" => Ok(None),
18891        Err(error) => Err(kernel_error(error)),
18892    }
18893}
18894
18895fn service_javascript_kernel_stdio_write_sync_rpc(
18896    kernel: &mut SidecarKernel,
18897    process: &mut ActiveProcess,
18898    request: &JavascriptSyncRpcRequest,
18899) -> Result<Value, SidecarError> {
18900    let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_stdio_write fd")?;
18901    let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "__kernel_stdio_write chunk")?;
18902    if fd != 1 && fd != 2 {
18903        return Err(SidecarError::InvalidState(format!(
18904            "__kernel_stdio_write only supports fd 1/2, got {fd}"
18905        )));
18906    }
18907
18908    // COOKED TTY (line shell): route the write through the PTY slave so it flows
18909    // through process_output (ONLCR) into the master output buffer interleaved
18910    // with cooked-mode echo, then surface that single ordered master stream so
18911    // ONLCR + echo reach the host. stderr shares the master, merging onto Stdout.
18912    let raw_mode = tty_is_raw_mode(kernel, process);
18913    if process.tty_master_fd.is_some() && !raw_mode {
18914        let written = if fd == 1 {
18915            kernel
18916                .write_process_stdout(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
18917                .map_err(kernel_error)?
18918        } else {
18919            kernel
18920                .write_process_stderr(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
18921                .map_err(kernel_error)?
18922        };
18923        if let Some(master_bytes) = drain_tty_master_output(kernel, process)? {
18924            process.queue_pending_execution_event(ActiveExecutionEvent::Stdout(master_bytes))?;
18925        }
18926        return Ok(json!(written));
18927    }
18928
18929    // RAW TTY (full-screen app) or non-TTY: emit the guest's bytes unmodified.
18930    // For a raw TTY we must NOT write through the slave (that would fill the
18931    // never-drained master and corrupt rendering); for non-TTY we write to the
18932    // underlying fd so pipes/files actually receive it.
18933    let written = if process.tty_master_fd.is_some() {
18934        chunk.len()
18935    } else if fd == 1 {
18936        kernel
18937            .write_process_stdout(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
18938            .map_err(kernel_error)?
18939    } else {
18940        kernel
18941            .write_process_stderr(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
18942            .map_err(kernel_error)?
18943    };
18944
18945    let event = if fd == 1 {
18946        ActiveExecutionEvent::Stdout(chunk)
18947    } else {
18948        ActiveExecutionEvent::Stderr(chunk)
18949    };
18950    process.queue_pending_execution_event(event)?;
18951
18952    Ok(json!(written))
18953}
18954
18955fn service_javascript_kernel_poll_sync_rpc(
18956    kernel: &mut SidecarKernel,
18957    process: &ActiveProcess,
18958    request: &JavascriptSyncRpcRequest,
18959) -> Result<Value, SidecarError> {
18960    let (fd_requests, timeout_ms) = parse_kernel_poll_args(request)?;
18961    kernel_poll_response(kernel, process.kernel_pid, &fd_requests, timeout_ms)
18962}
18963
18964/// Parse `__kernel_poll` args: (fd list, requested timeout ms).
18965pub(crate) fn parse_kernel_poll_args(
18966    request: &JavascriptSyncRpcRequest,
18967) -> Result<(Vec<KernelPollFdRequest>, i32), SidecarError> {
18968    let fd_requests: Vec<KernelPollFdRequest> = serde_json::from_value(
18969        request
18970            .args
18971            .first()
18972            .cloned()
18973            .unwrap_or_else(|| Value::Array(Vec::new())),
18974    )
18975    .map_err(|error| {
18976        SidecarError::InvalidState(format!(
18977            "__kernel_poll fd list must be a JSON array of {{ fd, events }} objects: {error}"
18978        ))
18979    })?;
18980    let timeout_ms =
18981        javascript_sync_rpc_arg_u64_optional(&request.args, 1, "__kernel_poll timeout ms")?
18982            .unwrap_or_default();
18983    let timeout_ms = i32::try_from(timeout_ms).map_err(|_| {
18984        SidecarError::InvalidState(String::from("__kernel_poll timeout ms must fit within i32"))
18985    })?;
18986    Ok((fd_requests, timeout_ms))
18987}
18988
18989/// One bounded kernel poll. Timeout `0` = non-blocking probe (deferred
18990/// servicing re-checks readiness with this before replying).
18991pub(crate) fn kernel_poll_response(
18992    kernel: &SidecarKernel,
18993    kernel_pid: u32,
18994    fd_requests: &[KernelPollFdRequest],
18995    timeout_ms: i32,
18996) -> Result<Value, SidecarError> {
18997    let poll_fds = fd_requests
18998        .iter()
18999        .map(|entry| PollFd {
19000            fd: entry.fd,
19001            events: PollEvents::from_bits(entry.events),
19002            revents: PollEvents::empty(),
19003        })
19004        .collect::<Vec<_>>();
19005    let result = kernel
19006        .poll_fds(EXECUTION_DRIVER_NAME, kernel_pid, poll_fds, timeout_ms)
19007        .map_err(kernel_error)?;
19008    Ok(json!({
19009        "readyCount": result.ready_count,
19010        "fds": result
19011            .fds
19012            .into_iter()
19013            .map(|entry| KernelPollFdResponse {
19014                fd: entry.fd,
19015                events: entry.events.bits(),
19016                revents: entry.revents.bits(),
19017            })
19018            .collect::<Vec<_>>(),
19019    }))
19020}
19021
19022fn install_kernel_stdin_pipe(kernel: &mut SidecarKernel, pid: u32) -> Result<u32, SidecarError> {
19023    let (read_fd, write_fd) = kernel
19024        .open_pipe(EXECUTION_DRIVER_NAME, pid)
19025        .map_err(kernel_error)?;
19026    kernel
19027        .fd_dup2(EXECUTION_DRIVER_NAME, pid, read_fd, 0)
19028        .map_err(kernel_error)?;
19029    kernel
19030        .fd_close(EXECUTION_DRIVER_NAME, pid, read_fd)
19031        .map_err(kernel_error)?;
19032    Ok(write_fd)
19033}
19034
19035fn requested_pty_window_size(env: &BTreeMap<String, String>) -> Option<(u16, u16)> {
19036    let cols = env
19037        .get("COLUMNS")
19038        .and_then(|value| value.parse::<u16>().ok())
19039        .filter(|value| *value > 0)?;
19040    let rows = env
19041        .get("LINES")
19042        .and_then(|value| value.parse::<u16>().ok())
19043        .filter(|value| *value > 0)?;
19044    Some((cols, rows))
19045}
19046
19047fn javascript_child_process_stdin_mode(request: &JavascriptChildProcessSpawnRequest) -> &str {
19048    request
19049        .options
19050        .stdio
19051        .first()
19052        .map(String::as_str)
19053        .unwrap_or("pipe")
19054}
19055
19056pub(crate) fn write_kernel_process_stdin(
19057    kernel: &mut SidecarKernel,
19058    process: &mut ActiveProcess,
19059    chunk: &[u8],
19060) -> Result<(), SidecarError> {
19061    // Non-TTY JavaScript uses the in-process local stdin bridge, not a kernel
19062    // fd; a TTY JavaScript process (tty_master_fd set) DOES route through the
19063    // kernel PTY master, exactly like wasm/python, so line discipline + echo
19064    // apply.
19065    if process.runtime == GuestRuntimeKind::JavaScript && process.tty_master_fd.is_none() {
19066        return Ok(());
19067    }
19068    let Some(writer_fd) = process.kernel_stdin_writer_fd else {
19069        return Ok(());
19070    };
19071    kernel
19072        .fd_write(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd, chunk)
19073        .map_err(kernel_error)?;
19074    // For a TTY process the master write above drives line-discipline echo into
19075    // the master output buffer. Drain it now and surface it as the single
19076    // ordered Stdout stream so typed-character echo reaches the host even while
19077    // the guest is blocked in read() and not producing output of its own.
19078    if let Some(echo) = drain_tty_master_output(kernel, process)? {
19079        process.queue_pending_execution_event(ActiveExecutionEvent::Stdout(echo))?;
19080    }
19081    forward_tty_slave_input_to_javascript(kernel, process)?;
19082    Ok(())
19083}
19084
19085/// For a TTY JavaScript guest, cooked input becomes readable on the PTY slave
19086/// only after line discipline runs (on newline/VEOF in canonical mode; every
19087/// byte in raw mode). The V8 isolate has no kernel-fd read loop of its own —
19088/// its stdin is the stream-stdin dispatch fed by `execution.write_stdin` — so
19089/// right after each master write, drain whatever the discipline released on
19090/// the slave (fd 0) and forward it to the isolate. A `None` read is the
19091/// discipline's VEOF: propagate it as end-of-stdin so `process.stdin` emits
19092/// `end`. Wasm/python guests read the slave themselves and are skipped.
19093fn forward_tty_slave_input_to_javascript(
19094    kernel: &mut SidecarKernel,
19095    process: &mut ActiveProcess,
19096) -> Result<(), SidecarError> {
19097    if process.tty_master_fd.is_none()
19098        || !matches!(process.execution, ActiveExecution::Javascript(_))
19099    {
19100        return Ok(());
19101    }
19102    loop {
19103        match kernel.fd_read_with_timeout_result(
19104            EXECUTION_DRIVER_NAME,
19105            process.kernel_pid,
19106            0,
19107            MAX_PTY_BUFFER_BYTES,
19108            Some(Duration::ZERO),
19109        ) {
19110            Ok(Some(bytes)) if !bytes.is_empty() => {
19111                process.execution.write_stdin(&bytes)?;
19112            }
19113            Ok(Some(_)) => return Ok(()),
19114            Ok(None) => {
19115                process.execution.close_stdin()?;
19116                return Ok(());
19117            }
19118            Err(error) if error.code() == "EAGAIN" => return Ok(()),
19119            Err(error) => return Err(kernel_error(error)),
19120        }
19121    }
19122}
19123
19124pub(crate) fn close_kernel_process_stdin(
19125    kernel: &mut SidecarKernel,
19126    process: &mut ActiveProcess,
19127) -> Result<(), SidecarError> {
19128    let Some(writer_fd) = process.kernel_stdin_writer_fd.take() else {
19129        return Ok(());
19130    };
19131    kernel
19132        .fd_close(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd)
19133        .map_err(kernel_error)
19134}
19135
19136fn parse_http_header_collection(
19137    headers: &BTreeMap<String, Value>,
19138    label: &str,
19139) -> Result<HttpHeaderCollection, SidecarError> {
19140    let mut normalized = BTreeMap::<String, Vec<String>>::new();
19141    let mut raw_pairs = Vec::new();
19142
19143    for (raw_name, value) in headers {
19144        let normalized_name = raw_name.to_ascii_lowercase();
19145        let values = match value {
19146            Value::String(text) => vec![text.clone()],
19147            Value::Array(values) => values
19148                .iter()
19149                .map(|entry| {
19150                    entry.as_str().map(str::to_owned).ok_or_else(|| {
19151                        SidecarError::InvalidState(format!(
19152                            "{label} header {raw_name} must contain only strings"
19153                        ))
19154                    })
19155                })
19156                .collect::<Result<Vec<_>, _>>()?,
19157            other => {
19158                return Err(SidecarError::InvalidState(format!(
19159                    "{label} header {raw_name} must be a string or string array, received {other}"
19160                )));
19161            }
19162        };
19163        raw_pairs.extend(
19164            values
19165                .iter()
19166                .cloned()
19167                .map(|entry| (raw_name.clone(), entry)),
19168        );
19169        normalized
19170            .entry(normalized_name)
19171            .or_default()
19172            .extend(values);
19173    }
19174
19175    Ok(HttpHeaderCollection {
19176        normalized,
19177        raw_pairs,
19178    })
19179}
19180
19181fn http_headers_json(headers: &HttpHeaderCollection) -> Value {
19182    let map = headers
19183        .normalized
19184        .iter()
19185        .map(|(name, values)| {
19186            let value = if values.len() == 1 {
19187                Value::String(values[0].clone())
19188            } else {
19189                Value::Array(values.iter().cloned().map(Value::String).collect())
19190            };
19191            (name.clone(), value)
19192        })
19193        .collect::<Map<String, Value>>();
19194    Value::Object(map)
19195}
19196
19197fn http_raw_headers_json(headers: &HttpHeaderCollection) -> Value {
19198    Value::Array(
19199        headers
19200            .raw_pairs
19201            .iter()
19202            .flat_map(|(name, value)| [Value::String(name.clone()), Value::String(value.clone())])
19203            .collect(),
19204    )
19205}
19206
19207fn is_loopback_request_host(host: &str) -> bool {
19208    let bare = host
19209        .strip_prefix('[')
19210        .and_then(|value| value.strip_suffix(']'))
19211        .unwrap_or(host);
19212    matches!(bare, "localhost" | "127.0.0.1" | "::1")
19213}
19214
19215fn serialize_http_loopback_request(
19216    url: &Url,
19217    options: &JavascriptHttpRequestOptions,
19218    headers: &HttpHeaderCollection,
19219) -> Result<String, SidecarError> {
19220    let body_base64 = options
19221        .body
19222        .as_ref()
19223        .map(|body| base64::engine::general_purpose::STANDARD.encode(body.as_bytes()));
19224    serde_json::to_string(&json!({
19225        "method": options.method.clone().unwrap_or_else(|| String::from("GET")),
19226        "url": http_request_target(url),
19227        "headers": http_headers_json(headers),
19228        "rawHeaders": http_raw_headers_json(headers),
19229        "bodyBase64": body_base64,
19230    }))
19231    .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
19232}
19233
19234fn http_request_target(url: &Url) -> String {
19235    let path = if url.path().is_empty() {
19236        "/"
19237    } else {
19238        url.path()
19239    };
19240    format!(
19241        "{path}{}",
19242        url.query()
19243            .map(|query| format!("?{query}"))
19244            .unwrap_or_default()
19245    )
19246}
19247
19248fn find_kernel_http_listener_process(vm: &VmState, port: u16) -> Option<String> {
19249    vm.active_processes
19250        .iter()
19251        .find_map(|(process_id, process)| {
19252            process.tcp_listeners.values().find_map(|listener| {
19253                let socket_id = listener.kernel_socket_id?;
19254                let record = vm.kernel.socket_get(socket_id)?;
19255                let local_addr = record
19256                    .local_address()
19257                    .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
19258                    .unwrap_or_else(|| listener.guest_local_addr());
19259                if local_addr.port() == port && is_vm_local_http_listener_addr(local_addr.ip()) {
19260                    Some(process_id.to_owned())
19261                } else {
19262                    None
19263                }
19264            })
19265        })
19266}
19267
19268fn is_vm_local_http_listener_addr(ip: IpAddr) -> bool {
19269    ip.is_loopback() || ip.is_unspecified()
19270}
19271
19272fn serialize_kernel_http_fetch_request(
19273    port: u16,
19274    path: &str,
19275    options: &JavascriptHttpRequestOptions,
19276    headers: &HttpHeaderCollection,
19277) -> Vec<u8> {
19278    let method = options.method.as_deref().unwrap_or("GET");
19279    let mut lines = vec![format!("{method} {path} HTTP/1.1")];
19280    let mut has_host = false;
19281    let mut has_connection = false;
19282    let mut has_content_length = false;
19283    for (name, values) in &headers.normalized {
19284        match name.as_str() {
19285            "host" => has_host = true,
19286            "connection" => has_connection = true,
19287            "content-length" => has_content_length = true,
19288            _ => {}
19289        }
19290        lines.push(format!("{name}: {}", values.join(", ")));
19291    }
19292    if !has_host {
19293        lines.push(format!("Host: 127.0.0.1:{port}"));
19294    }
19295    if !has_connection {
19296        lines.push(String::from("Connection: close"));
19297    }
19298    let body = options.body.as_deref().unwrap_or("").as_bytes();
19299    if !has_content_length && !body.is_empty() {
19300        lines.push(format!("Content-Length: {}", body.len()));
19301    }
19302    lines.push(String::new());
19303    lines.push(String::new());
19304
19305    let mut request = lines.join("\r\n").into_bytes();
19306    request.extend_from_slice(body);
19307    request
19308}
19309
19310fn kernel_http_fetch_target_exit_code(error: &SidecarError) -> Option<i32> {
19311    let SidecarError::Execution(message) = error else {
19312        return None;
19313    };
19314    message
19315        .strip_prefix("vm.fetch target exited before responding (exit code ")?
19316        .strip_suffix(')')?
19317        .parse()
19318        .ok()
19319}
19320
19321#[allow(clippy::too_many_arguments)]
19322fn service_host_fetch_target_event<B>(
19323    bridge: &SharedBridge<B>,
19324    vm_id: &str,
19325    dns: &VmDnsConfig,
19326    socket_paths: &JavascriptSocketPathContext,
19327    kernel: &mut SidecarKernel,
19328    kernel_readiness: &KernelSocketReadinessRegistry,
19329    process: &mut ActiveProcess,
19330    resource_limits: &ResourceLimits,
19331    wait: Duration,
19332) -> Result<bool, SidecarError>
19333where
19334    B: NativeSidecarBridge + Send + 'static,
19335    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19336{
19337    let Some(event) = process
19338        .execution
19339        .poll_event_blocking(wait)
19340        .map_err(|error| SidecarError::Execution(error.to_string()))?
19341    else {
19342        return Ok(false);
19343    };
19344
19345    match event {
19346        ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
19347            let network_counts = process.network_resource_counts();
19348            let response = service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
19349                bridge,
19350                vm_id,
19351                dns,
19352                socket_paths,
19353                kernel,
19354                kernel_readiness: Arc::clone(kernel_readiness),
19355                process,
19356                sync_request: &request,
19357                resource_limits,
19358                network_counts,
19359            });
19360            match response {
19361                Ok(result) => process
19362                    .execution
19363                    .respond_javascript_sync_rpc_response(request.id, result)
19364                    .or_else(ignore_stale_javascript_sync_rpc_response)?,
19365                Err(error) => process
19366                    .execution
19367                    .respond_javascript_sync_rpc_error(
19368                        request.id,
19369                        javascript_sync_rpc_error_code(&error),
19370                        error.to_string(),
19371                    )
19372                    .or_else(ignore_stale_javascript_sync_rpc_response)?,
19373            }
19374        }
19375        ActiveExecutionEvent::Exited(code) => {
19376            return Err(SidecarError::Execution(format!(
19377                "vm.fetch target exited before responding (exit code {code})"
19378            )));
19379        }
19380        other => {
19381            process.queue_pending_execution_event(other)?;
19382        }
19383    }
19384    Ok(true)
19385}
19386
19387fn drain_host_fetch_target_events<B>(
19388    bridge: &SharedBridge<B>,
19389    vm_id: &str,
19390    vm: &mut VmState,
19391    target_process_id: &str,
19392    socket_paths: &JavascriptSocketPathContext,
19393    resource_limits: &ResourceLimits,
19394) -> Result<(), SidecarError>
19395where
19396    B: NativeSidecarBridge + Send + 'static,
19397    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19398{
19399    for _ in 0..32 {
19400        let dns = vm.dns.clone();
19401        let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness);
19402        let Some(process) = vm.active_processes.get_mut(target_process_id) else {
19403            break;
19404        };
19405        let serviced = service_host_fetch_target_event(
19406            bridge,
19407            vm_id,
19408            &dns,
19409            socket_paths,
19410            &mut vm.kernel,
19411            &kernel_readiness,
19412            process,
19413            resource_limits,
19414            Duration::from_millis(1),
19415        )?;
19416        if !serviced {
19417            break;
19418        }
19419    }
19420    Ok(())
19421}
19422
19423#[allow(clippy::too_many_arguments)]
19424fn dispatch_kernel_http_fetch<B>(
19425    bridge: &SharedBridge<B>,
19426    vm_id: &str,
19427    vm: &mut VmState,
19428    target_process_id: &str,
19429    port: u16,
19430    path: &str,
19431    options: &JavascriptHttpRequestOptions,
19432    headers: &HttpHeaderCollection,
19433    max_fetch_response_bytes: usize,
19434) -> Result<String, SidecarError>
19435where
19436    B: NativeSidecarBridge + Send + 'static,
19437    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19438{
19439    let socket_paths = build_javascript_socket_path_context(vm)?;
19440    let family = JavascriptSocketFamily::Ipv4;
19441    let local_port = allocate_guest_listen_port(
19442        0,
19443        family,
19444        &socket_paths.used_tcp_guest_ports,
19445        socket_paths.listen_policy,
19446    )?;
19447    let resource_limits = vm.kernel.resource_limits().clone();
19448    let network_counts = vm_network_resource_counts(vm);
19449    check_network_resource_limit(
19450        resource_limits.max_sockets,
19451        network_counts.sockets,
19452        2,
19453        "socket",
19454    )?;
19455    check_network_resource_limit(
19456        resource_limits.max_connections,
19457        network_counts.connections,
19458        2,
19459        "connection",
19460    )?;
19461
19462    let kernel_pid = vm
19463        .active_processes
19464        .get(target_process_id)
19465        .ok_or_else(|| {
19466            SidecarError::InvalidState(format!(
19467                "vm.fetch target process disappeared: {target_process_id}"
19468            ))
19469        })?
19470        .kernel_pid;
19471    let socket_id = vm
19472        .kernel
19473        .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, SocketSpec::tcp())
19474        .map_err(kernel_error)?;
19475
19476    let result = dispatch_kernel_http_fetch_with_socket(
19477        bridge,
19478        vm_id,
19479        vm,
19480        target_process_id,
19481        kernel_pid,
19482        socket_id,
19483        local_port,
19484        port,
19485        path,
19486        options,
19487        headers,
19488        &socket_paths,
19489        &resource_limits,
19490        max_fetch_response_bytes,
19491    );
19492    let close_result = vm
19493        .kernel
19494        .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id)
19495        .map_err(kernel_error);
19496    let cleanup_result = if result.is_err() {
19497        drain_host_fetch_target_events(
19498            bridge,
19499            vm_id,
19500            vm,
19501            target_process_id,
19502            &socket_paths,
19503            &resource_limits,
19504        )
19505    } else {
19506        Ok(())
19507    };
19508    match (result, close_result) {
19509        (Ok(response), Ok(())) => cleanup_result.map(|()| response),
19510        (Err(error), _) => Err(error),
19511        (Ok(_), Err(error)) => Err(error),
19512    }
19513}
19514
19515#[allow(clippy::too_many_arguments)]
19516fn dispatch_kernel_http_fetch_with_socket<B>(
19517    bridge: &SharedBridge<B>,
19518    vm_id: &str,
19519    vm: &mut VmState,
19520    target_process_id: &str,
19521    kernel_pid: u32,
19522    socket_id: SocketId,
19523    local_port: u16,
19524    port: u16,
19525    path: &str,
19526    options: &JavascriptHttpRequestOptions,
19527    headers: &HttpHeaderCollection,
19528    socket_paths: &JavascriptSocketPathContext,
19529    resource_limits: &ResourceLimits,
19530    max_fetch_response_bytes: usize,
19531) -> Result<String, SidecarError>
19532where
19533    B: NativeSidecarBridge + Send + 'static,
19534    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19535{
19536    vm.kernel
19537        .socket_bind_inet(
19538            EXECUTION_DRIVER_NAME,
19539            kernel_pid,
19540            socket_id,
19541            InetSocketAddress::new("127.0.0.1", local_port),
19542        )
19543        .map_err(kernel_error)?;
19544    vm.kernel
19545        .socket_connect_inet_loopback(
19546            EXECUTION_DRIVER_NAME,
19547            kernel_pid,
19548            socket_id,
19549            InetSocketAddress::new("127.0.0.1", port),
19550        )
19551        .map_err(kernel_error)?;
19552
19553    let request_bytes = serialize_kernel_http_fetch_request(port, path, options, headers);
19554    vm.kernel
19555        .socket_write(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, &request_bytes)
19556        .map_err(kernel_error)?;
19557
19558    let mut response_buffer = Vec::new();
19559    let mut peer_closed = false;
19560    let url = format!("http://127.0.0.1:{port}{path}");
19561    let deadline = Instant::now() + http_loopback_request_timeout();
19562    loop {
19563        if let Some(response) =
19564            parse_kernel_http_fetch_response(&response_buffer, peer_closed, &url)
19565                .map_err(sidecar_core_execution_error)?
19566        {
19567            ensure_vm_fetch_response_within_limit(&response, "vm.fetch", max_fetch_response_bytes)
19568                .map_err(sidecar_core_execution_error)?;
19569            return Ok(response);
19570        }
19571        if Instant::now() >= deadline {
19572            let preview = String::from_utf8_lossy(&response_buffer);
19573            return Err(SidecarError::Execution(format!(
19574                "vm.fetch timed out waiting for kernel TCP HTTP response ({} buffered bytes: {:?})",
19575                response_buffer.len(),
19576                preview.chars().take(200).collect::<String>()
19577            )));
19578        }
19579
19580        {
19581            let dns = vm.dns.clone();
19582            let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness);
19583            let process = vm
19584                .active_processes
19585                .get_mut(target_process_id)
19586                .ok_or_else(|| {
19587                    SidecarError::InvalidState(format!(
19588                        "vm.fetch target process disappeared: {target_process_id}"
19589                    ))
19590                })?;
19591            service_host_fetch_target_event(
19592                bridge,
19593                vm_id,
19594                &dns,
19595                socket_paths,
19596                &mut vm.kernel,
19597                &kernel_readiness,
19598                process,
19599                resource_limits,
19600                Duration::from_millis(5),
19601            )?;
19602        }
19603
19604        let poll = vm
19605            .kernel
19606            .poll_targets(
19607                EXECUTION_DRIVER_NAME,
19608                kernel_pid,
19609                vec![PollTargetEntry::socket(
19610                    socket_id,
19611                    POLLIN | POLLHUP | POLLERR,
19612                )],
19613                5,
19614            )
19615            .map_err(kernel_error)?;
19616        let revents = poll
19617            .targets
19618            .first()
19619            .map(|entry| entry.revents)
19620            .unwrap_or_else(PollEvents::empty);
19621        if revents.intersects(POLLERR) {
19622            return Err(SidecarError::Execution(String::from(
19623                "vm.fetch kernel TCP socket reported POLLERR",
19624            )));
19625        }
19626        if revents.intersects(POLLIN) {
19627            loop {
19628                match vm
19629                    .kernel
19630                    .socket_read(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, 64 * 1024)
19631                {
19632                    Ok(Some(bytes)) if !bytes.is_empty() => {
19633                        response_buffer.extend(bytes);
19634                        ensure_vm_fetch_raw_response_buffer_within_limit(
19635                            response_buffer.len(),
19636                            "vm.fetch",
19637                        )
19638                        .map_err(sidecar_core_execution_error)?;
19639                    }
19640                    Ok(Some(_)) => break,
19641                    Ok(None) => {
19642                        peer_closed = true;
19643                        break;
19644                    }
19645                    Err(error) if error.code() == "EAGAIN" => break,
19646                    Err(error) => return Err(kernel_error(error)),
19647                }
19648            }
19649        }
19650        if revents.intersects(POLLHUP) {
19651            peer_closed = true;
19652        }
19653    }
19654}
19655
19656fn outbound_http_response_json(url: &Url, response: ureq::Response) -> Result<Value, SidecarError> {
19657    let status = response.status();
19658    let status_text = response.status_text().to_owned();
19659    let mut header_pairs = Vec::new();
19660    let mut raw_headers = Vec::new();
19661    for raw_name in response.headers_names() {
19662        for value in response.all(&raw_name) {
19663            header_pairs.push(json!([raw_name.to_ascii_lowercase(), value]));
19664            raw_headers.push(Value::String(raw_name.clone()));
19665            raw_headers.push(Value::String(value.to_owned()));
19666        }
19667    }
19668    let mut reader = response.into_reader();
19669    let mut body = Vec::new();
19670    reader.read_to_end(&mut body).map_err(|error| {
19671        SidecarError::Execution(format!("failed to read HTTP response: {error}"))
19672    })?;
19673    serde_json::to_string(&json!({
19674        "status": status,
19675        "statusText": status_text,
19676        "headers": header_pairs,
19677        "rawHeaders": raw_headers,
19678        "body": base64::engine::general_purpose::STANDARD.encode(body),
19679        "bodyEncoding": "base64",
19680        "url": url.as_str(),
19681    }))
19682    .map(Value::String)
19683    .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
19684}
19685
19686/// Split a ureq resolver `netloc` (`host:port`, with optional `[..]` IPv6
19687/// brackets) into its host and port components. Returns `None` if the port is
19688/// missing or unparseable.
19689fn split_netloc(netloc: &str) -> Option<(&str, u16)> {
19690    let (host, port) = netloc.rsplit_once(':')?;
19691    let port: u16 = port.parse().ok()?;
19692    let host = host
19693        .strip_prefix('[')
19694        .and_then(|rest| rest.strip_suffix(']'))
19695        .unwrap_or(host);
19696    Some((host, port))
19697}
19698
19699fn issue_outbound_http_request(
19700    url: &Url,
19701    options: &JavascriptHttpRequestOptions,
19702    headers: &HttpHeaderCollection,
19703    pinned_addresses: &[IpAddr],
19704) -> Result<Value, SidecarError> {
19705    let method = options.method.as_deref().unwrap_or("GET");
19706    // Pin the underlying resolver to the egress-vetted addresses. ureq performs
19707    // its own DNS resolution for the TCP/TLS connect; without this override an
19708    // https:// request would re-resolve the hostname through the host resolver
19709    // (a rebinding DNS server could then return a private/metadata IP that the
19710    // earlier range check would have rejected). The pinned resolver returns only
19711    // the vetted addresses and refuses any host it was not vetted for, while the
19712    // request URL keeps the original hostname so TLS SNI and the Host header stay
19713    // correct.
19714    let pinned_host = url.host_str().map(str::to_owned);
19715    let pinned: Vec<IpAddr> = pinned_addresses.to_vec();
19716    let resolver = move |netloc: &str| -> std::io::Result<Vec<SocketAddr>> {
19717        let (host, port) = split_netloc(netloc).ok_or_else(|| {
19718            std::io::Error::new(
19719                std::io::ErrorKind::InvalidInput,
19720                format!("invalid network location: {netloc}"),
19721            )
19722        })?;
19723        let expected_host = pinned_host.as_deref();
19724        if expected_host != Some(host) {
19725            return Err(std::io::Error::new(
19726                std::io::ErrorKind::PermissionDenied,
19727                format!(
19728                    "EACCES: outbound HTTP resolver pinned to {expected_host:?}, refusing {host}"
19729                ),
19730            ));
19731        }
19732        if pinned.is_empty() {
19733            return Err(std::io::Error::new(
19734                std::io::ErrorKind::PermissionDenied,
19735                "EACCES: no egress-vetted address available for outbound HTTP request",
19736            ));
19737        }
19738        Ok(pinned.iter().map(|ip| SocketAddr::new(*ip, port)).collect())
19739    };
19740    let mut agent_builder = ureq::AgentBuilder::new()
19741        .resolver(resolver)
19742        .timeout_connect(Duration::from_secs(5))
19743        .timeout_read(Duration::from_secs(15))
19744        .timeout_write(Duration::from_secs(15));
19745    if url.scheme() == "https" {
19746        let tls_options = JavascriptTlsBridgeOptions {
19747            is_server: false,
19748            servername: url.host_str().map(str::to_owned),
19749            alpn_protocols: Some(vec![String::from("http/1.1")]),
19750            reject_unauthorized: options.reject_unauthorized,
19751            ..JavascriptTlsBridgeOptions::default()
19752        };
19753        agent_builder = agent_builder.tls_config(Arc::new(build_client_tls_config(&tls_options)?));
19754    }
19755    let agent = agent_builder.build();
19756    let mut request = agent.request_url(method, url);
19757    for (name, values) in &headers.normalized {
19758        if name == "host" {
19759            continue;
19760        }
19761        let header_value = values.join(", ");
19762        request = request.set(name, &header_value);
19763    }
19764    let response = match options.body.as_deref() {
19765        Some(body) => request.send_string(body),
19766        None => request.call(),
19767    };
19768
19769    match response {
19770        Ok(response) => outbound_http_response_json(url, response),
19771        Err(ureq::Error::Status(_, response)) => outbound_http_response_json(url, response),
19772        Err(ureq::Error::Transport(error)) => Err(SidecarError::Execution(format!(
19773            "ERR_HTTP_REQUEST_FAILED: {error}"
19774        ))),
19775    }
19776}
19777
19778fn wait_for_loopback_http_response<B>(
19779    request: LoopbackHttpResponseWaitRequest<'_, B>,
19780) -> Result<String, SidecarError>
19781where
19782    B: NativeSidecarBridge + Send + 'static,
19783    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19784{
19785    let LoopbackHttpResponseWaitRequest {
19786        bridge,
19787        vm_id,
19788        dns,
19789        socket_paths,
19790        kernel,
19791        kernel_readiness,
19792        process,
19793        resource_limits,
19794        request_key,
19795    } = request;
19796    let deadline = Instant::now() + http_loopback_request_timeout();
19797    loop {
19798        if let Some(response) = process
19799            .pending_http_requests
19800            .get(&request_key)
19801            .and_then(|response| response.clone())
19802        {
19803            process.pending_http_requests.remove(&request_key);
19804            return Ok(response);
19805        }
19806
19807        if Instant::now() >= deadline {
19808            process.pending_http_requests.remove(&request_key);
19809            return Err(SidecarError::Execution(String::from(
19810                "HTTP loopback request timed out waiting for net.http_respond",
19811            )));
19812        }
19813
19814        let Some(event) = process
19815            .execution
19816            .poll_event_blocking(Duration::from_millis(10))
19817            .map_err(|error| SidecarError::Execution(error.to_string()))?
19818        else {
19819            continue;
19820        };
19821
19822        match event {
19823            ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
19824                let network_counts = process.network_resource_counts();
19825                let response = service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
19826                    bridge,
19827                    vm_id,
19828                    dns,
19829                    socket_paths,
19830                    kernel,
19831                    kernel_readiness: Arc::clone(&kernel_readiness),
19832                    process,
19833                    sync_request: &request,
19834                    resource_limits,
19835                    network_counts,
19836                });
19837                match response {
19838                    Ok(result) => process
19839                        .execution
19840                        .respond_javascript_sync_rpc_response(request.id, result)
19841                        .or_else(ignore_stale_javascript_sync_rpc_response)?,
19842                    Err(error) => process
19843                        .execution
19844                        .respond_javascript_sync_rpc_error(
19845                            request.id,
19846                            javascript_sync_rpc_error_code(&error),
19847                            error.to_string(),
19848                        )
19849                        .or_else(ignore_stale_javascript_sync_rpc_response)?,
19850                }
19851            }
19852            ActiveExecutionEvent::Exited(code) => {
19853                process.pending_http_requests.remove(&request_key);
19854                return Err(SidecarError::Execution(format!(
19855                    "HTTP loopback server exited before responding (exit code {code})"
19856                )));
19857            }
19858            ActiveExecutionEvent::Stdout(_)
19859            | ActiveExecutionEvent::Stderr(_)
19860            | ActiveExecutionEvent::PythonVfsRpcRequest(_)
19861            | ActiveExecutionEvent::SignalState { .. } => {}
19862        }
19863    }
19864}
19865
19866pub(crate) fn dispatch_loopback_http_request<B>(
19867    request: LoopbackHttpDispatchRequest<'_, B>,
19868) -> Result<String, SidecarError>
19869where
19870    B: NativeSidecarBridge + Send + 'static,
19871    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19872{
19873    let LoopbackHttpDispatchRequest {
19874        bridge,
19875        vm_id,
19876        dns,
19877        socket_paths,
19878        kernel,
19879        kernel_readiness,
19880        process,
19881        resource_limits,
19882        server_id,
19883        request_json,
19884    } = request;
19885    let request_id = {
19886        let server = process.http_servers.get_mut(&server_id).ok_or_else(|| {
19887            SidecarError::InvalidState(format!("HTTP target server disappeared: {server_id}"))
19888        })?;
19889        server.next_request_id += 1;
19890        server.next_request_id
19891    };
19892    process
19893        .pending_http_requests
19894        .insert((server_id, request_id), None);
19895    process.execution.send_javascript_stream_event(
19896        "http_request",
19897        json!({
19898            "serverId": server_id,
19899            "requestId": request_id,
19900            "request": request_json,
19901        }),
19902    )?;
19903    wait_for_loopback_http_response(LoopbackHttpResponseWaitRequest {
19904        bridge,
19905        vm_id,
19906        dns,
19907        socket_paths,
19908        kernel,
19909        kernel_readiness,
19910        process,
19911        resource_limits,
19912        request_key: (server_id, request_id),
19913    })
19914}
19915
19916fn sidecar_core_execution_error(error: SidecarCoreError) -> SidecarError {
19917    SidecarError::Execution(error.to_string())
19918}
19919
19920pub(crate) fn ensure_vm_fetch_response_frame_within_limit(
19921    response: &ResponseFrame,
19922    max_frame_bytes: usize,
19923) -> Result<(), SidecarError> {
19924    let max_frame_bytes = max_frame_bytes.min(VM_FETCH_BUFFER_LIMIT_BYTES);
19925    let frame = crate::protocol::to_generated_protocol_frame(
19926        &crate::protocol::ProtocolFrame::Response(response.clone()),
19927    )
19928    .map_err(|error| SidecarError::FrameTooLarge(error.to_string()))?;
19929    let WireProtocolFrame::ResponseFrame(_) = &frame else {
19930        return Err(SidecarError::FrameTooLarge(String::from(
19931            "vm fetch response converted to non-response wire frame",
19932        )));
19933    };
19934    WireFrameCodec::new(max_frame_bytes)
19935        .encode(&frame)
19936        .map(|_| ())
19937        .map_err(|error| SidecarError::FrameTooLarge(error.to_string()))
19938}
19939
19940fn service_javascript_dns_sync_rpc<B>(
19941    bridge: &SharedBridge<B>,
19942    kernel: &SidecarKernel,
19943    vm_id: &str,
19944    dns: &VmDnsConfig,
19945    request: &JavascriptSyncRpcRequest,
19946) -> Result<Value, SidecarError>
19947where
19948    B: NativeSidecarBridge + Send + 'static,
19949    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19950{
19951    match request.method.as_str() {
19952        "dns.lookup" => {
19953            let payload = request
19954                .args
19955                .first()
19956                .cloned()
19957                .ok_or_else(|| {
19958                    SidecarError::InvalidState(String::from(
19959                        "dns.lookup requires a request payload",
19960                    ))
19961                })
19962                .and_then(|value| {
19963                    serde_json::from_value::<JavascriptDnsLookupRequest>(value).map_err(|error| {
19964                        SidecarError::InvalidState(format!("invalid dns.lookup payload: {error}"))
19965                    })
19966                })?;
19967            let addresses = filter_dns_ip_addrs(
19968                resolve_dns_ip_addrs(
19969                    bridge,
19970                    kernel,
19971                    vm_id,
19972                    dns,
19973                    &payload.hostname,
19974                    DnsLookupPolicy::CheckPermissions,
19975                )?,
19976                payload.family,
19977            )?;
19978            let addresses = filter_dns_safe_ip_addrs(addresses, &payload.hostname)?;
19979            Ok(Value::Array(
19980                addresses
19981                    .into_iter()
19982                    .map(|ip| {
19983                        json!({
19984                            "address": ip.to_string(),
19985                            "family": if ip.is_ipv6() { 6 } else { 4 },
19986                        })
19987                    })
19988                    .collect(),
19989            ))
19990        }
19991        "dns.resolve" | "dns.resolve4" | "dns.resolve6" => {
19992            let payload = request
19993                .args
19994                .first()
19995                .cloned()
19996                .ok_or_else(|| {
19997                    SidecarError::InvalidState(String::from(
19998                        "dns.resolve requires a request payload",
19999                    ))
20000                })
20001                .and_then(|value| {
20002                    serde_json::from_value::<JavascriptDnsResolveRequest>(value).map_err(|error| {
20003                        SidecarError::InvalidState(format!("invalid dns.resolve payload: {error}"))
20004                    })
20005                })?;
20006            let requested_type = match request.method.as_str() {
20007                "dns.resolve4" => String::from("A"),
20008                "dns.resolve6" => String::from("AAAA"),
20009                _ => payload
20010                    .rrtype
20011                    .as_deref()
20012                    .unwrap_or("A")
20013                    .to_ascii_uppercase(),
20014            };
20015            let record_type = parse_dns_record_type(&requested_type)?;
20016            let resolution = resolve_dns_records(
20017                bridge,
20018                kernel,
20019                vm_id,
20020                dns,
20021                &payload.hostname,
20022                record_type,
20023                DnsLookupPolicy::CheckPermissions,
20024            )?;
20025            dns_resolution_to_node_value(&resolution, &requested_type)
20026        }
20027        other => Err(SidecarError::InvalidState(format!(
20028            "unsupported JavaScript dns sync RPC method {other}"
20029        ))),
20030    }
20031}
20032
20033fn service_javascript_dgram_sync_rpc<B>(
20034    request: JavascriptDgramSyncRpcServiceRequest<'_, B>,
20035) -> Result<Value, SidecarError>
20036where
20037    B: NativeSidecarBridge + Send + 'static,
20038    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
20039{
20040    let JavascriptDgramSyncRpcServiceRequest {
20041        bridge,
20042        kernel,
20043        vm_id,
20044        dns,
20045        socket_paths,
20046        process,
20047        kernel_readiness,
20048        sync_request: request,
20049        resource_limits,
20050        network_counts,
20051    } = request;
20052    match request.method.as_str() {
20053        "dgram.createSocket" => {
20054            check_network_resource_limit(
20055                resource_limits.max_sockets,
20056                network_counts.sockets,
20057                1,
20058                "socket",
20059            )?;
20060            let payload = request
20061                .args
20062                .first()
20063                .cloned()
20064                .ok_or_else(|| {
20065                    SidecarError::InvalidState(String::from(
20066                        "dgram.createSocket requires a request payload",
20067                    ))
20068                })
20069                .and_then(|value| {
20070                    serde_json::from_value::<JavascriptDgramCreateSocketRequest>(value).map_err(
20071                        |error| {
20072                            SidecarError::InvalidState(format!(
20073                                "invalid dgram.createSocket payload: {error}"
20074                            ))
20075                        },
20076                    )
20077                })?;
20078            let family = JavascriptUdpFamily::from_socket_type(&payload.socket_type)?;
20079            let socket_id = process.allocate_udp_socket_id();
20080            let socket = ActiveUdpSocket::new(kernel, process.kernel_pid, family)?;
20081            register_kernel_readiness_target(
20082                &kernel_readiness,
20083                socket.kernel_socket_id,
20084                process.execution.javascript_v8_session_handle(),
20085                socket_id.clone(),
20086                KernelSocketReadinessEvent::Datagram,
20087            );
20088            process.udp_sockets.insert(socket_id.clone(), socket);
20089            Ok(json!({
20090                "socketId": socket_id,
20091                "type": family.socket_type(),
20092            }))
20093        }
20094        "dgram.bind" => {
20095            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.bind socket id")?;
20096            let payload = request
20097                .args
20098                .get(1)
20099                .cloned()
20100                .ok_or_else(|| {
20101                    SidecarError::InvalidState(String::from(
20102                        "dgram.bind requires a request payload",
20103                    ))
20104                })
20105                .and_then(|value| {
20106                    serde_json::from_value::<JavascriptDgramBindRequest>(value).map_err(|error| {
20107                        SidecarError::InvalidState(format!("invalid dgram.bind payload: {error}"))
20108                    })
20109                })?;
20110            let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
20111                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
20112            })?;
20113            let local_addr = socket.bind(
20114                kernel,
20115                process.kernel_pid,
20116                payload.address.as_deref(),
20117                payload.port,
20118                socket_paths,
20119            )?;
20120            Ok(local_endpoint_value(&local_addr))
20121        }
20122        "dgram.send" => {
20123            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.send socket id")?;
20124            let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "dgram.send payload")?;
20125            let payload = request
20126                .args
20127                .get(2)
20128                .cloned()
20129                .ok_or_else(|| {
20130                    SidecarError::InvalidState(String::from(
20131                        "dgram.send requires a request payload",
20132                    ))
20133                })
20134                .and_then(|value| {
20135                    serde_json::from_value::<JavascriptDgramSendRequest>(value).map_err(|error| {
20136                        SidecarError::InvalidState(format!("invalid dgram.send payload: {error}"))
20137                    })
20138                })?;
20139            let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
20140                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
20141            })?;
20142            let (written, local_addr) = socket.send_to(ActiveUdpSendToRequest {
20143                bridge,
20144                kernel,
20145                kernel_pid: process.kernel_pid,
20146                vm_id,
20147                dns,
20148                host: payload.address.as_deref().unwrap_or("localhost"),
20149                port: payload.port,
20150                context: socket_paths,
20151                contents: &chunk,
20152            })?;
20153            Ok(json!({
20154                "bytes": written,
20155                "localAddress": local_addr.ip().to_string(),
20156                "localPort": local_addr.port(),
20157                "family": socket_addr_family(&local_addr),
20158            }))
20159        }
20160        "dgram.poll" => {
20161            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.poll socket id")?;
20162            let wait_ms =
20163                javascript_sync_rpc_arg_u64_optional(&request.args, 1, "dgram.poll wait ms")?
20164                    .unwrap_or_default();
20165            let event = {
20166                let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
20167                    SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
20168                })?;
20169                socket.poll(kernel, process.kernel_pid, Duration::from_millis(wait_ms))?
20170            };
20171
20172            match event {
20173                Some(JavascriptUdpSocketEvent::Message { data, remote_addr }) => {
20174                    let family = JavascriptSocketFamily::from_ip(remote_addr.ip());
20175                    let guest_remote_port = if is_loopback_ip(remote_addr.ip()) {
20176                        socket_paths
20177                            .guest_udp_port_for_host_port(family, remote_addr.port())
20178                            .unwrap_or(remote_addr.port())
20179                    } else {
20180                        remote_addr.port()
20181                    };
20182                    let mut response = remote_endpoint_value(&remote_addr, guest_remote_port);
20183                    if let Value::Object(fields) = &mut response {
20184                        fields.insert(String::from("type"), Value::String(String::from("message")));
20185                        fields.insert(String::from("data"), javascript_sync_rpc_bytes_value(&data));
20186                    }
20187                    Ok(response)
20188                }
20189                Some(JavascriptUdpSocketEvent::Error { code, message }) => Ok(json!({
20190                    "type": "error",
20191                    "code": code,
20192                    "message": message,
20193                })),
20194                None => Ok(Value::Null),
20195            }
20196        }
20197        "dgram.close" => {
20198            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.close socket id")?;
20199            let mut socket = process.udp_sockets.remove(socket_id).ok_or_else(|| {
20200                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
20201            })?;
20202            unregister_kernel_readiness_target(&kernel_readiness, socket.kernel_socket_id);
20203            socket.close(kernel, process.kernel_pid);
20204            Ok(Value::Null)
20205        }
20206        "dgram.address" => {
20207            let socket_id =
20208                javascript_sync_rpc_arg_str(&request.args, 0, "dgram.address socket id")?;
20209            let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
20210                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
20211            })?;
20212            let local_addr = socket.local_addr().ok_or_else(|| {
20213                SidecarError::Execution(String::from("EBADF: bad file descriptor"))
20214            })?;
20215            javascript_net_json_string(
20216                json!({
20217                    "address": local_addr.ip().to_string(),
20218                    "port": local_addr.port(),
20219                    "family": socket_addr_family(&local_addr),
20220                }),
20221                "dgram.address",
20222            )
20223        }
20224        "dgram.setBufferSize" => {
20225            let socket_id =
20226                javascript_sync_rpc_arg_str(&request.args, 0, "dgram.setBufferSize socket id")?;
20227            let which =
20228                javascript_sync_rpc_arg_str(&request.args, 1, "dgram.setBufferSize buffer kind")?;
20229            let size = javascript_sync_rpc_arg_u64(&request.args, 2, "dgram.setBufferSize size")?;
20230            let size = usize::try_from(size).map_err(|_| {
20231                SidecarError::InvalidState(String::from(
20232                    "dgram.setBufferSize size must fit within usize",
20233                ))
20234            })?;
20235            let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
20236                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
20237            })?;
20238            socket.set_buffer_size(which, size)?;
20239            Ok(Value::Null)
20240        }
20241        "dgram.getBufferSize" => {
20242            let socket_id =
20243                javascript_sync_rpc_arg_str(&request.args, 0, "dgram.getBufferSize socket id")?;
20244            let which =
20245                javascript_sync_rpc_arg_str(&request.args, 1, "dgram.getBufferSize buffer kind")?;
20246            let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
20247                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
20248            })?;
20249            let size = socket.get_buffer_size(which)?;
20250            Ok(json!(size))
20251        }
20252        other => Err(SidecarError::InvalidState(format!(
20253            "unsupported JavaScript dgram sync RPC method {other}"
20254        ))),
20255    }
20256}
20257
20258#[derive(Debug)]
20259struct ClientHttp2StreamState {
20260    send_stream: Option<h2::SendStream<Bytes>>,
20261}
20262
20263#[derive(Debug)]
20264struct ServerHttp2StreamState {
20265    send_response: Option<ServerHttp2Responder>,
20266    send_stream: Option<h2::SendStream<Bytes>>,
20267}
20268
20269#[derive(Debug)]
20270enum ServerHttp2Responder {
20271    Regular(server::SendResponse<Bytes>),
20272    Pushed(server::SendPushedResponse<Bytes>),
20273}
20274
20275const HTTP2_DEFAULT_WINDOW_SIZE: u32 = 65_535;
20276const HTTP2_POLL_DELAY: Duration = Duration::from_millis(10);
20277
20278fn http2_runtime_snapshot() -> Http2RuntimeSnapshot {
20279    Http2RuntimeSnapshot {
20280        effective_local_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
20281        local_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
20282        remote_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
20283        next_stream_id: 1,
20284        outbound_queue_size: 1,
20285        deflate_dynamic_table_size: 0,
20286        inflate_dynamic_table_size: 0,
20287    }
20288}
20289
20290fn http2_snapshot_json(snapshot: &Http2SessionSnapshot) -> Result<String, SidecarError> {
20291    serde_json::to_string(snapshot)
20292        .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
20293}
20294
20295fn http2_event_value(event: &Http2BridgeEvent) -> Result<Value, SidecarError> {
20296    serde_json::to_string(event)
20297        .map(Value::String)
20298        .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
20299}
20300
20301fn push_http2_server_event(
20302    shared: &Arc<Mutex<crate::state::Http2SharedState>>,
20303    server_id: u64,
20304    event: Http2BridgeEvent,
20305) {
20306    let (ready, wake_session, should_wake) = if let Ok(mut state) = shared.lock() {
20307        let queue = state.server_events.entry(server_id).or_default();
20308        let should_wake = queue.is_empty();
20309        queue.push_back(event);
20310        (
20311            Some(Arc::clone(&state.ready)),
20312            state.event_session.clone(),
20313            should_wake,
20314        )
20315    } else {
20316        (None, None, false)
20317    };
20318    if let Some(ready) = ready {
20319        ready.notify_all();
20320    }
20321    if should_wake {
20322        push_http2_retain_wake(wake_session, "server", server_id);
20323    }
20324}
20325
20326fn push_http2_session_event(
20327    shared: &Arc<Mutex<crate::state::Http2SharedState>>,
20328    session_id: u64,
20329    event: Http2BridgeEvent,
20330) {
20331    let (ready, wake_session, should_wake) = if let Ok(mut state) = shared.lock() {
20332        let queue = state.session_events.entry(session_id).or_default();
20333        let should_wake = queue.is_empty();
20334        queue.push_back(event);
20335        (
20336            Some(Arc::clone(&state.ready)),
20337            state.event_session.clone(),
20338            should_wake,
20339        )
20340    } else {
20341        (None, None, false)
20342    };
20343    if let Some(ready) = ready {
20344        ready.notify_all();
20345    }
20346    if should_wake {
20347        push_http2_retain_wake(wake_session, "session", session_id);
20348    }
20349}
20350
20351fn push_http2_retain_wake(session: Option<V8SessionHandle>, kind: &'static str, id: u64) {
20352    let Some(session) = session else {
20353        return;
20354    };
20355    let payload = v8_runtime::json_to_cbor_payload(&json!({
20356        "event": "http2",
20357        "kind": kind,
20358        "id": id,
20359    }))
20360    .unwrap_or_default();
20361    let _ = session.send_stream_event("net_socket", payload);
20362}
20363
20364fn pop_http2_event(
20365    queue: &mut BTreeMap<u64, VecDeque<Http2BridgeEvent>>,
20366    id: u64,
20367) -> Option<Http2BridgeEvent> {
20368    queue.get_mut(&id).and_then(VecDeque::pop_front)
20369}
20370
20371fn wait_for_http2_event(
20372    shared: &Arc<Mutex<crate::state::Http2SharedState>>,
20373    id: u64,
20374    is_server: bool,
20375    wait_ms: u64,
20376) -> Option<Http2BridgeEvent> {
20377    let deadline = Instant::now() + Duration::from_millis(wait_ms);
20378    let mut state = shared.lock().ok()?;
20379    loop {
20380        let queue = if is_server {
20381            &mut state.server_events
20382        } else {
20383            &mut state.session_events
20384        };
20385        if let Some(event) = pop_http2_event(queue, id) {
20386            return Some(event);
20387        }
20388        if wait_ms == 0 {
20389            return None;
20390        }
20391        let now = Instant::now();
20392        if now >= deadline {
20393            return None;
20394        }
20395        let remaining = deadline.saturating_duration_since(now);
20396        if remaining.is_zero() {
20397            return None;
20398        }
20399        let ready = Arc::clone(&state.ready);
20400        state = ready.wait_timeout(state, remaining).ok()?.0;
20401    }
20402}
20403
20404fn next_http2_session_id(shared: &mut crate::state::Http2SharedState) -> u64 {
20405    shared.next_session_id += 1;
20406    shared.next_session_id
20407}
20408
20409fn next_http2_stream_id(shared: &mut crate::state::Http2SharedState) -> u64 {
20410    shared.next_stream_id += 1;
20411    shared.next_stream_id
20412}
20413
20414fn http2_reason(code: Option<u32>) -> Reason {
20415    code.unwrap_or(Reason::NO_ERROR.into()).into()
20416}
20417
20418fn http2_error_payload(message: impl Into<String>) -> String {
20419    serde_json::to_string(&json!({
20420        "name": "Error",
20421        "code": "ERR_HTTP2_ERROR",
20422        "message": message.into(),
20423    }))
20424    .unwrap_or_else(|_| {
20425        String::from(
20426            "{\"name\":\"Error\",\"code\":\"ERR_HTTP2_ERROR\",\"message\":\"HTTP/2 bridge error\"}",
20427        )
20428    })
20429}
20430
20431fn http2_socket_snapshot(local_addr: SocketAddr, remote_addr: SocketAddr) -> Http2SocketSnapshot {
20432    Http2SocketSnapshot {
20433        encrypted: false,
20434        allow_half_open: false,
20435        local_address: Some(local_addr.ip().to_string()),
20436        local_port: Some(local_addr.port()),
20437        local_family: Some(socket_addr_family(&local_addr).to_string()),
20438        remote_address: Some(remote_addr.ip().to_string()),
20439        remote_port: Some(remote_addr.port()),
20440        remote_family: Some(socket_addr_family(&remote_addr).to_string()),
20441        servername: None,
20442        alpn_protocol: Some(String::from("h2c")),
20443    }
20444}
20445
20446fn http2_wait_result(kind: &str, id: u64) -> Value {
20447    json!({
20448        "kind": kind,
20449        "id": id,
20450    })
20451}
20452
20453fn is_http2_terminal_event(event: &Http2BridgeEvent, is_server: bool, id: u64) -> bool {
20454    if is_server {
20455        event.kind == "serverClose" && event.id == id
20456    } else {
20457        event.kind == "sessionClose" && event.id == id
20458    }
20459}
20460
20461fn dispatch_http2_wait_loop(
20462    process: &ActiveProcess,
20463    id: u64,
20464    is_server: bool,
20465) -> Result<Value, SidecarError> {
20466    loop {
20467        if let Some(event) = wait_for_http2_event(&process.http2.shared, id, is_server, 50) {
20468            let payload = serde_json::to_value(&event).map_err(|error| {
20469                SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}"))
20470            })?;
20471            process
20472                .execution
20473                .send_javascript_stream_event("http2", payload.clone())?;
20474            if is_http2_terminal_event(&event, is_server, id) {
20475                return Ok(payload);
20476            }
20477            continue;
20478        }
20479
20480        let exists = process
20481            .http2
20482            .shared
20483            .lock()
20484            .map(|state| {
20485                if is_server {
20486                    state.servers.contains_key(&id)
20487                } else {
20488                    state.sessions.contains_key(&id)
20489                }
20490            })
20491            .unwrap_or(false);
20492        if !exists {
20493            return Ok(if is_server {
20494                http2_wait_result("serverClose", id)
20495            } else {
20496                http2_wait_result("sessionClose", id)
20497            });
20498        }
20499    }
20500}
20501
20502fn dispatch_http_wait_loop(process: &ActiveProcess, server_id: u64) -> Result<Value, SidecarError> {
20503    loop {
20504        if !process.http_servers.contains_key(&server_id) {
20505            return Ok(json!({
20506                "kind": "serverClose",
20507                "id": server_id,
20508            }));
20509        }
20510        thread::sleep(Duration::from_millis(25));
20511    }
20512}
20513
20514fn http2_settings_from_value(settings: &BTreeMap<String, Value>) -> BTreeMap<String, Value> {
20515    settings.clone()
20516}
20517
20518fn parse_http2_headers_json(
20519    headers_json: &str,
20520    label: &str,
20521) -> Result<BTreeMap<String, Value>, SidecarError> {
20522    serde_json::from_str::<BTreeMap<String, Value>>(headers_json)
20523        .map_err(|error| SidecarError::InvalidState(format!("{label} must be valid JSON: {error}")))
20524}
20525
20526fn apply_http2_header_values(
20527    header_map: &mut HeaderMap,
20528    name: &str,
20529    value: &Value,
20530) -> Result<(), SidecarError> {
20531    let header_name = HeaderName::from_bytes(name.as_bytes()).map_err(|error| {
20532        SidecarError::InvalidState(format!("invalid HTTP/2 header name {name:?}: {error}"))
20533    })?;
20534    match value {
20535        Value::Array(values) => {
20536            for value in values {
20537                apply_http2_header_values(header_map, name, value)?;
20538            }
20539        }
20540        Value::String(text) => {
20541            let value = HeaderValue::from_str(text).map_err(|error| {
20542                SidecarError::InvalidState(format!(
20543                    "invalid HTTP/2 header value for {name}: {error}"
20544                ))
20545            })?;
20546            header_map.append(header_name.clone(), value);
20547        }
20548        Value::Number(number) => {
20549            let value = HeaderValue::from_str(&number.to_string()).map_err(|error| {
20550                SidecarError::InvalidState(format!(
20551                    "invalid HTTP/2 numeric header value for {name}: {error}"
20552                ))
20553            })?;
20554            header_map.append(header_name.clone(), value);
20555        }
20556        Value::Bool(boolean) => {
20557            let value = HeaderValue::from_str(if *boolean { "true" } else { "false" }).map_err(
20558                |error| {
20559                    SidecarError::InvalidState(format!(
20560                        "invalid HTTP/2 boolean header value for {name}: {error}"
20561                    ))
20562                },
20563            )?;
20564            header_map.append(header_name.clone(), value);
20565        }
20566        Value::Null => {}
20567        Value::Object(_) => {
20568            return Err(SidecarError::InvalidState(format!(
20569                "unsupported HTTP/2 header object value for {name}"
20570            )));
20571        }
20572    }
20573    Ok(())
20574}
20575
20576fn build_http2_request(headers_json: &str) -> Result<Request<()>, SidecarError> {
20577    let headers = parse_http2_headers_json(headers_json, "HTTP/2 request headers")?;
20578    let method = headers
20579        .get(":method")
20580        .and_then(Value::as_str)
20581        .unwrap_or("GET");
20582    let path = headers.get(":path").and_then(Value::as_str).unwrap_or("/");
20583    let mut builder = Request::builder()
20584        .method(Method::from_bytes(method.as_bytes()).map_err(|error| {
20585            SidecarError::InvalidState(format!("invalid HTTP/2 method {method:?}: {error}"))
20586        })?)
20587        .uri(path.parse::<Uri>().map_err(|error| {
20588            SidecarError::InvalidState(format!("invalid HTTP/2 path {path:?}: {error}"))
20589        })?);
20590    {
20591        let header_map = builder.headers_mut().expect("request header map");
20592        for (name, value) in &headers {
20593            if name.starts_with(':') {
20594                continue;
20595            }
20596            apply_http2_header_values(header_map, name, value)?;
20597        }
20598    }
20599    builder
20600        .body(())
20601        .map_err(|error| SidecarError::InvalidState(format!("invalid HTTP/2 request: {error}")))
20602}
20603
20604fn build_http2_response(headers_json: &str) -> Result<Response<()>, SidecarError> {
20605    let headers = parse_http2_headers_json(headers_json, "HTTP/2 response headers")?;
20606    let status = headers
20607        .get(":status")
20608        .and_then(Value::as_u64)
20609        .or_else(|| {
20610            headers
20611                .get(":status")
20612                .and_then(Value::as_str)
20613                .and_then(|value| value.parse::<u16>().ok().map(u64::from))
20614        })
20615        .unwrap_or(200);
20616    let mut builder = Response::builder().status(status as u16);
20617    {
20618        let header_map = builder.headers_mut().expect("response header map");
20619        for (name, value) in &headers {
20620            if name.starts_with(':') {
20621                continue;
20622            }
20623            apply_http2_header_values(header_map, name, value)?;
20624        }
20625    }
20626    builder.body(()).map_err(|error| {
20627        SidecarError::InvalidState(format!("invalid HTTP/2 response headers: {error}"))
20628    })
20629}
20630
20631fn serialize_http2_headers_map(
20632    pseudo: BTreeMap<String, Value>,
20633    headers: &HeaderMap,
20634) -> Result<String, SidecarError> {
20635    let mut serialized = pseudo;
20636    for (name, value) in headers {
20637        let name = name.as_str().to_string();
20638        let value = Value::String(
20639            value
20640                .to_str()
20641                .map_err(|error| {
20642                    SidecarError::Execution(format!("invalid HTTP/2 header value: {error}"))
20643                })?
20644                .to_owned(),
20645        );
20646        match serialized.get_mut(&name) {
20647            Some(Value::Array(values)) => values.push(value),
20648            Some(existing) => {
20649                let first = existing.clone();
20650                *existing = Value::Array(vec![first, value]);
20651            }
20652            None => {
20653                serialized.insert(name, value);
20654            }
20655        }
20656    }
20657    serde_json::to_string(&serialized)
20658        .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
20659}
20660
20661fn serialize_http2_request_headers(
20662    request: &Request<h2::RecvStream>,
20663) -> Result<String, SidecarError> {
20664    let mut pseudo = BTreeMap::new();
20665    pseudo.insert(
20666        String::from(":method"),
20667        Value::String(request.method().as_str().to_string()),
20668    );
20669    pseudo.insert(
20670        String::from(":path"),
20671        Value::String(
20672            request
20673                .uri()
20674                .path_and_query()
20675                .map(|value| value.as_str().to_string())
20676                .unwrap_or_else(|| String::from("/")),
20677        ),
20678    );
20679    serialize_http2_headers_map(pseudo, request.headers())
20680}
20681
20682fn serialize_http2_response_headers(
20683    response: &Response<h2::RecvStream>,
20684) -> Result<String, SidecarError> {
20685    let mut pseudo = BTreeMap::new();
20686    pseudo.insert(
20687        String::from(":status"),
20688        Value::Number(serde_json::Number::from(response.status().as_u16())),
20689    );
20690    serialize_http2_headers_map(pseudo, response.headers())
20691}
20692
20693fn remove_http2_session_resources(
20694    shared: &Arc<Mutex<crate::state::Http2SharedState>>,
20695    session_id: u64,
20696) {
20697    if let Ok(mut state) = shared.lock() {
20698        state.sessions.remove(&session_id);
20699        state.session_events.remove(&session_id);
20700        let stream_ids = state
20701            .streams
20702            .iter()
20703            .filter_map(|(stream_id, stream)| {
20704                (stream.session_id == session_id).then_some(*stream_id)
20705            })
20706            .collect::<Vec<_>>();
20707        for stream_id in stream_ids {
20708            state.streams.remove(&stream_id);
20709        }
20710    }
20711}
20712
20713fn spawn_http2_client_session(
20714    shared: Arc<Mutex<crate::state::Http2SharedState>>,
20715    session_id: u64,
20716    remote_addr: SocketAddr,
20717    tls: Option<JavascriptTlsBridgeOptions>,
20718    snapshot: Arc<Mutex<Http2SessionSnapshot>>,
20719    mut command_rx: UnboundedReceiver<Http2SessionCommand>,
20720) {
20721    thread::spawn(move || {
20722        let runtime = match TokioRuntimeBuilder::new_current_thread()
20723            .enable_all()
20724            .build()
20725        {
20726            Ok(runtime) => runtime,
20727            Err(error) => {
20728                push_http2_session_event(
20729                    &shared,
20730                    session_id,
20731                    Http2BridgeEvent {
20732                        kind: String::from("sessionError"),
20733                        id: session_id,
20734                        data: Some(http2_error_payload(error.to_string())),
20735                        ..Http2BridgeEvent::default()
20736                    },
20737                );
20738                remove_http2_session_resources(&shared, session_id);
20739                return;
20740            }
20741        };
20742
20743        runtime.block_on(async move {
20744            let stream = match tokio::net::TcpStream::connect(remote_addr).await {
20745                Ok(stream) => stream,
20746                Err(error) => {
20747                    push_http2_session_event(
20748                        &shared,
20749                        session_id,
20750                        Http2BridgeEvent {
20751                            kind: String::from("sessionError"),
20752                            id: session_id,
20753                            data: Some(http2_error_payload(error.to_string())),
20754                            ..Http2BridgeEvent::default()
20755                        },
20756                    );
20757                    remove_http2_session_resources(&shared, session_id);
20758                    return;
20759                }
20760            };
20761
20762            let local_addr = match stream.local_addr() {
20763                Ok(addr) => addr,
20764                Err(error) => {
20765                    push_http2_session_event(
20766                        &shared,
20767                        session_id,
20768                        Http2BridgeEvent {
20769                            kind: String::from("sessionError"),
20770                            id: session_id,
20771                            data: Some(http2_error_payload(error.to_string())),
20772                            ..Http2BridgeEvent::default()
20773                        },
20774                    );
20775                    remove_http2_session_resources(&shared, session_id);
20776                    return;
20777                }
20778            };
20779
20780            {
20781                let mut snapshot_guard = snapshot.lock().expect("http2 snapshot lock");
20782                snapshot_guard.socket = http2_socket_snapshot(local_addr, remote_addr);
20783                if let Some(options) = tls.as_ref() {
20784                    snapshot_guard.encrypted = true;
20785                    snapshot_guard.alpn_protocol = Some(String::from("h2"));
20786                    snapshot_guard.socket.encrypted = true;
20787                    snapshot_guard.socket.servername = options.servername.clone();
20788                    snapshot_guard.socket.alpn_protocol = Some(String::from("h2"));
20789                }
20790                snapshot_guard.state = http2_runtime_snapshot();
20791            }
20792            if let Ok(snapshot_json) =
20793                http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())
20794            {
20795                push_http2_session_event(
20796                    &shared,
20797                    session_id,
20798                    Http2BridgeEvent {
20799                        kind: String::from("sessionConnect"),
20800                        id: session_id,
20801                        data: Some(snapshot_json),
20802                        ..Http2BridgeEvent::default()
20803                    },
20804                );
20805            }
20806
20807            let io: Pin<Box<dyn Http2AsyncIo>> = if let Some(options) = tls.as_ref() {
20808                let server_name = match ServerName::try_from(
20809                    options
20810                        .servername
20811                        .clone()
20812                        .unwrap_or_else(|| String::from("localhost")),
20813                ) {
20814                    Ok(server_name) => server_name,
20815                    Err(_) => {
20816                        push_http2_session_event(
20817                            &shared,
20818                            session_id,
20819                            Http2BridgeEvent {
20820                                kind: String::from("sessionError"),
20821                                id: session_id,
20822                                data: Some(http2_error_payload("invalid TLS servername")),
20823                                ..Http2BridgeEvent::default()
20824                            },
20825                        );
20826                        remove_http2_session_resources(&shared, session_id);
20827                        return;
20828                    }
20829                };
20830                let connector = match build_client_tls_config(options) {
20831                    Ok(config) => TlsConnector::from(Arc::new(config)),
20832                    Err(error) => {
20833                        push_http2_session_event(
20834                            &shared,
20835                            session_id,
20836                            Http2BridgeEvent {
20837                                kind: String::from("sessionError"),
20838                                id: session_id,
20839                                data: Some(http2_error_payload(error.to_string())),
20840                                ..Http2BridgeEvent::default()
20841                            },
20842                        );
20843                        remove_http2_session_resources(&shared, session_id);
20844                        return;
20845                    }
20846                };
20847                match connector.connect(server_name, stream).await {
20848                    Ok(tls_stream) => Box::pin(tls_stream),
20849                    Err(error) => {
20850                        push_http2_session_event(
20851                            &shared,
20852                            session_id,
20853                            Http2BridgeEvent {
20854                                kind: String::from("sessionError"),
20855                                id: session_id,
20856                                data: Some(http2_error_payload(error.to_string())),
20857                                ..Http2BridgeEvent::default()
20858                            },
20859                        );
20860                        remove_http2_session_resources(&shared, session_id);
20861                        return;
20862                    }
20863                }
20864            } else {
20865                Box::pin(stream)
20866            };
20867
20868            let (mut sender, connection) = match client::handshake(io).await {
20869                Ok(parts) => parts,
20870                Err(error) => {
20871                    push_http2_session_event(
20872                        &shared,
20873                        session_id,
20874                        Http2BridgeEvent {
20875                            kind: String::from("sessionError"),
20876                            id: session_id,
20877                            data: Some(http2_error_payload(error.to_string())),
20878                            ..Http2BridgeEvent::default()
20879                        },
20880                    );
20881                    remove_http2_session_resources(&shared, session_id);
20882                    return;
20883                }
20884            };
20885
20886            let (status_tx, mut status_rx) = unbounded_channel::<Result<(), String>>();
20887            tokio::spawn(async move {
20888                let _ = status_tx.send(connection.await.map_err(|error| error.to_string()));
20889            });
20890
20891            let streams: Arc<Mutex<BTreeMap<u64, ClientHttp2StreamState>>> =
20892                Arc::new(Mutex::new(BTreeMap::new()));
20893
20894            loop {
20895                tokio::select! {
20896                    Some(result) = status_rx.recv() => {
20897                        if let Err(message) = result {
20898                            push_http2_session_event(
20899                                &shared,
20900                                session_id,
20901                                Http2BridgeEvent {
20902                                    kind: String::from("sessionError"),
20903                                    id: session_id,
20904                                    data: Some(http2_error_payload(message)),
20905                                    ..Http2BridgeEvent::default()
20906                                },
20907                            );
20908                        }
20909                        push_http2_session_event(
20910                            &shared,
20911                            session_id,
20912                            Http2BridgeEvent {
20913                                kind: String::from("sessionClose"),
20914                                id: session_id,
20915                                ..Http2BridgeEvent::default()
20916                            },
20917                        );
20918                        remove_http2_session_resources(&shared, session_id);
20919                        break;
20920                    }
20921                    Some(command) = command_rx.recv() => {
20922                        match command {
20923                            Http2SessionCommand::Request { headers_json, options_json, respond_to } => {
20924                                let request = match build_http2_request(&headers_json) {
20925                                    Ok(request) => request,
20926                                    Err(error) => {
20927                                        let _ = respond_to.send(Err(error.to_string()));
20928                                        continue;
20929                                    }
20930                                };
20931                                let options: JavascriptHttp2RequestOptions =
20932                                    serde_json::from_str(&options_json).unwrap_or_default();
20933                                let stream_id = {
20934                                    let mut state = shared.lock().expect("http2 shared state");
20935                                    let stream_id = next_http2_stream_id(&mut state);
20936                                    state.streams.insert(
20937                                        stream_id,
20938                                        ActiveHttp2Stream {
20939                                            session_id,
20940                                            paused: Arc::new(AtomicBool::new(false)),
20941                                            resume_notify: Arc::new(tokio::sync::Notify::new()),
20942                                        },
20943                                    );
20944                                    stream_id
20945                                };
20946                                match sender.send_request(request, options.end_stream) {
20947                                    Ok((response_future, send_stream)) => {
20948                                        if !options.end_stream {
20949                                            streams
20950                                                .lock()
20951                                                .expect("http2 client streams")
20952                                                .insert(stream_id, ClientHttp2StreamState { send_stream: Some(send_stream) });
20953                                        }
20954                                        let shared_clone = Arc::clone(&shared);
20955                                        let snapshot_clone = Arc::clone(&snapshot);
20956                                        tokio::spawn(async move {
20957                                            match response_future.await {
20958                                                Ok(response) => {
20959                                                    if let Ok(headers_json) = serialize_http2_response_headers(&response) {
20960                                                        push_http2_session_event(
20961                                                            &shared_clone,
20962                                                            session_id,
20963                                                            Http2BridgeEvent {
20964                                                                kind: String::from("clientResponseHeaders"),
20965                                                                id: stream_id,
20966                                                                data: Some(headers_json),
20967                                                                ..Http2BridgeEvent::default()
20968                                                            },
20969                                                        );
20970                                                    }
20971                                                    let mut body = response.into_body();
20972                                                    while let Some(chunk) = body.data().await {
20973                                                        match chunk {
20974                                                            Ok(bytes) => {
20975                                                                let pause_state = {
20976                                                                    let state = shared_clone.lock().expect("http2 shared state");
20977                                                                    state.streams.get(&stream_id).map(|stream| {
20978                                                                        (
20979                                                                            Arc::clone(&stream.paused),
20980                                                                            Arc::clone(&stream.resume_notify),
20981                                                                        )
20982                                                                    })
20983                                                                };
20984                                                                if let Some((paused, resume_notify)) = pause_state {
20985                                                                    while paused.load(Ordering::SeqCst) {
20986                                                                        let notified = resume_notify.notified();
20987                                                                        tokio::pin!(notified);
20988                                                                        notified.as_mut().enable();
20989                                                                        if !paused.load(Ordering::SeqCst) { break; }
20990                                                                        let _ = tokio::time::timeout(Duration::from_millis(250), notified).await;
20991                                                                    }
20992                                                                }
20993                                                                let _ = body.flow_control().release_capacity(bytes.len());
20994                                                                push_http2_session_event(
20995                                                                    &shared_clone,
20996                                                                    session_id,
20997                                                                    Http2BridgeEvent {
20998                                                                        kind: String::from("clientData"),
20999                                                                        id: stream_id,
21000                                                                        data: Some(base64::engine::general_purpose::STANDARD.encode(bytes)),
21001                                                                        ..Http2BridgeEvent::default()
21002                                                                    },
21003                                                                );
21004                                                            }
21005                                                            Err(error) => {
21006                                                                push_http2_session_event(
21007                                                                    &shared_clone,
21008                                                                    session_id,
21009                                                                    Http2BridgeEvent {
21010                                                                        kind: String::from("clientError"),
21011                                                                        id: stream_id,
21012                                                                        data: Some(http2_error_payload(error.to_string())),
21013                                                                        ..Http2BridgeEvent::default()
21014                                                                    },
21015                                                                );
21016                                                                break;
21017                                                            }
21018                                                        }
21019                                                    }
21020                                                    {
21021                                                        let mut snapshot = snapshot_clone.lock().expect("http2 snapshot lock");
21022                                                        snapshot.state.next_stream_id =
21023                                                            snapshot.state.next_stream_id.saturating_add(2);
21024                                                    }
21025                                                    push_http2_session_event(
21026                                                        &shared_clone,
21027                                                        session_id,
21028                                                        Http2BridgeEvent {
21029                                                            kind: String::from("clientEnd"),
21030                                                            id: stream_id,
21031                                                            ..Http2BridgeEvent::default()
21032                                                        },
21033                                                    );
21034                                                    push_http2_session_event(
21035                                                        &shared_clone,
21036                                                        session_id,
21037                                                        Http2BridgeEvent {
21038                                                            kind: String::from("clientClose"),
21039                                                            id: stream_id,
21040                                                            extra_number: Some(0),
21041                                                            ..Http2BridgeEvent::default()
21042                                                        },
21043                                                    );
21044                                                    if let Ok(mut state) = shared_clone.lock() {
21045                                                        state.streams.remove(&stream_id);
21046                                                    }
21047                                                }
21048                                                Err(error) => {
21049                                                    push_http2_session_event(
21050                                                        &shared_clone,
21051                                                        session_id,
21052                                                        Http2BridgeEvent {
21053                                                            kind: String::from("clientError"),
21054                                                            id: stream_id,
21055                                                            data: Some(http2_error_payload(error.to_string())),
21056                                                            ..Http2BridgeEvent::default()
21057                                                        },
21058                                                    );
21059                                                    push_http2_session_event(
21060                                                        &shared_clone,
21061                                                        session_id,
21062                                                        Http2BridgeEvent {
21063                                                            kind: String::from("clientClose"),
21064                                                            id: stream_id,
21065                                                            extra_number: Some(u32::from(Reason::INTERNAL_ERROR) as u64),
21066                                                            ..Http2BridgeEvent::default()
21067                                                        },
21068                                                    );
21069                                                    if let Ok(mut state) = shared_clone.lock() {
21070                                                        state.streams.remove(&stream_id);
21071                                                    }
21072                                                }
21073                                            }
21074                                        });
21075                                        let _ = respond_to.send(Ok(json!(stream_id)));
21076                                    }
21077                                    Err(error) => {
21078                                        if let Ok(mut state) = shared.lock() {
21079                                            state.streams.remove(&stream_id);
21080                                        }
21081                                        let _ = respond_to.send(Err(error.to_string()));
21082                                    }
21083                                }
21084                            }
21085                            Http2SessionCommand::Settings { settings_json, respond_to } => {
21086                                let settings = serde_json::from_str::<BTreeMap<String, Value>>(&settings_json)
21087                                    .unwrap_or_default();
21088                                {
21089                                    let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
21090                                    snapshot.local_settings = http2_settings_from_value(&settings);
21091                                }
21092                                if let Ok(headers_json) = serde_json::to_string(&settings) {
21093                                    push_http2_session_event(
21094                                        &shared,
21095                                        session_id,
21096                                        Http2BridgeEvent {
21097                                            kind: String::from("sessionLocalSettings"),
21098                                            id: session_id,
21099                                            data: Some(headers_json.clone()),
21100                                            ..Http2BridgeEvent::default()
21101                                        },
21102                                    );
21103                                    push_http2_session_event(
21104                                        &shared,
21105                                        session_id,
21106                                        Http2BridgeEvent {
21107                                            kind: String::from("sessionSettingsAck"),
21108                                            id: session_id,
21109                                            ..Http2BridgeEvent::default()
21110                                        },
21111                                    );
21112                                }
21113                                let _ = respond_to.send(Ok(Value::Null));
21114                            }
21115                            Http2SessionCommand::SetLocalWindowSize { size, respond_to } => {
21116                                {
21117                                    let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
21118                                    snapshot.state.local_window_size = size;
21119                                    snapshot.state.effective_local_window_size = size;
21120                                }
21121                                let value = snapshot
21122                                    .lock()
21123                                    .ok()
21124                                    .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok())
21125                                    .map(Value::String)
21126                                    .unwrap_or(Value::Null);
21127                                let _ = respond_to.send(Ok(value));
21128                            }
21129                            Http2SessionCommand::Goaway { error_code, last_stream_id, opaque_data, respond_to } => {
21130                                push_http2_session_event(
21131                                    &shared,
21132                                    session_id,
21133                                    Http2BridgeEvent {
21134                                        kind: String::from("sessionGoaway"),
21135                                        id: session_id,
21136                                        data: opaque_data.map(|value| {
21137                                            base64::engine::general_purpose::STANDARD.encode(value)
21138                                        }),
21139                                        extra_number: Some(error_code as u64),
21140                                        flags: Some(last_stream_id as u64),
21141                                        ..Http2BridgeEvent::default()
21142                                    },
21143                                );
21144                                let _ = respond_to.send(Ok(Value::Null));
21145                            }
21146                            Http2SessionCommand::Close { respond_to, .. } => {
21147                                let _ = respond_to.send(Ok(Value::Null));
21148                                push_http2_session_event(
21149                                    &shared,
21150                                    session_id,
21151                                    Http2BridgeEvent {
21152                                        kind: String::from("sessionClose"),
21153                                        id: session_id,
21154                                        ..Http2BridgeEvent::default()
21155                                    },
21156                                );
21157                                remove_http2_session_resources(&shared, session_id);
21158                                break;
21159                            }
21160                            Http2SessionCommand::StreamWrite { stream_id, chunk, end_stream, respond_to } => {
21161                                let result = streams
21162                                    .lock()
21163                                    .expect("http2 client streams")
21164                                    .get_mut(&stream_id)
21165                                    .and_then(|stream| stream.send_stream.as_mut())
21166                                    .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 client stream {stream_id}")))
21167                                    .and_then(|stream| stream.send_data(Bytes::from(chunk), end_stream).map_err(|error| SidecarError::Execution(error.to_string())));
21168                                match result {
21169                                    Ok(()) => {
21170                                        if end_stream {
21171                                            streams.lock().expect("http2 client streams").remove(&stream_id);
21172                                        }
21173                                        let _ = respond_to.send(Ok(Value::Bool(true)));
21174                                    }
21175                                    Err(error) => {
21176                                        let _ = respond_to.send(Err(error.to_string()));
21177                                    }
21178                                }
21179                            }
21180                            Http2SessionCommand::StreamClose { stream_id, error_code, respond_to } => {
21181                                let mut streams = streams.lock().expect("http2 client streams");
21182                                let Some(mut state) = streams.remove(&stream_id) else {
21183                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 client stream {stream_id}")));
21184                                    continue;
21185                                };
21186                                if let Some(stream) = state.send_stream.as_mut() {
21187                                    stream.send_reset(http2_reason(error_code));
21188                                }
21189                                if let Ok(mut state) = shared.lock() {
21190                                    state.streams.remove(&stream_id);
21191                                }
21192                                push_http2_session_event(
21193                                    &shared,
21194                                    session_id,
21195                                    Http2BridgeEvent {
21196                                        kind: String::from("clientClose"),
21197                                        id: stream_id,
21198                                        extra_number: Some(u32::from(http2_reason(error_code)) as u64),
21199                                        ..Http2BridgeEvent::default()
21200                                    },
21201                                );
21202                                let _ = respond_to.send(Ok(Value::Null));
21203                            }
21204                            Http2SessionCommand::StreamRespond { respond_to, .. }
21205                            | Http2SessionCommand::StreamPush { respond_to, .. }
21206                            | Http2SessionCommand::StreamRespondWithFile { respond_to, .. } => {
21207                                let _ = respond_to.send(Err(String::from("HTTP/2 client streams cannot send server responses")));
21208                            }
21209                        }
21210                    }
21211                    else => break,
21212                }
21213            }
21214        });
21215    });
21216}
21217
21218fn spawn_http2_server_session(
21219    shared: Arc<Mutex<crate::state::Http2SharedState>>,
21220    server_id: u64,
21221    session_id: u64,
21222    stream: TcpStream,
21223    tls: Option<JavascriptTlsBridgeOptions>,
21224    snapshot: Arc<Mutex<Http2SessionSnapshot>>,
21225    mut command_rx: UnboundedReceiver<Http2SessionCommand>,
21226) {
21227    thread::spawn(move || {
21228        let runtime = match TokioRuntimeBuilder::new_current_thread()
21229            .enable_all()
21230            .build()
21231        {
21232            Ok(runtime) => runtime,
21233            Err(error) => {
21234                push_http2_server_event(
21235                    &shared,
21236                    server_id,
21237                    Http2BridgeEvent {
21238                        kind: String::from("serverStreamError"),
21239                        id: session_id,
21240                        data: Some(http2_error_payload(error.to_string())),
21241                        ..Http2BridgeEvent::default()
21242                    },
21243                );
21244                remove_http2_session_resources(&shared, session_id);
21245                return;
21246            }
21247        };
21248
21249        runtime.block_on(async move {
21250            if let Err(error) = stream.set_nonblocking(true) {
21251                push_http2_server_event(
21252                    &shared,
21253                    server_id,
21254                    Http2BridgeEvent {
21255                        kind: String::from("serverStreamError"),
21256                        id: session_id,
21257                        data: Some(http2_error_payload(error.to_string())),
21258                        ..Http2BridgeEvent::default()
21259                    },
21260                );
21261                remove_http2_session_resources(&shared, session_id);
21262                return;
21263            }
21264            let stream = match tokio::net::TcpStream::from_std(stream) {
21265                Ok(stream) => stream,
21266                Err(error) => {
21267                    push_http2_server_event(
21268                        &shared,
21269                        server_id,
21270                        Http2BridgeEvent {
21271                            kind: String::from("serverStreamError"),
21272                            id: session_id,
21273                            data: Some(http2_error_payload(error.to_string())),
21274                            ..Http2BridgeEvent::default()
21275                        },
21276                    );
21277                    remove_http2_session_resources(&shared, session_id);
21278                    return;
21279                }
21280            };
21281            let local_addr = match stream.local_addr() {
21282                Ok(addr) => addr,
21283                Err(error) => {
21284                    push_http2_server_event(
21285                        &shared,
21286                        server_id,
21287                        Http2BridgeEvent {
21288                            kind: String::from("serverStreamError"),
21289                            id: session_id,
21290                            data: Some(http2_error_payload(error.to_string())),
21291                            ..Http2BridgeEvent::default()
21292                        },
21293                    );
21294                    remove_http2_session_resources(&shared, session_id);
21295                    return;
21296                }
21297            };
21298            let remote_addr = match stream.peer_addr() {
21299                Ok(addr) => addr,
21300                Err(error) => {
21301                    push_http2_server_event(
21302                        &shared,
21303                        server_id,
21304                        Http2BridgeEvent {
21305                            kind: String::from("serverStreamError"),
21306                            id: session_id,
21307                            data: Some(http2_error_payload(error.to_string())),
21308                            ..Http2BridgeEvent::default()
21309                        },
21310                    );
21311                    remove_http2_session_resources(&shared, session_id);
21312                    return;
21313                }
21314            };
21315            {
21316                let mut snapshot_guard = snapshot.lock().expect("http2 snapshot lock");
21317                snapshot_guard.socket = http2_socket_snapshot(local_addr, remote_addr);
21318                if tls.is_some() {
21319                    snapshot_guard.encrypted = true;
21320                    snapshot_guard.alpn_protocol = Some(String::from("h2"));
21321                    snapshot_guard.socket.encrypted = true;
21322                    snapshot_guard.socket.alpn_protocol = Some(String::from("h2"));
21323                }
21324                snapshot_guard.state = http2_runtime_snapshot();
21325            }
21326            if let Ok(snapshot_json) =
21327                http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())
21328            {
21329                push_http2_server_event(
21330                    &shared,
21331                    server_id,
21332                    Http2BridgeEvent {
21333                        kind: String::from(if tls.is_some() {
21334                            "serverSecureConnection"
21335                        } else {
21336                            "serverConnection"
21337                        }),
21338                        id: server_id,
21339                        data: Some(serde_json::to_string(&http2_socket_snapshot(local_addr, remote_addr)).unwrap_or_default()),
21340                        ..Http2BridgeEvent::default()
21341                    },
21342                );
21343                push_http2_server_event(
21344                    &shared,
21345                    server_id,
21346                    Http2BridgeEvent {
21347                        kind: String::from("serverSession"),
21348                        id: server_id,
21349                        data: Some(snapshot_json),
21350                        extra_number: Some(session_id),
21351                        ..Http2BridgeEvent::default()
21352                    },
21353                );
21354            }
21355
21356            let io: Pin<Box<dyn Http2AsyncIo>> = if let Some(options) = tls.as_ref() {
21357                let acceptor = match build_server_tls_config(options) {
21358                    Ok(config) => TlsAcceptor::from(Arc::new(config)),
21359                    Err(error) => {
21360                        push_http2_server_event(
21361                            &shared,
21362                            server_id,
21363                            Http2BridgeEvent {
21364                                kind: String::from("serverStreamError"),
21365                                id: session_id,
21366                                data: Some(http2_error_payload(error.to_string())),
21367                                ..Http2BridgeEvent::default()
21368                            },
21369                        );
21370                        remove_http2_session_resources(&shared, session_id);
21371                        return;
21372                    }
21373                };
21374                match acceptor.accept(stream).await {
21375                    Ok(tls_stream) => Box::pin(tls_stream),
21376                    Err(error) => {
21377                        push_http2_server_event(
21378                            &shared,
21379                            server_id,
21380                            Http2BridgeEvent {
21381                                kind: String::from("serverStreamError"),
21382                                id: session_id,
21383                                data: Some(http2_error_payload(error.to_string())),
21384                                ..Http2BridgeEvent::default()
21385                            },
21386                        );
21387                        remove_http2_session_resources(&shared, session_id);
21388                        return;
21389                    }
21390                }
21391            } else {
21392                Box::pin(stream)
21393            };
21394
21395            let mut connection = match server::handshake(io).await {
21396                Ok(connection) => connection,
21397                Err(error) => {
21398                    push_http2_server_event(
21399                        &shared,
21400                        server_id,
21401                        Http2BridgeEvent {
21402                            kind: String::from("serverStreamError"),
21403                            id: session_id,
21404                            data: Some(http2_error_payload(error.to_string())),
21405                            ..Http2BridgeEvent::default()
21406                        },
21407                    );
21408                    remove_http2_session_resources(&shared, session_id);
21409                    return;
21410                }
21411            };
21412
21413            let streams: Arc<Mutex<BTreeMap<u64, ServerHttp2StreamState>>> =
21414                Arc::new(Mutex::new(BTreeMap::new()));
21415
21416            loop {
21417                tokio::select! {
21418                    incoming = connection.accept() => {
21419                        match incoming {
21420                            Some(Ok((request, respond))) => {
21421                                let headers_json = match serialize_http2_request_headers(&request) {
21422                                    Ok(headers) => headers,
21423                                    Err(error) => {
21424                                        push_http2_server_event(
21425                                            &shared,
21426                                            server_id,
21427                                            Http2BridgeEvent {
21428                                                kind: String::from("serverStreamError"),
21429                                                id: server_id,
21430                                                data: Some(http2_error_payload(error.to_string())),
21431                                                ..Http2BridgeEvent::default()
21432                                            },
21433                                        );
21434                                        continue;
21435                                    }
21436                                };
21437                                let stream_id = {
21438                                    let mut state = shared.lock().expect("http2 shared state");
21439                                    let stream_id = next_http2_stream_id(&mut state);
21440                                    state.streams.insert(
21441                                        stream_id,
21442                                        ActiveHttp2Stream {
21443                                            session_id,
21444                                            paused: Arc::new(AtomicBool::new(false)),
21445                                            resume_notify: Arc::new(tokio::sync::Notify::new()),
21446                                        },
21447                                    );
21448                                    stream_id
21449                                };
21450                                streams.lock().expect("http2 server streams").insert(
21451                                    stream_id,
21452                                    ServerHttp2StreamState {
21453                                        send_response: Some(ServerHttp2Responder::Regular(respond)),
21454                                        send_stream: None,
21455                                    },
21456                                );
21457                                let snapshot_json = snapshot
21458                                    .lock()
21459                                    .ok()
21460                                    .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok());
21461                                push_http2_server_event(
21462                                    &shared,
21463                                    server_id,
21464                                    Http2BridgeEvent {
21465                                        kind: String::from("serverStream"),
21466                                        id: server_id,
21467                                        data: Some(stream_id.to_string()),
21468                                        extra: snapshot_json,
21469                                        extra_number: Some(session_id),
21470                                        extra_headers: Some(headers_json),
21471                                        flags: Some(0),
21472                                    },
21473                                );
21474                                let shared_clone = Arc::clone(&shared);
21475                                tokio::spawn(async move {
21476                                    let mut body = request.into_body();
21477                                    while let Some(chunk) = body.data().await {
21478                                        match chunk {
21479                                            Ok(bytes) => {
21480                                                let pause_state = {
21481                                                    let state = shared_clone.lock().expect("http2 shared state");
21482                                                    state.streams.get(&stream_id).map(|stream| {
21483                                                        (
21484                                                            Arc::clone(&stream.paused),
21485                                                            Arc::clone(&stream.resume_notify),
21486                                                        )
21487                                                    })
21488                                                };
21489                                                if let Some((paused, resume_notify)) = pause_state {
21490                                                    while paused.load(Ordering::SeqCst) {
21491                                                        let notified = resume_notify.notified();
21492                                                        tokio::pin!(notified);
21493                                                        notified.as_mut().enable();
21494                                                        if !paused.load(Ordering::SeqCst) { break; }
21495                                                        let _ = tokio::time::timeout(Duration::from_millis(250), notified).await;
21496                                                    }
21497                                                }
21498                                                let _ = body.flow_control().release_capacity(bytes.len());
21499                                                push_http2_server_event(
21500                                                    &shared_clone,
21501                                                    server_id,
21502                                                    Http2BridgeEvent {
21503                                                        kind: String::from("serverStreamData"),
21504                                                        id: stream_id,
21505                                                        data: Some(base64::engine::general_purpose::STANDARD.encode(bytes)),
21506                                                        ..Http2BridgeEvent::default()
21507                                                    },
21508                                                );
21509                                            }
21510                                            Err(error) => {
21511                                                push_http2_server_event(
21512                                                    &shared_clone,
21513                                                    server_id,
21514                                                    Http2BridgeEvent {
21515                                                        kind: String::from("serverStreamError"),
21516                                                        id: stream_id,
21517                                                        data: Some(http2_error_payload(error.to_string())),
21518                                                        ..Http2BridgeEvent::default()
21519                                                    },
21520                                                );
21521                                                break;
21522                                            }
21523                                        }
21524                                    }
21525                                    push_http2_server_event(
21526                                        &shared_clone,
21527                                        server_id,
21528                                        Http2BridgeEvent {
21529                                            kind: String::from("serverStreamEnd"),
21530                                            id: stream_id,
21531                                            ..Http2BridgeEvent::default()
21532                                        },
21533                                    );
21534                                });
21535                            }
21536                            Some(Err(error)) => {
21537                                push_http2_server_event(
21538                                    &shared,
21539                                    server_id,
21540                                    Http2BridgeEvent {
21541                                        kind: String::from("serverStreamError"),
21542                                        id: server_id,
21543                                        data: Some(http2_error_payload(error.to_string())),
21544                                        ..Http2BridgeEvent::default()
21545                                    },
21546                                );
21547                                break;
21548                            }
21549                            None => {
21550                                push_http2_server_event(
21551                                    &shared,
21552                                    server_id,
21553                                    Http2BridgeEvent {
21554                                        kind: String::from("sessionClose"),
21555                                        id: session_id,
21556                                        ..Http2BridgeEvent::default()
21557                                    },
21558                                );
21559                                remove_http2_session_resources(&shared, session_id);
21560                                break;
21561                            }
21562                        }
21563                    }
21564                    Some(command) = command_rx.recv() => {
21565                        match command {
21566                            Http2SessionCommand::Settings { settings_json, respond_to } => {
21567                                let settings = serde_json::from_str::<BTreeMap<String, Value>>(&settings_json)
21568                                    .unwrap_or_default();
21569                                if let Some(initial_window_size) = settings
21570                                    .get("initialWindowSize")
21571                                    .and_then(Value::as_u64)
21572                                {
21573                                    let _ = connection.set_initial_window_size(initial_window_size as u32);
21574                                }
21575                                {
21576                                    let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
21577                                    snapshot.local_settings = http2_settings_from_value(&settings);
21578                                }
21579                                if let Ok(headers_json) = serde_json::to_string(&settings) {
21580                                    push_http2_session_event(
21581                                        &shared,
21582                                        session_id,
21583                                        Http2BridgeEvent {
21584                                            kind: String::from("sessionLocalSettings"),
21585                                            id: session_id,
21586                                            data: Some(headers_json),
21587                                            ..Http2BridgeEvent::default()
21588                                        },
21589                                    );
21590                                }
21591                                let _ = respond_to.send(Ok(Value::Null));
21592                            }
21593                            Http2SessionCommand::SetLocalWindowSize { size, respond_to } => {
21594                                connection.set_target_window_size(size);
21595                                {
21596                                    let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
21597                                    snapshot.state.local_window_size = size;
21598                                    snapshot.state.effective_local_window_size = size;
21599                                }
21600                                let value = snapshot
21601                                    .lock()
21602                                    .ok()
21603                                    .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok())
21604                                    .map(Value::String)
21605                                    .unwrap_or(Value::Null);
21606                                let _ = respond_to.send(Ok(value));
21607                            }
21608                            Http2SessionCommand::Goaway { error_code, last_stream_id, opaque_data, respond_to } => {
21609                                connection.abrupt_shutdown(http2_reason(Some(error_code)));
21610                                push_http2_session_event(
21611                                    &shared,
21612                                    session_id,
21613                                    Http2BridgeEvent {
21614                                        kind: String::from("sessionGoaway"),
21615                                        id: session_id,
21616                                        data: opaque_data.map(|value| {
21617                                            base64::engine::general_purpose::STANDARD.encode(value)
21618                                        }),
21619                                        extra_number: Some(error_code as u64),
21620                                        flags: Some(last_stream_id as u64),
21621                                        ..Http2BridgeEvent::default()
21622                                    },
21623                                );
21624                                let _ = respond_to.send(Ok(Value::Null));
21625                            }
21626                            Http2SessionCommand::Close { abrupt, respond_to } => {
21627                                if abrupt {
21628                                    connection.abrupt_shutdown(Reason::NO_ERROR);
21629                                } else {
21630                                    connection.graceful_shutdown();
21631                                }
21632                                let _ = respond_to.send(Ok(Value::Null));
21633                                push_http2_session_event(
21634                                    &shared,
21635                                    session_id,
21636                                    Http2BridgeEvent {
21637                                        kind: String::from("sessionClose"),
21638                                        id: session_id,
21639                                        ..Http2BridgeEvent::default()
21640                                    },
21641                                );
21642                                remove_http2_session_resources(&shared, session_id);
21643                                break;
21644                            }
21645                            Http2SessionCommand::StreamRespond { stream_id, headers_json, respond_to } => {
21646                                let response = match build_http2_response(&headers_json) {
21647                                    Ok(response) => response,
21648                                    Err(error) => {
21649                                        let _ = respond_to.send(Err(error.to_string()));
21650                                        continue;
21651                                    }
21652                                };
21653                                let mut streams = streams.lock().expect("http2 server streams");
21654                                let Some(state) = streams.get_mut(&stream_id) else {
21655                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
21656                                    continue;
21657                                };
21658                                let Some(send_response) = state.send_response.as_mut() else {
21659                                    let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} already responded")));
21660                                    continue;
21661                                };
21662                                match match send_response {
21663                                    ServerHttp2Responder::Regular(send_response) => {
21664                                        send_response.send_response(response, false)
21665                                    }
21666                                    ServerHttp2Responder::Pushed(send_response) => {
21667                                        send_response.send_response(response, false)
21668                                    }
21669                                } {
21670                                    Ok(send_stream) => {
21671                                        state.send_stream = Some(send_stream);
21672                                        state.send_response = None;
21673                                        let _ = respond_to.send(Ok(Value::Null));
21674                                    }
21675                                    Err(error) => {
21676                                        let _ = respond_to.send(Err(error.to_string()));
21677                                    }
21678                                }
21679                            }
21680                            Http2SessionCommand::StreamPush { stream_id, headers_json, respond_to } => {
21681                                let request = match build_http2_request(&headers_json) {
21682                                    Ok(request) => request,
21683                                    Err(error) => {
21684                                        let _ = respond_to.send(Err(error.to_string()));
21685                                        continue;
21686                                    }
21687                                };
21688                                let mut streams_guard = streams.lock().expect("http2 server streams");
21689                                let Some(state) = streams_guard.get_mut(&stream_id) else {
21690                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
21691                                    continue;
21692                                };
21693                                let Some(send_response) = state.send_response.as_mut() else {
21694                                    let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} cannot push after responding")));
21695                                    continue;
21696                                };
21697                                let ServerHttp2Responder::Regular(send_response) = send_response else {
21698                                    let _ = respond_to.send(Err(format!("HTTP/2 pushed stream {stream_id} cannot create nested push promises")));
21699                                    continue;
21700                                };
21701                                match send_response.push_request(request) {
21702                                    Ok(pushed) => {
21703                                        let pushed_stream_id = {
21704                                            let mut state = shared.lock().expect("http2 shared state");
21705                                            let pushed_stream_id = next_http2_stream_id(&mut state);
21706                                            state.streams.insert(
21707                                                pushed_stream_id,
21708                                                ActiveHttp2Stream {
21709                                                    session_id,
21710                                                    paused: Arc::new(AtomicBool::new(false)),
21711                                                    resume_notify: Arc::new(tokio::sync::Notify::new()),
21712                                                },
21713                                            );
21714                                            pushed_stream_id
21715                                        };
21716                                        streams_guard.insert(
21717                                            pushed_stream_id,
21718                                            ServerHttp2StreamState {
21719                                                send_response: Some(ServerHttp2Responder::Pushed(pushed)),
21720                                                send_stream: None,
21721                                            },
21722                                        );
21723                                        let _ = respond_to.send(Ok(json!({
21724                                            "streamId": pushed_stream_id,
21725                                            "headers": headers_json,
21726                                        }).to_string().into()));
21727                                    }
21728                                    Err(error) => {
21729                                        let _ = respond_to.send(Err(error.to_string()));
21730                                    }
21731                                }
21732                            }
21733                            Http2SessionCommand::StreamWrite { stream_id, chunk, end_stream, respond_to } => {
21734                                let mut streams = streams.lock().expect("http2 server streams");
21735                                let Some(state) = streams.get_mut(&stream_id) else {
21736                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
21737                                    continue;
21738                                };
21739                                let Some(send_stream) = state.send_stream.as_mut() else {
21740                                    let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} has not sent response headers")));
21741                                    continue;
21742                                };
21743                                match send_stream.send_data(Bytes::from(chunk), end_stream) {
21744                                    Ok(()) => {
21745                                        if end_stream {
21746                                            streams.remove(&stream_id);
21747                                            if let Ok(mut state) = shared.lock() {
21748                                                state.streams.remove(&stream_id);
21749                                            }
21750                                            push_http2_server_event(
21751                                                &shared,
21752                                                server_id,
21753                                                Http2BridgeEvent {
21754                                                    kind: String::from("serverStreamClose"),
21755                                                    id: stream_id,
21756                                                    extra_number: Some(0),
21757                                                    ..Http2BridgeEvent::default()
21758                                                },
21759                                            );
21760                                        }
21761                                        let _ = respond_to.send(Ok(Value::Bool(true)));
21762                                    }
21763                                    Err(error) => {
21764                                        let _ = respond_to.send(Err(error.to_string()));
21765                                    }
21766                                }
21767                            }
21768                            Http2SessionCommand::StreamClose { stream_id, error_code, respond_to } => {
21769                                let mut streams_guard = streams.lock().expect("http2 server streams");
21770                                let Some(mut state) = streams_guard.remove(&stream_id) else {
21771                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
21772                                    continue;
21773                                };
21774                                let reason = http2_reason(error_code);
21775                                if let Some(send_stream) = state.send_stream.as_mut() {
21776                                    send_stream.send_reset(reason);
21777                                }
21778                                if let Some(send_response) = state.send_response.as_mut() {
21779                                    match send_response {
21780                                        ServerHttp2Responder::Regular(send_response) => {
21781                                            send_response.send_reset(reason)
21782                                        }
21783                                        ServerHttp2Responder::Pushed(send_response) => {
21784                                            send_response.send_reset(reason)
21785                                        }
21786                                    }
21787                                }
21788                                if let Ok(mut shared_guard) = shared.lock() {
21789                                    shared_guard.streams.remove(&stream_id);
21790                                }
21791                                push_http2_server_event(
21792                                    &shared,
21793                                    server_id,
21794                                    Http2BridgeEvent {
21795                                        kind: String::from("serverStreamClose"),
21796                                        id: stream_id,
21797                                        extra_number: Some(u32::from(reason) as u64),
21798                                        ..Http2BridgeEvent::default()
21799                                    },
21800                                );
21801                                let _ = respond_to.send(Ok(Value::Null));
21802                            }
21803                            Http2SessionCommand::StreamRespondWithFile { stream_id, body, headers_json, options_json, respond_to } => {
21804                                let options: JavascriptHttp2FileResponseOptions =
21805                                    serde_json::from_str(&options_json).unwrap_or_default();
21806                                let response = match build_http2_response(&headers_json) {
21807                                    Ok(response) => response,
21808                                    Err(error) => {
21809                                        let _ = respond_to.send(Err(error.to_string()));
21810                                        continue;
21811                                    }
21812                                };
21813                                let offset = usize::try_from(options.offset.unwrap_or_default()).unwrap_or(0);
21814                                let body = if offset >= body.len() {
21815                                    Vec::new()
21816                                } else {
21817                                    let body = &body[offset..];
21818                                    match options.length {
21819                                        Some(length) if length >= 0 => {
21820                                            body[..body.len().min(length as usize)].to_vec()
21821                                        }
21822                                        _ => body.to_vec(),
21823                                    }
21824                                };
21825                                let mut streams_guard = streams.lock().expect("http2 server streams");
21826                                let Some(state) = streams_guard.get_mut(&stream_id) else {
21827                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
21828                                    continue;
21829                                };
21830                                let Some(send_response) = state.send_response.as_mut() else {
21831                                    let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} already responded")));
21832                                    continue;
21833                                };
21834                                match match send_response {
21835                                    ServerHttp2Responder::Regular(send_response) => {
21836                                        send_response.send_response(response, body.is_empty())
21837                                    }
21838                                    ServerHttp2Responder::Pushed(send_response) => {
21839                                        send_response.send_response(response, body.is_empty())
21840                                    }
21841                                } {
21842                                    Ok(mut send_stream) => {
21843                                        state.send_response = None;
21844                                        if body.is_empty() {
21845                                            streams_guard.remove(&stream_id);
21846                                            if let Ok(mut shared_guard) = shared.lock() {
21847                                                shared_guard.streams.remove(&stream_id);
21848                                            }
21849                                        } else {
21850                                            if let Err(error) = send_stream.send_data(Bytes::from(body), true) {
21851                                                let _ = respond_to.send(Err(error.to_string()));
21852                                                continue;
21853                                            }
21854                                            streams_guard.remove(&stream_id);
21855                                            if let Ok(mut shared_guard) = shared.lock() {
21856                                                shared_guard.streams.remove(&stream_id);
21857                                            }
21858                                        }
21859                                        push_http2_server_event(
21860                                            &shared,
21861                                            server_id,
21862                                            Http2BridgeEvent {
21863                                                kind: String::from("serverStreamClose"),
21864                                                id: stream_id,
21865                                                extra_number: Some(0),
21866                                                ..Http2BridgeEvent::default()
21867                                            },
21868                                        );
21869                                        let _ = respond_to.send(Ok(Value::Null));
21870                                    }
21871                                    Err(error) => {
21872                                        let _ = respond_to.send(Err(error.to_string()));
21873                                    }
21874                                }
21875                            }
21876                            Http2SessionCommand::Request { respond_to, .. } => {
21877                                let _ = respond_to.send(Err(String::from("HTTP/2 server sessions cannot initiate client requests")));
21878                            }
21879                        }
21880                    }
21881                    else => break,
21882                }
21883            }
21884        });
21885    });
21886}
21887
21888fn spawn_http2_server_accept_loop(
21889    shared: Arc<Mutex<crate::state::Http2SharedState>>,
21890    server_id: u64,
21891    listener: TcpListener,
21892) {
21893    thread::spawn(move || {
21894        let listener = listener;
21895        loop {
21896            let closed = shared
21897                .lock()
21898                .ok()
21899                .and_then(|state| {
21900                    state
21901                        .servers
21902                        .get(&server_id)
21903                        .map(|server| server.closed.load(Ordering::SeqCst))
21904                })
21905                .unwrap_or(true);
21906            if closed {
21907                break;
21908            }
21909            match listener.accept() {
21910                Ok((stream, _)) => {
21911                    let (command_tx, command_rx) = unbounded_channel();
21912                    let (guest_local_addr, secure, tls) = {
21913                        let state = shared.lock().expect("http2 shared state");
21914                        let server = state.servers.get(&server_id).expect("http2 server state");
21915                        (server.guest_local_addr, server.secure, server.tls.clone())
21916                    };
21917                    let (local_addr, remote_addr) = match (stream.local_addr(), stream.peer_addr())
21918                    {
21919                        (Ok(local_addr), Ok(remote_addr)) => (local_addr, remote_addr),
21920                        _ => continue,
21921                    };
21922                    let session_snapshot = Arc::new(Mutex::new(Http2SessionSnapshot {
21923                        encrypted: secure,
21924                        alpn_protocol: Some(if secure {
21925                            String::from("h2")
21926                        } else {
21927                            String::from("h2c")
21928                        }),
21929                        local_settings: BTreeMap::new(),
21930                        remote_settings: BTreeMap::new(),
21931                        state: http2_runtime_snapshot(),
21932                        socket: Http2SocketSnapshot {
21933                            local_address: Some(guest_local_addr.ip().to_string()),
21934                            local_port: Some(guest_local_addr.port()),
21935                            local_family: Some(socket_addr_family(&guest_local_addr).to_string()),
21936                            remote_address: Some(remote_addr.ip().to_string()),
21937                            remote_port: Some(remote_addr.port()),
21938                            remote_family: Some(socket_addr_family(&remote_addr).to_string()),
21939                            ..http2_socket_snapshot(local_addr, remote_addr)
21940                        },
21941                        ..Http2SessionSnapshot::default()
21942                    }));
21943                    let session_id = {
21944                        let mut state = shared.lock().expect("http2 shared state");
21945                        let session_id = next_http2_session_id(&mut state);
21946                        state
21947                            .sessions
21948                            .insert(session_id, ActiveHttp2Session { command_tx });
21949                        session_id
21950                    };
21951                    spawn_http2_server_session(
21952                        Arc::clone(&shared),
21953                        server_id,
21954                        session_id,
21955                        stream,
21956                        tls,
21957                        session_snapshot,
21958                        command_rx,
21959                    );
21960                }
21961                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
21962                    wait_fd_readable_until(
21963                        listener.as_fd(),
21964                        Instant::now() + Duration::from_millis(100),
21965                    );
21966                }
21967                Err(error) => {
21968                    push_http2_server_event(
21969                        &shared,
21970                        server_id,
21971                        Http2BridgeEvent {
21972                            kind: String::from("serverStreamError"),
21973                            id: server_id,
21974                            data: Some(http2_error_payload(error.to_string())),
21975                            ..Http2BridgeEvent::default()
21976                        },
21977                    );
21978                    // Persistent accept errors (e.g. EMFILE) leave the fd readable, so a
21979                    // readiness wait would return immediately and spin; keep the throttle.
21980                    thread::sleep(HTTP2_POLL_DELAY);
21981                }
21982            }
21983        }
21984    });
21985}
21986
21987fn send_http2_command(
21988    session: &ActiveHttp2Session,
21989    command: impl FnOnce(Sender<Result<Value, String>>) -> Http2SessionCommand,
21990) -> Result<Value, SidecarError> {
21991    let (respond_to, response_rx) = mpsc::channel();
21992    session.command_tx.send(command(respond_to)).map_err(|_| {
21993        SidecarError::InvalidState(String::from("HTTP/2 session command channel closed"))
21994    })?;
21995    response_rx
21996        .recv_timeout(Duration::from_secs(30))
21997        .map_err(|_| {
21998            SidecarError::Execution(String::from("timed out waiting for HTTP/2 session command"))
21999        })?
22000        .map_err(SidecarError::Execution)
22001}
22002
22003fn parse_http2_server_listen_payload(
22004    request: &JavascriptSyncRpcRequest,
22005) -> Result<JavascriptHttp2ServerListenRequest, SidecarError> {
22006    let payload_json =
22007        javascript_sync_rpc_arg_str(&request.args, 0, "net.http2_server_listen payload")?;
22008    serde_json::from_str(payload_json).map_err(|error| {
22009        SidecarError::InvalidState(format!(
22010            "net.http2_server_listen payload must be valid JSON: {error}"
22011        ))
22012    })
22013}
22014
22015fn parse_http2_connect_payload(
22016    request: &JavascriptSyncRpcRequest,
22017) -> Result<JavascriptHttp2SessionConnectRequest, SidecarError> {
22018    let payload_json =
22019        javascript_sync_rpc_arg_str(&request.args, 0, "net.http2_session_connect payload")?;
22020    serde_json::from_str(payload_json).map_err(|error| {
22021        SidecarError::InvalidState(format!(
22022            "net.http2_session_connect payload must be valid JSON: {error}"
22023        ))
22024    })
22025}
22026
22027fn http2_session_for_id(
22028    process: &ActiveProcess,
22029    session_id: u64,
22030) -> Result<ActiveHttp2Session, SidecarError> {
22031    let shared = process
22032        .http2
22033        .shared
22034        .lock()
22035        .map_err(|_| SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned")))?;
22036    shared
22037        .sessions
22038        .get(&session_id)
22039        .cloned()
22040        .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 session {session_id}")))
22041}
22042
22043fn http2_stream_for_id(
22044    process: &ActiveProcess,
22045    stream_id: u64,
22046) -> Result<ActiveHttp2Stream, SidecarError> {
22047    let shared = process
22048        .http2
22049        .shared
22050        .lock()
22051        .map_err(|_| SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned")))?;
22052    shared
22053        .streams
22054        .get(&stream_id)
22055        .cloned()
22056        .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 stream {stream_id}")))
22057}
22058
22059fn service_javascript_http2_sync_rpc<B>(
22060    request: JavascriptHttp2SyncRpcServiceRequest<'_, B>,
22061) -> Result<Value, SidecarError>
22062where
22063    B: NativeSidecarBridge + Send + 'static,
22064    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
22065{
22066    let JavascriptHttp2SyncRpcServiceRequest {
22067        bridge,
22068        kernel,
22069        vm_id,
22070        dns,
22071        socket_paths,
22072        process,
22073        sync_request: request,
22074        resource_limits,
22075        network_counts,
22076    } = request;
22077    match request.method.as_str() {
22078        "net.http2_server_listen" => {
22079            check_network_resource_limit(
22080                resource_limits.max_sockets,
22081                network_counts.sockets,
22082                1,
22083                "socket",
22084            )?;
22085            let payload = parse_http2_server_listen_payload(request)?;
22086            let (family, bind_host, guest_host) =
22087                normalize_tcp_listen_host(payload.host.as_deref())?;
22088            let requested_port = payload.port.unwrap_or(0);
22089            bridge.require_network_access(
22090                vm_id,
22091                NetworkOperation::Listen,
22092                format_tcp_resource(bind_host, requested_port),
22093            )?;
22094            let port = allocate_guest_listen_port(
22095                requested_port,
22096                family,
22097                &socket_paths.used_tcp_guest_ports,
22098                socket_paths.listen_policy,
22099            )?;
22100            let mut listener =
22101                ActiveTcpListener::bind(bind_host, guest_host, port, payload.backlog)?;
22102            let guest_local_addr = listener.guest_local_addr();
22103            let closed = Arc::new(AtomicBool::new(false));
22104            {
22105                let mut state = process.http2.shared.lock().map_err(|_| {
22106                    SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
22107                })?;
22108                state.servers.insert(
22109                    payload.server_id,
22110                    ActiveHttp2Server {
22111                        actual_local_addr: listener.local_addr(),
22112                        guest_local_addr,
22113                        secure: payload.secure,
22114                        tls: payload.tls.clone().map(|mut tls| {
22115                            tls.is_server = payload.secure;
22116                            if payload.secure && tls.alpn_protocols.is_none() {
22117                                tls.alpn_protocols = Some(vec![String::from("h2")]);
22118                            }
22119                            tls
22120                        }),
22121                        closed: Arc::clone(&closed),
22122                    },
22123                );
22124                if state.event_session.is_none() {
22125                    state.event_session = process.execution.javascript_v8_session_handle();
22126                }
22127                state.server_events.entry(payload.server_id).or_default();
22128            }
22129            spawn_http2_server_accept_loop(
22130                Arc::clone(&process.http2.shared),
22131                payload.server_id,
22132                listener.listener.take().ok_or_else(|| {
22133                    SidecarError::InvalidState(String::from(
22134                        "HTTP/2 listener missing host TCP socket",
22135                    ))
22136                })?,
22137            );
22138            javascript_net_json_string(
22139                json!({
22140                    "address": socket_address_value(&guest_local_addr)
22141                }),
22142                "net.http2_server_listen",
22143            )
22144        }
22145        "net.http2_server_poll" => {
22146            let server_id =
22147                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_poll server id")?;
22148            let wait_ms = javascript_sync_rpc_arg_u64_optional(
22149                &request.args,
22150                1,
22151                "net.http2_server_poll wait ms",
22152            )?
22153            .unwrap_or_default();
22154            match wait_for_http2_event(&process.http2.shared, server_id, true, wait_ms) {
22155                Some(event) => http2_event_value(&event),
22156                None => Ok(Value::Null),
22157            }
22158        }
22159        "net.http2_server_wait" => {
22160            let server_id =
22161                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_wait server id")?;
22162            dispatch_http2_wait_loop(process, server_id, true)
22163        }
22164        "net.http2_server_close" => {
22165            let server_id =
22166                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_close server id")?;
22167            let server = {
22168                let mut state = process.http2.shared.lock().map_err(|_| {
22169                    SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
22170                })?;
22171                state.servers.remove(&server_id)
22172            }
22173            .ok_or_else(|| {
22174                SidecarError::InvalidState(format!("unknown HTTP/2 server {server_id}"))
22175            })?;
22176            server.closed.store(true, Ordering::SeqCst);
22177            push_http2_server_event(
22178                &process.http2.shared,
22179                server_id,
22180                Http2BridgeEvent {
22181                    kind: String::from("serverClose"),
22182                    id: server_id,
22183                    ..Http2BridgeEvent::default()
22184                },
22185            );
22186            Ok(Value::Null)
22187        }
22188        "net.http2_server_respond" => {
22189            let server_id = javascript_sync_rpc_arg_u64(
22190                &request.args,
22191                0,
22192                "net.http2_server_respond server id",
22193            )?;
22194            let request_id = javascript_sync_rpc_arg_u64(
22195                &request.args,
22196                1,
22197                "net.http2_server_respond request id",
22198            )?;
22199            let response_json =
22200                javascript_sync_rpc_arg_str(&request.args, 2, "net.http2_server_respond payload")?;
22201            ensure_vm_fetch_response_within_limit(
22202                response_json,
22203                "net.http2_server_respond",
22204                VM_FETCH_BUFFER_LIMIT_BYTES,
22205            )
22206            .map_err(sidecar_core_execution_error)?;
22207            serde_json::from_str::<Value>(response_json).map_err(|error| {
22208                SidecarError::Execution(format!(
22209                    "net.http2_server_respond payload must be valid JSON: {error}"
22210                ))
22211            })?;
22212            let Some(pending) = process
22213                .pending_http_requests
22214                .get_mut(&(server_id, request_id))
22215            else {
22216                return Err(SidecarError::InvalidState(format!(
22217                    "unknown pending HTTP/2 request {request_id} for server {server_id}"
22218                )));
22219            };
22220            *pending = Some(response_json.to_owned());
22221            Ok(Value::Bool(true))
22222        }
22223        "net.http2_session_connect" => {
22224            check_network_resource_limit(
22225                resource_limits.max_sockets,
22226                network_counts.sockets,
22227                1,
22228                "socket",
22229            )?;
22230            check_network_resource_limit(
22231                resource_limits.max_connections,
22232                network_counts.connections,
22233                1,
22234                "connection",
22235            )?;
22236            let payload = parse_http2_connect_payload(request)?;
22237            let authority = payload.authority.clone().unwrap_or_else(|| {
22238                format!(
22239                    "{}://{}:{}",
22240                    payload.protocol.as_deref().unwrap_or("http"),
22241                    payload.host.as_deref().unwrap_or("localhost"),
22242                    payload.port.unwrap_or(80)
22243                )
22244            });
22245            let url = Url::parse(&authority).map_err(|error| {
22246                SidecarError::InvalidState(format!(
22247                    "invalid HTTP/2 authority {authority:?}: {error}"
22248                ))
22249            })?;
22250            let secure = url.scheme() == "https" || payload.protocol.as_deref() == Some("https:");
22251            let host = payload
22252                .host
22253                .as_deref()
22254                .or_else(|| url.host_str())
22255                .unwrap_or("localhost");
22256            let port = payload.port.or_else(|| url.port()).unwrap_or(80);
22257            bridge.require_network_access(
22258                vm_id,
22259                NetworkOperation::Http,
22260                format_tcp_resource(host, port),
22261            )?;
22262            let resolved = {
22263                let shared = process.http2.shared.lock().map_err(|_| {
22264                    SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
22265                })?;
22266                shared
22267                    .servers
22268                    .values()
22269                    .find(|server| {
22270                        is_loopback_request_host(host) && server.guest_local_addr.port() == port
22271                    })
22272                    .map(|server| ResolvedTcpConnectAddr {
22273                        actual_addr: server.actual_local_addr,
22274                        guest_remote_addr: server.guest_local_addr,
22275                        use_kernel_loopback: false,
22276                    })
22277            };
22278            let resolved = match resolved {
22279                Some(resolved) => resolved,
22280                None => {
22281                    resolve_tcp_connect_addr(bridge, kernel, vm_id, dns, host, port, socket_paths)?
22282                }
22283            };
22284            let (command_tx, command_rx) = unbounded_channel();
22285            let snapshot = Arc::new(Mutex::new(Http2SessionSnapshot {
22286                encrypted: secure,
22287                alpn_protocol: Some(String::from(if secure { "h2" } else { "h2c" })),
22288                local_settings: http2_settings_from_value(&payload.settings),
22289                remote_settings: BTreeMap::new(),
22290                state: http2_runtime_snapshot(),
22291                socket: Http2SocketSnapshot {
22292                    encrypted: secure,
22293                    remote_address: Some(resolved.guest_remote_addr.ip().to_string()),
22294                    remote_port: Some(resolved.guest_remote_addr.port()),
22295                    remote_family: Some(
22296                        socket_addr_family(&resolved.guest_remote_addr).to_string(),
22297                    ),
22298                    servername: if secure {
22299                        payload
22300                            .tls
22301                            .as_ref()
22302                            .and_then(|tls| tls.servername.clone())
22303                            .or_else(|| Some(host.to_string()))
22304                    } else {
22305                        None
22306                    },
22307                    alpn_protocol: Some(String::from(if secure { "h2" } else { "h2c" })),
22308                    ..Http2SocketSnapshot::default()
22309                },
22310                ..Http2SessionSnapshot::default()
22311            }));
22312            let session_id = {
22313                let mut state = process.http2.shared.lock().map_err(|_| {
22314                    SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
22315                })?;
22316                let session_id = next_http2_session_id(&mut state);
22317                state
22318                    .sessions
22319                    .insert(session_id, ActiveHttp2Session { command_tx });
22320                if state.event_session.is_none() {
22321                    state.event_session = process.execution.javascript_v8_session_handle();
22322                }
22323                state.session_events.entry(session_id).or_default();
22324                session_id
22325            };
22326            spawn_http2_client_session(
22327                Arc::clone(&process.http2.shared),
22328                session_id,
22329                resolved.actual_addr,
22330                if secure {
22331                    Some(payload.tls.unwrap_or(JavascriptTlsBridgeOptions {
22332                        is_server: false,
22333                        servername: Some(host.to_string()),
22334                        alpn_protocols: Some(vec![String::from("h2")]),
22335                        ..JavascriptTlsBridgeOptions::default()
22336                    }))
22337                } else {
22338                    None
22339                },
22340                Arc::clone(&snapshot),
22341                command_rx,
22342            );
22343            let snapshot_json =
22344                http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())?;
22345            javascript_net_json_string(
22346                json!({
22347                    "sessionId": session_id,
22348                    "state": snapshot_json,
22349                }),
22350                "net.http2_session_connect",
22351            )
22352        }
22353        "net.http2_session_request" => {
22354            let session_id = javascript_sync_rpc_arg_u64(
22355                &request.args,
22356                0,
22357                "net.http2_session_request session id",
22358            )?;
22359            let headers_json =
22360                javascript_sync_rpc_arg_str(&request.args, 1, "net.http2_session_request headers")?;
22361            let options_json =
22362                javascript_sync_rpc_arg_str(&request.args, 2, "net.http2_session_request options")?;
22363            let session = http2_session_for_id(process, session_id)?;
22364            send_http2_command(&session, |respond_to| Http2SessionCommand::Request {
22365                headers_json: headers_json.to_owned(),
22366                options_json: options_json.to_owned(),
22367                respond_to,
22368            })
22369        }
22370        "net.http2_session_settings" => {
22371            let session_id = javascript_sync_rpc_arg_u64(
22372                &request.args,
22373                0,
22374                "net.http2_session_settings session id",
22375            )?;
22376            let settings_json = javascript_sync_rpc_arg_str(
22377                &request.args,
22378                1,
22379                "net.http2_session_settings settings",
22380            )?;
22381            let session = http2_session_for_id(process, session_id)?;
22382            send_http2_command(&session, |respond_to| Http2SessionCommand::Settings {
22383                settings_json: settings_json.to_owned(),
22384                respond_to,
22385            })
22386        }
22387        "net.http2_session_set_local_window_size" => {
22388            let session_id = javascript_sync_rpc_arg_u64(
22389                &request.args,
22390                0,
22391                "net.http2_session_set_local_window_size session id",
22392            )?;
22393            let window_size = javascript_sync_rpc_arg_u64(
22394                &request.args,
22395                1,
22396                "net.http2_session_set_local_window_size window size",
22397            )?;
22398            let session = http2_session_for_id(process, session_id)?;
22399            send_http2_command(&session, |respond_to| {
22400                Http2SessionCommand::SetLocalWindowSize {
22401                    size: window_size as u32,
22402                    respond_to,
22403                }
22404            })
22405        }
22406        "net.http2_session_goaway" => {
22407            let session_id = javascript_sync_rpc_arg_u64(
22408                &request.args,
22409                0,
22410                "net.http2_session_goaway session id",
22411            )?;
22412            let error_code = javascript_sync_rpc_arg_u64(
22413                &request.args,
22414                1,
22415                "net.http2_session_goaway error code",
22416            )?;
22417            let last_stream_id = javascript_sync_rpc_arg_u64(
22418                &request.args,
22419                2,
22420                "net.http2_session_goaway last stream id",
22421            )?;
22422            let opaque_data = request
22423                .args
22424                .get(3)
22425                .and_then(Value::as_str)
22426                .map(|value| {
22427                    base64::engine::general_purpose::STANDARD
22428                        .decode(value)
22429                        .map_err(|error| {
22430                            SidecarError::InvalidState(format!("invalid GOAWAY payload: {error}"))
22431                        })
22432                })
22433                .transpose()?;
22434            let session = http2_session_for_id(process, session_id)?;
22435            send_http2_command(&session, |respond_to| Http2SessionCommand::Goaway {
22436                error_code: error_code as u32,
22437                last_stream_id: last_stream_id as u32,
22438                opaque_data,
22439                respond_to,
22440            })
22441        }
22442        "net.http2_session_close" | "net.http2_session_destroy" => {
22443            let session_id = javascript_sync_rpc_arg_u64(
22444                &request.args,
22445                0,
22446                "net.http2_session_close session id",
22447            )?;
22448            let session = http2_session_for_id(process, session_id)?;
22449            send_http2_command(&session, |respond_to| Http2SessionCommand::Close {
22450                abrupt: request.method == "net.http2_session_destroy",
22451                respond_to,
22452            })
22453        }
22454        "net.http2_session_poll" => {
22455            let session_id =
22456                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_session_poll session id")?;
22457            let wait_ms = javascript_sync_rpc_arg_u64_optional(
22458                &request.args,
22459                1,
22460                "net.http2_session_poll wait ms",
22461            )?
22462            .unwrap_or_default();
22463            match wait_for_http2_event(&process.http2.shared, session_id, false, wait_ms) {
22464                Some(event) => http2_event_value(&event),
22465                None => Ok(Value::Null),
22466            }
22467        }
22468        "net.http2_session_wait" => {
22469            let session_id =
22470                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_session_wait session id")?;
22471            dispatch_http2_wait_loop(process, session_id, false)
22472        }
22473        "net.http2_stream_respond" => {
22474            let stream_id = javascript_sync_rpc_arg_u64(
22475                &request.args,
22476                0,
22477                "net.http2_stream_respond stream id",
22478            )?;
22479            let headers_json =
22480                javascript_sync_rpc_arg_str(&request.args, 1, "net.http2_stream_respond headers")?;
22481            let stream = http2_stream_for_id(process, stream_id)?;
22482            let session = http2_session_for_id(process, stream.session_id)?;
22483            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamRespond {
22484                stream_id,
22485                headers_json: headers_json.to_owned(),
22486                respond_to,
22487            })
22488        }
22489        "net.http2_stream_push_stream" => {
22490            let stream_id = javascript_sync_rpc_arg_u64(
22491                &request.args,
22492                0,
22493                "net.http2_stream_push_stream stream id",
22494            )?;
22495            let headers_json = javascript_sync_rpc_arg_str(
22496                &request.args,
22497                1,
22498                "net.http2_stream_push_stream headers",
22499            )?;
22500            let _options_json = javascript_sync_rpc_arg_str(
22501                &request.args,
22502                2,
22503                "net.http2_stream_push_stream options",
22504            )?;
22505            let stream = http2_stream_for_id(process, stream_id)?;
22506            let session = http2_session_for_id(process, stream.session_id)?;
22507            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamPush {
22508                stream_id,
22509                headers_json: headers_json.to_owned(),
22510                respond_to,
22511            })
22512        }
22513        "net.http2_stream_write" => {
22514            let stream_id =
22515                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_write stream id")?;
22516            let chunk =
22517                javascript_sync_rpc_base64_arg(&request.args, 1, "net.http2_stream_write data")?;
22518            let stream = http2_stream_for_id(process, stream_id)?;
22519            let session = http2_session_for_id(process, stream.session_id)?;
22520            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamWrite {
22521                stream_id,
22522                chunk,
22523                end_stream: false,
22524                respond_to,
22525            })
22526        }
22527        "net.http2_stream_end" => {
22528            let stream_id =
22529                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_end stream id")?;
22530            let chunk = request
22531                .args
22532                .get(1)
22533                .and_then(Value::as_str)
22534                .map(|value| {
22535                    base64::engine::general_purpose::STANDARD
22536                        .decode(value)
22537                        .map_err(|error| {
22538                            SidecarError::InvalidState(format!(
22539                                "invalid HTTP/2 stream payload: {error}"
22540                            ))
22541                        })
22542                })
22543                .transpose()?
22544                .unwrap_or_default();
22545            let stream = http2_stream_for_id(process, stream_id)?;
22546            let session = http2_session_for_id(process, stream.session_id)?;
22547            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamWrite {
22548                stream_id,
22549                chunk,
22550                end_stream: true,
22551                respond_to,
22552            })
22553        }
22554        "net.http2_stream_close" => {
22555            let stream_id =
22556                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_close stream id")?;
22557            let code = javascript_sync_rpc_arg_u64_optional(
22558                &request.args,
22559                1,
22560                "net.http2_stream_close error code",
22561            )?
22562            .map(|value| value as u32);
22563            let stream = http2_stream_for_id(process, stream_id)?;
22564            let session = http2_session_for_id(process, stream.session_id)?;
22565            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamClose {
22566                stream_id,
22567                error_code: code,
22568                respond_to,
22569            })
22570        }
22571        "net.http2_stream_pause" => {
22572            let stream_id =
22573                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_pause stream id")?;
22574            let stream = http2_stream_for_id(process, stream_id)?;
22575            stream.paused.store(true, Ordering::SeqCst);
22576            Ok(Value::Null)
22577        }
22578        "net.http2_stream_resume" => {
22579            let stream_id =
22580                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_resume stream id")?;
22581            let stream = http2_stream_for_id(process, stream_id)?;
22582            stream.paused.store(false, Ordering::SeqCst);
22583            stream.resume_notify.notify_waiters();
22584            Ok(Value::Null)
22585        }
22586        "net.http2_stream_respond_with_file" => {
22587            let stream_id = javascript_sync_rpc_arg_u64(
22588                &request.args,
22589                0,
22590                "net.http2_stream_respond_with_file stream id",
22591            )?;
22592            let path = javascript_sync_rpc_arg_str(
22593                &request.args,
22594                1,
22595                "net.http2_stream_respond_with_file path",
22596            )?;
22597            let headers_json = javascript_sync_rpc_arg_str(
22598                &request.args,
22599                2,
22600                "net.http2_stream_respond_with_file headers",
22601            )?;
22602            let options_json = javascript_sync_rpc_arg_str(
22603                &request.args,
22604                3,
22605                "net.http2_stream_respond_with_file options",
22606            )?;
22607            let stream = http2_stream_for_id(process, stream_id)?;
22608            let session = http2_session_for_id(process, stream.session_id)?;
22609            let guest_path = resolve_http2_file_response_guest_path(process, path);
22610            let body = kernel.read_file(&guest_path).map_err(kernel_error)?;
22611            send_http2_command(&session, |respond_to| {
22612                Http2SessionCommand::StreamRespondWithFile {
22613                    stream_id,
22614                    body,
22615                    headers_json: headers_json.to_owned(),
22616                    options_json: options_json.to_owned(),
22617                    respond_to,
22618                }
22619            })
22620        }
22621        other => Err(SidecarError::InvalidState(format!(
22622            "unsupported JavaScript HTTP/2 sync RPC method {other}"
22623        ))),
22624    }
22625}
22626
22627const JAVASCRIPT_NET_POLL_MAX_WAIT: Duration = Duration::from_millis(50);
22628const EXITED_PROCESS_SNAPSHOT_RETENTION: Duration = Duration::from_secs(2);
22629
22630fn resolve_http2_file_response_guest_path(process: &ActiveProcess, path: &str) -> String {
22631    if Path::new(path).is_absolute() {
22632        normalize_path(path)
22633    } else {
22634        normalize_path(&format!("{}/{}", process.guest_cwd, path))
22635    }
22636}
22637
22638pub(crate) fn clamp_javascript_net_poll_wait(wait_ms: u64) -> Duration {
22639    // WASM net.poll runs on the sidecar's sync-RPC main thread. Guest-controlled waits
22640    // must stay bounded so one VM cannot stall dispose/shutdown or unrelated VM work.
22641    if wait_ms == 0 {
22642        Duration::ZERO
22643    } else {
22644        Duration::from_millis(wait_ms).min(JAVASCRIPT_NET_POLL_MAX_WAIT)
22645    }
22646}
22647
22648fn service_javascript_net_sync_rpc_response<B>(
22649    request: JavascriptNetSyncRpcServiceRequest<'_, B>,
22650) -> Result<JavascriptSyncRpcServiceResponse, SidecarError>
22651where
22652    B: NativeSidecarBridge + Send + 'static,
22653    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
22654{
22655    if request.sync_request.method != "net.socket_read" {
22656        return service_javascript_net_sync_rpc(request).map(Into::into);
22657    }
22658
22659    let JavascriptNetSyncRpcServiceRequest {
22660        kernel,
22661        process,
22662        sync_request: request,
22663        ..
22664    } = request;
22665    let trace_enabled = net_tcp_trace_enabled(&process.env);
22666    let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_read socket id")?;
22667    if trace_enabled {
22668        NET_TCP_TRACE_COUNTERS
22669            .socket_read_calls
22670            .fetch_add(1, Ordering::Relaxed);
22671        NET_TCP_TRACE_COUNTERS
22672            .socket_read_zero_wait_calls
22673            .fetch_add(1, Ordering::Relaxed);
22674    }
22675
22676    let event = if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
22677        socket.poll(kernel, process.kernel_pid, Duration::ZERO, trace_enabled)?
22678    } else {
22679        let socket = process
22680            .unix_sockets
22681            .get_mut(socket_id)
22682            .ok_or_else(|| SidecarError::InvalidState(format!("unknown net socket {socket_id}")))?;
22683        socket.poll(Duration::ZERO)?
22684    };
22685
22686    match event {
22687        Some(JavascriptTcpSocketEvent::Data(chunk)) => {
22688            Ok(JavascriptSyncRpcServiceResponse::Raw(chunk))
22689        }
22690        other => javascript_net_read_value(other).map(Into::into),
22691    }
22692}
22693
22694pub(crate) fn service_javascript_net_sync_rpc<B>(
22695    request: JavascriptNetSyncRpcServiceRequest<'_, B>,
22696) -> Result<Value, SidecarError>
22697where
22698    B: NativeSidecarBridge + Send + 'static,
22699    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
22700{
22701    let JavascriptNetSyncRpcServiceRequest {
22702        bridge,
22703        vm_id,
22704        dns,
22705        socket_paths,
22706        kernel,
22707        kernel_readiness,
22708        process,
22709        sync_request: request,
22710        resource_limits,
22711        network_counts,
22712    } = request;
22713    let trace_enabled = net_tcp_trace_enabled(&process.env);
22714    match request.method.as_str() {
22715        "net.http_listen" => {
22716            check_network_resource_limit(
22717                resource_limits.max_sockets,
22718                network_counts.sockets,
22719                1,
22720                "socket",
22721            )?;
22722            let payload_json =
22723                javascript_sync_rpc_arg_str(&request.args, 0, "net.http_listen payload")?;
22724            let payload: JavascriptHttpListenRequest =
22725                serde_json::from_str(payload_json).map_err(|error| {
22726                    SidecarError::InvalidState(format!(
22727                        "net.http_listen payload must be valid JSON: {error}"
22728                    ))
22729                })?;
22730            let (family, bind_host, guest_host) =
22731                normalize_tcp_listen_host(payload.hostname.as_deref())?;
22732            let requested_port = payload.port.unwrap_or(0);
22733            bridge.require_network_access(
22734                vm_id,
22735                NetworkOperation::Listen,
22736                format_tcp_resource(bind_host, requested_port),
22737            )?;
22738            let port = allocate_guest_listen_port(
22739                requested_port,
22740                family,
22741                &socket_paths.used_tcp_guest_ports,
22742                socket_paths.listen_policy,
22743            )?;
22744            let mut listener = ActiveTcpListener::bind(
22745                bind_host,
22746                guest_host,
22747                port,
22748                Some(DEFAULT_JAVASCRIPT_NET_BACKLOG),
22749            )?;
22750            let guest_local_addr = listener.guest_local_addr();
22751            process.http_servers.insert(
22752                payload.server_id,
22753                ActiveHttpServer {
22754                    listener: listener.listener.take().ok_or_else(|| {
22755                        SidecarError::InvalidState(String::from(
22756                            "HTTP listener missing host TCP socket",
22757                        ))
22758                    })?,
22759                    guest_local_addr,
22760                    next_request_id: 0,
22761                },
22762            );
22763            serde_json::to_string(&json!({
22764                "address": socket_address_value(&guest_local_addr)
22765            }))
22766            .map(Value::String)
22767            .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
22768        }
22769        "net.http_close" => {
22770            let server_id =
22771                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_close server id")?;
22772            let server = process.http_servers.remove(&server_id).ok_or_else(|| {
22773                SidecarError::InvalidState(format!("unknown HTTP server {server_id}"))
22774            })?;
22775            drop(server.listener);
22776            process
22777                .pending_http_requests
22778                .retain(|(pending_server_id, _), _| *pending_server_id != server_id);
22779            Ok(Value::Null)
22780        }
22781        "net.http_wait" => {
22782            let server_id =
22783                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_wait server id")?;
22784            dispatch_http_wait_loop(process, server_id)
22785        }
22786        "net.http_respond" => {
22787            let server_id =
22788                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_respond server id")?;
22789            let request_id =
22790                javascript_sync_rpc_arg_u64(&request.args, 1, "net.http_respond request id")?;
22791            let response_json =
22792                javascript_sync_rpc_arg_str(&request.args, 2, "net.http_respond payload")?;
22793            ensure_vm_fetch_response_within_limit(
22794                response_json,
22795                "net.http_respond",
22796                VM_FETCH_BUFFER_LIMIT_BYTES,
22797            )
22798            .map_err(sidecar_core_execution_error)?;
22799            serde_json::from_str::<Value>(response_json).map_err(|error| {
22800                SidecarError::Execution(format!(
22801                    "net.http_respond payload must be valid JSON: {error}"
22802                ))
22803            })?;
22804            let Some(pending) = process
22805                .pending_http_requests
22806                .get_mut(&(server_id, request_id))
22807            else {
22808                return Err(SidecarError::InvalidState(format!(
22809                    "unknown pending HTTP request {request_id} for server {server_id}"
22810                )));
22811            };
22812            *pending = Some(response_json.to_owned());
22813            Ok(Value::Null)
22814        }
22815        "net.reserve_tcp_port" => {
22816            let payload = request
22817                .args
22818                .first()
22819                .cloned()
22820                .ok_or_else(|| {
22821                    SidecarError::InvalidState(String::from(
22822                        "net.reserve_tcp_port requires a request payload",
22823                    ))
22824                })
22825                .and_then(|value| {
22826                    serde_json::from_value::<JavascriptNetReserveTcpPortRequest>(value).map_err(
22827                        |error| {
22828                            SidecarError::InvalidState(format!(
22829                                "invalid net.reserve_tcp_port payload: {error}"
22830                            ))
22831                        },
22832                    )
22833                })?;
22834            let (family, _bind_host, guest_host) =
22835                normalize_tcp_listen_host(payload.host.as_deref())?;
22836            let requested_port = payload.port.unwrap_or(0);
22837            let port = allocate_guest_listen_port(
22838                requested_port,
22839                family,
22840                &socket_paths.used_tcp_guest_ports,
22841                socket_paths.listen_policy,
22842            )?;
22843            let reservation_id = process.allocate_tcp_port_reservation_id();
22844            process
22845                .tcp_port_reservations
22846                .insert(reservation_id.clone(), (family, port));
22847            Ok(json!({
22848                "reservationId": reservation_id,
22849                "localAddress": guest_host,
22850                "localPort": port,
22851                "family": match family {
22852                    JavascriptSocketFamily::Ipv4 => "IPv4",
22853                    JavascriptSocketFamily::Ipv6 => "IPv6",
22854                },
22855            }))
22856        }
22857        "net.release_tcp_port" => {
22858            let reservation_id =
22859                javascript_sync_rpc_arg_str(&request.args, 0, "net.release_tcp_port reservation")?;
22860            process.tcp_port_reservations.remove(reservation_id);
22861            Ok(Value::Null)
22862        }
22863        "net.connect" => {
22864            check_network_resource_limit(
22865                resource_limits.max_sockets,
22866                network_counts.sockets,
22867                1,
22868                "socket",
22869            )?;
22870            check_network_resource_limit(
22871                resource_limits.max_connections,
22872                network_counts.connections,
22873                1,
22874                "connection",
22875            )?;
22876            let payload = request
22877                .args
22878                .first()
22879                .cloned()
22880                .ok_or_else(|| {
22881                    SidecarError::InvalidState(String::from(
22882                        "net.connect requires a request payload",
22883                    ))
22884                })
22885                .and_then(|value| {
22886                    serde_json::from_value::<JavascriptNetConnectRequest>(value).map_err(|error| {
22887                        SidecarError::InvalidState(format!("invalid net.connect payload: {error}"))
22888                    })
22889                })?;
22890            if let Some(path) = payload.path.as_deref() {
22891                let guest_path = normalize_path(path);
22892                let host_path = resolve_guest_socket_host_path(socket_paths, &guest_path);
22893                let socket = ActiveUnixSocket::connect(&host_path, &guest_path)?;
22894                let socket_id = process.allocate_unix_socket_id();
22895                socket.set_event_pusher(
22896                    process.execution.javascript_v8_session_handle(),
22897                    socket_id.clone(),
22898                );
22899                process.unix_sockets.insert(socket_id.clone(), socket);
22900                Ok(json!({
22901                    "socketId": socket_id,
22902                    "remotePath": guest_path,
22903                }))
22904            } else {
22905                let port = payload.port.ok_or_else(|| {
22906                    SidecarError::InvalidState(String::from(
22907                        "net.connect requires either a path or port",
22908                    ))
22909                })?;
22910                let host = payload.host.as_deref().unwrap_or("localhost");
22911                let local_reservation = payload.local_reservation.as_deref().and_then(|id| {
22912                    process
22913                        .tcp_port_reservations
22914                        .remove(id)
22915                        .map(|reservation| (id.to_owned(), reservation))
22916                });
22917                bridge.require_network_access(
22918                    vm_id,
22919                    NetworkOperation::Http,
22920                    format_tcp_resource(host, port),
22921                )?;
22922                if is_loopback_socket_host(host) {
22923                    let families = [JavascriptSocketFamily::Ipv4, JavascriptSocketFamily::Ipv6];
22924                    if let Some((family, target)) = families.iter().find_map(|family| {
22925                        socket_paths
22926                            .http_loopback_target(*family, port)
22927                            .map(|target| (*family, target))
22928                    }) {
22929                        if let Some((reservation_id, reservation)) = local_reservation {
22930                            process
22931                                .tcp_port_reservations
22932                                .insert(reservation_id, reservation);
22933                        }
22934                        let remote_address = match family {
22935                            JavascriptSocketFamily::Ipv4 => "127.0.0.1",
22936                            JavascriptSocketFamily::Ipv6 => "::1",
22937                        };
22938                        return Ok(json!({
22939                            "loopbackHttpTarget": {
22940                                "processId": target.process_id.clone(),
22941                                "serverId": target.server_id,
22942                                "host": remote_address,
22943                                "port": port,
22944                            },
22945                            "localAddress": match family {
22946                                JavascriptSocketFamily::Ipv4 => "127.0.0.1",
22947                                JavascriptSocketFamily::Ipv6 => "::1",
22948                            },
22949                            "localPort": payload.local_port.unwrap_or(0),
22950                            "remoteAddress": remote_address,
22951                            "remotePort": port,
22952                            "remoteFamily": match family {
22953                                JavascriptSocketFamily::Ipv4 => "IPv4",
22954                                JavascriptSocketFamily::Ipv6 => "IPv6",
22955                            },
22956                        }));
22957                    }
22958                }
22959                let connect_result = ActiveTcpSocket::connect(ActiveTcpConnectRequest {
22960                    bridge,
22961                    kernel,
22962                    kernel_pid: process.kernel_pid,
22963                    vm_id,
22964                    dns,
22965                    host,
22966                    port,
22967                    local_address: payload.local_address.as_deref(),
22968                    local_port: payload.local_port,
22969                    local_reservation: local_reservation
22970                        .as_ref()
22971                        .map(|(_, reservation)| *reservation),
22972                    context: socket_paths,
22973                });
22974                if let Err(error) = connect_result {
22975                    if let Some((reservation_id, reservation)) = local_reservation {
22976                        process
22977                            .tcp_port_reservations
22978                            .insert(reservation_id, reservation);
22979                    }
22980                    return Err(error);
22981                }
22982                let socket = connect_result?;
22983                let socket_id = process.allocate_tcp_socket_id();
22984                let local_addr = socket.guest_local_addr;
22985                let remote_addr = socket.guest_remote_addr;
22986                socket.set_event_pusher(
22987                    process.execution.javascript_v8_session_handle(),
22988                    socket_id.clone(),
22989                );
22990                register_kernel_readiness_target(
22991                    &kernel_readiness,
22992                    socket.kernel_socket_id,
22993                    process.execution.javascript_v8_session_handle(),
22994                    socket_id.clone(),
22995                    KernelSocketReadinessEvent::Data,
22996                );
22997                process.tcp_sockets.insert(socket_id.clone(), socket);
22998                Ok(json!({
22999                    "socketId": socket_id,
23000                    "localAddress": local_addr.ip().to_string(),
23001                    "localPort": local_addr.port(),
23002                    "remoteAddress": remote_addr.ip().to_string(),
23003                    "remotePort": remote_addr.port(),
23004                    "remoteFamily": socket_addr_family(&remote_addr),
23005                }))
23006            }
23007        }
23008        "net.listen" => {
23009            check_network_resource_limit(
23010                resource_limits.max_sockets,
23011                network_counts.sockets,
23012                1,
23013                "socket",
23014            )?;
23015            let payload = request
23016                .args
23017                .first()
23018                .cloned()
23019                .ok_or_else(|| {
23020                    SidecarError::InvalidState(String::from(
23021                        "net.listen requires a request payload",
23022                    ))
23023                })
23024                .and_then(|value| match value {
23025                    Value::String(json) => {
23026                        serde_json::from_str::<JavascriptNetListenRequest>(&json).map_err(|error| {
23027                            SidecarError::InvalidState(format!(
23028                                "invalid net.listen payload: {error}"
23029                            ))
23030                        })
23031                    }
23032                    other => serde_json::from_value::<JavascriptNetListenRequest>(other).map_err(
23033                        |error| {
23034                            SidecarError::InvalidState(format!(
23035                                "invalid net.listen payload: {error}"
23036                            ))
23037                        },
23038                    ),
23039                })?;
23040            if let Some(path) = payload.path.as_deref() {
23041                let guest_path = normalize_path(path);
23042                if kernel.exists(&guest_path).map_err(kernel_error)? {
23043                    return Err(sidecar_net_error(std::io::Error::from_raw_os_error(
23044                        libc::EADDRINUSE,
23045                    )));
23046                }
23047
23048                let host_path = resolve_guest_socket_host_path(socket_paths, &guest_path);
23049                let on_host_mount =
23050                    host_mount_path_for_guest_path_from_mounts(&socket_paths.mounts, &guest_path)
23051                        .is_some();
23052                let listener = ActiveUnixListener::bind(&host_path, &guest_path, payload.backlog)?;
23053                if !on_host_mount {
23054                    ensure_kernel_parent_directories(kernel, &guest_path)?;
23055                    kernel
23056                        .write_file(&guest_path, Vec::new())
23057                        .map_err(kernel_error)?;
23058                }
23059                let listener_id = process.allocate_unix_listener_id();
23060                process.unix_listeners.insert(listener_id.clone(), listener);
23061                Ok(json!({
23062                    "serverId": listener_id,
23063                    "path": guest_path,
23064                }))
23065            } else {
23066                let (family, bind_host, guest_host) =
23067                    normalize_tcp_listen_host(payload.host.as_deref())?;
23068                let requested_port = payload.port.unwrap_or(0);
23069                bridge.require_network_access(
23070                    vm_id,
23071                    NetworkOperation::Listen,
23072                    format_tcp_resource(bind_host, requested_port),
23073                )?;
23074                let local_reservation = payload.local_reservation.as_deref().and_then(|id| {
23075                    process
23076                        .tcp_port_reservations
23077                        .remove(id)
23078                        .map(|reservation| (id.to_owned(), reservation))
23079                });
23080                let port = if requested_port != 0
23081                    && local_reservation
23082                        .as_ref()
23083                        .map(|(_, reservation)| *reservation)
23084                        == Some((family, requested_port))
23085                {
23086                    requested_port
23087                } else {
23088                    allocate_guest_listen_port(
23089                        requested_port,
23090                        family,
23091                        &socket_paths.used_tcp_guest_ports,
23092                        socket_paths.listen_policy,
23093                    )?
23094                };
23095                let listener_result = ActiveTcpListener::bind_kernel(
23096                    kernel,
23097                    process.kernel_pid,
23098                    guest_host,
23099                    port,
23100                    payload.backlog,
23101                );
23102                if let Err(error) = listener_result {
23103                    if let Some((reservation_id, reservation)) = local_reservation {
23104                        process
23105                            .tcp_port_reservations
23106                            .insert(reservation_id, reservation);
23107                    }
23108                    return Err(error);
23109                }
23110                let listener = listener_result?;
23111                let listener_id = process.allocate_tcp_listener_id();
23112                let local_addr = listener.guest_local_addr();
23113                register_kernel_readiness_target(
23114                    &kernel_readiness,
23115                    listener.kernel_socket_id,
23116                    process.execution.javascript_v8_session_handle(),
23117                    listener_id.clone(),
23118                    KernelSocketReadinessEvent::Accept,
23119                );
23120                process.tcp_listeners.insert(listener_id.clone(), listener);
23121                Ok(json!({
23122                    "serverId": listener_id,
23123                    "localAddress": local_addr.ip().to_string(),
23124                    "localPort": local_addr.port(),
23125                    "family": socket_addr_family(&local_addr),
23126                }))
23127            }
23128        }
23129        "net.poll" => {
23130            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.poll socket id")?;
23131            let wait_ms =
23132                javascript_sync_rpc_arg_u64_optional(&request.args, 1, "net.poll wait ms")?
23133                    .unwrap_or_default();
23134            let wait = clamp_javascript_net_poll_wait(wait_ms);
23135            let event = if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
23136                socket.poll(kernel, process.kernel_pid, wait, trace_enabled)?
23137            } else if let Some(socket) = process.unix_sockets.get_mut(socket_id) {
23138                socket.poll(wait)?
23139            } else {
23140                return Err(SidecarError::InvalidState(format!(
23141                    "unknown net socket {socket_id}"
23142                )));
23143            };
23144
23145            match event {
23146                Some(JavascriptTcpSocketEvent::Data(chunk)) => Ok(json!({
23147                    "type": "data",
23148                    "data": javascript_sync_rpc_bytes_value(&chunk),
23149                })),
23150                Some(JavascriptTcpSocketEvent::End) => Ok(json!({
23151                    "type": "end",
23152                })),
23153                Some(JavascriptTcpSocketEvent::Error { code, message }) => Ok(json!({
23154                    "type": "error",
23155                    "code": code,
23156                    "message": message,
23157                })),
23158                Some(JavascriptTcpSocketEvent::Close { had_error }) => {
23159                    if let Some(socket) = process.tcp_sockets.remove(socket_id) {
23160                        release_tcp_socket_handle(
23161                            process,
23162                            socket_id,
23163                            socket,
23164                            kernel,
23165                            &kernel_readiness,
23166                        );
23167                    } else if let Some(socket) = process.unix_sockets.remove(socket_id) {
23168                        release_unix_socket_handle(process, socket_id, socket);
23169                    }
23170                    Ok(json!({
23171                        "type": "close",
23172                        "hadError": had_error,
23173                    }))
23174                }
23175                None => Ok(Value::Null),
23176            }
23177        }
23178        "net.socket_wait_connect" => {
23179            let socket_id =
23180                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_wait_connect socket id")?;
23181            if let Some(socket) = process.tcp_sockets.get(socket_id) {
23182                javascript_net_json_string(socket.socket_info(), "net.socket_wait_connect")
23183            } else {
23184                let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
23185                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
23186                })?;
23187                javascript_net_json_string(socket.socket_info(), "net.socket_wait_connect")
23188            }
23189        }
23190        "net.socket_read" => {
23191            let socket_id =
23192                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_read socket id")?;
23193            if trace_enabled {
23194                NET_TCP_TRACE_COUNTERS
23195                    .socket_read_calls
23196                    .fetch_add(1, Ordering::Relaxed);
23197                NET_TCP_TRACE_COUNTERS
23198                    .socket_read_zero_wait_calls
23199                    .fetch_add(1, Ordering::Relaxed);
23200            }
23201            if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
23202                javascript_net_read_value(socket.poll(
23203                    kernel,
23204                    process.kernel_pid,
23205                    Duration::ZERO,
23206                    trace_enabled,
23207                )?)
23208            } else {
23209                let socket = process.unix_sockets.get_mut(socket_id).ok_or_else(|| {
23210                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
23211                })?;
23212                javascript_net_read_value(socket.poll(Duration::ZERO)?)
23213            }
23214        }
23215        "net.socket_set_no_delay" => {
23216            let socket_id =
23217                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_set_no_delay socket id")?;
23218            let enable =
23219                javascript_sync_rpc_arg_bool(&request.args, 1, "net.socket_set_no_delay enabled")?;
23220            if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
23221                socket.set_no_delay(enable)?;
23222            } else if !process.unix_sockets.contains_key(socket_id) {
23223                return Err(SidecarError::InvalidState(format!(
23224                    "unknown net socket {socket_id}"
23225                )));
23226            }
23227            Ok(Value::Null)
23228        }
23229        "net.socket_set_keep_alive" => {
23230            let socket_id = javascript_sync_rpc_arg_str(
23231                &request.args,
23232                0,
23233                "net.socket_set_keep_alive socket id",
23234            )?;
23235            let enable = javascript_sync_rpc_arg_bool(
23236                &request.args,
23237                1,
23238                "net.socket_set_keep_alive enabled",
23239            )?;
23240            let initial_delay_secs = javascript_sync_rpc_arg_u64_optional(
23241                &request.args,
23242                2,
23243                "net.socket_set_keep_alive initial delay seconds",
23244            )?;
23245            if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
23246                socket.set_keep_alive(enable, initial_delay_secs)?;
23247            } else if !process.unix_sockets.contains_key(socket_id) {
23248                return Err(SidecarError::InvalidState(format!(
23249                    "unknown net socket {socket_id}"
23250                )));
23251            }
23252            Ok(Value::Null)
23253        }
23254        "net.socket_upgrade_tls" => {
23255            let socket_id =
23256                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_upgrade_tls socket id")?;
23257            let options_json =
23258                javascript_sync_rpc_arg_str(&request.args, 1, "net.socket_upgrade_tls options")?;
23259            let options: JavascriptTlsBridgeOptions =
23260                serde_json::from_str(options_json).map_err(|error| {
23261                    SidecarError::InvalidState(format!(
23262                        "net.socket_upgrade_tls options must be valid JSON: {error}"
23263                    ))
23264                })?;
23265            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
23266                SidecarError::InvalidState(format!(
23267                    "unknown TCP socket {socket_id} for TLS upgrade"
23268                ))
23269            })?;
23270            socket.upgrade_tls(vm_id, kernel, options)?;
23271            Ok(Value::Null)
23272        }
23273        "net.socket_get_tls_client_hello" => {
23274            let socket_id = javascript_sync_rpc_arg_str(
23275                &request.args,
23276                0,
23277                "net.socket_get_tls_client_hello socket id",
23278            )?;
23279            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
23280                SidecarError::InvalidState(format!(
23281                    "unknown TCP socket {socket_id} for TLS client hello query"
23282                ))
23283            })?;
23284            socket.tls_client_hello_json(vm_id, kernel)
23285        }
23286        "net.socket_tls_query" => {
23287            let socket_id =
23288                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_tls_query socket id")?;
23289            let query =
23290                javascript_sync_rpc_arg_str(&request.args, 1, "net.socket_tls_query query")?;
23291            let detailed = request
23292                .args
23293                .get(2)
23294                .and_then(Value::as_bool)
23295                .unwrap_or(false);
23296            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
23297                SidecarError::InvalidState(format!("unknown TCP socket {socket_id} for TLS query"))
23298            })?;
23299            socket.tls_query(query, detailed)
23300        }
23301        "net.server_poll" => {
23302            let listener_id =
23303                javascript_sync_rpc_arg_str(&request.args, 0, "net.server_poll listener id")?;
23304            let wait_ms =
23305                javascript_sync_rpc_arg_u64_optional(&request.args, 1, "net.server_poll wait ms")?
23306                    .unwrap_or_default();
23307            let tcp_event = if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
23308                Some(listener.poll(
23309                    kernel,
23310                    process.kernel_pid,
23311                    Duration::from_millis(wait_ms),
23312                    trace_enabled,
23313                )?)
23314            } else {
23315                None
23316            };
23317
23318            if let Some(event) = tcp_event {
23319                return match event {
23320                    Some(JavascriptTcpListenerEvent::Connection(pending)) => {
23321                        let PendingTcpSocket {
23322                            stream,
23323                            kernel_socket_id,
23324                            preallocated,
23325                            guest_local_addr,
23326                            guest_remote_addr,
23327                        } = pending;
23328                        if !preallocated {
23329                            if let Err(error) = check_network_resource_limit(
23330                                resource_limits.max_sockets,
23331                                network_counts.sockets,
23332                                1,
23333                                "socket",
23334                            )
23335                            .and_then(|()| {
23336                                check_network_resource_limit(
23337                                    resource_limits.max_connections,
23338                                    network_counts.connections,
23339                                    1,
23340                                    "connection",
23341                                )
23342                            }) {
23343                                if let Some(stream) = stream {
23344                                    let _ = stream.shutdown(Shutdown::Both);
23345                                }
23346                                return Ok(json!({
23347                                    "type": "error",
23348                                    "code": "EAGAIN",
23349                                    "message": error.to_string(),
23350                                }));
23351                            }
23352                        }
23353                        let socket = if let Some(stream) = stream {
23354                            ActiveTcpSocket::from_stream(
23355                                stream,
23356                                Some(listener_id.to_string()),
23357                                guest_local_addr,
23358                                guest_remote_addr,
23359                            )?
23360                        } else {
23361                            ActiveTcpSocket::from_kernel(
23362                                kernel_socket_id.ok_or_else(|| {
23363                                    SidecarError::InvalidState(String::from(
23364                                        "kernel TCP accept missing socket id",
23365                                    ))
23366                                })?,
23367                                Some(listener_id.to_string()),
23368                                guest_local_addr,
23369                                guest_remote_addr,
23370                            )
23371                        };
23372                        let socket_id = process.allocate_tcp_socket_id();
23373                        socket.set_event_pusher(
23374                            process.execution.javascript_v8_session_handle(),
23375                            socket_id.clone(),
23376                        );
23377                        register_kernel_readiness_target(
23378                            &kernel_readiness,
23379                            socket.kernel_socket_id,
23380                            process.execution.javascript_v8_session_handle(),
23381                            socket_id.clone(),
23382                            KernelSocketReadinessEvent::Data,
23383                        );
23384                        if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
23385                            listener.register_connection(&socket_id);
23386                        }
23387                        process.tcp_sockets.insert(socket_id.clone(), socket);
23388                        Ok(json!({
23389                            "type": "connection",
23390                            "socketId": socket_id,
23391                            "localAddress": guest_local_addr.ip().to_string(),
23392                            "localPort": guest_local_addr.port(),
23393                            "remoteAddress": guest_remote_addr.ip().to_string(),
23394                            "remotePort": guest_remote_addr.port(),
23395                            "remoteFamily": socket_addr_family(&guest_remote_addr),
23396                        }))
23397                    }
23398                    Some(JavascriptTcpListenerEvent::Error { code, message }) => Ok(json!({
23399                        "type": "error",
23400                        "code": code,
23401                        "message": message,
23402                    })),
23403                    None => Ok(Value::Null),
23404                };
23405            }
23406
23407            let event = {
23408                let listener = process.unix_listeners.get_mut(listener_id).ok_or_else(|| {
23409                    SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
23410                })?;
23411                listener.poll(Duration::from_millis(wait_ms))?
23412            };
23413
23414            match event {
23415                Some(JavascriptUnixListenerEvent::Connection(pending)) => {
23416                    if let Err(error) = check_network_resource_limit(
23417                        resource_limits.max_sockets,
23418                        network_counts.sockets,
23419                        1,
23420                        "socket",
23421                    )
23422                    .and_then(|()| {
23423                        check_network_resource_limit(
23424                            resource_limits.max_connections,
23425                            network_counts.connections,
23426                            1,
23427                            "connection",
23428                        )
23429                    }) {
23430                        let _ = pending.stream.shutdown(Shutdown::Both);
23431                        return Ok(json!({
23432                            "type": "error",
23433                            "code": "EAGAIN",
23434                            "message": error.to_string(),
23435                        }));
23436                    }
23437                    let socket = ActiveUnixSocket::from_stream(
23438                        pending.stream,
23439                        Some(listener_id.to_string()),
23440                        pending.local_path.clone(),
23441                        pending.remote_path.clone(),
23442                    )?;
23443                    let socket_id = process.allocate_unix_socket_id();
23444                    socket.set_event_pusher(
23445                        process.execution.javascript_v8_session_handle(),
23446                        socket_id.clone(),
23447                    );
23448                    if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
23449                        listener.register_connection(&socket_id);
23450                    }
23451                    process.unix_sockets.insert(socket_id.clone(), socket);
23452                    Ok(json!({
23453                        "type": "connection",
23454                        "socketId": socket_id,
23455                        "localPath": pending.local_path,
23456                        "remotePath": pending.remote_path,
23457                    }))
23458                }
23459                Some(JavascriptUnixListenerEvent::Error { code, message }) => Ok(json!({
23460                    "type": "error",
23461                    "code": code,
23462                    "message": message,
23463                })),
23464                None => Ok(Value::Null),
23465            }
23466        }
23467        "net.server_accept" => {
23468            let listener_id =
23469                javascript_sync_rpc_arg_str(&request.args, 0, "net.server_accept listener id")?;
23470            if trace_enabled {
23471                NET_TCP_TRACE_COUNTERS
23472                    .server_accept_calls
23473                    .fetch_add(1, Ordering::Relaxed);
23474                NET_TCP_TRACE_COUNTERS
23475                    .server_accept_zero_wait_calls
23476                    .fetch_add(1, Ordering::Relaxed);
23477            }
23478            if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
23479                return match listener.poll(
23480                    kernel,
23481                    process.kernel_pid,
23482                    Duration::ZERO,
23483                    trace_enabled,
23484                )? {
23485                    Some(JavascriptTcpListenerEvent::Connection(pending)) => {
23486                        let PendingTcpSocket {
23487                            stream,
23488                            kernel_socket_id,
23489                            preallocated,
23490                            guest_local_addr,
23491                            guest_remote_addr,
23492                        } = pending;
23493                        if !preallocated {
23494                            check_network_resource_limit(
23495                                resource_limits.max_sockets,
23496                                network_counts.sockets,
23497                                1,
23498                                "socket",
23499                            )?;
23500                            check_network_resource_limit(
23501                                resource_limits.max_connections,
23502                                network_counts.connections,
23503                                1,
23504                                "connection",
23505                            )?;
23506                        }
23507                        let info = tcp_socket_info_value(&guest_local_addr, &guest_remote_addr);
23508                        let socket = if let Some(stream) = stream {
23509                            ActiveTcpSocket::from_stream(
23510                                stream,
23511                                Some(listener_id.to_string()),
23512                                guest_local_addr,
23513                                guest_remote_addr,
23514                            )?
23515                        } else {
23516                            ActiveTcpSocket::from_kernel(
23517                                kernel_socket_id.ok_or_else(|| {
23518                                    SidecarError::InvalidState(String::from(
23519                                        "kernel TCP accept missing socket id",
23520                                    ))
23521                                })?,
23522                                Some(listener_id.to_string()),
23523                                guest_local_addr,
23524                                guest_remote_addr,
23525                            )
23526                        };
23527                        let socket_id = process.allocate_tcp_socket_id();
23528                        socket.set_event_pusher(
23529                            process.execution.javascript_v8_session_handle(),
23530                            socket_id.clone(),
23531                        );
23532                        register_kernel_readiness_target(
23533                            &kernel_readiness,
23534                            socket.kernel_socket_id,
23535                            process.execution.javascript_v8_session_handle(),
23536                            socket_id.clone(),
23537                            KernelSocketReadinessEvent::Data,
23538                        );
23539                        if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
23540                            listener.register_connection(&socket_id);
23541                        }
23542                        process.tcp_sockets.insert(socket_id.clone(), socket);
23543                        javascript_net_json_string(
23544                            json!({
23545                                "socketId": socket_id,
23546                                "info": info,
23547                            }),
23548                            "net.server_accept",
23549                        )
23550                    }
23551                    Some(JavascriptTcpListenerEvent::Error { code, message }) => {
23552                        let detail = code.unwrap_or_else(|| String::from("server accept"));
23553                        Err(SidecarError::Execution(format!("{detail}: {message}")))
23554                    }
23555                    None => Ok(javascript_net_timeout_value()),
23556                };
23557            }
23558
23559            let listener = process.unix_listeners.get_mut(listener_id).ok_or_else(|| {
23560                SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
23561            })?;
23562            match listener.poll(Duration::ZERO)? {
23563                Some(JavascriptUnixListenerEvent::Connection(pending)) => {
23564                    check_network_resource_limit(
23565                        resource_limits.max_sockets,
23566                        network_counts.sockets,
23567                        1,
23568                        "socket",
23569                    )?;
23570                    check_network_resource_limit(
23571                        resource_limits.max_connections,
23572                        network_counts.connections,
23573                        1,
23574                        "connection",
23575                    )?;
23576                    let info = json!({
23577                        "localPath": pending.local_path.clone(),
23578                        "remotePath": pending.remote_path.clone(),
23579                    });
23580                    let socket = ActiveUnixSocket::from_stream(
23581                        pending.stream,
23582                        Some(listener_id.to_string()),
23583                        pending.local_path,
23584                        pending.remote_path,
23585                    )?;
23586                    let socket_id = process.allocate_unix_socket_id();
23587                    socket.set_event_pusher(
23588                        process.execution.javascript_v8_session_handle(),
23589                        socket_id.clone(),
23590                    );
23591                    if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
23592                        listener.register_connection(&socket_id);
23593                    }
23594                    process.unix_sockets.insert(socket_id.clone(), socket);
23595                    javascript_net_json_string(
23596                        json!({
23597                            "socketId": socket_id,
23598                            "info": info,
23599                        }),
23600                        "net.server_accept",
23601                    )
23602                }
23603                Some(JavascriptUnixListenerEvent::Error { code, message }) => {
23604                    let detail = code.unwrap_or_else(|| String::from("server accept"));
23605                    Err(SidecarError::Execution(format!("{detail}: {message}")))
23606                }
23607                None => Ok(javascript_net_timeout_value()),
23608            }
23609        }
23610        "net.server_connections" => {
23611            let listener_id = javascript_sync_rpc_arg_str(
23612                &request.args,
23613                0,
23614                "net.server_connections listener id",
23615            )?;
23616            if let Some(listener) = process.tcp_listeners.get(listener_id) {
23617                Ok(json!(listener.active_connection_count()))
23618            } else {
23619                let listener = process.unix_listeners.get(listener_id).ok_or_else(|| {
23620                    SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
23621                })?;
23622                Ok(json!(listener.active_connection_count()))
23623            }
23624        }
23625        "net.upgrade_socket_write" => {
23626            let socket_id = javascript_sync_rpc_arg_str(
23627                &request.args,
23628                0,
23629                "net.upgrade_socket_write socket id",
23630            )?;
23631            let chunk =
23632                javascript_sync_rpc_base64_arg(&request.args, 1, "net.upgrade_socket_write chunk")?;
23633            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
23634                SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
23635            })?;
23636            socket
23637                .write_all(kernel, process.kernel_pid, &chunk)
23638                .map(|written| json!(written))
23639        }
23640        "net.upgrade_socket_end" => {
23641            let socket_id =
23642                javascript_sync_rpc_arg_str(&request.args, 0, "net.upgrade_socket_end socket id")?;
23643            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
23644                SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
23645            })?;
23646            socket.shutdown_write(kernel, process.kernel_pid)?;
23647            Ok(Value::Null)
23648        }
23649        "net.upgrade_socket_destroy" => {
23650            let socket_id = javascript_sync_rpc_arg_str(
23651                &request.args,
23652                0,
23653                "net.upgrade_socket_destroy socket id",
23654            )?;
23655            let socket = process.tcp_sockets.remove(socket_id).ok_or_else(|| {
23656                SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
23657            })?;
23658            release_tcp_socket_handle(process, socket_id, socket, kernel, &kernel_readiness);
23659            Ok(Value::Null)
23660        }
23661        "net.write" => {
23662            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.write socket id")?;
23663            let chunk = if let Some(bytes) = request.raw_bytes_args.get(&1) {
23664                bytes.clone()
23665            } else {
23666                javascript_sync_rpc_bytes_arg(&request.args, 1, "net.write chunk")?
23667            };
23668            if trace_enabled {
23669                NET_TCP_TRACE_COUNTERS
23670                    .socket_write_calls
23671                    .fetch_add(1, Ordering::Relaxed);
23672                NET_TCP_TRACE_COUNTERS.socket_write_bytes.fetch_add(
23673                    u64::try_from(chunk.len()).unwrap_or(u64::MAX),
23674                    Ordering::Relaxed,
23675                );
23676            }
23677            if let Some(socket) = process.tcp_sockets.get(socket_id) {
23678                let write_started = trace_enabled.then(Instant::now);
23679                let write_result = socket.write_all(kernel, process.kernel_pid, &chunk);
23680                if let Some(write_started) = write_started {
23681                    NET_TCP_TRACE_COUNTERS.socket_write_kernel_us.fetch_add(
23682                        duration_micros_u64(write_started.elapsed()),
23683                        Ordering::Relaxed,
23684                    );
23685                }
23686                match write_result {
23687                    Ok(written) => Ok(json!(written)),
23688                    Err(error) => {
23689                        if trace_enabled {
23690                            NET_TCP_TRACE_COUNTERS
23691                                .socket_write_errors
23692                                .fetch_add(1, Ordering::Relaxed);
23693                        }
23694                        Err(error)
23695                    }
23696                }
23697            } else {
23698                let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
23699                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
23700                })?;
23701                socket.write_all(&chunk).map(|written| json!(written))
23702            }
23703        }
23704        "net.shutdown" => {
23705            let socket_id =
23706                javascript_sync_rpc_arg_str(&request.args, 0, "net.shutdown socket id")?;
23707            if let Some(socket) = process.tcp_sockets.get(socket_id) {
23708                socket.shutdown_write(kernel, process.kernel_pid)?;
23709            } else {
23710                let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
23711                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
23712                })?;
23713                socket.shutdown_write()?;
23714            }
23715            Ok(Value::Null)
23716        }
23717        "net.destroy" => {
23718            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.destroy socket id")?;
23719            if let Some(socket) = process.tcp_sockets.remove(socket_id) {
23720                release_tcp_socket_handle(process, socket_id, socket, kernel, &kernel_readiness);
23721                Ok(Value::Null)
23722            } else if let Some(socket) = process.unix_sockets.remove(socket_id) {
23723                release_unix_socket_handle(process, socket_id, socket);
23724                Ok(Value::Null)
23725            } else {
23726                Ok(Value::Null)
23727            }
23728        }
23729        "net.server_close" => {
23730            let listener_id =
23731                javascript_sync_rpc_arg_str(&request.args, 0, "net.server_close listener id")?;
23732            if let Some(listener) = process.tcp_listeners.remove(listener_id) {
23733                unregister_kernel_readiness_target(&kernel_readiness, listener.kernel_socket_id);
23734                listener.close(kernel, process.kernel_pid)?;
23735                Ok(Value::Null)
23736            } else {
23737                let listener = process.unix_listeners.remove(listener_id).ok_or_else(|| {
23738                    SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
23739                })?;
23740                listener.close()?;
23741                Ok(Value::Null)
23742            }
23743        }
23744        "tls.get_ciphers" => javascript_net_json_string(
23745            Value::Array(
23746                tls_provider()
23747                    .cipher_suites
23748                    .iter()
23749                    .filter_map(|suite| {
23750                        suite
23751                            .suite()
23752                            .as_str()
23753                            .map(|value| Value::String(value.to_owned()))
23754                    })
23755                    .collect(),
23756            ),
23757            "tls.get_ciphers",
23758        ),
23759        _ => Err(SidecarError::InvalidState(format!(
23760            "unsupported JavaScript net sync RPC method {}",
23761            request.method
23762        ))),
23763    }
23764}
23765
23766fn signal_name_for_stream_event(signal: i32) -> Option<&'static str> {
23767    match signal {
23768        libc::SIGHUP => Some("SIGHUP"),
23769        libc::SIGINT => Some("SIGINT"),
23770        libc::SIGUSR1 => Some("SIGUSR1"),
23771        libc::SIGALRM => Some("SIGALRM"),
23772        libc::SIGCONT => Some("SIGCONT"),
23773        libc::SIGTERM => Some("SIGTERM"),
23774        libc::SIGCHLD => Some("SIGCHLD"),
23775        libc::SIGWINCH => Some("SIGWINCH"),
23776        _ => None,
23777    }
23778}
23779
23780pub(crate) fn canonical_signal_name(signal: i32) -> Option<&'static str> {
23781    secure_exec_sidecar_core::canonical_signal_name(signal)
23782}
23783
23784fn dispatch_v8_process_signal(process: &ActiveProcess, signal: i32) -> Result<bool, SidecarError> {
23785    let Some(signal_name) = signal_name_for_stream_event(signal) else {
23786        return Ok(false);
23787    };
23788    process.execution.send_javascript_stream_event(
23789        "signal",
23790        json!({
23791            "signal": signal_name,
23792            "number": signal,
23793            "action": "default",
23794        }),
23795    )?;
23796    Ok(true)
23797}
23798
23799fn dispatch_v8_session_signal_async(session: V8SessionHandle, signal: i32) {
23800    let Some(signal_name) = signal_name_for_stream_event(signal).map(str::to_owned) else {
23801        return;
23802    };
23803    thread::spawn(move || {
23804        thread::sleep(Duration::from_millis(1));
23805        let payload = v8_runtime::json_to_cbor_payload(&json!({
23806            "signal": signal_name,
23807            "number": signal,
23808            "action": "default",
23809        }))
23810        .unwrap_or_default();
23811        let _ = session.send_stream_event("signal", payload);
23812    });
23813}
23814
23815pub(crate) fn parse_signal(signal: &str) -> Result<i32, SidecarError> {
23816    let trimmed = signal.trim();
23817    if trimmed.is_empty() {
23818        return Err(SidecarError::InvalidState(String::from(
23819            "kill_process requires a non-empty signal",
23820        )));
23821    }
23822
23823    if let Ok(value) = trimmed.parse::<i32>() {
23824        return match value {
23825            0..=31 => Ok(value),
23826            _ => Err(SidecarError::InvalidState(format!(
23827                "unsupported kill_process signal {signal}"
23828            ))),
23829        };
23830    }
23831
23832    secure_exec_sidecar_core::parse_posix_signal(trimmed).ok_or_else(|| {
23833        SidecarError::InvalidState(format!("unsupported kill_process signal {signal}"))
23834    })
23835}
23836
23837pub(crate) fn runtime_child_is_alive(child_pid: u32) -> Result<bool, SidecarError> {
23838    Ok(runtime_child_exit_status(child_pid)?.is_none())
23839}
23840
23841#[cfg(not(target_os = "macos"))]
23842fn runtime_child_exit_status(child_pid: u32) -> Result<Option<i32>, SidecarError> {
23843    if child_pid == 0 {
23844        return Ok(Some(0));
23845    }
23846
23847    let wait_flags = WaitPidFlag::WNOHANG
23848        | WaitPidFlag::WNOWAIT
23849        | WaitPidFlag::WEXITED
23850        | WaitPidFlag::WUNTRACED
23851        | WaitPidFlag::WCONTINUED;
23852    match wait_on_child(WaitId::Pid(Pid::from_raw(child_pid as i32)), wait_flags) {
23853        Ok(WaitStatus::StillAlive)
23854        | Ok(WaitStatus::Stopped(_, _))
23855        | Ok(WaitStatus::Continued(_)) => Ok(None),
23856        Ok(WaitStatus::Exited(_, status)) => Ok(Some(status)),
23857        Ok(WaitStatus::Signaled(_, signal, _)) => Ok(Some(128 + signal as i32)),
23858        #[cfg(any(target_os = "linux", target_os = "android"))]
23859        Ok(WaitStatus::PtraceEvent(_, _, _) | WaitStatus::PtraceSyscall(_)) => Ok(None),
23860        Err(nix::errno::Errno::ECHILD) => Ok(Some(0)),
23861        Err(error) => Err(SidecarError::Execution(format!(
23862            "failed to inspect guest runtime process {child_pid}: {error}"
23863        ))),
23864    }
23865}
23866
23867// macOS nix exposes no `waitid`/`WNOWAIT`, so we poll with `waitpid(WNOHANG)`.
23868// NOTE: unlike Linux's `waitid(WNOWAIT)`, `waitpid` REAPS an exited child rather
23869// than leaving it waitable. That is correct for this poll (the sidecar is the
23870// reaping parent), but a second status query after exit returns ECHILD → treated
23871// as "exited(0)" below.
23872#[cfg(target_os = "macos")]
23873fn runtime_child_exit_status(child_pid: u32) -> Result<Option<i32>, SidecarError> {
23874    if child_pid == 0 {
23875        return Ok(Some(0));
23876    }
23877
23878    match waitpid(Pid::from_raw(child_pid as i32), Some(WaitPidFlag::WNOHANG)) {
23879        Ok(WaitStatus::StillAlive)
23880        | Ok(WaitStatus::Stopped(_, _))
23881        | Ok(WaitStatus::Continued(_)) => Ok(None),
23882        Ok(WaitStatus::Exited(_, status)) => Ok(Some(status)),
23883        Ok(WaitStatus::Signaled(_, signal, _)) => Ok(Some(128 + signal as i32)),
23884        Err(nix::errno::Errno::ECHILD) => Ok(Some(0)),
23885        Err(error) => Err(SidecarError::Execution(format!(
23886            "failed to inspect guest runtime process {child_pid}: {error}"
23887        ))),
23888    }
23889}
23890
23891pub(crate) fn signal_runtime_process(child_pid: u32, signal: i32) -> Result<(), SidecarError> {
23892    if child_pid == 0 {
23893        return Ok(());
23894    }
23895
23896    if !runtime_child_is_alive(child_pid)? {
23897        return Ok(());
23898    }
23899
23900    if signal == 0 {
23901        return Ok(());
23902    }
23903
23904    let parsed = Signal::try_from(signal).map_err(|_| {
23905        SidecarError::InvalidState(format!("unsupported kill_process signal {signal}"))
23906    })?;
23907    let result = send_signal(Pid::from_raw(child_pid as i32), Some(parsed));
23908
23909    match result {
23910        Ok(()) => Ok(()),
23911        Err(nix::errno::Errno::ESRCH) => Ok(()),
23912        Err(error) => Err(SidecarError::Execution(format!(
23913            "failed to signal guest runtime process {child_pid}: {error}"
23914        ))),
23915    }
23916}
23917
23918pub(crate) fn error_code(error: &SidecarError) -> &'static str {
23919    match error {
23920        SidecarError::InvalidState(_) => "invalid_state",
23921        SidecarError::ProtocolVersionMismatch(_) => "protocol_version_mismatch",
23922        SidecarError::BridgeVersionMismatch(_) => "bridge_version_mismatch",
23923        SidecarError::Conflict(_) => "conflict",
23924        SidecarError::Unauthorized(_) => "unauthorized",
23925        SidecarError::Unsupported(_) => "unsupported",
23926        SidecarError::FrameTooLarge(_) => "frame_too_large",
23927        SidecarError::Kernel(_) => "kernel_error",
23928        SidecarError::Plugin(_) => "plugin_error",
23929        SidecarError::Execution(_) => "execution_error",
23930        SidecarError::Bridge(_) => "bridge_error",
23931        SidecarError::Io(_) => "io_error",
23932    }
23933}
23934
23935fn guest_errno_code(message: &str) -> Option<&str> {
23936    const TRUSTED_PREFIXES: &[&str] = &[
23937        "ERR_AGENTOS_NODE_SYNC_RPC",
23938        "ERR_AGENTOS_PYTHON_VFS_RPC",
23939        "ERR_AGENTOS_BRIDGE",
23940    ];
23941
23942    let mut segments = message.split(':').map(str::trim);
23943    let first = segments.next()?;
23944    if is_guest_errno_segment(first) {
23945        return Some(first);
23946    }
23947
23948    if TRUSTED_PREFIXES.contains(&first) {
23949        let second = segments.next()?;
23950        if is_guest_errno_segment(second) {
23951            return Some(second);
23952        }
23953    }
23954
23955    None
23956}
23957
23958fn is_guest_errno_segment(segment: &str) -> bool {
23959    segment.len() >= 2
23960        && segment.starts_with('E')
23961        && !segment.starts_with("ERR_")
23962        && segment[1..]
23963            .bytes()
23964            .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_')
23965}
23966
23967pub(crate) fn javascript_sync_rpc_error_code(error: &SidecarError) -> String {
23968    let message = error.to_string();
23969    if let Some(code) = guest_errno_code(&message) {
23970        return code.to_owned();
23971    }
23972    if message.starts_with("ERR_NATIVE_BINARY_NOT_SUPPORTED:") {
23973        return String::from("ERR_NATIVE_BINARY_NOT_SUPPORTED");
23974    }
23975
23976    let lower = message.to_ascii_lowercase();
23977    if lower.contains("no such file or directory")
23978        || lower.contains("entry not found")
23979        || lower.contains("not found")
23980    {
23981        return String::from("ENOENT");
23982    }
23983    if lower.contains("permission denied") {
23984        return String::from("EACCES");
23985    }
23986    if lower.contains("already exists")
23987        || lower.contains("already registered")
23988        || lower.contains("file exists")
23989    {
23990        return String::from("EEXIST");
23991    }
23992    if lower.contains("invalid argument") {
23993        return String::from("EINVAL");
23994    }
23995
23996    String::from("ERR_AGENTOS_NODE_SYNC_RPC")
23997}
23998
23999pub(crate) fn ignore_stale_javascript_sync_rpc_response(
24000    error: SidecarError,
24001) -> Result<(), SidecarError> {
24002    match error {
24003        SidecarError::Execution(message)
24004            if message.ends_with("is no longer pending")
24005                && message.starts_with("sync RPC request ") =>
24006        {
24007            Ok(())
24008        }
24009        SidecarError::Execution(message) => {
24010            let lower = message.to_ascii_lowercase();
24011            if lower.contains("sync rpc response")
24012                && (lower.contains("broken pipe") || lower.contains("channel closed unexpectedly"))
24013            {
24014                Ok(())
24015            } else {
24016                Err(SidecarError::Execution(message))
24017            }
24018        }
24019        other => Err(other),
24020    }
24021}
24022
24023#[cfg(test)]
24024mod error_code_tests {
24025    use super::{guest_errno_code, javascript_sync_rpc_error_code, SidecarError};
24026
24027    #[test]
24028    fn guest_errno_code_rejects_guest_controlled_errno_segments() {
24029        assert_eq!(guest_errno_code("user said 'EACCES: denied'"), None);
24030        assert_eq!(
24031            guest_errno_code("prefix: user said 'EPERM': more text"),
24032            None
24033        );
24034        assert_eq!(guest_errno_code("ERR_AGENTOS_FAKE: EACCES: denied"), None);
24035    }
24036
24037    #[test]
24038    fn guest_errno_code_accepts_trusted_secure_exec_prefixes() {
24039        assert_eq!(
24040            guest_errno_code("ERR_AGENTOS_NODE_SYNC_RPC: EACCES: permission denied on /foo"),
24041            Some("EACCES")
24042        );
24043        assert_eq!(
24044            guest_errno_code("ERR_AGENTOS_PYTHON_VFS_RPC: ENOENT: missing file"),
24045            Some("ENOENT")
24046        );
24047        assert_eq!(guest_errno_code("EEXIST: already exists"), Some("EEXIST"));
24048    }
24049
24050    #[test]
24051    fn javascript_sync_rpc_error_code_ignores_spoofed_errnos() {
24052        let error = SidecarError::Execution(String::from("user said 'EACCES: denied'"));
24053        assert_eq!(
24054            javascript_sync_rpc_error_code(&error),
24055            "ERR_AGENTOS_NODE_SYNC_RPC"
24056        );
24057    }
24058
24059    #[test]
24060    fn javascript_sync_rpc_error_code_preserves_real_sidecar_errnos() {
24061        let error = SidecarError::Execution(String::from(
24062            "ERR_AGENTOS_NODE_SYNC_RPC: EACCES: permission denied on /foo",
24063        ));
24064        assert_eq!(javascript_sync_rpc_error_code(&error), "EACCES");
24065    }
24066
24067    #[test]
24068    fn javascript_sync_rpc_error_code_maps_file_exists_messages() {
24069        let error = SidecarError::Io(String::from(
24070            "failed to create mapped guest directory /.next/server: File exists (os error 17)",
24071        ));
24072        assert_eq!(javascript_sync_rpc_error_code(&error), "EEXIST");
24073    }
24074
24075    #[test]
24076    fn javascript_sync_rpc_error_code_preserves_native_binary_rejections() {
24077        let error = SidecarError::Execution(String::from(
24078            "ERR_NATIVE_BINARY_NOT_SUPPORTED: refused to execute native ELF guest binary at /tmp/fake-rg inside the VM",
24079        ));
24080        assert_eq!(
24081            javascript_sync_rpc_error_code(&error),
24082            "ERR_NATIVE_BINARY_NOT_SUPPORTED"
24083        );
24084    }
24085}
24086#[cfg(test)]
24087mod ssrf_egress_classifier_tests {
24088    // F-005/006/007 (sec-sidecar T1/T7/T11): the egress classifier must treat the
24089    // unspecified address (0.0.0.0 / ::), CGNAT (100.64.0.0/10), IPv6 spellings of
24090    // restricted IPv4 targets (::a.b.c.d), and reserved/multicast (240/4, 224/4) as
24091    // restricted. 0.0.0.0 routes to 127.0.0.1 on connect(), so leaving it
24092    // unclassified let a guest bypass the loopback port-ownership gate.
24093    //
24094    // These are bounded SAFEGUARD tests: they exercise the classifier and the DNS
24095    // egress filter directly (no network I/O, no Node), so they run fast and
24096    // deterministically. See FAILURES.md#F-005, #F-006, #F-007.
24097    use super::{
24098        filter_dns_safe_ip_addrs, is_loopback_ip, restricted_non_loopback_ip_range, SidecarError,
24099    };
24100    use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
24101
24102    fn assert_restricted(ip: IpAddr, expected_label: &str) {
24103        let classification = restricted_non_loopback_ip_range(ip);
24104        assert!(
24105            classification.is_some(),
24106            "{ip} must be classified as a restricted egress target"
24107        );
24108        let (_cidr, label) = classification.unwrap();
24109        assert_eq!(
24110            label, expected_label,
24111            "{ip} should be labelled {expected_label}, got {label}"
24112        );
24113    }
24114
24115    fn assert_dns_denied(ip: IpAddr, label: &str) {
24116        match filter_dns_safe_ip_addrs(vec![ip], "attacker.example") {
24117            Err(SidecarError::Execution(message)) => assert!(
24118                message.starts_with("EACCES:"),
24119                "{label}: egress filter must deny with EACCES, got: {message}"
24120            ),
24121            other => panic!("{label}: expected EACCES denial, got {other:?}"),
24122        }
24123    }
24124
24125    // F-005 (sec-sidecar T1).
24126    #[test]
24127    fn classifier_denies_unspecified_and_cgnat_targets() {
24128        // 0.0.0.0 (IPv4 unspecified) -> would route to host loopback.
24129        assert_restricted(IpAddr::V4(Ipv4Addr::UNSPECIFIED), "unspecified");
24130        // :: (IPv6 unspecified).
24131        assert_restricted(IpAddr::V6(Ipv6Addr::UNSPECIFIED), "unspecified");
24132
24133        // CGNAT 100.64.0.0/10 spans 100.64.x.x .. 100.127.x.x.
24134        assert_restricted(
24135            IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1)),
24136            "carrier-grade-nat",
24137        );
24138        assert_restricted(
24139            IpAddr::V4(Ipv4Addr::new(100, 127, 255, 254)),
24140            "carrier-grade-nat",
24141        );
24142
24143        // Guard against over-blocking: addresses just outside 100.64/10 stay allowed.
24144        assert!(
24145            restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(100, 63, 255, 255)))
24146                .is_none(),
24147            "100.63.255.255 is outside CGNAT and must remain allowed"
24148        );
24149        assert!(
24150            restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(100, 128, 0, 0))).is_none(),
24151            "100.128.0.0 is outside CGNAT and must remain allowed"
24152        );
24153
24154        // The DNS egress filter must also deny these via EACCES.
24155        assert_dns_denied(IpAddr::V4(Ipv4Addr::UNSPECIFIED), "0.0.0.0 (unspecified)");
24156        assert_dns_denied(IpAddr::V6(Ipv6Addr::UNSPECIFIED), ":: (unspecified)");
24157        assert_dns_denied(
24158            IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1)),
24159            "100.64.0.1 (CGNAT)",
24160        );
24161    }
24162
24163    // F-006 (sec-sidecar T7).
24164    #[test]
24165    fn classifier_denies_ipv6_spelled_metadata_addresses() {
24166        // The IPv4-mapped form (::ffff:169.254.169.254) was already handled; the
24167        // IPv4-compatible form (::169.254.169.254) is the gap this fixes.
24168        let mapped = "::ffff:169.254.169.254".parse::<Ipv6Addr>().unwrap();
24169        assert_restricted(IpAddr::V6(mapped), "link-local");
24170
24171        let compat = "::169.254.169.254".parse::<Ipv6Addr>().unwrap();
24172        assert_restricted(IpAddr::V6(compat), "link-local");
24173
24174        // Other IPv4-compatible private/CGNAT spellings must also be canonicalized.
24175        assert_restricted(
24176            IpAddr::V6("::10.0.0.1".parse::<Ipv6Addr>().unwrap()),
24177            "private",
24178        );
24179        assert_restricted(
24180            IpAddr::V6("::100.64.0.1".parse::<Ipv6Addr>().unwrap()),
24181            "carrier-grade-nat",
24182        );
24183
24184        // Guard against over-blocking: the IPv6 unspecified/loopback addresses
24185        // are not IPv4-compatible host targets, and a public IPv4-compatible
24186        // address must remain allowed.
24187        assert_eq!(
24188            restricted_non_loopback_ip_range(IpAddr::V6(Ipv6Addr::UNSPECIFIED)),
24189            Some(("::/128", "unspecified")),
24190            ":: must classify as unspecified, not via the IPv4-compat path"
24191        );
24192        assert!(
24193            restricted_non_loopback_ip_range(IpAddr::V6(Ipv6Addr::LOCALHOST)).is_none()
24194                || is_loopback_ip(IpAddr::V6(Ipv6Addr::LOCALHOST)),
24195            "::1 must not be classified as a restricted IPv4-compatible target"
24196        );
24197        assert!(
24198            restricted_non_loopback_ip_range(IpAddr::V6("::8.8.8.8".parse::<Ipv6Addr>().unwrap()))
24199                .is_none(),
24200            "::8.8.8.8 (public IPv4-compatible) must remain allowed"
24201        );
24202
24203        // The DNS egress filter must deny the IPv4-compat metadata spelling.
24204        assert_dns_denied(
24205            IpAddr::V6("::169.254.169.254".parse::<Ipv6Addr>().unwrap()),
24206            "::169.254.169.254 (IPv4-compat metadata)",
24207        );
24208    }
24209
24210    // F-007 (sec-sidecar T11).
24211    #[test]
24212    fn classifier_denies_reserved_and_multicast_targets() {
24213        // 224.0.0.0/4 (multicast) and 240.0.0.0/4 (reserved / future use) are not
24214        // legitimate unicast egress targets; a guest connect to them must be
24215        // classified as restricted and denied.
24216        assert_restricted(IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)), "multicast");
24217        assert_restricted(IpAddr::V4(Ipv4Addr::new(239, 255, 255, 255)), "multicast");
24218        assert_restricted(IpAddr::V4(Ipv4Addr::new(240, 0, 0, 1)), "reserved");
24219        // 255.255.255.255 (limited broadcast) falls in 240.0.0.0/4.
24220        assert_restricted(IpAddr::V4(Ipv4Addr::BROADCAST), "reserved");
24221
24222        // IPv4-compatible IPv6 spellings must canonicalize and be denied too.
24223        assert_restricted(
24224            IpAddr::V6("::224.0.0.1".parse::<Ipv6Addr>().unwrap()),
24225            "multicast",
24226        );
24227        assert_restricted(
24228            IpAddr::V6("::240.0.0.1".parse::<Ipv6Addr>().unwrap()),
24229            "reserved",
24230        );
24231
24232        // Guard against over-blocking: addresses just outside 224/4 stay allowed.
24233        assert!(
24234            restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(223, 255, 255, 255)))
24235                .is_none(),
24236            "223.255.255.255 is outside 224/4 and must remain allowed"
24237        );
24238
24239        // The DNS egress filter must also deny these via EACCES.
24240        assert_dns_denied(
24241            IpAddr::V4(Ipv4Addr::new(240, 0, 0, 1)),
24242            "240.0.0.1 (reserved)",
24243        );
24244        assert_dns_denied(
24245            IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)),
24246            "224.0.0.1 (multicast)",
24247        );
24248    }
24249}
24250
24251/// Adversarial coverage for the DNS-rebinding gap (VECTORS.md D.3) on the
24252/// Python/Pyodide `httpRequestSync` outbound HTTP path. The egress range guard
24253/// (`filter_dns_safe_ip_addrs`) runs at resolution time, but `ureq` performs its
24254/// own DNS resolution for the TCP/TLS connect, so a rebinding DNS server could
24255/// previously make the second lookup land on a private/link-local/metadata IP
24256/// the first check rejected. The fix pins `ureq`'s resolver to the vetted
24257/// address set; these tests prove the connect is pinned and refuses any other
24258/// host or an empty (fully-rejected) address set.
24259#[cfg(test)]
24260mod dns_rebinding_pin_tests {
24261    use super::{issue_outbound_http_request, split_netloc, JavascriptHttpRequestOptions};
24262    use std::collections::BTreeMap;
24263    use std::io::{Read, Write};
24264    use std::net::{IpAddr, Ipv4Addr, TcpListener};
24265    use std::thread;
24266    use url::Url;
24267
24268    fn empty_headers() -> super::HttpHeaderCollection {
24269        super::parse_http_header_collection(&BTreeMap::new(), "test headers")
24270            .expect("empty header collection")
24271    }
24272
24273    fn options() -> JavascriptHttpRequestOptions {
24274        JavascriptHttpRequestOptions {
24275            method: Some(String::from("GET")),
24276            headers: BTreeMap::new(),
24277            body: None,
24278            reject_unauthorized: None,
24279        }
24280    }
24281
24282    #[test]
24283    fn split_netloc_handles_hostnames_and_bracketed_ipv6() {
24284        assert_eq!(
24285            split_netloc("attacker.example:80"),
24286            Some(("attacker.example", 80))
24287        );
24288        assert_eq!(split_netloc("[::1]:443"), Some(("::1", 443)));
24289        assert_eq!(split_netloc("10.0.0.1:8080"), Some(("10.0.0.1", 8080)));
24290        assert_eq!(split_netloc("no-port"), None);
24291        assert_eq!(split_netloc("host:notaport"), None);
24292    }
24293
24294    /// A loopback HTTP server stands in for the egress-vetted target. The
24295    /// request URL uses a *different* hostname (`attacker.example`) whose real
24296    /// DNS would resolve elsewhere; pinning forces the connect onto the vetted
24297    /// IP only. If the resolver were unpinned, the request would fail to reach
24298    /// this server (and on a real host could land on a private/metadata IP).
24299    #[test]
24300    fn outbound_http_connect_is_pinned_to_vetted_ip() {
24301        let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).expect("bind loopback server");
24302        let port = listener.local_addr().expect("local addr").port();
24303        let server = thread::spawn(move || {
24304            let (mut stream, _) = listener.accept().expect("accept");
24305            let mut buf = [0u8; 1024];
24306            let _ = stream.read(&mut buf);
24307            stream
24308                .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi")
24309                .expect("write response");
24310            let _ = stream.flush();
24311        });
24312
24313        let url = Url::parse(&format!("http://attacker.example:{port}/")).expect("url");
24314        let pinned = vec![IpAddr::V4(Ipv4Addr::LOCALHOST)];
24315        let result = issue_outbound_http_request(&url, &options(), &empty_headers(), &pinned)
24316            .expect("pinned request should reach the vetted loopback target");
24317        let payload = result.as_str().expect("string payload");
24318        assert!(
24319            payload.contains("\"status\":200"),
24320            "expected 200 from pinned target, got: {payload}"
24321        );
24322        server.join().expect("server thread");
24323    }
24324
24325    /// With no vetted address (every resolved IP was rejected by the range
24326    /// guard, or the literal IP was a blocked range), the pinned resolver must
24327    /// refuse rather than fall back to the host resolver.
24328    #[test]
24329    fn outbound_http_refuses_when_no_vetted_address() {
24330        let url = Url::parse("https://attacker.example/").expect("url");
24331        let error = issue_outbound_http_request(&url, &options(), &empty_headers(), &[])
24332            .expect_err("empty pinned set must be refused");
24333        let message = error.to_string();
24334        assert!(
24335            message.contains("EACCES") || message.contains("ERR_HTTP_REQUEST_FAILED"),
24336            "expected an egress refusal, got: {message}"
24337        );
24338    }
24339}