Skip to main content

agentos_native_sidecar/
execution.rs

1//! Process execution, networking, and runtime event handling extracted from service.rs.
2
3use agentos_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 agentos_bridge::{queue_tracker, LifecycleState};
89use agentos_execution::wasm::WasmExecutionError;
90use agentos_execution::{
91    javascript::handle_internal_bridge_call_from_host_context, v8_host::V8SessionHandle,
92    v8_runtime, CreateJavascriptContextRequest, CreatePythonContextRequest,
93    CreateWasmContextRequest, GuestModuleReader, GuestRuntimeConfig, JavascriptExecutionEvent,
94    JavascriptExecutionLimits, JavascriptSyncRpcRequest, ModuleFsReader,
95    NodeSignalDispositionAction, NodeSignalHandlerRegistration, PythonExecutionEvent,
96    PythonExecutionLimits, PythonVfsRpcMethod, PythonVfsRpcRequest, PythonVfsRpcResponsePayload,
97    StartJavascriptExecutionRequest, StartPythonExecutionRequest, StartWasmExecutionRequest,
98    WasmExecutionEvent, WasmExecutionLimits, WasmPermissionTier as ExecutionWasmPermissionTier,
99};
100use agentos_kernel::dns::{
101    DnsLookupPolicy, DnsRecordResolution, DnsResolutionSource as KernelDnsResolutionSource,
102};
103use agentos_kernel::kernel::{KernelProcessHandle, SpawnOptions, VirtualProcessOptions};
104pub(crate) use agentos_kernel::network_policy::format_tcp_resource;
105use agentos_kernel::network_policy::{
106    is_loopback_ip, loopback_cidr, restricted_non_loopback_ip_range,
107};
108use agentos_kernel::permissions::NetworkOperation;
109use agentos_kernel::poll::{PollEvents, PollFd, PollTargetEntry, POLLERR, POLLHUP, POLLIN};
110use agentos_kernel::process_table::{ProcessStatus, WaitPidFlags, SIGKILL, SIGTERM};
111use agentos_kernel::pty::{LineDisciplineConfig, MAX_PTY_BUFFER_BYTES};
112use agentos_kernel::resource_accounting::ResourceLimits;
113use agentos_kernel::root_fs::RootFilesystemMode;
114use agentos_kernel::socket_table::{
115    reset_socket_read_trace, set_socket_read_trace_enabled, socket_read_trace_snapshot,
116    InetSocketAddress, SocketDomain, SocketId, SocketShutdown as KernelSocketShutdown, SocketSpec,
117    SocketState, SocketType,
118};
119use agentos_native_sidecar_core::{
120    apply_process_signal_state_update, bound_udp_snapshot_response, bridge_buffer_value,
121    decode_base64, decode_bridge_buffer_value, decode_encoded_bytes_value, encoded_bytes_value,
122    ensure_vm_fetch_raw_response_buffer_within_limit, ensure_vm_fetch_response_within_limit,
123    listener_snapshot_response, local_endpoint_value, parse_kernel_http_fetch_response,
124    parse_process_signal_state_request, process_killed_response,
125    process_snapshot_entry_from_kernel, process_snapshot_response, process_started_response,
126    remote_endpoint_value, shared_guest_runtime_identity, signal_state_response,
127    socket_addr_family, socket_address_value, stdin_closed_response, stdin_written_response,
128    tcp_socket_info_value, unix_socket_info_value, zombie_timer_count_response,
129    SharedProcessSnapshotEntry, SharedProcessSnapshotStatus, SidecarCoreError,
130    VM_FETCH_BUFFER_LIMIT_BYTES,
131};
132use rusqlite::types::ValueRef as SqliteValueRef;
133use rusqlite::{
134    Connection as SqliteConnection, OpenFlags as SqliteOpenFlags, Statement as SqliteStatement,
135};
136use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
137use rustls::crypto::aws_lc_rs;
138use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName};
139use rustls::{
140    ClientConfig, ClientConnection, DigitallySignedStruct, RootCertStore, ServerConfig,
141    ServerConnection, SignatureScheme,
142};
143use scrypt::{scrypt, Params as ScryptParams};
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 = "__agentos_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 = "AGENTOS_TEST_HTTP_LOOPBACK_REQUEST_TIMEOUT_MS";
268
269trait Http2AsyncIo: AsyncRead + AsyncWrite + Unpin + Send {}
270
271impl<T> Http2AsyncIo for T where T: AsyncRead + AsyncWrite + Unpin + Send {}
272
273fn http_loopback_request_timeout() -> Duration {
274    std::env::var(HTTP_LOOPBACK_REQUEST_TIMEOUT_MS_ENV)
275        .ok()
276        .and_then(|value| value.parse::<u64>().ok())
277        .map(Duration::from_millis)
278        .unwrap_or(HTTP_LOOPBACK_REQUEST_TIMEOUT)
279}
280
281/// Block until `fd` is readable or `deadline` passes. Returns whether it became readable.
282///
283/// BLOCKING: parks the calling OS thread in `poll(2)`. The unix/tcp accept and
284/// udp recv callers run on the sidecar's single-thread tokio runtime, so a
285/// non-zero wait stalls the whole event loop for up to `deadline` — the same
286/// stall as the fixed sleeps this replaced, and only acceptable because the
287/// guest net path always polls with wait == 0. Keep deadlines bounded and do
288/// not add wait > 0 callers on paths that service concurrent VM traffic.
289fn wait_fd_readable_until(fd: BorrowedFd<'_>, deadline: Instant) -> bool {
290    let remaining = deadline.saturating_duration_since(Instant::now());
291    if remaining.is_zero() {
292        return false;
293    }
294
295    let timeout_ms = remaining.as_millis().saturating_add(u128::from(
296        !remaining.subsec_nanos().is_multiple_of(1_000_000),
297    ));
298    let timeout =
299        PollTimeout::try_from(timeout_ms.min(i32::MAX as u128)).unwrap_or(PollTimeout::MAX);
300    let mut fds = [NixPollFd::new(fd, PollFlags::POLLIN)];
301    match poll(&mut fds, timeout) {
302        Ok(0) => false,
303        Ok(_) => fds[0]
304            .revents()
305            .unwrap_or_else(PollFlags::empty)
306            .intersects(PollFlags::POLLIN | PollFlags::POLLHUP | PollFlags::POLLERR),
307        Err(_) => true,
308    }
309}
310
311const DEFAULT_ALLOWED_NODE_BUILTINS: &[&str] = &[
312    "assert",
313    "buffer",
314    "console",
315    "child_process",
316    "crypto",
317    "dns",
318    "events",
319    "fs",
320    "http",
321    "http2",
322    "https",
323    "module",
324    "os",
325    "path",
326    "perf_hooks",
327    "querystring",
328    "sqlite",
329    "stream",
330    "string_decoder",
331    "timers",
332    "tls",
333    "tty",
334    "url",
335    "util",
336    "zlib",
337];
338const EXECUTION_REQUEST_TTY_ENV: &str = "AGENTOS_EXEC_TTY";
339
340#[derive(Debug, Clone, Copy, PartialEq, Eq)]
341enum JavascriptCryptoDigestAlgorithm {
342    Md5,
343    Sha1,
344    Sha224,
345    Sha256,
346    Sha384,
347    Sha512,
348}
349
350#[derive(Debug, Default, Deserialize)]
351#[serde(default, rename_all = "camelCase")]
352struct JavascriptScryptOptions {
353    #[serde(alias = "N")]
354    cost: Option<u64>,
355    #[serde(alias = "r")]
356    block_size: Option<u32>,
357    #[serde(alias = "p")]
358    parallelization: Option<u32>,
359}
360
361#[derive(Debug, Deserialize)]
362#[serde(rename_all = "camelCase")]
363struct JavascriptHttpListenRequest {
364    server_id: u64,
365    #[serde(default)]
366    port: Option<u16>,
367    #[serde(default)]
368    hostname: Option<String>,
369}
370
371#[derive(Debug, Default, Deserialize)]
372#[serde(default, rename_all = "camelCase")]
373struct JavascriptHttpRequestOptions {
374    method: Option<String>,
375    headers: BTreeMap<String, Value>,
376    body: Option<String>,
377    reject_unauthorized: Option<bool>,
378}
379
380#[derive(Debug, Default, Deserialize)]
381#[serde(default, rename_all = "camelCase")]
382struct JavascriptHttp2ServerListenRequest {
383    server_id: u64,
384    secure: bool,
385    port: Option<u16>,
386    host: Option<String>,
387    backlog: Option<u32>,
388    timeout: Option<u64>,
389    settings: BTreeMap<String, Value>,
390    tls: Option<JavascriptTlsBridgeOptions>,
391}
392
393#[derive(Debug, Default, Deserialize)]
394#[serde(default, rename_all = "camelCase")]
395struct JavascriptHttp2SessionConnectRequest {
396    authority: Option<String>,
397    protocol: Option<String>,
398    host: Option<String>,
399    port: Option<u16>,
400    settings: BTreeMap<String, Value>,
401    tls: Option<JavascriptTlsBridgeOptions>,
402}
403
404#[derive(Debug, Default, Deserialize)]
405#[serde(default, rename_all = "camelCase")]
406struct JavascriptHttp2RequestOptions {
407    end_stream: bool,
408}
409
410#[derive(Debug, Default, Deserialize)]
411#[serde(default, rename_all = "camelCase")]
412struct JavascriptHttp2FileResponseOptions {
413    offset: Option<u64>,
414    length: Option<i64>,
415}
416
417#[derive(Debug, Clone)]
418struct HttpHeaderCollection {
419    normalized: BTreeMap<String, Vec<String>>,
420    raw_pairs: Vec<(String, String)>,
421}
422
423#[derive(Debug)]
424struct InsecureTlsVerifier {
425    supported_schemes: Vec<SignatureScheme>,
426}
427
428impl ServerCertVerifier for InsecureTlsVerifier {
429    fn verify_server_cert(
430        &self,
431        _end_entity: &CertificateDer<'_>,
432        _intermediates: &[CertificateDer<'_>],
433        _server_name: &ServerName<'_>,
434        _ocsp_response: &[u8],
435        _now: rustls::pki_types::UnixTime,
436    ) -> Result<ServerCertVerified, rustls::Error> {
437        Ok(ServerCertVerified::assertion())
438    }
439
440    fn verify_tls12_signature(
441        &self,
442        _message: &[u8],
443        _cert: &CertificateDer<'_>,
444        _dss: &DigitallySignedStruct,
445    ) -> Result<HandshakeSignatureValid, rustls::Error> {
446        Ok(HandshakeSignatureValid::assertion())
447    }
448
449    fn verify_tls13_signature(
450        &self,
451        _message: &[u8],
452        _cert: &CertificateDer<'_>,
453        _dss: &DigitallySignedStruct,
454    ) -> Result<HandshakeSignatureValid, rustls::Error> {
455        Ok(HandshakeSignatureValid::assertion())
456    }
457
458    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
459        self.supported_schemes.clone()
460    }
461}
462
463impl ActiveProcess {
464    pub(crate) fn new(
465        kernel_pid: u32,
466        kernel_handle: KernelProcessHandle,
467        runtime: GuestRuntimeKind,
468        execution: ActiveExecution,
469    ) -> Self {
470        Self {
471            kernel_pid,
472            kernel_handle,
473            kernel_stdin_writer_fd: None,
474            tty_master_fd: None,
475            runtime,
476            detached: false,
477            execution,
478            guest_cwd: String::from("/"),
479            env: BTreeMap::new(),
480            host_cwd: PathBuf::from("/"),
481            host_write_dirty: false,
482            mapped_host_fds: BTreeMap::new(),
483            next_mapped_host_fd: MAPPED_HOST_FD_START,
484            pending_execution_events: VecDeque::new(),
485            pending_self_signal_exit: None,
486            child_processes: BTreeMap::new(),
487            next_child_process_id: 0,
488            http_servers: BTreeMap::new(),
489            pending_http_requests: BTreeMap::new(),
490            http2: Default::default(),
491            tcp_listeners: BTreeMap::new(),
492            next_tcp_listener_id: 0,
493            tcp_sockets: BTreeMap::new(),
494            next_tcp_socket_id: 0,
495            tcp_port_reservations: BTreeMap::new(),
496            next_tcp_port_reservation_id: 0,
497            unix_listeners: BTreeMap::new(),
498            next_unix_listener_id: 0,
499            unix_sockets: BTreeMap::new(),
500            next_unix_socket_id: 0,
501            udp_sockets: BTreeMap::new(),
502            next_udp_socket_id: 0,
503            python_sockets: BTreeMap::new(),
504            next_python_socket_id: 0,
505            cipher_sessions: BTreeMap::new(),
506            next_cipher_session_id: 0,
507            diffie_hellman_sessions: BTreeMap::new(),
508            next_diffie_hellman_session_id: 0,
509            sqlite_databases: BTreeMap::new(),
510            next_sqlite_database_id: 0,
511            sqlite_statements: BTreeMap::new(),
512            next_sqlite_statement_id: 0,
513            tty_master_owner: None,
514            deferred_kernel_wait_rpc: None,
515            module_resolution_cache: agentos_execution::LocalModuleResolutionCache::default(),
516        }
517    }
518
519    pub(crate) fn queue_pending_execution_event(
520        &mut self,
521        event: ActiveExecutionEvent,
522    ) -> Result<(), SidecarError> {
523        if self.pending_execution_events.len() >= MAX_PROCESS_EVENT_QUEUE {
524            return Err(process_event_queue_overflow_error());
525        }
526        self.pending_execution_events.push_back(event);
527        Ok(())
528    }
529
530    pub(crate) fn with_host_cwd(mut self, host_cwd: PathBuf) -> Self {
531        self.host_cwd = host_cwd;
532        self
533    }
534
535    pub(crate) fn mark_host_write_dirty(&mut self) {
536        self.host_write_dirty = true;
537    }
538
539    pub(crate) fn host_write_dirty_recursive(&self) -> bool {
540        self.host_write_dirty
541            || self
542                .child_processes
543                .values()
544                .any(ActiveProcess::host_write_dirty_recursive)
545    }
546
547    pub(crate) fn clean_host_writes_are_observable_recursive(&self) -> bool {
548        matches!(
549            self.execution,
550            ActiveExecution::Javascript(_) | ActiveExecution::Python(_) | ActiveExecution::Wasm(_)
551        ) && self
552            .child_processes
553            .values()
554            .all(ActiveProcess::clean_host_writes_are_observable_recursive)
555    }
556
557    pub(crate) fn with_guest_cwd(mut self, guest_cwd: String) -> Self {
558        self.guest_cwd = guest_cwd;
559        self
560    }
561
562    pub(crate) fn with_env(mut self, env: BTreeMap<String, String>) -> Self {
563        self.env = env;
564        self
565    }
566
567    pub(crate) fn with_kernel_stdin_writer_fd(mut self, fd: u32) -> Self {
568        self.kernel_stdin_writer_fd = Some(fd);
569        self
570    }
571
572    pub(crate) fn with_tty_master_fd(mut self, fd: Option<u32>) -> Self {
573        self.tty_master_fd = fd;
574        self
575    }
576
577    pub(crate) fn with_detached(mut self, detached: bool) -> Self {
578        self.detached = detached;
579        self
580    }
581
582    pub(crate) fn allocate_mapped_host_fd(&mut self, fd: ActiveMappedHostFd) -> u32 {
583        let handle = self.next_mapped_host_fd;
584        self.next_mapped_host_fd = self
585            .next_mapped_host_fd
586            .checked_add(1)
587            .unwrap_or(MAPPED_HOST_FD_START);
588        self.mapped_host_fds.insert(handle, fd);
589        handle
590    }
591
592    pub(crate) fn mapped_host_fd(&self, fd: u32) -> Option<&ActiveMappedHostFd> {
593        self.mapped_host_fds.get(&fd)
594    }
595
596    pub(crate) fn mapped_host_fd_mut(&mut self, fd: u32) -> Option<&mut ActiveMappedHostFd> {
597        self.mapped_host_fds.get_mut(&fd)
598    }
599
600    pub(crate) fn close_mapped_host_fd(&mut self, fd: u32) -> bool {
601        self.mapped_host_fds.remove(&fd).is_some()
602    }
603
604    pub(crate) fn allocate_child_process_id(&mut self) -> String {
605        self.next_child_process_id += 1;
606        format!("child-{}", self.next_child_process_id)
607    }
608
609    fn allocate_tcp_listener_id(&mut self) -> String {
610        self.next_tcp_listener_id += 1;
611        format!("listener-{}", self.next_tcp_listener_id)
612    }
613
614    fn allocate_tcp_socket_id(&mut self) -> String {
615        self.next_tcp_socket_id += 1;
616        format!("socket-{}", self.next_tcp_socket_id)
617    }
618
619    fn allocate_tcp_port_reservation_id(&mut self) -> String {
620        self.next_tcp_port_reservation_id += 1;
621        format!("tcp-port-reservation-{}", self.next_tcp_port_reservation_id)
622    }
623
624    fn allocate_unix_listener_id(&mut self) -> String {
625        self.next_unix_listener_id += 1;
626        format!("unix-listener-{}", self.next_unix_listener_id)
627    }
628
629    fn allocate_unix_socket_id(&mut self) -> String {
630        self.next_unix_socket_id += 1;
631        format!("unix-socket-{}", self.next_unix_socket_id)
632    }
633
634    fn allocate_udp_socket_id(&mut self) -> String {
635        self.next_udp_socket_id += 1;
636        format!("udp-socket-{}", self.next_udp_socket_id)
637    }
638
639    pub(crate) fn network_resource_counts(&self) -> NetworkResourceCounts {
640        let mut counts = NetworkResourceCounts {
641            sockets: self.http_servers.len()
642                + self.tcp_listeners.len()
643                + self.tcp_sockets.len()
644                + self.unix_listeners.len()
645                + self.unix_sockets.len()
646                + self.udp_sockets.len()
647                + self.python_sockets.len(),
648            connections: self.tcp_sockets.len() + self.unix_sockets.len(),
649        };
650        if let Ok(http2) = self.http2.shared.lock() {
651            counts.sockets += http2.servers.len() + http2.sessions.len();
652            counts.connections += http2.sessions.len();
653        }
654
655        for child in self.child_processes.values() {
656            let child_counts = child.network_resource_counts();
657            counts.sockets += child_counts.sockets;
658            counts.connections += child_counts.connections;
659        }
660
661        counts
662    }
663
664    fn sidecar_only_network_resource_counts(&self) -> NetworkResourceCounts {
665        let mut counts = NetworkResourceCounts {
666            sockets: self.http_servers.len()
667                + self
668                    .tcp_listeners
669                    .values()
670                    .filter(|listener| listener.kernel_socket_id.is_none())
671                    .count()
672                + self
673                    .tcp_sockets
674                    .values()
675                    .filter(|socket| socket.kernel_socket_id.is_none())
676                    .count()
677                + self.unix_listeners.len()
678                + self.unix_sockets.len()
679                + self
680                    .udp_sockets
681                    .values()
682                    .filter(|socket| socket.kernel_socket_id.is_none())
683                    .count()
684                + self.python_sockets.len(),
685            connections: self
686                .tcp_sockets
687                .values()
688                .filter(|socket| socket.kernel_socket_id.is_none())
689                .count()
690                + self.unix_sockets.len(),
691        };
692        if let Ok(http2) = self.http2.shared.lock() {
693            counts.sockets += http2.servers.len() + http2.sessions.len();
694            counts.connections += http2.sessions.len();
695        }
696
697        for child in self.child_processes.values() {
698            let child_counts = child.sidecar_only_network_resource_counts();
699            counts.sockets += child_counts.sockets;
700            counts.connections += child_counts.connections;
701        }
702
703        counts
704    }
705}
706
707fn poll_tool_process_event(
708    execution: &ToolExecution,
709) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
710    let event = execution
711        .pending_events
712        .lock()
713        .unwrap_or_else(|poisoned| poisoned.into_inner())
714        .pop_front();
715    if event.is_some() {
716        return Ok(event);
717    }
718    if execution.events_overflowed.load(Ordering::Relaxed) {
719        return Err(process_event_queue_overflow_error());
720    }
721    Ok(None)
722}
723
724fn descendant_pending_execution_event_capacity(
725    root: &ActiveProcess,
726    child_path: &[&str],
727) -> Option<usize> {
728    let mut child = root;
729    for child_process_id in child_path {
730        child = child.child_processes.get(*child_process_id)?;
731    }
732    Some(MAX_PROCESS_EVENT_QUEUE.saturating_sub(child.pending_execution_events.len()))
733}
734
735fn poll_child_execution_after_exit(
736    child: &mut ActiveProcess,
737    wait: Duration,
738) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
739    match child.execution.poll_event_blocking(wait) {
740        Ok(event) => Ok(event),
741        Err(SidecarError::Execution(message))
742            if child.runtime == GuestRuntimeKind::WebAssembly
743                && message == WasmExecutionError::EventChannelClosed.to_string() =>
744        {
745            Ok(None)
746        }
747        Err(error) => Err(error),
748    }
749}
750
751fn closed_javascript_event_channel(message: &str) -> bool {
752    message == "guest JavaScript event channel closed unexpectedly"
753}
754
755fn closed_python_event_channel(message: &str) -> bool {
756    message == "guest Python event channel closed unexpectedly"
757}
758
759fn closed_wasm_event_channel(message: &str) -> bool {
760    message == WasmExecutionError::EventChannelClosed.to_string()
761}
762
763fn missing_vm_error(vm_id: &str) -> SidecarError {
764    SidecarError::InvalidState(format!("VM {vm_id} is no longer active"))
765}
766
767fn missing_process_error(vm_id: &str, process_id: &str) -> SidecarError {
768    SidecarError::InvalidState(format!(
769        "VM {vm_id} no longer has active process {process_id}"
770    ))
771}
772
773/// Map a shared guest-kernel-call dispatcher error into a sidecar error,
774/// preserving POSIX errno codes (`ECODE: message`) as kernel errors so guest
775/// callers observe Linux-faithful failures, mirroring the filesystem path.
776fn guest_kernel_core_error(error: agentos_native_sidecar_core::SidecarCoreError) -> SidecarError {
777    let message = error.to_string();
778    let is_errno = message.split_once(':').is_some_and(|(code, _)| {
779        code.len() >= 2
780            && code.starts_with('E')
781            && code[1..]
782                .bytes()
783                .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_')
784    });
785    if is_errno {
786        SidecarError::Kernel(message)
787    } else {
788        SidecarError::InvalidState(message)
789    }
790}
791
792fn is_broken_pipe_error(error: &SidecarError) -> bool {
793    matches!(error, SidecarError::Execution(message) if message.contains("Broken pipe") || message.contains("os error 32") || message.contains("EPIPE"))
794}
795
796fn javascript_child_process_gone_error(process_id: &str, child_path: &[&str]) -> SidecarError {
797    let child_label = if child_path.is_empty() {
798        process_id.to_owned()
799    } else {
800        format!("{process_id}/{}", child_path.join("/"))
801    };
802    SidecarError::Execution(format!(
803        "ECHILD: child_process {child_label} is no longer available"
804    ))
805}
806
807fn is_javascript_child_process_gone_error(error: &SidecarError) -> bool {
808    matches!(
809        error,
810        SidecarError::Execution(message) if guest_errno_code(message) == Some("ECHILD")
811    )
812}
813
814fn loopback_tls_transport_registry(
815) -> &'static Mutex<BTreeMap<String, Weak<crate::state::LoopbackTlsTransportPair>>> {
816    static REGISTRY: OnceLock<
817        Mutex<BTreeMap<String, Weak<crate::state::LoopbackTlsTransportPair>>>,
818    > = OnceLock::new();
819    REGISTRY.get_or_init(|| Mutex::new(BTreeMap::new()))
820}
821
822#[cfg(test)]
823#[allow(dead_code)]
824pub(crate) fn loopback_tls_registry_len() -> usize {
825    loopback_tls_transport_registry()
826        .lock()
827        .expect("loopback TLS transport registry lock poisoned")
828        .len()
829}
830
831#[cfg(test)]
832pub(crate) fn loopback_tls_registry_contains(key: &str) -> bool {
833    loopback_tls_transport_registry()
834        .lock()
835        .expect("loopback TLS transport registry lock poisoned")
836        .contains_key(key)
837}
838
839fn loopback_tls_transport_key(
840    vm_id: &str,
841    socket_id: SocketId,
842    peer_socket_id: SocketId,
843) -> String {
844    let (lower, higher) = if socket_id <= peer_socket_id {
845        (socket_id, peer_socket_id)
846    } else {
847        (peer_socket_id, socket_id)
848    };
849    format!("{vm_id}:{lower}:{higher}")
850}
851
852fn loopback_tls_endpoint(
853    vm_id: &str,
854    socket_id: SocketId,
855    peer_socket_id: SocketId,
856) -> Result<crate::state::LoopbackTlsEndpoint, SidecarError> {
857    let key = loopback_tls_transport_key(vm_id, socket_id, peer_socket_id);
858    let registry = loopback_tls_transport_registry();
859    let mut transports = registry.lock().map_err(|_| {
860        SidecarError::InvalidState(String::from(
861            "loopback TLS transport registry lock poisoned",
862        ))
863    })?;
864    transports.retain(|_, pair| pair.strong_count() > 0);
865    let pair = transports
866        .get(&key)
867        .and_then(Weak::upgrade)
868        .unwrap_or_else(|| {
869            let pair = Arc::new(crate::state::LoopbackTlsTransportPair {
870                state: Mutex::new(crate::state::LoopbackTlsTransportPairState::default()),
871                ready: std::sync::Condvar::new(),
872            });
873            transports.insert(key.clone(), Arc::downgrade(&pair));
874            pair
875        });
876    Ok(crate::state::LoopbackTlsEndpoint {
877        pair,
878        is_lower_socket: socket_id <= peer_socket_id,
879        poll_timeout: TCP_SOCKET_POLL_TIMEOUT,
880        registry_key: Some(key),
881    })
882}
883
884impl crate::state::LoopbackTlsEndpoint {
885    fn set_poll_timeout(&mut self, timeout: Duration) {
886        self.poll_timeout = timeout;
887    }
888
889    fn interrupt_reader(pair: &Arc<crate::state::LoopbackTlsTransportPair>, is_lower_socket: bool) {
890        if let Ok(mut state) = pair.state.lock() {
891            if is_lower_socket {
892                state.lower_read_interrupt = true;
893            } else {
894                state.higher_read_interrupt = true;
895            }
896            pair.ready.notify_all();
897        }
898    }
899
900    fn shutdown_write(&self) -> Result<(), SidecarError> {
901        let mut state = self.pair.state.lock().map_err(|_| {
902            SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
903        })?;
904        if self.is_lower_socket {
905            state.lower_write_closed = true;
906        } else {
907            state.higher_write_closed = true;
908        }
909        self.pair.ready.notify_all();
910        Ok(())
911    }
912
913    fn close_endpoint(&self) -> Result<(), SidecarError> {
914        let mut state = self.pair.state.lock().map_err(|_| {
915            SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
916        })?;
917        if self.is_lower_socket {
918            state.lower_write_closed = true;
919            state.lower_closed = true;
920        } else {
921            state.higher_write_closed = true;
922            state.higher_closed = true;
923        }
924        self.pair.ready.notify_all();
925        Ok(())
926    }
927}
928
929impl LoopbackTlsPendingWriteHandle {
930    pub(crate) fn new(endpoint: &crate::state::LoopbackTlsEndpoint) -> Self {
931        Self {
932            state: Arc::new(Mutex::new(LoopbackTlsPendingWriteState {
933                buffer: Vec::new(),
934                warned_near_cap: false,
935                flushing: false,
936                defer_shutdown_write: false,
937                failure_message: None,
938            })),
939            tls_handshake_complete: Arc::new(AtomicBool::new(false)),
940            failed: Arc::new(AtomicBool::new(false)),
941            pair: Arc::clone(&endpoint.pair),
942            is_lower_socket: endpoint.is_lower_socket,
943            handshake_started_at: Instant::now(),
944        }
945    }
946
947    fn failure_error(&self) -> SidecarError {
948        let message = self
949            .state
950            .lock()
951            .ok()
952            .and_then(|state| state.failure_message.clone())
953            .unwrap_or_else(|| String::from("loopback TLS pending write failed"));
954        SidecarError::Execution(message)
955    }
956
957    fn mark_failed(&self, message: impl Into<String>) {
958        self.failed.store(true, Ordering::SeqCst);
959        if let Ok(mut state) = self.state.lock() {
960            state.buffer.clear();
961            state.flushing = false;
962            state.defer_shutdown_write = false;
963            state.failure_message = Some(message.into());
964        }
965    }
966
967    fn clear_for_close(&self) {
968        self.mark_failed("loopback TLS socket closed before buffered writes flushed");
969    }
970
971    fn fail_if_pending(&self, message: impl Into<String>) {
972        let has_pending = self
973            .state
974            .lock()
975            .map(|state| !state.buffer.is_empty() || state.flushing || state.defer_shutdown_write)
976            .unwrap_or(true);
977        if has_pending {
978            self.mark_failed(message);
979        }
980    }
981
982    fn should_buffer_write(&self) -> Result<bool, SidecarError> {
983        if self.failed.load(Ordering::SeqCst) {
984            return Err(self.failure_error());
985        }
986        let state = self.state.lock().map_err(|_| {
987            SidecarError::InvalidState(String::from("loopback TLS pending write lock poisoned"))
988        })?;
989        Ok(!self.tls_handshake_complete.load(Ordering::SeqCst)
990            || !state.buffer.is_empty()
991            || state.flushing)
992    }
993
994    pub(crate) fn append_write(&self, contents: &[u8]) -> Result<(), SidecarError> {
995        if self.failed.load(Ordering::SeqCst) {
996            return Err(self.failure_error());
997        }
998        let mut state = self.state.lock().map_err(|_| {
999            SidecarError::InvalidState(String::from("loopback TLS pending write lock poisoned"))
1000        })?;
1001        let observed = state.buffer.len().saturating_add(contents.len());
1002        if observed > LOOPBACK_TLS_PENDING_WRITE_BUFFER_BYTES {
1003            return Err(SidecarError::Execution(format!(
1004                "loopback TLS pending write buffer exceeded: {observed} bytes > {cap} bytes (limit: loopback TLS pending write buffer)",
1005                cap = LOOPBACK_TLS_PENDING_WRITE_BUFFER_BYTES
1006            )));
1007        }
1008        if !state.warned_near_cap && observed >= LOOPBACK_TLS_PENDING_WRITE_WARNING_BYTES {
1009            state.warned_near_cap = true;
1010            tracing::warn!(
1011                limit = "loopback_tls_pending_write_buffer",
1012                observed_bytes = observed,
1013                cap_bytes = LOOPBACK_TLS_PENDING_WRITE_BUFFER_BYTES,
1014                fill_percent =
1015                    (observed as f64 / LOOPBACK_TLS_PENDING_WRITE_BUFFER_BYTES as f64) * 100.0,
1016                wired = "invariant.loopbackTlsPendingWriteBufferBytes",
1017                "loopback TLS pending write buffer is near capacity"
1018            );
1019        }
1020        state.buffer.extend_from_slice(contents);
1021        Ok(())
1022    }
1023
1024    fn defer_shutdown_write(&self) -> Result<(), SidecarError> {
1025        if self.failed.load(Ordering::SeqCst) {
1026            return Err(self.failure_error());
1027        }
1028        let mut state = self.state.lock().map_err(|_| {
1029            SidecarError::InvalidState(String::from("loopback TLS pending write lock poisoned"))
1030        })?;
1031        state.defer_shutdown_write = true;
1032        Ok(())
1033    }
1034
1035    fn take_buffer_for_flush(&self) -> Result<Option<Vec<u8>>, SidecarError> {
1036        let mut state = self.state.lock().map_err(|_| {
1037            SidecarError::InvalidState(String::from("loopback TLS pending write lock poisoned"))
1038        })?;
1039        if state.buffer.is_empty() {
1040            return Ok(None);
1041        }
1042        state.flushing = true;
1043        Ok(Some(std::mem::take(&mut state.buffer)))
1044    }
1045
1046    fn finish_flush(&self) {
1047        if let Ok(mut state) = self.state.lock() {
1048            state.flushing = false;
1049        }
1050    }
1051
1052    fn take_deferred_shutdown_write(&self) -> Result<bool, SidecarError> {
1053        let mut state = self.state.lock().map_err(|_| {
1054            SidecarError::InvalidState(String::from("loopback TLS pending write lock poisoned"))
1055        })?;
1056        if state.buffer.is_empty() && !state.flushing && state.defer_shutdown_write {
1057            state.defer_shutdown_write = false;
1058            return Ok(true);
1059        }
1060        Ok(false)
1061    }
1062
1063    fn interrupt_own_reader(&self) {
1064        crate::state::LoopbackTlsEndpoint::interrupt_reader(&self.pair, self.is_lower_socket);
1065    }
1066}
1067
1068fn parse_tls_client_hello_from_bytes(
1069    buffer: &[u8],
1070) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
1071    if buffer.is_empty() {
1072        return Ok(None);
1073    }
1074
1075    let mut acceptor = rustls::server::Acceptor::default();
1076    let mut cursor = Cursor::new(buffer);
1077    acceptor.read_tls(&mut cursor).map_err(sidecar_net_error)?;
1078    let Some(accepted) = acceptor.accept().map_err(|(error, _)| {
1079        SidecarError::Execution(format!("failed to parse TLS client hello: {error}"))
1080    })?
1081    else {
1082        return Ok(None);
1083    };
1084    let client_hello = accepted.client_hello();
1085    let alpn_protocols = client_hello.alpn().map(|protocols| {
1086        protocols
1087            .filter_map(|protocol| String::from_utf8(protocol.to_vec()).ok())
1088            .collect::<Vec<_>>()
1089    });
1090    Ok(Some(JavascriptTlsClientHello {
1091        servername: client_hello.server_name().map(str::to_owned),
1092        alpn_protocols,
1093    }))
1094}
1095
1096fn peek_loopback_tls_client_hello(
1097    vm_id: &str,
1098    socket_id: SocketId,
1099    peer_socket_id: SocketId,
1100) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
1101    let key = loopback_tls_transport_key(vm_id, socket_id, peer_socket_id);
1102    let registry = loopback_tls_transport_registry();
1103    let pair = registry
1104        .lock()
1105        .map_err(|_| {
1106            SidecarError::InvalidState(String::from(
1107                "loopback TLS transport registry lock poisoned",
1108            ))
1109        })?
1110        .get(&key)
1111        .and_then(Weak::upgrade);
1112    let Some(pair) = pair else {
1113        return Ok(None);
1114    };
1115    let is_lower_socket = socket_id <= peer_socket_id;
1116    let state = pair.state.lock().map_err(|_| {
1117        SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
1118    })?;
1119    let buffered = if is_lower_socket {
1120        state.higher_to_lower.iter().copied().collect::<Vec<_>>()
1121    } else {
1122        state.lower_to_higher.iter().copied().collect::<Vec<_>>()
1123    };
1124    drop(state);
1125    parse_tls_client_hello_from_bytes(&buffered)
1126}
1127
1128fn wait_for_loopback_peer_socket_id(
1129    kernel: &SidecarKernel,
1130    socket_id: SocketId,
1131) -> Option<SocketId> {
1132    // Keep the total wait budget aligned with the previous ~500ms poll window.
1133    let deadline = Instant::now() + Duration::from_millis(500);
1134    let mut backoff = Duration::from_micros(100);
1135    loop {
1136        if let Some(peer_socket_id) = kernel
1137            .socket_get(socket_id)
1138            .and_then(|record| record.peer_socket_id())
1139        {
1140            return Some(peer_socket_id);
1141        }
1142        let now = Instant::now();
1143        if now >= deadline {
1144            break;
1145        }
1146        std::thread::sleep(backoff.min(deadline.saturating_duration_since(now)));
1147        backoff = (backoff * 2).min(Duration::from_millis(1));
1148    }
1149    None
1150}
1151
1152impl Drop for crate::state::LoopbackTlsEndpoint {
1153    fn drop(&mut self) {
1154        let _ = self.close_endpoint();
1155
1156        // Eagerly prune this endpoint's registry entry once we are the last owner
1157        // of the shared transport pair. Without this, the `Weak` entry survives
1158        // until the next `loopback_tls_endpoint()` call runs its lazy `retain()`,
1159        // so dead entries accumulate under intermittent use. We must NOT remove
1160        // the entry while a peer endpoint still shares the pair, otherwise a later
1161        // connection for the same socket pair would fail to find it and build a
1162        // mismatched fresh pair.
1163        let Some(key) = self.registry_key.take() else {
1164            return;
1165        };
1166        let Ok(mut transports) = loopback_tls_transport_registry().lock() else {
1167            // Lock poisoned: leave the entry for the lazy `retain()` to reclaim.
1168            return;
1169        };
1170        let should_remove = match transports.get(&key) {
1171            // Only prune when the registered entry still points at *our* pair and
1172            // `self` is its last strong owner. During `Drop` `self.pair` is still
1173            // alive, so a strong count of 1 (after dropping the temporary upgrade)
1174            // means no other endpoint references it.
1175            Some(weak) => match weak.upgrade() {
1176                Some(existing) => {
1177                    let same_pair = Arc::ptr_eq(&existing, &self.pair);
1178                    drop(existing);
1179                    same_pair && Arc::strong_count(&self.pair) <= 1
1180                }
1181                None => true,
1182            },
1183            None => false,
1184        };
1185        if should_remove {
1186            transports.remove(&key);
1187        }
1188    }
1189}
1190
1191impl Read for crate::state::LoopbackTlsEndpoint {
1192    fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
1193        let mut state = self
1194            .pair
1195            .state
1196            .lock()
1197            .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
1198
1199        loop {
1200            let (peer_write_closed, peer_closed) = if self.is_lower_socket {
1201                (state.higher_write_closed, state.higher_closed)
1202            } else {
1203                (state.lower_write_closed, state.lower_closed)
1204            };
1205
1206            let incoming = if self.is_lower_socket {
1207                &mut state.higher_to_lower
1208            } else {
1209                &mut state.lower_to_higher
1210            };
1211
1212            if !incoming.is_empty() {
1213                let count = incoming.len().min(buffer.len());
1214                let (head, tail) = incoming.as_slices();
1215                let head_count = head.len().min(count);
1216                buffer[..head_count].copy_from_slice(&head[..head_count]);
1217                if head_count < count {
1218                    buffer[head_count..count].copy_from_slice(&tail[..count - head_count]);
1219                }
1220                incoming.drain(..count);
1221                return Ok(count);
1222            }
1223
1224            if peer_write_closed || peer_closed {
1225                return Ok(0);
1226            }
1227
1228            let read_interrupted = if self.is_lower_socket {
1229                std::mem::take(&mut state.lower_read_interrupt)
1230            } else {
1231                std::mem::take(&mut state.higher_read_interrupt)
1232            };
1233            if read_interrupted {
1234                return Err(std::io::Error::new(
1235                    std::io::ErrorKind::WouldBlock,
1236                    "loopback TLS transport read interrupted",
1237                ));
1238            }
1239
1240            let (next_state, wait_result) = self
1241                .pair
1242                .ready
1243                .wait_timeout(state, self.poll_timeout)
1244                .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
1245            state = next_state;
1246            if wait_result.timed_out() {
1247                return Err(std::io::Error::new(
1248                    std::io::ErrorKind::WouldBlock,
1249                    "loopback TLS transport read timed out",
1250                ));
1251            }
1252        }
1253    }
1254}
1255
1256impl Write for crate::state::LoopbackTlsEndpoint {
1257    fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
1258        let mut state = self
1259            .pair
1260            .state
1261            .lock()
1262            .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
1263
1264        let peer_closed = if self.is_lower_socket {
1265            state.higher_closed
1266        } else {
1267            state.lower_closed
1268        };
1269        let outgoing = if self.is_lower_socket {
1270            &mut state.lower_to_higher
1271        } else {
1272            &mut state.higher_to_lower
1273        };
1274        if peer_closed {
1275            return Err(std::io::Error::new(
1276                std::io::ErrorKind::BrokenPipe,
1277                "loopback TLS peer is closed",
1278            ));
1279        }
1280
1281        outgoing.extend(buffer.iter().copied());
1282        self.pair.ready.notify_all();
1283        Ok(buffer.len())
1284    }
1285
1286    fn flush(&mut self) -> std::io::Result<()> {
1287        Ok(())
1288    }
1289}
1290
1291#[cfg(test)]
1292mod loopback_tls_registry_tests {
1293    use super::{
1294        loopback_tls_endpoint, loopback_tls_registry_contains, loopback_tls_transport_key,
1295    };
1296
1297    // Each test uses a unique vm_id so the process-global registry stays
1298    // partitioned across concurrently running tests.
1299
1300    #[test]
1301    fn dropping_endpoint_removes_its_registry_entry() {
1302        let vm_id = "loopback-tls-drop-removes-entry";
1303        let key = loopback_tls_transport_key(vm_id, 1, 2);
1304
1305        let endpoint = loopback_tls_endpoint(vm_id, 1, 2).expect("create endpoint");
1306        assert!(
1307            loopback_tls_registry_contains(&key),
1308            "registry should contain the key while the endpoint is alive"
1309        );
1310
1311        drop(endpoint);
1312        assert!(
1313            !loopback_tls_registry_contains(&key),
1314            "registry entry must be pruned in the endpoint's Drop, not left for the lazy retain()"
1315        );
1316    }
1317
1318    #[test]
1319    fn registry_entry_survives_until_last_peer_endpoint_drops() {
1320        let vm_id = "loopback-tls-shared-pair";
1321        let key = loopback_tls_transport_key(vm_id, 3, 4);
1322
1323        // Both peers of a loopback connection share the same transport pair.
1324        let lower = loopback_tls_endpoint(vm_id, 3, 4).expect("create lower endpoint");
1325        let higher = loopback_tls_endpoint(vm_id, 4, 3).expect("create higher endpoint");
1326        assert!(loopback_tls_registry_contains(&key));
1327
1328        // Dropping one peer must keep the entry, since the other peer still owns
1329        // the shared pair and a later connection must be able to find it.
1330        drop(lower);
1331        assert!(
1332            loopback_tls_registry_contains(&key),
1333            "entry must survive while a peer endpoint still shares the pair"
1334        );
1335
1336        drop(higher);
1337        assert!(
1338            !loopback_tls_registry_contains(&key),
1339            "entry must be pruned once the last peer endpoint drops"
1340        );
1341    }
1342}
1343
1344// TCP types moved to crate::state
1345
1346struct ActiveTcpConnectRequest<'a, B> {
1347    bridge: &'a SharedBridge<B>,
1348    kernel: &'a mut SidecarKernel,
1349    kernel_pid: u32,
1350    vm_id: &'a str,
1351    dns: &'a VmDnsConfig,
1352    host: &'a str,
1353    port: u16,
1354    local_address: Option<&'a str>,
1355    local_port: Option<u16>,
1356    local_reservation: Option<(JavascriptSocketFamily, u16)>,
1357    context: &'a JavascriptSocketPathContext,
1358}
1359
1360struct ActiveUdpSendToRequest<'a, B> {
1361    bridge: &'a SharedBridge<B>,
1362    kernel: &'a mut SidecarKernel,
1363    kernel_pid: u32,
1364    vm_id: &'a str,
1365    dns: &'a VmDnsConfig,
1366    host: &'a str,
1367    port: u16,
1368    context: &'a JavascriptSocketPathContext,
1369    contents: &'a [u8],
1370}
1371
1372struct UdpRemoteAddrRequest<'a, B> {
1373    bridge: &'a SharedBridge<B>,
1374    kernel: &'a SidecarKernel,
1375    vm_id: &'a str,
1376    dns: &'a VmDnsConfig,
1377    host: &'a str,
1378    port: u16,
1379    family: JavascriptUdpFamily,
1380    context: &'a JavascriptSocketPathContext,
1381}
1382
1383pub(crate) struct JavascriptSyncRpcServiceRequest<'a, B> {
1384    pub(crate) bridge: &'a SharedBridge<B>,
1385    pub(crate) vm_id: &'a str,
1386    pub(crate) dns: &'a VmDnsConfig,
1387    pub(crate) socket_paths: &'a JavascriptSocketPathContext,
1388    pub(crate) kernel: &'a mut SidecarKernel,
1389    pub(crate) kernel_readiness: KernelSocketReadinessRegistry,
1390    pub(crate) process: &'a mut ActiveProcess,
1391    pub(crate) sync_request: &'a JavascriptSyncRpcRequest,
1392    pub(crate) resource_limits: &'a ResourceLimits,
1393    pub(crate) network_counts: NetworkResourceCounts,
1394}
1395
1396pub(crate) enum JavascriptSyncRpcServiceResponse {
1397    Json(Value),
1398    Raw(Vec<u8>),
1399}
1400
1401impl From<Value> for JavascriptSyncRpcServiceResponse {
1402    fn from(value: Value) -> Self {
1403        Self::Json(value)
1404    }
1405}
1406
1407impl JavascriptSyncRpcServiceResponse {
1408    fn as_json(&self) -> Option<&Value> {
1409        match self {
1410            Self::Json(value) => Some(value),
1411            Self::Raw(_) => None,
1412        }
1413    }
1414}
1415
1416pub(crate) struct JavascriptNetSyncRpcServiceRequest<'a, B> {
1417    pub(crate) bridge: &'a SharedBridge<B>,
1418    pub(crate) vm_id: &'a str,
1419    pub(crate) dns: &'a VmDnsConfig,
1420    pub(crate) socket_paths: &'a JavascriptSocketPathContext,
1421    pub(crate) kernel: &'a mut SidecarKernel,
1422    pub(crate) kernel_readiness: KernelSocketReadinessRegistry,
1423    pub(crate) process: &'a mut ActiveProcess,
1424    pub(crate) sync_request: &'a JavascriptSyncRpcRequest,
1425    pub(crate) resource_limits: &'a ResourceLimits,
1426    pub(crate) network_counts: NetworkResourceCounts,
1427}
1428
1429struct LoopbackHttpResponseWaitRequest<'a, B> {
1430    bridge: &'a SharedBridge<B>,
1431    vm_id: &'a str,
1432    dns: &'a VmDnsConfig,
1433    socket_paths: &'a JavascriptSocketPathContext,
1434    kernel: &'a mut SidecarKernel,
1435    kernel_readiness: KernelSocketReadinessRegistry,
1436    process: &'a mut ActiveProcess,
1437    resource_limits: &'a ResourceLimits,
1438    request_key: (u64, u64),
1439}
1440
1441pub(crate) struct LoopbackHttpDispatchRequest<'a, B> {
1442    pub(crate) bridge: &'a SharedBridge<B>,
1443    pub(crate) vm_id: &'a str,
1444    pub(crate) dns: &'a VmDnsConfig,
1445    pub(crate) socket_paths: &'a JavascriptSocketPathContext,
1446    pub(crate) kernel: &'a mut SidecarKernel,
1447    pub(crate) kernel_readiness: KernelSocketReadinessRegistry,
1448    pub(crate) process: &'a mut ActiveProcess,
1449    pub(crate) resource_limits: &'a ResourceLimits,
1450    pub(crate) server_id: u64,
1451    pub(crate) request_json: &'a str,
1452}
1453
1454struct JavascriptDgramSyncRpcServiceRequest<'a, B> {
1455    bridge: &'a SharedBridge<B>,
1456    kernel: &'a mut SidecarKernel,
1457    vm_id: &'a str,
1458    dns: &'a VmDnsConfig,
1459    socket_paths: &'a JavascriptSocketPathContext,
1460    process: &'a mut ActiveProcess,
1461    kernel_readiness: KernelSocketReadinessRegistry,
1462    sync_request: &'a JavascriptSyncRpcRequest,
1463    resource_limits: &'a ResourceLimits,
1464    network_counts: NetworkResourceCounts,
1465}
1466
1467struct JavascriptHttp2SyncRpcServiceRequest<'a, B> {
1468    bridge: &'a SharedBridge<B>,
1469    kernel: &'a mut SidecarKernel,
1470    vm_id: &'a str,
1471    dns: &'a VmDnsConfig,
1472    socket_paths: &'a JavascriptSocketPathContext,
1473    process: &'a mut ActiveProcess,
1474    sync_request: &'a JavascriptSyncRpcRequest,
1475    resource_limits: &'a ResourceLimits,
1476    network_counts: NetworkResourceCounts,
1477}
1478
1479impl ActiveTcpSocket {
1480    fn connect<B>(request: ActiveTcpConnectRequest<'_, B>) -> Result<Self, SidecarError>
1481    where
1482        B: NativeSidecarBridge + Send + 'static,
1483        BridgeError<B>: fmt::Debug + Send + Sync + 'static,
1484    {
1485        let ActiveTcpConnectRequest {
1486            bridge,
1487            kernel,
1488            kernel_pid,
1489            vm_id,
1490            dns,
1491            host,
1492            port,
1493            local_address,
1494            local_port,
1495            local_reservation,
1496            context,
1497        } = request;
1498        let resolved = resolve_tcp_connect_addr(bridge, kernel, vm_id, dns, host, port, context)?;
1499        if resolved.use_kernel_loopback {
1500            let family = JavascriptSocketFamily::from_ip(resolved.guest_remote_addr.ip());
1501            let requested_local_port = local_port.unwrap_or(0);
1502            let local_port = if requested_local_port != 0
1503                && local_reservation == Some((family, requested_local_port))
1504            {
1505                requested_local_port
1506            } else {
1507                allocate_guest_listen_port(
1508                    requested_local_port,
1509                    family,
1510                    &context.used_tcp_guest_ports,
1511                    context.listen_policy,
1512                )?
1513            };
1514            let local_ip = match (family, local_address) {
1515                (JavascriptSocketFamily::Ipv4, Some("0.0.0.0")) => {
1516                    IpAddr::V4(Ipv4Addr::UNSPECIFIED)
1517                }
1518                (JavascriptSocketFamily::Ipv4, Some("127.0.0.1") | Some("localhost") | None) => {
1519                    IpAddr::V4(Ipv4Addr::LOCALHOST)
1520                }
1521                (JavascriptSocketFamily::Ipv6, Some("::")) => IpAddr::V6(Ipv6Addr::UNSPECIFIED),
1522                (JavascriptSocketFamily::Ipv6, Some("::1") | Some("localhost") | None) => {
1523                    IpAddr::V6(Ipv6Addr::LOCALHOST)
1524                }
1525                (JavascriptSocketFamily::Ipv4, Some(other)) => {
1526                    return Err(SidecarError::Execution(format!(
1527                        "EACCES: TCP sockets must bind to loopback or unspecified addresses, got {other}"
1528                    )));
1529                }
1530                (JavascriptSocketFamily::Ipv6, Some(other)) => {
1531                    return Err(SidecarError::Execution(format!(
1532                        "EACCES: TCP sockets must bind to loopback or unspecified addresses, got {other}"
1533                    )));
1534                }
1535            };
1536            let local_addr = SocketAddr::new(local_ip, local_port);
1537            let spec = match family {
1538                JavascriptSocketFamily::Ipv4 => SocketSpec::tcp(),
1539                JavascriptSocketFamily::Ipv6 => {
1540                    SocketSpec::new(SocketDomain::Inet6, SocketType::Stream)
1541                }
1542            };
1543            let socket_id = kernel
1544                .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
1545                .map_err(kernel_error)?;
1546            kernel
1547                .socket_bind_inet(
1548                    EXECUTION_DRIVER_NAME,
1549                    kernel_pid,
1550                    socket_id,
1551                    InetSocketAddress::new(local_ip.to_string(), local_port),
1552                )
1553                .map_err(kernel_error)?;
1554            kernel
1555                .socket_connect_inet_loopback(
1556                    EXECUTION_DRIVER_NAME,
1557                    kernel_pid,
1558                    socket_id,
1559                    InetSocketAddress::new(
1560                        resolved.guest_remote_addr.ip().to_string(),
1561                        resolved.guest_remote_addr.port(),
1562                    ),
1563                )
1564                .map_err(kernel_error)?;
1565            return Ok(Self::from_kernel(
1566                socket_id,
1567                None,
1568                local_addr,
1569                resolved.guest_remote_addr,
1570            ));
1571        }
1572
1573        let stream = TcpStream::connect_timeout(&resolved.actual_addr, Duration::from_secs(30))
1574            .map_err(sidecar_net_error)?;
1575        let guest_local_addr = stream.local_addr().map_err(sidecar_net_error)?;
1576        Self::from_stream(stream, None, guest_local_addr, resolved.guest_remote_addr)
1577    }
1578
1579    fn from_stream(
1580        stream: TcpStream,
1581        listener_id: Option<String>,
1582        guest_local_addr: SocketAddr,
1583        guest_remote_addr: SocketAddr,
1584    ) -> Result<Self, SidecarError> {
1585        let read_stream = stream.try_clone().map_err(sidecar_net_error)?;
1586        read_stream
1587            .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
1588            .map_err(sidecar_net_error)?;
1589        let stream = Arc::new(Mutex::new(stream));
1590        let pending_read_stream = Arc::new(Mutex::new(Some(read_stream)));
1591        let (sender, events) = mpsc::channel();
1592        let tls_mode = Arc::new(AtomicBool::new(false));
1593        let tls_stream = Arc::new(Mutex::new(None));
1594        let tls_state = Arc::new(Mutex::new(None));
1595        let loopback_tls_pending_write = Arc::new(Mutex::new(None));
1596        let saw_local_shutdown = Arc::new(AtomicBool::new(false));
1597        let saw_remote_end = Arc::new(AtomicBool::new(false));
1598        let close_notified = Arc::new(AtomicBool::new(false));
1599
1600        Ok(Self {
1601            stream: Some(stream),
1602            pending_read_stream: Some(pending_read_stream),
1603            events: Some(events),
1604            event_sender: Some(sender),
1605            event_pusher: Arc::new(Mutex::new(None)),
1606            kernel_socket_id: None,
1607            no_delay: false,
1608            keep_alive: false,
1609            keep_alive_initial_delay_secs: None,
1610            guest_local_addr,
1611            guest_remote_addr,
1612            listener_id,
1613            tls_mode,
1614            tls_stream,
1615            tls_state,
1616            loopback_tls_pending_write,
1617            saw_local_shutdown,
1618            saw_remote_end,
1619            close_notified,
1620        })
1621    }
1622
1623    fn from_kernel(
1624        socket_id: SocketId,
1625        listener_id: Option<String>,
1626        guest_local_addr: SocketAddr,
1627        guest_remote_addr: SocketAddr,
1628    ) -> Self {
1629        let (sender, events) = mpsc::channel();
1630        Self {
1631            stream: None,
1632            pending_read_stream: None,
1633            events: Some(events),
1634            event_sender: Some(sender),
1635            event_pusher: Arc::new(Mutex::new(None)),
1636            kernel_socket_id: Some(socket_id),
1637            no_delay: false,
1638            keep_alive: false,
1639            keep_alive_initial_delay_secs: None,
1640            guest_local_addr,
1641            guest_remote_addr,
1642            listener_id,
1643            tls_mode: Arc::new(AtomicBool::new(false)),
1644            tls_stream: Arc::new(Mutex::new(None)),
1645            tls_state: Arc::new(Mutex::new(None)),
1646            loopback_tls_pending_write: Arc::new(Mutex::new(None)),
1647            saw_local_shutdown: Arc::new(AtomicBool::new(false)),
1648            saw_remote_end: Arc::new(AtomicBool::new(false)),
1649            close_notified: Arc::new(AtomicBool::new(false)),
1650        }
1651    }
1652
1653    fn set_event_pusher(&self, session: Option<V8SessionHandle>, socket_id: String) {
1654        let Some(session) = session else {
1655            return;
1656        };
1657        if let Ok(mut pusher) = self.event_pusher.lock() {
1658            *pusher = Some(JavascriptSocketEventPusher { session, socket_id });
1659        }
1660    }
1661
1662    fn poll(
1663        &mut self,
1664        kernel: &mut SidecarKernel,
1665        kernel_pid: u32,
1666        wait: Duration,
1667        trace_enabled: bool,
1668    ) -> Result<Option<JavascriptTcpSocketEvent>, SidecarError> {
1669        if self.tls_mode.load(Ordering::SeqCst) {
1670            self.ensure_tcp_reader()?;
1671            return match self
1672                .events
1673                .as_ref()
1674                .ok_or_else(|| {
1675                    SidecarError::InvalidState(String::from("TCP socket event channel missing"))
1676                })?
1677                .recv_timeout(wait)
1678            {
1679                Ok(event) => Ok(Some(event)),
1680                Err(RecvTimeoutError::Timeout) => Ok(None),
1681                Err(RecvTimeoutError::Disconnected) => Ok(None),
1682            };
1683        }
1684
1685        if let Some(socket_id) = self.kernel_socket_id {
1686            let poll_started = Instant::now();
1687            let result = kernel
1688                .poll_targets(
1689                    EXECUTION_DRIVER_NAME,
1690                    kernel_pid,
1691                    vec![PollTargetEntry::socket(
1692                        socket_id,
1693                        POLLIN | POLLHUP | POLLERR,
1694                    )],
1695                    i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
1696                )
1697                .map_err(kernel_error)?;
1698            let poll_elapsed = poll_started.elapsed();
1699            let revents = result
1700                .targets
1701                .first()
1702                .map(|entry| entry.revents)
1703                .unwrap_or_else(PollEvents::empty);
1704            record_net_tcp_kernel_poll(trace_enabled, wait, poll_elapsed, revents);
1705            if revents.is_empty() {
1706                return Ok(None);
1707            }
1708            if revents.intersects(POLLIN) {
1709                let read_started = Instant::now();
1710                let read_result =
1711                    kernel.socket_read(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, 64 * 1024);
1712                if trace_enabled {
1713                    NET_TCP_TRACE_COUNTERS.socket_read_kernel_us.fetch_add(
1714                        duration_micros_u64(read_started.elapsed()),
1715                        Ordering::Relaxed,
1716                    );
1717                }
1718                return match read_result {
1719                    Ok(Some(bytes)) if !bytes.is_empty() => {
1720                        if trace_enabled {
1721                            NET_TCP_TRACE_COUNTERS
1722                                .socket_read_data_events
1723                                .fetch_add(1, Ordering::Relaxed);
1724                            NET_TCP_TRACE_COUNTERS.socket_read_bytes.fetch_add(
1725                                u64::try_from(bytes.len()).unwrap_or(u64::MAX),
1726                                Ordering::Relaxed,
1727                            );
1728                        }
1729                        Ok(Some(JavascriptTcpSocketEvent::Data(bytes)))
1730                    }
1731                    Ok(Some(_)) => {
1732                        if trace_enabled {
1733                            NET_TCP_TRACE_COUNTERS
1734                                .socket_read_data_events
1735                                .fetch_add(1, Ordering::Relaxed);
1736                        }
1737                        Ok(Some(JavascriptTcpSocketEvent::Data(Vec::new())))
1738                    }
1739                    Ok(None) => {
1740                        if trace_enabled {
1741                            NET_TCP_TRACE_COUNTERS
1742                                .socket_read_end_events
1743                                .fetch_add(1, Ordering::Relaxed);
1744                        }
1745                        self.saw_remote_end.store(true, Ordering::SeqCst);
1746                        Ok(Some(JavascriptTcpSocketEvent::End))
1747                    }
1748                    Err(error) if error.code() == "EAGAIN" => {
1749                        if trace_enabled {
1750                            NET_TCP_TRACE_COUNTERS
1751                                .socket_read_eagain
1752                                .fetch_add(1, Ordering::Relaxed);
1753                        }
1754                        Ok(None)
1755                    }
1756                    Err(error) => {
1757                        if trace_enabled {
1758                            NET_TCP_TRACE_COUNTERS
1759                                .socket_read_errors
1760                                .fetch_add(1, Ordering::Relaxed);
1761                        }
1762                        Ok(Some(JavascriptTcpSocketEvent::Error {
1763                            code: Some(error.code().to_string()),
1764                            message: error.to_string(),
1765                        }))
1766                    }
1767                };
1768            }
1769            if revents.intersects(POLLHUP) {
1770                self.saw_remote_end.store(true, Ordering::SeqCst);
1771                return Ok(Some(JavascriptTcpSocketEvent::End));
1772            }
1773            if revents.intersects(POLLERR) {
1774                return Ok(Some(JavascriptTcpSocketEvent::Error {
1775                    code: Some(String::from("EPIPE")),
1776                    message: String::from("kernel TCP socket reported POLLERR"),
1777                }));
1778            }
1779            return Ok(None);
1780        }
1781
1782        self.ensure_tcp_reader()?;
1783        match self
1784            .events
1785            .as_ref()
1786            .ok_or_else(|| {
1787                SidecarError::InvalidState(String::from("TCP socket event channel missing"))
1788            })?
1789            .recv_timeout(wait)
1790        {
1791            Ok(event) => Ok(Some(event)),
1792            Err(RecvTimeoutError::Timeout) => Ok(None),
1793            Err(RecvTimeoutError::Disconnected) => Ok(None),
1794        }
1795    }
1796
1797    fn ensure_tcp_reader(&self) -> Result<(), SidecarError> {
1798        if self.kernel_socket_id.is_some() {
1799            return Ok(());
1800        }
1801        if self.tls_mode.load(Ordering::SeqCst) {
1802            return Ok(());
1803        }
1804        let read_stream = self
1805            .pending_read_stream
1806            .as_ref()
1807            .ok_or_else(|| {
1808                SidecarError::InvalidState(String::from("TCP socket reader handle missing"))
1809            })?
1810            .lock()
1811            .map_err(|_| {
1812                SidecarError::InvalidState(String::from("TCP socket reader lock poisoned"))
1813            })?
1814            .take();
1815        if let Some(read_stream) = read_stream {
1816            spawn_tcp_socket_reader(
1817                read_stream,
1818                self.event_sender
1819                    .as_ref()
1820                    .ok_or_else(|| {
1821                        SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1822                    })?
1823                    .clone(),
1824                Arc::clone(&self.event_pusher),
1825                Arc::clone(&self.tls_mode),
1826                Arc::clone(&self.saw_local_shutdown),
1827                Arc::clone(&self.saw_remote_end),
1828                Arc::clone(&self.close_notified),
1829            );
1830        }
1831        Ok(())
1832    }
1833
1834    fn socket_info(&self) -> Value {
1835        tcp_socket_info_value(&self.guest_local_addr, &self.guest_remote_addr)
1836    }
1837
1838    fn set_no_delay(&mut self, enable: bool) -> Result<(), SidecarError> {
1839        self.no_delay = enable;
1840        if self.kernel_socket_id.is_some() {
1841            return Ok(());
1842        }
1843        let stream = self
1844            .stream
1845            .as_ref()
1846            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1847            .lock()
1848            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1849        stream.set_nodelay(enable).map_err(sidecar_net_error)
1850    }
1851
1852    fn set_keep_alive(
1853        &mut self,
1854        enable: bool,
1855        initial_delay_secs: Option<u64>,
1856    ) -> Result<(), SidecarError> {
1857        self.keep_alive = enable;
1858        self.keep_alive_initial_delay_secs = initial_delay_secs;
1859        if self.kernel_socket_id.is_some() {
1860            return Ok(());
1861        }
1862        let stream = self
1863            .stream
1864            .as_ref()
1865            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1866            .lock()
1867            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1868        let socket = SockRef::from(&*stream);
1869        socket.set_keepalive(enable).map_err(sidecar_net_error)?;
1870        if enable {
1871            if let Some(delay_secs) = initial_delay_secs.filter(|delay_secs| *delay_secs > 0) {
1872                socket
1873                    .set_tcp_keepalive(
1874                        &TcpKeepalive::new().with_time(Duration::from_secs(delay_secs)),
1875                    )
1876                    .map_err(sidecar_net_error)?;
1877            }
1878        }
1879        Ok(())
1880    }
1881
1882    fn upgrade_tls(
1883        &self,
1884        vm_id: &str,
1885        kernel: &SidecarKernel,
1886        options: JavascriptTlsBridgeOptions,
1887    ) -> Result<(), SidecarError> {
1888        if self.tls_mode.load(Ordering::SeqCst) {
1889            return Ok(());
1890        }
1891
1892        let client_hello = if options.is_server {
1893            self.peek_tls_client_hello(vm_id, kernel)?
1894        } else {
1895            None
1896        };
1897
1898        let (tls_stream, loopback_pending_write) = if let Some(socket_id) = self.kernel_socket_id {
1899            let peer_socket_id = wait_for_loopback_peer_socket_id(kernel, socket_id)
1900                .ok_or_else(|| {
1901                    SidecarError::Execution(format!(
1902                        "ERR_NOT_IMPLEMENTED: kernel-backed loopback socket {socket_id} has no peer for TLS upgrade"
1903                    ))
1904                })?;
1905            let endpoint = loopback_tls_endpoint(vm_id, socket_id, peer_socket_id)?;
1906            let pending_write = LoopbackTlsPendingWriteHandle::new(&endpoint);
1907            let tls_stream = if options.is_server {
1908                ActiveTlsStream::LoopbackServer(build_server_loopback_tls_stream(
1909                    endpoint, &options,
1910                )?)
1911            } else {
1912                ActiveTlsStream::LoopbackClient(build_client_loopback_tls_stream(
1913                    endpoint, &options,
1914                )?)
1915            };
1916            (tls_stream, Some(pending_write))
1917        } else {
1918            self.pending_read_stream
1919                .as_ref()
1920                .ok_or_else(|| {
1921                    SidecarError::InvalidState(String::from("TCP socket reader handle missing"))
1922                })?
1923                .lock()
1924                .map_err(|_| {
1925                    SidecarError::InvalidState(String::from("TCP socket reader lock poisoned"))
1926                })?
1927                .take();
1928            let stream = self
1929                .stream
1930                .as_ref()
1931                .ok_or_else(|| {
1932                    SidecarError::InvalidState(String::from("TCP socket stream missing"))
1933                })?
1934                .lock()
1935                .map_err(|_| {
1936                    SidecarError::InvalidState(String::from("TCP socket lock poisoned"))
1937                })?;
1938            let cloned = stream.try_clone().map_err(sidecar_net_error)?;
1939            drop(stream);
1940
1941            if options.is_server {
1942                (
1943                    ActiveTlsStream::Server(build_server_tls_stream(cloned, &options)?),
1944                    None,
1945                )
1946            } else {
1947                (
1948                    ActiveTlsStream::Client(build_client_tls_stream(cloned, &options)?),
1949                    None,
1950                )
1951            }
1952        };
1953
1954        let tls_state = ActiveTlsState {
1955            client_hello,
1956            local_certificates: tls_local_certificates(&options)?,
1957            session_reused: false,
1958        };
1959
1960        self.tls_mode.store(true, Ordering::SeqCst);
1961        {
1962            let mut state = self
1963                .tls_state
1964                .lock()
1965                .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?;
1966            *state = Some(tls_state);
1967        }
1968        {
1969            let mut stream = self.tls_stream.lock().map_err(|_| {
1970                SidecarError::InvalidState(String::from("TLS stream lock poisoned"))
1971            })?;
1972            *stream = Some(tls_stream);
1973        }
1974        {
1975            let mut pending = self.loopback_tls_pending_write.lock().map_err(|_| {
1976                SidecarError::InvalidState(String::from(
1977                    "loopback TLS pending write handle lock poisoned",
1978                ))
1979            })?;
1980            *pending = loopback_pending_write.clone();
1981        }
1982
1983        spawn_tls_socket_reader(
1984            Arc::clone(&self.tls_stream),
1985            loopback_pending_write,
1986            self.event_sender
1987                .as_ref()
1988                .ok_or_else(|| {
1989                    SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1990                })?
1991                .clone(),
1992            Arc::clone(&self.event_pusher),
1993            Arc::clone(&self.saw_local_shutdown),
1994            Arc::clone(&self.saw_remote_end),
1995            Arc::clone(&self.close_notified),
1996        );
1997        Ok(())
1998    }
1999
2000    fn peek_tls_client_hello(
2001        &self,
2002        vm_id: &str,
2003        kernel: &SidecarKernel,
2004    ) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
2005        if let Some(socket_id) = self.kernel_socket_id {
2006            let Some(peer_socket_id) = kernel
2007                .socket_get(socket_id)
2008                .and_then(|record| record.peer_socket_id())
2009            else {
2010                return Ok(None);
2011            };
2012            return peek_loopback_tls_client_hello(vm_id, socket_id, peer_socket_id);
2013        }
2014
2015        let stream = self
2016            .stream
2017            .as_ref()
2018            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
2019            .lock()
2020            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
2021        let mut buffer = vec![0_u8; 16 * 1024];
2022        let bytes = match stream.peek(&mut buffer) {
2023            Ok(0) => return Ok(None),
2024            Ok(bytes) => bytes,
2025            Err(error)
2026                if matches!(
2027                    error.kind(),
2028                    std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
2029                ) =>
2030            {
2031                return Ok(None);
2032            }
2033            Err(error) => return Err(sidecar_net_error(error)),
2034        };
2035        parse_tls_client_hello_from_bytes(&buffer[..bytes])
2036    }
2037
2038    fn tls_client_hello_json(
2039        &self,
2040        vm_id: &str,
2041        kernel: &SidecarKernel,
2042    ) -> Result<Value, SidecarError> {
2043        if let Some(client_hello) = self
2044            .tls_state
2045            .lock()
2046            .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?
2047            .as_ref()
2048            .and_then(|state| state.client_hello.clone())
2049        {
2050            return javascript_net_json_string(
2051                serde_json::to_value(client_hello).map_err(|error| {
2052                    SidecarError::InvalidState(format!(
2053                        "failed to serialize TLS client hello: {error}"
2054                    ))
2055                })?,
2056                "net.socket_get_tls_client_hello",
2057            );
2058        }
2059
2060        javascript_net_json_string(
2061            serde_json::to_value(
2062                self.peek_tls_client_hello(vm_id, kernel)?
2063                    .unwrap_or_default(),
2064            )
2065            .map_err(|error| {
2066                SidecarError::InvalidState(format!("failed to serialize TLS client hello: {error}"))
2067            })?,
2068            "net.socket_get_tls_client_hello",
2069        )
2070    }
2071
2072    fn tls_query(&self, query: &str, detailed: bool) -> Result<Value, SidecarError> {
2073        let state = self
2074            .tls_state
2075            .lock()
2076            .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?
2077            .clone();
2078        let mut tls_stream = self
2079            .tls_stream
2080            .lock()
2081            .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?;
2082        let Some(stream) = tls_stream.as_mut() else {
2083            return javascript_net_json_string(
2084                tls_bridge_undefined_value(),
2085                "net.socket_tls_query",
2086            );
2087        };
2088
2089        let payload = match query {
2090            "getSession" => tls_bridge_undefined_value(),
2091            "isSessionReused" => Value::Bool(
2092                state
2093                    .as_ref()
2094                    .is_some_and(|tls_state| tls_state.session_reused),
2095            ),
2096            "getPeerCertificate" => {
2097                let certificate = stream
2098                    .peer_certificates()
2099                    .and_then(|certificates| certificates.first())
2100                    .map(|certificate| {
2101                        tls_certificate_bridge_value(certificate.as_ref(), detailed)
2102                    });
2103                certificate.unwrap_or_else(tls_bridge_undefined_value)
2104            }
2105            "getCertificate" => state
2106                .as_ref()
2107                .and_then(|tls_state| tls_state.local_certificates.first())
2108                .map(|certificate| tls_certificate_bridge_value(certificate, detailed))
2109                .unwrap_or_else(tls_bridge_undefined_value),
2110            "getProtocol" => stream
2111                .protocol_version()
2112                .map(tls_protocol_name)
2113                .map(Value::String)
2114                .unwrap_or(Value::Null),
2115            "getCipher" => stream
2116                .negotiated_cipher_suite()
2117                .map(tls_cipher_bridge_value)
2118                .unwrap_or_else(tls_bridge_undefined_value),
2119            other => {
2120                return Err(SidecarError::InvalidState(format!(
2121                    "unsupported TLS query {other}"
2122                )));
2123            }
2124        };
2125        javascript_net_json_string(payload, "net.socket_tls_query")
2126    }
2127
2128    fn write_all(
2129        &self,
2130        kernel: &mut SidecarKernel,
2131        kernel_pid: u32,
2132        contents: &[u8],
2133    ) -> Result<usize, SidecarError> {
2134        if self.tls_mode.load(Ordering::SeqCst) {
2135            let loopback_pending_write = self
2136                .loopback_tls_pending_write
2137                .lock()
2138                .map_err(|_| {
2139                    SidecarError::InvalidState(String::from(
2140                        "loopback TLS pending write handle lock poisoned",
2141                    ))
2142                })?
2143                .clone();
2144            if let Some(pending_write) = loopback_pending_write.as_ref() {
2145                if pending_write.should_buffer_write()? {
2146                    pending_write.append_write(contents)?;
2147                    return Ok(contents.len());
2148                }
2149                pending_write.interrupt_own_reader();
2150            }
2151            let mut tls_stream = self.tls_stream.lock().map_err(|_| {
2152                SidecarError::InvalidState(String::from("TLS stream lock poisoned"))
2153            })?;
2154            let stream = tls_stream.as_mut().ok_or_else(|| {
2155                SidecarError::InvalidState(String::from("TLS stream missing for upgraded socket"))
2156            })?;
2157            stream.write_all(contents)?;
2158            return Ok(contents.len());
2159        }
2160        if let Some(socket_id) = self.kernel_socket_id {
2161            return kernel
2162                .socket_write(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, contents)
2163                .map_err(kernel_error);
2164        }
2165
2166        let mut stream = self
2167            .stream
2168            .as_ref()
2169            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
2170            .lock()
2171            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
2172        stream.write_all(contents).map_err(sidecar_net_error)?;
2173        Ok(contents.len())
2174    }
2175
2176    fn shutdown_write(
2177        &self,
2178        kernel: &mut SidecarKernel,
2179        kernel_pid: u32,
2180    ) -> Result<(), SidecarError> {
2181        if self.tls_mode.load(Ordering::SeqCst) {
2182            let loopback_pending_write = self
2183                .loopback_tls_pending_write
2184                .lock()
2185                .map_err(|_| {
2186                    SidecarError::InvalidState(String::from(
2187                        "loopback TLS pending write handle lock poisoned",
2188                    ))
2189                })?
2190                .clone();
2191            if let Some(pending_write) = loopback_pending_write.as_ref() {
2192                if pending_write.should_buffer_write()? {
2193                    pending_write.defer_shutdown_write()?;
2194                    self.saw_local_shutdown.store(true, Ordering::SeqCst);
2195                    return Ok(());
2196                }
2197                pending_write.interrupt_own_reader();
2198            }
2199            if let Some(stream) = self
2200                .tls_stream
2201                .lock()
2202                .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?
2203                .as_mut()
2204            {
2205                let _ = stream.send_close_notify();
2206                let _ = stream.shutdown_write();
2207            }
2208            if self.kernel_socket_id.is_some() {
2209                self.saw_local_shutdown.store(true, Ordering::SeqCst);
2210                return Ok(());
2211            }
2212        }
2213        if let Some(socket_id) = self.kernel_socket_id {
2214            self.saw_local_shutdown.store(true, Ordering::SeqCst);
2215            match kernel.socket_shutdown(
2216                EXECUTION_DRIVER_NAME,
2217                kernel_pid,
2218                socket_id,
2219                KernelSocketShutdown::Write,
2220            ) {
2221                Ok(()) => {}
2222                Err(error) if error.code() == "ENOENT" => {}
2223                Err(error) => return Err(kernel_error(error)),
2224            }
2225            return Ok(());
2226        }
2227        let stream = self
2228            .stream
2229            .as_ref()
2230            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
2231            .lock()
2232            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
2233        self.saw_local_shutdown.store(true, Ordering::SeqCst);
2234        match stream.shutdown(Shutdown::Write) {
2235            Ok(()) => {}
2236            Err(error) if error.kind() == std::io::ErrorKind::NotConnected => {}
2237            Err(error) => return Err(sidecar_net_error(error)),
2238        }
2239        if self.saw_remote_end.load(Ordering::SeqCst)
2240            && !self.close_notified.swap(true, Ordering::SeqCst)
2241        {
2242            let _ = self
2243                .event_sender
2244                .as_ref()
2245                .ok_or_else(|| {
2246                    SidecarError::InvalidState(String::from("TCP socket event sender missing"))
2247                })?
2248                .send(JavascriptTcpSocketEvent::Close { had_error: false });
2249        }
2250        Ok(())
2251    }
2252
2253    fn close(&self, kernel: &mut SidecarKernel, kernel_pid: u32) -> Result<(), SidecarError> {
2254        if self.tls_mode.load(Ordering::SeqCst) {
2255            if let Some(pending_write) = self
2256                .loopback_tls_pending_write
2257                .lock()
2258                .map_err(|_| {
2259                    SidecarError::InvalidState(String::from(
2260                        "loopback TLS pending write handle lock poisoned",
2261                    ))
2262                })?
2263                .take()
2264            {
2265                pending_write.clear_for_close();
2266                pending_write.interrupt_own_reader();
2267            }
2268            if let Some(stream) = self
2269                .tls_stream
2270                .lock()
2271                .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?
2272                .as_mut()
2273            {
2274                let _ = stream.send_close_notify();
2275                let _ = stream.close();
2276            }
2277        }
2278        if let Some(socket_id) = self.kernel_socket_id {
2279            return close_kernel_socket_idempotent(kernel, kernel_pid, socket_id);
2280        }
2281        let stream = self
2282            .stream
2283            .as_ref()
2284            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
2285            .lock()
2286            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
2287        stream.shutdown(Shutdown::Both).map_err(sidecar_net_error)
2288    }
2289}
2290
2291fn close_kernel_socket_idempotent(
2292    kernel: &mut SidecarKernel,
2293    kernel_pid: u32,
2294    socket_id: SocketId,
2295) -> Result<(), SidecarError> {
2296    match kernel.socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id) {
2297        Ok(()) => Ok(()),
2298        Err(error) if error.code() == "ENOENT" => Ok(()),
2299        Err(error) => Err(kernel_error(error)),
2300    }
2301}
2302
2303fn register_kernel_readiness_target(
2304    registry: &KernelSocketReadinessRegistry,
2305    kernel_socket_id: Option<SocketId>,
2306    session: Option<V8SessionHandle>,
2307    target_id: String,
2308    event: KernelSocketReadinessEvent,
2309) {
2310    let (Some(kernel_socket_id), Some(session)) = (kernel_socket_id, session) else {
2311        return;
2312    };
2313    if let Ok(mut targets) = registry.lock() {
2314        targets.insert(
2315            kernel_socket_id,
2316            KernelSocketReadinessTarget {
2317                session,
2318                target_id,
2319                event,
2320            },
2321        );
2322    }
2323}
2324
2325fn unregister_kernel_readiness_target(
2326    registry: &KernelSocketReadinessRegistry,
2327    kernel_socket_id: Option<SocketId>,
2328) {
2329    let Some(kernel_socket_id) = kernel_socket_id else {
2330        return;
2331    };
2332    if let Ok(mut targets) = registry.lock() {
2333        targets.remove(&kernel_socket_id);
2334    }
2335}
2336
2337fn release_tcp_socket_handle(
2338    process: &mut ActiveProcess,
2339    socket_id: &str,
2340    socket: ActiveTcpSocket,
2341    kernel: &mut SidecarKernel,
2342    kernel_readiness: &KernelSocketReadinessRegistry,
2343) {
2344    unregister_kernel_readiness_target(kernel_readiness, socket.kernel_socket_id);
2345    if let Some(listener_id) = socket.listener_id.as_deref() {
2346        if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
2347            listener.release_connection(socket_id);
2348        }
2349    }
2350    let _ = socket.close(kernel, process.kernel_pid);
2351}
2352
2353fn release_unix_socket_handle(
2354    process: &mut ActiveProcess,
2355    socket_id: &str,
2356    socket: ActiveUnixSocket,
2357) {
2358    if let Some(listener_id) = socket.listener_id.as_deref() {
2359        if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
2360            listener.release_connection(socket_id);
2361        }
2362    }
2363    let _ = socket.close();
2364}
2365
2366impl ActiveTlsStream {
2367    fn is_loopback(&self) -> bool {
2368        matches!(self, Self::LoopbackClient(_) | Self::LoopbackServer(_))
2369    }
2370
2371    fn is_handshaking(&self) -> bool {
2372        match self {
2373            Self::Client(stream) => stream.conn.is_handshaking(),
2374            Self::Server(stream) => stream.conn.is_handshaking(),
2375            Self::LoopbackClient(stream) => stream.conn.is_handshaking(),
2376            Self::LoopbackServer(stream) => stream.conn.is_handshaking(),
2377        }
2378    }
2379
2380    fn set_loopback_poll_timeout(&mut self, timeout: Duration) {
2381        match self {
2382            Self::LoopbackClient(stream) => stream.sock.set_poll_timeout(timeout),
2383            Self::LoopbackServer(stream) => stream.sock.set_poll_timeout(timeout),
2384            Self::Client(_) | Self::Server(_) => {}
2385        }
2386    }
2387
2388    fn write_all(&mut self, contents: &[u8]) -> Result<(), SidecarError> {
2389        match self {
2390            Self::Client(stream) => {
2391                stream.write_all(contents).map_err(sidecar_net_error)?;
2392                stream.flush().map_err(sidecar_net_error)
2393            }
2394            Self::Server(stream) => {
2395                stream.write_all(contents).map_err(sidecar_net_error)?;
2396                stream.flush().map_err(sidecar_net_error)
2397            }
2398            Self::LoopbackClient(stream) => {
2399                stream.write_all(contents).map_err(sidecar_net_error)?;
2400                stream.flush().map_err(sidecar_net_error)
2401            }
2402            Self::LoopbackServer(stream) => {
2403                stream.write_all(contents).map_err(sidecar_net_error)?;
2404                stream.flush().map_err(sidecar_net_error)
2405            }
2406        }
2407    }
2408
2409    fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
2410        match self {
2411            Self::Client(stream) => stream.read(buffer),
2412            Self::Server(stream) => stream.read(buffer),
2413            Self::LoopbackClient(stream) => stream.read(buffer),
2414            Self::LoopbackServer(stream) => stream.read(buffer),
2415        }
2416    }
2417
2418    fn send_close_notify(&mut self) -> Result<(), SidecarError> {
2419        match self {
2420            Self::Client(stream) => {
2421                stream.conn.send_close_notify();
2422                let _ = stream.conn.complete_io(&mut stream.sock);
2423            }
2424            Self::Server(stream) => {
2425                stream.conn.send_close_notify();
2426                let _ = stream.conn.complete_io(&mut stream.sock);
2427            }
2428            Self::LoopbackClient(stream) => {
2429                stream.conn.send_close_notify();
2430                let _ = stream.conn.complete_io(&mut stream.sock);
2431            }
2432            Self::LoopbackServer(stream) => {
2433                stream.conn.send_close_notify();
2434                let _ = stream.conn.complete_io(&mut stream.sock);
2435            }
2436        }
2437        Ok(())
2438    }
2439
2440    fn shutdown_write(&mut self) -> Result<(), SidecarError> {
2441        match self {
2442            Self::Client(stream) => stream
2443                .sock
2444                .shutdown(Shutdown::Write)
2445                .map_err(sidecar_net_error),
2446            Self::Server(stream) => stream
2447                .sock
2448                .shutdown(Shutdown::Write)
2449                .map_err(sidecar_net_error),
2450            Self::LoopbackClient(stream) => stream.sock.shutdown_write(),
2451            Self::LoopbackServer(stream) => stream.sock.shutdown_write(),
2452        }
2453    }
2454
2455    fn close(&mut self) -> Result<(), SidecarError> {
2456        match self {
2457            Self::Client(stream) => stream
2458                .sock
2459                .shutdown(Shutdown::Both)
2460                .map_err(sidecar_net_error),
2461            Self::Server(stream) => stream
2462                .sock
2463                .shutdown(Shutdown::Both)
2464                .map_err(sidecar_net_error),
2465            Self::LoopbackClient(stream) => stream.sock.close_endpoint(),
2466            Self::LoopbackServer(stream) => stream.sock.close_endpoint(),
2467        }
2468    }
2469
2470    fn peer_certificates(&self) -> Option<&[CertificateDer<'static>]> {
2471        match self {
2472            Self::Client(stream) => stream.conn.peer_certificates(),
2473            Self::Server(stream) => stream.conn.peer_certificates(),
2474            Self::LoopbackClient(stream) => stream.conn.peer_certificates(),
2475            Self::LoopbackServer(stream) => stream.conn.peer_certificates(),
2476        }
2477    }
2478
2479    fn negotiated_cipher_suite(&self) -> Option<rustls::SupportedCipherSuite> {
2480        match self {
2481            Self::Client(stream) => stream.conn.negotiated_cipher_suite(),
2482            Self::Server(stream) => stream.conn.negotiated_cipher_suite(),
2483            Self::LoopbackClient(stream) => stream.conn.negotiated_cipher_suite(),
2484            Self::LoopbackServer(stream) => stream.conn.negotiated_cipher_suite(),
2485        }
2486    }
2487
2488    fn protocol_version(&self) -> Option<rustls::ProtocolVersion> {
2489        match self {
2490            Self::Client(stream) => stream.conn.protocol_version(),
2491            Self::Server(stream) => stream.conn.protocol_version(),
2492            Self::LoopbackClient(stream) => stream.conn.protocol_version(),
2493            Self::LoopbackServer(stream) => stream.conn.protocol_version(),
2494        }
2495    }
2496}
2497
2498// ActiveTcpListener moved to crate::state
2499
2500// Unix socket types moved to crate::state
2501
2502impl ActiveUnixSocket {
2503    fn connect(host_path: &Path, guest_path: &str) -> Result<Self, SidecarError> {
2504        let stream = UnixStream::connect(host_path).map_err(sidecar_net_error)?;
2505        Self::from_stream(stream, None, None, Some(guest_path.to_owned()))
2506    }
2507
2508    fn from_stream(
2509        stream: UnixStream,
2510        listener_id: Option<String>,
2511        local_path: Option<String>,
2512        remote_path: Option<String>,
2513    ) -> Result<Self, SidecarError> {
2514        let read_stream = stream.try_clone().map_err(sidecar_net_error)?;
2515        let stream = Arc::new(Mutex::new(stream));
2516        let (sender, events) = mpsc::channel();
2517        let event_pusher = Arc::new(Mutex::new(None));
2518        let saw_local_shutdown = Arc::new(AtomicBool::new(false));
2519        let saw_remote_end = Arc::new(AtomicBool::new(false));
2520        let close_notified = Arc::new(AtomicBool::new(false));
2521        spawn_unix_socket_reader(
2522            read_stream,
2523            sender.clone(),
2524            Arc::clone(&event_pusher),
2525            Arc::clone(&saw_local_shutdown),
2526            Arc::clone(&saw_remote_end),
2527            Arc::clone(&close_notified),
2528        );
2529
2530        Ok(Self {
2531            stream,
2532            events,
2533            event_sender: sender,
2534            event_pusher,
2535            listener_id,
2536            local_path,
2537            remote_path,
2538            saw_local_shutdown,
2539            saw_remote_end,
2540            close_notified,
2541        })
2542    }
2543
2544    fn set_event_pusher(&self, session: Option<V8SessionHandle>, socket_id: String) {
2545        let Some(session) = session else {
2546            return;
2547        };
2548        if let Ok(mut pusher) = self.event_pusher.lock() {
2549            *pusher = Some(JavascriptSocketEventPusher { session, socket_id });
2550        }
2551    }
2552
2553    fn poll(&mut self, wait: Duration) -> Result<Option<JavascriptTcpSocketEvent>, SidecarError> {
2554        match self.events.recv_timeout(wait) {
2555            Ok(event) => Ok(Some(event)),
2556            Err(RecvTimeoutError::Timeout) => Ok(None),
2557            Err(RecvTimeoutError::Disconnected) => Ok(None),
2558        }
2559    }
2560
2561    fn socket_info(&self) -> Value {
2562        unix_socket_info_value(self.local_path.as_deref(), self.remote_path.as_deref())
2563    }
2564
2565    fn write_all(&self, contents: &[u8]) -> Result<usize, SidecarError> {
2566        let mut stream = self
2567            .stream
2568            .lock()
2569            .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
2570        stream.write_all(contents).map_err(sidecar_net_error)?;
2571        Ok(contents.len())
2572    }
2573
2574    fn shutdown_write(&self) -> Result<(), SidecarError> {
2575        let stream = self
2576            .stream
2577            .lock()
2578            .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
2579        self.saw_local_shutdown.store(true, Ordering::SeqCst);
2580        stream
2581            .shutdown(Shutdown::Write)
2582            .map_err(sidecar_net_error)?;
2583        if self.saw_remote_end.load(Ordering::SeqCst)
2584            && !self.close_notified.swap(true, Ordering::SeqCst)
2585        {
2586            let _ = self
2587                .event_sender
2588                .send(JavascriptTcpSocketEvent::Close { had_error: false });
2589        }
2590        Ok(())
2591    }
2592
2593    fn close(&self) -> Result<(), SidecarError> {
2594        let stream = self
2595            .stream
2596            .lock()
2597            .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
2598        stream.shutdown(Shutdown::Both).map_err(sidecar_net_error)
2599    }
2600}
2601
2602// ActiveUnixListener moved to crate::state
2603
2604impl ActiveUnixListener {
2605    fn bind(
2606        host_path: &Path,
2607        guest_path: &str,
2608        backlog: Option<u32>,
2609    ) -> Result<Self, SidecarError> {
2610        if let Some(parent) = host_path.parent() {
2611            fs::create_dir_all(parent).map_err(sidecar_net_error)?;
2612        }
2613        let listener = UnixListener::bind(host_path).map_err(sidecar_net_error)?;
2614        listener.set_nonblocking(true).map_err(sidecar_net_error)?;
2615        Ok(Self {
2616            listener,
2617            path: guest_path.to_owned(),
2618            backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
2619                .expect("default backlog fits within usize"),
2620            active_connection_ids: BTreeSet::new(),
2621        })
2622    }
2623
2624    fn path(&self) -> &str {
2625        &self.path
2626    }
2627
2628    fn poll(
2629        &mut self,
2630        wait: Duration,
2631    ) -> Result<Option<JavascriptUnixListenerEvent>, SidecarError> {
2632        let deadline = Instant::now() + wait;
2633        loop {
2634            match self.listener.accept() {
2635                Ok((stream, remote_addr)) => {
2636                    if self.active_connection_ids.len() >= self.backlog {
2637                        let _ = stream.shutdown(Shutdown::Both);
2638                        if wait.is_zero() || Instant::now() >= deadline {
2639                            return Ok(None);
2640                        }
2641                        continue;
2642                    }
2643
2644                    let local_path = Some(self.path.clone());
2645                    let remote_path = unix_socket_path(&remote_addr);
2646                    return Ok(Some(JavascriptUnixListenerEvent::Connection(
2647                        PendingUnixSocket {
2648                            stream,
2649                            local_path,
2650                            remote_path,
2651                        },
2652                    )));
2653                }
2654                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
2655                    if wait.is_zero() || Instant::now() >= deadline {
2656                        return Ok(None);
2657                    }
2658                    if !wait_fd_readable_until(self.listener.as_fd(), deadline) {
2659                        return Ok(None);
2660                    }
2661                }
2662                Err(error) => {
2663                    return Ok(Some(JavascriptUnixListenerEvent::Error {
2664                        code: io_error_code(&error),
2665                        message: error.to_string(),
2666                    }));
2667                }
2668            }
2669        }
2670    }
2671
2672    fn close(&self) -> Result<(), SidecarError> {
2673        Ok(())
2674    }
2675
2676    fn active_connection_count(&self) -> usize {
2677        self.active_connection_ids.len()
2678    }
2679
2680    fn register_connection(&mut self, socket_id: &str) {
2681        self.active_connection_ids.insert(socket_id.to_string());
2682    }
2683
2684    fn release_connection(&mut self, socket_id: &str) {
2685        self.active_connection_ids.remove(socket_id);
2686    }
2687}
2688
2689impl ActiveTcpListener {
2690    fn bind(
2691        bind_host: &str,
2692        guest_host: &str,
2693        guest_port: u16,
2694        backlog: Option<u32>,
2695    ) -> Result<Self, SidecarError> {
2696        let bind_addr = resolve_tcp_bind_addr(bind_host, 0)?;
2697        let guest_addr = resolve_tcp_bind_addr(guest_host, guest_port)?;
2698        let listener = TcpListener::bind(bind_addr).map_err(sidecar_net_error)?;
2699        listener.set_nonblocking(true).map_err(sidecar_net_error)?;
2700        let local_addr = listener.local_addr().map_err(sidecar_net_error)?;
2701        Ok(Self {
2702            listener: Some(listener),
2703            kernel_socket_id: None,
2704            local_addr: Some(local_addr),
2705            guest_local_addr: guest_addr,
2706            backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
2707                .expect("default backlog fits within usize"),
2708            active_connection_ids: BTreeSet::new(),
2709        })
2710    }
2711
2712    fn bind_kernel(
2713        kernel: &mut SidecarKernel,
2714        kernel_pid: u32,
2715        guest_host: &str,
2716        guest_port: u16,
2717        backlog: Option<u32>,
2718    ) -> Result<Self, SidecarError> {
2719        let guest_addr = resolve_tcp_bind_addr(guest_host, guest_port)?;
2720        let spec = match guest_addr {
2721            SocketAddr::V4(_) => SocketSpec::tcp(),
2722            SocketAddr::V6(_) => SocketSpec::new(SocketDomain::Inet6, SocketType::Stream),
2723        };
2724        let socket_id = kernel
2725            .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
2726            .map_err(kernel_error)?;
2727        kernel
2728            .socket_bind_inet(
2729                EXECUTION_DRIVER_NAME,
2730                kernel_pid,
2731                socket_id,
2732                InetSocketAddress::new(guest_addr.ip().to_string(), guest_addr.port()),
2733            )
2734            .map_err(kernel_error)?;
2735        kernel
2736            .socket_listen(
2737                EXECUTION_DRIVER_NAME,
2738                kernel_pid,
2739                socket_id,
2740                usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
2741                    .expect("default backlog fits within usize"),
2742            )
2743            .map_err(kernel_error)?;
2744        Ok(Self {
2745            listener: None,
2746            kernel_socket_id: Some(socket_id),
2747            local_addr: Some(guest_addr),
2748            guest_local_addr: guest_addr,
2749            backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
2750                .expect("default backlog fits within usize"),
2751            active_connection_ids: BTreeSet::new(),
2752        })
2753    }
2754
2755    pub(crate) fn local_addr(&self) -> SocketAddr {
2756        self.local_addr.unwrap_or(self.guest_local_addr)
2757    }
2758
2759    fn guest_local_addr(&self) -> SocketAddr {
2760        self.guest_local_addr
2761    }
2762
2763    fn poll(
2764        &mut self,
2765        kernel: &mut SidecarKernel,
2766        kernel_pid: u32,
2767        wait: Duration,
2768        trace_enabled: bool,
2769    ) -> Result<Option<JavascriptTcpListenerEvent>, SidecarError> {
2770        if let Some(socket_id) = self.kernel_socket_id {
2771            let poll_started = Instant::now();
2772            let result = kernel
2773                .poll_targets(
2774                    EXECUTION_DRIVER_NAME,
2775                    kernel_pid,
2776                    vec![PollTargetEntry::socket(socket_id, POLLIN)],
2777                    i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
2778                )
2779                .map_err(kernel_error)?;
2780            let poll_elapsed = poll_started.elapsed();
2781            let revents = result
2782                .targets
2783                .first()
2784                .map(|entry| entry.revents)
2785                .unwrap_or_else(PollEvents::empty);
2786            record_net_tcp_kernel_poll(trace_enabled, wait, poll_elapsed, revents);
2787            if revents.is_empty() {
2788                return Ok(None);
2789            }
2790            let accepted_socket_id =
2791                match kernel.socket_accept(EXECUTION_DRIVER_NAME, kernel_pid, socket_id) {
2792                    Ok(accepted_socket_id) => accepted_socket_id,
2793                    Err(error) if error.code() == "EAGAIN" => {
2794                        if trace_enabled {
2795                            NET_TCP_TRACE_COUNTERS
2796                                .server_accept_eagain
2797                                .fetch_add(1, Ordering::Relaxed);
2798                        }
2799                        return Ok(None);
2800                    }
2801                    Err(error) => {
2802                        if trace_enabled {
2803                            NET_TCP_TRACE_COUNTERS
2804                                .server_accept_errors
2805                                .fetch_add(1, Ordering::Relaxed);
2806                        }
2807                        return Ok(Some(JavascriptTcpListenerEvent::Error {
2808                            code: Some(error.code().to_string()),
2809                            message: error.to_string(),
2810                        }));
2811                    }
2812                };
2813            let accepted = kernel.socket_get(accepted_socket_id).ok_or_else(|| {
2814                SidecarError::InvalidState(format!(
2815                    "accepted kernel TCP socket {accepted_socket_id} is missing"
2816                ))
2817            })?;
2818            let local_addr = accepted.local_address().ok_or_else(|| {
2819                SidecarError::InvalidState(format!(
2820                    "accepted kernel TCP socket {accepted_socket_id} missing local address"
2821                ))
2822            })?;
2823            let remote_addr = accepted.peer_address().ok_or_else(|| {
2824                SidecarError::InvalidState(format!(
2825                    "accepted kernel TCP socket {accepted_socket_id} missing peer address"
2826                ))
2827            })?;
2828            if trace_enabled {
2829                NET_TCP_TRACE_COUNTERS
2830                    .server_accept_connections
2831                    .fetch_add(1, Ordering::Relaxed);
2832            }
2833            return Ok(Some(JavascriptTcpListenerEvent::Connection(
2834                PendingTcpSocket {
2835                    stream: None,
2836                    kernel_socket_id: Some(accepted_socket_id),
2837                    preallocated: true,
2838                    guest_local_addr: resolve_tcp_bind_addr(local_addr.host(), local_addr.port())?,
2839                    guest_remote_addr: resolve_tcp_bind_addr(
2840                        remote_addr.host(),
2841                        remote_addr.port(),
2842                    )?,
2843                },
2844            )));
2845        }
2846
2847        let deadline = Instant::now() + wait;
2848        loop {
2849            match self
2850                .listener
2851                .as_ref()
2852                .ok_or_else(|| {
2853                    SidecarError::InvalidState(String::from("TCP listener socket missing"))
2854                })?
2855                .accept()
2856            {
2857                Ok((stream, remote_addr)) => {
2858                    if self.active_connection_ids.len() >= self.backlog {
2859                        let _ = stream.shutdown(Shutdown::Both);
2860                        if wait.is_zero() || Instant::now() >= deadline {
2861                            return Ok(None);
2862                        }
2863                        continue;
2864                    }
2865                    return Ok(Some(JavascriptTcpListenerEvent::Connection(
2866                        PendingTcpSocket {
2867                            stream: Some(stream),
2868                            kernel_socket_id: None,
2869                            preallocated: false,
2870                            guest_local_addr: self.guest_local_addr,
2871                            guest_remote_addr: SocketAddr::new(
2872                                remote_addr.ip(),
2873                                remote_addr.port(),
2874                            ),
2875                        },
2876                    )));
2877                }
2878                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
2879                    if wait.is_zero() || Instant::now() >= deadline {
2880                        return Ok(None);
2881                    }
2882                    if !wait_fd_readable_until(
2883                        self.listener
2884                            .as_ref()
2885                            .expect("TCP listener checked before accept")
2886                            .as_fd(),
2887                        deadline,
2888                    ) {
2889                        return Ok(None);
2890                    }
2891                }
2892                Err(error) => {
2893                    return Ok(Some(JavascriptTcpListenerEvent::Error {
2894                        code: io_error_code(&error),
2895                        message: error.to_string(),
2896                    }));
2897                }
2898            }
2899        }
2900    }
2901
2902    fn close(&self, kernel: &mut SidecarKernel, kernel_pid: u32) -> Result<(), SidecarError> {
2903        if let Some(socket_id) = self.kernel_socket_id {
2904            close_kernel_socket_idempotent(kernel, kernel_pid, socket_id)?;
2905        }
2906        Ok(())
2907    }
2908
2909    fn active_connection_count(&self) -> usize {
2910        self.active_connection_ids.len()
2911    }
2912
2913    fn register_connection(&mut self, socket_id: &str) {
2914        self.active_connection_ids.insert(socket_id.to_string());
2915    }
2916
2917    fn release_connection(&mut self, socket_id: &str) {
2918        self.active_connection_ids.remove(socket_id);
2919    }
2920}
2921
2922// UDP types moved to crate::state
2923
2924impl ActiveUdpSocket {
2925    fn new(
2926        kernel: &mut SidecarKernel,
2927        kernel_pid: u32,
2928        family: JavascriptUdpFamily,
2929    ) -> Result<Self, SidecarError> {
2930        let spec = match family {
2931            JavascriptUdpFamily::Ipv4 => SocketSpec::udp(),
2932            JavascriptUdpFamily::Ipv6 => SocketSpec::new(SocketDomain::Inet6, SocketType::Datagram),
2933        };
2934        let socket_id = kernel
2935            .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
2936            .map_err(kernel_error)?;
2937        Ok(Self {
2938            family,
2939            socket: None,
2940            kernel_socket_id: Some(socket_id),
2941            guest_local_addr: None,
2942            recv_buffer_size: 0,
2943            send_buffer_size: 0,
2944        })
2945    }
2946
2947    fn local_addr(&self) -> Option<SocketAddr> {
2948        self.guest_local_addr
2949    }
2950
2951    fn socket(&self) -> Result<&UdpSocket, SidecarError> {
2952        self.socket
2953            .as_ref()
2954            .ok_or_else(|| SidecarError::Execution(String::from("EBADF: bad file descriptor")))
2955    }
2956
2957    fn bind(
2958        &mut self,
2959        kernel: &mut SidecarKernel,
2960        kernel_pid: u32,
2961        host: Option<&str>,
2962        port: u16,
2963        context: &JavascriptSocketPathContext,
2964    ) -> Result<SocketAddr, SidecarError> {
2965        if self.socket.is_some() || self.guest_local_addr.is_some() {
2966            return Err(SidecarError::Execution(String::from(
2967                "EINVAL: secure-exec dgram socket is already bound",
2968            )));
2969        }
2970
2971        let (bind_host, guest_host, guest_family) = normalize_udp_bind_host(host, self.family)?;
2972        let guest_port = allocate_guest_listen_port(
2973            port,
2974            guest_family,
2975            &context.used_udp_guest_ports,
2976            context.listen_policy,
2977        )?;
2978        let local_addr = resolve_udp_bind_addr(guest_host, guest_port, self.family)?;
2979        if let Some(socket_id) = self.kernel_socket_id {
2980            kernel
2981                .socket_bind_inet(
2982                    EXECUTION_DRIVER_NAME,
2983                    kernel_pid,
2984                    socket_id,
2985                    InetSocketAddress::new(local_addr.ip().to_string(), local_addr.port()),
2986                )
2987                .map_err(kernel_error)?;
2988        } else {
2989            let bind_addr = resolve_udp_bind_addr(bind_host, 0, self.family)?;
2990            let socket = UdpSocket::bind(bind_addr).map_err(sidecar_net_error)?;
2991            socket.set_nonblocking(true).map_err(sidecar_net_error)?;
2992            self.socket = Some(socket);
2993        }
2994        self.guest_local_addr = Some(local_addr);
2995        Ok(local_addr)
2996    }
2997
2998    fn ensure_bound_for_send(
2999        &mut self,
3000        kernel: &mut SidecarKernel,
3001        kernel_pid: u32,
3002        context: &JavascriptSocketPathContext,
3003    ) -> Result<SocketAddr, SidecarError> {
3004        if let Some(local_addr) = self.local_addr() {
3005            return Ok(local_addr);
3006        }
3007
3008        self.bind(kernel, kernel_pid, None, 0, context)
3009    }
3010
3011    fn send_to<B>(
3012        &mut self,
3013        request: ActiveUdpSendToRequest<'_, B>,
3014    ) -> Result<(usize, SocketAddr), SidecarError>
3015    where
3016        B: NativeSidecarBridge + Send + 'static,
3017        BridgeError<B>: fmt::Debug + Send + Sync + 'static,
3018    {
3019        let ActiveUdpSendToRequest {
3020            bridge,
3021            kernel,
3022            kernel_pid,
3023            vm_id,
3024            dns,
3025            host,
3026            port,
3027            context,
3028            contents,
3029        } = request;
3030        let remote_addr = resolve_udp_addr(UdpRemoteAddrRequest {
3031            bridge,
3032            kernel,
3033            vm_id,
3034            dns,
3035            host,
3036            port,
3037            family: self.family,
3038            context,
3039        })?;
3040        let local_addr = self.ensure_bound_for_send(kernel, kernel_pid, context)?;
3041        let written = if let Some(socket_id) = self.kernel_socket_id {
3042            if is_loopback_ip(remote_addr.ip()) && remote_addr.port() == port {
3043                kernel
3044                    .socket_send_to_inet_loopback(
3045                        EXECUTION_DRIVER_NAME,
3046                        kernel_pid,
3047                        socket_id,
3048                        InetSocketAddress::new(remote_addr.ip().to_string(), remote_addr.port()),
3049                        contents,
3050                    )
3051                    .map_err(kernel_error)?
3052            } else {
3053                return Err(SidecarError::Execution(String::from(
3054                    "ERR_NOT_IMPLEMENTED: external UDP datagrams are not yet supported by the kernel-backed V8 bridge",
3055                )));
3056            }
3057        } else {
3058            let socket = self.socket.as_ref().ok_or_else(|| {
3059                SidecarError::InvalidState(String::from("UDP socket is not initialized"))
3060            })?;
3061            socket
3062                .send_to(contents, remote_addr)
3063                .map_err(sidecar_net_error)?
3064        };
3065        Ok((written, local_addr))
3066    }
3067
3068    fn poll(
3069        &self,
3070        kernel: &mut SidecarKernel,
3071        kernel_pid: u32,
3072        wait: Duration,
3073    ) -> Result<Option<JavascriptUdpSocketEvent>, SidecarError> {
3074        if let Some(socket_id) = self.kernel_socket_id {
3075            let result = kernel
3076                .poll_targets(
3077                    EXECUTION_DRIVER_NAME,
3078                    kernel_pid,
3079                    vec![PollTargetEntry::socket(socket_id, POLLIN)],
3080                    i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
3081                )
3082                .map_err(kernel_error)?;
3083            let revents = result
3084                .targets
3085                .first()
3086                .map(|entry| entry.revents)
3087                .unwrap_or_else(PollEvents::empty);
3088            if revents.is_empty() {
3089                return Ok(None);
3090            }
3091            return match kernel.socket_recv_datagram(
3092                EXECUTION_DRIVER_NAME,
3093                kernel_pid,
3094                socket_id,
3095                64 * 1024,
3096            ) {
3097                Ok(Some(datagram)) => {
3098                    let (source_address, payload) = datagram.into_parts();
3099                    let remote_addr = source_address
3100                        .map(|source| {
3101                            resolve_udp_bind_addr(source.host(), source.port(), self.family)
3102                        })
3103                        .transpose()?
3104                        .unwrap_or_else(|| match self.family {
3105                            JavascriptUdpFamily::Ipv4 => {
3106                                SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)
3107                            }
3108                            JavascriptUdpFamily::Ipv6 => {
3109                                SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0)
3110                            }
3111                        });
3112                    Ok(Some(JavascriptUdpSocketEvent::Message {
3113                        data: payload,
3114                        remote_addr,
3115                    }))
3116                }
3117                Ok(None) => Ok(None),
3118                Err(error) if error.code() == "EAGAIN" => Ok(None),
3119                Err(error) => Ok(Some(JavascriptUdpSocketEvent::Error {
3120                    code: Some(error.code().to_string()),
3121                    message: error.to_string(),
3122                })),
3123            };
3124        }
3125        let socket = self.socket()?;
3126        let deadline = Instant::now() + wait;
3127        let mut buffer = vec![0_u8; 64 * 1024];
3128
3129        loop {
3130            match socket.recv_from(&mut buffer) {
3131                Ok((bytes_read, remote_addr)) => {
3132                    return Ok(Some(JavascriptUdpSocketEvent::Message {
3133                        data: buffer[..bytes_read].to_vec(),
3134                        remote_addr,
3135                    }));
3136                }
3137                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
3138                    if wait.is_zero() || Instant::now() >= deadline {
3139                        return Ok(None);
3140                    }
3141                    if !wait_fd_readable_until(socket.as_fd(), deadline) {
3142                        return Ok(None);
3143                    }
3144                }
3145                Err(error) => {
3146                    return Ok(Some(JavascriptUdpSocketEvent::Error {
3147                        code: io_error_code(&error),
3148                        message: error.to_string(),
3149                    }));
3150                }
3151            }
3152        }
3153    }
3154
3155    fn close(&mut self, kernel: &mut SidecarKernel, kernel_pid: u32) {
3156        if let Some(socket_id) = self.kernel_socket_id {
3157            let _ = close_kernel_socket_idempotent(kernel, kernel_pid, socket_id);
3158        }
3159        self.socket.take();
3160        self.guest_local_addr = None;
3161    }
3162
3163    fn set_buffer_size(&mut self, which: &str, size: usize) -> Result<(), SidecarError> {
3164        match which {
3165            "recv" => self.recv_buffer_size = size,
3166            "send" => self.send_buffer_size = size,
3167            other => {
3168                return Err(SidecarError::InvalidState(format!(
3169                    "unsupported UDP buffer size kind {other}"
3170                )));
3171            }
3172        }
3173        if self.kernel_socket_id.is_some() {
3174            return Ok(());
3175        }
3176        let socket = self.socket()?;
3177        let socket = SockRef::from(socket);
3178        match which {
3179            "recv" => socket.set_recv_buffer_size(size).map_err(sidecar_net_error),
3180            "send" => socket.set_send_buffer_size(size).map_err(sidecar_net_error),
3181            other => Err(SidecarError::InvalidState(format!(
3182                "unsupported UDP buffer size kind {other}"
3183            ))),
3184        }
3185    }
3186
3187    fn get_buffer_size(&self, which: &str) -> Result<usize, SidecarError> {
3188        if self.kernel_socket_id.is_some() {
3189            return Ok(match which {
3190                "recv" => self.recv_buffer_size,
3191                "send" => self.send_buffer_size,
3192                other => {
3193                    return Err(SidecarError::InvalidState(format!(
3194                        "unsupported UDP buffer size kind {other}"
3195                    )));
3196                }
3197            });
3198        }
3199        let socket = self.socket()?;
3200        let socket = SockRef::from(socket);
3201        match which {
3202            "recv" => socket.recv_buffer_size().map_err(sidecar_net_error),
3203            "send" => socket.send_buffer_size().map_err(sidecar_net_error),
3204            other => Err(SidecarError::InvalidState(format!(
3205                "unsupported UDP buffer size kind {other}"
3206            ))),
3207        }
3208    }
3209}
3210
3211// ActiveExecution, ActiveExecutionEvent, SocketQueryKind moved to crate::state
3212
3213impl ActiveExecution {
3214    pub(crate) fn uses_shared_v8_runtime(&self) -> bool {
3215        match self {
3216            Self::Javascript(execution) => execution.uses_shared_v8_runtime(),
3217            Self::Python(execution) => execution.uses_shared_v8_runtime(),
3218            Self::Wasm(execution) => execution.uses_shared_v8_runtime(),
3219            Self::Tool(_) => false,
3220        }
3221    }
3222
3223    pub(crate) fn child_pid(&self) -> u32 {
3224        match self {
3225            Self::Javascript(execution) => execution.child_pid(),
3226            Self::Python(execution) => execution.child_pid(),
3227            Self::Wasm(execution) => execution.child_pid(),
3228            Self::Tool(_) => 0,
3229        }
3230    }
3231
3232    pub(crate) fn write_stdin(&mut self, chunk: &[u8]) -> Result<(), SidecarError> {
3233        match self {
3234            Self::Javascript(execution) => execution
3235                .write_stdin(chunk)
3236                .map_err(|error| SidecarError::Execution(error.to_string())),
3237            Self::Python(execution) => execution
3238                .write_stdin(chunk)
3239                .map_err(|error| SidecarError::Execution(error.to_string())),
3240            // Sidecar wasm always runs with kernel-managed stdio
3241            // (AGENTOS_WASI_STDIO_SYNC_RPC=1): the guest reads fd 0 via
3242            // `__kernel_stdin_read`, so skip the V8 `stdin` stream event —
3243            // it is never consumed and would flood the session's deferred
3244            // message queue while the guest blocks in a sync read.
3245            Self::Wasm(execution) => execution
3246                .write_stdin_kernel_only(chunk)
3247                .map_err(|error| SidecarError::Execution(error.to_string())),
3248            Self::Tool(_) => Ok(()),
3249        }
3250    }
3251
3252    pub(crate) fn close_stdin(&mut self) -> Result<(), SidecarError> {
3253        match self {
3254            Self::Javascript(execution) => execution
3255                .close_stdin()
3256                .map_err(|error| SidecarError::Execution(error.to_string())),
3257            Self::Python(execution) => execution
3258                .close_stdin()
3259                .map_err(|error| SidecarError::Execution(error.to_string())),
3260            Self::Wasm(execution) => execution
3261                .close_stdin()
3262                .map_err(|error| SidecarError::Execution(error.to_string())),
3263            Self::Tool(_) => Ok(()),
3264        }
3265    }
3266
3267    pub(crate) fn respond_python_vfs_rpc_success(
3268        &mut self,
3269        id: u64,
3270        payload: PythonVfsRpcResponsePayload,
3271    ) -> Result<(), SidecarError> {
3272        match self {
3273            Self::Python(execution) => execution
3274                .respond_vfs_rpc_success(id, payload)
3275                .map_err(|error| SidecarError::Execution(error.to_string())),
3276            _ => Err(SidecarError::InvalidState(String::from(
3277                "only Python executions can service Python VFS RPC responses",
3278            ))),
3279        }
3280    }
3281
3282    pub(crate) fn respond_python_vfs_rpc_error(
3283        &mut self,
3284        id: u64,
3285        code: impl Into<String>,
3286        message: impl Into<String>,
3287    ) -> Result<(), SidecarError> {
3288        match self {
3289            Self::Python(execution) => execution
3290                .respond_vfs_rpc_error(id, code, message)
3291                .map_err(|error| SidecarError::Execution(error.to_string())),
3292            _ => Err(SidecarError::InvalidState(String::from(
3293                "only Python executions can service Python VFS RPC responses",
3294            ))),
3295        }
3296    }
3297
3298    pub(crate) fn send_javascript_stream_event(
3299        &self,
3300        event_type: &str,
3301        payload: Value,
3302    ) -> Result<(), SidecarError> {
3303        match self {
3304            Self::Javascript(execution) => execution
3305                .send_stream_event(event_type, payload)
3306                .map_err(|error| SidecarError::Execution(error.to_string())),
3307            Self::Wasm(execution) => execution
3308                .send_stream_event(event_type, payload)
3309                .map_err(|error| SidecarError::Execution(error.to_string())),
3310            _ => Err(SidecarError::InvalidState(String::from(
3311                "only embedded V8 executions can receive JavaScript stream events",
3312            ))),
3313        }
3314    }
3315
3316    pub(crate) fn javascript_v8_session_handle(&self) -> Option<V8SessionHandle> {
3317        match self {
3318            Self::Javascript(execution) => Some(execution.v8_session_handle()),
3319            Self::Wasm(execution) => Some(execution.v8_session_handle()),
3320            _ => None,
3321        }
3322    }
3323
3324    pub(crate) fn terminate(&mut self) -> Result<(), SidecarError> {
3325        match self {
3326            Self::Javascript(execution) => execution
3327                .terminate()
3328                .map_err(|error| SidecarError::Execution(error.to_string())),
3329            Self::Python(execution) => execution
3330                .kill()
3331                .map_err(|error| SidecarError::Execution(error.to_string())),
3332            Self::Wasm(execution) => execution
3333                .terminate()
3334                .map_err(|error| SidecarError::Execution(error.to_string())),
3335            Self::Tool(_) => Ok(()),
3336        }
3337    }
3338
3339    pub(crate) fn respond_javascript_sync_rpc_success(
3340        &mut self,
3341        id: u64,
3342        result: Value,
3343    ) -> Result<(), SidecarError> {
3344        match self {
3345            Self::Javascript(execution) => execution
3346                .respond_sync_rpc_success(id, result)
3347                .map_err(|error| SidecarError::Execution(error.to_string())),
3348            Self::Python(execution) => execution
3349                .respond_javascript_sync_rpc_success(id, result)
3350                .map_err(|error| SidecarError::Execution(error.to_string())),
3351            Self::Wasm(execution) => execution
3352                .respond_sync_rpc_success(id, result)
3353                .map_err(|error| SidecarError::Execution(error.to_string())),
3354            _ => Err(SidecarError::InvalidState(String::from(
3355                "only JavaScript, Python, and WebAssembly executions can service JavaScript sync RPC responses",
3356            ))),
3357        }
3358    }
3359
3360    pub(crate) fn respond_javascript_sync_rpc_raw_success(
3361        &mut self,
3362        id: u64,
3363        payload: Vec<u8>,
3364    ) -> Result<(), SidecarError> {
3365        match self {
3366            Self::Javascript(execution) => execution
3367                .respond_sync_rpc_raw_success(id, payload)
3368                .map_err(|error| SidecarError::Execution(error.to_string())),
3369            Self::Wasm(execution) => execution
3370                .respond_sync_rpc_raw_success(id, payload)
3371                .map_err(|error| SidecarError::Execution(error.to_string())),
3372            _ => Err(SidecarError::InvalidState(String::from(
3373                "only embedded V8 executions can service raw JavaScript sync RPC responses",
3374            ))),
3375        }
3376    }
3377
3378    pub(crate) fn respond_javascript_sync_rpc_response(
3379        &mut self,
3380        id: u64,
3381        response: JavascriptSyncRpcServiceResponse,
3382    ) -> Result<(), SidecarError> {
3383        match response {
3384            JavascriptSyncRpcServiceResponse::Json(result) => {
3385                self.respond_javascript_sync_rpc_success(id, result)
3386            }
3387            JavascriptSyncRpcServiceResponse::Raw(payload) => {
3388                self.respond_javascript_sync_rpc_raw_success(id, payload)
3389            }
3390        }
3391    }
3392
3393    pub(crate) fn respond_javascript_sync_rpc_error(
3394        &mut self,
3395        id: u64,
3396        code: impl Into<String>,
3397        message: impl Into<String>,
3398    ) -> Result<(), SidecarError> {
3399        match self {
3400            Self::Javascript(execution) => execution
3401                .respond_sync_rpc_error(id, code, message)
3402                .map_err(|error| SidecarError::Execution(error.to_string())),
3403            Self::Python(execution) => execution
3404                .respond_javascript_sync_rpc_error(id, code, message)
3405                .map_err(|error| SidecarError::Execution(error.to_string())),
3406            Self::Wasm(execution) => execution
3407                .respond_sync_rpc_error(id, code, message)
3408                .map_err(|error| SidecarError::Execution(error.to_string())),
3409            _ => Err(SidecarError::InvalidState(String::from(
3410                "only JavaScript, Python, and WebAssembly executions can service JavaScript sync RPC responses",
3411            ))),
3412        }
3413    }
3414
3415    pub(crate) async fn poll_event(
3416        &mut self,
3417        timeout: Duration,
3418    ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
3419        match self {
3420            Self::Javascript(execution) => execution
3421                .poll_event(timeout)
3422                .await
3423                .map(|event| {
3424                    event.map(|event| match event {
3425                        JavascriptExecutionEvent::Stdout(chunk) => {
3426                            ActiveExecutionEvent::Stdout(chunk)
3427                        }
3428                        JavascriptExecutionEvent::Stderr(chunk) => {
3429                            ActiveExecutionEvent::Stderr(chunk)
3430                        }
3431                        JavascriptExecutionEvent::SyncRpcRequest(request) => {
3432                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
3433                        }
3434                        JavascriptExecutionEvent::SignalState {
3435                            signal,
3436                            registration,
3437                        } => ActiveExecutionEvent::SignalState {
3438                            signal,
3439                            registration: map_node_signal_registration(registration),
3440                        },
3441                        JavascriptExecutionEvent::Exited(code) => {
3442                            ActiveExecutionEvent::Exited(code)
3443                        }
3444                    })
3445                })
3446                .map_err(|error| SidecarError::Execution(error.to_string())),
3447            Self::Python(execution) => execution
3448                .poll_event(timeout)
3449                .await
3450                .map(|event| {
3451                    event.map(|event| match event {
3452                        PythonExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
3453                        PythonExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
3454                        PythonExecutionEvent::JavascriptSyncRpcRequest(request) => {
3455                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
3456                        }
3457                        PythonExecutionEvent::VfsRpcRequest(request) => {
3458                            ActiveExecutionEvent::PythonVfsRpcRequest(request)
3459                        }
3460                        PythonExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
3461                    })
3462                })
3463                .map_err(|error| SidecarError::Execution(error.to_string())),
3464            Self::Wasm(execution) => execution
3465                .poll_event(timeout)
3466                .await
3467                .map(|event| {
3468                    event.map(|event| match event {
3469                        WasmExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
3470                        WasmExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
3471                        WasmExecutionEvent::SyncRpcRequest(request) => {
3472                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
3473                        }
3474                        WasmExecutionEvent::SignalState {
3475                            signal,
3476                            registration,
3477                        } => ActiveExecutionEvent::SignalState {
3478                            signal,
3479                            registration: map_wasm_signal_registration(registration),
3480                        },
3481                        WasmExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
3482                    })
3483                })
3484                .map_err(|error| SidecarError::Execution(error.to_string())),
3485            Self::Tool(execution) => {
3486                let _ = timeout;
3487                poll_tool_process_event(execution)
3488            }
3489        }
3490    }
3491
3492    pub(crate) fn poll_event_blocking(
3493        &mut self,
3494        timeout: Duration,
3495    ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
3496        match self {
3497            Self::Javascript(execution) => execution
3498                .poll_event_blocking(timeout)
3499                .map(|event| {
3500                    event.map(|event| match event {
3501                        JavascriptExecutionEvent::Stdout(chunk) => {
3502                            ActiveExecutionEvent::Stdout(chunk)
3503                        }
3504                        JavascriptExecutionEvent::Stderr(chunk) => {
3505                            ActiveExecutionEvent::Stderr(chunk)
3506                        }
3507                        JavascriptExecutionEvent::SyncRpcRequest(request) => {
3508                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
3509                        }
3510                        JavascriptExecutionEvent::SignalState {
3511                            signal,
3512                            registration,
3513                        } => ActiveExecutionEvent::SignalState {
3514                            signal,
3515                            registration: map_node_signal_registration(registration),
3516                        },
3517                        JavascriptExecutionEvent::Exited(code) => {
3518                            ActiveExecutionEvent::Exited(code)
3519                        }
3520                    })
3521                })
3522                .map_err(|error| SidecarError::Execution(error.to_string())),
3523            Self::Python(execution) => execution
3524                .poll_event_blocking(timeout)
3525                .map(|event| {
3526                    event.map(|event| match event {
3527                        PythonExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
3528                        PythonExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
3529                        PythonExecutionEvent::JavascriptSyncRpcRequest(request) => {
3530                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
3531                        }
3532                        PythonExecutionEvent::VfsRpcRequest(request) => {
3533                            ActiveExecutionEvent::PythonVfsRpcRequest(request)
3534                        }
3535                        PythonExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
3536                    })
3537                })
3538                .map_err(|error| SidecarError::Execution(error.to_string())),
3539            Self::Wasm(execution) => execution
3540                .poll_event_blocking(timeout)
3541                .map(|event| {
3542                    event.map(|event| match event {
3543                        WasmExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
3544                        WasmExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
3545                        WasmExecutionEvent::SyncRpcRequest(request) => {
3546                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
3547                        }
3548                        WasmExecutionEvent::SignalState {
3549                            signal,
3550                            registration,
3551                        } => ActiveExecutionEvent::SignalState {
3552                            signal,
3553                            registration: map_wasm_signal_registration(registration),
3554                        },
3555                        WasmExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
3556                    })
3557                })
3558                .map_err(|error| SidecarError::Execution(error.to_string())),
3559            Self::Tool(execution) => {
3560                let _ = timeout;
3561                poll_tool_process_event(execution)
3562            }
3563        }
3564    }
3565}
3566
3567struct ToolProcessEventRequest {
3568    sidecar_requests: SharedSidecarRequestClient,
3569    connection_id: String,
3570    session_id: String,
3571    vm_id: String,
3572    tool_resolution: ToolCommandResolution,
3573    cancelled: Arc<AtomicBool>,
3574    pending_events: Arc<Mutex<VecDeque<ActiveExecutionEvent>>>,
3575    events_overflowed: Arc<AtomicBool>,
3576}
3577
3578pub(crate) fn send_tool_process_event(
3579    pending_events: &Arc<Mutex<VecDeque<ActiveExecutionEvent>>>,
3580    events_overflowed: &AtomicBool,
3581    event: ActiveExecutionEvent,
3582) -> bool {
3583    let mut pending_events = pending_events
3584        .lock()
3585        .unwrap_or_else(|poisoned| poisoned.into_inner());
3586    if pending_events.len() >= MAX_PROCESS_EVENT_QUEUE {
3587        events_overflowed.store(true, Ordering::Relaxed);
3588        return false;
3589    }
3590    pending_events.push_back(event);
3591    true
3592}
3593
3594fn spawn_tool_process_events(request: ToolProcessEventRequest) {
3595    let ToolProcessEventRequest {
3596        sidecar_requests,
3597        connection_id,
3598        session_id,
3599        vm_id,
3600        tool_resolution,
3601        cancelled,
3602        pending_events,
3603        events_overflowed,
3604    } = request;
3605    std::thread::spawn(move || match tool_resolution {
3606        ToolCommandResolution::Failure(message) => {
3607            if !send_tool_process_event(
3608                &pending_events,
3609                &events_overflowed,
3610                ActiveExecutionEvent::Stderr(format_tool_failure_output(&message)),
3611            ) {
3612                return;
3613            }
3614            let _ = send_tool_process_event(
3615                &pending_events,
3616                &events_overflowed,
3617                ActiveExecutionEvent::Exited(1),
3618            );
3619        }
3620        ToolCommandResolution::Invoke { request, timeout } => {
3621            let response = sidecar_requests.invoke(
3622                OwnershipScope::vm(connection_id.clone(), session_id.clone(), vm_id.clone()),
3623                SidecarRequestPayload::HostCallback(request.clone()),
3624                timeout,
3625            );
3626            if cancelled.load(Ordering::Relaxed) {
3627                return;
3628            }
3629
3630            match response {
3631                Ok(crate::protocol::SidecarResponsePayload::HostCallbackResult(result)) => {
3632                    if let Some(value) = result.result {
3633                        let value: serde_json::Value = serde_json::from_str(&value)
3634                            .unwrap_or(serde_json::Value::String(value));
3635                        let stdout = serde_json::to_vec(&json!({
3636                            "ok": true,
3637                            "result": value,
3638                        }))
3639                        .unwrap_or_else(|error| {
3640                            format_tool_failure_output(&format!(
3641                                "failed to serialize tool result: {error}"
3642                            ))
3643                        });
3644                        if !send_tool_process_event(
3645                            &pending_events,
3646                            &events_overflowed,
3647                            ActiveExecutionEvent::Stdout(stdout),
3648                        ) {
3649                            return;
3650                        }
3651                        let _ = send_tool_process_event(
3652                            &pending_events,
3653                            &events_overflowed,
3654                            ActiveExecutionEvent::Exited(0),
3655                        );
3656                    } else {
3657                        let message = result
3658                            .error
3659                            .unwrap_or_else(|| String::from("tool invocation returned no result"));
3660                        if !send_tool_process_event(
3661                            &pending_events,
3662                            &events_overflowed,
3663                            ActiveExecutionEvent::Stderr(format_tool_failure_output(&message)),
3664                        ) {
3665                            return;
3666                        }
3667                        let _ = send_tool_process_event(
3668                            &pending_events,
3669                            &events_overflowed,
3670                            ActiveExecutionEvent::Exited(1),
3671                        );
3672                    }
3673                }
3674                Ok(_) => {
3675                    if !send_tool_process_event(
3676                        &pending_events,
3677                        &events_overflowed,
3678                        ActiveExecutionEvent::Stderr(format_tool_failure_output(
3679                            "unexpected sidecar tool response",
3680                        )),
3681                    ) {
3682                        return;
3683                    }
3684                    let _ = send_tool_process_event(
3685                        &pending_events,
3686                        &events_overflowed,
3687                        ActiveExecutionEvent::Exited(1),
3688                    );
3689                }
3690                Err(error) => {
3691                    if !send_tool_process_event(
3692                        &pending_events,
3693                        &events_overflowed,
3694                        ActiveExecutionEvent::Stderr(format_tool_failure_output(
3695                            &error.to_string(),
3696                        )),
3697                    ) {
3698                        return;
3699                    }
3700                    let _ = send_tool_process_event(
3701                        &pending_events,
3702                        &events_overflowed,
3703                        ActiveExecutionEvent::Exited(1),
3704                    );
3705                }
3706            }
3707        }
3708    });
3709}
3710
3711impl<B> NativeSidecar<B>
3712where
3713    B: NativeSidecarBridge + Send + 'static,
3714    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
3715{
3716    pub(crate) async fn execute(
3717        &mut self,
3718        request: &RequestFrame,
3719        payload: ExecuteRequest,
3720    ) -> Result<DispatchResult, SidecarError> {
3721        let execute_total_start = Instant::now();
3722        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3723        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3724
3725        let vm = self
3726            .vms
3727            .get_mut(&vm_id)
3728            .ok_or_else(|| missing_vm_error(&vm_id))?;
3729        if vm.active_processes.contains_key(&payload.process_id) {
3730            return Err(SidecarError::InvalidState(format!(
3731                "VM {vm_id} already has an active process with id {}",
3732                payload.process_id
3733            )));
3734        }
3735
3736        if let Some(command) = payload.command.as_deref() {
3737            if let Some(tool_resolution) =
3738                resolve_tool_command(vm, command, &payload.args, payload.cwd.as_deref())?
3739            {
3740                let guest_cwd = payload
3741                    .cwd
3742                    .as_deref()
3743                    .map(normalize_path)
3744                    .unwrap_or_else(|| vm.guest_cwd.clone());
3745                let kernel_handle = vm
3746                    .kernel
3747                    .create_virtual_process(
3748                        EXECUTION_DRIVER_NAME,
3749                        TOOL_DRIVER_NAME,
3750                        command,
3751                        std::iter::once(command.to_owned())
3752                            .chain(payload.args.iter().cloned())
3753                            .collect(),
3754                        VirtualProcessOptions {
3755                            env: vm.guest_env.clone(),
3756                            cwd: Some(guest_cwd.clone()),
3757                            ..VirtualProcessOptions::default()
3758                        },
3759                    )
3760                    .map_err(kernel_error)?;
3761                let kernel_pid = kernel_handle.pid();
3762                let tool_execution = ToolExecution::default();
3763                let cancelled = tool_execution.cancelled.clone();
3764                let pending_events = tool_execution.pending_events.clone();
3765                let events_overflowed = tool_execution.events_overflowed.clone();
3766                vm.active_processes.insert(
3767                    payload.process_id.clone(),
3768                    ActiveProcess::new(
3769                        kernel_pid,
3770                        kernel_handle,
3771                        GuestRuntimeKind::JavaScript,
3772                        ActiveExecution::Tool(tool_execution),
3773                    )
3774                    .with_guest_cwd(guest_cwd.clone())
3775                    .with_host_cwd(resolve_vm_guest_path_to_host(vm, &guest_cwd)),
3776                );
3777                self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy)?;
3778                spawn_tool_process_events(ToolProcessEventRequest {
3779                    sidecar_requests: self.sidecar_requests.clone(),
3780                    connection_id: connection_id.clone(),
3781                    session_id: session_id.clone(),
3782                    vm_id: vm_id.clone(),
3783                    tool_resolution,
3784                    cancelled,
3785                    pending_events,
3786                    events_overflowed,
3787                });
3788
3789                return Ok(DispatchResult {
3790                    response: process_started_response(
3791                        request,
3792                        payload.process_id,
3793                        Some(kernel_pid),
3794                    ),
3795                    events: Vec::new(),
3796                });
3797            }
3798        }
3799
3800        let requested_tty = payload
3801            .env
3802            .get(EXECUTION_REQUEST_TTY_ENV)
3803            .is_some_and(|value| value == "1" || value.eq_ignore_ascii_case("true"));
3804        let phase_start = Instant::now();
3805        let mut resolved = resolve_execute_request(vm, &payload)?;
3806        stage_agentos_package_command(vm, &mut resolved)?;
3807        let resolved = resolved;
3808        record_execute_phase("resolve_execute_request", phase_start.elapsed());
3809        let phase_start = Instant::now();
3810        let mut env = resolved.env.clone();
3811        env.remove(EXECUTION_REQUEST_TTY_ENV);
3812        let sandbox_root = normalize_host_path(&vm.cwd);
3813        env.insert(
3814            String::from(EXECUTION_SANDBOX_ROOT_ENV),
3815            sandbox_root.to_string_lossy().into_owned(),
3816        );
3817        if resolved.runtime == GuestRuntimeKind::JavaScript {
3818            env.insert(String::from("AGENTOS_KEEP_STDIN_OPEN"), String::from("1"));
3819            // A TTY guest-node process reads stdin through the kernel PTY: host
3820            // input is written to the PTY master (write_kernel_process_stdin),
3821            // line discipline runs (echo / VERASE / ICRNL / VEOF), and the
3822            // sidecar drains the cooked bytes from the slave and forwards them
3823            // to the isolate's stream-stdin dispatch
3824            // (forward_tty_slave_input_to_javascript). The in-isolate
3825            // `_kernelStdinRead` bridge stays local; no RPC forwarding is
3826            // needed because the isolate never reads kernel fd 0 itself.
3827        } else if resolved.runtime == GuestRuntimeKind::WebAssembly {
3828            env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
3829        }
3830        let launch_entrypoint = if resolved.runtime == GuestRuntimeKind::JavaScript {
3831            resolve_agentos_package_javascript_launch_entrypoint(vm, &mut env)
3832                .unwrap_or_else(|| resolved.entrypoint.clone())
3833        } else {
3834            resolved.entrypoint.clone()
3835        };
3836        let argv = std::iter::once(launch_entrypoint.clone())
3837            .chain(resolved.execution_args.iter().cloned())
3838            .collect::<Vec<_>>();
3839        record_execute_phase("env_argv_setup", phase_start.elapsed());
3840        let phase_start = Instant::now();
3841        let kernel_handle = vm
3842            .kernel
3843            .spawn_process(
3844                &resolved.command,
3845                argv,
3846                SpawnOptions {
3847                    requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
3848                    cwd: Some(resolved.guest_cwd.clone()),
3849                    ..SpawnOptions::default()
3850                },
3851            )
3852            .map_err(kernel_error)?;
3853        let kernel_pid = kernel_handle.pid();
3854        record_execute_phase("kernel_spawn_process", phase_start.elapsed());
3855        let tty_master_fd = if requested_tty {
3856            let (master_fd, slave_fd, _) = vm
3857                .kernel
3858                .open_pty(EXECUTION_DRIVER_NAME, kernel_pid)
3859                .map_err(kernel_error)?;
3860            vm.kernel
3861                .fd_dup2(EXECUTION_DRIVER_NAME, kernel_pid, slave_fd, 0)
3862                .map_err(kernel_error)?;
3863            vm.kernel
3864                .fd_dup2(EXECUTION_DRIVER_NAME, kernel_pid, slave_fd, 1)
3865                .map_err(kernel_error)?;
3866            vm.kernel
3867                .fd_dup2(EXECUTION_DRIVER_NAME, kernel_pid, slave_fd, 2)
3868                .map_err(kernel_error)?;
3869            vm.kernel
3870                .pty_set_foreground_pgid(EXECUTION_DRIVER_NAME, kernel_pid, master_fd, kernel_pid)
3871                .map_err(kernel_error)?;
3872            if let Some((cols, rows)) = requested_pty_window_size(&env) {
3873                vm.kernel
3874                    .pty_resize(EXECUTION_DRIVER_NAME, kernel_pid, master_fd, cols, rows)
3875                    .map_err(kernel_error)?;
3876            }
3877            Some(master_fd)
3878        } else {
3879            None
3880        };
3881
3882        let (execution, process_env) = match resolved.runtime {
3883            GuestRuntimeKind::JavaScript => {
3884                let phase_start = Instant::now();
3885                let inline_code = load_javascript_entrypoint_source(
3886                    vm,
3887                    &resolved.host_cwd,
3888                    &launch_entrypoint,
3889                    &env,
3890                );
3891                record_execute_phase("js_load_entrypoint_source", phase_start.elapsed());
3892                let phase_start = Instant::now();
3893                prepare_javascript_shadow(vm, &resolved, &env)?;
3894                record_execute_phase("js_prepare_shadow", phase_start.elapsed());
3895
3896                let phase_start = Instant::now();
3897                let context =
3898                    self.javascript_engine
3899                        .create_context(CreateJavascriptContextRequest {
3900                            vm_id: vm_id.clone(),
3901                            bootstrap_module: None,
3902                            compile_cache_root: Some(self.cache_root.join("node-compile-cache")),
3903                        });
3904                record_execute_phase("js_create_context", phase_start.elapsed());
3905                let phase_start = Instant::now();
3906                let built_reader = build_module_reader(vm, &resolved);
3907                let guest_reader = built_reader.clone().map(|reader| {
3908                    Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader))
3909                        as Box<dyn GuestModuleReader>
3910                });
3911                let module_reader =
3912                    built_reader.map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
3913                record_execute_phase("js_build_module_reader", phase_start.elapsed());
3914                let phase_start = Instant::now();
3915                let execution = self
3916                    .javascript_engine
3917                    .start_execution_with_module_reader(
3918                        StartJavascriptExecutionRequest {
3919                            guest_runtime: guest_runtime_identity(vm, None, None),
3920                            vm_id: vm_id.clone(),
3921                            context_id: context.context_id,
3922                            argv: std::iter::once(launch_entrypoint.clone())
3923                                .chain(resolved.execution_args.iter().cloned())
3924                                .collect(),
3925                            env: env.clone(),
3926                            cwd: resolved.host_cwd.clone(),
3927                            limits: javascript_execution_limits(vm),
3928                            inline_code,
3929                            wasm_module_bytes: None,
3930                        },
3931                        module_reader,
3932                        guest_reader,
3933                    )
3934                    .map_err(javascript_error)?;
3935                record_execute_phase("js_start_execution", phase_start.elapsed());
3936                (ActiveExecution::Javascript(execution), env.clone())
3937            }
3938            GuestRuntimeKind::Python => {
3939                // The `python` command path (marked by AGENTOS_PYTHON_ARGV) is
3940                // explicit about file mode via AGENTOS_PYTHON_FILE, so a `-c` code
3941                // string that happens to end in `.py` is never mistaken for a path.
3942                // The low-level execute API keeps the `.py`-suffix heuristic.
3943                let python_file_path = if resolved.env.contains_key("AGENTOS_PYTHON_ARGV") {
3944                    resolved.env.get("AGENTOS_PYTHON_FILE").map(PathBuf::from)
3945                } else {
3946                    python_file_entrypoint(&resolved.entrypoint)
3947                };
3948                let pyodide_dist_path = self
3949                    .python_engine
3950                    .bundled_pyodide_dist_path_for_vm(&vm_id)
3951                    .map_err(python_error)?;
3952                let pyodide_cache_path = pyodide_dist_path
3953                    .parent()
3954                    .and_then(Path::parent)
3955                    .unwrap_or(pyodide_dist_path.as_path())
3956                    .join("pyodide-package-cache");
3957                add_runtime_guest_path_mapping(
3958                    &mut env,
3959                    PYTHON_PYODIDE_GUEST_ROOT,
3960                    &pyodide_dist_path,
3961                );
3962                add_runtime_guest_path_mapping(
3963                    &mut env,
3964                    PYTHON_PYODIDE_CACHE_GUEST_ROOT,
3965                    &pyodide_cache_path,
3966                );
3967                add_runtime_host_access_path(
3968                    &mut env,
3969                    "AGENTOS_EXTRA_FS_READ_PATHS",
3970                    &pyodide_dist_path,
3971                    true,
3972                );
3973                add_runtime_host_access_path(
3974                    &mut env,
3975                    "AGENTOS_EXTRA_FS_READ_PATHS",
3976                    &pyodide_cache_path,
3977                    true,
3978                );
3979                add_runtime_host_access_path(
3980                    &mut env,
3981                    "AGENTOS_EXTRA_FS_WRITE_PATHS",
3982                    &pyodide_cache_path,
3983                    false,
3984                );
3985                let context = self
3986                    .python_engine
3987                    .create_context(CreatePythonContextRequest {
3988                        vm_id: vm_id.clone(),
3989                        pyodide_dist_path,
3990                    });
3991                let execution = self
3992                    .python_engine
3993                    .start_execution(StartPythonExecutionRequest {
3994                        vm_id: vm_id.clone(),
3995                        context_id: context.context_id,
3996                        code: resolved.entrypoint.clone(),
3997                        file_path: python_file_path,
3998                        env: env.clone(),
3999                        cwd: resolved.host_cwd.clone(),
4000                        limits: python_execution_limits(vm),
4001                        guest_runtime: guest_runtime_identity(vm, None, None),
4002                    })
4003                    .map_err(python_error)?;
4004                (ActiveExecution::Python(execution), env.clone())
4005            }
4006            GuestRuntimeKind::WebAssembly => {
4007                let wasm_limits = wasm_execution_limits(vm);
4008                let wasm_guest_runtime =
4009                    guest_runtime_identity(vm, Some(u64::from(kernel_pid)), Some(0));
4010                let wasm_permission_tier = resolved.wasm_permission_tier.unwrap_or_else(|| {
4011                    resolve_wasm_permission_tier(
4012                        vm,
4013                        Some(&resolved.command),
4014                        None,
4015                        &resolved.entrypoint,
4016                    )
4017                });
4018                let context = self.wasm_engine.create_context(CreateWasmContextRequest {
4019                    vm_id: vm_id.clone(),
4020                    module_path: Some(resolved.entrypoint.clone()),
4021                });
4022                let execution = self
4023                    .wasm_engine
4024                    .start_execution(StartWasmExecutionRequest {
4025                        vm_id: vm_id.clone(),
4026                        context_id: context.context_id,
4027                        argv: resolved.process_args.clone(),
4028                        env: env.clone(),
4029                        cwd: resolved.host_cwd.clone(),
4030                        permission_tier: execution_wasm_permission_tier(wasm_permission_tier),
4031                        limits: wasm_limits,
4032                        guest_runtime: wasm_guest_runtime,
4033                    })
4034                    .map_err(wasm_error)?;
4035                (ActiveExecution::Wasm(Box::new(execution)), env)
4036            }
4037        };
4038        let child_pid = execution.child_pid();
4039        let phase_start = Instant::now();
4040        let kernel_stdin_writer_fd = if let Some(master_fd) = tty_master_fd {
4041            master_fd
4042        } else {
4043            install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?
4044        };
4045        vm.active_processes.insert(
4046            payload.process_id.clone(),
4047            ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
4048                .with_kernel_stdin_writer_fd(kernel_stdin_writer_fd)
4049                .with_tty_master_fd(tty_master_fd)
4050                .with_guest_cwd(resolved.guest_cwd.clone())
4051                .with_env(process_env)
4052                .with_host_cwd(resolved.host_cwd.clone()),
4053        );
4054        self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy)?;
4055        mark_execute_response_ready(&vm_id, &payload.process_id);
4056        record_execute_phase("process_register_and_lifecycle", phase_start.elapsed());
4057        record_execute_phase("execute_total", execute_total_start.elapsed());
4058
4059        Ok(DispatchResult {
4060            response: process_started_response(
4061                request,
4062                payload.process_id,
4063                Some(if child_pid == 0 {
4064                    kernel_pid
4065                } else {
4066                    child_pid
4067                }),
4068            ),
4069            events: Vec::new(),
4070        })
4071    }
4072
4073    pub(crate) async fn resize_pty(
4074        &mut self,
4075        request: &RequestFrame,
4076        payload: ResizePtyRequest,
4077    ) -> Result<DispatchResult, SidecarError> {
4078        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4079        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4080
4081        let vm = self
4082            .vms
4083            .get_mut(&vm_id)
4084            .ok_or_else(|| missing_vm_error(&vm_id))?;
4085        let process = vm
4086            .active_processes
4087            .get_mut(&payload.process_id)
4088            .ok_or_else(|| {
4089                SidecarError::InvalidState(format!(
4090                    "VM {vm_id} has no active process {}",
4091                    payload.process_id
4092                ))
4093            })?;
4094        let Some(writer_fd) = process.kernel_stdin_writer_fd else {
4095            return Err(SidecarError::InvalidState(format!(
4096                "process {} does not have a PTY",
4097                payload.process_id
4098            )));
4099        };
4100        vm.kernel
4101            .pty_resize(
4102                EXECUTION_DRIVER_NAME,
4103                process.kernel_pid,
4104                writer_fd,
4105                payload.cols,
4106                payload.rows,
4107            )
4108            .map_err(kernel_error)?;
4109
4110        Ok(DispatchResult {
4111            response: self.respond(
4112                request,
4113                ResponsePayload::PtyResized(PtyResizedResponse {
4114                    process_id: payload.process_id,
4115                    cols: payload.cols,
4116                    rows: payload.rows,
4117                }),
4118            ),
4119            events: Vec::new(),
4120        })
4121    }
4122
4123    pub(crate) async fn write_stdin(
4124        &mut self,
4125        request: &RequestFrame,
4126        payload: WriteStdinRequest,
4127    ) -> Result<DispatchResult, SidecarError> {
4128        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4129        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4130
4131        let vm = self
4132            .vms
4133            .get_mut(&vm_id)
4134            .ok_or_else(|| missing_vm_error(&vm_id))?;
4135        let process = vm
4136            .active_processes
4137            .get_mut(&payload.process_id)
4138            .ok_or_else(|| {
4139                SidecarError::InvalidState(format!(
4140                    "VM {vm_id} has no active process {}",
4141                    payload.process_id
4142                ))
4143            })?;
4144        // For a TTY JavaScript process, host stdin must go ONLY to the kernel PTY
4145        // master (so line discipline + echo apply); feeding the in-process local
4146        // stdin bridge as well would double-deliver the input. Non-TTY JS (piped
4147        // stdin) still uses the local bridge; wasm/python always take the
4148        // streaming/no-op `write_stdin` path plus the kernel master write below.
4149        let tty_js =
4150            process.runtime == GuestRuntimeKind::JavaScript && process.tty_master_fd.is_some();
4151        if !tty_js {
4152            process.execution.write_stdin(&payload.chunk)?;
4153        }
4154        write_kernel_process_stdin(&mut vm.kernel, process, &payload.chunk)?;
4155
4156        Ok(DispatchResult {
4157            response: stdin_written_response(
4158                request,
4159                payload.process_id,
4160                payload.chunk.len() as u64,
4161            ),
4162            events: Vec::new(),
4163        })
4164    }
4165
4166    pub(crate) async fn close_stdin(
4167        &mut self,
4168        request: &RequestFrame,
4169        payload: CloseStdinRequest,
4170    ) -> Result<DispatchResult, SidecarError> {
4171        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4172        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4173
4174        let vm = self
4175            .vms
4176            .get_mut(&vm_id)
4177            .ok_or_else(|| missing_vm_error(&vm_id))?;
4178        let process = vm
4179            .active_processes
4180            .get_mut(&payload.process_id)
4181            .ok_or_else(|| {
4182                SidecarError::InvalidState(format!(
4183                    "VM {vm_id} has no active process {}",
4184                    payload.process_id
4185                ))
4186            })?;
4187        process.execution.close_stdin()?;
4188        close_kernel_process_stdin(&mut vm.kernel, process)?;
4189
4190        Ok(DispatchResult {
4191            response: stdin_closed_response(request, payload.process_id),
4192            events: Vec::new(),
4193        })
4194    }
4195
4196    pub(crate) async fn kill_process(
4197        &mut self,
4198        request: &RequestFrame,
4199        payload: KillProcessRequest,
4200    ) -> Result<DispatchResult, SidecarError> {
4201        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4202        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4203        self.kill_process_internal(&vm_id, &payload.process_id, &payload.signal)?;
4204
4205        Ok(DispatchResult {
4206            response: process_killed_response(request, payload.process_id),
4207            events: Vec::new(),
4208        })
4209    }
4210
4211    pub(crate) async fn find_listener(
4212        &mut self,
4213        request: &RequestFrame,
4214        payload: FindListenerRequest,
4215    ) -> Result<DispatchResult, SidecarError> {
4216        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4217        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4218        require_vm_inspection_permission(
4219            &self.bridge,
4220            &vm_id,
4221            "network.inspect",
4222            "network",
4223            &socket_query_resource(SocketQueryKind::TcpListener, &payload),
4224        )?;
4225
4226        let listener =
4227            find_socket_state_entry(self.vms.get(&vm_id), SocketQueryKind::TcpListener, &payload)?;
4228
4229        Ok(DispatchResult {
4230            response: listener_snapshot_response(request, listener),
4231            events: Vec::new(),
4232        })
4233    }
4234
4235    pub(crate) async fn get_process_snapshot(
4236        &mut self,
4237        request: &RequestFrame,
4238        _payload: GetProcessSnapshotRequest,
4239    ) -> Result<DispatchResult, SidecarError> {
4240        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4241        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4242        require_vm_inspection_permission(
4243            &self.bridge,
4244            &vm_id,
4245            "process.inspect",
4246            "process",
4247            "process://snapshot",
4248        )?;
4249
4250        let processes = self
4251            .vms
4252            .get_mut(&vm_id)
4253            .map(|vm| {
4254                prune_exited_process_snapshots(vm);
4255                snapshot_vm_processes(vm)
4256            })
4257            .unwrap_or_default();
4258
4259        Ok(DispatchResult {
4260            response: process_snapshot_response(request, processes),
4261            events: Vec::new(),
4262        })
4263    }
4264
4265    pub(crate) async fn guest_kernel_call(
4266        &mut self,
4267        request: &RequestFrame,
4268        payload: GuestKernelCallRequest,
4269    ) -> Result<DispatchResult, SidecarError> {
4270        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4271        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4272
4273        let vm = self.vms.get_mut(&vm_id).ok_or_else(|| {
4274            SidecarError::InvalidState(format!("VM {vm_id} no longer exists for guest kernel call"))
4275        })?;
4276        let kernel_pid = vm
4277            .active_processes
4278            .get(&payload.execution_id)
4279            .map(|process| process.kernel_pid)
4280            .ok_or_else(|| {
4281                SidecarError::InvalidState(format!(
4282                    "VM {vm_id} has no active process {} for guest kernel call",
4283                    payload.execution_id
4284                ))
4285            })?;
4286
4287        let response = agentos_native_sidecar_core::handle_guest_kernel_call(
4288            &mut vm.kernel,
4289            kernel_pid,
4290            EXECUTION_DRIVER_NAME,
4291            &payload.operation,
4292            &payload.payload,
4293        )
4294        .map_err(guest_kernel_core_error)?;
4295
4296        Ok(DispatchResult {
4297            response: self.respond(
4298                request,
4299                ResponsePayload::GuestKernelResult(GuestKernelResultResponse { payload: response }),
4300            ),
4301            events: Vec::new(),
4302        })
4303    }
4304
4305    pub(crate) async fn get_resource_snapshot(
4306        &mut self,
4307        request: &RequestFrame,
4308        _payload: GetResourceSnapshotRequest,
4309    ) -> Result<DispatchResult, SidecarError> {
4310        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4311        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4312        require_vm_inspection_permission(
4313            &self.bridge,
4314            &vm_id,
4315            "process.inspect",
4316            "process",
4317            "process://resources",
4318        )?;
4319
4320        let snapshot = self
4321            .vms
4322            .get(&vm_id)
4323            .map(|vm| vm.kernel.resource_snapshot())
4324            .unwrap_or_default();
4325        let queue_snapshots = queue_tracker::queue_snapshot()
4326            .into_iter()
4327            .map(|queue| QueueSnapshotEntry {
4328                name: queue.name.as_str().to_owned(),
4329                category: queue.category.as_str().to_owned(),
4330                depth: queue.depth as u64,
4331                high_water: queue.high_water as u64,
4332                capacity: queue.capacity as u64,
4333                fill_percent: queue.fill_percent as u64,
4334            })
4335            .collect();
4336
4337        Ok(DispatchResult {
4338            response: self.respond(
4339                request,
4340                ResponsePayload::ResourceSnapshot(ResourceSnapshotResponse {
4341                    running_processes: snapshot.running_processes as u64,
4342                    exited_processes: snapshot.exited_processes as u64,
4343                    fd_tables: snapshot.fd_tables as u64,
4344                    open_fds: snapshot.open_fds as u64,
4345                    pipes: snapshot.pipes as u64,
4346                    pipe_buffered_bytes: snapshot.pipe_buffered_bytes as u64,
4347                    ptys: snapshot.ptys as u64,
4348                    pty_buffered_input_bytes: snapshot.pty_buffered_input_bytes as u64,
4349                    pty_buffered_output_bytes: snapshot.pty_buffered_output_bytes as u64,
4350                    sockets: snapshot.sockets as u64,
4351                    socket_listeners: snapshot.socket_listeners as u64,
4352                    socket_connections: snapshot.socket_connections as u64,
4353                    socket_buffered_bytes: snapshot.socket_buffered_bytes as u64,
4354                    socket_datagram_queue_len: snapshot.socket_datagram_queue_len as u64,
4355                    queue_snapshots,
4356                }),
4357            ),
4358            events: Vec::new(),
4359        })
4360    }
4361
4362    pub(crate) async fn find_bound_udp(
4363        &mut self,
4364        request: &RequestFrame,
4365        payload: FindBoundUdpRequest,
4366    ) -> Result<DispatchResult, SidecarError> {
4367        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4368        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4369
4370        let lookup_request = FindListenerRequest {
4371            host: payload.host,
4372            port: payload.port,
4373            path: None,
4374        };
4375        require_vm_inspection_permission(
4376            &self.bridge,
4377            &vm_id,
4378            "network.inspect",
4379            "network",
4380            &socket_query_resource(SocketQueryKind::UdpBound, &lookup_request),
4381        )?;
4382        let socket = find_socket_state_entry(
4383            self.vms.get(&vm_id),
4384            SocketQueryKind::UdpBound,
4385            &lookup_request,
4386        )?;
4387
4388        Ok(DispatchResult {
4389            response: bound_udp_snapshot_response(request, socket),
4390            events: Vec::new(),
4391        })
4392    }
4393
4394    pub(crate) async fn vm_fetch(
4395        &mut self,
4396        request: &RequestFrame,
4397        payload: VmFetchRequest,
4398    ) -> Result<DispatchResult, SidecarError> {
4399        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4400        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4401
4402        let vm = self
4403            .vms
4404            .get_mut(&vm_id)
4405            .ok_or_else(|| SidecarError::InvalidState(String::from("unknown sidecar VM")))?;
4406        let target_path = if payload.path.starts_with('/') {
4407            payload.path.clone()
4408        } else {
4409            format!("/{}", payload.path)
4410        };
4411        let request_url = Url::parse(&format!("http://127.0.0.1:{}{target_path}", payload.port))
4412            .map_err(|error| {
4413                SidecarError::InvalidState(format!(
4414                    "invalid vm.fetch target {target_path:?}: {error}"
4415                ))
4416            })?;
4417        let header_values: BTreeMap<String, Value> = serde_json::from_str(&payload.headers_json)
4418            .map_err(|error| {
4419                SidecarError::InvalidState(format!(
4420                    "vm.fetch headers_json must be valid JSON: {error}"
4421                ))
4422            })?;
4423        let options = JavascriptHttpRequestOptions {
4424            method: Some(payload.method),
4425            headers: header_values,
4426            body: payload.body,
4427            reject_unauthorized: None,
4428        };
4429        let headers = parse_http_header_collection(&options.headers, "vm.fetch headers")?;
4430        let target_process_id = find_kernel_http_listener_process(vm, payload.port);
4431        if let Some(target_process_id) = target_process_id {
4432            let max_fetch_response_bytes = vm.limits.http.max_fetch_response_bytes;
4433            let response_json = match dispatch_kernel_http_fetch(
4434                &self.bridge,
4435                &vm_id,
4436                vm,
4437                &target_process_id,
4438                payload.port,
4439                &target_path,
4440                &options,
4441                &headers,
4442                max_fetch_response_bytes,
4443            ) {
4444                Ok(response_json) => response_json,
4445                Err(error) => {
4446                    if let Some(exit_code) = kernel_http_fetch_target_exit_code(&error) {
4447                        let _ = vm;
4448                        self.finish_active_process_exit(&vm_id, &target_process_id, exit_code)?;
4449                    }
4450                    return Err(error);
4451                }
4452            };
4453            let response = self.respond(
4454                request,
4455                ResponsePayload::VmFetchResult(VmFetchResponse { response_json }),
4456            );
4457            ensure_vm_fetch_response_frame_within_limit(&response, self.config.max_frame_bytes)?;
4458
4459            return Ok(DispatchResult {
4460                response,
4461                events: Vec::new(),
4462            });
4463        }
4464
4465        let Some((target_process_id, server_id)) =
4466            vm.active_processes
4467                .iter()
4468                .find_map(|(process_id, process)| {
4469                    process
4470                        .http_servers
4471                        .iter()
4472                        .find(|(_, server)| server.guest_local_addr.port() == payload.port)
4473                        .map(|(server_id, _)| (process_id.clone(), *server_id))
4474                })
4475        else {
4476            return Err(SidecarError::Execution(format!(
4477                "vm.fetch could not find a guest HTTP listener on port {}",
4478                payload.port
4479            )));
4480        };
4481        let socket_paths = build_javascript_socket_path_context(vm)?;
4482        let resource_limits = vm.kernel.resource_limits().clone();
4483        let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness);
4484        let process = vm
4485            .active_processes
4486            .get_mut(&target_process_id)
4487            .ok_or_else(|| {
4488                SidecarError::InvalidState(format!(
4489                    "vm.fetch target process disappeared: {target_process_id}"
4490                ))
4491            })?;
4492        let request_json = serialize_http_loopback_request(&request_url, &options, &headers)?;
4493        let response_json = dispatch_loopback_http_request(LoopbackHttpDispatchRequest {
4494            bridge: &self.bridge,
4495            vm_id: &vm_id,
4496            dns: &vm.dns,
4497            socket_paths: &socket_paths,
4498            kernel: &mut vm.kernel,
4499            kernel_readiness,
4500            process,
4501            resource_limits: &resource_limits,
4502            server_id,
4503            request_json: &request_json,
4504        })?;
4505
4506        let response = self.respond(
4507            request,
4508            ResponsePayload::VmFetchResult(VmFetchResponse { response_json }),
4509        );
4510        ensure_vm_fetch_response_frame_within_limit(&response, self.config.max_frame_bytes)?;
4511
4512        Ok(DispatchResult {
4513            response,
4514            events: Vec::new(),
4515        })
4516    }
4517
4518    pub(crate) async fn get_signal_state(
4519        &mut self,
4520        request: &RequestFrame,
4521        payload: GetSignalStateRequest,
4522    ) -> Result<DispatchResult, SidecarError> {
4523        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4524        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4525
4526        let handlers = self
4527            .vms
4528            .get(&vm_id)
4529            .and_then(|vm| vm.signal_states.get(&payload.process_id))
4530            .cloned()
4531            .unwrap_or_default();
4532
4533        Ok(DispatchResult {
4534            response: signal_state_response(request, payload.process_id, handlers),
4535            events: Vec::new(),
4536        })
4537    }
4538
4539    pub(crate) async fn get_zombie_timer_count(
4540        &mut self,
4541        request: &RequestFrame,
4542        _payload: GetZombieTimerCountRequest,
4543    ) -> Result<DispatchResult, SidecarError> {
4544        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
4545        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
4546
4547        let count = self
4548            .vms
4549            .get(&vm_id)
4550            .map(|vm| vm.kernel.zombie_timer_count() as u64)
4551            .unwrap_or_default();
4552
4553        Ok(DispatchResult {
4554            response: zombie_timer_count_response(request, count),
4555            events: Vec::new(),
4556        })
4557    }
4558
4559    pub(crate) fn kill_process_internal(
4560        &mut self,
4561        vm_id: &str,
4562        process_id: &str,
4563        signal: &str,
4564    ) -> Result<(), SidecarError> {
4565        let signal_name = signal.to_owned();
4566        let signal = parse_signal(signal)?;
4567        let vm = self
4568            .vms
4569            .get_mut(vm_id)
4570            .ok_or_else(|| SidecarError::InvalidState(format!("unknown sidecar VM {vm_id}")))?;
4571        let process = vm.active_processes.get_mut(process_id).ok_or_else(|| {
4572            SidecarError::InvalidState(format!("VM {vm_id} has no active process {process_id}"))
4573        })?;
4574        let kernel_pid = process.kernel_pid;
4575        if !matches!(signal, 0 | libc::SIGCONT) {
4576            // A guest parked in a deferred kernel-wait sync RPC is blocked in a
4577            // native bridge wait the kill cannot interrupt; answer the parked
4578            // RPC first so the termination can take effect.
4579            flush_parked_kernel_wait_rpc(process);
4580        }
4581
4582        enum KillBehavior {
4583            Tool,
4584            SharedV8StateOnly,
4585            SharedV8Continue,
4586            SharedV8Terminate,
4587            SharedV8DispatchOrTerminate,
4588            Noop,
4589            HostPid(u32),
4590        }
4591
4592        let behavior = match &process.execution {
4593            ActiveExecution::Tool(_) => KillBehavior::Tool,
4594            ActiveExecution::Javascript(execution)
4595                if execution.uses_shared_v8_runtime() && matches!(signal, 0 | libc::SIGSTOP) =>
4596            {
4597                KillBehavior::SharedV8StateOnly
4598            }
4599            ActiveExecution::Javascript(execution)
4600                if execution.uses_shared_v8_runtime() && signal == libc::SIGCONT =>
4601            {
4602                KillBehavior::SharedV8Continue
4603            }
4604            ActiveExecution::Wasm(execution)
4605                if execution.uses_shared_v8_runtime()
4606                    && matches!(signal, 0 | libc::SIGSTOP | libc::SIGCONT) =>
4607            {
4608                KillBehavior::SharedV8StateOnly
4609            }
4610            ActiveExecution::Python(execution)
4611                if execution.uses_shared_v8_runtime()
4612                    && matches!(signal, 0 | libc::SIGSTOP | libc::SIGCONT) =>
4613            {
4614                KillBehavior::SharedV8StateOnly
4615            }
4616            ActiveExecution::Javascript(execution)
4617                if execution.uses_shared_v8_runtime() && signal == SIGKILL =>
4618            {
4619                KillBehavior::SharedV8Terminate
4620            }
4621            ActiveExecution::Wasm(execution)
4622                if execution.uses_shared_v8_runtime() && signal == SIGKILL =>
4623            {
4624                KillBehavior::SharedV8Terminate
4625            }
4626            ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime() => {
4627                KillBehavior::SharedV8DispatchOrTerminate
4628            }
4629            ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime() => {
4630                KillBehavior::SharedV8Terminate
4631            }
4632            ActiveExecution::Python(execution) if execution.uses_shared_v8_runtime() => {
4633                KillBehavior::SharedV8Terminate
4634            }
4635            ActiveExecution::Javascript(execution) if execution.child_pid() == 0 => {
4636                KillBehavior::Noop
4637            }
4638            _ => KillBehavior::HostPid(process.execution.child_pid()),
4639        };
4640
4641        match behavior {
4642            KillBehavior::Tool => {
4643                let ActiveExecution::Tool(execution) = &process.execution else {
4644                    unreachable!("kill behavior must match tool execution");
4645                };
4646                if signal != 0 {
4647                    execution.cancelled.store(true, Ordering::Relaxed);
4648                    process.queue_pending_execution_event(ActiveExecutionEvent::Exited(
4649                        128 + signal,
4650                    ))?;
4651                }
4652            }
4653            KillBehavior::SharedV8StateOnly => {
4654                if matches!(signal, libc::SIGSTOP | libc::SIGCONT) {
4655                    vm.kernel
4656                        .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal)
4657                        .map_err(kernel_error)?;
4658                }
4659            }
4660            KillBehavior::SharedV8Continue => {
4661                vm.kernel
4662                    .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal)
4663                    .map_err(kernel_error)?;
4664                if signal != 0 && !dispatch_v8_process_signal(process, signal)? {
4665                    process.execution.terminate()?;
4666                }
4667            }
4668            KillBehavior::SharedV8Terminate => {
4669                if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
4670                    close_kernel_process_stdin(&mut vm.kernel, process)?;
4671                }
4672                process.execution.terminate()?;
4673                let needs_synthetic_exit = matches!(process.execution, ActiveExecution::Wasm(_))
4674                    || (signal == SIGKILL
4675                        && matches!(process.execution, ActiveExecution::Javascript(_)));
4676                if signal != 0 && needs_synthetic_exit {
4677                    process.queue_pending_execution_event(ActiveExecutionEvent::Exited(
4678                        128 + signal,
4679                    ))?;
4680                }
4681            }
4682            KillBehavior::SharedV8DispatchOrTerminate => {
4683                if signal != 0 && !dispatch_v8_process_signal(process, signal)? {
4684                    process.execution.terminate()?;
4685                }
4686            }
4687            KillBehavior::Noop => {}
4688            KillBehavior::HostPid(pid) => {
4689                if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
4690                    close_kernel_process_stdin(&mut vm.kernel, process)?;
4691                }
4692                signal_runtime_process(pid, signal)?;
4693            }
4694        }
4695        emit_security_audit_event(
4696            &self.bridge,
4697            vm_id,
4698            "security.process.kill",
4699            audit_fields([
4700                (String::from("source"), String::from("control_plane")),
4701                (String::from("source_pid"), String::from("0")),
4702                (String::from("target_pid"), process.kernel_pid.to_string()),
4703                (String::from("process_id"), process_id.to_owned()),
4704                (String::from("signal"), signal_name),
4705                (
4706                    String::from("host_pid"),
4707                    process.execution.child_pid().to_string(),
4708                ),
4709            ]),
4710        );
4711        Ok(())
4712    }
4713
4714    pub async fn pump_process_events(
4715        &mut self,
4716        ownership: &OwnershipScope,
4717    ) -> Result<bool, SidecarError> {
4718        let mut emitted_any = false;
4719
4720        let mut queued_envelopes = Vec::new();
4721        {
4722            let pending_capacity = self.pending_process_event_capacity();
4723            let receiver = self.process_event_receiver.as_mut().ok_or_else(|| {
4724                SidecarError::InvalidState(String::from("process event receiver unavailable"))
4725            })?;
4726            loop {
4727                if queued_envelopes.len() >= pending_capacity {
4728                    if receiver.is_empty() {
4729                        break;
4730                    }
4731                    return Err(process_event_queue_overflow_error());
4732                }
4733                match receiver.try_recv() {
4734                    Ok(envelope) => {
4735                        queued_envelopes.push(envelope);
4736                        emitted_any = true;
4737                    }
4738                    Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
4739                    Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
4740                }
4741            }
4742        }
4743        for envelope in queued_envelopes {
4744            self.queue_pending_process_event(envelope)?;
4745        }
4746
4747        let vm_ids = self.vm_ids_for_scope(ownership)?;
4748        for vm_id in vm_ids {
4749            while let Some(vm) = self.vms.get(&vm_id) {
4750                let connection_id = vm.connection_id.clone();
4751                let session_id = vm.session_id.clone();
4752                let process_ids = self
4753                    .vms
4754                    .get(&vm_id)
4755                    .map(|vm| vm.active_processes.keys().cloned().collect::<Vec<_>>())
4756                    .unwrap_or_default();
4757                let mut emitted_this_pass = false;
4758
4759                for process_id in process_ids {
4760                    if self
4761                        .vms
4762                        .get(&vm_id)
4763                        .is_some_and(|vm| vm.detached_child_processes.contains(&process_id))
4764                    {
4765                        continue;
4766                    }
4767                    enum ProcessPollResult {
4768                        Event(Box<Option<ActiveExecutionEvent>>),
4769                        RecoverClosedChannel,
4770                    }
4771                    let poll_result = {
4772                        let Some(vm) = self.vms.get_mut(&vm_id) else {
4773                            continue;
4774                        };
4775                        let Some(process) = vm.active_processes.get_mut(&process_id) else {
4776                            continue;
4777                        };
4778                        if let Some(event) = process.pending_execution_events.pop_front() {
4779                            ProcessPollResult::Event(Box::new(Some(event)))
4780                        } else {
4781                            match process.execution.poll_event(Duration::ZERO).await {
4782                                Ok(event) => ProcessPollResult::Event(Box::new(event)),
4783                                Err(SidecarError::Execution(message))
4784                                    if (process.runtime == GuestRuntimeKind::JavaScript
4785                                        && closed_javascript_event_channel(&message))
4786                                        || (process.runtime == GuestRuntimeKind::Python
4787                                            && closed_python_event_channel(&message))
4788                                        || (process.runtime == GuestRuntimeKind::WebAssembly
4789                                            && closed_wasm_event_channel(&message)) =>
4790                                {
4791                                    ProcessPollResult::RecoverClosedChannel
4792                                }
4793                                Err(other) => return Err(other),
4794                            }
4795                        }
4796                    };
4797                    let event = match poll_result {
4798                        ProcessPollResult::Event(event) => *event,
4799                        ProcessPollResult::RecoverClosedChannel => {
4800                            self.recover_closed_root_runtime_process_event(&vm_id, &process_id)?
4801                        }
4802                    };
4803
4804                    let Some(event) = event else {
4805                        continue;
4806                    };
4807                    if matches!(&event, ActiveExecutionEvent::Exited(_)) {
4808                        record_execute_response_to_exit_milestone(
4809                            "execute_response_to_exit_event_polled",
4810                            &vm_id,
4811                            &process_id,
4812                        );
4813                    }
4814
4815                    if Self::internal_execution_event(&event) {
4816                        // These events are sidecar work items, not client-facing
4817                        // process events. Handle them immediately so a sibling
4818                        // process can service sync RPCs while another request
4819                        // waits on VM-local networking.
4820                        self.handle_execution_event(&vm_id, &process_id, event)?;
4821                    } else {
4822                        self.queue_pending_process_event(ProcessEventEnvelope {
4823                            connection_id: connection_id.clone(),
4824                            session_id: session_id.clone(),
4825                            vm_id: vm_id.clone(),
4826                            process_id: process_id.clone(),
4827                            event,
4828                        })?;
4829                    }
4830                    emitted_any = true;
4831                    emitted_this_pass = true;
4832                }
4833
4834                if !emitted_this_pass {
4835                    break;
4836                }
4837            }
4838
4839            if self.pump_detached_child_process_events(&vm_id)? {
4840                emitted_any = true;
4841            }
4842        }
4843
4844        Ok(emitted_any)
4845    }
4846
4847    fn internal_execution_event(event: &ActiveExecutionEvent) -> bool {
4848        matches!(
4849            event,
4850            ActiveExecutionEvent::JavascriptSyncRpcRequest(_)
4851                | ActiveExecutionEvent::PythonVfsRpcRequest(_)
4852                | ActiveExecutionEvent::SignalState { .. }
4853        )
4854    }
4855
4856    fn recover_closed_root_runtime_process_event(
4857        &mut self,
4858        vm_id: &str,
4859        process_id: &str,
4860    ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
4861        let Some(vm) = self.vms.get_mut(vm_id) else {
4862            return Ok(None);
4863        };
4864        let Some(process) = vm.active_processes.get(process_id) else {
4865            return Ok(None);
4866        };
4867        if process.execution.uses_shared_v8_runtime() {
4868            return Ok(None);
4869        }
4870        if process.runtime != GuestRuntimeKind::JavaScript
4871            && process.runtime != GuestRuntimeKind::Python
4872            && process.runtime != GuestRuntimeKind::WebAssembly
4873        {
4874            return Ok(None);
4875        }
4876        let runtime_child_pid = process.execution.child_pid();
4877        if runtime_child_pid == 0 {
4878            return Ok(None);
4879        }
4880        if let Some(status) = runtime_child_exit_status(runtime_child_pid)? {
4881            return Ok(Some(ActiveExecutionEvent::Exited(status)));
4882        }
4883        if runtime_child_is_alive(runtime_child_pid)? {
4884            return Ok(None);
4885        }
4886        Ok(Some(ActiveExecutionEvent::Exited(0)))
4887    }
4888
4889    fn active_process_by_path<'a>(
4890        process: &'a ActiveProcess,
4891        child_path: &[&str],
4892    ) -> Option<&'a ActiveProcess> {
4893        let mut current = process;
4894        for child_id in child_path {
4895            current = current.child_processes.get(*child_id)?;
4896        }
4897        Some(current)
4898    }
4899
4900    fn active_process_by_path_mut<'a>(
4901        process: &'a mut ActiveProcess,
4902        child_path: &[&str],
4903    ) -> Option<&'a mut ActiveProcess> {
4904        let mut current = process;
4905        for child_id in child_path {
4906            current = current.child_processes.get_mut(*child_id)?;
4907        }
4908        Some(current)
4909    }
4910
4911    fn active_process_by_owned_path_mut<'a>(
4912        process: &'a mut ActiveProcess,
4913        child_path: &[String],
4914    ) -> Option<&'a mut ActiveProcess> {
4915        let mut current = process;
4916        for child_id in child_path {
4917            current = current.child_processes.get_mut(child_id)?;
4918        }
4919        Some(current)
4920    }
4921
4922    fn active_process_path_by_kernel_pid(
4923        process: &ActiveProcess,
4924        kernel_pid: u32,
4925    ) -> Option<Vec<String>> {
4926        if process.kernel_pid == kernel_pid {
4927            return Some(Vec::new());
4928        }
4929
4930        for (child_id, child) in &process.child_processes {
4931            let Some(mut path) = Self::active_process_path_by_kernel_pid(child, kernel_pid) else {
4932                continue;
4933            };
4934            path.insert(0, child_id.clone());
4935            return Some(path);
4936        }
4937
4938        None
4939    }
4940
4941    fn descendant_parent_process<'a>(
4942        vm: &'a VmState,
4943        process_id: &str,
4944        child_path: &[&str],
4945    ) -> Option<&'a ActiveProcess> {
4946        let root = vm.active_processes.get(process_id)?;
4947        Self::active_process_by_path(root, child_path)
4948    }
4949
4950    fn descendant_parent_process_mut<'a>(
4951        vm: &'a mut VmState,
4952        process_id: &str,
4953        child_path: &[&str],
4954    ) -> Option<&'a mut ActiveProcess> {
4955        let root = vm.active_processes.get_mut(process_id)?;
4956        Self::active_process_by_path_mut(root, child_path)
4957    }
4958
4959    fn child_process_path_label(process_id: &str, child_path: &[&str]) -> String {
4960        if child_path.is_empty() {
4961            process_id.to_owned()
4962        } else {
4963            format!("{process_id}/{}", child_path.join("/"))
4964        }
4965    }
4966
4967    fn adopt_detached_child_processes(
4968        current_process_id: &str,
4969        process: &mut ActiveProcess,
4970    ) -> Vec<(String, ActiveProcess)> {
4971        let mut adopted = Vec::new();
4972        let child_ids = process.child_processes.keys().cloned().collect::<Vec<_>>();
4973        for child_id in child_ids {
4974            let child_process_id = format!("{current_process_id}/{child_id}");
4975            let Some(mut child) = process.child_processes.remove(&child_id) else {
4976                continue;
4977            };
4978            if child.detached {
4979                adopted.push((child_process_id, child));
4980                continue;
4981            }
4982
4983            adopted.extend(Self::adopt_detached_child_processes(
4984                &child_process_id,
4985                &mut child,
4986            ));
4987            process.child_processes.insert(child_id, child);
4988        }
4989        adopted
4990    }
4991
4992    fn child_process_signal_key<'a>(process_id: &'a str, child_path: &[&'a str]) -> &'a str {
4993        child_path.last().copied().unwrap_or(process_id)
4994    }
4995
4996    fn resolve_detached_child_process_path(
4997        vm: &VmState,
4998        detached_process_id: &str,
4999    ) -> Option<(String, Vec<String>)> {
5000        let root_process_id = vm
5001            .active_processes
5002            .keys()
5003            .filter(|candidate| {
5004                detached_process_id == candidate.as_str()
5005                    || detached_process_id
5006                        .strip_prefix(candidate.as_str())
5007                        .is_some_and(|remainder| remainder.starts_with('/'))
5008            })
5009            .max_by_key(|candidate| candidate.len())?
5010            .clone();
5011
5012        let remainder = detached_process_id
5013            .strip_prefix(root_process_id.as_str())
5014            .unwrap_or_default();
5015        if remainder.is_empty() {
5016            return Some((root_process_id, Vec::new()));
5017        }
5018
5019        Some((
5020            root_process_id,
5021            remainder
5022                .trim_start_matches('/')
5023                .split('/')
5024                .map(str::to_owned)
5025                .collect(),
5026        ))
5027    }
5028
5029    fn pump_detached_child_process_events(&mut self, vm_id: &str) -> Result<bool, SidecarError> {
5030        let detached_process_ids = self
5031            .vms
5032            .get(vm_id)
5033            .map(|vm| {
5034                vm.detached_child_processes
5035                    .iter()
5036                    .cloned()
5037                    .collect::<Vec<_>>()
5038            })
5039            .unwrap_or_default();
5040        let mut emitted_any = false;
5041        for detached_process_id in detached_process_ids {
5042            let Some((root_process_id, child_path)) = self
5043                .vms
5044                .get(vm_id)
5045                .and_then(|vm| Self::resolve_detached_child_process_path(vm, &detached_process_id))
5046            else {
5047                if let Some(vm) = self.vms.get_mut(vm_id) {
5048                    vm.detached_child_processes.remove(&detached_process_id);
5049                }
5050                continue;
5051            };
5052            if child_path.is_empty() {
5053                loop {
5054                    enum ProcessPollResult {
5055                        Event(Box<Option<ActiveExecutionEvent>>),
5056                        RecoverClosedChannel,
5057                    }
5058                    let poll_result = {
5059                        let Some(vm) = self.vms.get_mut(vm_id) else {
5060                            break;
5061                        };
5062                        let Some(process) = vm.active_processes.get_mut(&root_process_id) else {
5063                            break;
5064                        };
5065                        if let Some(event) = process.pending_execution_events.pop_front() {
5066                            ProcessPollResult::Event(Box::new(Some(event)))
5067                        } else {
5068                            match process.execution.poll_event_blocking(Duration::ZERO) {
5069                                Ok(event) => ProcessPollResult::Event(Box::new(event)),
5070                                Err(SidecarError::Execution(message))
5071                                    if (process.runtime == GuestRuntimeKind::JavaScript
5072                                        && closed_javascript_event_channel(&message))
5073                                        || (process.runtime == GuestRuntimeKind::Python
5074                                            && closed_python_event_channel(&message))
5075                                        || (process.runtime == GuestRuntimeKind::WebAssembly
5076                                            && closed_wasm_event_channel(&message)) =>
5077                                {
5078                                    ProcessPollResult::RecoverClosedChannel
5079                                }
5080                                Err(error) => return Err(error),
5081                            }
5082                        }
5083                    };
5084                    let event = match poll_result {
5085                        ProcessPollResult::Event(event) => *event,
5086                        ProcessPollResult::RecoverClosedChannel => {
5087                            self.recover_closed_root_runtime_process_event(vm_id, &root_process_id)?
5088                        }
5089                    };
5090                    let Some(event) = event else {
5091                        break;
5092                    };
5093                    if matches!(&event, ActiveExecutionEvent::Exited(_)) {
5094                        record_execute_response_to_exit_milestone(
5095                            "execute_response_to_detached_exit_event_polled",
5096                            vm_id,
5097                            &detached_process_id,
5098                        );
5099                    }
5100                    let Some((connection_id, session_id)) = self
5101                        .vms
5102                        .get(vm_id)
5103                        .map(|vm| (vm.connection_id.clone(), vm.session_id.clone()))
5104                    else {
5105                        break;
5106                    };
5107                    match event {
5108                        ActiveExecutionEvent::Stdout(chunk) => {
5109                            self.queue_pending_process_event(ProcessEventEnvelope {
5110                                connection_id,
5111                                session_id,
5112                                vm_id: vm_id.to_owned(),
5113                                process_id: detached_process_id.clone(),
5114                                event: ActiveExecutionEvent::Stdout(chunk),
5115                            })?;
5116                            emitted_any = true;
5117                        }
5118                        ActiveExecutionEvent::Stderr(chunk) => {
5119                            self.queue_pending_process_event(ProcessEventEnvelope {
5120                                connection_id,
5121                                session_id,
5122                                vm_id: vm_id.to_owned(),
5123                                process_id: detached_process_id.clone(),
5124                                event: ActiveExecutionEvent::Stderr(chunk),
5125                            })?;
5126                            emitted_any = true;
5127                        }
5128                        ActiveExecutionEvent::Exited(exit_code) => {
5129                            if let Some(vm) = self.vms.get_mut(vm_id) {
5130                                vm.detached_child_processes.remove(&detached_process_id);
5131                            }
5132                            self.queue_pending_process_event(ProcessEventEnvelope {
5133                                connection_id,
5134                                session_id,
5135                                vm_id: vm_id.to_owned(),
5136                                process_id: detached_process_id.clone(),
5137                                event: ActiveExecutionEvent::Exited(exit_code),
5138                            })?;
5139                            emitted_any = true;
5140                            break;
5141                        }
5142                        ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
5143                            self.handle_javascript_sync_rpc_request(
5144                                vm_id,
5145                                &root_process_id,
5146                                request,
5147                            )?;
5148                        }
5149                        ActiveExecutionEvent::PythonVfsRpcRequest(request) => {
5150                            self.handle_python_vfs_rpc_request(vm_id, &root_process_id, *request)?;
5151                        }
5152                        ActiveExecutionEvent::SignalState {
5153                            signal,
5154                            registration,
5155                        } => {
5156                            if let Some(vm) = self.vms.get_mut(vm_id) {
5157                                vm.signal_states
5158                                    .entry(root_process_id.clone())
5159                                    .or_default()
5160                                    .insert(signal, registration);
5161                            }
5162                        }
5163                    }
5164                }
5165                continue;
5166            }
5167
5168            let parent_path = child_path[..child_path.len() - 1]
5169                .iter()
5170                .map(String::as_str)
5171                .collect::<Vec<_>>();
5172            let child_process_id = child_path.last().expect("child path cannot be empty");
5173
5174            loop {
5175                let event = match self.poll_descendant_javascript_child_process(
5176                    vm_id,
5177                    &root_process_id,
5178                    &parent_path,
5179                    child_process_id,
5180                    0,
5181                ) {
5182                    Ok(event) => event,
5183                    Err(SidecarError::InvalidState(message))
5184                        if message.contains("unknown child process")
5185                            || message.contains("unknown child process path") =>
5186                    {
5187                        if let Some(vm) = self.vms.get_mut(vm_id) {
5188                            vm.detached_child_processes.remove(&detached_process_id);
5189                        }
5190                        break;
5191                    }
5192                    Err(error) if is_javascript_child_process_gone_error(&error) => {
5193                        if let Some(vm) = self.vms.get_mut(vm_id) {
5194                            vm.detached_child_processes.remove(&detached_process_id);
5195                        }
5196                        break;
5197                    }
5198                    Err(error) => return Err(error),
5199                };
5200
5201                let Some(event_type) = event.get("type").and_then(Value::as_str) else {
5202                    break;
5203                };
5204                let Some((connection_id, session_id)) = self
5205                    .vms
5206                    .get(vm_id)
5207                    .map(|vm| (vm.connection_id.clone(), vm.session_id.clone()))
5208                else {
5209                    break;
5210                };
5211
5212                let envelope = match event_type {
5213                    "stdout" => Some(ProcessEventEnvelope {
5214                        connection_id: connection_id.clone(),
5215                        session_id: session_id.clone(),
5216                        vm_id: vm_id.to_owned(),
5217                        process_id: detached_process_id.clone(),
5218                        event: ActiveExecutionEvent::Stdout(javascript_sync_rpc_bytes_arg(
5219                            &[event.get("data").cloned().unwrap_or(Value::Null)],
5220                            0,
5221                            "detached child_process stdout",
5222                        )?),
5223                    }),
5224                    "stderr" => Some(ProcessEventEnvelope {
5225                        connection_id: connection_id.clone(),
5226                        session_id: session_id.clone(),
5227                        vm_id: vm_id.to_owned(),
5228                        process_id: detached_process_id.clone(),
5229                        event: ActiveExecutionEvent::Stderr(javascript_sync_rpc_bytes_arg(
5230                            &[event.get("data").cloned().unwrap_or(Value::Null)],
5231                            0,
5232                            "detached child_process stderr",
5233                        )?),
5234                    }),
5235                    "exit" => {
5236                        if let Some(vm) = self.vms.get_mut(vm_id) {
5237                            vm.detached_child_processes.remove(&detached_process_id);
5238                        }
5239                        Some(ProcessEventEnvelope {
5240                            connection_id,
5241                            session_id,
5242                            vm_id: vm_id.to_owned(),
5243                            process_id: detached_process_id.clone(),
5244                            event: ActiveExecutionEvent::Exited(
5245                                event
5246                                    .get("exitCode")
5247                                    .and_then(Value::as_i64)
5248                                    .map(|value| value as i32)
5249                                    .unwrap_or(1),
5250                            ),
5251                        })
5252                    }
5253                    _ => None,
5254                };
5255
5256                let Some(envelope) = envelope else {
5257                    break;
5258                };
5259                self.queue_pending_process_event(envelope)?;
5260                emitted_any = true;
5261
5262                if event_type == "exit" {
5263                    break;
5264                }
5265            }
5266        }
5267
5268        Ok(emitted_any)
5269    }
5270    pub(crate) fn drain_queued_descendant_javascript_child_process_events(
5271        &mut self,
5272        vm_id: &str,
5273        process_id: &str,
5274        child_path: &[&str],
5275    ) -> Result<(), SidecarError> {
5276        if child_path.is_empty() {
5277            return Ok(());
5278        }
5279        let target_process_id = Self::child_process_path_label(process_id, child_path);
5280        let mut child_capacity = self
5281            .vms
5282            .get(vm_id)
5283            .and_then(|vm| vm.active_processes.get(process_id))
5284            .and_then(|root| descendant_pending_execution_event_capacity(root, child_path));
5285
5286        let mut deferred = VecDeque::new();
5287        while let Some(envelope) = self.pending_process_events.pop_front() {
5288            if envelope.vm_id == vm_id && envelope.process_id == target_process_id {
5289                if matches!(child_capacity, Some(0)) {
5290                    self.pending_process_events.push_front(envelope);
5291                    while let Some(deferred_envelope) = deferred.pop_back() {
5292                        self.pending_process_events.push_front(deferred_envelope);
5293                    }
5294                    return Err(process_event_queue_overflow_error());
5295                }
5296                if let Some(vm) = self.vms.get_mut(vm_id) {
5297                    if let Some(root) = vm.active_processes.get_mut(process_id) {
5298                        if let Some(child) = Self::active_process_by_path_mut(root, child_path) {
5299                            child.queue_pending_execution_event(envelope.event)?;
5300                            child_capacity = child_capacity.map(|capacity| capacity - 1);
5301                            continue;
5302                        }
5303                    }
5304                }
5305            }
5306            deferred.push_back(envelope);
5307        }
5308        self.pending_process_events = deferred;
5309
5310        let mut queued = Vec::new();
5311        {
5312            let transfer_capacity = self
5313                .pending_process_event_capacity()
5314                .min(child_capacity.unwrap_or(usize::MAX));
5315            let receiver = self.process_event_receiver.as_mut().ok_or_else(|| {
5316                SidecarError::InvalidState(String::from("process event receiver unavailable"))
5317            })?;
5318            loop {
5319                if queued.len() >= transfer_capacity {
5320                    if receiver.is_empty() {
5321                        break;
5322                    }
5323                    return Err(process_event_queue_overflow_error());
5324                }
5325                match receiver.try_recv() {
5326                    Ok(envelope) => queued.push(envelope),
5327                    Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
5328                    Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
5329                }
5330            }
5331        }
5332        for envelope in queued {
5333            if envelope.vm_id == vm_id && envelope.process_id == target_process_id {
5334                if let Some(vm) = self.vms.get_mut(vm_id) {
5335                    if let Some(root) = vm.active_processes.get_mut(process_id) {
5336                        if let Some(child) = Self::active_process_by_path_mut(root, child_path) {
5337                            child.queue_pending_execution_event(envelope.event)?;
5338                            continue;
5339                        }
5340                    }
5341                }
5342            }
5343            self.queue_pending_process_event(envelope)?;
5344        }
5345
5346        Ok(())
5347    }
5348
5349    pub(crate) fn handle_execution_event(
5350        &mut self,
5351        vm_id: &str,
5352        process_id: &str,
5353        event: ActiveExecutionEvent,
5354    ) -> Result<Option<EventFrame>, SidecarError> {
5355        let Some(vm) = self.vms.get(vm_id) else {
5356            log_stale_process_event(&self.bridge, vm_id, process_id, "execution event dispatch");
5357            return Ok(None);
5358        };
5359        if !vm.active_processes.contains_key(process_id) {
5360            log_stale_process_event(&self.bridge, vm_id, process_id, "execution event dispatch");
5361            return Ok(None);
5362        }
5363        let (connection_id, session_id) = { (vm.connection_id.clone(), vm.session_id.clone()) };
5364        let ownership = OwnershipScope::vm(&connection_id, &session_id, vm_id);
5365
5366        if self.capture_extension_process_output_event(vm_id, process_id, &event) {
5367            return Ok(None);
5368        }
5369
5370        match event {
5371            ActiveExecutionEvent::Stdout(chunk) => Ok(Some(EventFrame::new(
5372                ownership,
5373                EventPayload::ProcessOutput(ProcessOutputEvent {
5374                    process_id: process_id.to_owned(),
5375                    channel: StreamChannel::Stdout,
5376                    chunk,
5377                }),
5378            ))),
5379            ActiveExecutionEvent::Stderr(chunk) => Ok(Some(EventFrame::new(
5380                ownership,
5381                EventPayload::ProcessOutput(ProcessOutputEvent {
5382                    process_id: process_id.to_owned(),
5383                    channel: StreamChannel::Stderr,
5384                    chunk,
5385                }),
5386            ))),
5387            ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
5388                self.handle_javascript_sync_rpc_request(vm_id, process_id, request)?;
5389                Ok(None)
5390            }
5391            ActiveExecutionEvent::PythonVfsRpcRequest(request) => {
5392                self.handle_python_vfs_rpc_request(vm_id, process_id, *request)?;
5393                Ok(None)
5394            }
5395            ActiveExecutionEvent::SignalState {
5396                signal,
5397                registration,
5398            } => {
5399                let Some(vm) = self.vms.get_mut(vm_id) else {
5400                    return Ok(None);
5401                };
5402                if !vm.active_processes.contains_key(process_id) {
5403                    return Ok(None);
5404                }
5405                vm.signal_states
5406                    .entry(process_id.to_owned())
5407                    .or_default()
5408                    .insert(signal, registration);
5409                Ok(None)
5410            }
5411            ActiveExecutionEvent::Exited(exit_code) => {
5412                record_execute_response_to_exit_milestone(
5413                    "execute_response_to_exit_event_handle",
5414                    vm_id,
5415                    process_id,
5416                );
5417                record_execute_response_to_exit(vm_id, process_id);
5418                let phase_start = Instant::now();
5419                let became_idle = self
5420                    .finish_active_process_exit(vm_id, process_id, exit_code)?
5421                    .unwrap_or(false);
5422                record_execute_phase("process_exit_cleanup", phase_start.elapsed());
5423
5424                let phase_start = Instant::now();
5425                if became_idle {
5426                    self.bridge.emit_lifecycle(vm_id, LifecycleState::Ready)?;
5427                }
5428                record_execute_phase("process_exit_lifecycle_emit", phase_start.elapsed());
5429
5430                Ok(Some(EventFrame::new(
5431                    ownership,
5432                    EventPayload::ProcessExited(ProcessExitedEvent {
5433                        process_id: process_id.to_owned(),
5434                        exit_code,
5435                    }),
5436                )))
5437            }
5438        }
5439    }
5440
5441    pub(crate) fn finish_active_process_exit(
5442        &mut self,
5443        vm_id: &str,
5444        process_id: &str,
5445        exit_code: i32,
5446    ) -> Result<Option<bool>, SidecarError> {
5447        let Some(vm) = self.vms.get_mut(vm_id) else {
5448            log_stale_process_event(&self.bridge, vm_id, process_id, "process exit cleanup");
5449            return Ok(None);
5450        };
5451        if !vm.active_processes.contains_key(process_id) {
5452            log_stale_process_event(&self.bridge, vm_id, process_id, "process exit cleanup");
5453            return Ok(None);
5454        }
5455
5456        let phase_start = Instant::now();
5457        prune_exited_process_snapshots(vm);
5458        record_execute_phase(
5459            "process_exit_cleanup_prune_snapshots",
5460            phase_start.elapsed(),
5461        );
5462        let phase_start = Instant::now();
5463        let process_table = vm.kernel.list_processes();
5464        record_execute_phase("process_exit_cleanup_list_processes", phase_start.elapsed());
5465        let phase_start = Instant::now();
5466        let Some(mut process) = vm.active_processes.remove(process_id) else {
5467            return Ok(None);
5468        };
5469        record_execute_phase("process_exit_cleanup_remove_active", phase_start.elapsed());
5470        let phase_start = Instant::now();
5471        if let Some(info) = process_table.get(&process.kernel_pid) {
5472            vm.exited_process_snapshots
5473                .push_back(ExitedProcessSnapshot {
5474                    captured_at: Instant::now(),
5475                    process: build_process_snapshot_entry(
5476                        process_id,
5477                        &process,
5478                        info,
5479                        Some(exit_code),
5480                    ),
5481                });
5482        }
5483        record_execute_phase("process_exit_cleanup_build_snapshot", phase_start.elapsed());
5484        let phase_start = Instant::now();
5485        let detached_children = Self::adopt_detached_child_processes(process_id, &mut process);
5486        record_execute_phase("process_exit_cleanup_adopt_detached", phase_start.elapsed());
5487        let phase_start = Instant::now();
5488        let should_sync_host_writes = process.host_write_dirty_recursive()
5489            || !process.clean_host_writes_are_observable_recursive();
5490        if should_sync_host_writes {
5491            sync_process_host_writes_to_kernel(vm, &process)?;
5492        } else {
5493            record_execute_phase(
5494                "process_exit_cleanup_sync_host_writes_clean_skip",
5495                Duration::ZERO,
5496            );
5497        }
5498        record_execute_phase(
5499            "process_exit_cleanup_sync_host_writes",
5500            phase_start.elapsed(),
5501        );
5502        let phase_start = Instant::now();
5503        let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness);
5504        terminate_child_process_tree(&mut vm.kernel, &mut process, &kernel_readiness);
5505        record_execute_phase(
5506            "process_exit_cleanup_terminate_child_tree",
5507            phase_start.elapsed(),
5508        );
5509        let phase_start = Instant::now();
5510        process.kernel_handle.finish(exit_code);
5511        record_execute_phase("process_exit_cleanup_kernel_finish", phase_start.elapsed());
5512        let phase_start = Instant::now();
5513        let _ = vm.kernel.wait_and_reap(process.kernel_pid);
5514        record_execute_phase("process_exit_cleanup_wait_and_reap", phase_start.elapsed());
5515        let phase_start = Instant::now();
5516        vm.signal_states.remove(process_id);
5517        record_execute_phase(
5518            "process_exit_cleanup_signal_state_remove",
5519            phase_start.elapsed(),
5520        );
5521        let phase_start = Instant::now();
5522        for (detached_process_id, detached_child) in detached_children {
5523            vm.detached_child_processes
5524                .insert(detached_process_id.clone());
5525            vm.active_processes
5526                .insert(detached_process_id, detached_child);
5527        }
5528        record_execute_phase(
5529            "process_exit_cleanup_reinsert_detached",
5530            phase_start.elapsed(),
5531        );
5532        let phase_start = Instant::now();
5533        let became_idle = vm.active_processes.is_empty();
5534        record_execute_phase("process_exit_cleanup_became_idle", phase_start.elapsed());
5535        let phase_start = Instant::now();
5536        self.prune_extension_process_resource(process_id);
5537        record_execute_phase("process_exit_cleanup_prune_resource", phase_start.elapsed());
5538
5539        Ok(Some(became_idle))
5540    }
5541
5542    pub(crate) fn drain_process_events_blocking_with_limit(
5543        &mut self,
5544        vm_id: &str,
5545        process_id: &str,
5546        max_events: usize,
5547    ) -> Result<Vec<ActiveExecutionEvent>, SidecarError> {
5548        let mut events = Vec::new();
5549        if max_events == 0 {
5550            return Ok(events);
5551        }
5552        let mut deadline = Instant::now() + PROCESS_EXIT_DRAIN_INITIAL_QUIET;
5553
5554        loop {
5555            if events.len() >= max_events {
5556                break;
5557            }
5558            let event = {
5559                let Some(vm) = self.vms.get_mut(vm_id) else {
5560                    break;
5561                };
5562                let Some(process) = vm.active_processes.get_mut(process_id) else {
5563                    break;
5564                };
5565                if let Some(event) = process.pending_execution_events.pop_front() {
5566                    Some(event)
5567                } else {
5568                    match process.execution.poll_event_blocking(Duration::ZERO) {
5569                        Ok(event) => event,
5570                        Err(SidecarError::Execution(_)) => None,
5571                        Err(other) => return Err(other),
5572                    }
5573                }
5574            };
5575
5576            let Some(event) = event else {
5577                if Instant::now() >= deadline {
5578                    break;
5579                }
5580                let blocking_wait = deadline.saturating_duration_since(Instant::now());
5581                if blocking_wait.is_zero() {
5582                    break;
5583                }
5584                if events.len() >= max_events {
5585                    break;
5586                }
5587                let delayed_event = {
5588                    let Some(vm) = self.vms.get_mut(vm_id) else {
5589                        break;
5590                    };
5591                    let Some(process) = vm.active_processes.get_mut(process_id) else {
5592                        break;
5593                    };
5594                    if let Some(event) = process.pending_execution_events.pop_front() {
5595                        Some(event)
5596                    } else {
5597                        match process.execution.poll_event_blocking(blocking_wait) {
5598                            Ok(event) => event,
5599                            Err(SidecarError::Execution(_)) => None,
5600                            Err(other) => return Err(other),
5601                        }
5602                    }
5603                };
5604                let Some(event) = delayed_event else {
5605                    break;
5606                };
5607                events.push(event);
5608                deadline = Instant::now() + PROCESS_EXIT_DRAIN_TRAILING_QUIET;
5609                continue;
5610            };
5611            events.push(event);
5612            deadline = Instant::now() + PROCESS_EXIT_DRAIN_TRAILING_QUIET;
5613        }
5614
5615        Ok(events)
5616    }
5617
5618    pub(crate) fn handle_python_vfs_rpc_request(
5619        &mut self,
5620        vm_id: &str,
5621        process_id: &str,
5622        request: PythonVfsRpcRequest,
5623    ) -> Result<(), SidecarError> {
5624        match request.method {
5625            PythonVfsRpcMethod::Read
5626            | PythonVfsRpcMethod::Write
5627            | PythonVfsRpcMethod::Stat
5628            | PythonVfsRpcMethod::Lstat
5629            | PythonVfsRpcMethod::ReadDir
5630            | PythonVfsRpcMethod::Mkdir
5631            | PythonVfsRpcMethod::Unlink
5632            | PythonVfsRpcMethod::Rmdir
5633            | PythonVfsRpcMethod::Rename
5634            | PythonVfsRpcMethod::Symlink
5635            | PythonVfsRpcMethod::ReadLink
5636            | PythonVfsRpcMethod::Setattr => {
5637                filesystem_handle_python_vfs_rpc_request(self, vm_id, process_id, request)
5638            }
5639            PythonVfsRpcMethod::HttpRequest => {
5640                self.handle_python_http_rpc_request(vm_id, process_id, request)
5641            }
5642            PythonVfsRpcMethod::DnsLookup => {
5643                self.handle_python_dns_rpc_request(vm_id, process_id, request)
5644            }
5645            PythonVfsRpcMethod::SubprocessRun => {
5646                self.handle_python_subprocess_rpc_request(vm_id, process_id, request)
5647            }
5648            PythonVfsRpcMethod::SocketConnect
5649            | PythonVfsRpcMethod::SocketSend
5650            | PythonVfsRpcMethod::SocketRecv
5651            | PythonVfsRpcMethod::SocketClose
5652            | PythonVfsRpcMethod::UdpCreate
5653            | PythonVfsRpcMethod::UdpSendto
5654            | PythonVfsRpcMethod::UdpRecvfrom => {
5655                self.handle_python_socket_rpc_request(vm_id, process_id, request)
5656            }
5657        }
5658    }
5659
5660    fn handle_python_http_rpc_request(
5661        &mut self,
5662        vm_id: &str,
5663        process_id: &str,
5664        request: PythonVfsRpcRequest,
5665    ) -> Result<(), SidecarError> {
5666        let Some(vm) = self.vms.get(vm_id) else {
5667            return Ok(());
5668        };
5669        if !vm.active_processes.contains_key(process_id) {
5670            return Ok(());
5671        }
5672        let response = (|| {
5673            let url_text = request.url.as_deref().ok_or_else(|| {
5674                SidecarError::InvalidState(String::from("python httpRequest requires a url"))
5675            })?;
5676            let url = Url::parse(url_text)
5677                .map_err(|error| SidecarError::Execution(format!("ERR_INVALID_URL: {error}")))?;
5678            let host = url.host_str().ok_or_else(|| {
5679                SidecarError::Execution(String::from("ERR_INVALID_URL: missing host"))
5680            })?;
5681            let port = url.port_or_known_default().ok_or_else(|| {
5682                SidecarError::Execution(String::from("ERR_INVALID_URL: missing port"))
5683            })?;
5684            self.bridge.require_network_access(
5685                vm_id,
5686                NetworkOperation::Http,
5687                format_tcp_resource(host, port),
5688            )?;
5689            // Pin the outbound connection to the IP addresses that pass the
5690            // egress range guard at resolution time. A literal IP is validated
5691            // directly; a hostname is resolved once here and the resulting
5692            // address set is pinned into the HTTP client's resolver below so a
5693            // rebinding DNS server cannot make the second (TLS/TCP) lookup land
5694            // on a private/link-local/metadata IP that this check rejected.
5695            let pinned_addresses = if let Ok(literal_ip) = host.parse::<IpAddr>() {
5696                filter_dns_safe_ip_addrs(vec![literal_ip], host)?
5697            } else {
5698                filter_dns_safe_ip_addrs(
5699                    resolve_dns_ip_addrs(
5700                        &self.bridge,
5701                        &vm.kernel,
5702                        vm_id,
5703                        &vm.dns,
5704                        host,
5705                        DnsLookupPolicy::SkipPermissions,
5706                    )?,
5707                    host,
5708                )?
5709            };
5710            let mut headers = BTreeMap::new();
5711            for (name, value) in &request.headers {
5712                headers.insert(name.clone(), Value::String(value.clone()));
5713            }
5714            let options = JavascriptHttpRequestOptions {
5715                method: Some(
5716                    request
5717                        .http_method
5718                        .clone()
5719                        .unwrap_or_else(|| String::from("GET")),
5720                ),
5721                headers,
5722                body: request.body_base64.as_deref().map(|body| {
5723                    String::from_utf8(
5724                        base64::engine::general_purpose::STANDARD
5725                            .decode(body)
5726                            .unwrap_or_default(),
5727                    )
5728                    .unwrap_or_default()
5729                }),
5730                reject_unauthorized: None,
5731            };
5732            let headers =
5733                parse_http_header_collection(&options.headers, "python httpRequest headers")?;
5734            let response =
5735                issue_outbound_http_request(&url, &options, &headers, &pinned_addresses)?;
5736            let payload_json = response.as_str().ok_or_else(|| {
5737                SidecarError::Execution(String::from(
5738                    "python httpRequest returned a non-string response payload",
5739                ))
5740            })?;
5741            let payload: Value = serde_json::from_str(payload_json).map_err(|error| {
5742                SidecarError::Execution(format!(
5743                    "python httpRequest response must be valid JSON: {error}"
5744                ))
5745            })?;
5746            let header_map = payload
5747                .get("headers")
5748                .and_then(Value::as_array)
5749                .map(|entries| {
5750                    let mut normalized = BTreeMap::<String, Vec<String>>::new();
5751                    for entry in entries {
5752                        let Some(pair) = entry.as_array() else {
5753                            continue;
5754                        };
5755                        let Some(name) = pair.first().and_then(Value::as_str) else {
5756                            continue;
5757                        };
5758                        let Some(value) = pair.get(1).and_then(Value::as_str) else {
5759                            continue;
5760                        };
5761                        normalized
5762                            .entry(name.to_owned())
5763                            .or_default()
5764                            .push(value.to_owned());
5765                    }
5766                    normalized
5767                })
5768                .unwrap_or_default();
5769            Ok(PythonVfsRpcResponsePayload::Http {
5770                status: payload
5771                    .get("status")
5772                    .and_then(Value::as_u64)
5773                    .map(|value| value as u16)
5774                    .unwrap_or_default(),
5775                reason: payload
5776                    .get("statusText")
5777                    .and_then(Value::as_str)
5778                    .unwrap_or_default()
5779                    .to_owned(),
5780                url: payload
5781                    .get("url")
5782                    .and_then(Value::as_str)
5783                    .unwrap_or(url_text)
5784                    .to_owned(),
5785                headers: header_map,
5786                body_base64: payload
5787                    .get("body")
5788                    .and_then(Value::as_str)
5789                    .unwrap_or_default()
5790                    .to_owned(),
5791            })
5792        })();
5793
5794        self.respond_python_rpc(vm_id, process_id, request.id, response)
5795    }
5796
5797    fn handle_python_dns_rpc_request(
5798        &mut self,
5799        vm_id: &str,
5800        process_id: &str,
5801        request: PythonVfsRpcRequest,
5802    ) -> Result<(), SidecarError> {
5803        let Some(vm) = self.vms.get(vm_id) else {
5804            return Ok(());
5805        };
5806        if !vm.active_processes.contains_key(process_id) {
5807            return Ok(());
5808        }
5809        let response = (|| {
5810            let hostname = request.hostname.as_deref().ok_or_else(|| {
5811                SidecarError::InvalidState(String::from("python dnsLookup requires a hostname"))
5812            })?;
5813            let mut addresses = filter_dns_safe_ip_addrs(
5814                resolve_dns_ip_addrs(
5815                    &self.bridge,
5816                    &vm.kernel,
5817                    vm_id,
5818                    &vm.dns,
5819                    hostname,
5820                    DnsLookupPolicy::CheckPermissions,
5821                )?,
5822                hostname,
5823            )?;
5824            if let Some(family) = request.family {
5825                addresses.retain(|address| {
5826                    matches!((family, address), (4, IpAddr::V4(_)) | (6, IpAddr::V6(_)))
5827                });
5828            }
5829            Ok(PythonVfsRpcResponsePayload::DnsLookup {
5830                addresses: addresses
5831                    .into_iter()
5832                    .map(|address| address.to_string())
5833                    .collect(),
5834            })
5835        })();
5836
5837        self.respond_python_rpc(vm_id, process_id, request.id, response)
5838    }
5839
5840    fn handle_python_subprocess_rpc_request(
5841        &mut self,
5842        vm_id: &str,
5843        process_id: &str,
5844        request: PythonVfsRpcRequest,
5845    ) -> Result<(), SidecarError> {
5846        let command = request.command.clone().ok_or_else(|| {
5847            SidecarError::InvalidState(String::from("python subprocessRun requires a command"))
5848        })?;
5849        let (internal_bootstrap_env, cwd) = {
5850            let Some(vm) = self.vms.get(vm_id) else {
5851                return Ok(());
5852            };
5853            let Some(process) = vm.active_processes.get(process_id) else {
5854                return Ok(());
5855            };
5856            let virtual_home = guest_virtual_home(vm);
5857            let cwd = request.cwd.clone().or_else(|| {
5858                guest_runtime_path_for_host_path(
5859                    &vm.guest_env,
5860                    &virtual_home,
5861                    &vm.host_cwd,
5862                    &process.host_cwd.to_string_lossy(),
5863                )
5864            });
5865            (
5866                sanitize_javascript_child_process_internal_bootstrap_env(&vm.guest_env),
5867                cwd,
5868            )
5869        };
5870        let response = self
5871            .spawn_javascript_child_process_sync(
5872                vm_id,
5873                process_id,
5874                JavascriptChildProcessSpawnRequest {
5875                    command,
5876                    args: request.args.clone(),
5877                    options: JavascriptChildProcessSpawnOptions {
5878                        cwd,
5879                        env: request.env.clone(),
5880                        input: None,
5881                        internal_bootstrap_env,
5882                        shell: request.shell,
5883                        detached: false,
5884                        stdio: vec![
5885                            String::from("pipe"),
5886                            String::from("pipe"),
5887                            String::from("pipe"),
5888                        ],
5889                        timeout: None,
5890                        kill_signal: None,
5891                    },
5892                },
5893                request.max_buffer,
5894            )
5895            .map(|payload| PythonVfsRpcResponsePayload::SubprocessRun {
5896                exit_code: payload
5897                    .get("code")
5898                    .and_then(Value::as_i64)
5899                    .map(|value| value as i32)
5900                    .unwrap_or(1),
5901                stdout: payload
5902                    .get("stdout")
5903                    .and_then(Value::as_str)
5904                    .unwrap_or_default()
5905                    .to_owned(),
5906                stderr: payload
5907                    .get("stderr")
5908                    .and_then(Value::as_str)
5909                    .unwrap_or_default()
5910                    .to_owned(),
5911                max_buffer_exceeded: payload
5912                    .get("maxBufferExceeded")
5913                    .and_then(Value::as_bool)
5914                    .unwrap_or(false),
5915            });
5916
5917        self.respond_python_rpc(vm_id, process_id, request.id, response)
5918    }
5919
5920    fn handle_python_socket_rpc_request(
5921        &mut self,
5922        vm_id: &str,
5923        process_id: &str,
5924        request: PythonVfsRpcRequest,
5925    ) -> Result<(), SidecarError> {
5926        if !self.vms.contains_key(vm_id) {
5927            return Ok(());
5928        }
5929        let response = self.python_socket_op(vm_id, process_id, &request);
5930        self.respond_python_rpc(vm_id, process_id, request.id, response)
5931    }
5932
5933    fn python_socket_op(
5934        &mut self,
5935        vm_id: &str,
5936        process_id: &str,
5937        request: &PythonVfsRpcRequest,
5938    ) -> Result<PythonVfsRpcResponsePayload, SidecarError> {
5939        match request.method {
5940            PythonVfsRpcMethod::SocketConnect => {
5941                let host = python_socket_host(request)?;
5942                let port = python_socket_port(request)?;
5943                self.check_python_socket_limit(vm_id)?;
5944                self.bridge.require_network_access(
5945                    vm_id,
5946                    NetworkOperation::Http,
5947                    format_tcp_resource(&host, port),
5948                )?;
5949                let pinned = self.python_socket_pinned_addrs(vm_id, &host, port)?;
5950                let stream = python_connect_tcp(&pinned, port)?;
5951                stream
5952                    .set_read_timeout(Some(PYTHON_SOCKET_READ_POLL))
5953                    .map_err(python_socket_io_error)?;
5954                // Bound writes too: without a write timeout `write_all` on a
5955                // stalled peer would block the shared event loop indefinitely.
5956                stream
5957                    .set_write_timeout(Some(PYTHON_SOCKET_WRITE_TIMEOUT))
5958                    .map_err(python_socket_io_error)?;
5959                let socket_id =
5960                    self.store_python_socket(vm_id, process_id, PythonHostSocket::Tcp(stream))?;
5961                Ok(PythonVfsRpcResponsePayload::SocketCreated { socket_id })
5962            }
5963            PythonVfsRpcMethod::SocketSend => {
5964                let data = python_socket_payload(request)?;
5965                let socket = self.python_socket_mut(vm_id, process_id, request)?;
5966                let PythonHostSocket::Tcp(stream) = socket else {
5967                    return Err(python_socket_kind_error("send", "TCP"));
5968                };
5969                stream.write_all(&data).map_err(python_socket_io_error)?;
5970                Ok(PythonVfsRpcResponsePayload::SocketSent {
5971                    bytes_sent: data.len(),
5972                })
5973            }
5974            PythonVfsRpcMethod::SocketRecv => {
5975                let max = python_socket_recv_len(request);
5976                let socket = self.python_socket_mut(vm_id, process_id, request)?;
5977                let PythonHostSocket::Tcp(stream) = socket else {
5978                    return Err(python_socket_kind_error("recv", "TCP"));
5979                };
5980                let mut buf = vec![0u8; max];
5981                match stream.read(&mut buf) {
5982                    Ok(0) => Ok(PythonVfsRpcResponsePayload::SocketReceived {
5983                        data_base64: String::new(),
5984                        closed: true,
5985                        timed_out: false,
5986                    }),
5987                    Ok(n) => Ok(PythonVfsRpcResponsePayload::SocketReceived {
5988                        data_base64: base64::engine::general_purpose::STANDARD.encode(&buf[..n]),
5989                        closed: false,
5990                        timed_out: false,
5991                    }),
5992                    Err(error) if python_socket_would_block(&error) => {
5993                        Ok(PythonVfsRpcResponsePayload::SocketReceived {
5994                            data_base64: String::new(),
5995                            closed: false,
5996                            timed_out: true,
5997                        })
5998                    }
5999                    Err(error) => Err(python_socket_io_error(error)),
6000                }
6001            }
6002            PythonVfsRpcMethod::SocketClose => {
6003                self.remove_python_socket(vm_id, process_id, request);
6004                Ok(PythonVfsRpcResponsePayload::Empty)
6005            }
6006            PythonVfsRpcMethod::UdpCreate => {
6007                self.check_python_socket_limit(vm_id)?;
6008                let socket = UdpSocket::bind("0.0.0.0:0").map_err(python_socket_io_error)?;
6009                socket
6010                    .set_read_timeout(Some(PYTHON_SOCKET_READ_POLL))
6011                    .map_err(python_socket_io_error)?;
6012                let socket_id =
6013                    self.store_python_socket(vm_id, process_id, PythonHostSocket::Udp(socket))?;
6014                Ok(PythonVfsRpcResponsePayload::SocketCreated { socket_id })
6015            }
6016            PythonVfsRpcMethod::UdpSendto => {
6017                let host = python_socket_host(request)?;
6018                let port = python_socket_port(request)?;
6019                let data = python_socket_payload(request)?;
6020                self.bridge.require_network_access(
6021                    vm_id,
6022                    NetworkOperation::Http,
6023                    format_tcp_resource(&host, port),
6024                )?;
6025                let pinned = self.python_socket_pinned_addrs(vm_id, &host, port)?;
6026                let target = pinned
6027                    .first()
6028                    .map(|ip| SocketAddr::new(*ip, port))
6029                    .ok_or_else(|| {
6030                        SidecarError::Execution(format!("EAI_NONAME: cannot resolve {host}"))
6031                    })?;
6032                let socket = self.python_socket_mut(vm_id, process_id, request)?;
6033                let PythonHostSocket::Udp(udp) = socket else {
6034                    return Err(python_socket_kind_error("sendto", "UDP"));
6035                };
6036                let sent = udp.send_to(&data, target).map_err(python_socket_io_error)?;
6037                Ok(PythonVfsRpcResponsePayload::SocketSent { bytes_sent: sent })
6038            }
6039            PythonVfsRpcMethod::UdpRecvfrom => {
6040                let max = python_socket_recv_len(request);
6041                let socket = self.python_socket_mut(vm_id, process_id, request)?;
6042                let PythonHostSocket::Udp(udp) = socket else {
6043                    return Err(python_socket_kind_error("recvfrom", "UDP"));
6044                };
6045                let mut buf = vec![0u8; max];
6046                match udp.recv_from(&mut buf) {
6047                    Ok((n, addr)) => Ok(PythonVfsRpcResponsePayload::UdpReceived {
6048                        data_base64: base64::engine::general_purpose::STANDARD.encode(&buf[..n]),
6049                        host: addr.ip().to_string(),
6050                        port: addr.port(),
6051                        timed_out: false,
6052                    }),
6053                    Err(error) if python_socket_would_block(&error) => {
6054                        Ok(PythonVfsRpcResponsePayload::UdpReceived {
6055                            data_base64: String::new(),
6056                            host: String::new(),
6057                            port: 0,
6058                            timed_out: true,
6059                        })
6060                    }
6061                    Err(error) => Err(python_socket_io_error(error)),
6062                }
6063            }
6064            _ => Err(SidecarError::InvalidState(String::from(
6065                "non-socket python RPC reached the socket dispatcher unexpectedly",
6066            ))),
6067        }
6068    }
6069
6070    /// Resolve `host` to the egress-guard-approved IP set, then apply the same
6071    /// loopback-connect gate the JS raw-TCP path uses (`filter_tcp_connect_ip_addrs`)
6072    /// so a rebinding DNS server can't map an allowlisted hostname onto a
6073    /// sidecar-local loopback port the VM policy didn't open.
6074    fn python_socket_pinned_addrs(
6075        &self,
6076        vm_id: &str,
6077        host: &str,
6078        port: u16,
6079    ) -> Result<Vec<IpAddr>, SidecarError> {
6080        let Some(vm) = self.vms.get(vm_id) else {
6081            return Err(SidecarError::InvalidState(String::from(
6082                "python socket op for unknown vm",
6083            )));
6084        };
6085        let context = build_javascript_socket_path_context(vm)?;
6086        if let Ok(literal_ip) = host.parse::<IpAddr>() {
6087            filter_tcp_connect_ip_addrs(vec![literal_ip], host, port, &context)
6088        } else {
6089            filter_tcp_connect_ip_addrs(
6090                resolve_dns_ip_addrs(
6091                    &self.bridge,
6092                    &vm.kernel,
6093                    vm_id,
6094                    &vm.dns,
6095                    host,
6096                    DnsLookupPolicy::SkipPermissions,
6097                )?,
6098                host,
6099                port,
6100                &context,
6101            )
6102        }
6103    }
6104
6105    /// Enforce the VM's `max_sockets` resource limit before opening another
6106    /// host socket for the guest (the registry is otherwise unbounded — a
6107    /// hostile guest could exhaust the sidecar's fds/memory).
6108    fn check_python_socket_limit(&self, vm_id: &str) -> Result<(), SidecarError> {
6109        let Some(vm) = self.vms.get(vm_id) else {
6110            return Ok(());
6111        };
6112        let limit = vm.kernel.resource_limits().max_sockets;
6113        let current = vm_network_resource_counts(vm).sockets;
6114        check_network_resource_limit(limit, current, 1, "socket")
6115    }
6116
6117    fn store_python_socket(
6118        &mut self,
6119        vm_id: &str,
6120        process_id: &str,
6121        socket: PythonHostSocket,
6122    ) -> Result<u64, SidecarError> {
6123        let process = self
6124            .vms
6125            .get_mut(vm_id)
6126            .and_then(|vm| vm.active_processes.get_mut(process_id))
6127            .ok_or_else(|| {
6128                SidecarError::InvalidState(String::from("python socket op for reaped vm/process"))
6129            })?;
6130        let socket_id = process.next_python_socket_id;
6131        process.next_python_socket_id = process.next_python_socket_id.wrapping_add(1);
6132        process.python_sockets.insert(socket_id, socket);
6133        Ok(socket_id)
6134    }
6135
6136    fn python_socket_mut(
6137        &mut self,
6138        vm_id: &str,
6139        process_id: &str,
6140        request: &PythonVfsRpcRequest,
6141    ) -> Result<&mut PythonHostSocket, SidecarError> {
6142        let socket_id = request.socket_id.ok_or_else(|| {
6143            SidecarError::InvalidState(String::from("python socket op requires socketId"))
6144        })?;
6145        self.vms
6146            .get_mut(vm_id)
6147            .and_then(|vm| vm.active_processes.get_mut(process_id))
6148            .and_then(|process| process.python_sockets.get_mut(&socket_id))
6149            .ok_or_else(|| {
6150                SidecarError::Execution(format!("EBADF: unknown python socket {socket_id}"))
6151            })
6152    }
6153
6154    fn remove_python_socket(
6155        &mut self,
6156        vm_id: &str,
6157        process_id: &str,
6158        request: &PythonVfsRpcRequest,
6159    ) {
6160        let Some(socket_id) = request.socket_id else {
6161            return;
6162        };
6163        if let Some(process) = self
6164            .vms
6165            .get_mut(vm_id)
6166            .and_then(|vm| vm.active_processes.get_mut(process_id))
6167        {
6168            process.python_sockets.remove(&socket_id);
6169        }
6170    }
6171
6172    fn respond_python_rpc(
6173        &mut self,
6174        vm_id: &str,
6175        process_id: &str,
6176        request_id: u64,
6177        response: Result<PythonVfsRpcResponsePayload, SidecarError>,
6178    ) -> Result<(), SidecarError> {
6179        let Some(vm) = self.vms.get_mut(vm_id) else {
6180            return Ok(());
6181        };
6182        let Some(process) = vm.active_processes.get_mut(process_id) else {
6183            return Ok(());
6184        };
6185        let result = match response {
6186            Ok(payload) => process
6187                .execution
6188                .respond_python_vfs_rpc_success(request_id, payload),
6189            Err(error) => process.execution.respond_python_vfs_rpc_error(
6190                request_id,
6191                "ERR_AGENTOS_PYTHON_VFS_RPC",
6192                error.to_string(),
6193            ),
6194        };
6195        match result {
6196            Ok(()) => Ok(()),
6197            Err(error) if is_broken_pipe_error(&error) => Ok(()),
6198            Err(error) => Err(error),
6199        }
6200    }
6201
6202    pub(crate) fn resolve_javascript_child_process_execution(
6203        &self,
6204        vm: &VmState,
6205        parent_env: &BTreeMap<String, String>,
6206        parent_guest_cwd: &str,
6207        parent_host_cwd: &Path,
6208        request: &JavascriptChildProcessSpawnRequest,
6209    ) -> Result<ResolvedChildProcessExecution, SidecarError> {
6210        let mut runtime_env = parent_env.clone();
6211        runtime_env.extend(request.options.internal_bootstrap_env.clone());
6212        let (guest_cwd, host_cwd_override) = request
6213            .options
6214            .cwd
6215            .as_deref()
6216            .map(|cwd| {
6217                let normalized_parent_host_cwd = normalize_host_path(parent_host_cwd);
6218                let requested_host_cwd = normalize_host_path(Path::new(cwd));
6219                if path_is_within_root(&requested_host_cwd, &normalized_parent_host_cwd) {
6220                    let relative = requested_host_cwd
6221                        .strip_prefix(&normalized_parent_host_cwd)
6222                        .unwrap_or_else(|_| Path::new(""));
6223                    let relative = relative.to_string_lossy().replace('\\', "/");
6224                    let guest_cwd = if relative.is_empty() {
6225                        parent_guest_cwd.to_owned()
6226                    } else {
6227                        normalize_path(&format!("{parent_guest_cwd}/{relative}"))
6228                    };
6229                    (guest_cwd, Some(requested_host_cwd))
6230                } else if Path::new(cwd).is_relative() {
6231                    (
6232                        normalize_path(&format!("{parent_guest_cwd}/{cwd}")),
6233                        Some(normalize_host_path(&parent_host_cwd.join(cwd))),
6234                    )
6235                } else {
6236                    (normalize_path(cwd), None)
6237                }
6238            })
6239            .unwrap_or_else(|| (parent_guest_cwd.to_owned(), None));
6240        let inherited_host_cwd = (host_cwd_override.is_none() && guest_cwd == parent_guest_cwd)
6241            .then(|| normalize_host_path(parent_host_cwd));
6242        let host_cwd = host_cwd_override
6243            .or(inherited_host_cwd)
6244            .or_else(|| {
6245                host_runtime_path_for_guest_path_with_env(
6246                    vm,
6247                    &runtime_env,
6248                    &guest_cwd,
6249                    parent_host_cwd,
6250                )
6251            })
6252            .unwrap_or_else(|| {
6253                let candidate = PathBuf::from(&guest_cwd);
6254                if guest_cwd == parent_guest_cwd {
6255                    normalize_host_path(parent_host_cwd)
6256                } else if candidate.is_absolute() {
6257                    shadow_path_for_guest(vm, &guest_cwd)
6258                } else {
6259                    vm.host_cwd.clone()
6260                }
6261            });
6262        let mut env = parent_env.clone();
6263        env.extend(request.options.env.clone());
6264        // Child JavaScript executions must resolve their own entrypoint/eval state.
6265        // Reusing the parent's values makes the sidecar load the wrong source file.
6266        env.remove("AGENTOS_GUEST_ENTRYPOINT");
6267        env.remove("AGENTOS_NODE_EVAL");
6268
6269        let (command, process_args) = if request.options.shell {
6270            let tokens = tokenize_shell_free_command(&request.command);
6271            let requires_shell = command_requires_shell(&request.command)
6272                || tokens.first().is_some_and(|command| {
6273                    is_posix_shell_builtin(command) || shell_first_token_requires_shell(command)
6274                });
6275            if requires_shell {
6276                if !vm.command_guest_paths.contains_key("sh") {
6277                    return Err(SidecarError::InvalidState(format!(
6278                        "shell-mode child_process command requires /bin/sh, which is not \
6279                         installed in this VM (install a software package that provides sh, \
6280                         for example @agentos-software/coreutils): {}",
6281                        request.command
6282                    )));
6283                }
6284                (
6285                    String::from("sh"),
6286                    vec![String::from("-c"), request.command.clone()],
6287                )
6288            } else {
6289                let Some((command, args)) = tokens.split_first() else {
6290                    return Err(SidecarError::InvalidState(String::from(
6291                        "child_process shell command must not be empty",
6292                    )));
6293                };
6294                (command.clone(), args.to_vec())
6295            }
6296        } else {
6297            (request.command.clone(), request.args.clone())
6298        };
6299        let process_args = apply_shell_cwd_prefix(&command, process_args, &guest_cwd);
6300        if is_tool_command(vm, &command) {
6301            let command = normalized_tool_command_name(&command).unwrap_or(command);
6302            return Ok(ResolvedChildProcessExecution {
6303                command: command.clone(),
6304                process_args: std::iter::once(command.clone())
6305                    .chain(process_args.iter().cloned())
6306                    .collect(),
6307                runtime: GuestRuntimeKind::JavaScript,
6308                entrypoint: command,
6309                execution_args: process_args,
6310                env,
6311                guest_cwd,
6312                host_cwd,
6313                wasm_permission_tier: None,
6314                tool_command: true,
6315            });
6316        }
6317
6318        if is_path_like_specifier(&command)
6319            && matches!(
6320                Path::new(&command).extension().and_then(|ext| ext.to_str()),
6321                Some("js" | "mjs" | "cjs" | "ts" | "mts" | "cts")
6322            )
6323        {
6324            let guest_entrypoint = if command.starts_with('/') {
6325                normalize_path(&command)
6326            } else if command.starts_with("file:") {
6327                normalize_path(command.trim_start_matches("file:"))
6328            } else {
6329                normalize_path(&format!("{guest_cwd}/{command}"))
6330            };
6331            let host_entrypoint = if command.starts_with("./") || command.starts_with("../") {
6332                normalize_host_path(&host_cwd.join(&command))
6333            } else {
6334                host_runtime_path_for_guest_path_with_env(
6335                    vm,
6336                    &runtime_env,
6337                    &guest_entrypoint,
6338                    parent_host_cwd,
6339                )
6340                .unwrap_or_else(|| {
6341                    let candidate = PathBuf::from(&guest_entrypoint);
6342                    if candidate.is_absolute() {
6343                        candidate
6344                    } else {
6345                        host_cwd.join(&guest_entrypoint)
6346                    }
6347                })
6348            };
6349            env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint);
6350            let guest_entrypoint = env.get("AGENTOS_GUEST_ENTRYPOINT").cloned();
6351            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
6352
6353            return Ok(ResolvedChildProcessExecution {
6354                command: command.clone(),
6355                process_args: std::iter::once(command)
6356                    .chain(process_args.iter().cloned())
6357                    .collect(),
6358                runtime: GuestRuntimeKind::JavaScript,
6359                entrypoint: host_entrypoint.to_string_lossy().into_owned(),
6360                execution_args: process_args,
6361                env,
6362                guest_cwd,
6363                host_cwd,
6364                wasm_permission_tier: None,
6365                tool_command: false,
6366            });
6367        }
6368
6369        if is_node_runtime_command(&command) {
6370            if let Some(cli) = resolve_host_node_cli_entrypoint(&command) {
6371                env.insert(
6372                    String::from("AGENTOS_NODE_EVAL"),
6373                    build_host_node_cli_eval(&cli),
6374                );
6375                prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
6376                add_runtime_guest_path_mapping(&mut env, &cli.guest_root, &cli.package_root);
6377                add_runtime_host_access_path(
6378                    &mut env,
6379                    "AGENTOS_EXTRA_FS_READ_PATHS",
6380                    &cli.package_root,
6381                    true,
6382                );
6383
6384                return Ok(ResolvedChildProcessExecution {
6385                    command: command.clone(),
6386                    process_args: std::iter::once(command.clone())
6387                        .chain(process_args.iter().cloned())
6388                        .collect(),
6389                    runtime: GuestRuntimeKind::JavaScript,
6390                    entrypoint: String::from("-e"),
6391                    execution_args: std::iter::once(cli.guest_entrypoint.clone())
6392                        .chain(process_args.iter().cloned())
6393                        .collect(),
6394                    env,
6395                    guest_cwd,
6396                    host_cwd,
6397                    wasm_permission_tier: None,
6398                    tool_command: false,
6399                });
6400            }
6401
6402            if process_args.is_empty() {
6403                env.insert(String::from("AGENTOS_NODE_EVAL"), String::new());
6404                prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
6405
6406                return Ok(ResolvedChildProcessExecution {
6407                    command: command.clone(),
6408                    process_args: vec![command.clone()],
6409                    runtime: GuestRuntimeKind::JavaScript,
6410                    entrypoint: String::from("-e"),
6411                    execution_args: Vec::new(),
6412                    env,
6413                    guest_cwd,
6414                    host_cwd,
6415                    wasm_permission_tier: None,
6416                    tool_command: false,
6417                });
6418            }
6419
6420            if let Some((entrypoint, execution_args)) =
6421                resolve_special_node_cli_invocation(&process_args, &mut env)
6422            {
6423                prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
6424
6425                return Ok(ResolvedChildProcessExecution {
6426                    command: command.clone(),
6427                    process_args: std::iter::once(command.clone())
6428                        .chain(process_args.iter().cloned())
6429                        .collect(),
6430                    runtime: GuestRuntimeKind::JavaScript,
6431                    entrypoint,
6432                    execution_args,
6433                    env,
6434                    guest_cwd,
6435                    host_cwd,
6436                    wasm_permission_tier: None,
6437                    tool_command: false,
6438                });
6439            }
6440
6441            let Some(entrypoint_specifier) = process_args.first() else {
6442                return Err(SidecarError::InvalidState(format!(
6443                    "{command} child_process spawn requires an entrypoint"
6444                )));
6445            };
6446
6447            let (entrypoint, execution_args) = if is_path_like_specifier(entrypoint_specifier) {
6448                let guest_entrypoint = if entrypoint_specifier.starts_with('/') {
6449                    normalize_path(entrypoint_specifier)
6450                } else if entrypoint_specifier.starts_with("file:") {
6451                    normalize_path(entrypoint_specifier.trim_start_matches("file:"))
6452                } else {
6453                    normalize_path(&format!("{guest_cwd}/{entrypoint_specifier}"))
6454                };
6455                let host_entrypoint = if entrypoint_specifier.starts_with("./")
6456                    || entrypoint_specifier.starts_with("../")
6457                {
6458                    normalize_host_path(&host_cwd.join(entrypoint_specifier))
6459                } else {
6460                    host_runtime_path_for_guest_path_with_env(
6461                        vm,
6462                        &runtime_env,
6463                        &guest_entrypoint,
6464                        parent_host_cwd,
6465                    )
6466                    .unwrap_or_else(|| {
6467                        let candidate = PathBuf::from(&guest_entrypoint);
6468                        if candidate.is_absolute() {
6469                            candidate
6470                        } else {
6471                            host_cwd.join(&guest_entrypoint)
6472                        }
6473                    })
6474                };
6475                env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint);
6476                (
6477                    host_entrypoint.to_string_lossy().into_owned(),
6478                    process_args.iter().skip(1).cloned().collect(),
6479                )
6480            } else {
6481                (
6482                    entrypoint_specifier.clone(),
6483                    process_args.iter().skip(1).cloned().collect(),
6484                )
6485            };
6486            let guest_entrypoint = env.get("AGENTOS_GUEST_ENTRYPOINT").cloned();
6487            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
6488
6489            return Ok(ResolvedChildProcessExecution {
6490                command: command.clone(),
6491                process_args: std::iter::once(command)
6492                    .chain(process_args.iter().cloned())
6493                    .collect(),
6494                runtime: GuestRuntimeKind::JavaScript,
6495                entrypoint,
6496                execution_args,
6497                env,
6498                guest_cwd,
6499                host_cwd,
6500                wasm_permission_tier: None,
6501                tool_command: false,
6502            });
6503        }
6504
6505        if is_python_runtime_command(&command) {
6506            return resolve_python_command_execution(
6507                vm,
6508                &command,
6509                &process_args,
6510                env,
6511                guest_cwd,
6512                host_cwd,
6513            );
6514        }
6515
6516        let guest_entrypoint = resolve_guest_command_entrypoint(
6517            vm,
6518            &guest_cwd,
6519            &command,
6520            env.get("PATH").map(String::as_str),
6521        )
6522        .ok_or_else(|| SidecarError::InvalidState(format!("command not found: {command}")))?;
6523        let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint);
6524        let wasm_permission_tier = vm.command_permissions.get(&command).copied().or_else(|| {
6525            Path::new(&guest_entrypoint)
6526                .file_name()
6527                .and_then(|name| name.to_str())
6528                .and_then(|name| vm.command_permissions.get(name).copied())
6529        });
6530        if let Some((javascript_guest_entrypoint, javascript_host_entrypoint)) =
6531            resolve_javascript_command_entrypoint(vm, &guest_entrypoint, &host_entrypoint)
6532        {
6533            prepare_guest_runtime_env(
6534                vm,
6535                &mut env,
6536                &guest_cwd,
6537                &host_cwd,
6538                Some(javascript_guest_entrypoint),
6539            )?;
6540
6541            return Ok(ResolvedChildProcessExecution {
6542                command: command.clone(),
6543                process_args: std::iter::once(command)
6544                    .chain(process_args.iter().cloned())
6545                    .collect(),
6546                runtime: GuestRuntimeKind::JavaScript,
6547                entrypoint: javascript_host_entrypoint.to_string_lossy().into_owned(),
6548                execution_args: process_args,
6549                env,
6550                guest_cwd,
6551                host_cwd,
6552                wasm_permission_tier: None,
6553                tool_command: false,
6554            });
6555        }
6556        prepare_guest_runtime_env(
6557            vm,
6558            &mut env,
6559            &guest_cwd,
6560            &host_cwd,
6561            Some(guest_entrypoint.clone()),
6562        )?;
6563
6564        Ok(ResolvedChildProcessExecution {
6565            command: command.clone(),
6566            process_args: std::iter::once(command)
6567                .chain(process_args.iter().cloned())
6568                .collect(),
6569            runtime: GuestRuntimeKind::WebAssembly,
6570            entrypoint: host_entrypoint.to_string_lossy().into_owned(),
6571            execution_args: process_args,
6572            env,
6573            guest_cwd,
6574            host_cwd,
6575            wasm_permission_tier,
6576            tool_command: false,
6577        })
6578    }
6579
6580    pub(crate) fn spawn_javascript_child_process(
6581        &mut self,
6582        vm_id: &str,
6583        process_id: &str,
6584        request: JavascriptChildProcessSpawnRequest,
6585    ) -> Result<Value, SidecarError> {
6586        let total_start = Instant::now();
6587        let phase_start = Instant::now();
6588        let mut resolved = {
6589            let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?;
6590            let parent = vm
6591                .active_processes
6592                .get(process_id)
6593                .ok_or_else(|| missing_process_error(vm_id, process_id))?;
6594            self.resolve_javascript_child_process_execution(
6595                vm,
6596                &parent.env,
6597                &parent.guest_cwd,
6598                &parent.host_cwd,
6599                &request,
6600            )?
6601        };
6602        {
6603            let vm = self
6604                .vms
6605                .get_mut(vm_id)
6606                .ok_or_else(|| missing_vm_error(vm_id))?;
6607            stage_agentos_package_command(vm, &mut resolved)?;
6608        }
6609        let resolved = resolved;
6610        record_execute_phase("child_process_resolve_execution", phase_start.elapsed());
6611        let (parent_kernel_pid, child_process_id) = {
6612            let vm = self
6613                .vms
6614                .get_mut(vm_id)
6615                .ok_or_else(|| missing_vm_error(vm_id))?;
6616            let process = vm
6617                .active_processes
6618                .get_mut(process_id)
6619                .ok_or_else(|| missing_process_error(vm_id, process_id))?;
6620            (process.kernel_pid, process.allocate_child_process_id())
6621        };
6622        let sidecar_requests = self.sidecar_requests.clone();
6623        let vm = self
6624            .vms
6625            .get_mut(vm_id)
6626            .ok_or_else(|| missing_vm_error(vm_id))?;
6627        let phase_start = Instant::now();
6628        let (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd) = if resolved
6629            .tool_command
6630        {
6631            let tool_resolution = resolve_tool_command(
6632                vm,
6633                &resolved.command,
6634                &resolved.execution_args,
6635                Some(&resolved.guest_cwd),
6636            )?
6637            .ok_or_else(|| {
6638                SidecarError::InvalidState(format!(
6639                    "tool command no longer resolves: {}",
6640                    resolved.command
6641                ))
6642            })?;
6643            let kernel_handle = vm
6644                .kernel
6645                .create_virtual_process(
6646                    EXECUTION_DRIVER_NAME,
6647                    TOOL_DRIVER_NAME,
6648                    &resolved.command,
6649                    resolved.process_args.clone(),
6650                    VirtualProcessOptions {
6651                        parent_pid: Some(parent_kernel_pid),
6652                        env: resolved.env.clone(),
6653                        cwd: Some(resolved.guest_cwd.clone()),
6654                    },
6655                )
6656                .map_err(kernel_error)?;
6657            let kernel_pid = kernel_handle.pid();
6658            let tool_execution = ToolExecution::default();
6659            let cancelled = tool_execution.cancelled.clone();
6660            let pending_events = tool_execution.pending_events.clone();
6661            let events_overflowed = tool_execution.events_overflowed.clone();
6662            spawn_tool_process_events(ToolProcessEventRequest {
6663                sidecar_requests: sidecar_requests.clone(),
6664                connection_id: vm.connection_id.clone(),
6665                session_id: vm.session_id.clone(),
6666                vm_id: vm_id.to_owned(),
6667                tool_resolution,
6668                cancelled,
6669                pending_events,
6670                events_overflowed,
6671            });
6672            (
6673                kernel_pid,
6674                kernel_handle,
6675                ActiveExecution::Tool(tool_execution),
6676                None,
6677            )
6678        } else {
6679            let kernel_command = match resolved.runtime {
6680                GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND,
6681                GuestRuntimeKind::WebAssembly => WASM_COMMAND,
6682                GuestRuntimeKind::Python => PYTHON_COMMAND,
6683            };
6684            let kernel_handle = vm
6685                .kernel
6686                .spawn_process(
6687                    kernel_command,
6688                    resolved.process_args.clone(),
6689                    SpawnOptions {
6690                        requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
6691                        parent_pid: Some(parent_kernel_pid),
6692                        env: resolved.env.clone(),
6693                        cwd: Some(resolved.guest_cwd.clone()),
6694                    },
6695                )
6696                .map_err(kernel_error)?;
6697            let kernel_pid = kernel_handle.pid();
6698            if request.options.detached {
6699                vm.kernel
6700                    .setsid(EXECUTION_DRIVER_NAME, kernel_pid)
6701                    .map_err(kernel_error)?;
6702            }
6703            let mut execution_env = resolved.env.clone();
6704            execution_env.insert(
6705                String::from(EXECUTION_SANDBOX_ROOT_ENV),
6706                normalize_host_path(&vm.cwd).to_string_lossy().into_owned(),
6707            );
6708
6709            let execution = match resolved.runtime {
6710                GuestRuntimeKind::JavaScript => {
6711                    execution_env.extend(sanitize_javascript_child_process_internal_bootstrap_env(
6712                        &request.options.internal_bootstrap_env,
6713                    ));
6714                    execution_env
6715                        .insert(String::from("AGENTOS_KEEP_STDIN_OPEN"), String::from("1"));
6716                    let launch_entrypoint = resolve_agentos_package_javascript_launch_entrypoint(
6717                        vm,
6718                        &mut execution_env,
6719                    )
6720                    .unwrap_or_else(|| resolved.entrypoint.clone());
6721                    let context =
6722                        self.javascript_engine
6723                            .create_context(CreateJavascriptContextRequest {
6724                                vm_id: vm_id.to_owned(),
6725                                bootstrap_module: None,
6726                                compile_cache_root: Some(
6727                                    self.cache_root.join("node-compile-cache"),
6728                                ),
6729                            });
6730                    let inline_code = load_javascript_entrypoint_source(
6731                        vm,
6732                        &resolved.host_cwd,
6733                        &launch_entrypoint,
6734                        &execution_env,
6735                    );
6736                    prepare_javascript_shadow(vm, &resolved, &execution_env)?;
6737
6738                    let built_reader = build_module_reader(vm, &resolved);
6739                    let guest_reader = built_reader.clone().map(|reader| {
6740                        Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader))
6741                            as Box<dyn GuestModuleReader>
6742                    });
6743                    let module_reader = built_reader
6744                        .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
6745                    let execution = self
6746                        .javascript_engine
6747                        .start_execution_with_module_reader(
6748                            StartJavascriptExecutionRequest {
6749                                guest_runtime: guest_runtime_identity(
6750                                    vm,
6751                                    Some(u64::from(kernel_pid)),
6752                                    Some(u64::from(parent_kernel_pid)),
6753                                ),
6754                                vm_id: vm_id.to_owned(),
6755                                context_id: context.context_id,
6756                                argv: std::iter::once(launch_entrypoint)
6757                                    .chain(resolved.execution_args.clone())
6758                                    .collect(),
6759                                env: execution_env,
6760                                cwd: resolved.host_cwd.clone(),
6761                                limits: javascript_execution_limits(vm),
6762                                inline_code,
6763                                wasm_module_bytes: None,
6764                            },
6765                            module_reader,
6766                            guest_reader,
6767                        )
6768                        .map_err(javascript_error)?;
6769                    ActiveExecution::Javascript(execution)
6770                }
6771                GuestRuntimeKind::WebAssembly => {
6772                    execution_env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
6773                    let wasm_limits = wasm_execution_limits(vm);
6774                    let wasm_guest_runtime = guest_runtime_identity(
6775                        vm,
6776                        Some(u64::from(kernel_pid)),
6777                        Some(u64::from(parent_kernel_pid)),
6778                    );
6779                    let context = self.wasm_engine.create_context(CreateWasmContextRequest {
6780                        vm_id: vm_id.to_owned(),
6781                        module_path: Some(resolved.entrypoint.clone()),
6782                    });
6783                    let execution = self
6784                        .wasm_engine
6785                        .start_execution(StartWasmExecutionRequest {
6786                            vm_id: vm_id.to_owned(),
6787                            context_id: context.context_id,
6788                            argv: resolved.process_args.clone(),
6789                            env: execution_env,
6790                            cwd: resolved.host_cwd.clone(),
6791                            permission_tier: execution_wasm_permission_tier(
6792                                resolved
6793                                    .wasm_permission_tier
6794                                    .unwrap_or(WasmPermissionTier::Full),
6795                            ),
6796                            limits: wasm_limits,
6797                            guest_runtime: wasm_guest_runtime,
6798                        })
6799                        .map_err(wasm_error)?;
6800                    ActiveExecution::Wasm(Box::new(execution))
6801                }
6802                GuestRuntimeKind::Python => {
6803                    // Nested `python` child_process: set up the Pyodide context the
6804                    // same way the top-level execute path does, so a guest shell or
6805                    // node parent can spawn `python` exactly like `node`.
6806                    let python_file_path = if execution_env.contains_key("AGENTOS_PYTHON_ARGV") {
6807                        execution_env.get("AGENTOS_PYTHON_FILE").map(PathBuf::from)
6808                    } else {
6809                        python_file_entrypoint(&resolved.entrypoint)
6810                    };
6811                    let pyodide_dist_path = self
6812                        .python_engine
6813                        .bundled_pyodide_dist_path_for_vm(vm_id)
6814                        .map_err(python_error)?;
6815                    let pyodide_cache_path = pyodide_dist_path
6816                        .parent()
6817                        .and_then(Path::parent)
6818                        .unwrap_or(pyodide_dist_path.as_path())
6819                        .join("pyodide-package-cache");
6820                    add_runtime_guest_path_mapping(
6821                        &mut execution_env,
6822                        PYTHON_PYODIDE_GUEST_ROOT,
6823                        &pyodide_dist_path,
6824                    );
6825                    add_runtime_guest_path_mapping(
6826                        &mut execution_env,
6827                        PYTHON_PYODIDE_CACHE_GUEST_ROOT,
6828                        &pyodide_cache_path,
6829                    );
6830                    add_runtime_host_access_path(
6831                        &mut execution_env,
6832                        "AGENTOS_EXTRA_FS_READ_PATHS",
6833                        &pyodide_dist_path,
6834                        true,
6835                    );
6836                    add_runtime_host_access_path(
6837                        &mut execution_env,
6838                        "AGENTOS_EXTRA_FS_READ_PATHS",
6839                        &pyodide_cache_path,
6840                        true,
6841                    );
6842                    add_runtime_host_access_path(
6843                        &mut execution_env,
6844                        "AGENTOS_EXTRA_FS_WRITE_PATHS",
6845                        &pyodide_cache_path,
6846                        false,
6847                    );
6848                    let context = self
6849                        .python_engine
6850                        .create_context(CreatePythonContextRequest {
6851                            vm_id: vm_id.to_owned(),
6852                            pyodide_dist_path,
6853                        });
6854                    let execution = self
6855                        .python_engine
6856                        .start_execution(StartPythonExecutionRequest {
6857                            vm_id: vm_id.to_owned(),
6858                            context_id: context.context_id,
6859                            code: resolved.entrypoint.clone(),
6860                            file_path: python_file_path,
6861                            env: execution_env,
6862                            cwd: resolved.host_cwd.clone(),
6863                            limits: python_execution_limits(vm),
6864                            guest_runtime: guest_runtime_identity(
6865                                vm,
6866                                Some(u64::from(kernel_pid)),
6867                                Some(u64::from(parent_kernel_pid)),
6868                            ),
6869                        })
6870                        .map_err(python_error)?;
6871                    ActiveExecution::Python(execution)
6872                }
6873            };
6874            let kernel_stdin_writer_fd = match javascript_child_process_stdin_mode(&request) {
6875                "pipe" => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
6876                "ignore" => {
6877                    vm.kernel
6878                        .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, 0)
6879                        .map_err(kernel_error)?;
6880                    None
6881                }
6882                "inherit" => None,
6883                _ => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
6884            };
6885            (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd)
6886        };
6887        record_execute_phase(
6888            "child_process_spawn_and_start_execution",
6889            phase_start.elapsed(),
6890        );
6891
6892        let phase_start = Instant::now();
6893        // Shared-terminal detection: when the child's kernel fd 1 is a PTY (the
6894        // slave inherited from a TTY shell), record who owns the host-facing
6895        // master so the child's stdio writes surface through master drains
6896        // instead of child stdout events (see `tty_master_owner`).
6897        let child_fd1_is_tty = vm
6898            .kernel
6899            .isatty(EXECUTION_DRIVER_NAME, kernel_pid, 1)
6900            .unwrap_or(false);
6901        let process = vm
6902            .active_processes
6903            .get_mut(process_id)
6904            .ok_or_else(|| missing_process_error(vm_id, process_id))?;
6905        let inherited_tty_master_owner = if child_fd1_is_tty {
6906            process
6907                .tty_master_fd
6908                .map(|master_fd| (process.kernel_pid, master_fd))
6909                .or(process.tty_master_owner)
6910        } else {
6911            None
6912        };
6913        process.child_processes.insert(
6914            child_process_id.clone(),
6915            ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
6916                .with_detached(request.options.detached)
6917                .with_guest_cwd(resolved.guest_cwd.clone())
6918                .with_env(resolved.env.clone())
6919                .with_host_cwd(resolved.host_cwd.clone()),
6920        );
6921        {
6922            let child = process
6923                .child_processes
6924                .get_mut(&child_process_id)
6925                .ok_or_else(|| {
6926                    SidecarError::InvalidState(format!(
6927                        "child process {child_process_id} disappeared during spawn"
6928                    ))
6929                })?;
6930            child.tty_master_owner = inherited_tty_master_owner;
6931            if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd {
6932                child.kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd);
6933            }
6934        }
6935        record_execute_phase("child_process_register", phase_start.elapsed());
6936        record_execute_phase("child_process_spawn_total", total_start.elapsed());
6937        Ok(json!({
6938            "childId": child_process_id,
6939            "pid": kernel_pid,
6940            "command": resolved.command,
6941            "args": resolved.process_args,
6942        }))
6943    }
6944
6945    pub(crate) fn spawn_javascript_child_process_sync(
6946        &mut self,
6947        vm_id: &str,
6948        process_id: &str,
6949        request: JavascriptChildProcessSpawnRequest,
6950        max_buffer: Option<usize>,
6951    ) -> Result<Value, SidecarError> {
6952        let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?;
6953        let timeout_deadline = request
6954            .options
6955            .timeout
6956            .map(|timeout_ms| Instant::now() + Duration::from_millis(timeout_ms));
6957        let timeout_signal = request
6958            .options
6959            .kill_signal
6960            .clone()
6961            .unwrap_or_else(|| String::from("SIGTERM"));
6962        let spawned = self.spawn_javascript_child_process(vm_id, process_id, request)?;
6963        let child_process_id = spawned
6964            .get("childId")
6965            .and_then(Value::as_str)
6966            .ok_or_else(|| {
6967                SidecarError::InvalidState(String::from(
6968                    "child_process.spawn_sync response is missing childId",
6969                ))
6970            })?
6971            .to_owned();
6972
6973        if let Some(input) = sync_input.as_deref() {
6974            self.write_javascript_child_process_stdin(vm_id, process_id, &child_process_id, input)?;
6975        }
6976        self.close_javascript_child_process_stdin(vm_id, process_id, &child_process_id)?;
6977
6978        let max_buffer = max_buffer.unwrap_or(1024 * 1024);
6979        let mut stdout = Vec::new();
6980        let mut stderr = Vec::new();
6981        let mut max_buffer_exceeded = false;
6982        let mut kill_sent = false;
6983        let mut timed_out = false;
6984
6985        let exit_code = loop {
6986            let wait_ms = if let Some(deadline) = timeout_deadline {
6987                let now = Instant::now();
6988                if now >= deadline {
6989                    if !kill_sent {
6990                        timed_out = true;
6991                        self.kill_javascript_child_process(
6992                            vm_id,
6993                            process_id,
6994                            &child_process_id,
6995                            &timeout_signal,
6996                        )?;
6997                        kill_sent = true;
6998                    }
6999                    0
7000                } else {
7001                    u64::try_from(deadline.saturating_duration_since(now).as_millis().min(50))
7002                        .unwrap_or(50)
7003                }
7004            } else {
7005                50
7006            };
7007            let event =
7008                self.poll_javascript_child_process(vm_id, process_id, &child_process_id, wait_ms)?;
7009            if event.is_null() {
7010                continue;
7011            }
7012
7013            match event.get("type").and_then(Value::as_str) {
7014                Some("stdout") => {
7015                    let chunk = javascript_sync_rpc_bytes_arg(
7016                        &[event.get("data").cloned().unwrap_or(Value::Null)],
7017                        0,
7018                        "child_process.spawn_sync stdout",
7019                    )?;
7020                    stdout.extend_from_slice(&chunk);
7021                    if stdout.len() > max_buffer && !kill_sent {
7022                        max_buffer_exceeded = true;
7023                        self.kill_javascript_child_process(
7024                            vm_id,
7025                            process_id,
7026                            &child_process_id,
7027                            "SIGTERM",
7028                        )?;
7029                        kill_sent = true;
7030                    }
7031                }
7032                Some("stderr") => {
7033                    let chunk = javascript_sync_rpc_bytes_arg(
7034                        &[event.get("data").cloned().unwrap_or(Value::Null)],
7035                        0,
7036                        "child_process.spawn_sync stderr",
7037                    )?;
7038                    stderr.extend_from_slice(&chunk);
7039                    if stderr.len() > max_buffer && !kill_sent {
7040                        max_buffer_exceeded = true;
7041                        self.kill_javascript_child_process(
7042                            vm_id,
7043                            process_id,
7044                            &child_process_id,
7045                            "SIGTERM",
7046                        )?;
7047                        kill_sent = true;
7048                    }
7049                }
7050                Some("exit") => {
7051                    break event
7052                        .get("exitCode")
7053                        .and_then(Value::as_i64)
7054                        .map(|value| value as i32)
7055                        .unwrap_or(1);
7056                }
7057                _ => {}
7058            }
7059        };
7060
7061        Ok(json!({
7062            "stdout": String::from_utf8_lossy(&stdout),
7063            "stderr": String::from_utf8_lossy(&stderr),
7064            "code": exit_code,
7065            "signal": if timed_out { Value::String(timeout_signal) } else { Value::Null },
7066            "timedOut": timed_out,
7067            "maxBufferExceeded": max_buffer_exceeded,
7068        }))
7069    }
7070
7071    fn spawn_descendant_javascript_child_process(
7072        &mut self,
7073        vm_id: &str,
7074        process_id: &str,
7075        current_process_path: &[&str],
7076        request: JavascriptChildProcessSpawnRequest,
7077    ) -> Result<Value, SidecarError> {
7078        let total_start = Instant::now();
7079        let current_process_label =
7080            Self::child_process_path_label(process_id, current_process_path);
7081        let phase_start = Instant::now();
7082        let (mut resolved, parent_kernel_pid) = {
7083            let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?;
7084            let root = vm
7085                .active_processes
7086                .get(process_id)
7087                .ok_or_else(|| missing_process_error(vm_id, process_id))?;
7088            let parent =
7089                Self::active_process_by_path(root, current_process_path).ok_or_else(|| {
7090                    SidecarError::InvalidState(format!(
7091                        "unknown child process path {current_process_label} during nested spawn"
7092                    ))
7093                })?;
7094            (
7095                self.resolve_javascript_child_process_execution(
7096                    vm,
7097                    &parent.env,
7098                    &parent.guest_cwd,
7099                    &parent.host_cwd,
7100                    &request,
7101                )?,
7102                parent.kernel_pid,
7103            )
7104        };
7105        {
7106            let vm = self
7107                .vms
7108                .get_mut(vm_id)
7109                .ok_or_else(|| missing_vm_error(vm_id))?;
7110            stage_agentos_package_command(vm, &mut resolved)?;
7111        }
7112        let resolved = resolved;
7113        record_execute_phase("child_process_resolve_execution", phase_start.elapsed());
7114
7115        let sidecar_requests = self.sidecar_requests.clone();
7116        let vm = self
7117            .vms
7118            .get_mut(vm_id)
7119            .ok_or_else(|| missing_vm_error(vm_id))?;
7120        let phase_start = Instant::now();
7121        let child_process_id = {
7122            let root = vm
7123                .active_processes
7124                .get_mut(process_id)
7125                .ok_or_else(|| missing_process_error(vm_id, process_id))?;
7126            let parent =
7127                Self::active_process_by_path_mut(root, current_process_path).ok_or_else(|| {
7128                    SidecarError::InvalidState(format!(
7129                        "unknown child process path {current_process_label} during nested spawn"
7130                    ))
7131                })?;
7132            parent.allocate_child_process_id()
7133        };
7134        let mut child_path = current_process_path.to_vec();
7135        child_path.push(child_process_id.as_str());
7136        let (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd) = if resolved
7137            .tool_command
7138        {
7139            let tool_resolution = resolve_tool_command(
7140                vm,
7141                &resolved.command,
7142                &resolved.execution_args,
7143                Some(&resolved.guest_cwd),
7144            )?
7145            .ok_or_else(|| {
7146                SidecarError::InvalidState(format!(
7147                    "tool command no longer resolves: {}",
7148                    resolved.command
7149                ))
7150            })?;
7151            let kernel_handle = vm
7152                .kernel
7153                .create_virtual_process(
7154                    EXECUTION_DRIVER_NAME,
7155                    TOOL_DRIVER_NAME,
7156                    &resolved.command,
7157                    resolved.process_args.clone(),
7158                    VirtualProcessOptions {
7159                        parent_pid: Some(parent_kernel_pid),
7160                        env: resolved.env.clone(),
7161                        cwd: Some(resolved.guest_cwd.clone()),
7162                    },
7163                )
7164                .map_err(kernel_error)?;
7165            let kernel_pid = kernel_handle.pid();
7166            let tool_execution = ToolExecution::default();
7167            let cancelled = tool_execution.cancelled.clone();
7168            let pending_events = tool_execution.pending_events.clone();
7169            let events_overflowed = tool_execution.events_overflowed.clone();
7170            spawn_tool_process_events(ToolProcessEventRequest {
7171                sidecar_requests: sidecar_requests.clone(),
7172                connection_id: vm.connection_id.clone(),
7173                session_id: vm.session_id.clone(),
7174                vm_id: vm_id.to_owned(),
7175                tool_resolution,
7176                cancelled,
7177                pending_events,
7178                events_overflowed,
7179            });
7180            (
7181                kernel_pid,
7182                kernel_handle,
7183                ActiveExecution::Tool(tool_execution),
7184                None,
7185            )
7186        } else {
7187            let kernel_command = match resolved.runtime {
7188                GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND,
7189                GuestRuntimeKind::WebAssembly => WASM_COMMAND,
7190                GuestRuntimeKind::Python => PYTHON_COMMAND,
7191            };
7192            let kernel_handle = vm
7193                .kernel
7194                .spawn_process(
7195                    kernel_command,
7196                    resolved.process_args.clone(),
7197                    SpawnOptions {
7198                        requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
7199                        parent_pid: Some(parent_kernel_pid),
7200                        env: resolved.env.clone(),
7201                        cwd: Some(resolved.guest_cwd.clone()),
7202                    },
7203                )
7204                .map_err(kernel_error)?;
7205            let kernel_pid = kernel_handle.pid();
7206            if request.options.detached {
7207                vm.kernel
7208                    .setsid(EXECUTION_DRIVER_NAME, kernel_pid)
7209                    .map_err(kernel_error)?;
7210            }
7211            let mut execution_env = resolved.env.clone();
7212            execution_env.insert(
7213                String::from(EXECUTION_SANDBOX_ROOT_ENV),
7214                normalize_host_path(&vm.cwd).to_string_lossy().into_owned(),
7215            );
7216            let execution = match resolved.runtime {
7217                GuestRuntimeKind::JavaScript => {
7218                    execution_env.extend(sanitize_javascript_child_process_internal_bootstrap_env(
7219                        &request.options.internal_bootstrap_env,
7220                    ));
7221                    execution_env
7222                        .insert(String::from("AGENTOS_KEEP_STDIN_OPEN"), String::from("1"));
7223                    let launch_entrypoint = resolve_agentos_package_javascript_launch_entrypoint(
7224                        vm,
7225                        &mut execution_env,
7226                    )
7227                    .unwrap_or_else(|| resolved.entrypoint.clone());
7228                    let context =
7229                        self.javascript_engine
7230                            .create_context(CreateJavascriptContextRequest {
7231                                vm_id: vm_id.to_owned(),
7232                                bootstrap_module: None,
7233                                compile_cache_root: Some(
7234                                    self.cache_root.join("node-compile-cache"),
7235                                ),
7236                            });
7237                    let inline_code = load_javascript_entrypoint_source(
7238                        vm,
7239                        &resolved.host_cwd,
7240                        &launch_entrypoint,
7241                        &execution_env,
7242                    );
7243                    prepare_javascript_shadow(vm, &resolved, &execution_env)?;
7244
7245                    let built_reader = build_module_reader(vm, &resolved);
7246                    let guest_reader = built_reader.clone().map(|reader| {
7247                        Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader))
7248                            as Box<dyn GuestModuleReader>
7249                    });
7250                    let module_reader = built_reader
7251                        .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
7252                    let execution = self
7253                        .javascript_engine
7254                        .start_execution_with_module_reader(
7255                            StartJavascriptExecutionRequest {
7256                                guest_runtime: guest_runtime_identity(
7257                                    vm,
7258                                    Some(u64::from(kernel_pid)),
7259                                    Some(u64::from(parent_kernel_pid)),
7260                                ),
7261                                vm_id: vm_id.to_owned(),
7262                                context_id: context.context_id,
7263                                argv: std::iter::once(launch_entrypoint)
7264                                    .chain(resolved.execution_args.clone())
7265                                    .collect(),
7266                                env: execution_env,
7267                                cwd: resolved.host_cwd.clone(),
7268                                limits: javascript_execution_limits(vm),
7269                                inline_code,
7270                                wasm_module_bytes: None,
7271                            },
7272                            module_reader,
7273                            guest_reader,
7274                        )
7275                        .map_err(javascript_error)?;
7276                    ActiveExecution::Javascript(execution)
7277                }
7278                GuestRuntimeKind::WebAssembly => {
7279                    execution_env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
7280                    let wasm_limits = wasm_execution_limits(vm);
7281                    let wasm_guest_runtime = guest_runtime_identity(
7282                        vm,
7283                        Some(u64::from(kernel_pid)),
7284                        Some(u64::from(parent_kernel_pid)),
7285                    );
7286                    let context = self.wasm_engine.create_context(CreateWasmContextRequest {
7287                        vm_id: vm_id.to_owned(),
7288                        module_path: Some(resolved.entrypoint.clone()),
7289                    });
7290                    let execution = self
7291                        .wasm_engine
7292                        .start_execution(StartWasmExecutionRequest {
7293                            vm_id: vm_id.to_owned(),
7294                            context_id: context.context_id,
7295                            argv: resolved.process_args.clone(),
7296                            env: execution_env,
7297                            cwd: resolved.host_cwd.clone(),
7298                            permission_tier: execution_wasm_permission_tier(
7299                                resolved
7300                                    .wasm_permission_tier
7301                                    .unwrap_or(WasmPermissionTier::Full),
7302                            ),
7303                            limits: wasm_limits,
7304                            guest_runtime: wasm_guest_runtime,
7305                        })
7306                        .map_err(wasm_error)?;
7307                    ActiveExecution::Wasm(Box::new(execution))
7308                }
7309                GuestRuntimeKind::Python => {
7310                    // Nested `python` child_process: set up the Pyodide context the
7311                    // same way the top-level execute path does, so a guest shell or
7312                    // node parent can spawn `python` exactly like `node`.
7313                    let python_file_path = if execution_env.contains_key("AGENTOS_PYTHON_ARGV") {
7314                        execution_env.get("AGENTOS_PYTHON_FILE").map(PathBuf::from)
7315                    } else {
7316                        python_file_entrypoint(&resolved.entrypoint)
7317                    };
7318                    let pyodide_dist_path = self
7319                        .python_engine
7320                        .bundled_pyodide_dist_path_for_vm(vm_id)
7321                        .map_err(python_error)?;
7322                    let pyodide_cache_path = pyodide_dist_path
7323                        .parent()
7324                        .and_then(Path::parent)
7325                        .unwrap_or(pyodide_dist_path.as_path())
7326                        .join("pyodide-package-cache");
7327                    add_runtime_guest_path_mapping(
7328                        &mut execution_env,
7329                        PYTHON_PYODIDE_GUEST_ROOT,
7330                        &pyodide_dist_path,
7331                    );
7332                    add_runtime_guest_path_mapping(
7333                        &mut execution_env,
7334                        PYTHON_PYODIDE_CACHE_GUEST_ROOT,
7335                        &pyodide_cache_path,
7336                    );
7337                    add_runtime_host_access_path(
7338                        &mut execution_env,
7339                        "AGENTOS_EXTRA_FS_READ_PATHS",
7340                        &pyodide_dist_path,
7341                        true,
7342                    );
7343                    add_runtime_host_access_path(
7344                        &mut execution_env,
7345                        "AGENTOS_EXTRA_FS_READ_PATHS",
7346                        &pyodide_cache_path,
7347                        true,
7348                    );
7349                    add_runtime_host_access_path(
7350                        &mut execution_env,
7351                        "AGENTOS_EXTRA_FS_WRITE_PATHS",
7352                        &pyodide_cache_path,
7353                        false,
7354                    );
7355                    let context = self
7356                        .python_engine
7357                        .create_context(CreatePythonContextRequest {
7358                            vm_id: vm_id.to_owned(),
7359                            pyodide_dist_path,
7360                        });
7361                    let execution = self
7362                        .python_engine
7363                        .start_execution(StartPythonExecutionRequest {
7364                            vm_id: vm_id.to_owned(),
7365                            context_id: context.context_id,
7366                            code: resolved.entrypoint.clone(),
7367                            file_path: python_file_path,
7368                            env: execution_env,
7369                            cwd: resolved.host_cwd.clone(),
7370                            limits: python_execution_limits(vm),
7371                            guest_runtime: guest_runtime_identity(
7372                                vm,
7373                                Some(u64::from(kernel_pid)),
7374                                Some(u64::from(parent_kernel_pid)),
7375                            ),
7376                        })
7377                        .map_err(python_error)?;
7378                    ActiveExecution::Python(execution)
7379                }
7380            };
7381            let kernel_stdin_writer_fd = match javascript_child_process_stdin_mode(&request) {
7382                "pipe" => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
7383                "ignore" => {
7384                    vm.kernel
7385                        .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, 0)
7386                        .map_err(kernel_error)?;
7387                    None
7388                }
7389                "inherit" => None,
7390                _ => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
7391            };
7392            (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd)
7393        };
7394        record_execute_phase(
7395            "child_process_spawn_and_start_execution",
7396            phase_start.elapsed(),
7397        );
7398
7399        let phase_start = Instant::now();
7400        let child_fd1_is_tty = vm
7401            .kernel
7402            .isatty(EXECUTION_DRIVER_NAME, kernel_pid, 1)
7403            .unwrap_or(false);
7404        let root = vm
7405            .active_processes
7406            .get_mut(process_id)
7407            .ok_or_else(|| missing_process_error(vm_id, process_id))?;
7408        let parent =
7409            Self::active_process_by_path_mut(root, current_process_path).ok_or_else(|| {
7410                SidecarError::InvalidState(format!(
7411                    "unknown child process path {current_process_label} during nested spawn"
7412                ))
7413            })?;
7414        let inherited_tty_master_owner = if child_fd1_is_tty {
7415            parent
7416                .tty_master_fd
7417                .map(|master_fd| (parent.kernel_pid, master_fd))
7418                .or(parent.tty_master_owner)
7419        } else {
7420            None
7421        };
7422        parent.child_processes.insert(
7423            child_process_id.clone(),
7424            ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
7425                .with_detached(request.options.detached)
7426                .with_guest_cwd(resolved.guest_cwd.clone())
7427                .with_env(resolved.env.clone())
7428                .with_host_cwd(resolved.host_cwd.clone()),
7429        );
7430        {
7431            let child = parent
7432                .child_processes
7433                .get_mut(&child_process_id)
7434                .ok_or_else(|| {
7435                    SidecarError::InvalidState(format!(
7436                        "child process {child_process_id} disappeared during nested spawn"
7437                    ))
7438                })?;
7439            child.tty_master_owner = inherited_tty_master_owner;
7440            if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd {
7441                child.kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd);
7442            }
7443        }
7444        record_execute_phase("child_process_register", phase_start.elapsed());
7445        record_execute_phase("child_process_spawn_total", total_start.elapsed());
7446        Ok(json!({
7447            "childId": child_process_id,
7448            "pid": kernel_pid,
7449            "command": resolved.command,
7450            "args": resolved.process_args,
7451        }))
7452    }
7453
7454    fn spawn_descendant_javascript_child_process_sync(
7455        &mut self,
7456        vm_id: &str,
7457        process_id: &str,
7458        current_process_path: &[&str],
7459        request: JavascriptChildProcessSpawnRequest,
7460        max_buffer: Option<usize>,
7461    ) -> Result<Value, SidecarError> {
7462        let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?;
7463        let timeout_deadline = request
7464            .options
7465            .timeout
7466            .map(|timeout_ms| Instant::now() + Duration::from_millis(timeout_ms));
7467        let timeout_signal = request
7468            .options
7469            .kill_signal
7470            .clone()
7471            .unwrap_or_else(|| String::from("SIGTERM"));
7472        let spawned = self.spawn_descendant_javascript_child_process(
7473            vm_id,
7474            process_id,
7475            current_process_path,
7476            request,
7477        )?;
7478        let child_process_id = spawned
7479            .get("childId")
7480            .and_then(Value::as_str)
7481            .ok_or_else(|| {
7482                SidecarError::InvalidState(String::from(
7483                    "child_process.spawn_sync response is missing childId",
7484                ))
7485            })?
7486            .to_owned();
7487
7488        if let Some(input) = sync_input.as_deref() {
7489            self.write_descendant_javascript_child_process_stdin(
7490                vm_id,
7491                process_id,
7492                current_process_path,
7493                &child_process_id,
7494                input,
7495            )?;
7496        }
7497        self.close_descendant_javascript_child_process_stdin(
7498            vm_id,
7499            process_id,
7500            current_process_path,
7501            &child_process_id,
7502        )?;
7503
7504        let max_buffer = max_buffer.unwrap_or(1024 * 1024);
7505        let mut stdout = Vec::new();
7506        let mut stderr = Vec::new();
7507        let mut max_buffer_exceeded = false;
7508        let mut kill_sent = false;
7509        let mut timed_out = false;
7510
7511        let exit_code = loop {
7512            let wait_ms = if let Some(deadline) = timeout_deadline {
7513                let now = Instant::now();
7514                if now >= deadline {
7515                    if !kill_sent {
7516                        timed_out = true;
7517                        self.kill_descendant_javascript_child_process(
7518                            vm_id,
7519                            process_id,
7520                            current_process_path,
7521                            &child_process_id,
7522                            &timeout_signal,
7523                        )?;
7524                        kill_sent = true;
7525                    }
7526                    0
7527                } else {
7528                    u64::try_from(deadline.saturating_duration_since(now).as_millis().min(50))
7529                        .unwrap_or(50)
7530                }
7531            } else {
7532                50
7533            };
7534            let event = self.poll_descendant_javascript_child_process(
7535                vm_id,
7536                process_id,
7537                current_process_path,
7538                &child_process_id,
7539                wait_ms,
7540            )?;
7541            if event.is_null() {
7542                continue;
7543            }
7544
7545            match event.get("type").and_then(Value::as_str) {
7546                Some("stdout") => {
7547                    let chunk = javascript_sync_rpc_bytes_arg(
7548                        &[event.get("data").cloned().unwrap_or(Value::Null)],
7549                        0,
7550                        "child_process.spawn_sync stdout",
7551                    )?;
7552                    stdout.extend_from_slice(&chunk);
7553                    if stdout.len() > max_buffer && !kill_sent {
7554                        max_buffer_exceeded = true;
7555                        self.kill_descendant_javascript_child_process(
7556                            vm_id,
7557                            process_id,
7558                            current_process_path,
7559                            &child_process_id,
7560                            "SIGTERM",
7561                        )?;
7562                        kill_sent = true;
7563                    }
7564                }
7565                Some("stderr") => {
7566                    let chunk = javascript_sync_rpc_bytes_arg(
7567                        &[event.get("data").cloned().unwrap_or(Value::Null)],
7568                        0,
7569                        "child_process.spawn_sync stderr",
7570                    )?;
7571                    stderr.extend_from_slice(&chunk);
7572                    if stderr.len() > max_buffer && !kill_sent {
7573                        max_buffer_exceeded = true;
7574                        self.kill_descendant_javascript_child_process(
7575                            vm_id,
7576                            process_id,
7577                            current_process_path,
7578                            &child_process_id,
7579                            "SIGTERM",
7580                        )?;
7581                        kill_sent = true;
7582                    }
7583                }
7584                Some("exit") => {
7585                    break event
7586                        .get("exitCode")
7587                        .and_then(Value::as_i64)
7588                        .map(|value| value as i32)
7589                        .unwrap_or(1);
7590                }
7591                _ => {}
7592            }
7593        };
7594
7595        Ok(json!({
7596            "stdout": String::from_utf8_lossy(&stdout),
7597            "stderr": String::from_utf8_lossy(&stderr),
7598            "code": exit_code,
7599            "signal": if timed_out { Value::String(timeout_signal) } else { Value::Null },
7600            "timedOut": timed_out,
7601            "maxBufferExceeded": max_buffer_exceeded,
7602        }))
7603    }
7604
7605    fn handle_descendant_javascript_child_process_rpc(
7606        &mut self,
7607        vm_id: &str,
7608        process_id: &str,
7609        current_process_path: &[&str],
7610        request: &JavascriptSyncRpcRequest,
7611    ) -> Result<Value, SidecarError> {
7612        match request.method.as_str() {
7613            "child_process.spawn" => {
7614                let Some(vm) = self.vms.get(vm_id) else {
7615                    return Ok(Value::Null);
7616                };
7617                let (payload, _) = parse_javascript_child_process_spawn_request(vm, &request.args)?;
7618                self.spawn_descendant_javascript_child_process(
7619                    vm_id,
7620                    process_id,
7621                    current_process_path,
7622                    payload,
7623                )
7624            }
7625            "child_process.spawn_sync" => {
7626                let Some(vm) = self.vms.get(vm_id) else {
7627                    return Ok(Value::Null);
7628                };
7629                let (payload, max_buffer) =
7630                    parse_javascript_child_process_spawn_request(vm, &request.args)?;
7631                self.spawn_descendant_javascript_child_process_sync(
7632                    vm_id,
7633                    process_id,
7634                    current_process_path,
7635                    payload,
7636                    max_buffer,
7637                )
7638            }
7639            "child_process.poll" => {
7640                let child_process_id =
7641                    javascript_sync_rpc_arg_str(&request.args, 0, "child_process.poll child id")?;
7642                let wait_ms = javascript_sync_rpc_arg_u64_optional(
7643                    &request.args,
7644                    1,
7645                    "child_process.poll wait ms",
7646                )?
7647                .unwrap_or_default();
7648                self.poll_descendant_javascript_child_process(
7649                    vm_id,
7650                    process_id,
7651                    current_process_path,
7652                    child_process_id,
7653                    wait_ms,
7654                )
7655            }
7656            "child_process.write_stdin" => {
7657                let child_process_id = javascript_sync_rpc_arg_str(
7658                    &request.args,
7659                    0,
7660                    "child_process.write_stdin child id",
7661                )?;
7662                let chunk = javascript_sync_rpc_bytes_arg(
7663                    &request.args,
7664                    1,
7665                    "child_process.write_stdin chunk",
7666                )?;
7667                self.write_descendant_javascript_child_process_stdin(
7668                    vm_id,
7669                    process_id,
7670                    current_process_path,
7671                    child_process_id,
7672                    &chunk,
7673                )?;
7674                Ok(Value::Null)
7675            }
7676            "child_process.close_stdin" => {
7677                let child_process_id = javascript_sync_rpc_arg_str(
7678                    &request.args,
7679                    0,
7680                    "child_process.close_stdin child id",
7681                )?;
7682                self.close_descendant_javascript_child_process_stdin(
7683                    vm_id,
7684                    process_id,
7685                    current_process_path,
7686                    child_process_id,
7687                )?;
7688                Ok(Value::Null)
7689            }
7690            "child_process.kill" => {
7691                let child_process_id =
7692                    javascript_sync_rpc_arg_str(&request.args, 0, "child_process.kill child id")?;
7693                let signal =
7694                    javascript_sync_rpc_arg_str(&request.args, 1, "child_process.kill signal")?;
7695                self.kill_descendant_javascript_child_process(
7696                    vm_id,
7697                    process_id,
7698                    current_process_path,
7699                    child_process_id,
7700                    signal,
7701                )?;
7702                Ok(Value::Null)
7703            }
7704            _ => Err(SidecarError::InvalidState(format!(
7705                "unsupported nested child process RPC method {}",
7706                request.method
7707            ))),
7708        }
7709    }
7710
7711    /// Deferred servicing for a CHILD's `__kernel_stdin_read` / `__kernel_poll`
7712    /// inside the child-event pump: probe readiness with a zero timeout, reply
7713    /// when ready / expired / non-blocking, otherwise park the RPC on the child
7714    /// (reply-by-token). The pump loop re-checks the parked RPC every
7715    /// iteration, so the dispatch loop never blocks in a kernel wait on the
7716    /// child's behalf. Returns false when the RPC must be serviced inline
7717    /// (non-TTY JavaScript local stdin bridge).
7718    fn service_child_kernel_wait_rpc(
7719        &mut self,
7720        vm_id: &str,
7721        process_id: &str,
7722        current_process_path: &[&str],
7723        child_process_id: &str,
7724        request: &JavascriptSyncRpcRequest,
7725    ) -> Result<bool, SidecarError> {
7726        let Some(vm) = self.vms.get_mut(vm_id) else {
7727            return Ok(true);
7728        };
7729        let kernel = &mut vm.kernel;
7730        let Some(root) = vm.active_processes.get_mut(process_id) else {
7731            return Ok(true);
7732        };
7733        let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
7734            return Ok(true);
7735        };
7736        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
7737            return Ok(true);
7738        };
7739        if request.method == "__kernel_stdin_read"
7740            && matches!(child.execution, ActiveExecution::Javascript(_))
7741            && child.tty_master_fd.is_none()
7742        {
7743            return Ok(false);
7744        }
7745        let now = Instant::now();
7746        let requested_timeout_ms = match request.method.as_str() {
7747            "__kernel_stdin_read" => parse_kernel_stdin_read_args(request)?.1,
7748            _ => u64::try_from(parse_kernel_poll_args(request)?.1).unwrap_or(0),
7749        };
7750        let deadline = match &child.deferred_kernel_wait_rpc {
7751            Some((parked, parked_deadline)) if parked.id == request.id => *parked_deadline,
7752            _ => now + Duration::from_millis(requested_timeout_ms),
7753        };
7754        let kernel_pid = child.kernel_pid;
7755        let probe = match request.method.as_str() {
7756            "__kernel_stdin_read" => {
7757                let (max_bytes, _) = parse_kernel_stdin_read_args(request)?;
7758                kernel_stdin_read_response(kernel, kernel_pid, max_bytes, Duration::ZERO)
7759            }
7760            _ => {
7761                let (fd_requests, _) = parse_kernel_poll_args(request)?;
7762                kernel_poll_response(kernel, kernel_pid, &fd_requests, 0)
7763            }
7764        };
7765        let probe = match probe {
7766            Ok(value) => value,
7767            Err(error) => {
7768                child.deferred_kernel_wait_rpc = None;
7769                child
7770                    .execution
7771                    .respond_javascript_sync_rpc_error(
7772                        request.id,
7773                        javascript_sync_rpc_error_code(&error),
7774                        error.to_string(),
7775                    )
7776                    .or_else(ignore_stale_javascript_sync_rpc_response)?;
7777                return Ok(true);
7778            }
7779        };
7780        let ready = match request.method.as_str() {
7781            "__kernel_stdin_read" => !probe.is_null(),
7782            _ => probe.get("readyCount").and_then(Value::as_u64).unwrap_or(0) > 0,
7783        };
7784        if ready || requested_timeout_ms == 0 || now >= deadline {
7785            child.deferred_kernel_wait_rpc = None;
7786            child
7787                .execution
7788                .respond_javascript_sync_rpc_response(request.id, probe.into())
7789                .or_else(ignore_stale_javascript_sync_rpc_response)?;
7790            return Ok(true);
7791        }
7792        child.deferred_kernel_wait_rpc = Some((request.clone(), deadline));
7793        Ok(true)
7794    }
7795
7796    /// Service `__kernel_stdio_write` for a process writing to the SHARED
7797    /// terminal (`tty_master_owner` set): write through the process's own PTY
7798    /// slave (line discipline applies), then drain the master and surface the
7799    /// drained bytes as the OWNER's ordered output stream — the single
7800    /// host-facing path. No child stdout event is queued, so nothing gets
7801    /// relayed (and re-rendered) by the parent shell.
7802    pub(crate) fn service_shared_tty_stdio_write(
7803        &mut self,
7804        vm_id: &str,
7805        writer_kernel_pid: u32,
7806        owner: (u32, u32),
7807        request: &JavascriptSyncRpcRequest,
7808    ) -> Result<Value, SidecarError> {
7809        let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_stdio_write fd")?;
7810        let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "__kernel_stdio_write chunk")?;
7811        if fd != 1 && fd != 2 {
7812            return Err(SidecarError::InvalidState(format!(
7813                "__kernel_stdio_write only supports fd 1/2, got {fd}"
7814            )));
7815        }
7816        let Some(vm) = self.vms.get_mut(vm_id) else {
7817            return Ok(json!(chunk.len()));
7818        };
7819        let written = if fd == 1 {
7820            vm.kernel
7821                .write_process_stdout(EXECUTION_DRIVER_NAME, writer_kernel_pid, &chunk)
7822                .map_err(kernel_error)?
7823        } else {
7824            vm.kernel
7825                .write_process_stderr(EXECUTION_DRIVER_NAME, writer_kernel_pid, &chunk)
7826                .map_err(kernel_error)?
7827        };
7828        let (owner_pid, master_fd) = owner;
7829        let mut drained: Vec<u8> = Vec::new();
7830        loop {
7831            match vm.kernel.fd_read_with_timeout_result(
7832                EXECUTION_DRIVER_NAME,
7833                owner_pid,
7834                master_fd,
7835                MAX_PTY_BUFFER_BYTES,
7836                Some(Duration::ZERO),
7837            ) {
7838                Ok(Some(bytes)) if !bytes.is_empty() => drained.extend(bytes),
7839                Ok(_) => break,
7840                Err(error) if error.code() == "EAGAIN" => break,
7841                Err(error) => return Err(kernel_error(error)),
7842            }
7843        }
7844        if !drained.is_empty() {
7845            if let Some(owner_process) = vm
7846                .active_processes
7847                .values_mut()
7848                .find(|process| process.kernel_pid == owner_pid)
7849            {
7850                owner_process
7851                    .queue_pending_execution_event(ActiveExecutionEvent::Stdout(drained))?;
7852            }
7853        }
7854        Ok(json!(written))
7855    }
7856
7857    /// Re-check a child's parked kernel-wait RPC (see
7858    /// `service_child_kernel_wait_rpc`); called once per pump-loop iteration.
7859    fn recheck_child_deferred_kernel_wait_rpc(
7860        &mut self,
7861        vm_id: &str,
7862        process_id: &str,
7863        current_process_path: &[&str],
7864        child_process_id: &str,
7865    ) -> Result<(), SidecarError> {
7866        let parked = {
7867            let Some(vm) = self.vms.get_mut(vm_id) else {
7868                return Ok(());
7869            };
7870            let Some(root) = vm.active_processes.get_mut(process_id) else {
7871                return Ok(());
7872            };
7873            let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
7874                return Ok(());
7875            };
7876            let Some(child) = parent.child_processes.get_mut(child_process_id) else {
7877                return Ok(());
7878            };
7879            child
7880                .deferred_kernel_wait_rpc
7881                .as_ref()
7882                .map(|(request, _)| request.clone())
7883        };
7884        if let Some(request) = parked {
7885            let _ = self.service_child_kernel_wait_rpc(
7886                vm_id,
7887                process_id,
7888                current_process_path,
7889                child_process_id,
7890                &request,
7891            )?;
7892        }
7893        Ok(())
7894    }
7895
7896    fn poll_descendant_javascript_child_process(
7897        &mut self,
7898        vm_id: &str,
7899        process_id: &str,
7900        current_process_path: &[&str],
7901        child_process_id: &str,
7902        wait_ms: u64,
7903    ) -> Result<Value, SidecarError> {
7904        let mut child_path = current_process_path.to_vec();
7905        child_path.push(child_process_id);
7906        let child_gone_error = || javascript_child_process_gone_error(process_id, &child_path);
7907        let deadline = Instant::now() + Duration::from_millis(wait_ms);
7908        let mut polled_once = false;
7909
7910        loop {
7911            self.drain_queued_descendant_javascript_child_process_events(
7912                vm_id,
7913                process_id,
7914                &child_path,
7915            )?;
7916            self.recheck_child_deferred_kernel_wait_rpc(
7917                vm_id,
7918                process_id,
7919                current_process_path,
7920                child_process_id,
7921            )?;
7922            enum ChildPollResult {
7923                Event(Box<Option<ActiveExecutionEvent>>),
7924                RecoverRuntimeExit,
7925                Timeout,
7926            }
7927            let wait = if wait_ms == 0 {
7928                Duration::ZERO
7929            } else {
7930                deadline.saturating_duration_since(Instant::now())
7931            };
7932            let poll_result = {
7933                let Some(vm) = self.vms.get_mut(vm_id) else {
7934                    return Ok(Value::Null);
7935                };
7936                let Some(parent) =
7937                    Self::descendant_parent_process_mut(vm, process_id, current_process_path)
7938                else {
7939                    return Err(child_gone_error());
7940                };
7941                let Some(child) = parent.child_processes.get_mut(child_process_id) else {
7942                    return Err(child_gone_error());
7943                };
7944                if let Some(event) = child.pending_execution_events.pop_front() {
7945                    ChildPollResult::Event(Box::new(Some(event)))
7946                } else if polled_once && wait.is_zero() {
7947                    ChildPollResult::Timeout
7948                } else {
7949                    polled_once = true;
7950                    match child.execution.poll_event_blocking(wait) {
7951                        Ok(Some(event)) => ChildPollResult::Event(Box::new(Some(event))),
7952                        Ok(None) => ChildPollResult::RecoverRuntimeExit,
7953                        Err(SidecarError::Execution(message))
7954                            if (child.runtime == GuestRuntimeKind::JavaScript
7955                                && closed_javascript_event_channel(&message))
7956                                || (child.runtime == GuestRuntimeKind::Python
7957                                    && closed_python_event_channel(&message))
7958                                || (child.runtime == GuestRuntimeKind::WebAssembly
7959                                    && closed_wasm_event_channel(&message)) =>
7960                        {
7961                            ChildPollResult::RecoverRuntimeExit
7962                        }
7963                        Err(error) => return Err(error),
7964                    }
7965                }
7966            };
7967            let event = match poll_result {
7968                ChildPollResult::Event(event) => *event,
7969                ChildPollResult::Timeout => return Ok(Value::Null),
7970                ChildPollResult::RecoverRuntimeExit => self
7971                    .recover_descendant_runtime_child_process_event(
7972                        vm_id,
7973                        process_id,
7974                        current_process_path,
7975                        child_process_id,
7976                        wait.as_millis().try_into().unwrap_or(u64::MAX),
7977                    )?,
7978            };
7979
7980            let Some(event) = event else {
7981                return Ok(Value::Null);
7982            };
7983
7984            match event {
7985                ActiveExecutionEvent::Stdout(chunk) => {
7986                    return Ok(json!({
7987                        "type": "stdout",
7988                        "data": javascript_sync_rpc_bytes_value(&chunk),
7989                    }));
7990                }
7991                ActiveExecutionEvent::Stderr(chunk) => {
7992                    return Ok(json!({
7993                        "type": "stderr",
7994                        "data": javascript_sync_rpc_bytes_value(&chunk),
7995                    }));
7996                }
7997                ActiveExecutionEvent::Exited(exit_code) => {
7998                    let cleanup_start = Instant::now();
7999                    let had_trailing_events = {
8000                        let Some(vm) = self.vms.get_mut(vm_id) else {
8001                            return Ok(Value::Null);
8002                        };
8003                        let Some(parent) = Self::descendant_parent_process_mut(
8004                            vm,
8005                            process_id,
8006                            current_process_path,
8007                        ) else {
8008                            return Ok(Value::Null);
8009                        };
8010                        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
8011                            return Ok(Value::Null);
8012                        };
8013                        let mut quiet_deadline = Instant::now() + PROCESS_EXIT_DRAIN_INITIAL_QUIET;
8014                        loop {
8015                            let wait = quiet_deadline.saturating_duration_since(Instant::now());
8016                            let next = poll_child_execution_after_exit(child, wait)?;
8017                            let Some(next) = next else {
8018                                break;
8019                            };
8020                            if matches!(next, ActiveExecutionEvent::Exited(_)) {
8021                                continue;
8022                            }
8023                            child.queue_pending_execution_event(next)?;
8024                            quiet_deadline = Instant::now() + PROCESS_EXIT_DRAIN_TRAILING_QUIET;
8025                        }
8026                        if !child.pending_execution_events.is_empty() {
8027                            child.queue_pending_execution_event(ActiveExecutionEvent::Exited(
8028                                exit_code,
8029                            ))?;
8030                            true
8031                        } else {
8032                            false
8033                        }
8034                    };
8035                    if had_trailing_events {
8036                        continue;
8037                    }
8038
8039                    let parent_signal_key =
8040                        Self::child_process_signal_key(process_id, current_process_path);
8041                    let Some(vm) = self.vms.get_mut(vm_id) else {
8042                        return Ok(Value::Null);
8043                    };
8044                    let signal_name = {
8045                        let Some(parent) = Self::descendant_parent_process_mut(
8046                            vm,
8047                            process_id,
8048                            current_process_path,
8049                        ) else {
8050                            return Ok(Value::Null);
8051                        };
8052                        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
8053                            return Ok(Value::Null);
8054                        };
8055                        child.pending_self_signal_exit.take().and_then(|signal| {
8056                            if exit_code == 128 + signal {
8057                                canonical_signal_name(signal).map(str::to_owned)
8058                            } else {
8059                                None
8060                            }
8061                        })
8062                    };
8063                    let (parent_runtime_pid, parent_v8_signal_session, should_signal_parent) = {
8064                        let Some(parent) =
8065                            Self::descendant_parent_process(vm, process_id, current_process_path)
8066                        else {
8067                            return Ok(Value::Null);
8068                        };
8069                        (
8070                            parent.execution.child_pid(),
8071                            parent.execution.javascript_v8_session_handle().filter(|_| {
8072                                matches!(
8073                                    &parent.execution,
8074                                    ActiveExecution::Javascript(execution)
8075                                        if execution.uses_shared_v8_runtime()
8076                                )
8077                            }),
8078                            vm.signal_states
8079                                .get(parent_signal_key)
8080                                .and_then(|handlers| handlers.get(&(libc::SIGCHLD as u32)))
8081                                .is_some_and(|registration| {
8082                                    registration.action != SignalDispositionAction::Default
8083                                }),
8084                        )
8085                    };
8086                    let Some(parent) =
8087                        Self::descendant_parent_process_mut(vm, process_id, current_process_path)
8088                    else {
8089                        return Ok(Value::Null);
8090                    };
8091                    let Some(mut child) = parent.child_processes.remove(child_process_id) else {
8092                        return Ok(Value::Null);
8093                    };
8094                    let child_process_label =
8095                        Self::child_process_path_label(process_id, &child_path);
8096                    let detached_children =
8097                        Self::adopt_detached_child_processes(&child_process_label, &mut child);
8098                    sync_process_host_writes_to_kernel(vm, &child)?;
8099                    let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness);
8100                    terminate_child_process_tree(&mut vm.kernel, &mut child, &kernel_readiness);
8101                    child.kernel_handle.finish(exit_code);
8102                    let _ = vm.kernel.wait_and_reap(child.kernel_pid);
8103                    vm.signal_states.remove(child_process_id);
8104                    for (detached_process_id, detached_child) in detached_children {
8105                        vm.detached_child_processes
8106                            .insert(detached_process_id.clone());
8107                        vm.active_processes
8108                            .insert(detached_process_id, detached_child);
8109                    }
8110                    if should_signal_parent {
8111                        if let Some(session) = parent_v8_signal_session {
8112                            dispatch_v8_session_signal_async(session, libc::SIGCHLD);
8113                        } else {
8114                            signal_runtime_process(parent_runtime_pid, libc::SIGCHLD)?;
8115                        }
8116                    }
8117                    let mut payload = Map::new();
8118                    payload.insert(String::from("type"), Value::String(String::from("exit")));
8119                    payload.insert(String::from("exitCode"), Value::from(exit_code));
8120                    if let Some(signal_name) = signal_name {
8121                        payload.insert(String::from("signal"), Value::String(signal_name));
8122                    }
8123                    record_execute_phase("child_process_exit_cleanup", cleanup_start.elapsed());
8124                    return Ok(Value::Object(payload));
8125                }
8126                ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
8127                    let mut current_child_path = current_process_path.to_vec();
8128                    current_child_path.push(child_process_id);
8129                    if matches!(
8130                        request.method.as_str(),
8131                        "__kernel_stdin_read" | "__kernel_poll"
8132                    ) && self.service_child_kernel_wait_rpc(
8133                        vm_id,
8134                        process_id,
8135                        current_process_path,
8136                        child_process_id,
8137                        &request,
8138                    )? {
8139                        // Replied immediately or parked on the child; the pump
8140                        // loop re-checks parked RPCs every iteration.
8141                        continue;
8142                    }
8143                    if request.method == "__kernel_stdio_write" {
8144                        let shared_tty = {
8145                            let Some(vm) = self.vms.get_mut(vm_id) else {
8146                                return Ok(Value::Null);
8147                            };
8148                            let Some(root) = vm.active_processes.get_mut(process_id) else {
8149                                return Ok(Value::Null);
8150                            };
8151                            let Some(parent) =
8152                                Self::active_process_by_path_mut(root, current_process_path)
8153                            else {
8154                                return Ok(Value::Null);
8155                            };
8156                            parent
8157                                .child_processes
8158                                .get(child_process_id)
8159                                .and_then(|child| {
8160                                    child
8161                                        .tty_master_owner
8162                                        .map(|owner| (child.kernel_pid, owner))
8163                                })
8164                        };
8165                        if let Some((child_kernel_pid, owner)) = shared_tty {
8166                            let response = self.service_shared_tty_stdio_write(
8167                                vm_id,
8168                                child_kernel_pid,
8169                                owner,
8170                                &request,
8171                            );
8172                            let Some(vm) = self.vms.get_mut(vm_id) else {
8173                                return Ok(Value::Null);
8174                            };
8175                            let Some(root) = vm.active_processes.get_mut(process_id) else {
8176                                return Ok(Value::Null);
8177                            };
8178                            let Some(parent) =
8179                                Self::active_process_by_path_mut(root, current_process_path)
8180                            else {
8181                                return Ok(Value::Null);
8182                            };
8183                            let Some(child) = parent.child_processes.get_mut(child_process_id)
8184                            else {
8185                                return Ok(Value::Null);
8186                            };
8187                            match response {
8188                                Ok(result) => child
8189                                    .execution
8190                                    .respond_javascript_sync_rpc_response(request.id, result.into())
8191                                    .or_else(ignore_stale_javascript_sync_rpc_response)?,
8192                                Err(error) => child
8193                                    .execution
8194                                    .respond_javascript_sync_rpc_error(
8195                                        request.id,
8196                                        javascript_sync_rpc_error_code(&error),
8197                                        error.to_string(),
8198                                    )
8199                                    .or_else(ignore_stale_javascript_sync_rpc_response)?,
8200                            }
8201                            continue;
8202                        }
8203                    }
8204                    let response = if request.method == "process.signal_state" {
8205                        let (signal, registration) =
8206                            parse_process_signal_state_request(&request.args)
8207                                .map_err(|error| SidecarError::InvalidState(error.to_string()))?;
8208                        let Some(vm) = self.vms.get_mut(vm_id) else {
8209                            return Ok(Value::Null);
8210                        };
8211                        let signal_key =
8212                            Self::child_process_signal_key(process_id, &current_child_path)
8213                                .to_owned();
8214                        apply_process_signal_state_update(
8215                            &mut vm.signal_states,
8216                            &signal_key,
8217                            signal,
8218                            registration,
8219                        );
8220                        Ok(Value::Null.into())
8221                    } else if request.method == "process.kill" {
8222                        self.handle_descendant_process_kill_rpc(
8223                            vm_id,
8224                            process_id,
8225                            current_process_path,
8226                            child_process_id,
8227                            &request,
8228                        )
8229                        .map(Into::into)
8230                    } else if request.method.starts_with("child_process.") {
8231                        self.handle_descendant_javascript_child_process_rpc(
8232                            vm_id,
8233                            process_id,
8234                            &current_child_path,
8235                            &request,
8236                        )
8237                        .map(Into::into)
8238                    } else {
8239                        let Some(vm) = self.vms.get_mut(vm_id) else {
8240                            return Ok(Value::Null);
8241                        };
8242                        let resource_limits = vm.kernel.resource_limits().clone();
8243                        let network_counts = vm_network_resource_counts(vm);
8244                        let socket_paths = build_javascript_socket_path_context(vm)?;
8245                        let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness);
8246                        let Some(root) = vm.active_processes.get_mut(process_id) else {
8247                            return Ok(Value::Null);
8248                        };
8249                        let Some(parent) =
8250                            Self::active_process_by_path_mut(root, current_process_path)
8251                        else {
8252                            return Ok(Value::Null);
8253                        };
8254                        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
8255                            return Ok(Value::Null);
8256                        };
8257                        service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
8258                            bridge: &self.bridge,
8259                            vm_id,
8260                            dns: &vm.dns,
8261                            socket_paths: &socket_paths,
8262                            kernel: &mut vm.kernel,
8263                            kernel_readiness,
8264                            process: child,
8265                            sync_request: &request,
8266                            resource_limits: &resource_limits,
8267                            network_counts,
8268                        })
8269                    };
8270
8271                    let Some(vm) = self.vms.get_mut(vm_id) else {
8272                        return Ok(Value::Null);
8273                    };
8274                    let Some(parent) =
8275                        Self::descendant_parent_process_mut(vm, process_id, current_process_path)
8276                    else {
8277                        return Ok(Value::Null);
8278                    };
8279                    let Some(child) = parent.child_processes.get_mut(child_process_id) else {
8280                        return Ok(Value::Null);
8281                    };
8282                    let parent_signal_event = response
8283                        .as_ref()
8284                        .ok()
8285                        .and_then(JavascriptSyncRpcServiceResponse::as_json)
8286                        .and_then(|result| {
8287                        let target_path_label =
8288                            Self::child_process_path_label(process_id, current_process_path);
8289                        if request.method != "process.kill"
8290                            || result.get("action").and_then(Value::as_str) != Some("user")
8291                            || result.get("targetProcessPath").and_then(Value::as_str)
8292                                != Some(target_path_label.as_str())
8293                        {
8294                            return None;
8295                        }
8296                        Some(json!({
8297                            "type": "signal",
8298                            "signal": result.get("signal").and_then(Value::as_str).unwrap_or_default(),
8299                            "number": result.get("number").and_then(Value::as_i64).unwrap_or_default(),
8300                        }))
8301                    });
8302                    match response {
8303                        Ok(result) => child
8304                            .execution
8305                            .respond_javascript_sync_rpc_response(request.id, result)
8306                            .or_else(ignore_stale_javascript_sync_rpc_response)?,
8307                        Err(error) => child
8308                            .execution
8309                            .respond_javascript_sync_rpc_error(
8310                                request.id,
8311                                javascript_sync_rpc_error_code(&error),
8312                                error.to_string(),
8313                            )
8314                            .or_else(ignore_stale_javascript_sync_rpc_response)?,
8315                    }
8316                    if let Some(event) = parent_signal_event {
8317                        return Ok(event);
8318                    }
8319                }
8320                ActiveExecutionEvent::PythonVfsRpcRequest(request) => {
8321                    // The kernel-VFS bridge is wired for top-level Python
8322                    // executions; a nested Python child (spawned by a JS/Python
8323                    // parent) cannot service VFS RPCs through this child-event
8324                    // path. Respond with a recoverable error instead of aborting
8325                    // the child, so its runner falls back to the in-isolate FS
8326                    // for the nested process — top-level Python keeps the full
8327                    // VFS root.
8328                    let Some(vm) = self.vms.get_mut(vm_id) else {
8329                        return Ok(Value::Null);
8330                    };
8331                    let Some(parent) =
8332                        Self::descendant_parent_process_mut(vm, process_id, current_process_path)
8333                    else {
8334                        return Ok(Value::Null);
8335                    };
8336                    let Some(child) = parent.child_processes.get_mut(child_process_id) else {
8337                        return Ok(Value::Null);
8338                    };
8339                    // Best-effort: deliver the "unavailable" error so the child's
8340                    // pending VFS RPC resolves and its runner falls back to the
8341                    // in-isolate FS. If delivery fails the child has already gone
8342                    // away (broken pipe / no-longer-pending), so dropping the
8343                    // result is correct here — there is nothing left to hang.
8344                    let _ = child.execution.respond_python_vfs_rpc_error(
8345                        request.id,
8346                        "ERR_AGENTOS_PYTHON_VFS_UNAVAILABLE",
8347                        "python VFS is not available for nested child processes",
8348                    );
8349                }
8350                ActiveExecutionEvent::SignalState {
8351                    signal,
8352                    registration,
8353                } => {
8354                    let Some(vm) = self.vms.get_mut(vm_id) else {
8355                        return Ok(Value::Null);
8356                    };
8357                    let signal_key =
8358                        Self::child_process_signal_key(process_id, &child_path).to_owned();
8359                    apply_process_signal_state_update(
8360                        &mut vm.signal_states,
8361                        &signal_key,
8362                        signal,
8363                        registration.clone(),
8364                    );
8365                    return Ok(json!({
8366                        "type": "signal_state",
8367                        "signal": signal,
8368                        "registration": registration,
8369                    }));
8370                }
8371            }
8372        }
8373    }
8374
8375    fn recover_descendant_runtime_child_process_event(
8376        &mut self,
8377        vm_id: &str,
8378        process_id: &str,
8379        current_process_path: &[&str],
8380        child_process_id: &str,
8381        wait_ms: u64,
8382    ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
8383        let (
8384            parent_kernel_pid,
8385            child_kernel_pid,
8386            child_runtime_pid,
8387            child_runtime,
8388            child_shared_runtime,
8389        ) = {
8390            let mut child_path = current_process_path.to_vec();
8391            child_path.push(child_process_id);
8392            let Some(vm) = self.vms.get_mut(vm_id) else {
8393                return Ok(None);
8394            };
8395            let Some(parent) =
8396                Self::descendant_parent_process_mut(vm, process_id, current_process_path)
8397            else {
8398                return Err(javascript_child_process_gone_error(process_id, &child_path));
8399            };
8400            let Some(child) = parent.child_processes.get_mut(child_process_id) else {
8401                return Err(javascript_child_process_gone_error(process_id, &child_path));
8402            };
8403            (
8404                parent.kernel_pid,
8405                child.kernel_pid,
8406                child.execution.child_pid(),
8407                child.runtime.clone(),
8408                child.execution.uses_shared_v8_runtime(),
8409            )
8410        };
8411        if child_runtime != GuestRuntimeKind::JavaScript
8412            && child_runtime != GuestRuntimeKind::Python
8413            && child_runtime != GuestRuntimeKind::WebAssembly
8414        {
8415            return Ok(None);
8416        }
8417        let wait_deadline = Instant::now() + Duration::from_millis(wait_ms.min(25));
8418        loop {
8419            let Some(vm) = self.vms.get_mut(vm_id) else {
8420                return Ok(None);
8421            };
8422            if let Some(process_info) = vm.kernel.list_processes().get(&child_kernel_pid) {
8423                if process_info.status == ProcessStatus::Exited {
8424                    return Ok(Some(ActiveExecutionEvent::Exited(
8425                        process_info.exit_code.unwrap_or(0),
8426                    )));
8427                }
8428            }
8429            if let Some(wait_result) = vm
8430                .kernel
8431                .waitpid_with_options(
8432                    EXECUTION_DRIVER_NAME,
8433                    parent_kernel_pid,
8434                    child_kernel_pid as i32,
8435                    WaitPidFlags::WNOHANG,
8436                )
8437                .map_err(kernel_error)?
8438            {
8439                return Ok(Some(ActiveExecutionEvent::Exited(wait_result.status)));
8440            }
8441
8442            if !child_shared_runtime && child_runtime_pid != 0 {
8443                if let Some(status) = runtime_child_exit_status(child_runtime_pid)? {
8444                    return Ok(Some(ActiveExecutionEvent::Exited(status)));
8445                }
8446                if !runtime_child_is_alive(child_runtime_pid)? {
8447                    return Ok(Some(ActiveExecutionEvent::Exited(0)));
8448                }
8449            }
8450            if Instant::now() >= wait_deadline {
8451                return Ok(None);
8452            }
8453            std::thread::sleep(Duration::from_millis(5));
8454        }
8455    }
8456
8457    fn write_descendant_javascript_child_process_stdin(
8458        &mut self,
8459        vm_id: &str,
8460        process_id: &str,
8461        current_process_path: &[&str],
8462        child_process_id: &str,
8463        chunk: &[u8],
8464    ) -> Result<(), SidecarError> {
8465        let mut child_path = current_process_path.to_vec();
8466        child_path.push(child_process_id);
8467        let Some(vm) = self.vms.get_mut(vm_id) else {
8468            return Err(javascript_child_process_gone_error(process_id, &child_path));
8469        };
8470        let Some(root) = vm.active_processes.get_mut(process_id) else {
8471            return Err(javascript_child_process_gone_error(process_id, &child_path));
8472        };
8473        let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
8474            return Err(javascript_child_process_gone_error(process_id, &child_path));
8475        };
8476        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
8477            return Err(javascript_child_process_gone_error(process_id, &child_path));
8478        };
8479        if let Err(error) = child.execution.write_stdin(chunk) {
8480            if is_broken_pipe_error(&error) {
8481                return Ok(());
8482            }
8483            return Err(error);
8484        }
8485        write_kernel_process_stdin(&mut vm.kernel, child, chunk)
8486    }
8487
8488    fn close_descendant_javascript_child_process_stdin(
8489        &mut self,
8490        vm_id: &str,
8491        process_id: &str,
8492        current_process_path: &[&str],
8493        child_process_id: &str,
8494    ) -> Result<(), SidecarError> {
8495        let mut child_path = current_process_path.to_vec();
8496        child_path.push(child_process_id);
8497        let Some(vm) = self.vms.get_mut(vm_id) else {
8498            return Err(javascript_child_process_gone_error(process_id, &child_path));
8499        };
8500        let Some(root) = vm.active_processes.get_mut(process_id) else {
8501            return Err(javascript_child_process_gone_error(process_id, &child_path));
8502        };
8503        let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
8504            return Err(javascript_child_process_gone_error(process_id, &child_path));
8505        };
8506        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
8507            return Err(javascript_child_process_gone_error(process_id, &child_path));
8508        };
8509        child.execution.close_stdin()?;
8510        close_kernel_process_stdin(&mut vm.kernel, child)
8511    }
8512
8513    fn kill_descendant_javascript_child_process(
8514        &mut self,
8515        vm_id: &str,
8516        process_id: &str,
8517        current_process_path: &[&str],
8518        child_process_id: &str,
8519        signal: &str,
8520    ) -> Result<(), SidecarError> {
8521        let signal_name = signal.to_owned();
8522        let signal = parse_signal(signal)?;
8523        let Some(vm) = self.vms.get_mut(vm_id) else {
8524            return Ok(());
8525        };
8526        let Some(root) = vm.active_processes.get_mut(process_id) else {
8527            return Ok(());
8528        };
8529        let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
8530            return Ok(());
8531        };
8532        let source_pid = parent.kernel_pid;
8533        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
8534            return Ok(());
8535        };
8536        terminate_tracked_child_process_for_signal(&mut vm.kernel, child, signal)?;
8537        let child_process_label = if current_process_path.is_empty() {
8538            child_process_id.to_owned()
8539        } else {
8540            format!("{}/{}", current_process_path.join("/"), child_process_id)
8541        };
8542        emit_security_audit_event(
8543            &self.bridge,
8544            vm_id,
8545            "security.process.kill",
8546            audit_fields([
8547                (String::from("source"), String::from("guest_child_process")),
8548                (String::from("source_pid"), source_pid.to_string()),
8549                (String::from("target_pid"), child.kernel_pid.to_string()),
8550                (String::from("process_id"), process_id.to_owned()),
8551                (String::from("child_process_id"), child_process_label),
8552                (String::from("signal"), signal_name),
8553            ]),
8554        );
8555        Ok(())
8556    }
8557
8558    fn handle_descendant_process_kill_rpc(
8559        &mut self,
8560        vm_id: &str,
8561        process_id: &str,
8562        current_process_path: &[&str],
8563        child_process_id: &str,
8564        request: &JavascriptSyncRpcRequest,
8565    ) -> Result<Value, SidecarError> {
8566        let target_pid = javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?;
8567        let signal_name = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?;
8568        let signal = parse_signal(signal_name)?;
8569
8570        let mut source_path = current_process_path.to_vec();
8571        source_path.push(child_process_id);
8572
8573        if signal != 0 && target_pid < 0 {
8574            let pgid = target_pid.unsigned_abs();
8575            let caller_kernel_pid = {
8576                let Some(vm) = self.vms.get(vm_id) else {
8577                    return Err(SidecarError::InvalidState(String::from(
8578                        "ESRCH: unknown VM during process.kill",
8579                    )));
8580                };
8581                let Some(root) = vm.active_processes.get(process_id) else {
8582                    return Err(SidecarError::InvalidState(format!(
8583                        "ESRCH: unknown process {process_id} during process.kill",
8584                    )));
8585                };
8586                let Some(source) = Self::active_process_by_path(root, &source_path) else {
8587                    return Err(SidecarError::InvalidState(format!(
8588                        "ESRCH: unknown child process {child_process_id} during process.kill",
8589                    )));
8590                };
8591                source.kernel_pid
8592            };
8593            let caller_is_member =
8594                self.signal_vm_process_group(vm_id, caller_kernel_pid, pgid, signal_name)?;
8595            if !caller_is_member {
8596                return Ok(Value::Null);
8597            }
8598            let Some(vm) = self.vms.get_mut(vm_id) else {
8599                return Ok(Value::Null);
8600            };
8601            let Some(root) = vm.active_processes.get_mut(process_id) else {
8602                return Ok(Value::Null);
8603            };
8604            let Some(source) = Self::active_process_by_path_mut(root, &source_path) else {
8605                return Ok(Value::Null);
8606            };
8607            source.pending_self_signal_exit = None;
8608            if !matches!(
8609                canonical_signal_name(signal),
8610                Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
8611            ) {
8612                source.pending_self_signal_exit = Some(signal);
8613            }
8614            return Ok(json!({
8615                "self": true,
8616                "action": "default",
8617            }));
8618        }
8619
8620        let Some(vm) = self.vms.get_mut(vm_id) else {
8621            return Err(SidecarError::InvalidState(String::from(
8622                "ESRCH: unknown VM during process.kill",
8623            )));
8624        };
8625
8626        if signal == 0 {
8627            vm.kernel
8628                .signal_process(EXECUTION_DRIVER_NAME, target_pid, signal)
8629                .map_err(kernel_error)?;
8630            return Ok(Value::Null);
8631        }
8632
8633        let target_kernel_pid = u32::try_from(target_pid).map_err(|_| {
8634            SidecarError::InvalidState(format!("EINVAL: invalid process pid {target_pid}"))
8635        })?;
8636        let (source_pid, located_target_path) = {
8637            let Some(root) = vm.active_processes.get(process_id) else {
8638                return Err(SidecarError::InvalidState(format!(
8639                    "ESRCH: unknown process {process_id} during process.kill",
8640                )));
8641            };
8642            let Some(source) = Self::active_process_by_path(root, &source_path) else {
8643                return Err(SidecarError::InvalidState(format!(
8644                    "ESRCH: unknown child process {child_process_id} during process.kill",
8645                )));
8646            };
8647            vm.kernel
8648                .signal_process(EXECUTION_DRIVER_NAME, target_pid, 0)
8649                .map_err(kernel_error)?;
8650            (
8651                source.kernel_pid,
8652                Self::active_process_path_by_kernel_pid(root, target_kernel_pid),
8653            )
8654        };
8655        let Some(target_path) = located_target_path else {
8656            // The target is alive but not part of this root's process tree.
8657            // Resolve it VM-wide so cross-tree pids and untracked kernel
8658            // processes still receive the signal.
8659            self.signal_vm_kernel_pid(vm_id, target_kernel_pid, signal_name)?;
8660            return Ok(Value::Null);
8661        };
8662        let Some(vm) = self.vms.get_mut(vm_id) else {
8663            return Err(SidecarError::InvalidState(String::from(
8664                "ESRCH: unknown VM during process.kill",
8665            )));
8666        };
8667
8668        if source_pid == target_kernel_pid {
8669            let Some(root) = vm.active_processes.get_mut(process_id) else {
8670                return Ok(Value::Null);
8671            };
8672            let Some(source) = Self::active_process_by_path_mut(root, &source_path) else {
8673                return Ok(Value::Null);
8674            };
8675            source.pending_self_signal_exit = None;
8676            if !matches!(
8677                canonical_signal_name(signal),
8678                Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
8679            ) {
8680                source.pending_self_signal_exit = Some(signal);
8681            }
8682            return Ok(json!({
8683                "self": true,
8684                "action": "default",
8685            }));
8686        }
8687
8688        let signal_key = target_path.last().map(String::as_str).unwrap_or(process_id);
8689        let registration = vm
8690            .signal_states
8691            .get(signal_key)
8692            .and_then(|handlers| handlers.get(&(signal as u32)))
8693            .cloned();
8694
8695        let action = match registration
8696            .as_ref()
8697            .map(|registration| &registration.action)
8698        {
8699            Some(SignalDispositionAction::Ignore) => "ignore",
8700            Some(SignalDispositionAction::User) => {
8701                let Some(root) = vm.active_processes.get_mut(process_id) else {
8702                    return Ok(Value::Null);
8703                };
8704                let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path)
8705                else {
8706                    return Err(SidecarError::InvalidState(format!(
8707                        "ESRCH: unknown process pid {target_pid}"
8708                    )));
8709                };
8710                if let Some(session) = target.execution.javascript_v8_session_handle().filter(
8711                    |_| matches!(&target.execution, ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime())
8712                        || matches!(&target.execution, ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime()),
8713                ) {
8714                    dispatch_v8_session_signal_async(session, signal);
8715                } else if !dispatch_v8_process_signal(target, signal)? {
8716                    return Err(SidecarError::InvalidState(format!(
8717                        "unsupported guest signal delivery for pid {target_pid}"
8718                    )));
8719                }
8720                "user"
8721            }
8722            Some(SignalDispositionAction::Default) | None
8723                if matches!(
8724                    canonical_signal_name(signal),
8725                    Some("SIGWINCH" | "SIGCHLD" | "SIGURG")
8726                ) =>
8727            {
8728                "ignore"
8729            }
8730            Some(SignalDispositionAction::Default) | None => {
8731                let Some(root) = vm.active_processes.get_mut(process_id) else {
8732                    return Ok(Value::Null);
8733                };
8734                let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path)
8735                else {
8736                    return Err(SidecarError::InvalidState(format!(
8737                        "ESRCH: unknown process pid {target_pid}"
8738                    )));
8739                };
8740                apply_active_process_default_signal(&mut vm.kernel, target, signal)?;
8741                "default"
8742            }
8743        };
8744
8745        let target_path_label = Self::child_process_path_label(
8746            process_id,
8747            &target_path.iter().map(String::as_str).collect::<Vec<_>>(),
8748        );
8749        emit_security_audit_event(
8750            &self.bridge,
8751            vm_id,
8752            "security.process.kill",
8753            audit_fields([
8754                (String::from("source"), String::from("guest_process")),
8755                (String::from("source_pid"), source_pid.to_string()),
8756                (String::from("target_pid"), target_pid.to_string()),
8757                (String::from("process_id"), process_id.to_owned()),
8758                (
8759                    String::from("target_process_path"),
8760                    target_path_label.clone(),
8761                ),
8762                (String::from("signal"), signal_name.to_owned()),
8763            ]),
8764        );
8765
8766        Ok(json!({
8767            "self": false,
8768            "action": action,
8769            "signal": signal_name,
8770            "number": signal,
8771            "targetProcessPath": target_path_label,
8772        }))
8773    }
8774
8775    pub(crate) fn poll_javascript_child_process(
8776        &mut self,
8777        vm_id: &str,
8778        process_id: &str,
8779        child_process_id: &str,
8780        wait_ms: u64,
8781    ) -> Result<Value, SidecarError> {
8782        self.poll_descendant_javascript_child_process(
8783            vm_id,
8784            process_id,
8785            &[],
8786            child_process_id,
8787            wait_ms,
8788        )
8789    }
8790
8791    pub(crate) fn write_javascript_child_process_stdin(
8792        &mut self,
8793        vm_id: &str,
8794        process_id: &str,
8795        child_process_id: &str,
8796        chunk: &[u8],
8797    ) -> Result<(), SidecarError> {
8798        let Some(vm) = self.vms.get_mut(vm_id) else {
8799            return Err(javascript_child_process_gone_error(
8800                process_id,
8801                &[child_process_id],
8802            ));
8803        };
8804        let Some(child) = vm
8805            .active_processes
8806            .get_mut(process_id)
8807            .ok_or_else(|| missing_process_error(vm_id, process_id))?
8808            .child_processes
8809            .get_mut(child_process_id)
8810        else {
8811            return Err(javascript_child_process_gone_error(
8812                process_id,
8813                &[child_process_id],
8814            ));
8815        };
8816        if let Err(error) = child.execution.write_stdin(chunk) {
8817            if is_broken_pipe_error(&error) {
8818                return Ok(());
8819            }
8820            return Err(error);
8821        }
8822        write_kernel_process_stdin(&mut vm.kernel, child, chunk)
8823    }
8824
8825    pub(crate) fn close_javascript_child_process_stdin(
8826        &mut self,
8827        vm_id: &str,
8828        process_id: &str,
8829        child_process_id: &str,
8830    ) -> Result<(), SidecarError> {
8831        let Some(vm) = self.vms.get_mut(vm_id) else {
8832            return Err(javascript_child_process_gone_error(
8833                process_id,
8834                &[child_process_id],
8835            ));
8836        };
8837        let Some(child) = vm
8838            .active_processes
8839            .get_mut(process_id)
8840            .ok_or_else(|| missing_process_error(vm_id, process_id))?
8841            .child_processes
8842            .get_mut(child_process_id)
8843        else {
8844            return Err(javascript_child_process_gone_error(
8845                process_id,
8846                &[child_process_id],
8847            ));
8848        };
8849        child.execution.close_stdin()?;
8850        close_kernel_process_stdin(&mut vm.kernel, child)
8851    }
8852
8853    pub(crate) fn kill_javascript_child_process(
8854        &mut self,
8855        vm_id: &str,
8856        process_id: &str,
8857        child_process_id: &str,
8858        signal: &str,
8859    ) -> Result<(), SidecarError> {
8860        let signal_name = signal.to_owned();
8861        let signal = parse_signal(signal)?;
8862        let Some(vm) = self.vms.get_mut(vm_id) else {
8863            return Ok(());
8864        };
8865        let process = vm
8866            .active_processes
8867            .get_mut(process_id)
8868            .ok_or_else(|| missing_process_error(vm_id, process_id))?;
8869        let source_pid = process.kernel_pid;
8870        let child = process
8871            .child_processes
8872            .get_mut(child_process_id)
8873            .ok_or_else(|| {
8874                SidecarError::InvalidState(format!(
8875                    "unknown child process {child_process_id} during kill"
8876                ))
8877            })?;
8878        terminate_tracked_child_process_for_signal(&mut vm.kernel, child, signal)?;
8879        emit_security_audit_event(
8880            &self.bridge,
8881            vm_id,
8882            "security.process.kill",
8883            audit_fields([
8884                (String::from("source"), String::from("guest_child_process")),
8885                (String::from("source_pid"), source_pid.to_string()),
8886                (String::from("target_pid"), child.kernel_pid.to_string()),
8887                (String::from("process_id"), process_id.to_owned()),
8888                (
8889                    String::from("child_process_id"),
8890                    child_process_id.to_owned(),
8891                ),
8892                (String::from("signal"), signal_name),
8893            ]),
8894        );
8895        Ok(())
8896    }
8897
8898    /// Delivers a signal to one kernel pid inside a VM, resolving the target
8899    /// through the active-process tree first so tracked sidecar executions get
8900    /// the same termination handling as a direct `child_process.kill`.
8901    /// Untracked kernel processes (for example WASM subprocess trees) receive
8902    /// the signal through the kernel process table directly.
8903    pub(crate) fn signal_vm_kernel_pid(
8904        &mut self,
8905        vm_id: &str,
8906        target_kernel_pid: u32,
8907        signal_name: &str,
8908    ) -> Result<(), SidecarError> {
8909        let signal = parse_signal(signal_name)?;
8910        let located = {
8911            let Some(vm) = self.vms.get(vm_id) else {
8912                return Err(SidecarError::InvalidState(String::from(
8913                    "ESRCH: unknown VM during process.kill",
8914                )));
8915            };
8916            let alive = vm
8917                .kernel
8918                .list_processes()
8919                .get(&target_kernel_pid)
8920                .is_some_and(|info| info.status != ProcessStatus::Exited);
8921            if !alive {
8922                return Err(SidecarError::InvalidState(format!(
8923                    "ESRCH: no such process {target_kernel_pid}"
8924                )));
8925            }
8926            vm.active_processes.iter().find_map(|(process_id, root)| {
8927                Self::active_process_path_by_kernel_pid(root, target_kernel_pid)
8928                    .map(|path| (process_id.clone(), path))
8929            })
8930        };
8931
8932        match located {
8933            Some((process_id, path)) if path.is_empty() => {
8934                self.kill_process_internal(vm_id, &process_id, signal_name)
8935            }
8936            Some((process_id, path)) => {
8937                let Some(vm) = self.vms.get_mut(vm_id) else {
8938                    return Ok(());
8939                };
8940                let Some(root) = vm.active_processes.get_mut(&process_id) else {
8941                    return Ok(());
8942                };
8943                let Some(target) = Self::active_process_by_owned_path_mut(root, &path) else {
8944                    return Err(SidecarError::InvalidState(format!(
8945                        "ESRCH: no such process {target_kernel_pid}"
8946                    )));
8947                };
8948                terminate_tracked_child_process_for_signal(&mut vm.kernel, target, signal)?;
8949                emit_security_audit_event(
8950                    &self.bridge,
8951                    vm_id,
8952                    "security.process.kill",
8953                    audit_fields([
8954                        (String::from("source"), String::from("guest_process")),
8955                        (String::from("target_pid"), target_kernel_pid.to_string()),
8956                        (String::from("process_id"), process_id),
8957                        (String::from("signal"), signal_name.to_owned()),
8958                    ]),
8959                );
8960                Ok(())
8961            }
8962            None => {
8963                let Some(vm) = self.vms.get_mut(vm_id) else {
8964                    return Ok(());
8965                };
8966                let target_pid = i32::try_from(target_kernel_pid).map_err(|_| {
8967                    SidecarError::InvalidState(format!(
8968                        "EINVAL: invalid process pid {target_kernel_pid}"
8969                    ))
8970                })?;
8971                vm.kernel
8972                    .signal_process(EXECUTION_DRIVER_NAME, target_pid, signal)
8973                    .map_err(kernel_error)?;
8974                emit_security_audit_event(
8975                    &self.bridge,
8976                    vm_id,
8977                    "security.process.kill",
8978                    audit_fields([
8979                        (String::from("source"), String::from("guest_process")),
8980                        (String::from("target_pid"), target_kernel_pid.to_string()),
8981                        (String::from("signal"), signal_name.to_owned()),
8982                    ]),
8983                );
8984                Ok(())
8985            }
8986        }
8987    }
8988
8989    /// Delivers a signal to every live member of a VM process group, matching
8990    /// Linux `kill(-pgid, sig)` semantics. Returns whether the caller itself
8991    /// is a member of the group so entry points can apply self-signal
8992    /// delivery; the caller is intentionally skipped here.
8993    pub(crate) fn signal_vm_process_group(
8994        &mut self,
8995        vm_id: &str,
8996        caller_kernel_pid: u32,
8997        pgid: u32,
8998        signal_name: &str,
8999    ) -> Result<bool, SidecarError> {
9000        parse_signal(signal_name)?;
9001        let members = {
9002            let Some(vm) = self.vms.get(vm_id) else {
9003                return Err(SidecarError::InvalidState(String::from(
9004                    "ESRCH: unknown VM during process.kill",
9005                )));
9006            };
9007            vm.kernel
9008                .list_processes()
9009                .into_iter()
9010                .filter(|(_, info)| info.pgid == pgid && info.status != ProcessStatus::Exited)
9011                .map(|(pid, _)| pid)
9012                .collect::<Vec<_>>()
9013        };
9014        if members.is_empty() {
9015            return Err(SidecarError::InvalidState(format!(
9016                "ESRCH: no such process group {pgid}"
9017            )));
9018        }
9019
9020        let mut caller_is_member = false;
9021        for member_pid in members {
9022            if member_pid == caller_kernel_pid {
9023                caller_is_member = true;
9024                continue;
9025            }
9026            match self.signal_vm_kernel_pid(vm_id, member_pid, signal_name) {
9027                Ok(()) => {}
9028                // Group members can exit while the group is being signaled. A
9029                // vanished member is not an error for the group kill overall.
9030                Err(error) if sidecar_error_is_esrch(&error) => {}
9031                Err(error) => return Err(error),
9032            }
9033        }
9034        Ok(caller_is_member)
9035    }
9036}
9037
9038/// Applies a kill signal to a tracked child execution. Shared-runtime
9039/// executions for lethal signals are terminated directly with a synthetic
9040/// signal exit so child polls observe a prompt close; everything else routes
9041/// through the kernel process table.
9042fn terminate_tracked_child_process_for_signal(
9043    kernel: &mut SidecarKernel,
9044    child: &mut ActiveProcess,
9045    signal: i32,
9046) -> Result<(), SidecarError> {
9047    let should_terminate_shared_runtime = child.execution.uses_shared_v8_runtime()
9048        && signal != 0
9049        && !matches!(
9050            signal,
9051            libc::SIGHUP
9052                | libc::SIGINT
9053                | libc::SIGTERM
9054                | libc::SIGCHLD
9055                | libc::SIGWINCH
9056                | libc::SIGSTOP
9057                | libc::SIGCONT
9058        );
9059    if should_terminate_shared_runtime {
9060        child.execution.terminate()?;
9061        child.pending_self_signal_exit = Some(signal);
9062        child.queue_pending_execution_event(ActiveExecutionEvent::Exited(128 + signal))?;
9063    } else {
9064        kernel
9065            .kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, signal)
9066            .map_err(kernel_error)?;
9067    }
9068    Ok(())
9069}
9070
9071fn sidecar_error_is_esrch(error: &SidecarError) -> bool {
9072    error.to_string().contains("ESRCH")
9073}
9074
9075fn apply_active_process_default_signal(
9076    kernel: &mut SidecarKernel,
9077    process: &mut ActiveProcess,
9078    signal: i32,
9079) -> Result<(), SidecarError> {
9080    if matches!(signal, libc::SIGSTOP | libc::SIGCONT) {
9081        return kernel
9082            .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal)
9083            .map_err(kernel_error);
9084    }
9085
9086    if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
9087        close_kernel_process_stdin(kernel, process)?;
9088    }
9089
9090    if process.execution.uses_shared_v8_runtime() {
9091        process.execution.terminate()?;
9092        if signal != 0 && matches!(process.execution, ActiveExecution::Wasm(_)) {
9093            process.queue_pending_execution_event(ActiveExecutionEvent::Exited(128 + signal))?;
9094        }
9095        return Ok(());
9096    }
9097
9098    kernel
9099        .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal)
9100        .map_err(kernel_error)
9101}
9102
9103fn map_wasm_signal_registration(
9104    registration: agentos_execution::wasm::WasmSignalHandlerRegistration,
9105) -> SignalHandlerRegistration {
9106    SignalHandlerRegistration {
9107        action: match registration.action {
9108            agentos_execution::wasm::WasmSignalDispositionAction::Default => {
9109                crate::protocol::SignalDispositionAction::Default
9110            }
9111            agentos_execution::wasm::WasmSignalDispositionAction::Ignore => {
9112                crate::protocol::SignalDispositionAction::Ignore
9113            }
9114            agentos_execution::wasm::WasmSignalDispositionAction::User => {
9115                crate::protocol::SignalDispositionAction::User
9116            }
9117        },
9118        mask: registration.mask,
9119        flags: registration.flags,
9120    }
9121}
9122
9123fn map_node_signal_registration(
9124    registration: NodeSignalHandlerRegistration,
9125) -> SignalHandlerRegistration {
9126    SignalHandlerRegistration {
9127        action: match registration.action {
9128            NodeSignalDispositionAction::Default => SignalDispositionAction::Default,
9129            NodeSignalDispositionAction::Ignore => SignalDispositionAction::Ignore,
9130            NodeSignalDispositionAction::User => SignalDispositionAction::User,
9131        },
9132        mask: registration.mask,
9133        flags: registration.flags,
9134    }
9135}
9136
9137fn javascript_child_process_sync_input_bytes(
9138    value: Option<&Value>,
9139) -> Result<Option<Vec<u8>>, SidecarError> {
9140    let Some(value) = value else {
9141        return Ok(None);
9142    };
9143
9144    match value {
9145        Value::Null => Ok(None),
9146        Value::String(text) => Ok(Some(text.as_bytes().to_vec())),
9147        other => javascript_sync_rpc_bytes_arg(
9148            std::slice::from_ref(other),
9149            0,
9150            "child_process.spawn_sync input",
9151        )
9152        .map(Some),
9153    }
9154}
9155
9156// bridge_permissions moved to crate::bridge
9157
9158// reconcile_mounts, resolve_cwd moved to crate::vm
9159
9160fn resolve_execute_request(
9161    vm: &VmState,
9162    payload: &ExecuteRequest,
9163) -> Result<ResolvedChildProcessExecution, SidecarError> {
9164    let payload_env: BTreeMap<String, String> = payload
9165        .env
9166        .iter()
9167        .map(|(k, v)| (k.clone(), v.clone()))
9168        .collect();
9169    if let Some(command) = payload.command.as_deref() {
9170        return resolve_command_execution(
9171            vm,
9172            command,
9173            &payload.args,
9174            &payload_env,
9175            payload.cwd.as_deref(),
9176            payload.wasm_permission_tier,
9177        );
9178    }
9179
9180    let runtime = payload.runtime.clone().ok_or_else(|| {
9181        SidecarError::InvalidState(String::from("execute requires either command or runtime"))
9182    })?;
9183    let entrypoint = payload.entrypoint.clone().ok_or_else(|| {
9184        SidecarError::InvalidState(String::from(
9185            "execute requires either command or entrypoint",
9186        ))
9187    })?;
9188    let (guest_cwd, host_cwd, allow_host_path_overrides) =
9189        resolve_execution_cwds(vm, payload.cwd.as_deref());
9190    let mut env = vm.guest_env.clone();
9191    env.extend(payload_env.clone());
9192
9193    let requested_host_entrypoint = resolve_host_entrypoint_within_vm_host_cwd(vm, &entrypoint);
9194    if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
9195        let requested_cwd = payload.cwd.as_deref().unwrap_or(guest_cwd.as_str());
9196        return Err(SidecarError::InvalidState(format!(
9197            "execution cwd {requested_cwd} is outside sandbox root {}",
9198            vm.host_cwd.to_string_lossy()
9199        )));
9200    }
9201    let host_entrypoint_override = allow_host_path_overrides
9202        .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, &entrypoint))
9203        .flatten();
9204
9205    let guest_entrypoint = host_entrypoint_override
9206        .as_ref()
9207        .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
9208        .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, &entrypoint));
9209    prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
9210
9211    Ok(ResolvedChildProcessExecution {
9212        command: match runtime {
9213            GuestRuntimeKind::JavaScript => String::from(JAVASCRIPT_COMMAND),
9214            GuestRuntimeKind::Python => String::from(PYTHON_COMMAND),
9215            GuestRuntimeKind::WebAssembly => String::from(WASM_COMMAND),
9216        },
9217        process_args: std::iter::once(entrypoint.clone())
9218            .chain(payload.args.iter().cloned())
9219            .collect(),
9220        runtime,
9221        entrypoint: host_entrypoint_override
9222            .map(|(_, host_entrypoint)| host_entrypoint)
9223            .unwrap_or(entrypoint),
9224        execution_args: payload.args.clone(),
9225        env,
9226        guest_cwd,
9227        host_cwd,
9228        wasm_permission_tier: payload.wasm_permission_tier,
9229        tool_command: false,
9230    })
9231}
9232
9233fn resolve_command_execution(
9234    vm: &VmState,
9235    command: &str,
9236    args: &[String],
9237    extra_env: &BTreeMap<String, String>,
9238    cwd: Option<&str>,
9239    explicit_wasm_permission_tier: Option<WasmPermissionTier>,
9240) -> Result<ResolvedChildProcessExecution, SidecarError> {
9241    let (guest_cwd, host_cwd, allow_host_path_overrides) = resolve_execution_cwds(vm, cwd);
9242    let mut env = vm.guest_env.clone();
9243    env.extend(extra_env.clone());
9244    let args = apply_shell_cwd_prefix(command, args.to_vec(), &guest_cwd);
9245
9246    if is_tool_command(vm, command) {
9247        let command = normalized_tool_command_name(command).unwrap_or_else(|| command.to_owned());
9248        return Ok(ResolvedChildProcessExecution {
9249            command: command.clone(),
9250            process_args: std::iter::once(command.clone())
9251                .chain(args.iter().cloned())
9252                .collect(),
9253            runtime: GuestRuntimeKind::JavaScript,
9254            entrypoint: command,
9255            execution_args: args,
9256            env,
9257            guest_cwd,
9258            host_cwd,
9259            wasm_permission_tier: None,
9260            tool_command: true,
9261        });
9262    }
9263
9264    if is_python_runtime_command(command) {
9265        return resolve_python_command_execution(vm, command, &args, env, guest_cwd, host_cwd);
9266    }
9267
9268    if is_node_runtime_command(command) {
9269        if let Some(cli) = resolve_host_node_cli_entrypoint(command) {
9270            env.insert(
9271                String::from("AGENTOS_NODE_EVAL"),
9272                build_host_node_cli_eval(&cli),
9273            );
9274            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
9275            add_runtime_guest_path_mapping(&mut env, &cli.guest_root, &cli.package_root);
9276            add_runtime_host_access_path(
9277                &mut env,
9278                "AGENTOS_EXTRA_FS_READ_PATHS",
9279                &cli.package_root,
9280                true,
9281            );
9282
9283            return Ok(ResolvedChildProcessExecution {
9284                command: String::from(JAVASCRIPT_COMMAND),
9285                process_args: std::iter::once(command.to_owned())
9286                    .chain(args.iter().cloned())
9287                    .collect(),
9288                runtime: GuestRuntimeKind::JavaScript,
9289                entrypoint: String::from("-e"),
9290                execution_args: std::iter::once(cli.guest_entrypoint.clone())
9291                    .chain(args.iter().cloned())
9292                    .collect(),
9293                env,
9294                guest_cwd,
9295                host_cwd,
9296                wasm_permission_tier: None,
9297                tool_command: false,
9298            });
9299        }
9300
9301        if args.is_empty() {
9302            env.insert(String::from("AGENTOS_NODE_EVAL"), String::new());
9303            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
9304
9305            return Ok(ResolvedChildProcessExecution {
9306                command: String::from(JAVASCRIPT_COMMAND),
9307                process_args: vec![command.to_owned()],
9308                runtime: GuestRuntimeKind::JavaScript,
9309                entrypoint: String::from("-e"),
9310                execution_args: Vec::new(),
9311                env,
9312                guest_cwd,
9313                host_cwd,
9314                wasm_permission_tier: None,
9315                tool_command: false,
9316            });
9317        }
9318
9319        if let Some((entrypoint, execution_args)) =
9320            resolve_special_node_cli_invocation(&args, &mut env)
9321        {
9322            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
9323
9324            return Ok(ResolvedChildProcessExecution {
9325                command: String::from(JAVASCRIPT_COMMAND),
9326                process_args: std::iter::once(command.to_owned())
9327                    .chain(args.iter().cloned())
9328                    .collect(),
9329                runtime: GuestRuntimeKind::JavaScript,
9330                entrypoint,
9331                execution_args,
9332                env,
9333                guest_cwd,
9334                host_cwd,
9335                wasm_permission_tier: None,
9336                tool_command: false,
9337            });
9338        }
9339
9340        let Some(entrypoint_specifier) = args.first() else {
9341            return Err(SidecarError::InvalidState(format!(
9342                "{command} execution requires an entrypoint"
9343            )));
9344        };
9345
9346        let (entrypoint, execution_args, guest_entrypoint) = {
9347            let requested_host_entrypoint =
9348                resolve_host_entrypoint_within_vm_host_cwd(vm, entrypoint_specifier);
9349            if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
9350                let requested_cwd = cwd.unwrap_or(guest_cwd.as_str());
9351                return Err(SidecarError::InvalidState(format!(
9352                    "execution cwd {requested_cwd} is outside sandbox root {}",
9353                    vm.host_cwd.to_string_lossy()
9354                )));
9355            }
9356            let host_entrypoint_override = allow_host_path_overrides
9357                .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, entrypoint_specifier))
9358                .flatten();
9359            let guest_entrypoint = host_entrypoint_override
9360                .as_ref()
9361                .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
9362                .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, entrypoint_specifier));
9363            let entrypoint = host_entrypoint_override.map_or_else(
9364                || {
9365                    guest_entrypoint.as_ref().map_or_else(
9366                        || entrypoint_specifier.clone(),
9367                        |guest_entrypoint| {
9368                            resolve_vm_guest_path_to_host(vm, guest_entrypoint)
9369                                .to_string_lossy()
9370                                .into_owned()
9371                        },
9372                    )
9373                },
9374                |(_, host_entrypoint)| host_entrypoint,
9375            );
9376            (
9377                entrypoint,
9378                args.iter().skip(1).cloned().collect(),
9379                guest_entrypoint,
9380            )
9381        };
9382
9383        prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
9384
9385        return Ok(ResolvedChildProcessExecution {
9386            command: String::from(JAVASCRIPT_COMMAND),
9387            process_args: std::iter::once(command.to_owned())
9388                .chain(args.iter().cloned())
9389                .collect(),
9390            runtime: GuestRuntimeKind::JavaScript,
9391            entrypoint,
9392            execution_args,
9393            env,
9394            guest_cwd,
9395            host_cwd,
9396            wasm_permission_tier: None,
9397            tool_command: false,
9398        });
9399    }
9400
9401    if command.ends_with(".js") || command.ends_with(".mjs") || command.ends_with(".cjs") {
9402        let requested_host_entrypoint = resolve_host_entrypoint_within_vm_host_cwd(vm, command);
9403        if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
9404            let requested_cwd = cwd.unwrap_or(guest_cwd.as_str());
9405            return Err(SidecarError::InvalidState(format!(
9406                "execution cwd {requested_cwd} is outside sandbox root {}",
9407                vm.host_cwd.to_string_lossy()
9408            )));
9409        }
9410        let host_entrypoint_override = allow_host_path_overrides
9411            .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, command))
9412            .flatten();
9413        let guest_entrypoint = host_entrypoint_override
9414            .as_ref()
9415            .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
9416            .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, command));
9417        let entrypoint = host_entrypoint_override.map_or_else(
9418            || {
9419                guest_entrypoint.as_ref().map_or_else(
9420                    || command.to_owned(),
9421                    |guest_entrypoint| {
9422                        resolve_vm_guest_path_to_host(vm, guest_entrypoint)
9423                            .to_string_lossy()
9424                            .into_owned()
9425                    },
9426                )
9427            },
9428            |(_, host_entrypoint)| host_entrypoint,
9429        );
9430        prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
9431
9432        return Ok(ResolvedChildProcessExecution {
9433            command: String::from(JAVASCRIPT_COMMAND),
9434            process_args: std::iter::once(command.to_owned())
9435                .chain(args.iter().cloned())
9436                .collect(),
9437            runtime: GuestRuntimeKind::JavaScript,
9438            entrypoint,
9439            execution_args: args.to_vec(),
9440            env,
9441            guest_cwd,
9442            host_cwd,
9443            wasm_permission_tier: None,
9444            tool_command: false,
9445        });
9446    }
9447
9448    let guest_entrypoint = resolve_guest_command_entrypoint(
9449        vm,
9450        &guest_cwd,
9451        command,
9452        env.get("PATH").map(String::as_str),
9453    )
9454    .ok_or_else(|| {
9455        SidecarError::InvalidState(format!(
9456            "command not found on native sidecar path: {command}"
9457        ))
9458    })?;
9459    let wasm_permission_tier = explicit_wasm_permission_tier
9460        .or_else(|| vm.command_permissions.get(command).copied())
9461        .or_else(|| {
9462            Path::new(&guest_entrypoint)
9463                .file_name()
9464                .and_then(|name| name.to_str())
9465                .and_then(|name| vm.command_permissions.get(name).copied())
9466        });
9467
9468    let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint);
9469    if let Some((javascript_guest_entrypoint, javascript_host_entrypoint)) =
9470        resolve_javascript_command_entrypoint(vm, &guest_entrypoint, &host_entrypoint)
9471    {
9472        prepare_guest_runtime_env(
9473            vm,
9474            &mut env,
9475            &guest_cwd,
9476            &host_cwd,
9477            Some(javascript_guest_entrypoint),
9478        )?;
9479
9480        return Ok(ResolvedChildProcessExecution {
9481            command: command.to_owned(),
9482            process_args: std::iter::once(command.to_owned())
9483                .chain(args.iter().cloned())
9484                .collect(),
9485            runtime: GuestRuntimeKind::JavaScript,
9486            entrypoint: javascript_host_entrypoint.to_string_lossy().into_owned(),
9487            execution_args: args.to_vec(),
9488            env,
9489            guest_cwd,
9490            host_cwd,
9491            wasm_permission_tier: None,
9492            tool_command: false,
9493        });
9494    }
9495    prepare_guest_runtime_env(
9496        vm,
9497        &mut env,
9498        &guest_cwd,
9499        &host_cwd,
9500        Some(guest_entrypoint.clone()),
9501    )?;
9502
9503    Ok(ResolvedChildProcessExecution {
9504        command: command.to_owned(),
9505        process_args: std::iter::once(command.to_owned())
9506            .chain(args.iter().cloned())
9507            .collect(),
9508        runtime: GuestRuntimeKind::WebAssembly,
9509        entrypoint: host_entrypoint.to_string_lossy().into_owned(),
9510        execution_args: args.to_vec(),
9511        env,
9512        guest_cwd,
9513        host_cwd,
9514        wasm_permission_tier,
9515        tool_command: false,
9516    })
9517}
9518
9519const MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH: usize = 4;
9520
9521fn resolve_javascript_command_entrypoint(
9522    vm: &VmState,
9523    guest_entrypoint: &str,
9524    host_entrypoint: &Path,
9525) -> Option<(String, PathBuf)> {
9526    // agentOS package content is served guest-native (tar + single-symlink
9527    // mounts) and is never materialized on the host, so the shebang-reading
9528    // fallback below (which reads the host path) cannot classify these
9529    // entrypoints. Within the package mount the only runtimes are WebAssembly
9530    // (`*.wasm`) and JavaScript, and `bin/<cmd>` launchers are frequently
9531    // extensionless — so classify by extension here: `.wasm` is WASM (fall
9532    // through), everything else in the mount is JavaScript.
9533    if guest_path_is_within_agentos_package_mount(vm, guest_entrypoint) {
9534        let extension = Path::new(guest_entrypoint)
9535            .extension()
9536            .and_then(|extension| extension.to_str());
9537        if extension != Some("wasm") {
9538            return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf()));
9539        }
9540        return None;
9541    }
9542
9543    resolve_javascript_command_entrypoint_inner(
9544        vm,
9545        guest_entrypoint,
9546        host_entrypoint,
9547        MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH,
9548    )
9549}
9550
9551fn resolve_javascript_command_entrypoint_inner(
9552    vm: &VmState,
9553    guest_entrypoint: &str,
9554    host_entrypoint: &Path,
9555    redirects_remaining: usize,
9556) -> Option<(String, PathBuf)> {
9557    if redirects_remaining > 0 {
9558        let symlink_target = fs::symlink_metadata(host_entrypoint)
9559            .ok()
9560            .filter(|metadata| metadata.file_type().is_symlink())
9561            .and_then(|_| fs::read_link(host_entrypoint).ok());
9562        if let Some(symlink_target) = symlink_target {
9563            let guest_parent = Path::new(guest_entrypoint)
9564                .parent()
9565                .and_then(|path| path.to_str())
9566                .unwrap_or("/");
9567            let symlink_guest_entrypoint = if symlink_target.is_absolute() {
9568                normalize_path(&symlink_target.to_string_lossy())
9569            } else {
9570                normalize_path(&format!(
9571                    "{guest_parent}/{}",
9572                    symlink_target.to_string_lossy().replace('\\', "/")
9573                ))
9574            };
9575            let symlink_host_entrypoint =
9576                resolve_vm_guest_path_to_host(vm, &symlink_guest_entrypoint);
9577            return resolve_javascript_command_entrypoint_inner(
9578                vm,
9579                &symlink_guest_entrypoint,
9580                &symlink_host_entrypoint,
9581                redirects_remaining - 1,
9582            );
9583        }
9584    }
9585
9586    let script = load_executable_script_preview(host_entrypoint)?;
9587    let interpreter = parse_script_interpreter_name(&script);
9588
9589    if interpreter.is_none() && is_probable_javascript_entrypoint(host_entrypoint, &script) {
9590        return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf()));
9591    }
9592
9593    let interpreter = interpreter?;
9594    if interpreter == "node" {
9595        return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf()));
9596    }
9597
9598    if redirects_remaining == 0 || !matches!(interpreter.as_str(), "sh" | "bash" | "dash") {
9599        return None;
9600    }
9601
9602    let shim_target = parse_node_shell_shim_target(&script)?;
9603    let guest_parent = Path::new(guest_entrypoint)
9604        .parent()
9605        .and_then(|path| path.to_str())
9606        .unwrap_or("/");
9607    let shim_guest_entrypoint = normalize_path(&format!("{guest_parent}/{shim_target}"));
9608    let shim_host_entrypoint = resolve_vm_guest_path_to_host(vm, &shim_guest_entrypoint);
9609    resolve_javascript_command_entrypoint_inner(
9610        vm,
9611        &shim_guest_entrypoint,
9612        &shim_host_entrypoint,
9613        redirects_remaining - 1,
9614    )
9615}
9616
9617fn load_executable_script_preview(path: &Path) -> Option<String> {
9618    let bytes = fs::read(path).ok()?;
9619    let preview_len = bytes.len().min(16 * 1024);
9620    Some(String::from_utf8_lossy(&bytes[..preview_len]).into_owned())
9621}
9622
9623fn parse_script_interpreter_name(script: &str) -> Option<String> {
9624    let shebang = script.lines().next()?.strip_prefix("#!")?.trim();
9625    let mut tokens = shebang.split_whitespace();
9626    let command = tokens.next()?;
9627    let command_name = Path::new(command).file_name()?.to_str()?;
9628    if command_name == "env" {
9629        for token in tokens {
9630            if token.starts_with('-') {
9631                continue;
9632            }
9633            return Path::new(token)
9634                .file_name()
9635                .and_then(|name| name.to_str())
9636                .map(ToOwned::to_owned);
9637        }
9638        return None;
9639    }
9640
9641    Some(command_name.to_owned())
9642}
9643
9644fn parse_node_shell_shim_target(script: &str) -> Option<String> {
9645    for line in script.lines() {
9646        let trimmed = line.trim();
9647        if !trimmed.starts_with("exec ") {
9648            continue;
9649        }
9650
9651        let mut remaining = trimmed;
9652        while let Some(start) = remaining.find("\"$basedir/") {
9653            let after_prefix = &remaining[start + "\"$basedir/".len()..];
9654            let end = after_prefix.find('"')?;
9655            let candidate = &after_prefix[..end];
9656            remaining = &after_prefix[end + 1..];
9657
9658            if candidate.is_empty() || candidate == "node" || candidate.ends_with("/node") {
9659                continue;
9660            }
9661
9662            return Some(candidate.to_owned());
9663        }
9664    }
9665
9666    None
9667}
9668
9669fn is_probable_javascript_entrypoint(path: &Path, script: &str) -> bool {
9670    let extension = path
9671        .extension()
9672        .and_then(|value| value.to_str())
9673        .unwrap_or_default();
9674    if matches!(extension, "js" | "cjs" | "mjs") {
9675        return true;
9676    }
9677
9678    if !path
9679        .components()
9680        .any(|component| component.as_os_str() == "node_modules")
9681    {
9682        return false;
9683    }
9684
9685    let preview = script.trim_start_matches('\u{feff}').trim_start();
9686    !preview.is_empty()
9687        && !preview.starts_with("#!")
9688        && (preview.starts_with("\"use strict\"")
9689            || preview.starts_with("'use strict'")
9690            || preview.starts_with("import ")
9691            || preview.starts_with("export ")
9692            || preview.starts_with("const ")
9693            || preview.starts_with("let ")
9694            || preview.starts_with("var ")
9695            || preview.starts_with("Object.defineProperty(exports")
9696            || preview.starts_with("module.exports")
9697            || preview.starts_with("require("))
9698}
9699
9700fn resolve_guest_execution_cwd(vm: &VmState, value: Option<&str>) -> String {
9701    value
9702        .map(normalize_path)
9703        .unwrap_or_else(|| vm.guest_cwd.clone())
9704}
9705
9706fn resolve_execution_cwds(vm: &VmState, value: Option<&str>) -> (String, PathBuf, bool) {
9707    if let Some(raw_cwd) = value {
9708        let normalized_vm_host_cwd = normalize_host_path(&vm.host_cwd);
9709        let requested_host_cwd = normalize_host_path(Path::new(raw_cwd));
9710        if path_is_within_root(&requested_host_cwd, &normalized_vm_host_cwd) {
9711            let relative = requested_host_cwd
9712                .strip_prefix(&normalized_vm_host_cwd)
9713                .unwrap_or_else(|_| Path::new(""));
9714            let relative = relative.to_string_lossy().replace('\\', "/");
9715            let guest_cwd = if relative.is_empty() {
9716                String::from("/")
9717            } else {
9718                normalize_path(&format!("/{relative}"))
9719            };
9720            return (guest_cwd, requested_host_cwd, true);
9721        }
9722    }
9723
9724    let guest_cwd = resolve_guest_execution_cwd(vm, value);
9725    let host_cwd = if value.is_none() {
9726        vm.host_cwd.clone()
9727    } else {
9728        resolve_vm_guest_path_to_host(vm, &guest_cwd)
9729    };
9730    (guest_cwd, host_cwd, value.is_none())
9731}
9732
9733fn resolve_vm_guest_path_to_host(vm: &VmState, guest_path: &str) -> PathBuf {
9734    host_mount_path_for_guest_path(vm, guest_path)
9735        .unwrap_or_else(|| shadow_path_for_guest(vm, guest_path))
9736}
9737
9738fn shadow_path_for_guest(vm: &VmState, guest_path: &str) -> PathBuf {
9739    let normalized = normalize_path(guest_path);
9740    let relative = normalized.trim_start_matches('/');
9741    if relative.is_empty() {
9742        return vm.cwd.clone();
9743    }
9744    vm.cwd.join(relative)
9745}
9746
9747fn apply_shell_cwd_prefix(command: &str, mut args: Vec<String>, guest_cwd: &str) -> Vec<String> {
9748    if guest_cwd == "/" || !is_shell_command(command) {
9749        return args;
9750    }
9751
9752    let Some(flag) = args.first() else {
9753        return args;
9754    };
9755    if !matches!(flag.as_str(), "-c" | "-lc") || args.len() < 2 {
9756        return args;
9757    }
9758
9759    let command_text = args[1].clone();
9760    let quoted_cwd = shell_single_quote(guest_cwd);
9761    args[1] = format!("cd {quoted_cwd} && {command_text}");
9762    args
9763}
9764
9765fn is_shell_command(command: &str) -> bool {
9766    Path::new(command)
9767        .file_name()
9768        .and_then(|name| name.to_str())
9769        .unwrap_or(command)
9770        .trim_end_matches(".exe")
9771        .eq("sh")
9772        || Path::new(command)
9773            .file_name()
9774            .and_then(|name| name.to_str())
9775            .unwrap_or(command)
9776            .trim_end_matches(".exe")
9777            .eq("bash")
9778}
9779
9780fn shell_single_quote(value: &str) -> String {
9781    if value.is_empty() {
9782        return String::from("''");
9783    }
9784    format!("'{}'", value.replace('\'', "'\"'\"'"))
9785}
9786
9787pub(crate) fn sync_active_process_host_writes_to_kernel(
9788    vm: &mut VmState,
9789) -> Result<(), SidecarError> {
9790    if vm.root_filesystem_mode != RootFilesystemMode::ReadOnly {
9791        let shadow_root = vm.cwd.clone();
9792        sync_host_directory_tree_to_kernel(vm, &shadow_root, "/")?;
9793    }
9794
9795    let normalized_vm_root = normalize_host_path(&vm.cwd);
9796    let extra_roots = collect_active_process_host_sync_roots(vm, &normalized_vm_root);
9797    for (host_cwd, guest_cwd) in extra_roots {
9798        sync_host_directory_tree_to_kernel(vm, &host_cwd, &guest_cwd)?;
9799    }
9800
9801    Ok(())
9802}
9803
9804fn collect_active_process_host_sync_roots(
9805    vm: &VmState,
9806    normalized_vm_root: &Path,
9807) -> Vec<(PathBuf, String)> {
9808    let mut roots = Vec::new();
9809    let mut seen = BTreeSet::new();
9810
9811    for process in vm.active_processes.values() {
9812        collect_process_host_sync_roots(process, normalized_vm_root, &mut seen, &mut roots);
9813    }
9814
9815    roots
9816}
9817
9818fn collect_process_host_sync_roots(
9819    process: &ActiveProcess,
9820    normalized_vm_root: &Path,
9821    seen: &mut BTreeSet<(PathBuf, String)>,
9822    roots: &mut Vec<(PathBuf, String)>,
9823) {
9824    let normalized_host_cwd = normalize_host_path(&process.host_cwd);
9825    if !path_is_within_root(&normalized_host_cwd, normalized_vm_root) {
9826        let guest_cwd = normalize_path(&process.guest_cwd);
9827        if seen.insert((normalized_host_cwd.clone(), guest_cwd.clone())) {
9828            roots.push((normalized_host_cwd, guest_cwd));
9829        }
9830    }
9831
9832    for child in process.child_processes.values() {
9833        collect_process_host_sync_roots(child, normalized_vm_root, seen, roots);
9834    }
9835}
9836
9837fn sync_process_host_writes_to_kernel(
9838    vm: &mut VmState,
9839    process: &ActiveProcess,
9840) -> Result<(), SidecarError> {
9841    if vm.root_filesystem_mode != RootFilesystemMode::ReadOnly {
9842        let shadow_root = vm.cwd.clone();
9843        sync_host_directory_tree_to_kernel(vm, &shadow_root, "/")?;
9844    }
9845
9846    if !path_is_within_root(
9847        &normalize_host_path(&process.host_cwd),
9848        &normalize_host_path(&vm.cwd),
9849    ) {
9850        sync_host_directory_tree_to_kernel(vm, &process.host_cwd, &process.guest_cwd)?;
9851    }
9852
9853    Ok(())
9854}
9855
9856fn host_sync_root_is_filesystem_root(host_root: &Path) -> bool {
9857    normalize_host_path(host_root) == Path::new("/")
9858}
9859
9860fn sync_host_directory_tree_to_kernel(
9861    vm: &mut VmState,
9862    host_root: &Path,
9863    guest_root: &str,
9864) -> Result<(), SidecarError> {
9865    let normalized_host_root = normalize_host_path(host_root);
9866    let normalized_guest_root = normalize_path(guest_root);
9867    if host_sync_root_is_filesystem_root(host_root) {
9868        // A process tracked with host cwd "/" would pull the entire host
9869        // filesystem into the kernel VFS (until the size/inode caps fire).
9870        // No sanctioned flow shadows the host root wholesale; host access is
9871        // scoped through mounts.
9872        tracing::warn!("skipping host shadow sync rooted at the host filesystem root");
9873        return Ok(());
9874    }
9875    let mut synced_file_times = BTreeMap::new();
9876    sync_host_directory_tree_to_kernel_inner(
9877        vm,
9878        &normalized_host_root,
9879        &normalized_host_root,
9880        &normalized_guest_root,
9881        &mut synced_file_times,
9882    )
9883}
9884
9885fn sync_host_directory_tree_to_kernel_inner(
9886    vm: &mut VmState,
9887    host_root: &Path,
9888    current_host_dir: &Path,
9889    guest_root: &str,
9890    synced_file_times: &mut BTreeMap<(u64, u64), (u64, u64)>,
9891) -> Result<(), SidecarError> {
9892    let entries = match fs::read_dir(current_host_dir) {
9893        Ok(entries) => entries,
9894        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
9895        Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => {
9896            // Host dirs the sidecar user cannot read (e.g. root-owned
9897            // /lost+found under a host-root mount) are skipped rather than
9898            // failing the whole shadow sync; the guest just won't see them.
9899            tracing::warn!(
9900                path = %current_host_dir.display(),
9901                "skipping unreadable host shadow directory"
9902            );
9903            return Ok(());
9904        }
9905        Err(error) => {
9906            return Err(SidecarError::Io(format!(
9907                "failed to read host shadow directory {}: {error}",
9908                current_host_dir.display()
9909            )));
9910        }
9911    };
9912
9913    for entry in entries {
9914        let entry = entry.map_err(|error| {
9915            SidecarError::Io(format!(
9916                "failed to read host shadow entry in {}: {error}",
9917                current_host_dir.display()
9918            ))
9919        })?;
9920        let host_path = entry.path();
9921        let file_type = entry.file_type().map_err(|error| {
9922            SidecarError::Io(format!(
9923                "failed to stat host shadow entry {}: {error}",
9924                host_path.display()
9925            ))
9926        })?;
9927        let relative_path = host_path
9928            .strip_prefix(host_root)
9929            .map_err(|error| {
9930                SidecarError::InvalidState(format!(
9931                    "failed to relativize host shadow path {} against {}: {error}",
9932                    host_path.display(),
9933                    host_root.display()
9934                ))
9935            })?
9936            .to_string_lossy()
9937            .replace('\\', "/");
9938        let guest_path = if guest_root == "/" {
9939            normalize_path(&format!("/{relative_path}"))
9940        } else {
9941            normalize_path(&format!(
9942                "{}/{}",
9943                guest_root.trim_end_matches('/'),
9944                relative_path
9945            ))
9946        };
9947
9948        if should_skip_shadow_sync_path(vm, &guest_path) {
9949            continue;
9950        }
9951
9952        if file_type.is_dir() {
9953            let metadata = entry.metadata().map_err(|error| {
9954                SidecarError::Io(format!(
9955                    "failed to read host shadow metadata {}: {error}",
9956                    host_path.display()
9957                ))
9958            })?;
9959            if !is_shadow_bootstrap_dir(&guest_path)
9960                && !vm.kernel.exists(&guest_path).unwrap_or(false)
9961            {
9962                vm.kernel.mkdir(&guest_path, true).map_err(|error| {
9963                    SidecarError::InvalidState(format!(
9964                        "failed to sync host shadow directory {} to guest {}: {}",
9965                        host_path.display(),
9966                        guest_path,
9967                        kernel_error(error)
9968                    ))
9969                })?;
9970                vm.kernel
9971                    .chmod(&guest_path, host_shadow_mode(&metadata))
9972                    .map_err(|error| {
9973                        SidecarError::InvalidState(format!(
9974                            "failed to sync host shadow directory mode {} to guest {}: {}",
9975                            host_path.display(),
9976                            guest_path,
9977                            kernel_error(error)
9978                        ))
9979                    })?;
9980            }
9981            sync_host_directory_tree_to_kernel_inner(
9982                vm,
9983                host_root,
9984                &host_path,
9985                guest_root,
9986                synced_file_times,
9987            )?;
9988            continue;
9989        }
9990
9991        if file_type.is_file() {
9992            let metadata = entry.metadata().map_err(|error| {
9993                SidecarError::Io(format!(
9994                    "failed to read host shadow metadata {}: {error}",
9995                    host_path.display()
9996                ))
9997            })?;
9998            let timestamp_key = (metadata.dev(), metadata.ino());
9999            let (atime_ms, mtime_ms) =
10000                *synced_file_times.entry(timestamp_key).or_insert_with(|| {
10001                    (
10002                        metadata_time_ms(metadata.atime(), metadata.atime_nsec()),
10003                        metadata_time_ms(metadata.mtime(), metadata.mtime_nsec()),
10004                    )
10005                });
10006            let desired_mode = host_shadow_mode(&metadata);
10007            // Fast path: skip the expensive re-read + re-write when the kernel already
10008            // holds a copy of this shadow file that matches on size, mode, and mtime.
10009            //
10010            // Every read-side fs op (exists/stat/readFile/...) triggers a full
10011            // shadow-tree reconciliation walk. Without this skip the walk re-reads every
10012            // file's bytes from the host and re-writes them into the kernel VFS on every
10013            // op -- O(whole tree) per op, and super-linear as the VM's shadow grows,
10014            // which is a dominant source of session-creation/runtime latency on
10015            // populated VMs.
10016            //
10017            // This is a (size, mode, mtime) quick-check, the same heuristic rsync uses
10018            // by default. It needs no separate cache to invalidate -- it compares against
10019            // the kernel's own stat, so a kernel reset (e.g. a layer swap) or any host
10020            // change that moves size/mode/mtime forces a resync. Limitation: mtime is
10021            // compared at the millisecond granularity the kernel stores (utimes truncates
10022            // to ms), so a host-side rewrite that preserves byte length AND mode AND lands
10023            // in the same wall-clock millisecond can be skipped and leave stale bytes.
10024            // That window is sub-millisecond same-length edits; if it ever matters here,
10025            // upgrade this to a content digest (or full-precision mtime) for files whose
10026            // mtime is within the last few ms of `now`.
10027            if let Ok(existing) = vm.kernel.lstat(&guest_path) {
10028                if !existing.is_directory
10029                    && !existing.is_symbolic_link
10030                    && existing.size == metadata.len()
10031                    && (existing.mode & 0o7777) == (desired_mode & 0o7777)
10032                    && existing.mtime_ms == mtime_ms
10033                {
10034                    continue;
10035                }
10036            }
10037            let bytes = match read_host_shadow_file(&host_path, desired_mode) {
10038                Ok(bytes) => bytes,
10039                // The host entry vanished between the walk and the read
10040                // (short-lived files churn constantly — editor swap files,
10041                // temp files). Skipping matches native semantics; failing
10042                // here would poison EVERY subsequent fs op on the VM.
10043                Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
10044                // Same tolerance for entries the sidecar user cannot read
10045                // (root-owned files under a host-root mount): skip them
10046                // rather than poisoning the whole sync.
10047                Err(error)
10048                    if error.kind() == std::io::ErrorKind::PermissionDenied
10049                        || error.raw_os_error() == Some(libc::EPERM) =>
10050                {
10051                    tracing::warn!(
10052                        path = %host_path.display(),
10053                        "skipping unreadable host shadow file"
10054                    );
10055                    continue;
10056                }
10057                Err(error) => {
10058                    return Err(SidecarError::Io(format!(
10059                        "failed to read host shadow file {}: {error}",
10060                        host_path.display()
10061                    )));
10062                }
10063            };
10064            match vm.kernel.write_file(&guest_path, bytes) {
10065                Ok(()) => {}
10066                // ENOENT here means the guest-side path cannot currently
10067                // receive the write (e.g. it is a symlink whose target was
10068                // just unlinked by the guest — vim's swap-file dance). The
10069                // entry is mid-churn; skip it rather than failing the VM.
10070                Err(error) if error.code() == "ENOENT" => continue,
10071                Err(error) => {
10072                    return Err(SidecarError::InvalidState(format!(
10073                        "failed to sync host shadow file {} to guest {}: {}",
10074                        host_path.display(),
10075                        guest_path,
10076                        kernel_error(error)
10077                    )));
10078                }
10079            }
10080            vm.kernel
10081                .chmod(&guest_path, desired_mode)
10082                .map_err(|error| {
10083                    SidecarError::InvalidState(format!(
10084                        "failed to sync host shadow file mode {} to guest {}: {}",
10085                        host_path.display(),
10086                        guest_path,
10087                        kernel_error(error)
10088                    ))
10089                })?;
10090            vm.kernel
10091                .utimes(&guest_path, atime_ms, mtime_ms)
10092                .map_err(|error| {
10093                    SidecarError::InvalidState(format!(
10094                        "failed to sync host shadow file times {} to guest {}: {}",
10095                        host_path.display(),
10096                        guest_path,
10097                        kernel_error(error)
10098                    ))
10099                })?;
10100            continue;
10101        }
10102
10103        if file_type.is_symlink() {
10104            let target = match fs::read_link(&host_path) {
10105                Ok(target) => target,
10106                Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
10107                Err(error) => {
10108                    return Err(SidecarError::Io(format!(
10109                        "failed to read host shadow symlink {}: {error}",
10110                        host_path.display()
10111                    )));
10112                }
10113            };
10114            replace_kernel_symlink(vm, &guest_path, &target.to_string_lossy())?;
10115        }
10116    }
10117
10118    Ok(())
10119}
10120
10121fn replace_kernel_symlink(
10122    vm: &mut VmState,
10123    guest_path: &str,
10124    target: &str,
10125) -> Result<(), SidecarError> {
10126    if vm.kernel.symlink(target, guest_path).is_ok() {
10127        return Ok(());
10128    }
10129
10130    if let Ok(existing_target) = vm.kernel.read_link(guest_path) {
10131        if existing_target == target {
10132            return Ok(());
10133        }
10134    }
10135
10136    let _ = vm.kernel.remove_file(guest_path);
10137    let _ = vm.kernel.remove_dir(guest_path);
10138    vm.kernel
10139        .symlink(target, guest_path)
10140        .map_err(kernel_error)?;
10141    Ok(())
10142}
10143
10144fn host_shadow_mode(metadata: &fs::Metadata) -> u32 {
10145    metadata.permissions().mode() & 0o7777
10146}
10147
10148/// Reads a shadow-root file back into the kernel even when guest-visible mode
10149/// bits make it unreadable for the host user. The sidecar is the kernel for
10150/// this tree, so guest permission bits (for example a 0o200 write-only file
10151/// produced by `chmod` plus a shell append redirect) must not break the
10152/// exit-time shadow sync. The original mode is restored after the read.
10153fn read_host_shadow_file(host_path: &Path, mode: u32) -> std::io::Result<Vec<u8>> {
10154    match fs::read(host_path) {
10155        Ok(bytes) => Ok(bytes),
10156        Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => {
10157            fs::set_permissions(host_path, fs::Permissions::from_mode(mode | 0o400))?;
10158            let result = fs::read(host_path);
10159            fs::set_permissions(host_path, fs::Permissions::from_mode(mode))?;
10160            result
10161        }
10162        Err(error) => Err(error),
10163    }
10164}
10165
10166fn metadata_time_ms(seconds: i64, nanos: i64) -> u64 {
10167    let seconds = seconds.max(0) as u64;
10168    let nanos = nanos.max(0) as u64;
10169    seconds
10170        .saturating_mul(1_000)
10171        .saturating_add(nanos / 1_000_000)
10172}
10173
10174fn is_shadow_bootstrap_dir(path: &str) -> bool {
10175    matches!(
10176        path,
10177        "/dev"
10178            | "/proc"
10179            | "/tmp"
10180            | "/bin"
10181            | "/lib"
10182            | "/sbin"
10183            | "/boot"
10184            | "/etc"
10185            | "/root"
10186            | "/run"
10187            | "/srv"
10188            | "/sys"
10189            | "/opt"
10190            | "/mnt"
10191            | "/media"
10192            | "/home"
10193            | "/home/agentos"
10194            | "/usr"
10195            | "/usr/bin"
10196            | "/usr/games"
10197            | "/usr/include"
10198            | "/usr/lib"
10199            | "/usr/libexec"
10200            | "/usr/man"
10201            | "/usr/local"
10202            | "/usr/local/bin"
10203            | "/usr/sbin"
10204            | "/usr/share"
10205            | "/usr/share/man"
10206            | "/var"
10207            | "/var/cache"
10208            | "/var/empty"
10209            | "/var/lib"
10210            | "/var/lock"
10211            | "/var/log"
10212            | "/var/run"
10213            | "/var/spool"
10214            | "/var/tmp"
10215            | "/etc/agentos"
10216            | "/workspace"
10217    )
10218}
10219
10220#[cfg(test)]
10221mod shadow_sync_tests {
10222    use super::{is_protected_agentos_shadow_sync_path, is_shadow_bootstrap_dir};
10223
10224    #[test]
10225    fn shadow_bootstrap_sync_skips_virtual_home_tree() {
10226        assert!(is_shadow_bootstrap_dir("/home"));
10227        assert!(is_shadow_bootstrap_dir("/home/agentos"));
10228    }
10229
10230    #[test]
10231    fn protected_agentos_paths_are_not_shadow_synced() {
10232        assert!(is_protected_agentos_shadow_sync_path("/etc/agentos"));
10233        assert!(is_protected_agentos_shadow_sync_path(
10234            "/etc/agentos/instructions.md"
10235        ));
10236        assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos-copy"));
10237        assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos.md"));
10238    }
10239}
10240
10241fn is_kernel_owned_shadow_sync_path(path: &str) -> bool {
10242    matches!(path, "/dev" | "/proc" | "/sys")
10243        || path.starts_with("/dev/")
10244        || path.starts_with("/proc/")
10245        || path.starts_with("/sys/")
10246}
10247
10248pub(crate) fn is_protected_agentos_shadow_sync_path(path: &str) -> bool {
10249    path == "/etc/agentos" || path.starts_with("/etc/agentos/")
10250}
10251
10252fn should_skip_shadow_sync_path(vm: &VmState, guest_path: &str) -> bool {
10253    is_kernel_owned_shadow_sync_path(guest_path)
10254        || is_protected_agentos_shadow_sync_path(guest_path)
10255        // agentOS package content is served guest-native from read-only tar
10256        // mounts; it is already present in the guest and cannot be written, so a
10257        // host->guest shadow sync would fail with EROFS. Skip it.
10258        || guest_path_is_within_agentos_package_mount(vm, guest_path)
10259        || host_mount_path_for_guest_path_from_mounts(&vm.configuration.mounts, guest_path)
10260            .is_some()
10261}
10262
10263fn resolve_path_like_guest_specifier(cwd: &str, specifier: &str) -> String {
10264    if specifier.starts_with("file://") {
10265        normalize_path(specifier.trim_start_matches("file://"))
10266    } else if specifier.starts_with("file:") {
10267        normalize_path(specifier.trim_start_matches("file:"))
10268    } else if specifier.starts_with('/') {
10269        normalize_path(specifier)
10270    } else {
10271        normalize_path(&format!("{cwd}/{specifier}"))
10272    }
10273}
10274
10275fn guest_entrypoint_for_specifier(cwd: &str, specifier: &str) -> Option<String> {
10276    is_path_like_specifier(specifier).then(|| resolve_path_like_guest_specifier(cwd, specifier))
10277}
10278
10279fn is_node_runtime_command(command: &str) -> bool {
10280    matches!(command, "node" | "npm" | "npx")
10281        || Path::new(command)
10282            .file_name()
10283            .and_then(|name| name.to_str())
10284            .is_some_and(|name| matches!(name, "node" | "npm" | "npx"))
10285}
10286
10287fn python_command_base_name(command: &str) -> &str {
10288    Path::new(command)
10289        .file_name()
10290        .and_then(|name| name.to_str())
10291        .unwrap_or(command)
10292}
10293
10294/// `python` / `python3` (and `pip` / `pip3`, which map to `python -m pip`) are
10295/// served by the embedded Pyodide runtime, mirroring how `node` is served by the
10296/// embedded V8 runtime.
10297fn is_python_runtime_command(command: &str) -> bool {
10298    matches!(
10299        python_command_base_name(command),
10300        "python" | "python3" | "pip" | "pip3"
10301    )
10302}
10303
10304/// Parse a `python` / `pip` command line into a Pyodide execution. Supports the
10305/// CPython program selectors `-c CODE`, `-m MODULE`, a `SCRIPT` path, `-` /
10306/// piped stdin programs, and a bare interpreter (interactive REPL). The chosen
10307/// mode plus `sys.argv` are forwarded to the runner as `AGENTOS_PYTHON_*` control
10308/// env, which the runner consumes and never exposes in the guest `os.environ`.
10309fn resolve_python_command_execution(
10310    vm: &VmState,
10311    command: &str,
10312    args: &[String],
10313    mut env: BTreeMap<String, String>,
10314    guest_cwd: String,
10315    host_cwd: PathBuf,
10316) -> Result<ResolvedChildProcessExecution, SidecarError> {
10317    let base_name = python_command_base_name(command);
10318    let is_pip = matches!(base_name, "pip" | "pip3");
10319
10320    let mut entrypoint = String::new();
10321    let mut argv: Vec<String> = Vec::new();
10322    let mut module: Option<String> = None;
10323    let mut stdin_program = false;
10324    let mut interactive = false;
10325    let mut guest_entrypoint: Option<String> = None;
10326
10327    if is_pip {
10328        module = Some(String::from("pip"));
10329        argv.push(String::from("pip"));
10330        argv.extend(args.iter().cloned());
10331    } else {
10332        // Skip the value-less interpreter flags we can safely ignore so they do
10333        // not get mistaken for a script path.
10334        let mut idx = 0;
10335        while let Some(flag) = args.get(idx) {
10336            match flag.as_str() {
10337                "-B" | "-E" | "-I" | "-O" | "-OO" | "-q" | "-s" | "-S" | "-u" | "-v" | "-b"
10338                | "-d" | "-x" => idx += 1,
10339                _ => break,
10340            }
10341        }
10342        let rest = &args[idx..];
10343        match rest.first().map(String::as_str) {
10344            Some("-c") => {
10345                entrypoint = rest.get(1).cloned().ok_or_else(|| {
10346                    SidecarError::InvalidState(String::from("argument expected for the -c option"))
10347                })?;
10348                argv.push(String::from("-c"));
10349                argv.extend(rest.iter().skip(2).cloned());
10350            }
10351            Some("-m") => {
10352                let name = rest.get(1).cloned().ok_or_else(|| {
10353                    SidecarError::InvalidState(String::from("argument expected for the -m option"))
10354                })?;
10355                module = Some(name);
10356                argv.push(String::from("-m"));
10357                argv.extend(rest.iter().skip(2).cloned());
10358            }
10359            Some("-") => {
10360                stdin_program = true;
10361                argv.push(String::from("-"));
10362                argv.extend(rest.iter().skip(1).cloned());
10363            }
10364            Some(spec) if !spec.starts_with('-') => {
10365                let resolved_guest = guest_entrypoint_for_specifier(&guest_cwd, spec)
10366                    .unwrap_or_else(|| spec.to_string());
10367                entrypoint = resolved_guest.clone();
10368                env.insert(String::from("AGENTOS_PYTHON_FILE"), resolved_guest.clone());
10369                guest_entrypoint = Some(resolved_guest);
10370                argv.push(spec.to_string());
10371                argv.extend(rest.iter().skip(1).cloned());
10372            }
10373            Some(other) => {
10374                return Err(SidecarError::InvalidState(format!(
10375                    "unsupported python option: {other}"
10376                )));
10377            }
10378            None => {
10379                interactive = true;
10380                argv.push(String::new());
10381            }
10382        }
10383    }
10384
10385    env.insert(
10386        String::from("AGENTOS_PYTHON_ARGV"),
10387        serde_json::to_string(&argv).unwrap_or_else(|_| String::from("[]")),
10388    );
10389    if let Some(module) = &module {
10390        env.insert(String::from("AGENTOS_PYTHON_MODULE"), module.clone());
10391    }
10392    if stdin_program {
10393        env.insert(
10394            String::from("AGENTOS_PYTHON_STDIN_PROGRAM"),
10395            String::from("1"),
10396        );
10397    }
10398    if interactive {
10399        env.insert(
10400            String::from("AGENTOS_PYTHON_INTERACTIVE"),
10401            String::from("1"),
10402        );
10403    }
10404
10405    prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
10406
10407    Ok(ResolvedChildProcessExecution {
10408        command: String::from(PYTHON_COMMAND),
10409        process_args: std::iter::once(command.to_owned())
10410            .chain(args.iter().cloned())
10411            .collect(),
10412        runtime: GuestRuntimeKind::Python,
10413        entrypoint,
10414        execution_args: args.to_vec(),
10415        env,
10416        guest_cwd,
10417        host_cwd,
10418        wasm_permission_tier: None,
10419        tool_command: false,
10420    })
10421}
10422
10423fn resolve_special_node_cli_invocation(
10424    args: &[String],
10425    env: &mut BTreeMap<String, String>,
10426) -> Option<(String, Vec<String>)> {
10427    let first = args.first()?;
10428    match first.as_str() {
10429        "-e" | "--eval" => {
10430            env.insert(
10431                String::from("AGENTOS_NODE_EVAL"),
10432                args.get(1).cloned().unwrap_or_default(),
10433            );
10434            Some((first.clone(), args.iter().skip(2).cloned().collect()))
10435        }
10436        "-v" | "--version" => {
10437            env.insert(
10438                String::from("AGENTOS_NODE_EVAL"),
10439                String::from("console.log(process.version);"),
10440            );
10441            Some((String::from("-e"), args.to_vec()))
10442        }
10443        _ => None,
10444    }
10445}
10446
10447fn node_runtime_command_name(command: &str) -> Option<&str> {
10448    let name = Path::new(command)
10449        .file_name()
10450        .and_then(|name| name.to_str())?;
10451    matches!(name, "node" | "npm" | "npx").then_some(name)
10452}
10453
10454struct ResolvedHostNodeCliEntrypoint {
10455    command_name: String,
10456    guest_root: String,
10457    guest_entrypoint: String,
10458    package_root: PathBuf,
10459}
10460
10461fn resolve_host_node_cli_entrypoint(command: &str) -> Option<ResolvedHostNodeCliEntrypoint> {
10462    let command_name = node_runtime_command_name(command)?;
10463    if !matches!(command_name, "npm" | "npx") {
10464        return None;
10465    }
10466
10467    let path = std::env::var_os("PATH")?;
10468    for root in std::env::split_paths(&path) {
10469        let candidate = root.join(command_name);
10470        if !candidate.is_file() {
10471            continue;
10472        }
10473        let entrypoint = candidate.canonicalize().ok().unwrap_or(candidate);
10474        let package_root = entrypoint.parent()?.parent()?.to_path_buf();
10475        let guest_root = format!("/__secure_exec/node-runtime/{command_name}");
10476        let relative_entrypoint = entrypoint.strip_prefix(&package_root).ok()?;
10477        let guest_entrypoint = normalize_path(&format!(
10478            "{guest_root}/{}",
10479            relative_entrypoint.to_string_lossy().replace('\\', "/")
10480        ));
10481        return Some(ResolvedHostNodeCliEntrypoint {
10482            command_name: command_name.to_owned(),
10483            guest_root,
10484            guest_entrypoint,
10485            package_root,
10486        });
10487    }
10488
10489    None
10490}
10491
10492fn build_host_node_cli_eval(cli: &ResolvedHostNodeCliEntrypoint) -> String {
10493    let guest_npm_main = normalize_path(&format!("{}/lib/npm.js", cli.guest_root));
10494    let guest_npm_cli = normalize_path(&format!("{}/bin/npm-cli.js", cli.guest_root));
10495    let guest_package_json = normalize_path(&format!("{}/package.json", cli.guest_root));
10496    let guest_display_module = normalize_path(&format!("{}/lib/utils/display.js", cli.guest_root));
10497    let guest_log_file_module =
10498        normalize_path(&format!("{}/lib/utils/log-file.js", cli.guest_root));
10499    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); } } }";
10500    let display_stub = format!(
10501        "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 }};",
10502        display_module = serde_json::to_string(&guest_display_module)
10503            .unwrap_or_else(|_| format!("\"{guest_display_module}\"")),
10504        log_file_module = serde_json::to_string(&guest_log_file_module)
10505            .unwrap_or_else(|_| format!("\"{guest_log_file_module}\"")),
10506    );
10507    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)); }";
10508    match cli.command_name.as_str() {
10509        "npx" => format!(
10510            "{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); }});",
10511            debug_preamble = debug_preamble,
10512            display_stub = display_stub,
10513            registry_fetch_stub = registry_fetch_stub.replace(
10514                "__AGENTOS_NPM_MAIN__",
10515                &serde_json::to_string(&guest_npm_main)
10516                    .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
10517            ),
10518            npm_main = serde_json::to_string(&guest_npm_main)
10519                .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
10520            npm_cli = serde_json::to_string(&guest_npm_cli)
10521                .unwrap_or_else(|_| format!("\"{guest_npm_cli}\"")),
10522            package_json = serde_json::to_string(&guest_package_json)
10523                .unwrap_or_else(|_| format!("\"{guest_package_json}\"")),
10524        ),
10525        _ => format!(
10526            "{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); }});",
10527            debug_preamble = debug_preamble,
10528            display_stub = display_stub,
10529            registry_fetch_stub = registry_fetch_stub.replace(
10530                "__AGENTOS_NPM_MAIN__",
10531                &serde_json::to_string(&guest_npm_main)
10532                    .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
10533            ),
10534            npm_main = serde_json::to_string(&guest_npm_main)
10535                .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
10536            package_json = serde_json::to_string(&guest_package_json)
10537                .unwrap_or_else(|_| format!("\"{guest_package_json}\"")),
10538        ),
10539    }
10540}
10541
10542fn resolve_guest_command_entrypoint(
10543    vm: &VmState,
10544    guest_cwd: &str,
10545    command: &str,
10546    path_env: Option<&str>,
10547) -> Option<String> {
10548    if !is_path_like_specifier(command) {
10549        if let Some(entrypoint) = vm.command_guest_paths.get(command) {
10550            return Some(entrypoint.clone());
10551        }
10552
10553        for search_dir in guest_command_search_dirs(vm, guest_cwd, path_env) {
10554            let candidate = normalize_path(&format!("{search_dir}/{command}"));
10555            if let Some(entrypoint) = resolve_guest_command_path_candidate(vm, &candidate) {
10556                return Some(entrypoint);
10557            }
10558        }
10559
10560        return None;
10561    }
10562
10563    let normalized = resolve_path_like_guest_specifier(guest_cwd, command);
10564    resolve_guest_command_path_candidate(vm, &normalized).or_else(|| {
10565        // Some guest shells materialize PATH lookups into absolute candidate paths.
10566        // If that path points into a searched directory but does not exist, fall
10567        // back to the command basename so the sidecar can remap VM command packages.
10568        let parent_dir = Path::new(&normalized).parent()?.to_str()?;
10569        if !guest_command_search_dirs(vm, guest_cwd, path_env)
10570            .iter()
10571            .any(|search_dir| normalize_path(search_dir) == normalize_path(parent_dir))
10572        {
10573            return None;
10574        }
10575
10576        let file_name = Path::new(&normalized).file_name()?.to_str()?;
10577        vm.command_guest_paths.get(file_name).cloned()
10578    })
10579}
10580
10581fn guest_command_search_dirs(vm: &VmState, guest_cwd: &str, path_env: Option<&str>) -> Vec<String> {
10582    let mut search_dirs = Vec::new();
10583    let mut seen = BTreeSet::new();
10584
10585    if let Some(path) = path_env.or_else(|| vm.guest_env.get("PATH").map(String::as_str)) {
10586        for segment in path.split(':') {
10587            let trimmed = segment.trim();
10588            if trimmed.is_empty() {
10589                continue;
10590            }
10591            let normalized = if trimmed.starts_with('/') {
10592                normalize_path(trimmed)
10593            } else {
10594                normalize_path(&format!("{guest_cwd}/{trimmed}"))
10595            };
10596            if seen.insert(normalized.clone()) {
10597                search_dirs.push(normalized);
10598            }
10599        }
10600    }
10601
10602    for fallback in ["/bin", "/usr/bin", "/usr/local/bin"] {
10603        let normalized = String::from(fallback);
10604        if seen.insert(normalized.clone()) {
10605            search_dirs.push(normalized);
10606        }
10607    }
10608
10609    search_dirs
10610}
10611
10612fn resolve_guest_command_path_candidate(vm: &VmState, candidate: &str) -> Option<String> {
10613    if candidate.starts_with(&format!("{}/", crate::package_projection::OPT_AGENTOS_BIN)) {
10614        if let Ok(realpath) = vm.kernel.realpath(candidate) {
10615            return Some(normalize_path(&realpath));
10616        }
10617    }
10618
10619    if candidate.starts_with("/bin/")
10620        || candidate.starts_with("/usr/bin/")
10621        || candidate.starts_with("/usr/local/bin/")
10622        || candidate.starts_with(&format!("{}/", crate::package_projection::OPT_AGENTOS_BIN))
10623        || candidate.starts_with("/__secure_exec/commands/")
10624    {
10625        if let Some(file_name) = Path::new(candidate)
10626            .file_name()
10627            .and_then(|name| name.to_str())
10628        {
10629            if let Some(guest_entrypoint) = vm.command_guest_paths.get(file_name) {
10630                return Some(guest_entrypoint.clone());
10631            }
10632        }
10633    }
10634
10635    if vm
10636        .kernel
10637        .exists(candidate)
10638        .ok()
10639        .is_some_and(|exists| exists)
10640    {
10641        return Some(normalize_path(candidate));
10642    }
10643
10644    resolve_vm_guest_path_to_host(vm, candidate)
10645        .is_file()
10646        .then(|| normalize_path(candidate))
10647}
10648
10649fn resolve_host_entrypoint_within_vm_host_cwd(
10650    vm: &VmState,
10651    specifier: &str,
10652) -> Option<(String, String)> {
10653    let candidate = Path::new(specifier);
10654    if !candidate.is_absolute() {
10655        return None;
10656    }
10657
10658    let normalized_entrypoint = normalize_host_path(candidate);
10659    let normalized_host_cwd = normalize_host_path(&vm.host_cwd);
10660    if !path_is_within_root(&normalized_entrypoint, &normalized_host_cwd) {
10661        return None;
10662    }
10663
10664    let relative = normalized_entrypoint
10665        .strip_prefix(&normalized_host_cwd)
10666        .ok()?
10667        .to_string_lossy()
10668        .replace('\\', "/");
10669    let guest_entrypoint = if relative.is_empty() {
10670        String::from("/")
10671    } else {
10672        normalize_path(&format!("/{relative}"))
10673    };
10674    Some((
10675        guest_entrypoint,
10676        normalized_entrypoint.to_string_lossy().into_owned(),
10677    ))
10678}
10679
10680fn prepare_guest_runtime_env(
10681    vm: &VmState,
10682    env: &mut BTreeMap<String, String>,
10683    guest_cwd: &str,
10684    host_cwd: &Path,
10685    guest_entrypoint: Option<String>,
10686) -> Result<(), SidecarError> {
10687    let user = vm.kernel.user_profile();
10688    let path_mappings = runtime_guest_path_mappings(vm);
10689    let read_paths = expand_host_access_paths(
10690        std::iter::once(vm.cwd.clone())
10691            .chain(
10692                path_mappings
10693                    .iter()
10694                    .map(|mapping| PathBuf::from(&mapping.host_path)),
10695            )
10696            .chain(std::iter::once(host_cwd.to_path_buf()))
10697            .collect::<Vec<_>>()
10698            .as_slice(),
10699    );
10700    let write_paths = dedupe_host_paths(
10701        std::iter::once(vm.cwd.clone())
10702            .chain(std::iter::once(host_cwd.to_path_buf()))
10703            .chain(runtime_guest_writable_host_paths(vm))
10704            .collect::<Vec<_>>()
10705            .as_slice(),
10706    );
10707    let allowed_node_builtins = configured_allowed_node_builtins(vm);
10708    let loopback_exempt_ports = configured_loopback_exempt_ports(vm);
10709
10710    env.insert(
10711        String::from("AGENTOS_GUEST_PATH_MAPPINGS"),
10712        serde_json::to_string(&path_mappings).map_err(|error| {
10713            SidecarError::InvalidState(format!("failed to encode guest path mappings: {error}"))
10714        })?,
10715    );
10716    env.entry(String::from(EXECUTION_SANDBOX_ROOT_ENV))
10717        .or_insert_with(|| normalize_host_path(&vm.cwd).to_string_lossy().into_owned());
10718    env.insert(
10719        String::from("AGENTOS_EXTRA_FS_READ_PATHS"),
10720        serde_json::to_string(
10721            &read_paths
10722                .iter()
10723                .map(|path| path.to_string_lossy().into_owned())
10724                .collect::<Vec<_>>(),
10725        )
10726        .map_err(|error| {
10727            SidecarError::InvalidState(format!("failed to encode read paths: {error}"))
10728        })?,
10729    );
10730    env.insert(
10731        String::from("AGENTOS_EXTRA_FS_WRITE_PATHS"),
10732        serde_json::to_string(
10733            &write_paths
10734                .iter()
10735                .map(|path| path.to_string_lossy().into_owned())
10736                .collect::<Vec<_>>(),
10737        )
10738        .map_err(|error| {
10739            SidecarError::InvalidState(format!("failed to encode write paths: {error}"))
10740        })?,
10741    );
10742    env.insert(
10743        String::from("AGENTOS_ALLOWED_NODE_BUILTINS"),
10744        serde_json::to_string(&allowed_node_builtins).map_err(|error| {
10745            SidecarError::InvalidState(format!("failed to encode allowed builtins: {error}"))
10746        })?,
10747    );
10748    // The guest JS host platform drives subtractive global scrubbing in the
10749    // per-execution runtime shim (see prepend_v8_runtime_shim).
10750    env.insert(
10751        String::from("AGENTOS_JS_PLATFORM"),
10752        js_runtime_platform_env(vm).to_owned(),
10753    );
10754    // Module-resolution mode (omitted when full Node resolution / the default).
10755    if let Some(resolution) = js_runtime_module_resolution_env(vm) {
10756        env.insert(
10757            String::from("AGENTOS_JS_MODULE_RESOLUTION"),
10758            resolution.to_owned(),
10759        );
10760    }
10761    // Builtin allow-list gate for the live resolver. Present only when builtins
10762    // should be restricted (non-node platform => deny all; node + explicit
10763    // allow-list => exactly those). Absent => unrestricted (node default).
10764    if let Some(allowlist) = js_runtime_enforced_builtins(vm) {
10765        env.insert(
10766            String::from("AGENTOS_JS_BUILTIN_ALLOWLIST"),
10767            serde_json::to_string(&allowlist).map_err(|error| {
10768                SidecarError::InvalidState(format!(
10769                    "failed to encode jsRuntime builtin allow-list: {error}"
10770                ))
10771            })?,
10772        );
10773    }
10774    // Virtual OS identity (os.cpus/totalmem/freemem/homedir/userInfo/...) now
10775    // rides the typed `guest_runtime` (see `guest_runtime_identity`), exposed to
10776    // the guest as the `__agentOSVirtualOs` structured global by the runtime
10777    // shim — no longer the `AGENTOS_VIRTUAL_OS_*` env vars.
10778    // Virtual process uid/gid now ride the typed `guest_runtime` identity
10779    // (see `guest_runtime_identity`), not the `AGENTOS_VIRTUAL_PROCESS_*` env.
10780    env.entry(String::from("HOME"))
10781        .or_insert_with(|| user.homedir.clone());
10782    env.entry(String::from("USER"))
10783        .or_insert_with(|| user.username.clone());
10784    env.entry(String::from("LOGNAME"))
10785        .or_insert_with(|| user.username.clone());
10786    env.entry(String::from("SHELL"))
10787        .or_insert_with(|| user.shell.clone());
10788    env.entry(String::from("PATH")).or_insert_with(|| {
10789        vm.guest_env
10790            .get("PATH")
10791            .cloned()
10792            .unwrap_or_else(|| crate::vm::DEFAULT_GUEST_PATH_ENV.to_owned())
10793    });
10794    env.entry(String::from("TMPDIR"))
10795        .or_insert_with(|| String::from("/tmp"));
10796    env.insert(String::from("PWD"), guest_cwd.to_owned());
10797    if !loopback_exempt_ports.is_empty() {
10798        env.insert(
10799            String::from(LOOPBACK_EXEMPT_PORTS_ENV),
10800            serde_json::to_string(&loopback_exempt_ports).map_err(|error| {
10801                SidecarError::InvalidState(format!("failed to encode loopback exemptions: {error}"))
10802            })?,
10803        );
10804    }
10805    if let Some(guest_entrypoint) = guest_entrypoint {
10806        env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint);
10807    }
10808    Ok(())
10809}
10810
10811/// Build the typed per-execution JavaScript limits from the per-VM `VmLimits`
10812/// (sourced from `CreateVmConfig` on the BARE wire). These ride the execution
10813/// request, not `AGENTOS_*` env vars — see the env-vs-wire rule in
10814/// `crates/sidecar/CLAUDE.md`.
10815fn javascript_execution_limits(vm: &VmState) -> JavascriptExecutionLimits {
10816    JavascriptExecutionLimits {
10817        v8_heap_limit_mb: vm.limits.js_runtime.v8_heap_limit_mb,
10818        sync_rpc_wait_timeout_ms: vm.limits.js_runtime.sync_rpc_wait_timeout_ms,
10819        cpu_time_limit_ms: Some(vm.limits.js_runtime.cpu_time_limit_ms),
10820        wall_clock_limit_ms: Some(vm.limits.js_runtime.wall_clock_limit_ms),
10821        import_cache_materialize_timeout_ms: Some(
10822            vm.limits.js_runtime.import_cache_materialize_timeout_ms,
10823        ),
10824    }
10825}
10826
10827/// Build the typed per-execution guest-runtime identity (virtual `process.*`)
10828/// from kernel state. Replaces the `AGENTOS_VIRTUAL_PROCESS_{UID,GID,PID,PPID}`
10829/// env round-trip: the runtime shim reads these from `guest_runtime`, not env.
10830/// `uid`/`gid` come from the VM user profile (applied to every guest);
10831/// `pid`/`ppid` are per-process and only set for paths that assigned them.
10832fn guest_runtime_identity(
10833    vm: &VmState,
10834    virtual_pid: Option<u64>,
10835    virtual_ppid: Option<u64>,
10836) -> GuestRuntimeConfig {
10837    let user = vm.kernel.user_profile();
10838    let resource_limits = vm.kernel.resource_limits();
10839    let identity = shared_guest_runtime_identity(&user, resource_limits, virtual_pid, virtual_ppid);
10840    GuestRuntimeConfig {
10841        virtual_uid: Some(identity.virtual_uid),
10842        virtual_gid: Some(identity.virtual_gid),
10843        virtual_pid: identity.virtual_pid,
10844        virtual_ppid: identity.virtual_ppid,
10845        virtual_exec_path: None,
10846        os_cpu_count: Some(identity.os_cpu_count),
10847        os_totalmem: Some(identity.os_totalmem),
10848        os_freemem: Some(identity.os_freemem),
10849        os_homedir: Some(identity.os_homedir),
10850        os_hostname: Some(identity.os_hostname),
10851        os_tmpdir: Some(identity.os_tmpdir),
10852        os_type: Some(identity.os_type),
10853        os_release: Some(identity.os_release),
10854        os_version: Some(identity.os_version),
10855        os_machine: Some(identity.os_machine),
10856        os_shell: Some(identity.os_shell),
10857        os_user: Some(identity.os_user),
10858        high_resolution_time: vm
10859            .configuration
10860            .js_runtime
10861            .as_ref()
10862            .is_some_and(|cfg| cfg.high_resolution_time.unwrap_or(false)),
10863        // Userland bundle to bake into the per-sidecar snapshot. The sidecar
10864        // derives this from configured agent packages with `agent.snapshot`.
10865        snapshot_userland_code: vm.configuration.snapshot_userland_code.clone(),
10866    }
10867}
10868
10869/// The guest's virtual home directory, sourced from the VM user profile (the
10870/// same value carried to the guest as `os.homedir()` via `guest_runtime`). Used
10871/// by sidecar-internal `~`-path resolution; falls back to `/root` for a
10872/// non-absolute profile value.
10873fn guest_virtual_home(vm: &VmState) -> String {
10874    let homedir = vm.kernel.user_profile().homedir;
10875    if homedir.starts_with('/') {
10876        homedir
10877    } else {
10878        String::from("/root")
10879    }
10880}
10881
10882/// Build the typed per-execution Python limits from the per-VM `VmLimits`.
10883fn python_execution_limits(vm: &VmState) -> PythonExecutionLimits {
10884    PythonExecutionLimits {
10885        output_buffer_max_bytes: Some(vm.limits.python.output_buffer_max_bytes),
10886        execution_timeout_ms: Some(vm.limits.python.execution_timeout_ms),
10887        max_old_space_mb: Some(vm.limits.python.max_old_space_mb),
10888        vfs_rpc_timeout_ms: Some(vm.limits.python.vfs_rpc_timeout_ms),
10889    }
10890}
10891
10892/// Build the typed per-execution WebAssembly limits from the per-VM kernel
10893/// `ResourceLimits`. Replaces the old `apply_wasm_limit_env` env round-trip;
10894/// notably this is the path that finally enforces the stack cap that the
10895/// `AGENTOS_WASM_MAX_STACK_BYTES` env knob set but no reader consumed.
10896fn wasm_execution_limits(vm: &VmState) -> WasmExecutionLimits {
10897    let resource_limits = vm.kernel.resource_limits();
10898    WasmExecutionLimits {
10899        max_fuel: resource_limits.max_wasm_fuel,
10900        max_memory_bytes: resource_limits.max_wasm_memory_bytes,
10901        max_stack_bytes: resource_limits
10902            .max_wasm_stack_bytes
10903            .map(|value| value as u64),
10904        prewarm_timeout_ms: Some(vm.limits.wasm.prewarm_timeout_ms),
10905        runner_heap_limit_mb: Some(vm.limits.wasm.runner_heap_limit_mb),
10906    }
10907}
10908
10909/// The guest JavaScript host platform configured for this VM, defaulting to
10910/// full Node.js emulation when no `jsRuntime` config was supplied at create.
10911fn js_runtime_platform(vm: &VmState) -> vm_config::JsRuntimePlatform {
10912    vm.configuration
10913        .js_runtime
10914        .as_ref()
10915        .map(|cfg| cfg.platform)
10916        .unwrap_or(vm_config::JsRuntimePlatform::Node)
10917}
10918
10919/// Lowercase wire name for the configured platform, mirroring the serde
10920/// representation of `vm_config::JsRuntimePlatform`.
10921fn js_runtime_platform_env(vm: &VmState) -> &'static str {
10922    match js_runtime_platform(vm) {
10923        vm_config::JsRuntimePlatform::Node => "node",
10924        vm_config::JsRuntimePlatform::Browser => "browser",
10925        vm_config::JsRuntimePlatform::Neutral => "neutral",
10926        vm_config::JsRuntimePlatform::Bare => "bare",
10927    }
10928}
10929
10930/// Wire name for the configured module-resolution mode, or `None` when it is the
10931/// full-Node default (which the live resolver also assumes when the env is unset).
10932fn js_runtime_module_resolution_env(vm: &VmState) -> Option<&'static str> {
10933    let resolution = vm
10934        .configuration
10935        .js_runtime
10936        .as_ref()
10937        .map(|cfg| cfg.module_resolution)
10938        .unwrap_or(vm_config::JsModuleResolution::Node);
10939    match resolution {
10940        vm_config::JsModuleResolution::Node => None,
10941        vm_config::JsModuleResolution::Relative => Some("relative"),
10942        vm_config::JsModuleResolution::None => Some("none"),
10943    }
10944}
10945
10946/// The builtin allow-list the live resolver should enforce, or `None` to leave
10947/// builtins unrestricted (full Node default — preserving today's behavior).
10948/// Non-node platforms enforce an empty list (deny all builtins).
10949fn js_runtime_enforced_builtins(vm: &VmState) -> Option<Vec<String>> {
10950    if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node {
10951        return Some(Vec::new());
10952    }
10953    vm.configuration
10954        .js_runtime
10955        .as_ref()
10956        .and_then(|cfg| cfg.allowed_builtins.clone())
10957}
10958
10959fn configured_allowed_node_builtins(vm: &VmState) -> Vec<String> {
10960    // Non-node platforms expose no Node builtin modules at all.
10961    if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node {
10962        return Vec::new();
10963    }
10964    // Under the node platform an explicit allow-list wins — including an explicit
10965    // empty list, which means deny all. Absence falls back to the engine default.
10966    let configured = match vm
10967        .configuration
10968        .js_runtime
10969        .as_ref()
10970        .and_then(|cfg| cfg.allowed_builtins.as_ref())
10971    {
10972        Some(list) => list.clone(),
10973        None => DEFAULT_ALLOWED_NODE_BUILTINS
10974            .iter()
10975            .map(|value| (*value).to_owned())
10976            .collect::<Vec<_>>(),
10977    };
10978    dedupe_strings(&configured)
10979}
10980
10981fn configured_loopback_exempt_ports(vm: &VmState) -> Vec<String> {
10982    if !vm.configuration.loopback_exempt_ports.is_empty() {
10983        return vm
10984            .configuration
10985            .loopback_exempt_ports
10986            .iter()
10987            .map(ToString::to_string)
10988            .collect();
10989    }
10990
10991    vm.create_loopback_exempt_ports
10992        .iter()
10993        .map(ToString::to_string)
10994        .collect()
10995}
10996
10997/// Extract the `hostPath` string from a mount plugin's JSON-encoded config.
10998fn mount_config_host_path(config: &str) -> Option<String> {
10999    serde_json::from_str::<Value>(config)
11000        .ok()?
11001        .get("hostPath")
11002        .and_then(Value::as_str)
11003        .map(str::to_owned)
11004}
11005
11006/// Host path backing a mount for HOST-SIDE resolution (entrypoint launch, import
11007/// cache location). `agentos_packages` is deliberately excluded by callers:
11008/// package tar mounts are guest-native and resolve through the kernel VFS.
11009fn mount_config_host_backing_path(config: &str) -> Option<String> {
11010    let value = serde_json::from_str::<Value>(config).ok()?;
11011    value
11012        .get("hostPath")
11013        .and_then(Value::as_str)
11014        .map(str::to_owned)
11015}
11016
11017fn runtime_guest_writable_host_paths(vm: &VmState) -> Vec<PathBuf> {
11018    vm.configuration
11019        .mounts
11020        .iter()
11021        .filter(|mount| !mount.read_only)
11022        .filter_map(|mount| {
11023            ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
11024                .then(|| mount_config_host_path(&mount.plugin.config))
11025                .flatten()
11026                .map(PathBuf::from)
11027        })
11028        .collect()
11029}
11030
11031fn runtime_guest_path_mappings(vm: &VmState) -> Vec<RuntimeGuestPathMapping> {
11032    let mut mappings = vm
11033        .configuration
11034        .mounts
11035        .iter()
11036        .filter_map(|mount| {
11037            ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
11038                .then(|| {
11039                    mount_config_host_path(&mount.plugin.config).map(|host_path| {
11040                        RuntimeGuestPathMapping {
11041                            guest_path: normalize_path(&mount.guest_path),
11042                            host_path,
11043                            read_only: mount.read_only,
11044                        }
11045                    })
11046                })
11047                .flatten()
11048        })
11049        .collect::<Vec<_>>();
11050    let mut command_root_mappings = vm
11051        .command_guest_paths
11052        .values()
11053        .filter_map(|guest_path| {
11054            Path::new(guest_path)
11055                .parent()
11056                .and_then(|parent| parent.to_str())
11057                .map(normalize_path)
11058        })
11059        .collect::<BTreeSet<_>>()
11060        .into_iter()
11061        .map(|guest_path| RuntimeGuestPathMapping {
11062            host_path: resolve_vm_guest_path_to_host(vm, &guest_path)
11063                .to_string_lossy()
11064                .into_owned(),
11065            guest_path,
11066            read_only: false,
11067        })
11068        .collect::<Vec<_>>();
11069    mappings.append(&mut command_root_mappings);
11070    let mut extra_node_modules_roots = mappings
11071        .iter()
11072        .filter(|mapping| mapping.guest_path.starts_with("/root/node_modules/"))
11073        .filter_map(|mapping| {
11074            host_node_modules_root(Path::new(&mapping.host_path)).map(|host_root| {
11075                RuntimeGuestPathMapping {
11076                    guest_path: String::from("/root/node_modules"),
11077                    host_path: host_root.to_string_lossy().into_owned(),
11078                    read_only: mapping.read_only,
11079                }
11080            })
11081        })
11082        .collect::<Vec<_>>();
11083    mappings.append(&mut extra_node_modules_roots);
11084    mappings.push(RuntimeGuestPathMapping {
11085        guest_path: String::from("/"),
11086        host_path: vm.cwd.to_string_lossy().into_owned(),
11087        read_only: false,
11088    });
11089    mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.guest_path.len()));
11090    mappings.dedup_by(|left, right| {
11091        left.guest_path == right.guest_path && left.host_path == right.host_path
11092    });
11093    mappings
11094}
11095
11096/// Build a `Send`-able, read-only VFS module reader over the VM's read-only
11097/// `host_dir`/`module_access` mounts (and the derived `/root/node_modules` root
11098/// for nested mounts). When present, the V8 bridge thread resolves modules
11099/// inline against this reader — concurrently with the service loop — so a large
11100/// cold-start module graph never serializes behind / starves an in-flight ACP
11101/// `session/new` bootstrap on the single service-loop thread. The reader reads
11102/// the same mounted tree the guest sees (anchored `openat2`, escaping-symlink
11103/// refusal), never the host-direct path translator. Returns `None` when the VM
11104/// has no usable read-only mount, so resolution falls back to the service-loop
11105/// kernel reader.
11106fn build_module_reader(
11107    vm: &VmState,
11108    resolved: &ResolvedChildProcessExecution,
11109) -> Option<crate::plugins::host_dir::HostDirModuleReader> {
11110    let mut pairs: Vec<(String, PathBuf)> = vm
11111        .configuration
11112        .mounts
11113        .iter()
11114        .filter(|mount| mount.read_only)
11115        .filter(|mount| (mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
11116        .filter_map(|mount| {
11117            mount_config_host_path(&mount.plugin.config)
11118                .map(|host_path| (normalize_path(&mount.guest_path), PathBuf::from(host_path)))
11119        })
11120        .collect();
11121
11122    // Packed package-version leaves: module resolution reads packed
11123    // `node_modules` content straight from the `.aospkg` mount index (shared
11124    // mmap cache; no kernel access), mirroring what the guest sees through the
11125    // kernel tar mount. `(guest_path, aospkg_path, tar_root)` triples.
11126    let mut package_tars: Vec<(String, String, String)> = vm
11127        .configuration
11128        .mounts
11129        .iter()
11130        .filter(|mount| mount.plugin.id == "agentos_packages")
11131        .filter_map(|mount| {
11132            let config = serde_json::from_str::<Value>(&mount.plugin.config).ok()?;
11133            if config.get("kind").and_then(Value::as_str) != Some("tar") {
11134                return None;
11135            }
11136            let tar_path = config.get("tarPath").and_then(Value::as_str)?.to_owned();
11137            let root = config
11138                .get("root")
11139                .and_then(Value::as_str)
11140                .unwrap_or("/")
11141                .to_owned();
11142            Some((normalize_path(&mount.guest_path), tar_path, root))
11143        })
11144        .collect();
11145    // `<pkg>/current -> <version>` symlink leaves: alias the current prefix to
11146    // the same tar so modules that self-locate through `current` (rather than
11147    // the realpathed version dir) still resolve.
11148    let current_aliases: Vec<(String, String, String)> = vm
11149        .configuration
11150        .mounts
11151        .iter()
11152        .filter(|mount| mount.plugin.id == "agentos_packages")
11153        .filter_map(|mount| {
11154            let config = serde_json::from_str::<Value>(&mount.plugin.config).ok()?;
11155            if config.get("kind").and_then(Value::as_str) != Some("singleSymlink") {
11156                return None;
11157            }
11158            let link_path = normalize_path(&mount.guest_path);
11159            let target = config.get("target").and_then(Value::as_str)?;
11160            let resolved_target = if target.starts_with('/') {
11161                normalize_path(target)
11162            } else {
11163                let parent = Path::new(&link_path).parent()?.to_str()?;
11164                normalize_path(&format!("{parent}/{target}"))
11165            };
11166            package_tars
11167                .iter()
11168                .find(|(guest, _, _)| *guest == resolved_target)
11169                .map(|(_, tar_path, root)| (link_path, tar_path.clone(), root.clone()))
11170        })
11171        .collect();
11172    package_tars.extend(current_aliases);
11173
11174    let guest_entrypoint = resolved
11175        .env
11176        .get("AGENTOS_GUEST_ENTRYPOINT")
11177        .map(|path| normalize_path(path));
11178    if let Some(guest_entrypoint) = guest_entrypoint.as_deref() {
11179        // Package entrypoints may still carry their pre-realpath launch path
11180        // (`/opt/agentos/bin/<cmd>` or `<pkg>/current/...` symlink leaves), so
11181        // gate on EVERY agentos_packages mount prefix, not just the tar leaves.
11182        let package_mount_prefixes: Vec<String> = vm
11183            .configuration
11184            .mounts
11185            .iter()
11186            .filter(|mount| mount.plugin.id == "agentos_packages")
11187            .map(|mount| normalize_path(&mount.guest_path))
11188            .collect();
11189        let entrypoint_in_read_only_mount = pairs
11190            .iter()
11191            .map(|(guest_path, _)| guest_path)
11192            .chain(package_mount_prefixes.iter())
11193            .any(|guest_path| {
11194                guest_entrypoint == guest_path
11195                    || guest_entrypoint.starts_with(&format!("{guest_path}/"))
11196            });
11197        if !entrypoint_in_read_only_mount {
11198            return None;
11199        }
11200    }
11201
11202    // Mirror runtime_guest_path_mappings: a mount nested under
11203    // `/root/node_modules/<pkg>` implies a `/root/node_modules` root the resolver
11204    // walks, so expose that root too (e.g. software-package mounts).
11205    let extra_roots: Vec<(String, PathBuf)> = pairs
11206        .iter()
11207        .filter(|(guest_path, _)| guest_path.starts_with("/root/node_modules/"))
11208        .filter_map(|(_, host_path)| {
11209            host_node_modules_root(host_path).map(|root| (String::from("/root/node_modules"), root))
11210        })
11211        .collect();
11212    pairs.extend(extra_roots);
11213
11214    if std::env::var("AGENTOS_MODULE_READER_TRACE").is_ok() {
11215        eprintln!(
11216            "module-reader: entrypoint={:?} host_pairs={} package_tars={:?}",
11217            resolved.env.get("AGENTOS_GUEST_ENTRYPOINT"),
11218            pairs.len(),
11219            package_tars
11220                .iter()
11221                .map(|(guest, _, _)| guest.as_str())
11222                .collect::<Vec<_>>()
11223        );
11224    }
11225    crate::plugins::host_dir::HostDirModuleReader::from_mounts_and_package_tars(
11226        pairs,
11227        package_tars,
11228    )
11229}
11230
11231fn host_node_modules_root(path: &Path) -> Option<PathBuf> {
11232    if let Some(root) = path
11233        .ancestors()
11234        .filter(|candidate| {
11235            candidate.file_name().and_then(|name| name.to_str()) == Some("node_modules")
11236        })
11237        .last()
11238        .map(Path::to_path_buf)
11239    {
11240        return Some(root);
11241    }
11242
11243    fs::canonicalize(path)
11244        .ok()?
11245        .ancestors()
11246        .filter(|candidate| {
11247            candidate.file_name().and_then(|name| name.to_str()) == Some("node_modules")
11248        })
11249        .last()
11250        .map(Path::to_path_buf)
11251}
11252
11253#[cfg(test)]
11254mod runtime_guest_path_mapping_tests {
11255    use super::{host_node_modules_root, javascript_sync_rpc_option_bool};
11256    use serde_json::json;
11257    use std::fs;
11258    use std::time::{SystemTime, UNIX_EPOCH};
11259
11260    #[test]
11261    fn host_node_modules_root_prefers_workspace_root_over_pnpm_package_node_modules() {
11262        let unique = SystemTime::now()
11263            .duration_since(UNIX_EPOCH)
11264            .expect("clock should be monotonic")
11265            .as_nanos();
11266        let temp =
11267            std::env::temp_dir().join(format!("agentos-native-sidecar-node-modules-{unique}"));
11268        let workspace_node_modules = temp.join("node_modules");
11269        let package_root = workspace_node_modules
11270            .join(".pnpm")
11271            .join("example@1.0.0")
11272            .join("node_modules")
11273            .join("@scope")
11274            .join("pkg");
11275        fs::create_dir_all(&package_root).expect("package root should be created");
11276
11277        let resolved =
11278            host_node_modules_root(&package_root).expect("node_modules root should resolve");
11279
11280        assert_eq!(resolved, workspace_node_modules);
11281
11282        fs::remove_dir_all(&temp).expect("temp tree should be removed");
11283    }
11284
11285    #[test]
11286    fn host_node_modules_root_preserves_symlinked_workspace_node_modules_path() {
11287        let unique = SystemTime::now()
11288            .duration_since(UNIX_EPOCH)
11289            .expect("clock should be monotonic")
11290            .as_nanos();
11291        let temp = std::env::temp_dir().join(format!(
11292            "agentos-native-sidecar-node-modules-symlink-{unique}"
11293        ));
11294        let workspace_node_modules = temp.join("node_modules");
11295        let package_link = workspace_node_modules.join("@scope").join("pkg");
11296        let real_package = temp.join("registry").join("agent").join("pkg");
11297        fs::create_dir_all(package_link.parent().expect("package parent should exist"))
11298            .expect("scoped parent should be created");
11299        fs::create_dir_all(&real_package).expect("real package root should be created");
11300        std::os::unix::fs::symlink(&real_package, &package_link)
11301            .expect("package symlink should be created");
11302
11303        let resolved =
11304            host_node_modules_root(&package_link).expect("node_modules root should resolve");
11305
11306        assert_eq!(resolved, workspace_node_modules);
11307
11308        fs::remove_dir_all(&temp).expect("temp tree should be removed");
11309    }
11310
11311    #[test]
11312    fn javascript_sync_rpc_option_bool_accepts_boolean_recursive_argument() {
11313        assert_eq!(
11314            javascript_sync_rpc_option_bool(&[json!("/workspace"), json!(true)], 1, "recursive"),
11315            Some(true)
11316        );
11317        assert_eq!(
11318            javascript_sync_rpc_option_bool(
11319                &[json!("/workspace"), json!({ "recursive": false })],
11320                1,
11321                "recursive"
11322            ),
11323            Some(false)
11324        );
11325    }
11326}
11327
11328#[cfg(test)]
11329mod kernel_poll_sync_rpc_tests {
11330    use super::{
11331        service_javascript_kernel_poll_sync_rpc, ActiveExecution, ActiveProcess,
11332        JavascriptSyncRpcRequest, KernelPollFdResponse, SidecarKernel, ToolExecution,
11333        EXECUTION_DRIVER_NAME, JAVASCRIPT_COMMAND,
11334    };
11335    use agentos_kernel::command_registry::CommandDriver;
11336    use agentos_kernel::kernel::{KernelVmConfig, SpawnOptions};
11337    use agentos_kernel::mount_table::MountTable;
11338    use agentos_kernel::permissions::Permissions;
11339    use agentos_kernel::poll::{POLLHUP, POLLIN};
11340    use agentos_kernel::vfs::MemoryFileSystem;
11341    use serde_json::{json, Value};
11342    use std::collections::HashMap;
11343    #[test]
11344    fn javascript_kernel_poll_sync_rpc_reports_multiple_kernel_fds() {
11345        let mut config = KernelVmConfig::new("vm-js-kernel-poll");
11346        config.permissions = Permissions::allow_all();
11347        let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config);
11348        kernel
11349            .register_driver(CommandDriver::new(
11350                EXECUTION_DRIVER_NAME,
11351                [JAVASCRIPT_COMMAND],
11352            ))
11353            .expect("register execution driver");
11354
11355        let kernel_handle = kernel
11356            .spawn_process(
11357                JAVASCRIPT_COMMAND,
11358                Vec::new(),
11359                SpawnOptions {
11360                    requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
11361                    ..SpawnOptions::default()
11362                },
11363            )
11364            .expect("spawn javascript kernel process");
11365        let pid = kernel_handle.pid();
11366
11367        let (stdin_read_fd, stdin_write_fd) = kernel
11368            .open_pipe(EXECUTION_DRIVER_NAME, pid)
11369            .expect("open kernel stdin pipe");
11370        kernel
11371            .fd_dup2(EXECUTION_DRIVER_NAME, pid, stdin_read_fd, 0)
11372            .expect("dup stdin pipe onto fd 0");
11373        kernel
11374            .fd_close(EXECUTION_DRIVER_NAME, pid, stdin_read_fd)
11375            .expect("close original stdin read fd");
11376
11377        let process = ActiveProcess::new(
11378            pid,
11379            kernel_handle,
11380            super::GuestRuntimeKind::JavaScript,
11381            ActiveExecution::Tool(ToolExecution::default()),
11382        );
11383
11384        kernel
11385            .fd_write(EXECUTION_DRIVER_NAME, pid, stdin_write_fd, b"poll-ready")
11386            .expect("write kernel stdin payload");
11387        kernel
11388            .fd_close(EXECUTION_DRIVER_NAME, pid, stdin_write_fd)
11389            .expect("close kernel stdin writer");
11390
11391        let response = service_javascript_kernel_poll_sync_rpc(
11392            &mut kernel,
11393            &process,
11394            &JavascriptSyncRpcRequest {
11395                id: 1,
11396                method: String::from("__kernel_poll"),
11397                raw_bytes_args: HashMap::new(),
11398                args: vec![
11399                    json!([
11400                        { "fd": 0, "events": POLLIN.bits() },
11401                        { "fd": 1, "events": POLLIN.bits() }
11402                    ]),
11403                    json!(250),
11404                ],
11405            },
11406        )
11407        .expect("poll kernel fds");
11408
11409        assert_eq!(response["readyCount"], Value::from(1));
11410        let fds: Vec<KernelPollFdResponse> =
11411            serde_json::from_value(response["fds"].clone()).expect("kernel poll fd response");
11412        assert_eq!(
11413            fds,
11414            vec![
11415                KernelPollFdResponse {
11416                    fd: 0,
11417                    events: POLLIN.bits(),
11418                    revents: (POLLIN | POLLHUP).bits(),
11419                },
11420                KernelPollFdResponse {
11421                    fd: 1,
11422                    events: POLLIN.bits(),
11423                    revents: 0,
11424                },
11425            ]
11426        );
11427
11428        process.kernel_handle.finish(0);
11429        kernel.waitpid(pid).expect("wait javascript kernel process");
11430    }
11431}
11432
11433fn dedupe_strings(values: &[String]) -> Vec<String> {
11434    let mut seen = BTreeSet::new();
11435    let mut deduped = Vec::new();
11436    for value in values {
11437        if seen.insert(value.clone()) {
11438            deduped.push(value.clone());
11439        }
11440    }
11441    deduped
11442}
11443
11444fn dedupe_host_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
11445    let mut seen = BTreeSet::new();
11446    let mut deduped = Vec::new();
11447    for path in paths {
11448        let normalized = normalize_host_path(path);
11449        let key = normalized.to_string_lossy().into_owned();
11450        if seen.insert(key) {
11451            deduped.push(normalized);
11452        }
11453    }
11454    deduped
11455}
11456
11457fn expand_host_access_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
11458    let mut expanded = Vec::new();
11459    let mut seen = BTreeSet::new();
11460
11461    let mut add_path = |candidate: PathBuf| {
11462        let normalized = normalize_host_path(&candidate);
11463        let key = normalized.to_string_lossy().into_owned();
11464        if seen.insert(key) {
11465            expanded.push(normalized);
11466        }
11467    };
11468
11469    for host_path in paths {
11470        add_path(host_path.clone());
11471        if let Ok(realpath) = fs::canonicalize(host_path) {
11472            add_path(realpath);
11473        }
11474
11475        if host_path.file_name().and_then(|name| name.to_str()) != Some("node_modules") {
11476            continue;
11477        }
11478
11479        let mut current = host_path.parent();
11480        while let Some(parent) = current {
11481            let candidate = parent.join("node_modules");
11482            if candidate.exists() {
11483                add_path(candidate.clone());
11484                if let Ok(realpath) = fs::canonicalize(&candidate) {
11485                    add_path(realpath);
11486                }
11487            }
11488            current = parent.parent();
11489        }
11490    }
11491
11492    expanded
11493}
11494
11495/// Package content is tar-mounted guest-native and never materialized on the
11496/// host, so command resolution classifies package-mount entrypoints by
11497/// extension only (`resolve_javascript_command_entrypoint`) and the resolved
11498/// host path for a WebAssembly module may not exist. Correct both here, where
11499/// the kernel is available: sniff the real entrypoint's magic through the
11500/// kernel VFS, flip misclassified extensionless WebAssembly binaries from
11501/// JavaScript to WebAssembly, and stage the module bytes into the VM shadow
11502/// tree so the wasm engine (which loads modules from a host path) can read
11503/// them. Staging is per-VM and write-once per resolved version path — package
11504/// versions are immutable — and only commands that actually execute are
11505/// materialized; filesystem reads stay on the zero-extraction tar mount.
11506fn stage_agentos_package_command(
11507    vm: &mut VmState,
11508    resolved: &mut ResolvedChildProcessExecution,
11509) -> Result<(), SidecarError> {
11510    const WASM_MAGIC: &[u8] = b"\0asm";
11511    if resolved.tool_command
11512        || !matches!(
11513            resolved.runtime,
11514            GuestRuntimeKind::JavaScript | GuestRuntimeKind::WebAssembly
11515        )
11516    {
11517        return Ok(());
11518    }
11519    let Some(guest_entrypoint) = resolved
11520        .env
11521        .get("AGENTOS_GUEST_ENTRYPOINT")
11522        .filter(|path| path.starts_with('/'))
11523        .map(|path| normalize_path(path))
11524    else {
11525        return Ok(());
11526    };
11527    if !guest_path_is_within_agentos_package_mount(vm, &guest_entrypoint) {
11528        return Ok(());
11529    }
11530    let Ok(real_entrypoint) = vm.kernel.realpath(&guest_entrypoint) else {
11531        return Ok(());
11532    };
11533    let real_entrypoint = normalize_path(&real_entrypoint);
11534    let Ok(magic) = vm.kernel.pread_file(&real_entrypoint, 0, WASM_MAGIC.len()) else {
11535        return Ok(());
11536    };
11537    if magic != WASM_MAGIC {
11538        return Ok(());
11539    }
11540    let shadow_path = shadow_path_for_guest(vm, &real_entrypoint);
11541    if !shadow_path.is_file() {
11542        let bytes = vm.kernel.read_file(&real_entrypoint).map_err(kernel_error)?;
11543        if let Some(parent) = shadow_path.parent() {
11544            fs::create_dir_all(parent).map_err(|error| {
11545                SidecarError::Io(format!("failed to create wasm shadow parent: {error}"))
11546            })?;
11547        }
11548        fs::write(&shadow_path, &bytes).map_err(|error| {
11549            SidecarError::Io(format!(
11550                "failed to stage wasm module {}: {error}",
11551                shadow_path.display()
11552            ))
11553        })?;
11554    }
11555    resolved.runtime = GuestRuntimeKind::WebAssembly;
11556    resolved.entrypoint = shadow_path.to_string_lossy().into_owned();
11557    Ok(())
11558}
11559
11560fn prepare_javascript_shadow(
11561    vm: &mut VmState,
11562    resolved: &ResolvedChildProcessExecution,
11563    env: &BTreeMap<String, String>,
11564) -> Result<(), SidecarError> {
11565    let guest_entrypoint = env
11566        .get("AGENTOS_GUEST_ENTRYPOINT")
11567        .cloned()
11568        // An absolute `entrypoint` may be a host path that lives inside the VM's
11569        // host cwd (callers can pass a fully-qualified host path). The guest sees
11570        // it at its translated guest path (host_cwd -> guest_cwd), so the shadow
11571        // must be keyed by that guest path rather than the raw host path. Falling
11572        // back to the host path here would materialize the file at the wrong guest
11573        // location and the runtime's `require()` would fail with "Cannot find
11574        // module".
11575        .or_else(|| {
11576            resolve_host_entrypoint_within_vm_host_cwd(vm, &resolved.entrypoint)
11577                .map(|(guest_entrypoint, _)| guest_entrypoint)
11578        })
11579        .or_else(|| {
11580            resolved
11581                .entrypoint
11582                .starts_with('/')
11583                .then(|| normalize_path(&resolved.entrypoint))
11584        });
11585    let Some(guest_entrypoint) = guest_entrypoint else {
11586        return Ok(());
11587    };
11588    if host_mount_path_for_guest_path(vm, &guest_entrypoint).is_some() {
11589        return Ok(());
11590    }
11591    if vm.kernel.lstat(&guest_entrypoint).is_err() {
11592        let host_entrypoint = {
11593            let candidate = Path::new(&resolved.entrypoint);
11594            if candidate.is_absolute() {
11595                candidate.to_path_buf()
11596            } else {
11597                resolved.host_cwd.join(candidate)
11598            }
11599        };
11600        if host_entrypoint.exists() {
11601            materialize_host_path_to_shadow(vm, &guest_entrypoint, &host_entrypoint)?;
11602            // The shadow write only stages the file on the host side; the runtime
11603            // resolves modules against the kernel VFS, so the staged entrypoint
11604            // must be synced into the kernel before execution starts (otherwise
11605            // `require()` reports "Cannot find module").
11606            return sync_shadow_entrypoint_into_kernel(vm, &guest_entrypoint);
11607        }
11608    }
11609    materialize_guest_path_to_shadow(vm, &guest_entrypoint)
11610}
11611
11612fn resolve_agentos_package_javascript_launch_entrypoint(
11613    vm: &mut VmState,
11614    env: &mut BTreeMap<String, String>,
11615) -> Option<String> {
11616    let guest_entrypoint = env
11617        .get("AGENTOS_GUEST_ENTRYPOINT")
11618        .filter(|path| path.starts_with('/'))
11619        .map(|path| normalize_path(path))?;
11620    if !guest_path_is_within_agentos_package_mount(vm, &guest_entrypoint) {
11621        return None;
11622    }
11623
11624    let real_entrypoint = normalize_path(&vm.kernel.realpath(&guest_entrypoint).ok()?);
11625    if !guest_path_is_within_agentos_package_mount(vm, &real_entrypoint) {
11626        return None;
11627    }
11628
11629    env.insert(
11630        String::from("AGENTOS_GUEST_ENTRYPOINT"),
11631        real_entrypoint.clone(),
11632    );
11633    if guest_javascript_entrypoint_uses_module_mode(vm, &real_entrypoint) {
11634        env.insert(
11635            String::from("AGENTOS_GUEST_ENTRYPOINT_MODULE_MODE"),
11636            String::from("1"),
11637        );
11638    } else {
11639        env.remove("AGENTOS_GUEST_ENTRYPOINT_MODULE_MODE");
11640    }
11641    Some(real_entrypoint)
11642}
11643
11644fn guest_path_is_within_agentos_package_mount(vm: &VmState, guest_path: &str) -> bool {
11645    let normalized = normalize_path(guest_path);
11646    vm.configuration.mounts.iter().any(|mount| {
11647        mount.plugin.id == "agentos_packages" && {
11648            let guest_root = normalize_path(&mount.guest_path);
11649            normalized == guest_root || normalized.starts_with(&format!("{guest_root}/"))
11650        }
11651    })
11652}
11653
11654fn guest_javascript_entrypoint_uses_module_mode(vm: &mut VmState, guest_path: &str) -> bool {
11655    match Path::new(guest_path)
11656        .extension()
11657        .and_then(|ext| ext.to_str())
11658    {
11659        Some("mjs" | "mts") => true,
11660        Some("js") => nearest_guest_package_json_type(vm, guest_path).as_deref() == Some("module"),
11661        _ => false,
11662    }
11663}
11664
11665fn nearest_guest_package_json_type(vm: &mut VmState, guest_path: &str) -> Option<String> {
11666    let mut dir = dirname(guest_path);
11667    loop {
11668        let package_json_path = if dir == "/" {
11669            String::from("/package.json")
11670        } else {
11671            normalize_path(&format!("{dir}/package.json"))
11672        };
11673        if let Ok(bytes) = vm.kernel.read_file(&package_json_path) {
11674            if let Ok(value) = serde_json::from_slice::<Value>(&bytes) {
11675                if let Some(package_type) = value.get("type").and_then(Value::as_str) {
11676                    return Some(package_type.to_owned());
11677                }
11678            }
11679        }
11680        if dir == "/" {
11681            return None;
11682        }
11683        dir = dirname(&dir);
11684    }
11685}
11686
11687/// Sync a freshly-staged shadow entrypoint into the kernel VFS so the runtime's
11688/// kernel-backed module resolver can read it. Mirrors the host->kernel file sync
11689/// used by the broader shadow reconciliation, but scoped to the single
11690/// entrypoint we just materialized.
11691fn sync_shadow_entrypoint_into_kernel(
11692    vm: &mut VmState,
11693    guest_entrypoint: &str,
11694) -> Result<(), SidecarError> {
11695    if vm.kernel.exists(guest_entrypoint).unwrap_or(false) {
11696        return Ok(());
11697    }
11698    let shadow_path = shadow_path_for_guest(vm, guest_entrypoint);
11699    let bytes = match fs::read(&shadow_path) {
11700        Ok(bytes) => bytes,
11701        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
11702        Err(error) => {
11703            return Err(SidecarError::Io(format!(
11704                "failed to read staged shadow entrypoint {}: {error}",
11705                shadow_path.display()
11706            )));
11707        }
11708    };
11709    if let Some(parent) = guest_parent_path(guest_entrypoint) {
11710        if !vm.kernel.exists(&parent).unwrap_or(false) {
11711            vm.kernel.mkdir(&parent, true).map_err(kernel_error)?;
11712        }
11713    }
11714    vm.kernel
11715        .write_file(guest_entrypoint, bytes)
11716        .map_err(kernel_error)?;
11717    Ok(())
11718}
11719
11720fn guest_parent_path(guest_path: &str) -> Option<String> {
11721    let parent = Path::new(guest_path).parent()?;
11722    let parent = parent.to_string_lossy();
11723    if parent.is_empty() || parent == "/" {
11724        None
11725    } else {
11726        Some(parent.into_owned())
11727    }
11728}
11729
11730fn materialize_host_path_to_shadow(
11731    vm: &VmState,
11732    guest_path: &str,
11733    host_path: &Path,
11734) -> Result<(), SidecarError> {
11735    let shadow_path = shadow_path_for_guest(vm, guest_path);
11736    let metadata = fs::symlink_metadata(host_path)
11737        .map_err(|error| SidecarError::Io(format!("failed to stat host entrypoint: {error}")))?;
11738
11739    if metadata.file_type().is_symlink() {
11740        if let Some(parent) = shadow_path.parent() {
11741            fs::create_dir_all(parent).map_err(|error| {
11742                SidecarError::Io(format!("failed to create shadow symlink parent: {error}"))
11743            })?;
11744        }
11745        let _ = fs::remove_file(&shadow_path);
11746        let _ = fs::remove_dir_all(&shadow_path);
11747        let target = fs::read_link(host_path)
11748            .map_err(|error| SidecarError::Io(format!("failed to read host symlink: {error}")))?;
11749        std::os::unix::fs::symlink(&target, &shadow_path)
11750            .map_err(|error| SidecarError::Io(format!("failed to mirror host symlink: {error}")))?;
11751        return Ok(());
11752    }
11753
11754    if metadata.is_dir() {
11755        fs::create_dir_all(&shadow_path).map_err(|error| {
11756            SidecarError::Io(format!("failed to create shadow directory: {error}"))
11757        })?;
11758        fs::set_permissions(
11759            &shadow_path,
11760            fs::Permissions::from_mode(metadata.permissions().mode() & 0o7777),
11761        )
11762        .map_err(|error| {
11763            SidecarError::Io(format!(
11764                "failed to set shadow directory mode on {}: {error}",
11765                shadow_path.display()
11766            ))
11767        })?;
11768        return Ok(());
11769    }
11770
11771    if let Some(parent) = shadow_path.parent() {
11772        fs::create_dir_all(parent).map_err(|error| {
11773            SidecarError::Io(format!("failed to create shadow parent: {error}"))
11774        })?;
11775    }
11776    let bytes = fs::read(host_path)
11777        .map_err(|error| SidecarError::Io(format!("failed to read host entrypoint: {error}")))?;
11778    fs::write(&shadow_path, bytes).map_err(|error| {
11779        SidecarError::Io(format!(
11780            "failed to mirror host file into shadow root: {error}"
11781        ))
11782    })?;
11783    fs::set_permissions(
11784        &shadow_path,
11785        fs::Permissions::from_mode(metadata.permissions().mode() & 0o7777),
11786    )
11787    .map_err(|error| {
11788        SidecarError::Io(format!(
11789            "failed to set shadow file mode on {}: {error}",
11790            shadow_path.display()
11791        ))
11792    })?;
11793    Ok(())
11794}
11795
11796fn materialize_guest_path_to_shadow(
11797    vm: &mut VmState,
11798    guest_path: &str,
11799) -> Result<(), SidecarError> {
11800    let stat = vm.kernel.lstat(guest_path).map_err(kernel_error)?;
11801    let shadow_path = shadow_path_for_guest(vm, guest_path);
11802
11803    if stat.is_symbolic_link {
11804        if let Some(parent) = shadow_path.parent() {
11805            fs::create_dir_all(parent).map_err(|error| {
11806                SidecarError::Io(format!("failed to create shadow symlink parent: {error}"))
11807            })?;
11808        }
11809        let _ = fs::remove_file(&shadow_path);
11810        let _ = fs::remove_dir_all(&shadow_path);
11811        let target = vm.kernel.read_link(guest_path).map_err(kernel_error)?;
11812        std::os::unix::fs::symlink(&target, &shadow_path)
11813            .map_err(|error| SidecarError::Io(format!("failed to mirror symlink: {error}")))?;
11814        return Ok(());
11815    }
11816
11817    if stat.is_directory {
11818        fs::create_dir_all(&shadow_path).map_err(|error| {
11819            SidecarError::Io(format!("failed to create shadow directory: {error}"))
11820        })?;
11821        fs::set_permissions(&shadow_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err(
11822            |error| {
11823                SidecarError::Io(format!(
11824                    "failed to set shadow directory mode on {}: {error}",
11825                    shadow_path.display()
11826                ))
11827            },
11828        )?;
11829        return Ok(());
11830    }
11831
11832    if let Some(parent) = shadow_path.parent() {
11833        fs::create_dir_all(parent).map_err(|error| {
11834            SidecarError::Io(format!("failed to create shadow parent: {error}"))
11835        })?;
11836    }
11837    let bytes = vm.kernel.read_file(guest_path).map_err(kernel_error)?;
11838    fs::write(&shadow_path, bytes).map_err(|error| {
11839        SidecarError::Io(format!(
11840            "failed to mirror guest file into shadow root: {error}"
11841        ))
11842    })?;
11843    fs::set_permissions(&shadow_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err(
11844        |error| {
11845            SidecarError::Io(format!(
11846                "failed to set shadow file mode on {}: {error}",
11847                shadow_path.display()
11848            ))
11849        },
11850    )?;
11851    Ok(())
11852}
11853
11854fn load_javascript_entrypoint_source(
11855    vm: &mut VmState,
11856    host_cwd: &Path,
11857    entrypoint: &str,
11858    env: &BTreeMap<String, String>,
11859) -> Option<String> {
11860    let mut read_guest_file = |path: &str| {
11861        vm.kernel
11862            .read_file(path)
11863            .ok()
11864            .and_then(|bytes| String::from_utf8(bytes).ok())
11865    };
11866
11867    if let Some(source) = env
11868        .get("AGENTOS_GUEST_ENTRYPOINT")
11869        .filter(|path| path.starts_with('/'))
11870        .and_then(|path| read_guest_file(path))
11871    {
11872        return Some(source);
11873    }
11874
11875    if entrypoint.starts_with('/') {
11876        if let Some(source) = read_guest_file(entrypoint) {
11877            return Some(source);
11878        }
11879    }
11880
11881    let host_entrypoint = if Path::new(entrypoint).is_absolute() {
11882        PathBuf::from(entrypoint)
11883    } else {
11884        host_cwd.join(entrypoint)
11885    };
11886    let normalized_entrypoint = normalize_host_path(&host_entrypoint);
11887    let sandbox_root = normalize_host_path(&vm.cwd);
11888    let host_cwd = normalize_host_path(&vm.host_cwd);
11889    if !path_is_within_root(&normalized_entrypoint, &sandbox_root)
11890        && !path_is_within_root(&normalized_entrypoint, &host_cwd)
11891    {
11892        return None;
11893    }
11894
11895    fs::read_to_string(&normalized_entrypoint).ok()
11896}
11897
11898fn emit_dns_resolution_event<B>(
11899    bridge: &SharedBridge<B>,
11900    vm_id: &str,
11901    hostname: &str,
11902    source: KernelDnsResolutionSource,
11903    addresses: &[IpAddr],
11904    dns: &VmDnsConfig,
11905) where
11906    B: NativeSidecarBridge + Send + 'static,
11907    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11908{
11909    let _ = emit_structured_event(
11910        bridge,
11911        vm_id,
11912        "network.dns.resolved",
11913        audit_fields([
11914            ("hostname", hostname.to_owned()),
11915            ("source", source.as_str().to_owned()),
11916            (
11917                "addresses",
11918                addresses
11919                    .iter()
11920                    .map(ToString::to_string)
11921                    .collect::<Vec<_>>()
11922                    .join(","),
11923            ),
11924            ("address_count", addresses.len().to_string()),
11925            ("resolver_count", dns.name_servers.len().to_string()),
11926            (
11927                "resolvers",
11928                dns.name_servers
11929                    .iter()
11930                    .map(ToString::to_string)
11931                    .collect::<Vec<_>>()
11932                    .join(","),
11933            ),
11934        ]),
11935    );
11936}
11937
11938fn emit_dns_record_resolution_event<B>(
11939    bridge: &SharedBridge<B>,
11940    vm_id: &str,
11941    hostname: &str,
11942    resolution: &DnsRecordResolution,
11943    dns: &VmDnsConfig,
11944) where
11945    B: NativeSidecarBridge + Send + 'static,
11946    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11947{
11948    if let Some(addresses) = dns_resolution_ip_addrs(resolution.records()) {
11949        emit_dns_resolution_event(
11950            bridge,
11951            vm_id,
11952            hostname,
11953            resolution.source(),
11954            &addresses,
11955            dns,
11956        );
11957        return;
11958    }
11959
11960    let _ = emit_structured_event(
11961        bridge,
11962        vm_id,
11963        "network.dns.resolved",
11964        audit_fields([
11965            ("hostname", hostname.to_owned()),
11966            ("source", resolution.source().as_str().to_owned()),
11967            (
11968                "addresses",
11969                resolution
11970                    .records()
11971                    .iter()
11972                    .map(summarize_dns_record)
11973                    .collect::<Vec<_>>()
11974                    .join(","),
11975            ),
11976            ("address_count", resolution.records().len().to_string()),
11977            ("resolver_count", dns.name_servers.len().to_string()),
11978            (
11979                "resolvers",
11980                dns.name_servers
11981                    .iter()
11982                    .map(ToString::to_string)
11983                    .collect::<Vec<_>>()
11984                    .join(","),
11985            ),
11986        ]),
11987    );
11988}
11989
11990fn emit_dns_resolution_failure_event<B>(
11991    bridge: &SharedBridge<B>,
11992    vm_id: &str,
11993    hostname: &str,
11994    dns: &VmDnsConfig,
11995    error: &SidecarError,
11996) where
11997    B: NativeSidecarBridge + Send + 'static,
11998    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11999{
12000    let _ = emit_structured_event(
12001        bridge,
12002        vm_id,
12003        "network.dns.resolve_failed",
12004        audit_fields([
12005            ("hostname", hostname.to_owned()),
12006            ("reason", error.to_string()),
12007            ("resolver_count", dns.name_servers.len().to_string()),
12008            (
12009                "resolvers",
12010                dns.name_servers
12011                    .iter()
12012                    .map(ToString::to_string)
12013                    .collect::<Vec<_>>()
12014                    .join(","),
12015            ),
12016        ]),
12017    );
12018}
12019
12020fn parse_dns_record_type(rrtype: &str) -> Result<RecordType, SidecarError> {
12021    match rrtype {
12022        "A" => Ok(RecordType::A),
12023        "AAAA" => Ok(RecordType::AAAA),
12024        "MX" => Ok(RecordType::MX),
12025        "TXT" => Ok(RecordType::TXT),
12026        "SRV" => Ok(RecordType::SRV),
12027        "CNAME" => Ok(RecordType::CNAME),
12028        "PTR" => Ok(RecordType::PTR),
12029        "NS" => Ok(RecordType::NS),
12030        "SOA" => Ok(RecordType::SOA),
12031        "NAPTR" => Ok(RecordType::NAPTR),
12032        "CAA" => Ok(RecordType::CAA),
12033        "ANY" => Ok(RecordType::ANY),
12034        other => Err(SidecarError::Execution(format!(
12035            "ERR_NOT_IMPLEMENTED: dns rrtype {other} is not supported by the secure-exec dns bridge"
12036        ))),
12037    }
12038}
12039
12040fn dns_resolution_to_node_value(
12041    resolution: &DnsRecordResolution,
12042    requested_type: &str,
12043) -> Result<Value, SidecarError> {
12044    let safe_ips = dns_resolution_safe_ip_set(resolution.records(), resolution.hostname())?;
12045    match requested_type {
12046        "A" | "AAAA" => Ok(Value::Array(
12047            resolution
12048                .records()
12049                .iter()
12050                .filter_map(|record| dns_record_ip_string(record, &safe_ips))
12051                .map(Value::String)
12052                .collect(),
12053        )),
12054        "MX" => Ok(Value::Array(
12055            resolution
12056                .records()
12057                .iter()
12058                .filter_map(|record| match record.data() {
12059                    RData::MX(mx) => Some(json!({
12060                        "priority": mx.preference,
12061                        "exchange": normalize_dns_name_for_node(&mx.exchange),
12062                        "type": "MX",
12063                    })),
12064                    _ => None,
12065                })
12066                .collect(),
12067        )),
12068        "TXT" => Ok(Value::Array(
12069            resolution
12070                .records()
12071                .iter()
12072                .filter_map(|record| match record.data() {
12073                    RData::TXT(txt) => Some(Value::Array(
12074                        txt.txt_data
12075                            .iter()
12076                            .map(|entry| Value::String(String::from_utf8_lossy(entry).into_owned()))
12077                            .collect(),
12078                    )),
12079                    _ => None,
12080                })
12081                .collect(),
12082        )),
12083        "SRV" => Ok(Value::Array(
12084            resolution
12085                .records()
12086                .iter()
12087                .filter_map(|record| match record.data() {
12088                    RData::SRV(srv) => Some(json!({
12089                        "priority": srv.priority,
12090                        "weight": srv.weight,
12091                        "port": srv.port,
12092                        "name": normalize_dns_name_for_node(&srv.target),
12093                        "type": "SRV",
12094                    })),
12095                    _ => None,
12096                })
12097                .collect(),
12098        )),
12099        "CNAME" => Ok(Value::Array(
12100            resolution
12101                .records()
12102                .iter()
12103                .filter_map(|record| match record.data() {
12104                    RData::CNAME(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
12105                    _ => None,
12106                })
12107                .collect(),
12108        )),
12109        "PTR" => Ok(Value::Array(
12110            resolution
12111                .records()
12112                .iter()
12113                .filter_map(|record| match record.data() {
12114                    RData::PTR(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
12115                    _ => None,
12116                })
12117                .collect(),
12118        )),
12119        "NS" => Ok(Value::Array(
12120            resolution
12121                .records()
12122                .iter()
12123                .filter_map(|record| match record.data() {
12124                    RData::NS(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
12125                    _ => None,
12126                })
12127                .collect(),
12128        )),
12129        "SOA" => resolution
12130            .records()
12131            .iter()
12132            .find_map(|record| match record.data() {
12133                RData::SOA(soa) => Some(json!({
12134                    "nsname": normalize_dns_name_for_node(&soa.mname),
12135                    "hostmaster": normalize_dns_name_for_node(&soa.rname),
12136                    "serial": soa.serial,
12137                    "refresh": soa.refresh,
12138                    "retry": soa.retry,
12139                    "expire": soa.expire,
12140                    "minttl": soa.minimum,
12141                })),
12142                _ => None,
12143            })
12144            .ok_or_else(|| {
12145                SidecarError::Execution(String::from("failed to resolve DNS SOA record"))
12146            }),
12147        "NAPTR" => Ok(Value::Array(
12148            resolution
12149                .records()
12150                .iter()
12151                .filter_map(|record| match record.data() {
12152                    RData::NAPTR(naptr) => Some(json!({
12153                        "flags": String::from_utf8_lossy(&naptr.flags).into_owned(),
12154                        "service": String::from_utf8_lossy(&naptr.services).into_owned(),
12155                        "regexp": String::from_utf8_lossy(&naptr.regexp).into_owned(),
12156                        "replacement": normalize_dns_name_for_node(&naptr.replacement),
12157                        "order": naptr.order,
12158                        "preference": naptr.preference,
12159                    })),
12160                    _ => None,
12161                })
12162                .collect(),
12163        )),
12164        "CAA" => Ok(Value::Array(
12165            resolution
12166                .records()
12167                .iter()
12168                .filter_map(|record| match record.data() {
12169                    RData::CAA(caa) => {
12170                        let mut value = serde_json::Map::new();
12171                        value.insert(
12172                            "critical".to_owned(),
12173                            Value::from(u8::from(caa.issuer_critical)),
12174                        );
12175                        value.insert("type".to_owned(), Value::String(String::from("CAA")));
12176                        if caa.tag.eq_ignore_ascii_case("iodef") {
12177                            value.insert(
12178                                "iodef".to_owned(),
12179                                Value::String(
12180                                    caa.value_as_iodef()
12181                                        .map(|url| url.to_string())
12182                                        .unwrap_or_else(|_| {
12183                                            String::from_utf8_lossy(&caa.value).into_owned()
12184                                        }),
12185                                ),
12186                            );
12187                        } else if let Ok((issuer, _params)) = caa.value_as_issue() {
12188                            let field = if caa.tag.eq_ignore_ascii_case("issuewild") {
12189                                "issuewild"
12190                            } else {
12191                                "issue"
12192                            };
12193                            value.insert(
12194                                field.to_owned(),
12195                                Value::String(
12196                                    issuer.as_ref().map(ToString::to_string).unwrap_or_else(|| {
12197                                        String::from_utf8_lossy(&caa.value).into_owned()
12198                                    }),
12199                                ),
12200                            );
12201                        } else {
12202                            value.insert(
12203                                caa.tag.to_ascii_lowercase(),
12204                                Value::String(String::from_utf8_lossy(&caa.value).into_owned()),
12205                            );
12206                        }
12207                        Some(Value::Object(value))
12208                    }
12209                    _ => None,
12210                })
12211                .collect(),
12212        )),
12213        "ANY" => Ok(Value::Array(
12214            resolution
12215                .records()
12216                .iter()
12217                .filter_map(|record| dns_any_record_to_value(record, &safe_ips))
12218                .collect(),
12219        )),
12220        other => Err(SidecarError::Execution(format!(
12221            "ERR_NOT_IMPLEMENTED: dns rrtype {other} is not supported by the secure-exec dns bridge"
12222        ))),
12223    }
12224}
12225
12226fn dns_resolution_safe_ip_set(
12227    records: &[Record],
12228    hostname: &str,
12229) -> Result<BTreeSet<IpAddr>, SidecarError> {
12230    let ips = records
12231        .iter()
12232        .filter_map(dns_record_ip_addr)
12233        .collect::<Vec<_>>();
12234    if ips.is_empty() {
12235        return Ok(BTreeSet::new());
12236    }
12237    Ok(filter_dns_safe_ip_addrs(ips, hostname)?
12238        .into_iter()
12239        .collect())
12240}
12241
12242fn dns_resolution_ip_addrs(records: &[Record]) -> Option<Vec<IpAddr>> {
12243    let ips = records
12244        .iter()
12245        .filter_map(dns_record_ip_addr)
12246        .collect::<Vec<_>>();
12247    if ips.is_empty() {
12248        return None;
12249    }
12250    Some(ips)
12251}
12252
12253fn dns_record_ip_addr(record: &Record) -> Option<IpAddr> {
12254    match record.data() {
12255        RData::A(address) => Some(IpAddr::V4(**address)),
12256        RData::AAAA(address) => Some(IpAddr::V6(**address)),
12257        _ => None,
12258    }
12259}
12260
12261fn dns_record_ip_string(record: &Record, safe_ips: &BTreeSet<IpAddr>) -> Option<String> {
12262    let ip = dns_record_ip_addr(record)?;
12263    safe_ips.contains(&ip).then(|| ip.to_string())
12264}
12265
12266fn dns_any_record_to_value(record: &Record, safe_ips: &BTreeSet<IpAddr>) -> Option<Value> {
12267    let value = match record.data() {
12268        RData::A(_) | RData::AAAA(_) => json!({
12269            "address": dns_record_ip_string(record, safe_ips)?,
12270            "ttl": record.ttl(),
12271            "type": record.record_type().to_string(),
12272        }),
12273        RData::MX(mx) => json!({
12274            "exchange": normalize_dns_name_for_node(&mx.exchange),
12275            "priority": mx.preference,
12276            "type": "MX",
12277        }),
12278        RData::TXT(txt) => json!({
12279            "entries": txt
12280                .txt_data
12281                .iter()
12282                .map(|entry| String::from_utf8_lossy(entry).into_owned())
12283                .collect::<Vec<_>>(),
12284            "type": "TXT",
12285        }),
12286        RData::SRV(srv) => json!({
12287            "name": normalize_dns_name_for_node(&srv.target),
12288            "port": srv.port,
12289            "priority": srv.priority,
12290            "weight": srv.weight,
12291            "type": "SRV",
12292        }),
12293        RData::CNAME(name) => json!({
12294            "value": normalize_dns_name_for_node(&name.0),
12295            "type": "CNAME",
12296        }),
12297        RData::PTR(name) => json!({
12298            "value": normalize_dns_name_for_node(&name.0),
12299            "type": "PTR",
12300        }),
12301        RData::NS(name) => json!({
12302            "value": normalize_dns_name_for_node(&name.0),
12303            "type": "NS",
12304        }),
12305        RData::SOA(soa) => json!({
12306            "nsname": normalize_dns_name_for_node(&soa.mname),
12307            "hostmaster": normalize_dns_name_for_node(&soa.rname),
12308            "serial": soa.serial,
12309            "refresh": soa.refresh,
12310            "retry": soa.retry,
12311            "expire": soa.expire,
12312            "minttl": soa.minimum,
12313            "type": "SOA",
12314        }),
12315        RData::NAPTR(naptr) => json!({
12316            "flags": String::from_utf8_lossy(&naptr.flags).into_owned(),
12317            "service": String::from_utf8_lossy(&naptr.services).into_owned(),
12318            "regexp": String::from_utf8_lossy(&naptr.regexp).into_owned(),
12319            "replacement": normalize_dns_name_for_node(&naptr.replacement),
12320            "order": naptr.order,
12321            "preference": naptr.preference,
12322            "type": "NAPTR",
12323        }),
12324        RData::CAA(caa) => {
12325            let mut value = serde_json::Map::new();
12326            value.insert(
12327                "critical".to_owned(),
12328                Value::from(u8::from(caa.issuer_critical)),
12329            );
12330            value.insert("type".to_owned(), Value::String(String::from("CAA")));
12331            if caa.tag.eq_ignore_ascii_case("iodef") {
12332                value.insert(
12333                    "iodef".to_owned(),
12334                    Value::String(
12335                        caa.value_as_iodef()
12336                            .map(|url| url.to_string())
12337                            .unwrap_or_else(|_| String::from_utf8_lossy(&caa.value).into_owned()),
12338                    ),
12339                );
12340            } else if let Ok((issuer, _params)) = caa.value_as_issue() {
12341                let field = if caa.tag.eq_ignore_ascii_case("issuewild") {
12342                    "issuewild"
12343                } else {
12344                    "issue"
12345                };
12346                value.insert(
12347                    field.to_owned(),
12348                    Value::String(
12349                        issuer
12350                            .as_ref()
12351                            .map(ToString::to_string)
12352                            .unwrap_or_else(|| String::from_utf8_lossy(&caa.value).into_owned()),
12353                    ),
12354                );
12355            }
12356            Value::Object(value)
12357        }
12358        _ => return None,
12359    };
12360    Some(value)
12361}
12362
12363fn normalize_dns_name_for_node(name: &impl ToString) -> String {
12364    name.to_string().trim_end_matches('.').to_owned()
12365}
12366
12367fn summarize_dns_record(record: &Record) -> String {
12368    match record.data() {
12369        RData::A(_) | RData::AAAA(_) => record.data().to_string(),
12370        _ => format!("{} {}", record.record_type(), record.data()),
12371    }
12372}
12373
12374// build_root_filesystem, convert_root_lower_descriptor, convert_root_filesystem_entry,
12375// root_snapshot_entry moved to crate::bootstrap
12376
12377// apply_root_filesystem_entry, ensure_parent_directories moved to crate::bootstrap
12378
12379// ProcNetEntry moved to crate::state
12380
12381fn find_socket_state_entry(
12382    vm: Option<&VmState>,
12383    kind: SocketQueryKind,
12384    request: &FindListenerRequest,
12385) -> Result<Option<SocketStateEntry>, SidecarError> {
12386    let vm = vm.ok_or_else(|| SidecarError::InvalidState(String::from("unknown sidecar VM")))?;
12387
12388    for (process_id, process) in &vm.active_processes {
12389        if let Some(path) = request.path.as_deref() {
12390            if matches!(kind, SocketQueryKind::TcpListener) {
12391                for listener in process.unix_listeners.values() {
12392                    if listener.path() != path {
12393                        continue;
12394                    }
12395                    return Ok(Some(SocketStateEntry {
12396                        process_id: process_id.to_owned(),
12397                        host: None,
12398                        port: None,
12399                        path: Some(path.to_owned()),
12400                    }));
12401                }
12402            }
12403        }
12404
12405        if request.path.is_none() {
12406            if let Some(entry) =
12407                find_kernel_socket_state_entry(&vm.kernel, process_id, process, kind, request)?
12408            {
12409                return Ok(Some(entry));
12410            }
12411
12412            match kind {
12413                SocketQueryKind::TcpListener => {
12414                    for server in process.http_servers.values() {
12415                        let local_addr = server.guest_local_addr;
12416                        let local_host = local_addr.ip().to_string();
12417                        if !socket_host_matches(request.host.as_deref(), &local_host) {
12418                            continue;
12419                        }
12420                        if let Some(port) = request.port {
12421                            if local_addr.port() != port {
12422                                continue;
12423                            }
12424                        }
12425                        return Ok(Some(SocketStateEntry {
12426                            process_id: process_id.to_owned(),
12427                            host: Some(local_host),
12428                            port: Some(local_addr.port()),
12429                            path: None,
12430                        }));
12431                    }
12432
12433                    for listener in process.tcp_listeners.values() {
12434                        if listener.kernel_socket_id.is_some() {
12435                            continue;
12436                        }
12437                        let local_addr = listener.guest_local_addr();
12438                        let local_host = local_addr.ip().to_string();
12439                        if !socket_host_matches(request.host.as_deref(), &local_host) {
12440                            continue;
12441                        }
12442                        if let Some(port) = request.port {
12443                            if local_addr.port() != port {
12444                                continue;
12445                            }
12446                        }
12447                        return Ok(Some(SocketStateEntry {
12448                            process_id: process_id.to_owned(),
12449                            host: Some(local_host),
12450                            port: Some(local_addr.port()),
12451                            path: None,
12452                        }));
12453                    }
12454                }
12455                SocketQueryKind::UdpBound => {
12456                    for socket in process.udp_sockets.values() {
12457                        if socket.kernel_socket_id.is_some() {
12458                            continue;
12459                        }
12460                        let Some(local_addr) = socket.local_addr() else {
12461                            continue;
12462                        };
12463                        let local_host = local_addr.ip().to_string();
12464                        if !socket_host_matches(request.host.as_deref(), &local_host) {
12465                            continue;
12466                        }
12467                        if let Some(port) = request.port {
12468                            if local_addr.port() != port {
12469                                continue;
12470                            }
12471                        }
12472                        return Ok(Some(SocketStateEntry {
12473                            process_id: process_id.to_owned(),
12474                            host: Some(local_host),
12475                            port: Some(local_addr.port()),
12476                            path: None,
12477                        }));
12478                    }
12479                }
12480            }
12481        }
12482
12483        let child_pid = process.execution.child_pid();
12484        let inodes = socket_inodes_for_pid(child_pid)?;
12485        if inodes.is_empty() {
12486            continue;
12487        }
12488
12489        if let Some(path) = request.path.as_deref() {
12490            if let Some(listener) = find_unix_socket_for_pid(child_pid, &inodes, path, process_id)?
12491            {
12492                return Ok(Some(listener));
12493            }
12494            continue;
12495        }
12496
12497        let table_paths = match kind {
12498            SocketQueryKind::TcpListener => [
12499                format!("/proc/{child_pid}/net/tcp"),
12500                format!("/proc/{child_pid}/net/tcp6"),
12501            ],
12502            SocketQueryKind::UdpBound => [
12503                format!("/proc/{child_pid}/net/udp"),
12504                format!("/proc/{child_pid}/net/udp6"),
12505            ],
12506        };
12507        for table_path in table_paths {
12508            if let Some(entry) = find_inet_socket_for_pid(
12509                &table_path,
12510                &inodes,
12511                kind,
12512                request.host.as_deref(),
12513                request.port,
12514                process_id,
12515            )? {
12516                return Ok(Some(entry));
12517            }
12518        }
12519    }
12520
12521    Ok(None)
12522}
12523
12524fn require_vm_inspection_permission<B>(
12525    bridge: &SharedBridge<B>,
12526    vm_id: &str,
12527    capability: &str,
12528    domain: &str,
12529    resource: &str,
12530) -> Result<(), SidecarError>
12531where
12532    B: NativeSidecarBridge + Send + 'static,
12533    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
12534{
12535    let decision = bridge.static_permission_decision(vm_id, capability, domain, Some(resource));
12536    if decision.as_ref().is_some_and(|decision| decision.allow) {
12537        return Ok(());
12538    }
12539
12540    let reason = decision
12541        .and_then(|decision| decision.reason)
12542        .unwrap_or_else(|| format!("{capability} permission required"));
12543    Err(SidecarError::Execution(format!(
12544        "EACCES: permission denied, {resource}: {reason}"
12545    )))
12546}
12547
12548fn socket_query_resource(kind: SocketQueryKind, request: &FindListenerRequest) -> String {
12549    if let Some(path) = request.path.as_deref() {
12550        return format!("unix://{path}");
12551    }
12552
12553    let host = request.host.as_deref().unwrap_or("*");
12554    let port = request
12555        .port
12556        .map_or_else(|| String::from("*"), |port| port.to_string());
12557    match kind {
12558        SocketQueryKind::TcpListener => format!("tcp://{host}:{port}"),
12559        SocketQueryKind::UdpBound => format!("udp://{host}:{port}"),
12560    }
12561}
12562
12563fn snapshot_vm_processes(vm: &VmState) -> Vec<ProcessSnapshotEntry> {
12564    let process_table = vm.kernel.list_processes();
12565    snapshot_vm_processes_inner(vm, &process_table)
12566}
12567
12568fn snapshot_vm_processes_inner(
12569    vm: &VmState,
12570    process_table: &BTreeMap<u32, agentos_kernel::process_table::ProcessInfo>,
12571) -> Vec<ProcessSnapshotEntry> {
12572    let mut entries = Vec::new();
12573
12574    for (process_id, process) in &vm.active_processes {
12575        collect_process_snapshot_entries(process_id, process, process_table, &mut entries);
12576    }
12577
12578    for exited in &vm.exited_process_snapshots {
12579        entries.push(exited.process.clone());
12580    }
12581
12582    entries
12583}
12584
12585fn prune_exited_process_snapshots(vm: &mut VmState) {
12586    let cutoff = Instant::now() - EXITED_PROCESS_SNAPSHOT_RETENTION;
12587    while vm
12588        .exited_process_snapshots
12589        .front()
12590        .is_some_and(|snapshot| snapshot.captured_at < cutoff)
12591    {
12592        vm.exited_process_snapshots.pop_front();
12593    }
12594}
12595
12596fn build_process_snapshot_entry(
12597    process_id: &str,
12598    process: &ActiveProcess,
12599    info: &agentos_kernel::process_table::ProcessInfo,
12600    exit_code: Option<i32>,
12601) -> ProcessSnapshotEntry {
12602    wire_process_snapshot_entry_from_shared(process_snapshot_entry_from_kernel(
12603        process_id,
12604        info,
12605        process.guest_cwd.clone(),
12606        exit_code,
12607    ))
12608}
12609
12610fn wire_process_snapshot_entry_from_shared(
12611    entry: SharedProcessSnapshotEntry,
12612) -> ProcessSnapshotEntry {
12613    ProcessSnapshotEntry {
12614        process_id: entry.process_id,
12615        pid: entry.pid,
12616        ppid: entry.ppid,
12617        pgid: entry.pgid,
12618        sid: entry.sid,
12619        driver: entry.driver,
12620        command: entry.command,
12621        args: entry.args,
12622        cwd: entry.cwd,
12623        status: match entry.status {
12624            SharedProcessSnapshotStatus::Running => ProcessSnapshotStatus::Running,
12625            SharedProcessSnapshotStatus::Stopped => ProcessSnapshotStatus::Stopped,
12626            SharedProcessSnapshotStatus::Exited => ProcessSnapshotStatus::Exited,
12627        },
12628        exit_code: entry.exit_code,
12629    }
12630}
12631
12632fn collect_process_snapshot_entries(
12633    process_id: &str,
12634    process: &ActiveProcess,
12635    process_table: &BTreeMap<u32, agentos_kernel::process_table::ProcessInfo>,
12636    entries: &mut Vec<ProcessSnapshotEntry>,
12637) {
12638    if let Some(info) = process_table.get(&process.kernel_pid) {
12639        entries.push(build_process_snapshot_entry(
12640            process_id, process, info, None,
12641        ));
12642    }
12643
12644    for (child_id, child) in &process.child_processes {
12645        let child_process_id = format!("{process_id}/{child_id}");
12646        collect_process_snapshot_entries(&child_process_id, child, process_table, entries);
12647    }
12648}
12649
12650fn find_kernel_socket_state_entry(
12651    kernel: &SidecarKernel,
12652    process_id: &str,
12653    process: &ActiveProcess,
12654    kind: SocketQueryKind,
12655    request: &FindListenerRequest,
12656) -> Result<Option<SocketStateEntry>, SidecarError> {
12657    let entry = match kind {
12658        SocketQueryKind::TcpListener => process
12659            .tcp_listeners
12660            .values()
12661            .filter_map(|listener| listener.kernel_socket_id)
12662            .find_map(|socket_id| {
12663                kernel_socket_state_entry(kernel, process_id, socket_id, kind, request)
12664            }),
12665        SocketQueryKind::UdpBound => process
12666            .udp_sockets
12667            .values()
12668            .filter_map(|socket| socket.kernel_socket_id)
12669            .find_map(|socket_id| {
12670                kernel_socket_state_entry(kernel, process_id, socket_id, kind, request)
12671            }),
12672    };
12673
12674    if entry.is_some() {
12675        return Ok(entry);
12676    }
12677
12678    for child in process.child_processes.values() {
12679        if let Some(entry) =
12680            find_kernel_socket_state_entry(kernel, process_id, child, kind, request)?
12681        {
12682            return Ok(Some(entry));
12683        }
12684    }
12685
12686    Ok(None)
12687}
12688
12689fn kernel_socket_state_entry(
12690    kernel: &SidecarKernel,
12691    process_id: &str,
12692    socket_id: SocketId,
12693    kind: SocketQueryKind,
12694    request: &FindListenerRequest,
12695) -> Option<SocketStateEntry> {
12696    let record = kernel.socket_get(socket_id)?;
12697    let local_address = record.local_address()?;
12698    match kind {
12699        SocketQueryKind::TcpListener if record.state() == SocketState::Listening => {}
12700        SocketQueryKind::TcpListener => return None,
12701        SocketQueryKind::UdpBound => {}
12702    }
12703
12704    if !socket_host_matches(request.host.as_deref(), local_address.host()) {
12705        return None;
12706    }
12707    if request
12708        .port
12709        .is_some_and(|port| local_address.port() != port)
12710    {
12711        return None;
12712    }
12713
12714    Some(SocketStateEntry {
12715        process_id: process_id.to_owned(),
12716        host: Some(local_address.host().to_owned()),
12717        port: Some(local_address.port()),
12718        path: None,
12719    })
12720}
12721
12722fn socket_inodes_for_pid(pid: u32) -> Result<BTreeSet<u64>, SidecarError> {
12723    let fd_dir = PathBuf::from(format!("/proc/{pid}/fd"));
12724    let entries = match fs::read_dir(&fd_dir) {
12725        Ok(entries) => entries,
12726        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(BTreeSet::new()),
12727        Err(error) => {
12728            return Err(SidecarError::Io(format!(
12729                "failed to read socket descriptors for process {pid}: {error}"
12730            )));
12731        }
12732    };
12733
12734    let mut inodes = BTreeSet::new();
12735    for entry in entries {
12736        let entry = entry.map_err(|error| {
12737            SidecarError::Io(format!(
12738                "failed to inspect fd entry for process {pid}: {error}"
12739            ))
12740        })?;
12741        let target = match fs::read_link(entry.path()) {
12742            Ok(target) => target,
12743            Err(_) => continue,
12744        };
12745        if let Some(inode) = parse_socket_inode(&target) {
12746            inodes.insert(inode);
12747        }
12748    }
12749
12750    Ok(inodes)
12751}
12752
12753fn parse_socket_inode(target: &Path) -> Option<u64> {
12754    let value = target.to_string_lossy();
12755    let trimmed = value.strip_prefix("socket:[")?.strip_suffix(']')?;
12756    trimmed.parse().ok()
12757}
12758
12759fn unix_socket_path(addr: &UnixSocketAddr) -> Option<String> {
12760    addr.as_pathname()
12761        .map(|path| path.to_string_lossy().into_owned())
12762}
12763
12764fn find_unix_socket_for_pid(
12765    pid: u32,
12766    inodes: &BTreeSet<u64>,
12767    path: &str,
12768    process_id: &str,
12769) -> Result<Option<SocketStateEntry>, SidecarError> {
12770    let table_path = format!("/proc/{pid}/net/unix");
12771    let contents = match fs::read_to_string(&table_path) {
12772        Ok(contents) => contents,
12773        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
12774        Err(error) => {
12775            return Err(SidecarError::Io(format!(
12776                "failed to inspect unix sockets for process {pid}: {error}"
12777            )));
12778        }
12779    };
12780
12781    for line in contents.lines().skip(1) {
12782        let columns = line.split_whitespace().collect::<Vec<_>>();
12783        if columns.len() < 8 {
12784            continue;
12785        }
12786        let Ok(inode) = columns[6].parse::<u64>() else {
12787            continue;
12788        };
12789        if !inodes.contains(&inode) || columns[7] != path {
12790            continue;
12791        }
12792        return Ok(Some(SocketStateEntry {
12793            process_id: process_id.to_owned(),
12794            host: None,
12795            port: None,
12796            path: Some(path.to_owned()),
12797        }));
12798    }
12799
12800    Ok(None)
12801}
12802
12803fn find_inet_socket_for_pid(
12804    table_path: &str,
12805    inodes: &BTreeSet<u64>,
12806    kind: SocketQueryKind,
12807    requested_host: Option<&str>,
12808    requested_port: Option<u16>,
12809    process_id: &str,
12810) -> Result<Option<SocketStateEntry>, SidecarError> {
12811    for entry in parse_proc_net_entries(table_path)? {
12812        if !inodes.contains(&entry.inode) {
12813            continue;
12814        }
12815        if matches!(kind, SocketQueryKind::TcpListener) && entry.state != "0A" {
12816            continue;
12817        }
12818        if !socket_host_matches(requested_host, &entry.local_host) {
12819            continue;
12820        }
12821        if let Some(port) = requested_port {
12822            if entry.local_port != port {
12823                continue;
12824            }
12825        }
12826        return Ok(Some(SocketStateEntry {
12827            process_id: process_id.to_owned(),
12828            host: Some(entry.local_host),
12829            port: Some(entry.local_port),
12830            path: None,
12831        }));
12832    }
12833
12834    Ok(None)
12835}
12836
12837fn is_unspecified_socket_host(host: &str) -> bool {
12838    host == "0.0.0.0" || host == "::"
12839}
12840
12841fn is_loopback_socket_host(host: &str) -> bool {
12842    host == "127.0.0.1" || host == "::1" || host.eq_ignore_ascii_case("localhost")
12843}
12844
12845pub(crate) fn vm_network_resource_counts(vm: &VmState) -> NetworkResourceCounts {
12846    let snapshot = vm.kernel.resource_snapshot();
12847    let mut counts = NetworkResourceCounts {
12848        sockets: snapshot.sockets,
12849        connections: snapshot.socket_connections,
12850    };
12851    for process in vm.active_processes.values() {
12852        let process_counts = process.sidecar_only_network_resource_counts();
12853        counts.sockets += process_counts.sockets;
12854        counts.connections += process_counts.connections;
12855    }
12856    counts
12857}
12858
12859#[allow(clippy::too_many_arguments)]
12860fn collect_javascript_socket_port_state(
12861    kernel: &SidecarKernel,
12862    process_id: &str,
12863    process: &ActiveProcess,
12864    tcp_guest_to_host: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
12865    http_loopback_targets: &mut BTreeMap<
12866        (JavascriptSocketFamily, u16),
12867        JavascriptHttpLoopbackTarget,
12868    >,
12869    udp_guest_to_host: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
12870    udp_host_to_guest: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
12871    used_tcp_ports: &mut BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
12872    used_udp_ports: &mut BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
12873) {
12874    for (family, port) in process.tcp_port_reservations.values() {
12875        used_tcp_ports.entry(*family).or_default().insert(*port);
12876    }
12877
12878    let mut record_tcp_listener = |guest_addr: SocketAddr, host_port: u16| {
12879        let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
12880        used_tcp_ports
12881            .entry(family)
12882            .or_default()
12883            .insert(guest_addr.port());
12884        // VM-local loopback connects should also resolve listeners bound to
12885        // unspecified guest addresses like 0.0.0.0/::.
12886        tcp_guest_to_host.insert((family, guest_addr.port()), host_port);
12887    };
12888
12889    for listener in process.tcp_listeners.values() {
12890        let local_addr = listener
12891            .kernel_socket_id
12892            .and_then(|socket_id| kernel.socket_get(socket_id))
12893            .and_then(|record| record.local_address().cloned())
12894            .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
12895            .unwrap_or_else(|| listener.guest_local_addr());
12896        record_tcp_listener(local_addr, local_addr.port());
12897    }
12898
12899    for (server_id, server) in &process.http_servers {
12900        let host_port = match server.listener.local_addr() {
12901            Ok(addr) => addr.port(),
12902            Err(_) => continue,
12903        };
12904        record_tcp_listener(server.guest_local_addr, host_port);
12905        let family = JavascriptSocketFamily::from_ip(server.guest_local_addr.ip());
12906        http_loopback_targets.insert(
12907            (family, server.guest_local_addr.port()),
12908            JavascriptHttpLoopbackTarget {
12909                process_id: process_id.to_owned(),
12910                server_id: *server_id,
12911            },
12912        );
12913    }
12914
12915    if let Ok(http2) = process.http2.shared.lock() {
12916        for server in http2.servers.values() {
12917            record_tcp_listener(server.guest_local_addr, server.actual_local_addr.port());
12918        }
12919    }
12920
12921    for socket in process.tcp_sockets.values() {
12922        let guest_addr = socket
12923            .kernel_socket_id
12924            .and_then(|socket_id| kernel.socket_get(socket_id))
12925            .and_then(|record| record.local_address().cloned())
12926            .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
12927            .unwrap_or(socket.guest_local_addr);
12928        let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
12929        used_tcp_ports
12930            .entry(family)
12931            .or_default()
12932            .insert(guest_addr.port());
12933    }
12934
12935    for socket in process.udp_sockets.values() {
12936        let guest_addr = socket
12937            .kernel_socket_id
12938            .and_then(|socket_id| kernel.socket_get(socket_id))
12939            .and_then(|record| record.local_address().cloned())
12940            .and_then(|address| {
12941                resolve_udp_bind_addr(address.host(), address.port(), socket.family).ok()
12942            })
12943            .or_else(|| socket.local_addr());
12944        let Some(guest_addr) = guest_addr else {
12945            continue;
12946        };
12947        let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
12948        used_udp_ports
12949            .entry(family)
12950            .or_default()
12951            .insert(guest_addr.port());
12952        if let Some(host_addr) = socket
12953            .socket
12954            .as_ref()
12955            .and_then(|socket| socket.local_addr().ok())
12956        {
12957            if is_loopback_ip(guest_addr.ip()) {
12958                udp_guest_to_host.insert((family, guest_addr.port()), host_addr.port());
12959                udp_host_to_guest.insert((family, host_addr.port()), guest_addr.port());
12960            }
12961        } else if socket.kernel_socket_id.is_some() && is_loopback_ip(guest_addr.ip()) {
12962            udp_guest_to_host.insert((family, guest_addr.port()), guest_addr.port());
12963            udp_host_to_guest.insert((family, guest_addr.port()), guest_addr.port());
12964        }
12965    }
12966
12967    for (child_process_id, child) in &process.child_processes {
12968        let child_id = format!("{process_id}/{child_process_id}");
12969        collect_javascript_socket_port_state(
12970            kernel,
12971            &child_id,
12972            child,
12973            tcp_guest_to_host,
12974            http_loopback_targets,
12975            udp_guest_to_host,
12976            udp_host_to_guest,
12977            used_tcp_ports,
12978            used_udp_ports,
12979        );
12980    }
12981}
12982
12983pub(crate) fn build_javascript_socket_path_context(
12984    vm: &VmState,
12985) -> Result<JavascriptSocketPathContext, SidecarError> {
12986    let mut loopback_exempt_ports = vm.create_loopback_exempt_ports.clone();
12987    loopback_exempt_ports.extend(vm.configuration.loopback_exempt_ports.iter().copied());
12988    let mut tcp_loopback_guest_to_host_ports = BTreeMap::new();
12989    let mut http_loopback_targets = BTreeMap::new();
12990    let mut udp_loopback_guest_to_host_ports = BTreeMap::new();
12991    let mut udp_loopback_host_to_guest_ports = BTreeMap::new();
12992    let mut used_tcp_guest_ports = BTreeMap::new();
12993    let mut used_udp_guest_ports = BTreeMap::new();
12994    for (process_id, process) in &vm.active_processes {
12995        collect_javascript_socket_port_state(
12996            &vm.kernel,
12997            process_id,
12998            process,
12999            &mut tcp_loopback_guest_to_host_ports,
13000            &mut http_loopback_targets,
13001            &mut udp_loopback_guest_to_host_ports,
13002            &mut udp_loopback_host_to_guest_ports,
13003            &mut used_tcp_guest_ports,
13004            &mut used_udp_guest_ports,
13005        );
13006    }
13007    Ok(JavascriptSocketPathContext {
13008        sandbox_root: vm.cwd.clone(),
13009        mounts: vm.configuration.mounts.clone(),
13010        listen_policy: vm.listen_policy,
13011        loopback_exempt_ports,
13012        tcp_loopback_guest_to_host_ports,
13013        http_loopback_targets,
13014        udp_loopback_guest_to_host_ports,
13015        udp_loopback_host_to_guest_ports,
13016        used_tcp_guest_ports,
13017        used_udp_guest_ports,
13018    })
13019}
13020
13021fn check_network_resource_limit(
13022    limit: Option<usize>,
13023    current: usize,
13024    additional: usize,
13025    label: &str,
13026) -> Result<(), SidecarError> {
13027    if let Some(limit) = limit {
13028        if current.saturating_add(additional) > limit {
13029            return Err(SidecarError::Execution(format!(
13030                "EAGAIN: maximum {label} count reached"
13031            )));
13032        }
13033    }
13034    Ok(())
13035}
13036
13037fn normalize_tcp_listen_host(
13038    host: Option<&str>,
13039) -> Result<(JavascriptSocketFamily, &'static str, &'static str), SidecarError> {
13040    match host.unwrap_or("127.0.0.1") {
13041        "127.0.0.1" | "localhost" => Ok((JavascriptSocketFamily::Ipv4, "127.0.0.1", "127.0.0.1")),
13042        "::1" => Ok((JavascriptSocketFamily::Ipv6, "::1", "::1")),
13043        "0.0.0.0" => Ok((JavascriptSocketFamily::Ipv4, "127.0.0.1", "0.0.0.0")),
13044        "::" => Ok((JavascriptSocketFamily::Ipv6, "::1", "::")),
13045        other => Err(SidecarError::Execution(format!(
13046            "EACCES: TCP listeners must bind to loopback or unspecified addresses, got {other}"
13047        ))),
13048    }
13049}
13050
13051fn normalize_udp_bind_host(
13052    host: Option<&str>,
13053    family: JavascriptUdpFamily,
13054) -> Result<(&'static str, &'static str, JavascriptSocketFamily), SidecarError> {
13055    match (family, host) {
13056        (JavascriptUdpFamily::Ipv4, None) | (JavascriptUdpFamily::Ipv4, Some("0.0.0.0")) => {
13057            Ok(("127.0.0.1", "0.0.0.0", JavascriptSocketFamily::Ipv4))
13058        }
13059        (JavascriptUdpFamily::Ipv4, Some("127.0.0.1"))
13060        | (JavascriptUdpFamily::Ipv4, Some("localhost")) => {
13061            Ok(("127.0.0.1", "127.0.0.1", JavascriptSocketFamily::Ipv4))
13062        }
13063        (JavascriptUdpFamily::Ipv6, None) | (JavascriptUdpFamily::Ipv6, Some("::")) => {
13064            Ok(("::1", "::", JavascriptSocketFamily::Ipv6))
13065        }
13066        (JavascriptUdpFamily::Ipv6, Some("::1"))
13067        | (JavascriptUdpFamily::Ipv6, Some("localhost")) => {
13068            Ok(("::1", "::1", JavascriptSocketFamily::Ipv6))
13069        }
13070        (JavascriptUdpFamily::Ipv4, Some(other)) => Err(SidecarError::Execution(format!(
13071            "EACCES: udp4 sockets must bind to 127.0.0.1 or 0.0.0.0, got {other}"
13072        ))),
13073        (JavascriptUdpFamily::Ipv6, Some(other)) => Err(SidecarError::Execution(format!(
13074            "EACCES: udp6 sockets must bind to ::1 or ::, got {other}"
13075        ))),
13076    }
13077}
13078
13079fn allocate_guest_listen_port(
13080    requested_port: u16,
13081    family: JavascriptSocketFamily,
13082    used_ports: &BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
13083    policy: VmListenPolicy,
13084) -> Result<u16, SidecarError> {
13085    let is_allowed = |port: u16| {
13086        port >= policy.port_min
13087            && port <= policy.port_max
13088            && (policy.allow_privileged || port >= 1024)
13089    };
13090    let used = used_ports.get(&family);
13091
13092    if requested_port != 0 {
13093        if !is_allowed(requested_port) {
13094            let reason = if requested_port < 1024 && !policy.allow_privileged {
13095                format!(
13096                    "EACCES: privileged listen port {requested_port} requires {}=true",
13097                    VM_LISTEN_ALLOW_PRIVILEGED_METADATA_KEY
13098                )
13099            } else {
13100                format!(
13101                    "EACCES: listen port {requested_port} is outside the allowed range {}-{}",
13102                    policy.port_min, policy.port_max
13103                )
13104            };
13105            return Err(SidecarError::Execution(reason));
13106        }
13107        if used.is_some_and(|ports| ports.contains(&requested_port)) {
13108            return Err(sidecar_net_error(std::io::Error::from_raw_os_error(
13109                libc::EADDRINUSE,
13110            )));
13111        }
13112        return Ok(requested_port);
13113    }
13114
13115    let allocation_start = policy
13116        .port_min
13117        .max(if policy.allow_privileged { 1 } else { 1024 });
13118    for candidate in allocation_start..=policy.port_max {
13119        if used.is_some_and(|ports| ports.contains(&candidate)) {
13120            continue;
13121        }
13122        return Ok(candidate);
13123    }
13124
13125    Err(sidecar_net_error(std::io::Error::from_raw_os_error(
13126        libc::EADDRINUSE,
13127    )))
13128}
13129
13130fn socket_host_matches(requested: Option<&str>, actual: &str) -> bool {
13131    match requested {
13132        None => true,
13133        Some(requested) if requested == actual => true,
13134        Some(requested)
13135            if is_unspecified_socket_host(requested) && is_unspecified_socket_host(actual) =>
13136        {
13137            true
13138        }
13139        Some(requested) if is_unspecified_socket_host(requested) => is_loopback_socket_host(actual),
13140        Some(requested) if requested.eq_ignore_ascii_case("localhost") => {
13141            is_loopback_socket_host(actual)
13142        }
13143        _ => false,
13144    }
13145}
13146
13147fn parse_proc_net_entries(table_path: &str) -> Result<Vec<ProcNetEntry>, SidecarError> {
13148    let contents = match fs::read_to_string(table_path) {
13149        Ok(contents) => contents,
13150        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
13151        Err(error) => {
13152            return Err(SidecarError::Io(format!(
13153                "failed to inspect socket table {table_path}: {error}"
13154            )));
13155        }
13156    };
13157
13158    let mut entries = Vec::new();
13159    for line in contents.lines().skip(1) {
13160        let columns = line.split_whitespace().collect::<Vec<_>>();
13161        if columns.len() < 10 {
13162            continue;
13163        }
13164        let Some((host, port)) = parse_proc_ip_port(columns[1]) else {
13165            continue;
13166        };
13167        let Ok(inode) = columns[9].parse::<u64>() else {
13168            continue;
13169        };
13170        entries.push(ProcNetEntry {
13171            local_host: host,
13172            local_port: port,
13173            state: columns[3].to_owned(),
13174            inode,
13175        });
13176    }
13177
13178    Ok(entries)
13179}
13180
13181fn parse_proc_ip_port(value: &str) -> Option<(String, u16)> {
13182    let (raw_ip, raw_port) = value.split_once(':')?;
13183    let port = u16::from_str_radix(raw_port, 16).ok()?;
13184    let host = match raw_ip.len() {
13185        8 => {
13186            let raw = u32::from_str_radix(raw_ip, 16).ok()?;
13187            Ipv4Addr::from(raw.to_le_bytes()).to_string()
13188        }
13189        32 => {
13190            let mut bytes = [0_u8; 16];
13191            for (index, chunk) in raw_ip.as_bytes().chunks(8).enumerate() {
13192                let word = u32::from_str_radix(std::str::from_utf8(chunk).ok()?, 16).ok()?;
13193                bytes[index * 4..(index + 1) * 4].copy_from_slice(&word.to_le_bytes());
13194            }
13195            Ipv6Addr::from(bytes).to_string()
13196        }
13197        _ => return None,
13198    };
13199    Some((host, port))
13200}
13201
13202fn python_file_entrypoint(entrypoint: &str) -> Option<PathBuf> {
13203    let path = Path::new(entrypoint);
13204    (path.extension().and_then(|extension| extension.to_str()) == Some("py"))
13205        .then(|| path.to_path_buf())
13206}
13207
13208fn add_runtime_guest_path_mapping(
13209    env: &mut BTreeMap<String, String>,
13210    guest_path: &str,
13211    host_path: &Path,
13212) {
13213    let mut mappings = env
13214        .get("AGENTOS_GUEST_PATH_MAPPINGS")
13215        .and_then(|value| serde_json::from_str::<Vec<Value>>(value).ok())
13216        .unwrap_or_default();
13217    mappings.retain(|mapping| {
13218        mapping
13219            .get("guestPath")
13220            .and_then(Value::as_str)
13221            .map(|existing| normalize_path(existing) != normalize_path(guest_path))
13222            .unwrap_or(true)
13223    });
13224    mappings.push(json!({
13225        "guestPath": normalize_path(guest_path),
13226        "hostPath": host_path.display().to_string(),
13227    }));
13228    if let Ok(serialized) = serde_json::to_string(&mappings) {
13229        env.insert(String::from("AGENTOS_GUEST_PATH_MAPPINGS"), serialized);
13230    }
13231}
13232
13233fn add_runtime_host_access_path(
13234    env: &mut BTreeMap<String, String>,
13235    key: &str,
13236    host_path: &Path,
13237    expand: bool,
13238) {
13239    let existing = env
13240        .get(key)
13241        .and_then(|value| serde_json::from_str::<Vec<String>>(value).ok())
13242        .unwrap_or_default()
13243        .into_iter()
13244        .map(PathBuf::from)
13245        .collect::<Vec<_>>();
13246    let mut paths = existing;
13247    paths.push(host_path.to_path_buf());
13248    let normalized = if expand {
13249        expand_host_access_paths(&paths)
13250    } else {
13251        dedupe_host_paths(&paths)
13252    };
13253    let serialized = normalized
13254        .iter()
13255        .map(|path| path.to_string_lossy().into_owned())
13256        .collect::<Vec<_>>();
13257    if let Ok(serialized) = serde_json::to_string(&serialized) {
13258        env.insert(key.to_owned(), serialized);
13259    }
13260}
13261
13262// discover_command_guest_paths moved to crate::bootstrap
13263
13264fn is_path_like_specifier(specifier: &str) -> bool {
13265    specifier.starts_with('/')
13266        || specifier.starts_with("./")
13267        || specifier.starts_with("../")
13268        || specifier.starts_with("file:")
13269}
13270
13271fn execution_wasm_permission_tier(tier: WasmPermissionTier) -> ExecutionWasmPermissionTier {
13272    match tier {
13273        WasmPermissionTier::Full => ExecutionWasmPermissionTier::Full,
13274        WasmPermissionTier::ReadWrite => ExecutionWasmPermissionTier::ReadWrite,
13275        WasmPermissionTier::ReadOnly => ExecutionWasmPermissionTier::ReadOnly,
13276        WasmPermissionTier::Isolated => ExecutionWasmPermissionTier::Isolated,
13277    }
13278}
13279
13280fn resolve_wasm_permission_tier(
13281    vm: &VmState,
13282    command_name: Option<&str>,
13283    explicit_tier: Option<WasmPermissionTier>,
13284    entrypoint: &str,
13285) -> WasmPermissionTier {
13286    explicit_tier
13287        .or_else(|| command_name.and_then(|command| vm.command_permissions.get(command).copied()))
13288        .or_else(|| {
13289            Path::new(entrypoint)
13290                .file_name()
13291                .and_then(|name| name.to_str())
13292                .and_then(|command| vm.command_permissions.get(command).copied())
13293        })
13294        .unwrap_or(WasmPermissionTier::Full)
13295}
13296
13297fn tokenize_shell_free_command(command: &str) -> Vec<String> {
13298    command
13299        .split_whitespace()
13300        .filter(|segment| !segment.is_empty())
13301        .map(str::to_owned)
13302        .collect()
13303}
13304
13305fn is_posix_shell_builtin(command: &str) -> bool {
13306    matches!(
13307        command,
13308        "." | ":"
13309            | "break"
13310            | "cd"
13311            | "continue"
13312            | "eval"
13313            | "exec"
13314            | "exit"
13315            | "export"
13316            | "readonly"
13317            | "return"
13318            | "set"
13319            | "shift"
13320            | "times"
13321            | "trap"
13322            | "umask"
13323            | "unset"
13324    )
13325}
13326
13327/// Single-token checks for shell-mode commands whose first word forces a real
13328/// shell even when the command string has no shell metacharacters. This is not
13329/// a parser: env-assignment prefixes (`FOO=bar cmd`) and shell reserved words
13330/// have no meaning outside `sh`, so whitespace-tokenizing them would silently
13331/// run the wrong program.
13332fn shell_first_token_requires_shell(token: &str) -> bool {
13333    token.contains('=') || is_shell_reserved_word(token)
13334}
13335
13336fn is_shell_reserved_word(token: &str) -> bool {
13337    matches!(
13338        token,
13339        "if" | "then"
13340            | "elif"
13341            | "else"
13342            | "fi"
13343            | "for"
13344            | "in"
13345            | "do"
13346            | "done"
13347            | "while"
13348            | "until"
13349            | "case"
13350            | "esac"
13351            | "{"
13352            | "}"
13353            | "!"
13354    )
13355}
13356
13357fn command_requires_shell(command: &str) -> bool {
13358    command.chars().any(|ch| {
13359        matches!(
13360            ch,
13361            '|' | '&'
13362                | ';'
13363                | '<'
13364                | '>'
13365                | '('
13366                | ')'
13367                | '$'
13368                | '`'
13369                | '*'
13370                | '?'
13371                | '['
13372                | ']'
13373                | '{'
13374                | '}'
13375                | '~'
13376                | '\''
13377                | '"'
13378                | '\\'
13379                | '\n'
13380        )
13381    })
13382}
13383
13384fn host_mount_path_for_guest_path(vm: &VmState, guest_path: &str) -> Option<PathBuf> {
13385    let normalized = normalize_path(guest_path);
13386
13387    let mut mounts = vm
13388        .configuration
13389        .mounts
13390        .iter()
13391        .filter_map(|mount| {
13392            ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
13393                .then(|| {
13394                    mount_config_host_backing_path(&mount.plugin.config)
13395                        .map(|host_path| (mount.guest_path.as_str(), host_path))
13396                })
13397                .flatten()
13398        })
13399        .collect::<Vec<_>>();
13400    mounts.sort_by_key(|mount| std::cmp::Reverse(mount.0.len()));
13401
13402    for (guest_root, host_root) in mounts {
13403        if normalized != guest_root && !normalized.starts_with(&format!("{guest_root}/")) {
13404            continue;
13405        }
13406
13407        let suffix = normalized
13408            .strip_prefix(guest_root)
13409            .unwrap_or_default()
13410            .trim_start_matches('/');
13411        let mut path = PathBuf::from(host_root);
13412        if !suffix.is_empty() {
13413            path.push(suffix);
13414        }
13415        return Some(path);
13416    }
13417
13418    None
13419}
13420
13421fn host_runtime_path_for_guest_path_with_env(
13422    vm: &VmState,
13423    runtime_env: &BTreeMap<String, String>,
13424    guest_path: &str,
13425    default_host_cwd: &Path,
13426) -> Option<PathBuf> {
13427    if let Some(path) = host_mount_path_for_guest_path(vm, guest_path) {
13428        return Some(path);
13429    }
13430    if let Some(path) = host_path_from_runtime_guest_mappings(runtime_env, guest_path) {
13431        return Some(path);
13432    }
13433
13434    let normalized = normalize_path(guest_path);
13435    let virtual_home = guest_virtual_home(vm);
13436
13437    if normalized == virtual_home || normalized.starts_with(&format!("{virtual_home}/")) {
13438        let suffix = normalized
13439            .strip_prefix(&virtual_home)
13440            .unwrap_or_default()
13441            .trim_start_matches('/');
13442        let mut host_path = default_host_cwd.to_path_buf();
13443        if !suffix.is_empty() {
13444            host_path.push(suffix);
13445        }
13446        return Some(host_path);
13447    }
13448
13449    None
13450}
13451
13452#[derive(Deserialize, Serialize)]
13453struct RuntimeGuestPathMapping {
13454    #[serde(rename = "guestPath")]
13455    guest_path: String,
13456    #[serde(rename = "hostPath")]
13457    host_path: String,
13458    #[serde(rename = "readOnly", default)]
13459    read_only: bool,
13460}
13461
13462pub(crate) fn host_path_from_runtime_guest_mappings(
13463    runtime_env: &BTreeMap<String, String>,
13464    guest_path: &str,
13465) -> Option<PathBuf> {
13466    let mappings = runtime_env
13467        .get("AGENTOS_GUEST_PATH_MAPPINGS")
13468        .and_then(|value| serde_json::from_str::<Vec<RuntimeGuestPathMapping>>(value).ok())?;
13469    let normalized = normalize_path(guest_path);
13470
13471    let mut sorted_mappings = mappings
13472        .into_iter()
13473        .filter_map(|mapping| {
13474            (!mapping.guest_path.is_empty() && !mapping.host_path.is_empty()).then_some((
13475                normalize_path(&mapping.guest_path),
13476                PathBuf::from(mapping.host_path),
13477            ))
13478        })
13479        .collect::<Vec<_>>();
13480    sorted_mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.0.len()));
13481
13482    for (guest_root, mut host_root) in sorted_mappings {
13483        if guest_root != "/"
13484            && normalized != guest_root
13485            && !normalized.starts_with(&format!("{guest_root}/"))
13486        {
13487            continue;
13488        }
13489        if guest_root == "/" && !normalized.starts_with('/') {
13490            continue;
13491        }
13492
13493        if host_root.is_relative() {
13494            host_root = std::env::current_dir().ok()?.join(host_root);
13495        }
13496
13497        let suffix = if guest_root == "/" {
13498            normalized.trim_start_matches('/')
13499        } else {
13500            normalized
13501                .strip_prefix(&guest_root)
13502                .unwrap_or_default()
13503                .trim_start_matches('/')
13504        };
13505        if !suffix.is_empty() {
13506            host_root.push(suffix);
13507        }
13508        return Some(host_root);
13509    }
13510
13511    None
13512}
13513
13514fn guest_runtime_path_for_host_path(
13515    runtime_env: &BTreeMap<String, String>,
13516    virtual_home: &str,
13517    cwd: &Path,
13518    host_path: &str,
13519) -> Option<String> {
13520    let resolved = if host_path.starts_with("file://") {
13521        PathBuf::from(host_path.trim_start_matches("file://"))
13522    } else if host_path.starts_with("file:") {
13523        PathBuf::from(host_path.trim_start_matches("file:"))
13524    } else {
13525        let candidate = PathBuf::from(host_path);
13526        if candidate.is_absolute() {
13527            candidate
13528        } else if host_path.starts_with("./") || host_path.starts_with("../") {
13529            cwd.join(candidate)
13530        } else {
13531            return None;
13532        }
13533    };
13534    let normalized = normalize_host_path(&resolved);
13535
13536    if let Some(path) = guest_path_from_runtime_host_mappings(runtime_env, &normalized) {
13537        return Some(path);
13538    }
13539
13540    let normalized_cwd = normalize_host_path(cwd);
13541    if !path_is_within_root(&normalized, &normalized_cwd) {
13542        return None;
13543    }
13544
13545    let virtual_home = if virtual_home.starts_with('/') {
13546        virtual_home.to_string()
13547    } else {
13548        String::from("/root")
13549    };
13550    let suffix = normalized
13551        .strip_prefix(&normalized_cwd)
13552        .ok()?
13553        .to_string_lossy()
13554        .replace('\\', "/")
13555        .trim_start_matches('/')
13556        .to_owned();
13557
13558    Some(if suffix.is_empty() {
13559        virtual_home
13560    } else {
13561        normalize_path(&format!("{virtual_home}/{suffix}"))
13562    })
13563}
13564
13565fn guest_path_from_runtime_host_mappings(
13566    runtime_env: &BTreeMap<String, String>,
13567    host_path: &Path,
13568) -> Option<String> {
13569    let mappings = runtime_env
13570        .get("AGENTOS_GUEST_PATH_MAPPINGS")
13571        .and_then(|value| serde_json::from_str::<Vec<RuntimeGuestPathMapping>>(value).ok())?;
13572    let normalized = normalize_host_path(host_path);
13573
13574    let mut sorted_mappings = mappings
13575        .into_iter()
13576        .filter_map(|mapping| {
13577            (!mapping.guest_path.is_empty() && !mapping.host_path.is_empty()).then_some((
13578                normalize_path(&mapping.guest_path),
13579                normalize_host_path(Path::new(&mapping.host_path)),
13580            ))
13581        })
13582        .collect::<Vec<_>>();
13583    sorted_mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.1.as_os_str().len()));
13584
13585    for (guest_root, host_root) in sorted_mappings {
13586        if !path_is_within_root(&normalized, &host_root) {
13587            continue;
13588        }
13589        let suffix = normalized
13590            .strip_prefix(&host_root)
13591            .ok()?
13592            .to_string_lossy()
13593            .replace('\\', "/")
13594            .trim_start_matches('/')
13595            .to_owned();
13596
13597        return Some(if suffix.is_empty() {
13598            guest_root
13599        } else if guest_root == "/" {
13600            normalize_path(&format!("/{suffix}"))
13601        } else {
13602            normalize_path(&format!("{guest_root}/{suffix}"))
13603        });
13604    }
13605
13606    None
13607}
13608
13609fn host_mount_path_for_guest_path_from_mounts(
13610    mounts: &[crate::protocol::MountDescriptor],
13611    guest_path: &str,
13612) -> Option<PathBuf> {
13613    let normalized = normalize_path(guest_path);
13614
13615    let mut host_mounts = mounts
13616        .iter()
13617        .filter_map(|mount| {
13618            ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
13619                .then(|| {
13620                    mount_config_host_backing_path(&mount.plugin.config)
13621                        .map(|host_path| (mount.guest_path.as_str(), host_path))
13622                })
13623                .flatten()
13624        })
13625        .collect::<Vec<_>>();
13626    host_mounts.sort_by_key(|mount| std::cmp::Reverse(mount.0.len()));
13627
13628    for (guest_root, host_root) in host_mounts {
13629        if normalized != guest_root && !normalized.starts_with(&format!("{guest_root}/")) {
13630            continue;
13631        }
13632
13633        let suffix = normalized
13634            .strip_prefix(guest_root)
13635            .unwrap_or_default()
13636            .trim_start_matches('/');
13637        let mut path = PathBuf::from(host_root);
13638        if !suffix.is_empty() {
13639            path.push(suffix);
13640        }
13641        return Some(path);
13642    }
13643
13644    None
13645}
13646
13647#[cfg(test)]
13648mod host_mount_path_for_guest_path_from_mounts_tests {
13649    use super::host_mount_path_for_guest_path_from_mounts;
13650    use crate::protocol::{MountDescriptor, MountPluginDescriptor};
13651    use serde_json::json;
13652    use std::path::PathBuf;
13653
13654    #[test]
13655    fn resolves_module_access_mount_paths() {
13656        let mounts = vec![MountDescriptor {
13657            guest_path: String::from("/root/node_modules"),
13658            read_only: true,
13659            plugin: MountPluginDescriptor {
13660                id: String::from("module_access"),
13661                config: json!({
13662                    "hostPath": "/tmp/workspace/node_modules",
13663                })
13664                .to_string(),
13665            },
13666        }];
13667
13668        let resolved =
13669            host_mount_path_for_guest_path_from_mounts(&mounts, "/root/node_modules/pkg/index.js")
13670                .expect("module_access mount should resolve");
13671
13672        assert_eq!(
13673            resolved,
13674            PathBuf::from("/tmp/workspace/node_modules/pkg/index.js")
13675        );
13676    }
13677
13678    #[test]
13679    fn does_not_resolve_agentos_packages_as_host_paths() {
13680        let mounts = vec![MountDescriptor {
13681            guest_path: String::from("/opt/agentos/bin/pi"),
13682            read_only: true,
13683            plugin: MountPluginDescriptor {
13684                id: String::from("agentos_packages"),
13685                config: json!({
13686                    "kind": "singleSymlink",
13687                    "target": "../pkgs/pi/current/bin/pi",
13688                })
13689                .to_string(),
13690            },
13691        }];
13692
13693        assert!(
13694            host_mount_path_for_guest_path_from_mounts(&mounts, "/opt/agentos/bin/pi").is_none()
13695        );
13696    }
13697}
13698
13699fn resolve_guest_socket_host_path(
13700    context: &JavascriptSocketPathContext,
13701    guest_path: &str,
13702) -> PathBuf {
13703    if let Some(path) = host_mount_path_for_guest_path_from_mounts(&context.mounts, guest_path) {
13704        return path;
13705    }
13706
13707    let normalized = normalize_path(guest_path);
13708    let mut host_path = context.sandbox_root.clone();
13709    let suffix = normalized.trim_start_matches('/');
13710    if !suffix.is_empty() {
13711        host_path.push(suffix);
13712    }
13713    host_path
13714}
13715
13716fn ensure_kernel_parent_directories(
13717    kernel: &mut SidecarKernel,
13718    path: &str,
13719) -> Result<(), SidecarError> {
13720    let parent = dirname(path);
13721    if parent != "/" && !kernel.exists(&parent).map_err(kernel_error)? {
13722        kernel.mkdir(&parent, true).map_err(kernel_error)?;
13723    }
13724    Ok(())
13725}
13726
13727// JavascriptChildProcessSpawnOptions, JavascriptChildProcessSpawnRequest moved to crate::protocol
13728// ResolvedChildProcessExecution moved to crate::state
13729
13730pub(crate) fn sanitize_javascript_child_process_internal_bootstrap_env(
13731    env: &BTreeMap<String, String>,
13732) -> BTreeMap<String, String> {
13733    const ALLOWED_KEYS: &[&str] = &[
13734        "AGENTOS_ALLOWED_NODE_BUILTINS",
13735        "AGENTOS_GUEST_PATH_MAPPINGS",
13736        "AGENTOS_LOOPBACK_EXEMPT_PORTS",
13737        "AGENTOS_VIRTUAL_PROCESS_EXEC_PATH",
13738        "AGENTOS_VIRTUAL_PROCESS_UID",
13739        "AGENTOS_VIRTUAL_PROCESS_GID",
13740        "AGENTOS_VIRTUAL_PROCESS_VERSION",
13741    ];
13742
13743    env.iter()
13744        .filter(|(key, _)| {
13745            ALLOWED_KEYS.contains(&key.as_str()) || key.starts_with("AGENTOS_VIRTUAL_OS_")
13746        })
13747        .map(|(key, value)| (key.clone(), value.clone()))
13748        .collect()
13749}
13750
13751// Network request types moved to crate::protocol
13752
13753// VmDnsConfig, DnsResolutionSource moved to crate::state
13754
13755fn resolve_tcp_bind_addr(host: &str, port: u16) -> Result<SocketAddr, SidecarError> {
13756    (host, port)
13757        .to_socket_addrs()
13758        .map_err(sidecar_net_error)?
13759        .next()
13760        .ok_or_else(|| {
13761            SidecarError::Execution(format!("failed to resolve TCP bind address {host}:{port}"))
13762        })
13763}
13764
13765pub(crate) fn format_dns_resource(hostname: &str) -> String {
13766    format!("dns://{hostname}")
13767}
13768
13769// --- Guest Python socket bridge helpers ------------------------------------
13770
13771/// Host-socket read timeout for one `recv`/`recvfrom` RPC. Kept short so the
13772/// synchronous RPC returns promptly (data, or a `timed_out` flag the Python
13773/// shim re-polls on) and never stalls the shared sidecar event loop.
13774// Short so a recv/recvfrom RPC holds the shared event loop only briefly; the
13775// guest shim adds a capped backoff between polls to bound the poll rate.
13776const PYTHON_SOCKET_READ_POLL: Duration = Duration::from_millis(25);
13777const PYTHON_SOCKET_CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
13778const PYTHON_SOCKET_WRITE_TIMEOUT: Duration = Duration::from_secs(30);
13779const PYTHON_SOCKET_DEFAULT_RECV: usize = 65536;
13780const PYTHON_SOCKET_MAX_RECV: usize = 4 * 1024 * 1024;
13781
13782fn python_socket_host(request: &PythonVfsRpcRequest) -> Result<String, SidecarError> {
13783    request
13784        .hostname
13785        .clone()
13786        .ok_or_else(|| SidecarError::InvalidState(String::from("python socket op requires a host")))
13787}
13788
13789fn python_socket_port(request: &PythonVfsRpcRequest) -> Result<u16, SidecarError> {
13790    request
13791        .port
13792        .ok_or_else(|| SidecarError::InvalidState(String::from("python socket op requires a port")))
13793}
13794
13795fn python_socket_payload(request: &PythonVfsRpcRequest) -> Result<Vec<u8>, SidecarError> {
13796    let Some(body) = request.body_base64.as_deref() else {
13797        return Ok(Vec::new());
13798    };
13799    base64::engine::general_purpose::STANDARD
13800        .decode(body)
13801        .map_err(|error| {
13802            SidecarError::InvalidState(format!("invalid base64 python socket payload: {error}"))
13803        })
13804}
13805
13806fn python_socket_recv_len(request: &PythonVfsRpcRequest) -> usize {
13807    request
13808        .max_buffer
13809        .unwrap_or(PYTHON_SOCKET_DEFAULT_RECV)
13810        .clamp(1, PYTHON_SOCKET_MAX_RECV)
13811}
13812
13813fn python_connect_tcp(addrs: &[IpAddr], port: u16) -> Result<TcpStream, SidecarError> {
13814    let mut last_error: Option<String> = None;
13815    for ip in addrs {
13816        let addr = SocketAddr::new(*ip, port);
13817        match TcpStream::connect_timeout(&addr, PYTHON_SOCKET_CONNECT_TIMEOUT) {
13818            Ok(stream) => return Ok(stream),
13819            Err(error) => last_error = Some(error.to_string()),
13820        }
13821    }
13822    Err(SidecarError::Execution(format!(
13823        "ECONNREFUSED: {}",
13824        last_error.unwrap_or_else(|| String::from("no resolved addresses"))
13825    )))
13826}
13827
13828fn python_socket_would_block(error: &std::io::Error) -> bool {
13829    matches!(
13830        error.kind(),
13831        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
13832    )
13833}
13834
13835fn python_socket_io_error(error: std::io::Error) -> SidecarError {
13836    SidecarError::Execution(format!("EIO: python socket: {error}"))
13837}
13838
13839fn python_socket_kind_error(op: &str, expected: &str) -> SidecarError {
13840    SidecarError::Execution(format!(
13841        "EOPNOTSUPP: python socket {op} requires a {expected} socket"
13842    ))
13843}
13844
13845fn blocked_dns_resolution_error(
13846    resource: &str,
13847    ip: IpAddr,
13848    cidr: &str,
13849    label: &str,
13850) -> SidecarError {
13851    SidecarError::Execution(format!(
13852        "EACCES: blocked outbound network access to {resource}: {ip} is within restricted {label} range {cidr}"
13853    ))
13854}
13855
13856fn blocked_loopback_connect_error(resource: &str, ip: IpAddr, port: u16) -> SidecarError {
13857    SidecarError::Execution(format!(
13858        "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}",
13859        loopback_cidr(ip)
13860    ))
13861}
13862
13863fn filter_dns_safe_ip_addrs(
13864    addresses: Vec<IpAddr>,
13865    hostname: &str,
13866) -> Result<Vec<IpAddr>, SidecarError> {
13867    let resource = format_dns_resource(hostname);
13868    let mut allowed = Vec::new();
13869    let mut blocked = None;
13870
13871    for ip in addresses {
13872        if let Some((cidr, label)) = restricted_non_loopback_ip_range(ip) {
13873            blocked.get_or_insert((ip, cidr, label));
13874            continue;
13875        }
13876        allowed.push(ip);
13877    }
13878
13879    if allowed.is_empty() {
13880        let (ip, cidr, label) = blocked.expect("blocked DNS results should capture a reason");
13881        return Err(blocked_dns_resolution_error(&resource, ip, cidr, label));
13882    }
13883
13884    Ok(allowed)
13885}
13886
13887fn loopback_connect_allowed(context: &JavascriptSocketPathContext, port: u16) -> bool {
13888    context.loopback_port_allowed(port)
13889}
13890
13891fn filter_tcp_connect_ip_addrs(
13892    addresses: Vec<IpAddr>,
13893    host: &str,
13894    port: u16,
13895    context: &JavascriptSocketPathContext,
13896) -> Result<Vec<IpAddr>, SidecarError> {
13897    let resource = format_tcp_resource(host, port);
13898    let mut allowed = Vec::new();
13899    let mut blocked = None;
13900
13901    for ip in addresses {
13902        if let Some((cidr, label)) = restricted_non_loopback_ip_range(ip) {
13903            blocked.get_or_insert_with(|| blocked_dns_resolution_error(&resource, ip, cidr, label));
13904            continue;
13905        }
13906        if is_loopback_ip(ip) && !loopback_connect_allowed(context, port) {
13907            blocked.get_or_insert_with(|| blocked_loopback_connect_error(&resource, ip, port));
13908            continue;
13909        }
13910        allowed.push(ip);
13911    }
13912
13913    if allowed.is_empty() {
13914        return Err(blocked.expect("blocked TCP connect results should capture a reason"));
13915    }
13916
13917    Ok(allowed)
13918}
13919
13920fn resolve_tcp_connect_addr<B>(
13921    bridge: &SharedBridge<B>,
13922    kernel: &SidecarKernel,
13923    vm_id: &str,
13924    dns: &VmDnsConfig,
13925    host: &str,
13926    port: u16,
13927    context: &JavascriptSocketPathContext,
13928) -> Result<ResolvedTcpConnectAddr, SidecarError>
13929where
13930    B: NativeSidecarBridge + Send + 'static,
13931    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
13932{
13933    let allowed = filter_tcp_connect_ip_addrs(
13934        resolve_dns_ip_addrs(
13935            bridge,
13936            kernel,
13937            vm_id,
13938            dns,
13939            host,
13940            DnsLookupPolicy::SkipPermissions,
13941        )?,
13942        host,
13943        port,
13944        context,
13945    )?;
13946    let ip = allowed
13947        .iter()
13948        .copied()
13949        .find(|candidate| {
13950            let family = JavascriptSocketFamily::from_ip(*candidate);
13951            context.translate_tcp_loopback_port(family, port).is_some()
13952        })
13953        // We do not implement Happy Eyeballs yet, so prefer IPv4 over a
13954        // verbatim IPv6-first DNS answer for general outbound TCP connects.
13955        .or_else(|| allowed.iter().copied().find(IpAddr::is_ipv4))
13956        .or_else(|| allowed.first().copied())
13957        .ok_or_else(|| {
13958            SidecarError::Execution(format!("failed to resolve TCP address {host}:{port}"))
13959        })?;
13960    let family = JavascriptSocketFamily::from_ip(ip);
13961    let translated_loopback_port = context.translate_tcp_loopback_port(family, port);
13962    let use_kernel_loopback = is_loopback_ip(ip) && translated_loopback_port == Some(port);
13963    let actual_port = if is_loopback_ip(ip) {
13964        translated_loopback_port.unwrap_or(port)
13965    } else {
13966        port
13967    };
13968    Ok(ResolvedTcpConnectAddr {
13969        actual_addr: SocketAddr::new(ip, actual_port),
13970        guest_remote_addr: SocketAddr::new(ip, port),
13971        use_kernel_loopback,
13972    })
13973}
13974
13975fn resolve_dns_ip_addrs<B>(
13976    bridge: &SharedBridge<B>,
13977    kernel: &SidecarKernel,
13978    vm_id: &str,
13979    dns: &VmDnsConfig,
13980    hostname: &str,
13981    policy: DnsLookupPolicy,
13982) -> Result<Vec<IpAddr>, SidecarError>
13983where
13984    B: NativeSidecarBridge + Send + 'static,
13985    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
13986{
13987    let resolution = match kernel.resolve_dns(hostname, policy) {
13988        Ok(resolution) => resolution,
13989        Err(error) => {
13990            let sidecar_error = kernel_error(error.clone());
13991            if error.code() != "EACCES" {
13992                emit_dns_resolution_failure_event(bridge, vm_id, hostname, dns, &sidecar_error);
13993            }
13994            return Err(sidecar_error);
13995        }
13996    };
13997    emit_dns_resolution_event(
13998        bridge,
13999        vm_id,
14000        hostname,
14001        resolution.source(),
14002        resolution.addresses(),
14003        dns,
14004    );
14005    Ok(resolution.addresses().to_vec())
14006}
14007
14008fn resolve_dns_records<B>(
14009    bridge: &SharedBridge<B>,
14010    kernel: &SidecarKernel,
14011    vm_id: &str,
14012    dns: &VmDnsConfig,
14013    hostname: &str,
14014    record_type: RecordType,
14015    policy: DnsLookupPolicy,
14016) -> Result<DnsRecordResolution, SidecarError>
14017where
14018    B: NativeSidecarBridge + Send + 'static,
14019    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
14020{
14021    let resolution = match kernel.resolve_dns_records(hostname, record_type, policy) {
14022        Ok(resolution) => resolution,
14023        Err(error) => {
14024            let sidecar_error = kernel_error(error.clone());
14025            if error.code() != "EACCES" {
14026                emit_dns_resolution_failure_event(bridge, vm_id, hostname, dns, &sidecar_error);
14027            }
14028            return Err(sidecar_error);
14029        }
14030    };
14031    emit_dns_record_resolution_event(bridge, vm_id, hostname, &resolution, dns);
14032    Ok(resolution)
14033}
14034
14035fn filter_dns_ip_addrs(
14036    addresses: Vec<IpAddr>,
14037    family: Option<u8>,
14038) -> Result<Vec<IpAddr>, SidecarError> {
14039    let filtered: Vec<_> = match family.unwrap_or(0) {
14040        0 => addresses,
14041        4 => addresses
14042            .into_iter()
14043            .filter(|ip| matches!(ip, IpAddr::V4(_)))
14044            .collect(),
14045        6 => addresses
14046            .into_iter()
14047            .filter(|ip| matches!(ip, IpAddr::V6(_)))
14048            .collect(),
14049        other => {
14050            return Err(SidecarError::InvalidState(format!(
14051                "unsupported dns family {other}"
14052            )));
14053        }
14054    };
14055
14056    if filtered.is_empty() {
14057        return Err(SidecarError::Execution(String::from(
14058            "failed to resolve DNS address for requested family",
14059        )));
14060    }
14061
14062    Ok(filtered)
14063}
14064
14065fn resolve_udp_bind_addr(
14066    host: &str,
14067    port: u16,
14068    family: JavascriptUdpFamily,
14069) -> Result<SocketAddr, SidecarError> {
14070    (host, port)
14071        .to_socket_addrs()
14072        .map_err(sidecar_net_error)?
14073        .find(|addr| family.matches_addr(addr))
14074        .ok_or_else(|| {
14075            SidecarError::Execution(format!(
14076                "failed to resolve {} UDP bind address {host}:{port}",
14077                family.socket_type()
14078            ))
14079        })
14080}
14081
14082fn resolve_udp_addr<B>(request: UdpRemoteAddrRequest<'_, B>) -> Result<SocketAddr, SidecarError>
14083where
14084    B: NativeSidecarBridge + Send + 'static,
14085    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
14086{
14087    let UdpRemoteAddrRequest {
14088        bridge,
14089        kernel,
14090        vm_id,
14091        dns,
14092        host,
14093        port,
14094        family,
14095        context,
14096    } = request;
14097    resolve_dns_ip_addrs(
14098        bridge,
14099        kernel,
14100        vm_id,
14101        dns,
14102        host,
14103        DnsLookupPolicy::SkipPermissions,
14104    )?
14105    .into_iter()
14106    .map(|ip| {
14107        let family_key = JavascriptSocketFamily::from_ip(ip);
14108        let actual_port = if is_loopback_ip(ip) {
14109            context
14110                .translate_udp_loopback_port(family_key, port)
14111                .unwrap_or(port)
14112        } else {
14113            port
14114        };
14115        SocketAddr::new(ip, actual_port)
14116    })
14117    .find(|addr| family.matches_addr(addr))
14118    .ok_or_else(|| {
14119        SidecarError::Execution(format!(
14120            "failed to resolve {} UDP address {host}:{port}",
14121            family.socket_type()
14122        ))
14123    })
14124}
14125
14126fn javascript_net_timeout_value() -> Value {
14127    Value::String(String::from(JAVASCRIPT_NET_TIMEOUT_SENTINEL))
14128}
14129
14130fn javascript_net_json_string(value: Value, label: &str) -> Result<Value, SidecarError> {
14131    serde_json::to_string(&value)
14132        .map(Value::String)
14133        .map_err(|error| {
14134            SidecarError::InvalidState(format!("failed to serialize {label} payload: {error}"))
14135        })
14136}
14137
14138fn javascript_net_read_value(
14139    event: Option<JavascriptTcpSocketEvent>,
14140) -> Result<Value, SidecarError> {
14141    match event {
14142        Some(JavascriptTcpSocketEvent::Data(chunk)) => Ok(Value::String(
14143            base64::engine::general_purpose::STANDARD.encode(chunk),
14144        )),
14145        Some(JavascriptTcpSocketEvent::End | JavascriptTcpSocketEvent::Close { .. }) => {
14146            Ok(Value::Null)
14147        }
14148        Some(JavascriptTcpSocketEvent::Error { code, message }) => {
14149            let detail = code.unwrap_or_else(|| String::from("socket read"));
14150            Err(SidecarError::Execution(format!("{detail}: {message}")))
14151        }
14152        None => Ok(javascript_net_timeout_value()),
14153    }
14154}
14155
14156fn io_error_code(error: &std::io::Error) -> Option<String> {
14157    match error.raw_os_error() {
14158        Some(libc::EADDRINUSE) => Some(String::from("EADDRINUSE")),
14159        Some(libc::EADDRNOTAVAIL) => Some(String::from("EADDRNOTAVAIL")),
14160        Some(libc::ECONNREFUSED) => Some(String::from("ECONNREFUSED")),
14161        Some(libc::ECONNRESET) => Some(String::from("ECONNRESET")),
14162        Some(libc::EINVAL) => Some(String::from("EINVAL")),
14163        Some(libc::EPIPE) => Some(String::from("EPIPE")),
14164        Some(libc::ETIMEDOUT) => Some(String::from("ETIMEDOUT")),
14165        Some(libc::EHOSTUNREACH) => Some(String::from("EHOSTUNREACH")),
14166        Some(libc::ENETUNREACH) => Some(String::from("ENETUNREACH")),
14167        _ => None,
14168    }
14169}
14170
14171fn sidecar_net_error(error: std::io::Error) -> SidecarError {
14172    let message = match io_error_code(&error) {
14173        Some(code) => format!("{code}: {error}"),
14174        None => error.to_string(),
14175    };
14176    SidecarError::Execution(message)
14177}
14178
14179fn tls_provider() -> Arc<rustls::crypto::CryptoProvider> {
14180    Arc::new(aws_lc_rs::default_provider())
14181}
14182
14183fn tls_local_certificates(
14184    options: &JavascriptTlsBridgeOptions,
14185) -> Result<Vec<Vec<u8>>, SidecarError> {
14186    let Some(certificates) = options.cert.as_ref() else {
14187        return Ok(Vec::new());
14188    };
14189    tls_material_entries(certificates)
14190}
14191
14192fn tls_material_entries(material: &JavascriptTlsMaterial) -> Result<Vec<Vec<u8>>, SidecarError> {
14193    match material {
14194        JavascriptTlsMaterial::Single(entry) => tls_data_value(entry).map(|value| vec![value]),
14195        JavascriptTlsMaterial::Many(entries) => entries.iter().map(tls_data_value).collect(),
14196    }
14197}
14198
14199fn tls_data_value(value: &JavascriptTlsDataValue) -> Result<Vec<u8>, SidecarError> {
14200    match value {
14201        JavascriptTlsDataValue::Buffer { data } => base64::engine::general_purpose::STANDARD
14202            .decode(data)
14203            .map_err(|error| {
14204                SidecarError::InvalidState(format!("TLS material contains invalid base64: {error}"))
14205            }),
14206        JavascriptTlsDataValue::String { data } => Ok(data.as_bytes().to_vec()),
14207    }
14208}
14209
14210fn tls_certificates_from_material(
14211    material: &JavascriptTlsMaterial,
14212) -> Result<Vec<CertificateDer<'static>>, SidecarError> {
14213    let mut certificates = Vec::new();
14214    for entry in tls_material_entries(material)? {
14215        let mut reader = std::io::BufReader::new(Cursor::new(entry.clone()));
14216        let parsed = rustls_pemfile::certs(&mut reader)
14217            .collect::<Result<Vec<_>, _>>()
14218            .map_err(sidecar_net_error)?;
14219        if parsed.is_empty() {
14220            certificates.push(CertificateDer::from(entry));
14221        } else {
14222            certificates.extend(parsed);
14223        }
14224    }
14225    if certificates.is_empty() {
14226        return Err(SidecarError::InvalidState(String::from(
14227            "TLS certificate material did not contain any certificates",
14228        )));
14229    }
14230    Ok(certificates)
14231}
14232
14233fn tls_private_key_from_material(
14234    material: &JavascriptTlsMaterial,
14235) -> Result<PrivateKeyDer<'static>, SidecarError> {
14236    for entry in tls_material_entries(material)? {
14237        let mut reader = std::io::BufReader::new(Cursor::new(entry));
14238        if let Some(key) = rustls_pemfile::private_key(&mut reader).map_err(sidecar_net_error)? {
14239            return Ok(key);
14240        }
14241    }
14242    Err(SidecarError::InvalidState(String::from(
14243        "TLS private key material did not contain a supported key",
14244    )))
14245}
14246
14247fn tls_root_store(options: &JavascriptTlsBridgeOptions) -> Result<RootCertStore, SidecarError> {
14248    let mut roots = RootCertStore::empty();
14249    if let Some(ca) = options.ca.as_ref() {
14250        for certificate in tls_certificates_from_material(ca)? {
14251            roots.add(certificate).map_err(|error| {
14252                SidecarError::InvalidState(format!("failed to add TLS CA certificate: {error}"))
14253            })?;
14254        }
14255        return Ok(roots);
14256    }
14257
14258    for certificate in rustls_native_certs::load_native_certs().certs {
14259        roots.add(certificate).map_err(|error| {
14260            SidecarError::InvalidState(format!(
14261                "failed to add native TLS certificate to root store: {error}"
14262            ))
14263        })?;
14264    }
14265    Ok(roots)
14266}
14267
14268fn build_client_tls_stream(
14269    stream: TcpStream,
14270    options: &JavascriptTlsBridgeOptions,
14271) -> Result<rustls::StreamOwned<ClientConnection, TcpStream>, SidecarError> {
14272    let config = build_client_tls_config(options)?;
14273    let server_name = options
14274        .servername
14275        .clone()
14276        .unwrap_or_else(|| String::from("localhost"));
14277    let server_name = ServerName::try_from(server_name)
14278        .map_err(|_| SidecarError::InvalidState(String::from("invalid TLS servername")))?;
14279    stream
14280        .set_read_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
14281        .map_err(sidecar_net_error)?;
14282    stream
14283        .set_write_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
14284        .map_err(sidecar_net_error)?;
14285    let mut tls_stream = rustls::StreamOwned::new(
14286        ClientConnection::new(Arc::new(config), server_name).map_err(|error| {
14287            SidecarError::Execution(format!("failed to start TLS client: {error}"))
14288        })?,
14289        stream,
14290    );
14291    while tls_stream.conn.is_handshaking() {
14292        tls_stream
14293            .conn
14294            .complete_io(&mut tls_stream.sock)
14295            .map_err(sidecar_net_error)?;
14296    }
14297    tls_stream
14298        .sock
14299        .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
14300        .map_err(sidecar_net_error)?;
14301    tls_stream
14302        .sock
14303        .set_write_timeout(None)
14304        .map_err(sidecar_net_error)?;
14305    Ok(tls_stream)
14306}
14307
14308fn build_client_loopback_tls_stream(
14309    transport: crate::state::LoopbackTlsEndpoint,
14310    options: &JavascriptTlsBridgeOptions,
14311) -> Result<rustls::StreamOwned<ClientConnection, crate::state::LoopbackTlsEndpoint>, SidecarError>
14312{
14313    let config = build_client_tls_config(options)?;
14314    let server_name = options
14315        .servername
14316        .clone()
14317        .unwrap_or_else(|| String::from("localhost"));
14318    let server_name = ServerName::try_from(server_name)
14319        .map_err(|_| SidecarError::InvalidState(String::from("invalid TLS servername")))?;
14320    let mut tls_stream = rustls::StreamOwned::new(
14321        ClientConnection::new(Arc::new(config), server_name).map_err(|error| {
14322            SidecarError::Execution(format!("failed to start TLS client: {error}"))
14323        })?,
14324        transport,
14325    );
14326    match tls_stream.conn.complete_io(&mut tls_stream.sock) {
14327        Ok(_) => {}
14328        Err(error)
14329            if matches!(
14330                error.kind(),
14331                std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
14332            ) => {}
14333        Err(error) => return Err(sidecar_net_error(error)),
14334    }
14335    Ok(tls_stream)
14336}
14337
14338fn build_client_tls_config(
14339    options: &JavascriptTlsBridgeOptions,
14340) -> Result<ClientConfig, SidecarError> {
14341    let provider = tls_provider();
14342    let builder = ClientConfig::builder_with_provider(provider.clone())
14343        .with_safe_default_protocol_versions()
14344        .map_err(|error| {
14345            SidecarError::InvalidState(format!("invalid TLS protocol config: {error}"))
14346        })?;
14347
14348    let mut config = if options.reject_unauthorized == Some(false) {
14349        let verifier = Arc::new(InsecureTlsVerifier {
14350            supported_schemes: provider
14351                .signature_verification_algorithms
14352                .supported_schemes(),
14353        });
14354        builder
14355            .dangerous()
14356            .with_custom_certificate_verifier(verifier)
14357            .with_no_client_auth()
14358    } else {
14359        builder
14360            .with_root_certificates(tls_root_store(options)?)
14361            .with_no_client_auth()
14362    };
14363
14364    if let Some(protocols) = options.alpn_protocols.as_ref() {
14365        config.alpn_protocols = protocols
14366            .iter()
14367            .map(|protocol| protocol.as_bytes().to_vec())
14368            .collect();
14369    }
14370    Ok(config)
14371}
14372
14373fn build_server_tls_stream(
14374    stream: TcpStream,
14375    options: &JavascriptTlsBridgeOptions,
14376) -> Result<rustls::StreamOwned<ServerConnection, TcpStream>, SidecarError> {
14377    let config = build_server_tls_config(options)?;
14378    stream
14379        .set_read_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
14380        .map_err(sidecar_net_error)?;
14381    stream
14382        .set_write_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
14383        .map_err(sidecar_net_error)?;
14384    let mut tls_stream = rustls::StreamOwned::new(
14385        ServerConnection::new(Arc::new(config)).map_err(|error| {
14386            SidecarError::Execution(format!("failed to start TLS server: {error}"))
14387        })?,
14388        stream,
14389    );
14390    while tls_stream.conn.is_handshaking() {
14391        tls_stream
14392            .conn
14393            .complete_io(&mut tls_stream.sock)
14394            .map_err(sidecar_net_error)?;
14395    }
14396    tls_stream
14397        .sock
14398        .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
14399        .map_err(sidecar_net_error)?;
14400    tls_stream
14401        .sock
14402        .set_write_timeout(None)
14403        .map_err(sidecar_net_error)?;
14404    Ok(tls_stream)
14405}
14406
14407fn build_server_loopback_tls_stream(
14408    transport: crate::state::LoopbackTlsEndpoint,
14409    options: &JavascriptTlsBridgeOptions,
14410) -> Result<rustls::StreamOwned<ServerConnection, crate::state::LoopbackTlsEndpoint>, SidecarError>
14411{
14412    let config = build_server_tls_config(options)?;
14413    Ok(rustls::StreamOwned::new(
14414        ServerConnection::new(Arc::new(config)).map_err(|error| {
14415            SidecarError::Execution(format!("failed to start TLS server: {error}"))
14416        })?,
14417        transport,
14418    ))
14419}
14420
14421fn build_server_tls_config(
14422    options: &JavascriptTlsBridgeOptions,
14423) -> Result<ServerConfig, SidecarError> {
14424    let certificates = tls_certificates_from_material(options.cert.as_ref().ok_or_else(|| {
14425        SidecarError::InvalidState(String::from("TLS server upgrade requires a certificate"))
14426    })?)?;
14427    let key = tls_private_key_from_material(options.key.as_ref().ok_or_else(|| {
14428        SidecarError::InvalidState(String::from("TLS server upgrade requires a private key"))
14429    })?)?;
14430
14431    let mut config = ServerConfig::builder_with_provider(tls_provider())
14432        .with_safe_default_protocol_versions()
14433        .map_err(|error| {
14434            SidecarError::InvalidState(format!("invalid TLS protocol config: {error}"))
14435        })?
14436        .with_no_client_auth()
14437        .with_single_cert(certificates, key)
14438        .map_err(|error| {
14439            SidecarError::InvalidState(format!("invalid TLS server config: {error}"))
14440        })?;
14441
14442    if let Some(protocols) = options.alpn_protocols.as_ref() {
14443        config.alpn_protocols = protocols
14444            .iter()
14445            .map(|protocol| protocol.as_bytes().to_vec())
14446            .collect();
14447    }
14448    Ok(config)
14449}
14450
14451fn tls_protocol_name(version: rustls::ProtocolVersion) -> String {
14452    match version {
14453        rustls::ProtocolVersion::TLSv1_2 => String::from("TLSv1.2"),
14454        rustls::ProtocolVersion::TLSv1_3 => String::from("TLSv1.3"),
14455        other => other
14456            .as_str()
14457            .map(str::to_owned)
14458            .unwrap_or_else(|| format!("{other:?}")),
14459    }
14460}
14461
14462fn tls_cipher_bridge_value(suite: rustls::SupportedCipherSuite) -> Value {
14463    tls_bridge_object(vec![
14464        (
14465            "name",
14466            suite
14467                .suite()
14468                .as_str()
14469                .map(|value| Value::String(value.to_owned()))
14470                .unwrap_or(Value::Null),
14471        ),
14472        (
14473            "standardName",
14474            suite
14475                .suite()
14476                .as_str()
14477                .map(|value| Value::String(value.to_owned()))
14478                .unwrap_or(Value::Null),
14479        ),
14480        (
14481            "version",
14482            Value::String(if suite.tls13().is_some() {
14483                String::from("TLSv1.3")
14484            } else {
14485                String::from("TLSv1.2")
14486            }),
14487        ),
14488    ])
14489}
14490
14491fn tls_certificate_bridge_value(certificate: &[u8], detailed: bool) -> Value {
14492    let mut fields = vec![("raw", tls_bridge_buffer_value(certificate))];
14493    if detailed {
14494        fields.push(("issuerCertificate", tls_bridge_undefined_value()));
14495    }
14496    tls_bridge_object(fields)
14497}
14498
14499fn tls_bridge_buffer_value(bytes: &[u8]) -> Value {
14500    json!({
14501        "type": "buffer",
14502        "data": base64::engine::general_purpose::STANDARD.encode(bytes),
14503    })
14504}
14505
14506fn tls_bridge_object(entries: Vec<(&str, Value)>) -> Value {
14507    let value = entries
14508        .into_iter()
14509        .map(|(key, value)| (key.to_owned(), value))
14510        .collect::<serde_json::Map<String, Value>>();
14511    json!({
14512        "type": "object",
14513        "id": 1,
14514        "value": value,
14515    })
14516}
14517
14518fn tls_bridge_undefined_value() -> Value {
14519    json!({
14520        "type": "undefined",
14521    })
14522}
14523
14524fn spawn_tcp_socket_reader(
14525    stream: TcpStream,
14526    sender: Sender<JavascriptTcpSocketEvent>,
14527    event_pusher: Arc<Mutex<Option<JavascriptSocketEventPusher>>>,
14528    tls_mode: Arc<AtomicBool>,
14529    saw_local_shutdown: Arc<AtomicBool>,
14530    saw_remote_end: Arc<AtomicBool>,
14531    close_notified: Arc<AtomicBool>,
14532) {
14533    thread::spawn(move || {
14534        let mut stream = stream;
14535        let mut buffer = vec![0_u8; 64 * 1024];
14536        loop {
14537            if tls_mode.load(Ordering::SeqCst) {
14538                break;
14539            }
14540            match stream.read(&mut buffer) {
14541                Ok(0) => {
14542                    saw_remote_end.store(true, Ordering::SeqCst);
14543                    let _ = sender.send(JavascriptTcpSocketEvent::End);
14544                    push_socket_event(&event_pusher, "end");
14545                    if saw_local_shutdown.load(Ordering::SeqCst)
14546                        && !close_notified.swap(true, Ordering::SeqCst)
14547                    {
14548                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
14549                        push_socket_event(&event_pusher, "close");
14550                    }
14551                    break;
14552                }
14553                Ok(bytes_read) => {
14554                    if sender
14555                        .send(JavascriptTcpSocketEvent::Data(
14556                            buffer[..bytes_read].to_vec(),
14557                        ))
14558                        .is_err()
14559                    {
14560                        break;
14561                    }
14562                    push_socket_event(&event_pusher, "data");
14563                }
14564                Err(error)
14565                    if matches!(
14566                        error.kind(),
14567                        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
14568                    ) =>
14569                {
14570                    continue;
14571                }
14572                Err(error) => {
14573                    let code = io_error_code(&error);
14574                    let _ = sender.send(JavascriptTcpSocketEvent::Error {
14575                        code,
14576                        message: error.to_string(),
14577                    });
14578                    push_socket_event(&event_pusher, "error");
14579                    if !close_notified.swap(true, Ordering::SeqCst) {
14580                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
14581                        push_socket_event(&event_pusher, "close");
14582                    }
14583                    break;
14584                }
14585            }
14586        }
14587    });
14588}
14589
14590fn push_socket_event(
14591    event_pusher: &Arc<Mutex<Option<JavascriptSocketEventPusher>>>,
14592    event: &'static str,
14593) {
14594    NET_TCP_TRACE_COUNTERS
14595        .socket_read_push_attempts
14596        .fetch_add(1, Ordering::Relaxed);
14597    let target = event_pusher.lock().ok().and_then(|guard| guard.clone());
14598    let Some(target) = target else {
14599        NET_TCP_TRACE_COUNTERS
14600            .socket_read_push_missing
14601            .fetch_add(1, Ordering::Relaxed);
14602        return;
14603    };
14604    let payload = v8_runtime::json_to_cbor_payload(&json!({
14605        "socketId": target.socket_id,
14606        "event": event,
14607    }))
14608    .unwrap_or_default();
14609    match target.session.send_stream_event("net_socket", payload) {
14610        Ok(()) => {
14611            NET_TCP_TRACE_COUNTERS
14612                .socket_read_push_sent
14613                .fetch_add(1, Ordering::Relaxed);
14614        }
14615        Err(_) => {
14616            NET_TCP_TRACE_COUNTERS
14617                .socket_read_push_errors
14618                .fetch_add(1, Ordering::Relaxed);
14619        }
14620    }
14621}
14622
14623fn send_tls_socket_error_and_close(
14624    sender: &Sender<JavascriptTcpSocketEvent>,
14625    event_pusher: &Arc<Mutex<Option<JavascriptSocketEventPusher>>>,
14626    close_notified: &Arc<AtomicBool>,
14627    code: Option<String>,
14628    message: String,
14629) {
14630    let _ = sender.send(JavascriptTcpSocketEvent::Error { code, message });
14631    push_socket_event(event_pusher, "error");
14632    if !close_notified.swap(true, Ordering::SeqCst) {
14633        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
14634        push_socket_event(event_pusher, "close");
14635    }
14636}
14637
14638fn fail_loopback_tls_pending_reader(
14639    pending_write: &LoopbackTlsPendingWriteHandle,
14640    sender: &Sender<JavascriptTcpSocketEvent>,
14641    event_pusher: &Arc<Mutex<Option<JavascriptSocketEventPusher>>>,
14642    close_notified: &Arc<AtomicBool>,
14643    code: Option<String>,
14644    message: String,
14645) {
14646    pending_write.mark_failed(message.clone());
14647    send_tls_socket_error_and_close(sender, event_pusher, close_notified, code, message);
14648}
14649
14650fn flush_loopback_tls_pending_writes(
14651    tls_stream: &Arc<Mutex<Option<ActiveTlsStream>>>,
14652    pending_write: &LoopbackTlsPendingWriteHandle,
14653    sender: &Sender<JavascriptTcpSocketEvent>,
14654    event_pusher: &Arc<Mutex<Option<JavascriptSocketEventPusher>>>,
14655    close_notified: &Arc<AtomicBool>,
14656) -> bool {
14657    loop {
14658        let bytes = match pending_write.take_buffer_for_flush() {
14659            Ok(bytes) => bytes,
14660            Err(error) => {
14661                fail_loopback_tls_pending_reader(
14662                    pending_write,
14663                    sender,
14664                    event_pusher,
14665                    close_notified,
14666                    Some(String::from("EIO")),
14667                    error.to_string(),
14668                );
14669                return false;
14670            }
14671        };
14672
14673        if let Some(bytes) = bytes {
14674            let write_result = {
14675                let mut guard = match tls_stream.lock() {
14676                    Ok(guard) => guard,
14677                    Err(_) => {
14678                        pending_write.finish_flush();
14679                        fail_loopback_tls_pending_reader(
14680                            pending_write,
14681                            sender,
14682                            event_pusher,
14683                            close_notified,
14684                            Some(String::from("EIO")),
14685                            String::from("TLS stream lock poisoned while flushing pending writes"),
14686                        );
14687                        return false;
14688                    }
14689                };
14690                let Some(stream) = guard.as_mut() else {
14691                    pending_write.finish_flush();
14692                    fail_loopback_tls_pending_reader(
14693                        pending_write,
14694                        sender,
14695                        event_pusher,
14696                        close_notified,
14697                        Some(String::from("EIO")),
14698                        String::from("TLS stream missing while flushing pending writes"),
14699                    );
14700                    return false;
14701                };
14702                stream.write_all(&bytes)
14703            };
14704            pending_write.finish_flush();
14705            if let Err(error) = write_result {
14706                fail_loopback_tls_pending_reader(
14707                    pending_write,
14708                    sender,
14709                    event_pusher,
14710                    close_notified,
14711                    Some(String::from("EIO")),
14712                    format!("loopback TLS pending write flush failed: {error}"),
14713                );
14714                return false;
14715            }
14716            continue;
14717        }
14718
14719        let should_shutdown = match pending_write.take_deferred_shutdown_write() {
14720            Ok(should_shutdown) => should_shutdown,
14721            Err(error) => {
14722                fail_loopback_tls_pending_reader(
14723                    pending_write,
14724                    sender,
14725                    event_pusher,
14726                    close_notified,
14727                    Some(String::from("EIO")),
14728                    error.to_string(),
14729                );
14730                return false;
14731            }
14732        };
14733        if should_shutdown {
14734            let shutdown_result = {
14735                let mut guard = match tls_stream.lock() {
14736                    Ok(guard) => guard,
14737                    Err(_) => {
14738                        fail_loopback_tls_pending_reader(
14739                            pending_write,
14740                            sender,
14741                            event_pusher,
14742                            close_notified,
14743                            Some(String::from("EIO")),
14744                            String::from("TLS stream lock poisoned during deferred shutdown"),
14745                        );
14746                        return false;
14747                    }
14748                };
14749                let Some(stream) = guard.as_mut() else {
14750                    fail_loopback_tls_pending_reader(
14751                        pending_write,
14752                        sender,
14753                        event_pusher,
14754                        close_notified,
14755                        Some(String::from("EIO")),
14756                        String::from("TLS stream missing during deferred shutdown"),
14757                    );
14758                    return false;
14759                };
14760                stream
14761                    .send_close_notify()
14762                    .and_then(|_| stream.shutdown_write())
14763            };
14764            if let Err(error) = shutdown_result {
14765                fail_loopback_tls_pending_reader(
14766                    pending_write,
14767                    sender,
14768                    event_pusher,
14769                    close_notified,
14770                    Some(String::from("EIO")),
14771                    format!("loopback TLS deferred shutdown failed: {error}"),
14772                );
14773                return false;
14774            }
14775            continue;
14776        }
14777
14778        return true;
14779    }
14780}
14781
14782fn spawn_tls_socket_reader(
14783    tls_stream: Arc<Mutex<Option<ActiveTlsStream>>>,
14784    loopback_pending_write: Option<LoopbackTlsPendingWriteHandle>,
14785    sender: Sender<JavascriptTcpSocketEvent>,
14786    event_pusher: Arc<Mutex<Option<JavascriptSocketEventPusher>>>,
14787    saw_local_shutdown: Arc<AtomicBool>,
14788    saw_remote_end: Arc<AtomicBool>,
14789    close_notified: Arc<AtomicBool>,
14790) {
14791    thread::spawn(move || {
14792        let mut buffer = vec![0_u8; 64 * 1024];
14793        loop {
14794            if let Some(pending_write) = loopback_pending_write.as_ref() {
14795                let is_handshaking = {
14796                    let mut guard = match tls_stream.lock() {
14797                        Ok(guard) => guard,
14798                        Err(_) => {
14799                            pending_write.mark_failed("TLS stream lock poisoned");
14800                            return;
14801                        }
14802                    };
14803                    let Some(stream) = guard.as_mut() else {
14804                        pending_write.mark_failed("TLS stream missing");
14805                        return;
14806                    };
14807                    if stream.is_loopback() {
14808                        let is_handshaking = stream.is_handshaking();
14809                        stream.set_loopback_poll_timeout(LOOPBACK_TLS_HANDSHAKE_POLL_TIMEOUT);
14810                        is_handshaking
14811                    } else {
14812                        false
14813                    }
14814                };
14815
14816                if is_handshaking {
14817                    if pending_write.handshake_started_at.elapsed() >= TLS_HANDSHAKE_TIMEOUT {
14818                        fail_loopback_tls_pending_reader(
14819                            pending_write,
14820                            &sender,
14821                            &event_pusher,
14822                            &close_notified,
14823                            Some(String::from("ETIMEDOUT")),
14824                            format!(
14825                                "loopback TLS handshake timed out after {}ms",
14826                                TLS_HANDSHAKE_TIMEOUT.as_millis()
14827                            ),
14828                        );
14829                        return;
14830                    }
14831                } else {
14832                    pending_write
14833                        .tls_handshake_complete
14834                        .store(true, Ordering::SeqCst);
14835                    if !flush_loopback_tls_pending_writes(
14836                        &tls_stream,
14837                        pending_write,
14838                        &sender,
14839                        &event_pusher,
14840                        &close_notified,
14841                    ) {
14842                        return;
14843                    }
14844                }
14845            }
14846
14847            let read_result = {
14848                let mut guard = match tls_stream.lock() {
14849                    Ok(guard) => guard,
14850                    Err(_) => {
14851                        if let Some(pending_write) = loopback_pending_write.as_ref() {
14852                            pending_write.mark_failed("TLS stream lock poisoned");
14853                        }
14854                        return;
14855                    }
14856                };
14857                let Some(stream) = guard.as_mut() else {
14858                    if let Some(pending_write) = loopback_pending_write.as_ref() {
14859                        pending_write.mark_failed("TLS stream missing");
14860                    }
14861                    return;
14862                };
14863                stream.read(&mut buffer)
14864            };
14865
14866            match read_result {
14867                Ok(0) => {
14868                    if let Some(pending_write) = loopback_pending_write.as_ref() {
14869                        pending_write.fail_if_pending(
14870                            "loopback TLS peer closed before buffered writes flushed",
14871                        );
14872                    }
14873                    saw_remote_end.store(true, Ordering::SeqCst);
14874                    let _ = sender.send(JavascriptTcpSocketEvent::End);
14875                    push_socket_event(&event_pusher, "end");
14876                    if saw_local_shutdown.load(Ordering::SeqCst)
14877                        && !close_notified.swap(true, Ordering::SeqCst)
14878                    {
14879                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
14880                        push_socket_event(&event_pusher, "close");
14881                    }
14882                    break;
14883                }
14884                Ok(bytes_read) => {
14885                    if sender
14886                        .send(JavascriptTcpSocketEvent::Data(
14887                            buffer[..bytes_read].to_vec(),
14888                        ))
14889                        .is_err()
14890                    {
14891                        break;
14892                    }
14893                    push_socket_event(&event_pusher, "data");
14894                }
14895                Err(error)
14896                    if matches!(
14897                        error.kind(),
14898                        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
14899                    ) =>
14900                {
14901                    // The TLS reader and writer share one rustls stream mutex. Yield after
14902                    // timed-out reads so request writes can acquire the lock promptly.
14903                    std::thread::sleep(Duration::from_millis(1));
14904                    continue;
14905                }
14906                Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => {
14907                    if let Some(pending_write) = loopback_pending_write.as_ref() {
14908                        pending_write.fail_if_pending(
14909                            "loopback TLS peer closed before buffered writes flushed",
14910                        );
14911                    }
14912                    saw_remote_end.store(true, Ordering::SeqCst);
14913                    let _ = sender.send(JavascriptTcpSocketEvent::End);
14914                    push_socket_event(&event_pusher, "end");
14915                    if saw_local_shutdown.load(Ordering::SeqCst)
14916                        && !close_notified.swap(true, Ordering::SeqCst)
14917                    {
14918                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
14919                        push_socket_event(&event_pusher, "close");
14920                    }
14921                    break;
14922                }
14923                Err(error) => {
14924                    if let Some(pending_write) = loopback_pending_write.as_ref() {
14925                        pending_write.mark_failed(format!(
14926                            "loopback TLS reader failed before buffered writes flushed: {error}"
14927                        ));
14928                    }
14929                    let code = io_error_code(&error);
14930                    let _ = sender.send(JavascriptTcpSocketEvent::Error {
14931                        code,
14932                        message: error.to_string(),
14933                    });
14934                    push_socket_event(&event_pusher, "error");
14935                    if !close_notified.swap(true, Ordering::SeqCst) {
14936                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
14937                        push_socket_event(&event_pusher, "close");
14938                    }
14939                    break;
14940                }
14941            }
14942        }
14943    });
14944}
14945
14946fn spawn_unix_socket_reader(
14947    stream: UnixStream,
14948    sender: Sender<JavascriptTcpSocketEvent>,
14949    event_pusher: Arc<Mutex<Option<JavascriptSocketEventPusher>>>,
14950    saw_local_shutdown: Arc<AtomicBool>,
14951    saw_remote_end: Arc<AtomicBool>,
14952    close_notified: Arc<AtomicBool>,
14953) {
14954    thread::spawn(move || {
14955        let mut stream = stream;
14956        let mut buffer = vec![0_u8; 64 * 1024];
14957        loop {
14958            match stream.read(&mut buffer) {
14959                Ok(0) => {
14960                    saw_remote_end.store(true, Ordering::SeqCst);
14961                    let _ = sender.send(JavascriptTcpSocketEvent::End);
14962                    push_socket_event(&event_pusher, "end");
14963                    if saw_local_shutdown.load(Ordering::SeqCst)
14964                        && !close_notified.swap(true, Ordering::SeqCst)
14965                    {
14966                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
14967                        push_socket_event(&event_pusher, "close");
14968                    }
14969                    break;
14970                }
14971                Ok(bytes_read) => {
14972                    if sender
14973                        .send(JavascriptTcpSocketEvent::Data(
14974                            buffer[..bytes_read].to_vec(),
14975                        ))
14976                        .is_err()
14977                    {
14978                        break;
14979                    }
14980                    push_socket_event(&event_pusher, "data");
14981                }
14982                Err(error) => {
14983                    let code = io_error_code(&error);
14984                    let _ = sender.send(JavascriptTcpSocketEvent::Error {
14985                        code,
14986                        message: error.to_string(),
14987                    });
14988                    push_socket_event(&event_pusher, "error");
14989                    if !close_notified.swap(true, Ordering::SeqCst) {
14990                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
14991                        push_socket_event(&event_pusher, "close");
14992                    }
14993                    break;
14994                }
14995            }
14996        }
14997    });
14998}
14999
15000/// Unblock a guest thread parked in a deferred `__kernel_stdin_read` /
15001/// `__kernel_poll` sync RPC. Isolate termination cannot interrupt the native
15002/// bridge wait, so teardown must answer the parked RPC BEFORE dropping the
15003/// execution (drop joins the guest thread) or cleanup deadlocks against it.
15004fn flush_parked_kernel_wait_rpc(process: &mut ActiveProcess) {
15005    if let Some((request, _)) = process.deferred_kernel_wait_rpc.take() {
15006        let _ = process
15007            .execution
15008            .respond_javascript_sync_rpc_error(request.id, "EINTR", "process teardown")
15009            .or_else(ignore_stale_javascript_sync_rpc_response);
15010    }
15011}
15012
15013fn terminate_child_process_tree(
15014    kernel: &mut SidecarKernel,
15015    process: &mut ActiveProcess,
15016    kernel_readiness: &KernelSocketReadinessRegistry,
15017) {
15018    flush_parked_kernel_wait_rpc(process);
15019    let sqlite_database_ids = process.sqlite_databases.keys().copied().collect::<Vec<_>>();
15020    for database_id in sqlite_database_ids {
15021        let _ = close_sqlite_database(kernel, process, database_id);
15022    }
15023    process.sqlite_statements.clear();
15024    process.http_servers.clear();
15025    process.pending_http_requests.clear();
15026    if let Ok(mut http2) = process.http2.shared.lock() {
15027        let sessions = http2.sessions.values().cloned().collect::<Vec<_>>();
15028        http2.server_events.clear();
15029        http2.session_events.clear();
15030        http2.streams.clear();
15031        http2.servers.clear();
15032        http2.sessions.clear();
15033        http2.event_session = None;
15034        drop(http2);
15035        for session in sessions {
15036            let (respond_to, _rx) = mpsc::channel();
15037            let _ = session.command_tx.send(Http2SessionCommand::Close {
15038                abrupt: true,
15039                respond_to,
15040            });
15041        }
15042    }
15043
15044    let listener_ids = process.tcp_listeners.keys().cloned().collect::<Vec<_>>();
15045    for listener_id in listener_ids {
15046        if let Some(listener) = process.tcp_listeners.remove(&listener_id) {
15047            unregister_kernel_readiness_target(kernel_readiness, listener.kernel_socket_id);
15048            let _ = listener.close(kernel, process.kernel_pid);
15049        }
15050    }
15051
15052    let sockets = process.tcp_sockets.keys().cloned().collect::<Vec<_>>();
15053    for socket_id in sockets {
15054        if let Some(socket) = process.tcp_sockets.remove(&socket_id) {
15055            unregister_kernel_readiness_target(kernel_readiness, socket.kernel_socket_id);
15056            let _ = socket.close(kernel, process.kernel_pid);
15057        }
15058    }
15059
15060    let unix_listener_ids = process.unix_listeners.keys().cloned().collect::<Vec<_>>();
15061    for listener_id in unix_listener_ids {
15062        if let Some(listener) = process.unix_listeners.remove(&listener_id) {
15063            let _ = listener.close();
15064        }
15065    }
15066
15067    let unix_sockets = process.unix_sockets.keys().cloned().collect::<Vec<_>>();
15068    for socket_id in unix_sockets {
15069        if let Some(socket) = process.unix_sockets.remove(&socket_id) {
15070            let _ = socket.close();
15071        }
15072    }
15073
15074    let udp_socket_ids = process.udp_sockets.keys().cloned().collect::<Vec<_>>();
15075    for socket_id in udp_socket_ids {
15076        if let Some(mut socket) = process.udp_sockets.remove(&socket_id) {
15077            unregister_kernel_readiness_target(kernel_readiness, socket.kernel_socket_id);
15078            socket.close(kernel, process.kernel_pid);
15079        }
15080    }
15081
15082    let child_ids = process.child_processes.keys().cloned().collect::<Vec<_>>();
15083    for child_id in child_ids {
15084        let Some(mut child) = process.child_processes.remove(&child_id) else {
15085            continue;
15086        };
15087        terminate_child_process_tree(kernel, &mut child, kernel_readiness);
15088        let _ = kernel.kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, SIGTERM);
15089        let _ = signal_runtime_process(child.execution.child_pid(), SIGTERM);
15090        child.kernel_handle.finish(0);
15091        let _ = kernel.wait_and_reap(child.kernel_pid);
15092    }
15093}
15094
15095fn service_javascript_sqlite_sync_rpc(
15096    kernel: &mut SidecarKernel,
15097    process: &mut ActiveProcess,
15098    request: &JavascriptSyncRpcRequest,
15099) -> Result<Value, SidecarError> {
15100    match request.method.as_str() {
15101        "sqlite.constants" => Ok(json!({})),
15102        "sqlite.open" => sqlite_open_database(kernel, process, request),
15103        "sqlite.close" => {
15104            let database_id =
15105                javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.close database id")?;
15106            close_sqlite_database(kernel, process, database_id)?;
15107            Ok(Value::Null)
15108        }
15109        "sqlite.exec" => sqlite_exec_database(kernel, process, request),
15110        "sqlite.query" => sqlite_query_database(process, request),
15111        "sqlite.prepare" => sqlite_prepare_statement(process, request),
15112        "sqlite.location" => {
15113            let database_id =
15114                javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.location database id")?;
15115            let database = sqlite_database(process, database_id)?;
15116            Ok(database
15117                .vm_path
15118                .as_ref()
15119                .map(|path| Value::String(path.clone()))
15120                .unwrap_or(Value::Null))
15121        }
15122        "sqlite.checkpoint" => {
15123            let database_id =
15124                javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.checkpoint database id")?;
15125            let kernel_pid = process.kernel_pid;
15126            let database = sqlite_database_mut(process, database_id)?;
15127            sqlite_sync_database(kernel, kernel_pid, database)?;
15128            Ok(Value::Null)
15129        }
15130        "sqlite.statement.run" => sqlite_run_statement(kernel, process, request),
15131        "sqlite.statement.get" => sqlite_get_statement(process, request),
15132        "sqlite.statement.all" | "sqlite.statement.iterate" => {
15133            sqlite_all_statement(process, request)
15134        }
15135        "sqlite.statement.columns" => sqlite_statement_columns(process, request),
15136        "sqlite.statement.setReturnArrays" => {
15137            let statement_id = javascript_sync_rpc_arg_u64(
15138                &request.args,
15139                0,
15140                "sqlite.statement.setReturnArrays statement id",
15141            )?;
15142            let enabled = javascript_sync_rpc_arg_bool(
15143                &request.args,
15144                1,
15145                "sqlite.statement.setReturnArrays enabled",
15146            )?;
15147            sqlite_statement_mut(process, statement_id)?.return_arrays = enabled;
15148            Ok(Value::Null)
15149        }
15150        "sqlite.statement.setReadBigInts" => {
15151            let statement_id = javascript_sync_rpc_arg_u64(
15152                &request.args,
15153                0,
15154                "sqlite.statement.setReadBigInts statement id",
15155            )?;
15156            let enabled = javascript_sync_rpc_arg_bool(
15157                &request.args,
15158                1,
15159                "sqlite.statement.setReadBigInts enabled",
15160            )?;
15161            sqlite_statement_mut(process, statement_id)?.read_bigints = enabled;
15162            Ok(Value::Null)
15163        }
15164        "sqlite.statement.setAllowBareNamedParameters" => {
15165            let statement_id = javascript_sync_rpc_arg_u64(
15166                &request.args,
15167                0,
15168                "sqlite.statement.setAllowBareNamedParameters statement id",
15169            )?;
15170            let enabled = javascript_sync_rpc_arg_bool(
15171                &request.args,
15172                1,
15173                "sqlite.statement.setAllowBareNamedParameters enabled",
15174            )?;
15175            sqlite_statement_mut(process, statement_id)?.allow_bare_named_parameters = enabled;
15176            Ok(Value::Null)
15177        }
15178        "sqlite.statement.setAllowUnknownNamedParameters" => {
15179            let statement_id = javascript_sync_rpc_arg_u64(
15180                &request.args,
15181                0,
15182                "sqlite.statement.setAllowUnknownNamedParameters statement id",
15183            )?;
15184            let enabled = javascript_sync_rpc_arg_bool(
15185                &request.args,
15186                1,
15187                "sqlite.statement.setAllowUnknownNamedParameters enabled",
15188            )?;
15189            sqlite_statement_mut(process, statement_id)?.allow_unknown_named_parameters = enabled;
15190            Ok(Value::Null)
15191        }
15192        "sqlite.statement.finalize" => {
15193            let statement_id = javascript_sync_rpc_arg_u64(
15194                &request.args,
15195                0,
15196                "sqlite.statement.finalize statement id",
15197            )?;
15198            process
15199                .sqlite_statements
15200                .remove(&statement_id)
15201                .ok_or_else(|| {
15202                    SidecarError::InvalidState(format!(
15203                        "sqlite statement handle not found: {statement_id}"
15204                    ))
15205                })?;
15206            Ok(Value::Null)
15207        }
15208        other => Err(SidecarError::InvalidState(format!(
15209            "unsupported JavaScript sqlite sync RPC method {other}"
15210        ))),
15211    }
15212}
15213
15214fn sqlite_open_database(
15215    kernel: &mut SidecarKernel,
15216    process: &mut ActiveProcess,
15217    request: &JavascriptSyncRpcRequest,
15218) -> Result<Value, SidecarError> {
15219    ensure_per_process_state_handle_capacity(process.sqlite_databases.len(), "sqlite database")?;
15220    let path = request.args.first().and_then(Value::as_str);
15221    let vm_path = path.filter(|value| !value.is_empty() && *value != ":memory:");
15222    let options = request.args.get(1);
15223    let read_only = sqlite_option_bool(options, "readOnly").unwrap_or(false);
15224    let create = sqlite_option_bool(options, "create").unwrap_or(!read_only);
15225    let timeout_ms = sqlite_option_u64(options, "timeout");
15226
15227    process.next_sqlite_database_id += 1;
15228    let database_id = process.next_sqlite_database_id;
15229
15230    let host_path = if vm_path.is_some() {
15231        Some(
15232            std::env::temp_dir()
15233                .join(format!(
15234                    "agentos-native-sidecar-sqlite-{}-{database_id}",
15235                    process.kernel_pid
15236                ))
15237                .join("database.sqlite"),
15238        )
15239    } else {
15240        None
15241    };
15242
15243    if let Some(host_path) = host_path.as_ref() {
15244        if let Some(parent) = host_path.parent() {
15245            fs::create_dir_all(parent).map_err(|error| {
15246                SidecarError::Io(format!(
15247                    "failed to prepare sqlite temp directory {}: {error}",
15248                    parent.display()
15249                ))
15250            })?;
15251        }
15252    }
15253
15254    if let (Some(vm_path), Some(host_path)) = (vm_path, host_path.as_ref()) {
15255        if kernel
15256            .exists_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, vm_path)
15257            .map_err(kernel_error)?
15258        {
15259            let contents = kernel
15260                .read_file_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, vm_path)
15261                .map_err(kernel_error)?;
15262            fs::write(host_path, contents).map_err(|error| {
15263                SidecarError::Io(format!(
15264                    "failed to materialize sqlite database {}: {error}",
15265                    host_path.display()
15266                ))
15267            })?;
15268        } else if read_only && !create {
15269            return Err(SidecarError::InvalidState(format!(
15270                "sqlite database does not exist: {vm_path}"
15271            )));
15272        }
15273    }
15274
15275    let target = host_path
15276        .as_ref()
15277        .map(|path| path.to_string_lossy().into_owned())
15278        .unwrap_or_else(|| String::from(":memory:"));
15279    let mut flags = if read_only {
15280        SqliteOpenFlags::SQLITE_OPEN_READ_ONLY
15281    } else {
15282        SqliteOpenFlags::SQLITE_OPEN_READ_WRITE
15283    };
15284    if create && !read_only {
15285        flags |= SqliteOpenFlags::SQLITE_OPEN_CREATE;
15286    }
15287
15288    let connection = SqliteConnection::open_with_flags(&target, flags).map_err(|error| {
15289        SidecarError::InvalidState(format!(
15290            "sqlite database open failed for {}: {error}",
15291            vm_path.unwrap_or(":memory:")
15292        ))
15293    })?;
15294    if let Some(timeout_ms) = timeout_ms {
15295        connection
15296            .busy_timeout(Duration::from_millis(timeout_ms))
15297            .map_err(sqlite_error)?;
15298    }
15299    if host_path.is_some() && !read_only {
15300        let _ = connection.pragma_update(None, "journal_mode", "WAL");
15301    }
15302
15303    process.sqlite_databases.insert(
15304        database_id,
15305        ActiveSqliteDatabase {
15306            connection,
15307            host_path,
15308            vm_path: vm_path.map(String::from),
15309            dirty: false,
15310            transaction_depth: 0,
15311            read_only,
15312        },
15313    );
15314
15315    Ok(json!(database_id))
15316}
15317
15318fn sqlite_exec_database(
15319    kernel: &mut SidecarKernel,
15320    process: &mut ActiveProcess,
15321    request: &JavascriptSyncRpcRequest,
15322) -> Result<Value, SidecarError> {
15323    let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.exec database id")?;
15324    let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.exec sql")?;
15325    let kernel_pid = process.kernel_pid;
15326    let database = sqlite_database_mut(process, database_id)?;
15327    let before = database.connection.total_changes();
15328    database
15329        .connection
15330        .execute_batch(sql)
15331        .map_err(sqlite_error)?;
15332    mark_sqlite_mutation(database, sql);
15333    sqlite_sync_database(kernel, kernel_pid, database)?;
15334    Ok(json!(database
15335        .connection
15336        .total_changes()
15337        .saturating_sub(before)))
15338}
15339
15340fn sqlite_query_database(
15341    process: &mut ActiveProcess,
15342    request: &JavascriptSyncRpcRequest,
15343) -> Result<Value, SidecarError> {
15344    let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.query database id")?;
15345    let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.query sql")?;
15346    let params = request.args.get(2);
15347    let options = request.args.get(3);
15348    let return_arrays = sqlite_option_bool(options, "returnArrays").unwrap_or(false);
15349    let read_bigints = sqlite_option_bool(options, "readBigInts").unwrap_or(false);
15350    let database = sqlite_database_mut(process, database_id)?;
15351    sqlite_query_rows(
15352        &mut database.connection,
15353        sql,
15354        params,
15355        return_arrays,
15356        read_bigints,
15357        true,
15358        false,
15359    )
15360}
15361
15362fn sqlite_prepare_statement(
15363    process: &mut ActiveProcess,
15364    request: &JavascriptSyncRpcRequest,
15365) -> Result<Value, SidecarError> {
15366    ensure_per_process_state_handle_capacity(process.sqlite_statements.len(), "sqlite statement")?;
15367    let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.prepare database id")?;
15368    let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.prepare sql")?;
15369    let _ = sqlite_database(process, database_id)?;
15370    process.next_sqlite_statement_id += 1;
15371    let statement_id = process.next_sqlite_statement_id;
15372    process.sqlite_statements.insert(
15373        statement_id,
15374        ActiveSqliteStatement {
15375            database_id,
15376            sql: sql.to_owned(),
15377            return_arrays: false,
15378            read_bigints: false,
15379            allow_bare_named_parameters: false,
15380            allow_unknown_named_parameters: false,
15381        },
15382    );
15383    Ok(json!(statement_id))
15384}
15385
15386fn sqlite_run_statement(
15387    kernel: &mut SidecarKernel,
15388    process: &mut ActiveProcess,
15389    request: &JavascriptSyncRpcRequest,
15390) -> Result<Value, SidecarError> {
15391    let statement_id =
15392        javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.run statement id")?;
15393    let params = request.args.get(1);
15394    let statement_state = sqlite_statement(process, statement_id)?.clone();
15395    let kernel_pid = process.kernel_pid;
15396    let database = sqlite_database_mut(process, statement_state.database_id)?;
15397    let before = database.connection.total_changes();
15398    {
15399        let mut statement = database
15400            .connection
15401            .prepare(&statement_state.sql)
15402            .map_err(sqlite_error)?;
15403        bind_sqlite_parameters(
15404            &mut statement,
15405            params,
15406            statement_state.allow_bare_named_parameters,
15407            statement_state.allow_unknown_named_parameters,
15408        )?;
15409        statement.raw_execute().map_err(sqlite_error)?;
15410    }
15411    let changes = database.connection.total_changes().saturating_sub(before);
15412    let last_insert_rowid = database.connection.last_insert_rowid();
15413    mark_sqlite_mutation(database, &statement_state.sql);
15414    sqlite_sync_database(kernel, kernel_pid, database)?;
15415    let result = json!({
15416        "changes": changes,
15417        "lastInsertRowid": encode_sqlite_integer(last_insert_rowid, true),
15418    });
15419    Ok(result)
15420}
15421
15422fn sqlite_get_statement(
15423    process: &mut ActiveProcess,
15424    request: &JavascriptSyncRpcRequest,
15425) -> Result<Value, SidecarError> {
15426    let statement_id =
15427        javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.get statement id")?;
15428    let params = request.args.get(1);
15429    let statement_state = sqlite_statement(process, statement_id)?.clone();
15430    let database = sqlite_database_mut(process, statement_state.database_id)?;
15431    let rows = sqlite_query_rows(
15432        &mut database.connection,
15433        &statement_state.sql,
15434        params,
15435        statement_state.return_arrays,
15436        statement_state.read_bigints,
15437        statement_state.allow_bare_named_parameters,
15438        statement_state.allow_unknown_named_parameters,
15439    )?;
15440    Ok(rows
15441        .as_array()
15442        .and_then(|rows| rows.first().cloned())
15443        .unwrap_or(Value::Null))
15444}
15445
15446fn sqlite_all_statement(
15447    process: &mut ActiveProcess,
15448    request: &JavascriptSyncRpcRequest,
15449) -> Result<Value, SidecarError> {
15450    let statement_id =
15451        javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.all statement id")?;
15452    let params = request.args.get(1);
15453    let statement_state = sqlite_statement(process, statement_id)?.clone();
15454    let database = sqlite_database_mut(process, statement_state.database_id)?;
15455    sqlite_query_rows(
15456        &mut database.connection,
15457        &statement_state.sql,
15458        params,
15459        statement_state.return_arrays,
15460        statement_state.read_bigints,
15461        statement_state.allow_bare_named_parameters,
15462        statement_state.allow_unknown_named_parameters,
15463    )
15464}
15465
15466fn sqlite_statement_columns(
15467    process: &mut ActiveProcess,
15468    request: &JavascriptSyncRpcRequest,
15469) -> Result<Value, SidecarError> {
15470    let statement_id =
15471        javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.columns statement id")?;
15472    let statement_state = sqlite_statement(process, statement_id)?.clone();
15473    let database = sqlite_database_mut(process, statement_state.database_id)?;
15474    let statement = database
15475        .connection
15476        .prepare(&statement_state.sql)
15477        .map_err(sqlite_error)?;
15478    Ok(Value::Array(
15479        statement
15480            .column_names()
15481            .iter()
15482            .map(|name| json!({ "name": name }))
15483            .collect(),
15484    ))
15485}
15486
15487fn sqlite_query_rows(
15488    connection: &mut SqliteConnection,
15489    sql: &str,
15490    params: Option<&Value>,
15491    return_arrays: bool,
15492    read_bigints: bool,
15493    allow_bare_named_parameters: bool,
15494    allow_unknown_named_parameters: bool,
15495) -> Result<Value, SidecarError> {
15496    let mut statement = connection.prepare(sql).map_err(sqlite_error)?;
15497    let column_names = statement
15498        .column_names()
15499        .iter()
15500        .map(|name| (*name).to_owned())
15501        .collect::<Vec<_>>();
15502    let column_count = statement.column_count();
15503    bind_sqlite_parameters(
15504        &mut statement,
15505        params,
15506        allow_bare_named_parameters,
15507        allow_unknown_named_parameters,
15508    )?;
15509    let mut rows = statement.raw_query();
15510    let mut encoded_rows = Vec::new();
15511    while let Some(row) = rows.next().map_err(sqlite_error)? {
15512        encoded_rows.push(encode_sqlite_row(
15513            row,
15514            &column_names,
15515            column_count,
15516            return_arrays,
15517            read_bigints,
15518        )?);
15519    }
15520    Ok(Value::Array(encoded_rows))
15521}
15522
15523fn encode_sqlite_row(
15524    row: &rusqlite::Row<'_>,
15525    column_names: &[String],
15526    column_count: usize,
15527    return_arrays: bool,
15528    read_bigints: bool,
15529) -> Result<Value, SidecarError> {
15530    if return_arrays {
15531        let mut values = Vec::with_capacity(column_count);
15532        for index in 0..column_count {
15533            values.push(encode_sqlite_value_ref(
15534                row.get_ref(index).map_err(sqlite_error)?,
15535                read_bigints,
15536            )?);
15537        }
15538        return Ok(Value::Array(values));
15539    }
15540
15541    let mut object = Map::with_capacity(column_count);
15542    for (index, name) in column_names.iter().enumerate() {
15543        object.insert(
15544            name.clone(),
15545            encode_sqlite_value_ref(row.get_ref(index).map_err(sqlite_error)?, read_bigints)?,
15546        );
15547    }
15548    Ok(Value::Object(object))
15549}
15550
15551fn encode_sqlite_value_ref(
15552    value: SqliteValueRef<'_>,
15553    read_bigints: bool,
15554) -> Result<Value, SidecarError> {
15555    Ok(match value {
15556        SqliteValueRef::Null => Value::Null,
15557        SqliteValueRef::Integer(number) => encode_sqlite_integer(number, read_bigints),
15558        SqliteValueRef::Real(number) => json!(number),
15559        SqliteValueRef::Text(text) => Value::String(String::from_utf8_lossy(text).into_owned()),
15560        SqliteValueRef::Blob(bytes) => json!({
15561            "__agentosSqliteType": "uint8array",
15562            "value": base64::engine::general_purpose::STANDARD.encode(bytes),
15563        }),
15564    })
15565}
15566
15567fn encode_sqlite_integer(number: i64, read_bigints: bool) -> Value {
15568    if read_bigints || number.abs() > SQLITE_JS_SAFE_INTEGER_MAX {
15569        json!({
15570            "__agentosSqliteType": "bigint",
15571            "value": number.to_string(),
15572        })
15573    } else {
15574        json!(number)
15575    }
15576}
15577
15578fn bind_sqlite_parameters(
15579    statement: &mut SqliteStatement<'_>,
15580    params: Option<&Value>,
15581    allow_bare_named_parameters: bool,
15582    allow_unknown_named_parameters: bool,
15583) -> Result<(), SidecarError> {
15584    let Some(params) = params else {
15585        return Ok(());
15586    };
15587    match params {
15588        Value::Null => Ok(()),
15589        Value::Array(values) => {
15590            for (index, value) in values.iter().enumerate() {
15591                statement
15592                    .raw_bind_parameter(index + 1, decode_sqlite_parameter(value)?)
15593                    .map_err(sqlite_error)?;
15594            }
15595            Ok(())
15596        }
15597        Value::Object(map)
15598            if map
15599                .get("__agentosSqliteType")
15600                .and_then(Value::as_str)
15601                .is_none() =>
15602        {
15603            for (key, value) in map {
15604                let index =
15605                    resolve_sqlite_parameter_index(statement, key, allow_bare_named_parameters)?;
15606                let Some(index) = index else {
15607                    if allow_unknown_named_parameters {
15608                        continue;
15609                    }
15610                    return Err(SidecarError::InvalidState(format!(
15611                        "sqlite named parameter not found: {key}"
15612                    )));
15613                };
15614                statement
15615                    .raw_bind_parameter(index, decode_sqlite_parameter(value)?)
15616                    .map_err(sqlite_error)?;
15617            }
15618            Ok(())
15619        }
15620        other => statement
15621            .raw_bind_parameter(1, decode_sqlite_parameter(other)?)
15622            .map_err(sqlite_error),
15623    }
15624}
15625
15626fn resolve_sqlite_parameter_index(
15627    statement: &mut SqliteStatement<'_>,
15628    key: &str,
15629    allow_bare_named_parameters: bool,
15630) -> Result<Option<usize>, SidecarError> {
15631    let mut candidates = vec![key.to_owned()];
15632    if allow_bare_named_parameters
15633        && !key.starts_with(':')
15634        && !key.starts_with('@')
15635        && !key.starts_with('$')
15636    {
15637        candidates.push(format!(":{key}"));
15638        candidates.push(format!("@{key}"));
15639        candidates.push(format!("${key}"));
15640    }
15641    for candidate in candidates {
15642        if let Some(index) = statement
15643            .parameter_index(&candidate)
15644            .map_err(sqlite_error)?
15645        {
15646            return Ok(Some(index));
15647        }
15648    }
15649    Ok(None)
15650}
15651
15652fn decode_sqlite_parameter(value: &Value) -> Result<rusqlite::types::Value, SidecarError> {
15653    Ok(match value {
15654        Value::Null => rusqlite::types::Value::Null,
15655        Value::Bool(value) => rusqlite::types::Value::Integer(i64::from(*value)),
15656        Value::Number(value) => match (value.as_i64(), value.as_f64()) {
15657            (Some(integer), _) => rusqlite::types::Value::Integer(integer),
15658            (_, Some(real)) => rusqlite::types::Value::Real(real),
15659            _ => {
15660                return Err(SidecarError::InvalidState(String::from(
15661                    "sqlite parameter number is not representable",
15662                )));
15663            }
15664        },
15665        Value::String(value) => rusqlite::types::Value::Text(value.clone()),
15666        Value::Array(_) => {
15667            return Err(SidecarError::InvalidState(String::from(
15668                "sqlite parameters do not support nested arrays",
15669            )));
15670        }
15671        Value::Object(map) => match map.get("__agentosSqliteType").and_then(Value::as_str) {
15672            Some("bigint") => rusqlite::types::Value::Integer(
15673                map.get("value")
15674                    .and_then(Value::as_str)
15675                    .ok_or_else(|| {
15676                        SidecarError::InvalidState(String::from(
15677                            "sqlite bigint parameter missing string value",
15678                        ))
15679                    })?
15680                    .parse::<i64>()
15681                    .map_err(|error| {
15682                        SidecarError::InvalidState(format!(
15683                            "sqlite bigint parameter is not a signed 64-bit integer: {error}"
15684                        ))
15685                    })?,
15686            ),
15687            Some("uint8array") => rusqlite::types::Value::Blob(
15688                base64::engine::general_purpose::STANDARD
15689                    .decode(map.get("value").and_then(Value::as_str).ok_or_else(|| {
15690                        SidecarError::InvalidState(String::from(
15691                            "sqlite blob parameter missing base64 value",
15692                        ))
15693                    })?)
15694                    .map_err(|error| {
15695                        SidecarError::InvalidState(format!(
15696                            "sqlite blob parameter contains invalid base64: {error}"
15697                        ))
15698                    })?,
15699            ),
15700            Some(other) => {
15701                return Err(SidecarError::InvalidState(format!(
15702                    "unsupported sqlite tagged parameter type {other}"
15703                )));
15704            }
15705            None => {
15706                return Err(SidecarError::InvalidState(String::from(
15707                    "sqlite named parameter objects must be passed as the top-level params object",
15708                )));
15709            }
15710        },
15711    })
15712}
15713
15714fn close_sqlite_database(
15715    kernel: &mut SidecarKernel,
15716    process: &mut ActiveProcess,
15717    database_id: u64,
15718) -> Result<(), SidecarError> {
15719    let mut database = process
15720        .sqlite_databases
15721        .remove(&database_id)
15722        .ok_or_else(|| {
15723            SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
15724        })?;
15725    process
15726        .sqlite_statements
15727        .retain(|_, statement| statement.database_id != database_id);
15728    sqlite_sync_database(kernel, process.kernel_pid, &mut database)?;
15729    let host_path = database.host_path.clone();
15730    drop(database);
15731    cleanup_sqlite_host_artifacts(host_path.as_deref())?;
15732    Ok(())
15733}
15734
15735fn ensure_per_process_state_handle_capacity(len: usize, label: &str) -> Result<(), SidecarError> {
15736    if len >= MAX_PER_PROCESS_STATE_HANDLES {
15737        return Err(SidecarError::InvalidState(format!(
15738            "{label} handle limit exceeded: limit is {MAX_PER_PROCESS_STATE_HANDLES}"
15739        )));
15740    }
15741    Ok(())
15742}
15743
15744fn sqlite_sync_database(
15745    kernel: &mut SidecarKernel,
15746    kernel_pid: u32,
15747    database: &mut ActiveSqliteDatabase,
15748) -> Result<(), SidecarError> {
15749    if !database.dirty
15750        || database.transaction_depth > 0
15751        || database.read_only
15752        || database.host_path.is_none()
15753        || database.vm_path.is_none()
15754    {
15755        return Ok(());
15756    }
15757
15758    let _ = database
15759        .connection
15760        .execute_batch("PRAGMA wal_checkpoint(TRUNCATE)");
15761    let host_path = database.host_path.as_ref().expect("sqlite host path");
15762    if !host_path.exists() {
15763        return Ok(());
15764    }
15765    ensure_vm_parent_dir(
15766        kernel,
15767        kernel_pid,
15768        database.vm_path.as_deref().expect("sqlite vm path"),
15769    )?;
15770    let contents = fs::read(host_path).map_err(|error| {
15771        SidecarError::Io(format!(
15772            "failed to read sqlite temp database {}: {error}",
15773            host_path.display()
15774        ))
15775    })?;
15776    kernel
15777        .write_file_for_process(
15778            EXECUTION_DRIVER_NAME,
15779            kernel_pid,
15780            database.vm_path.as_deref().expect("sqlite vm path"),
15781            contents,
15782            None,
15783        )
15784        .map_err(kernel_error)?;
15785    database.dirty = false;
15786    Ok(())
15787}
15788
15789fn cleanup_sqlite_host_artifacts(host_path: Option<&Path>) -> Result<(), SidecarError> {
15790    let Some(host_path) = host_path else {
15791        return Ok(());
15792    };
15793    let parent = host_path.parent().map(PathBuf::from);
15794    for suffix in ["", "-wal", "-shm"] {
15795        let path = PathBuf::from(format!("{}{}", host_path.display(), suffix));
15796        if path.exists() {
15797            fs::remove_file(&path).map_err(|error| {
15798                SidecarError::Io(format!(
15799                    "failed to remove sqlite temp artifact {}: {error}",
15800                    path.display()
15801                ))
15802            })?;
15803        }
15804    }
15805    if let Some(parent) = parent {
15806        let _ = fs::remove_dir_all(parent);
15807    }
15808    Ok(())
15809}
15810
15811fn ensure_vm_parent_dir(
15812    kernel: &mut SidecarKernel,
15813    kernel_pid: u32,
15814    path: &str,
15815) -> Result<(), SidecarError> {
15816    let parent = dirname(path);
15817    if parent == "/" || parent == "." {
15818        return Ok(());
15819    }
15820    let mut current = String::new();
15821    for segment in parent.split('/').filter(|segment| !segment.is_empty()) {
15822        current.push('/');
15823        current.push_str(segment);
15824        if !kernel
15825            .exists_for_process(EXECUTION_DRIVER_NAME, kernel_pid, &current)
15826            .map_err(kernel_error)?
15827        {
15828            kernel
15829                .mkdir_for_process(EXECUTION_DRIVER_NAME, kernel_pid, &current, false, None)
15830                .map_err(kernel_error)?;
15831        }
15832    }
15833    Ok(())
15834}
15835
15836fn sqlite_database(
15837    process: &ActiveProcess,
15838    database_id: u64,
15839) -> Result<&ActiveSqliteDatabase, SidecarError> {
15840    process.sqlite_databases.get(&database_id).ok_or_else(|| {
15841        SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
15842    })
15843}
15844
15845fn sqlite_database_mut(
15846    process: &mut ActiveProcess,
15847    database_id: u64,
15848) -> Result<&mut ActiveSqliteDatabase, SidecarError> {
15849    process
15850        .sqlite_databases
15851        .get_mut(&database_id)
15852        .ok_or_else(|| {
15853            SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
15854        })
15855}
15856
15857fn sqlite_statement(
15858    process: &ActiveProcess,
15859    statement_id: u64,
15860) -> Result<&ActiveSqliteStatement, SidecarError> {
15861    process.sqlite_statements.get(&statement_id).ok_or_else(|| {
15862        SidecarError::InvalidState(format!("sqlite statement handle not found: {statement_id}"))
15863    })
15864}
15865
15866fn sqlite_statement_mut(
15867    process: &mut ActiveProcess,
15868    statement_id: u64,
15869) -> Result<&mut ActiveSqliteStatement, SidecarError> {
15870    process
15871        .sqlite_statements
15872        .get_mut(&statement_id)
15873        .ok_or_else(|| {
15874            SidecarError::InvalidState(format!("sqlite statement handle not found: {statement_id}"))
15875        })
15876}
15877
15878fn mark_sqlite_mutation(database: &mut ActiveSqliteDatabase, sql: &str) {
15879    let normalized = sql.trim_start().to_ascii_lowercase();
15880    if normalized.starts_with("begin") || normalized.starts_with("savepoint") {
15881        database.dirty = true;
15882        database.transaction_depth += 1;
15883        return;
15884    }
15885    if normalized.starts_with("commit") || normalized.starts_with("release savepoint") {
15886        database.dirty = true;
15887        database.transaction_depth = database.transaction_depth.saturating_sub(1);
15888        return;
15889    }
15890    if normalized.starts_with("rollback") && !normalized.starts_with("rollback to") {
15891        database.dirty = true;
15892        database.transaction_depth = database.transaction_depth.saturating_sub(1);
15893        return;
15894    }
15895    if normalized.starts_with("insert")
15896        || normalized.starts_with("update")
15897        || normalized.starts_with("delete")
15898        || normalized.starts_with("replace")
15899        || normalized.starts_with("create")
15900        || normalized.starts_with("alter")
15901        || normalized.starts_with("drop")
15902        || normalized.starts_with("vacuum")
15903        || normalized.starts_with("reindex")
15904        || normalized.starts_with("analyze")
15905        || normalized.starts_with("attach")
15906        || normalized.starts_with("detach")
15907        || normalized.starts_with("pragma")
15908    {
15909        database.dirty = true;
15910    }
15911}
15912
15913fn sqlite_option_bool(options: Option<&Value>, key: &str) -> Option<bool> {
15914    options
15915        .and_then(|value| value.get(key))
15916        .and_then(Value::as_bool)
15917}
15918
15919fn sqlite_option_u64(options: Option<&Value>, key: &str) -> Option<u64> {
15920    options
15921        .and_then(|value| value.get(key))
15922        .and_then(Value::as_u64)
15923}
15924
15925fn sqlite_error(error: rusqlite::Error) -> SidecarError {
15926    SidecarError::InvalidState(format!("sqlite error: {error}"))
15927}
15928
15929pub(crate) fn javascript_sync_rpc_arg_str<'a>(
15930    args: &'a [Value],
15931    index: usize,
15932    label: &str,
15933) -> Result<&'a str, SidecarError> {
15934    args.get(index)
15935        .and_then(Value::as_str)
15936        .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a string argument")))
15937}
15938
15939pub(crate) fn javascript_sync_rpc_arg_bool(
15940    args: &[Value],
15941    index: usize,
15942    label: &str,
15943) -> Result<bool, SidecarError> {
15944    args.get(index)
15945        .and_then(Value::as_bool)
15946        .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a boolean argument")))
15947}
15948
15949pub(crate) fn javascript_sync_rpc_encoding(args: &[Value]) -> Option<String> {
15950    args.get(1).and_then(|value| {
15951        value.as_str().map(str::to_owned).or_else(|| {
15952            value
15953                .get("encoding")
15954                .and_then(Value::as_str)
15955                .map(str::to_owned)
15956        })
15957    })
15958}
15959
15960pub(crate) fn javascript_sync_rpc_option_bool(
15961    args: &[Value],
15962    index: usize,
15963    key: &str,
15964) -> Option<bool> {
15965    let value = args.get(index)?;
15966    if key == "recursive" {
15967        if let Some(boolean) = value.as_bool() {
15968            return Some(boolean);
15969        }
15970    }
15971    value.get(key).and_then(Value::as_bool)
15972}
15973
15974pub(crate) fn javascript_sync_rpc_option_u32(
15975    args: &[Value],
15976    index: usize,
15977    key: &str,
15978) -> Result<Option<u32>, SidecarError> {
15979    let Some(value) = args.get(index).and_then(|value| {
15980        if value.is_object() {
15981            value.get(key)
15982        } else if key == "mode" && value.is_number() {
15983            Some(value)
15984        } else {
15985            None
15986        }
15987    }) else {
15988        return Ok(None);
15989    };
15990    if value.is_null() {
15991        return Ok(None);
15992    }
15993
15994    let numeric = value
15995        .as_u64()
15996        .or_else(|| {
15997            value
15998                .as_f64()
15999                .filter(|number| number.is_finite() && *number >= 0.0)
16000                .map(|number| number as u64)
16001        })
16002        .ok_or_else(|| SidecarError::InvalidState(format!("{key} must be numeric")))?;
16003
16004    u32::try_from(numeric)
16005        .map(Some)
16006        .map_err(|_| SidecarError::InvalidState(format!("{key} must fit within u32")))
16007}
16008
16009pub(crate) fn javascript_sync_rpc_arg_u32(
16010    args: &[Value],
16011    index: usize,
16012    label: &str,
16013) -> Result<u32, SidecarError> {
16014    let value = javascript_sync_rpc_arg_u64(args, index, label)?;
16015    u32::try_from(value)
16016        .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")))
16017}
16018
16019pub(crate) fn javascript_sync_rpc_arg_i32(
16020    args: &[Value],
16021    index: usize,
16022    label: &str,
16023) -> Result<i32, SidecarError> {
16024    let Some(value) = args.get(index) else {
16025        return Err(SidecarError::InvalidState(format!("{label} is required")));
16026    };
16027
16028    let numeric = value
16029        .as_i64()
16030        .or_else(|| {
16031            value
16032                .as_f64()
16033                .filter(|number| number.is_finite())
16034                .map(|number| number as i64)
16035        })
16036        .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a numeric argument")))?;
16037
16038    i32::try_from(numeric)
16039        .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within i32")))
16040}
16041
16042pub(crate) fn javascript_sync_rpc_arg_u32_optional(
16043    args: &[Value],
16044    index: usize,
16045    label: &str,
16046) -> Result<Option<u32>, SidecarError> {
16047    javascript_sync_rpc_arg_u64_optional(args, index, label)?
16048        .map(|value| {
16049            u32::try_from(value)
16050                .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")))
16051        })
16052        .transpose()
16053}
16054
16055pub(crate) fn javascript_sync_rpc_arg_u64(
16056    args: &[Value],
16057    index: usize,
16058    label: &str,
16059) -> Result<u64, SidecarError> {
16060    let Some(value) = args.get(index) else {
16061        return Err(SidecarError::InvalidState(format!("{label} is required")));
16062    };
16063
16064    value
16065        .as_u64()
16066        .or_else(|| {
16067            value
16068                .as_f64()
16069                .filter(|number| number.is_finite() && *number >= 0.0)
16070                .map(|number| number as u64)
16071        })
16072        .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a numeric argument")))
16073}
16074
16075pub(crate) fn javascript_sync_rpc_arg_u64_optional(
16076    args: &[Value],
16077    index: usize,
16078    label: &str,
16079) -> Result<Option<u64>, SidecarError> {
16080    let Some(value) = args.get(index) else {
16081        return Ok(None);
16082    };
16083    if value.is_null() {
16084        return Ok(None);
16085    }
16086    javascript_sync_rpc_arg_u64(args, index, label).map(Some)
16087}
16088
16089pub(crate) fn javascript_sync_rpc_bytes_arg(
16090    args: &[Value],
16091    index: usize,
16092    label: &str,
16093) -> Result<Vec<u8>, SidecarError> {
16094    let Some(value) = args.get(index) else {
16095        return Err(SidecarError::InvalidState(format!("{label} is required")));
16096    };
16097
16098    if let Some(text) = value.as_str() {
16099        return Ok(text.as_bytes().to_vec());
16100    }
16101
16102    decode_encoded_bytes_value(value)
16103        .map_err(|error| SidecarError::InvalidState(format!("{label} {error}")))
16104}
16105
16106pub(crate) fn javascript_sync_rpc_bytes_value(bytes: &[u8]) -> Value {
16107    encoded_bytes_value(bytes)
16108}
16109
16110#[derive(Debug, Deserialize)]
16111pub(crate) struct KernelPollFdRequest {
16112    fd: u32,
16113    events: u16,
16114}
16115
16116#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
16117struct KernelPollFdResponse {
16118    fd: u32,
16119    events: u16,
16120    revents: u16,
16121}
16122
16123fn javascript_sync_rpc_base64_arg(
16124    args: &[Value],
16125    index: usize,
16126    label: &str,
16127) -> Result<Vec<u8>, SidecarError> {
16128    let value = javascript_sync_rpc_arg_str(args, index, label)?;
16129    decode_base64(value).map_err(|error| SidecarError::InvalidState(format!("{label} {error}")))
16130}
16131
16132// ── Sync-RPC round-trip counting (opt-in via AGENTOS_SYNC_RPC_TRACE=1) ──
16133// Each guest fs/module/net sync RPC funnels through service_javascript_sync_rpc,
16134// so this is the one place to measure the kernel-VFS "syscall storm" that makes
16135// metadata-heavy phases (resourceLoader.reload, createAgentSession) 40-90x slower
16136// in the VM than on bare node. Emits a perf log line every 200 calls with the
16137// running per-method breakdown.
16138static SYNC_RPC_STATS: std::sync::OnceLock<
16139    std::sync::Mutex<std::collections::BTreeMap<String, u64>>,
16140> = std::sync::OnceLock::new();
16141
16142#[derive(Default)]
16143struct ExecutePhaseStats {
16144    calls: u64,
16145    total_ns: u128,
16146    max_ns: u128,
16147}
16148
16149static EXECUTE_PHASES: OnceLock<Mutex<BTreeMap<String, ExecutePhaseStats>>> = OnceLock::new();
16150static EXECUTE_LIFETIMES: OnceLock<Mutex<BTreeMap<String, Instant>>> = OnceLock::new();
16151static EXECUTE_EXIT_EVENT_QUEUED: OnceLock<Mutex<BTreeMap<String, Instant>>> = OnceLock::new();
16152
16153fn execute_phases_enabled() -> bool {
16154    std::env::var("AGENTOS_EXECUTE_PHASES").as_deref() == Ok("1")
16155}
16156
16157fn execute_phase_key(vm_id: &str, process_id: &str) -> String {
16158    format!("{vm_id}/{process_id}")
16159}
16160
16161pub(crate) fn record_execute_phase(stage: &str, elapsed: Duration) {
16162    if !execute_phases_enabled() {
16163        return;
16164    }
16165    let phases = EXECUTE_PHASES.get_or_init(|| Mutex::new(BTreeMap::new()));
16166    let Ok(mut phases) = phases.lock() else {
16167        return;
16168    };
16169    let stats = phases.entry(stage.to_string()).or_default();
16170    stats.calls += 1;
16171    let elapsed_ns = elapsed.as_nanos();
16172    stats.total_ns += elapsed_ns;
16173    stats.max_ns = stats.max_ns.max(elapsed_ns);
16174
16175    let Some(path) = std::env::var_os("AGENTOS_EXECUTE_PHASES_FILE") else {
16176        return;
16177    };
16178    let mut output = String::new();
16179    for (stage, stats) in phases.iter() {
16180        let total_us = stats.total_ns / 1_000;
16181        let avg_us = if stats.calls == 0 {
16182            0
16183        } else {
16184            total_us / u128::from(stats.calls)
16185        };
16186        let max_us = stats.max_ns / 1_000;
16187        output.push_str(&format!(
16188            "stage={stage} calls={} total_us={total_us} avg_us={avg_us} max_us={max_us}\n",
16189            stats.calls
16190        ));
16191    }
16192    let _ = fs::write(path, output);
16193}
16194
16195fn mark_execute_response_ready(vm_id: &str, process_id: &str) {
16196    if !execute_phases_enabled() {
16197        return;
16198    }
16199    let lifetimes = EXECUTE_LIFETIMES.get_or_init(|| Mutex::new(BTreeMap::new()));
16200    if let Ok(mut lifetimes) = lifetimes.lock() {
16201        lifetimes.insert(execute_phase_key(vm_id, process_id), Instant::now());
16202    }
16203}
16204
16205pub(crate) fn mark_execute_exit_event_queued(vm_id: &str, process_id: &str) {
16206    if !execute_phases_enabled() {
16207        return;
16208    }
16209    let queued = EXECUTE_EXIT_EVENT_QUEUED.get_or_init(|| Mutex::new(BTreeMap::new()));
16210    if let Ok(mut queued) = queued.lock() {
16211        let key = execute_phase_key(vm_id, process_id);
16212        if let std::collections::btree_map::Entry::Vacant(entry) = queued.entry(key) {
16213            record_execute_response_to_exit_milestone(
16214                "execute_response_to_exit_event_queued",
16215                vm_id,
16216                process_id,
16217            );
16218            entry.insert(Instant::now());
16219        }
16220    }
16221}
16222
16223pub(crate) fn record_execute_exit_event_queue_wait(stage: &str, vm_id: &str, process_id: &str) {
16224    if !execute_phases_enabled() {
16225        return;
16226    }
16227    let Some(queued) = EXECUTE_EXIT_EVENT_QUEUED.get() else {
16228        return;
16229    };
16230    let Ok(mut queued) = queued.lock() else {
16231        return;
16232    };
16233    if let Some(started) = queued.remove(&execute_phase_key(vm_id, process_id)) {
16234        record_execute_phase(stage, started.elapsed());
16235    }
16236}
16237
16238pub(crate) fn record_execute_response_to_exit_milestone(
16239    stage: &str,
16240    vm_id: &str,
16241    process_id: &str,
16242) {
16243    if !execute_phases_enabled() {
16244        return;
16245    }
16246    let Some(lifetimes) = EXECUTE_LIFETIMES.get() else {
16247        return;
16248    };
16249    let Ok(lifetimes) = lifetimes.lock() else {
16250        return;
16251    };
16252    if let Some(started) = lifetimes.get(&execute_phase_key(vm_id, process_id)) {
16253        record_execute_phase(stage, started.elapsed());
16254    }
16255}
16256
16257fn record_execute_response_to_exit(vm_id: &str, process_id: &str) {
16258    if !execute_phases_enabled() {
16259        return;
16260    }
16261    let Some(lifetimes) = EXECUTE_LIFETIMES.get() else {
16262        return;
16263    };
16264    let Ok(mut lifetimes) = lifetimes.lock() else {
16265        return;
16266    };
16267    if let Some(started) = lifetimes.remove(&execute_phase_key(vm_id, process_id)) {
16268        record_execute_phase("execute_response_to_exit_event", started.elapsed());
16269    }
16270}
16271
16272fn sync_rpc_trace_enabled() -> bool {
16273    std::env::var("AGENTOS_SYNC_RPC_TRACE").as_deref() == Ok("1")
16274}
16275
16276fn record_sync_rpc(method: &str) {
16277    let stats =
16278        SYNC_RPC_STATS.get_or_init(|| std::sync::Mutex::new(std::collections::BTreeMap::new()));
16279    let Ok(mut map) = stats.lock() else {
16280        return;
16281    };
16282    *map.entry(method.to_string()).or_insert(0) += 1;
16283    let total: u64 = map.values().sum();
16284    if total == 1 || total.is_multiple_of(50) {
16285        let mut top: Vec<(&String, &u64)> = map.iter().collect();
16286        top.sort_by(|a, b| b.1.cmp(a.1));
16287        let breakdown = top
16288            .iter()
16289            .take(8)
16290            .map(|(m, c)| format!("{m}={c}"))
16291            .collect::<Vec<_>>()
16292            .join(" ");
16293        tracing::info!(target: "agentos_native_sidecar::perf", total, %breakdown, "sync_rpc count");
16294    }
16295}
16296
16297fn net_tcp_trace_enabled(env: &BTreeMap<String, String>) -> bool {
16298    env.get("AGENTOS_NET_BRIDGE_TRACE").map(String::as_str) == Some("1")
16299}
16300
16301fn duration_micros_u64(duration: Duration) -> u64 {
16302    u64::try_from(duration.as_micros()).unwrap_or(u64::MAX)
16303}
16304
16305fn net_tcp_trace_reset() {
16306    reset_socket_read_trace();
16307    set_socket_read_trace_enabled(true);
16308    for counter in [
16309        &NET_TCP_TRACE_COUNTERS.socket_read_calls,
16310        &NET_TCP_TRACE_COUNTERS.socket_read_zero_wait_calls,
16311        &NET_TCP_TRACE_COUNTERS.socket_read_data_events,
16312        &NET_TCP_TRACE_COUNTERS.socket_read_bytes,
16313        &NET_TCP_TRACE_COUNTERS.socket_read_kernel_us,
16314        &NET_TCP_TRACE_COUNTERS.socket_read_end_events,
16315        &NET_TCP_TRACE_COUNTERS.socket_read_eagain,
16316        &NET_TCP_TRACE_COUNTERS.socket_read_errors,
16317        &NET_TCP_TRACE_COUNTERS.socket_read_push_attempts,
16318        &NET_TCP_TRACE_COUNTERS.socket_read_push_sent,
16319        &NET_TCP_TRACE_COUNTERS.socket_read_push_missing,
16320        &NET_TCP_TRACE_COUNTERS.socket_read_push_errors,
16321        &NET_TCP_TRACE_COUNTERS.socket_write_calls,
16322        &NET_TCP_TRACE_COUNTERS.socket_write_bytes,
16323        &NET_TCP_TRACE_COUNTERS.socket_write_kernel_us,
16324        &NET_TCP_TRACE_COUNTERS.socket_write_errors,
16325        &NET_TCP_TRACE_COUNTERS.server_accept_calls,
16326        &NET_TCP_TRACE_COUNTERS.server_accept_zero_wait_calls,
16327        &NET_TCP_TRACE_COUNTERS.server_accept_connections,
16328        &NET_TCP_TRACE_COUNTERS.server_accept_eagain,
16329        &NET_TCP_TRACE_COUNTERS.server_accept_errors,
16330        &NET_TCP_TRACE_COUNTERS.kernel_poll_targets,
16331        &NET_TCP_TRACE_COUNTERS.kernel_poll_zero_wait_calls,
16332        &NET_TCP_TRACE_COUNTERS.kernel_poll_wait_us,
16333        &NET_TCP_TRACE_COUNTERS.kernel_poll_elapsed_us,
16334        &NET_TCP_TRACE_COUNTERS.kernel_poll_empty,
16335        &NET_TCP_TRACE_COUNTERS.kernel_poll_ready,
16336        &NET_TCP_TRACE_COUNTERS.kernel_poll_revents_read,
16337        &NET_TCP_TRACE_COUNTERS.kernel_poll_revents_hup,
16338        &NET_TCP_TRACE_COUNTERS.kernel_poll_revents_err,
16339        &NET_TCP_TRACE_COUNTERS.kernel_poll_revents_bits_or,
16340    ] {
16341        counter.store(0, Ordering::Relaxed);
16342    }
16343}
16344
16345fn net_tcp_trace_snapshot() -> Value {
16346    let load = |counter: &AtomicU64| counter.load(Ordering::Relaxed);
16347    let socket_read = socket_read_trace_snapshot();
16348    json!({
16349        "socketReadCalls": load(&NET_TCP_TRACE_COUNTERS.socket_read_calls),
16350        "socketReadZeroWaitCalls": load(&NET_TCP_TRACE_COUNTERS.socket_read_zero_wait_calls),
16351        "socketReadDataEvents": load(&NET_TCP_TRACE_COUNTERS.socket_read_data_events),
16352        "socketReadBytes": load(&NET_TCP_TRACE_COUNTERS.socket_read_bytes),
16353        "socketReadKernelUs": load(&NET_TCP_TRACE_COUNTERS.socket_read_kernel_us),
16354        "socketReadRecordCloneCalls": socket_read.socket_record_clone_calls,
16355        "socketReadRecordCloneUs": socket_read.socket_record_clone_us,
16356        "socketReadRecvCalls": socket_read.read_recv_calls,
16357        "socketReadRecvBytes": socket_read.read_recv_bytes,
16358        "socketReadRecvChunks": socket_read.read_recv_chunks,
16359        "socketReadRecvCopyUs": socket_read.read_recv_copy_us,
16360        "socketReadEndEvents": load(&NET_TCP_TRACE_COUNTERS.socket_read_end_events),
16361        "socketReadEagain": load(&NET_TCP_TRACE_COUNTERS.socket_read_eagain),
16362        "socketReadErrors": load(&NET_TCP_TRACE_COUNTERS.socket_read_errors),
16363        "socketReadPushAttempts": load(&NET_TCP_TRACE_COUNTERS.socket_read_push_attempts),
16364        "socketReadPushSent": load(&NET_TCP_TRACE_COUNTERS.socket_read_push_sent),
16365        "socketReadPushMissing": load(&NET_TCP_TRACE_COUNTERS.socket_read_push_missing),
16366        "socketReadPushErrors": load(&NET_TCP_TRACE_COUNTERS.socket_read_push_errors),
16367        "socketWriteCalls": load(&NET_TCP_TRACE_COUNTERS.socket_write_calls),
16368        "socketWriteBytes": load(&NET_TCP_TRACE_COUNTERS.socket_write_bytes),
16369        "socketWriteKernelUs": load(&NET_TCP_TRACE_COUNTERS.socket_write_kernel_us),
16370        "socketWriteErrors": load(&NET_TCP_TRACE_COUNTERS.socket_write_errors),
16371        "serverAcceptCalls": load(&NET_TCP_TRACE_COUNTERS.server_accept_calls),
16372        "serverAcceptZeroWaitCalls": load(&NET_TCP_TRACE_COUNTERS.server_accept_zero_wait_calls),
16373        "serverAcceptConnections": load(&NET_TCP_TRACE_COUNTERS.server_accept_connections),
16374        "serverAcceptEagain": load(&NET_TCP_TRACE_COUNTERS.server_accept_eagain),
16375        "serverAcceptErrors": load(&NET_TCP_TRACE_COUNTERS.server_accept_errors),
16376        "kernelPollTargets": load(&NET_TCP_TRACE_COUNTERS.kernel_poll_targets),
16377        "kernelPollZeroWaitCalls": load(&NET_TCP_TRACE_COUNTERS.kernel_poll_zero_wait_calls),
16378        "kernelPollWaitUs": load(&NET_TCP_TRACE_COUNTERS.kernel_poll_wait_us),
16379        "kernelPollElapsedUs": load(&NET_TCP_TRACE_COUNTERS.kernel_poll_elapsed_us),
16380        "kernelPollEmpty": load(&NET_TCP_TRACE_COUNTERS.kernel_poll_empty),
16381        "kernelPollReady": load(&NET_TCP_TRACE_COUNTERS.kernel_poll_ready),
16382        "kernelPollReventsRead": load(&NET_TCP_TRACE_COUNTERS.kernel_poll_revents_read),
16383        "kernelPollReventsHup": load(&NET_TCP_TRACE_COUNTERS.kernel_poll_revents_hup),
16384        "kernelPollReventsErr": load(&NET_TCP_TRACE_COUNTERS.kernel_poll_revents_err),
16385        "kernelPollReventsBitsOr": load(&NET_TCP_TRACE_COUNTERS.kernel_poll_revents_bits_or),
16386    })
16387}
16388
16389fn record_net_tcp_kernel_poll(
16390    enabled: bool,
16391    wait: Duration,
16392    elapsed: Duration,
16393    revents: PollEvents,
16394) {
16395    if !enabled {
16396        return;
16397    }
16398    NET_TCP_TRACE_COUNTERS
16399        .kernel_poll_targets
16400        .fetch_add(1, Ordering::Relaxed);
16401    if wait.is_zero() {
16402        NET_TCP_TRACE_COUNTERS
16403            .kernel_poll_zero_wait_calls
16404            .fetch_add(1, Ordering::Relaxed);
16405    }
16406    NET_TCP_TRACE_COUNTERS
16407        .kernel_poll_wait_us
16408        .fetch_add(duration_micros_u64(wait), Ordering::Relaxed);
16409    NET_TCP_TRACE_COUNTERS
16410        .kernel_poll_elapsed_us
16411        .fetch_add(duration_micros_u64(elapsed), Ordering::Relaxed);
16412    if revents.is_empty() {
16413        NET_TCP_TRACE_COUNTERS
16414            .kernel_poll_empty
16415            .fetch_add(1, Ordering::Relaxed);
16416    } else {
16417        NET_TCP_TRACE_COUNTERS
16418            .kernel_poll_ready
16419            .fetch_add(1, Ordering::Relaxed);
16420        NET_TCP_TRACE_COUNTERS
16421            .kernel_poll_revents_bits_or
16422            .fetch_or(u64::from(revents.bits()), Ordering::Relaxed);
16423    }
16424    if revents.intersects(POLLIN) {
16425        NET_TCP_TRACE_COUNTERS
16426            .kernel_poll_revents_read
16427            .fetch_add(1, Ordering::Relaxed);
16428    }
16429    if revents.intersects(POLLHUP) {
16430        NET_TCP_TRACE_COUNTERS
16431            .kernel_poll_revents_hup
16432            .fetch_add(1, Ordering::Relaxed);
16433    }
16434    if revents.intersects(POLLERR) {
16435        NET_TCP_TRACE_COUNTERS
16436            .kernel_poll_revents_err
16437            .fetch_add(1, Ordering::Relaxed);
16438    }
16439}
16440
16441pub(crate) fn service_javascript_sync_rpc<B>(
16442    request: JavascriptSyncRpcServiceRequest<'_, B>,
16443) -> Result<JavascriptSyncRpcServiceResponse, SidecarError>
16444where
16445    B: NativeSidecarBridge + Send + 'static,
16446    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16447{
16448    if sync_rpc_trace_enabled() {
16449        record_sync_rpc(request.sync_request.method.as_str());
16450    }
16451    let JavascriptSyncRpcServiceRequest {
16452        bridge,
16453        vm_id,
16454        dns,
16455        socket_paths,
16456        kernel,
16457        kernel_readiness,
16458        process,
16459        sync_request: request,
16460        resource_limits,
16461        network_counts,
16462    } = request;
16463    if request.raw_bytes_args.contains_key(&usize::MAX) && request.method == "fs.readSync" {
16464        let kernel_pid = process.kernel_pid;
16465        let bytes = service_javascript_fs_read_sync_rpc(kernel, process, kernel_pid, request)?;
16466        return Ok(JavascriptSyncRpcServiceResponse::Raw(bytes));
16467    }
16468    if request.method == "fs.readdirSync" {
16469        let kernel_pid = process.kernel_pid;
16470        let bytes =
16471            service_javascript_fs_readdir_raw_sync_rpc(kernel, process, kernel_pid, request)?;
16472        return Ok(JavascriptSyncRpcServiceResponse::Raw(bytes));
16473    }
16474    let response = match request.method.as_str() {
16475        "__bench.noop" => Ok(Value::Null),
16476        "__bench.net_tcp_metrics_reset" => {
16477            net_tcp_trace_reset();
16478            Ok(Value::Null)
16479        }
16480        "__bench.net_tcp_metrics_snapshot" => Ok(net_tcp_trace_snapshot()),
16481        // Module resolution / loading / format detection read the kernel VFS so
16482        // the resolver sees exactly what the guest and `kernel.readFile()` see.
16483        "_resolveModule"
16484        | "_resolveModuleSync"
16485        | "__resolve_module"
16486        | "_batchResolveModules"
16487        | "__batch_resolve_modules"
16488        | "_loadFile"
16489        | "_loadFileSync"
16490        | "__load_file"
16491        | "_moduleFormat"
16492        | "__module_format" => service_javascript_module_sync_rpc(kernel, process, request),
16493        // Polyfills are static guest expressions, not VFS reads.
16494        "_loadPolyfill" | "__load_polyfill" => {
16495            service_javascript_internal_bridge_sync_rpc(process, request)
16496        }
16497        "__kernel_stdin_read" => {
16498            // A TTY (PTY-backed) JavaScript process must read its stdin from the
16499            // kernel PTY slave (fd 0) so cooked-mode line discipline (echo,
16500            // VERASE/VKILL/VWERASE, ICRNL, VEOF) applies exactly as it does for
16501            // wasm/python. Non-TTY JS keeps using the in-process local stdin
16502            // bridge (piped stdin fed via process.execution.write_stdin).
16503            let js_local_bridge = matches!(process.execution, ActiveExecution::Javascript(_))
16504                && process.tty_master_fd.is_none();
16505            if js_local_bridge {
16506                match &process.execution {
16507                    ActiveExecution::Javascript(execution) => execution
16508                        .read_kernel_stdin_sync_rpc(request)
16509                        .map_err(|error| SidecarError::Execution(error.to_string())),
16510                    _ => unreachable!("js_local_bridge implies a JavaScript execution"),
16511                }
16512            } else {
16513                service_javascript_kernel_stdin_sync_rpc(kernel, process, request)
16514            }
16515        }
16516        "__kernel_stdio_write" => {
16517            service_javascript_kernel_stdio_write_sync_rpc(kernel, process, request)
16518        }
16519        "__kernel_isatty" => service_javascript_kernel_isatty_sync_rpc(kernel, process, request),
16520        "__kernel_tty_size" => {
16521            service_javascript_kernel_tty_size_sync_rpc(kernel, process, request)
16522        }
16523        "__kernel_poll" => service_javascript_kernel_poll_sync_rpc(kernel, process, request),
16524        "__pty_set_raw_mode" => {
16525            service_javascript_pty_set_raw_mode_sync_rpc(kernel, process, request)
16526        }
16527        "crypto.hashDigest"
16528        | "crypto.hmacDigest"
16529        | "crypto.pbkdf2"
16530        | "crypto.scrypt"
16531        | "crypto.cipheriv"
16532        | "crypto.decipheriv"
16533        | "crypto.cipherivCreate"
16534        | "crypto.cipherivUpdate"
16535        | "crypto.cipherivFinal"
16536        | "crypto.sign"
16537        | "crypto.verify"
16538        | "crypto.asymmetricOp"
16539        | "crypto.createKeyObject"
16540        | "crypto.generateKeyPairSync"
16541        | "crypto.generateKeySync"
16542        | "crypto.generatePrimeSync"
16543        | "crypto.diffieHellman"
16544        | "crypto.diffieHellmanGroup"
16545        | "crypto.diffieHellmanSessionCreate"
16546        | "crypto.diffieHellmanSessionCall"
16547        | "crypto.diffieHellmanSessionDestroy"
16548        | "crypto.subtle" => service_javascript_crypto_sync_rpc(process, request),
16549        "dns.lookup" | "dns.resolve" | "dns.resolve4" | "dns.resolve6" => {
16550            service_javascript_dns_sync_rpc(bridge, kernel, vm_id, dns, request)
16551        }
16552        "net.http_listen" | "net.http_close" | "net.http_wait" | "net.http_respond" => {
16553            return service_javascript_net_sync_rpc_response(JavascriptNetSyncRpcServiceRequest {
16554                bridge,
16555                vm_id,
16556                dns,
16557                socket_paths,
16558                kernel,
16559                kernel_readiness: Arc::clone(&kernel_readiness),
16560                process,
16561                sync_request: request,
16562                resource_limits,
16563                network_counts,
16564            })
16565        }
16566        "net.http2_server_listen"
16567        | "net.http2_server_poll"
16568        | "net.http2_server_close"
16569        | "net.http2_server_respond"
16570        | "net.http2_server_wait"
16571        | "net.http2_session_connect"
16572        | "net.http2_session_request"
16573        | "net.http2_session_settings"
16574        | "net.http2_session_set_local_window_size"
16575        | "net.http2_session_goaway"
16576        | "net.http2_session_close"
16577        | "net.http2_session_destroy"
16578        | "net.http2_session_poll"
16579        | "net.http2_session_wait"
16580        | "net.http2_stream_respond"
16581        | "net.http2_stream_push_stream"
16582        | "net.http2_stream_write"
16583        | "net.http2_stream_end"
16584        | "net.http2_stream_close"
16585        | "net.http2_stream_pause"
16586        | "net.http2_stream_resume"
16587        | "net.http2_stream_respond_with_file" => {
16588            service_javascript_http2_sync_rpc(JavascriptHttp2SyncRpcServiceRequest {
16589                bridge,
16590                kernel,
16591                vm_id,
16592                dns,
16593                socket_paths,
16594                process,
16595                sync_request: request,
16596                resource_limits,
16597                network_counts,
16598            })
16599        }
16600        "net.connect"
16601        | "net.reserve_tcp_port"
16602        | "net.release_tcp_port"
16603        | "net.listen"
16604        | "net.poll"
16605        | "net.socket_wait_connect"
16606        | "net.socket_read"
16607        | "net.socket_set_no_delay"
16608        | "net.socket_set_keep_alive"
16609        | "net.socket_upgrade_tls"
16610        | "net.socket_get_tls_client_hello"
16611        | "net.socket_tls_query"
16612        | "net.server_poll"
16613        | "net.server_accept"
16614        | "net.server_connections"
16615        | "net.upgrade_socket_write"
16616        | "net.upgrade_socket_end"
16617        | "net.upgrade_socket_destroy"
16618        | "net.write"
16619        | "net.shutdown"
16620        | "net.destroy"
16621        | "net.server_close"
16622        | "tls.get_ciphers" => {
16623            return service_javascript_net_sync_rpc_response(JavascriptNetSyncRpcServiceRequest {
16624                bridge,
16625                vm_id,
16626                dns,
16627                socket_paths,
16628                kernel,
16629                kernel_readiness: Arc::clone(&kernel_readiness),
16630                process,
16631                sync_request: request,
16632                resource_limits,
16633                network_counts,
16634            })
16635        }
16636        "dgram.createSocket"
16637        | "dgram.bind"
16638        | "dgram.send"
16639        | "dgram.poll"
16640        | "dgram.close"
16641        | "dgram.address"
16642        | "dgram.setBufferSize"
16643        | "dgram.getBufferSize" => {
16644            service_javascript_dgram_sync_rpc(JavascriptDgramSyncRpcServiceRequest {
16645                bridge,
16646                kernel,
16647                vm_id,
16648                dns,
16649                socket_paths,
16650                process,
16651                kernel_readiness,
16652                sync_request: request,
16653                resource_limits,
16654                network_counts,
16655            })
16656        }
16657        "sqlite.constants"
16658        | "sqlite.open"
16659        | "sqlite.close"
16660        | "sqlite.exec"
16661        | "sqlite.query"
16662        | "sqlite.prepare"
16663        | "sqlite.location"
16664        | "sqlite.checkpoint"
16665        | "sqlite.statement.run"
16666        | "sqlite.statement.get"
16667        | "sqlite.statement.all"
16668        | "sqlite.statement.iterate"
16669        | "sqlite.statement.columns"
16670        | "sqlite.statement.setReturnArrays"
16671        | "sqlite.statement.setReadBigInts"
16672        | "sqlite.statement.setAllowBareNamedParameters"
16673        | "sqlite.statement.setAllowUnknownNamedParameters"
16674        | "sqlite.statement.finalize" => {
16675            service_javascript_sqlite_sync_rpc(kernel, process, request)
16676        }
16677        "process.kill" => {
16678            let target_pid =
16679                javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?;
16680            let signal = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?;
16681            let parsed_signal = parse_signal(signal)?;
16682            if parsed_signal == 0 {
16683                kernel
16684                    .signal_process(EXECUTION_DRIVER_NAME, target_pid, parsed_signal)
16685                    .map_err(kernel_error)?;
16686                return Ok(Value::Null.into());
16687            }
16688            let process_pid = i32::try_from(process.kernel_pid)
16689                .map_err(|_| SidecarError::InvalidState("process pid exceeds i32".into()))?;
16690            if target_pid != process_pid {
16691                return Err(SidecarError::InvalidState(format!(
16692                    "unknown process pid {target_pid}"
16693                )));
16694            }
16695            process.pending_self_signal_exit = None;
16696            if parsed_signal != 0
16697                && !matches!(
16698                    canonical_signal_name(parsed_signal),
16699                    Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
16700                )
16701            {
16702                process.pending_self_signal_exit = Some(parsed_signal);
16703            }
16704            Ok(json!({
16705                "self": true,
16706                "action": "default",
16707            }))
16708        }
16709        "process.umask" => {
16710            let new_mask = javascript_sync_rpc_arg_u32_optional(&request.args, 0, "process umask")?;
16711            kernel
16712                .umask(EXECUTION_DRIVER_NAME, process.kernel_pid, new_mask)
16713                .map(|mask| json!(mask))
16714                .map_err(kernel_error)
16715        }
16716        "fs.chmodSync" | "fs.promises.chmod" => {
16717            let response =
16718                service_javascript_fs_sync_rpc(kernel, process, process.kernel_pid, request)?;
16719            mirror_process_chmod_to_host(process, request)?;
16720            Ok(response)
16721        }
16722        _ => service_javascript_fs_sync_rpc(kernel, process, process.kernel_pid, request),
16723    }?;
16724    Ok(response.into())
16725}
16726
16727fn service_javascript_internal_bridge_sync_rpc(
16728    process: &ActiveProcess,
16729    request: &JavascriptSyncRpcRequest,
16730) -> Result<Value, SidecarError> {
16731    // Module resolution / loading / format now reads the kernel VFS via
16732    // `service_javascript_module_sync_rpc`. This host-context path only handles
16733    // polyfills, which are static guest expressions independent of the FS.
16734    let method = match request.method.as_str() {
16735        "_loadPolyfill" | "__load_polyfill" => "_loadPolyfill",
16736        other => {
16737            return Err(SidecarError::InvalidState(format!(
16738                "unsupported JavaScript internal bridge method {other}"
16739            )));
16740        }
16741    };
16742
16743    handle_internal_bridge_call_from_host_context(
16744        &process.host_cwd,
16745        &process.guest_cwd,
16746        &process.env,
16747        method,
16748        &request.args,
16749    )
16750    .ok_or_else(|| {
16751        SidecarError::InvalidState(format!(
16752            "JavaScript internal bridge method {method} returned no value"
16753        ))
16754    })
16755}
16756
16757fn mirror_process_chmod_to_host(
16758    process: &ActiveProcess,
16759    request: &JavascriptSyncRpcRequest,
16760) -> Result<(), SidecarError> {
16761    let guest_path = javascript_sync_rpc_arg_str(&request.args, 0, "filesystem chmod path")?;
16762    let mode = javascript_sync_rpc_arg_u32(&request.args, 1, "filesystem chmod mode")? & 0o7777;
16763    let Some(host_path) = resolve_process_guest_path_to_host(process, guest_path) else {
16764        return Ok(());
16765    };
16766    if !host_path.exists() {
16767        return Ok(());
16768    }
16769    fs::set_permissions(&host_path, fs::Permissions::from_mode(mode)).map_err(|error| {
16770        SidecarError::Io(format!(
16771            "failed to mirror chmod to host path {}: {error}",
16772            host_path.display()
16773        ))
16774    })
16775}
16776
16777fn resolve_process_guest_path_to_host(
16778    process: &ActiveProcess,
16779    guest_path: &str,
16780) -> Option<PathBuf> {
16781    let normalized_guest_path = if guest_path.starts_with('/') {
16782        normalize_path(guest_path)
16783    } else {
16784        normalize_path(&format!(
16785            "{}/{}",
16786            process.guest_cwd.trim_end_matches('/'),
16787            guest_path
16788        ))
16789    };
16790    if let Some(host_path) =
16791        host_path_from_runtime_guest_mappings(&process.env, &normalized_guest_path)
16792    {
16793        return Some(host_path);
16794    }
16795    let normalized_guest_cwd = normalize_path(&process.guest_cwd);
16796    let mut host_root = normalize_host_path(&process.host_cwd);
16797    for _ in normalized_guest_cwd
16798        .trim_start_matches('/')
16799        .split('/')
16800        .filter(|segment| !segment.is_empty())
16801    {
16802        host_root = host_root.parent()?.to_path_buf();
16803    }
16804    if normalized_guest_path == "/" {
16805        Some(host_root)
16806    } else {
16807        Some(host_root.join(normalized_guest_path.trim_start_matches('/')))
16808    }
16809}
16810
16811pub(crate) fn service_javascript_crypto_sync_rpc(
16812    process: &mut ActiveProcess,
16813    request: &JavascriptSyncRpcRequest,
16814) -> Result<Value, SidecarError> {
16815    match request.method.as_str() {
16816        "crypto.hashDigest" => {
16817            let algorithm = javascript_crypto_digest_algorithm(
16818                &request.args,
16819                0,
16820                "crypto.hashDigest algorithm",
16821            )?;
16822            let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.hashDigest data")?;
16823            Ok(Value::String(
16824                base64::engine::general_purpose::STANDARD.encode(algorithm.digest(&data)),
16825            ))
16826        }
16827        "crypto.hmacDigest" => {
16828            let algorithm = javascript_crypto_digest_algorithm(
16829                &request.args,
16830                0,
16831                "crypto.hmacDigest algorithm",
16832            )?;
16833            let key = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.hmacDigest key")?;
16834            let data = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.hmacDigest data")?;
16835            Ok(Value::String(
16836                base64::engine::general_purpose::STANDARD.encode(algorithm.hmac(&key, &data)?),
16837            ))
16838        }
16839        "crypto.pbkdf2" => {
16840            let password =
16841                javascript_sync_rpc_base64_arg(&request.args, 0, "crypto.pbkdf2 password")?;
16842            let salt = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.pbkdf2 salt")?;
16843            let iterations =
16844                javascript_sync_rpc_arg_u32(&request.args, 2, "crypto.pbkdf2 iterations")?;
16845            if iterations == 0 {
16846                return Err(SidecarError::InvalidState(String::from(
16847                    "crypto.pbkdf2 iterations must be greater than zero",
16848                )));
16849            }
16850            let key_len = usize::try_from(javascript_sync_rpc_arg_u64(
16851                &request.args,
16852                3,
16853                "crypto.pbkdf2 key length",
16854            )?)
16855            .map_err(|_| {
16856                SidecarError::InvalidState(String::from(
16857                    "crypto.pbkdf2 key length must fit within usize",
16858                ))
16859            })?;
16860            let algorithm =
16861                javascript_crypto_digest_algorithm(&request.args, 4, "crypto.pbkdf2 digest")?;
16862            let mut output = vec![0u8; key_len];
16863            algorithm.pbkdf2(&password, &salt, iterations, &mut output);
16864            Ok(Value::String(
16865                base64::engine::general_purpose::STANDARD.encode(output),
16866            ))
16867        }
16868        "crypto.scrypt" => {
16869            let password =
16870                javascript_sync_rpc_base64_arg(&request.args, 0, "crypto.scrypt password")?;
16871            let salt = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.scrypt salt")?;
16872            let key_len = usize::try_from(javascript_sync_rpc_arg_u64(
16873                &request.args,
16874                2,
16875                "crypto.scrypt key length",
16876            )?)
16877            .map_err(|_| {
16878                SidecarError::InvalidState(String::from(
16879                    "crypto.scrypt key length must fit within usize",
16880                ))
16881            })?;
16882            let options_json =
16883                javascript_sync_rpc_arg_str(&request.args, 3, "crypto.scrypt options")?;
16884            let options: JavascriptScryptOptions =
16885                serde_json::from_str(options_json).map_err(|error| {
16886                    SidecarError::InvalidState(format!(
16887                        "crypto.scrypt options must be valid JSON: {error}"
16888                    ))
16889                })?;
16890            let cost = options.cost.unwrap_or(DEFAULT_SCRYPT_COST);
16891            if cost == 0 || !cost.is_power_of_two() {
16892                return Err(SidecarError::InvalidState(String::from(
16893                    "crypto.scrypt cost must be a positive power of two",
16894                )));
16895            }
16896            let log_n = u8::try_from(cost.ilog2()).map_err(|_| {
16897                SidecarError::InvalidState(String::from(
16898                    "crypto.scrypt cost exceeds supported parameter range",
16899                ))
16900            })?;
16901            let params = ScryptParams::new(
16902                log_n,
16903                options.block_size.unwrap_or(DEFAULT_SCRYPT_BLOCK_SIZE),
16904                options
16905                    .parallelization
16906                    .unwrap_or(DEFAULT_SCRYPT_PARALLELIZATION),
16907                key_len,
16908            )
16909            .map_err(|error| {
16910                SidecarError::InvalidState(format!("crypto.scrypt options are invalid: {error}"))
16911            })?;
16912            let mut output = vec![0u8; key_len];
16913            scrypt(&password, &salt, &params, &mut output).map_err(|error| {
16914                SidecarError::Execution(format!("crypto.scrypt failed: {error}"))
16915            })?;
16916            Ok(Value::String(
16917                base64::engine::general_purpose::STANDARD.encode(output),
16918            ))
16919        }
16920        "crypto.cipheriv" => service_javascript_crypto_cipheriv_sync_rpc(request),
16921        "crypto.decipheriv" => service_javascript_crypto_decipheriv_sync_rpc(request),
16922        "crypto.cipherivCreate" => {
16923            service_javascript_crypto_cipheriv_create_sync_rpc(process, request)
16924        }
16925        "crypto.cipherivUpdate" => {
16926            service_javascript_crypto_cipheriv_update_sync_rpc(process, request)
16927        }
16928        "crypto.cipherivFinal" => {
16929            service_javascript_crypto_cipheriv_final_sync_rpc(process, request)
16930        }
16931        "crypto.sign" => service_javascript_crypto_sign_sync_rpc(request),
16932        "crypto.verify" => service_javascript_crypto_verify_sync_rpc(request),
16933        "crypto.asymmetricOp" => service_javascript_crypto_asymmetric_op_sync_rpc(request),
16934        "crypto.createKeyObject" => service_javascript_crypto_create_key_object_sync_rpc(request),
16935        "crypto.generateKeyPairSync" => {
16936            service_javascript_crypto_generate_key_pair_sync_rpc(request)
16937        }
16938        "crypto.generateKeySync" => service_javascript_crypto_generate_key_sync_rpc(request),
16939        "crypto.generatePrimeSync" => service_javascript_crypto_generate_prime_sync_rpc(request),
16940        "crypto.diffieHellman" => service_javascript_crypto_diffie_hellman_sync_rpc(request),
16941        "crypto.diffieHellmanGroup" => {
16942            service_javascript_crypto_diffie_hellman_group_sync_rpc(request)
16943        }
16944        "crypto.diffieHellmanSessionCreate" => {
16945            service_javascript_crypto_diffie_hellman_session_create_sync_rpc(process, request)
16946        }
16947        "crypto.diffieHellmanSessionCall" => {
16948            service_javascript_crypto_diffie_hellman_session_call_sync_rpc(process, request)
16949        }
16950        "crypto.diffieHellmanSessionDestroy" => {
16951            service_javascript_crypto_diffie_hellman_session_destroy_sync_rpc(process, request)
16952        }
16953        "crypto.subtle" => service_javascript_crypto_subtle_sync_rpc(request),
16954        _ => Err(SidecarError::InvalidState(format!(
16955            "unsupported JavaScript crypto sync RPC method {}",
16956            request.method
16957        ))),
16958    }
16959}
16960
16961fn javascript_crypto_digest_algorithm(
16962    args: &[Value],
16963    index: usize,
16964    label: &str,
16965) -> Result<JavascriptCryptoDigestAlgorithm, SidecarError> {
16966    JavascriptCryptoDigestAlgorithm::parse(javascript_sync_rpc_arg_str(args, index, label)?)
16967}
16968
16969impl JavascriptCryptoDigestAlgorithm {
16970    fn parse(value: &str) -> Result<Self, SidecarError> {
16971        match value.trim().to_ascii_lowercase().replace('-', "").as_str() {
16972            "md5" => Ok(Self::Md5),
16973            "sha1" => Ok(Self::Sha1),
16974            "sha224" => Ok(Self::Sha224),
16975            "sha256" => Ok(Self::Sha256),
16976            "sha384" => Ok(Self::Sha384),
16977            "sha512" => Ok(Self::Sha512),
16978            _ => Err(SidecarError::InvalidState(format!(
16979                "unsupported crypto digest algorithm {value}"
16980            ))),
16981        }
16982    }
16983
16984    fn digest(self, data: &[u8]) -> Vec<u8> {
16985        match self {
16986            Self::Md5 => Md5::digest(data).to_vec(),
16987            Self::Sha1 => Sha1::digest(data).to_vec(),
16988            Self::Sha224 => Sha224::digest(data).to_vec(),
16989            Self::Sha256 => Sha256::digest(data).to_vec(),
16990            Self::Sha384 => Sha384::digest(data).to_vec(),
16991            Self::Sha512 => Sha512::digest(data).to_vec(),
16992        }
16993    }
16994
16995    fn hmac(self, key: &[u8], data: &[u8]) -> Result<Vec<u8>, SidecarError> {
16996        match self {
16997            Self::Md5 => {
16998                let mut mac = Hmac::<Md5>::new_from_slice(key).map_err(|error| {
16999                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
17000                })?;
17001                mac.update(data);
17002                Ok(mac.finalize().into_bytes().to_vec())
17003            }
17004            Self::Sha1 => {
17005                let mut mac = Hmac::<Sha1>::new_from_slice(key).map_err(|error| {
17006                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
17007                })?;
17008                mac.update(data);
17009                Ok(mac.finalize().into_bytes().to_vec())
17010            }
17011            Self::Sha224 => {
17012                let mut mac = Hmac::<Sha224>::new_from_slice(key).map_err(|error| {
17013                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
17014                })?;
17015                mac.update(data);
17016                Ok(mac.finalize().into_bytes().to_vec())
17017            }
17018            Self::Sha256 => {
17019                let mut mac = Hmac::<Sha256>::new_from_slice(key).map_err(|error| {
17020                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
17021                })?;
17022                mac.update(data);
17023                Ok(mac.finalize().into_bytes().to_vec())
17024            }
17025            Self::Sha384 => {
17026                let mut mac = Hmac::<Sha384>::new_from_slice(key).map_err(|error| {
17027                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
17028                })?;
17029                mac.update(data);
17030                Ok(mac.finalize().into_bytes().to_vec())
17031            }
17032            Self::Sha512 => {
17033                let mut mac = Hmac::<Sha512>::new_from_slice(key).map_err(|error| {
17034                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
17035                })?;
17036                mac.update(data);
17037                Ok(mac.finalize().into_bytes().to_vec())
17038            }
17039        }
17040    }
17041
17042    fn pbkdf2(self, password: &[u8], salt: &[u8], iterations: u32, output: &mut [u8]) {
17043        match self {
17044            Self::Md5 => pbkdf2_hmac::<Md5>(password, salt, iterations, output),
17045            Self::Sha1 => pbkdf2_hmac::<Sha1>(password, salt, iterations, output),
17046            Self::Sha224 => pbkdf2_hmac::<Sha224>(password, salt, iterations, output),
17047            Self::Sha256 => pbkdf2_hmac::<Sha256>(password, salt, iterations, output),
17048            Self::Sha384 => pbkdf2_hmac::<Sha384>(password, salt, iterations, output),
17049            Self::Sha512 => pbkdf2_hmac::<Sha512>(password, salt, iterations, output),
17050        }
17051    }
17052}
17053
17054#[derive(Debug, Clone)]
17055enum JavascriptCryptoKeyMaterial {
17056    Private(PKey<Private>),
17057    Public(PKey<Public>),
17058    Secret(Vec<u8>),
17059}
17060
17061#[derive(Debug, Clone, Deserialize, Serialize)]
17062struct JavascriptSerializedSandboxKeyObject {
17063    #[serde(rename = "type")]
17064    kind: String,
17065    #[serde(skip_serializing_if = "Option::is_none")]
17066    pem: Option<String>,
17067    #[serde(skip_serializing_if = "Option::is_none")]
17068    raw: Option<String>,
17069    #[serde(skip_serializing_if = "Option::is_none", rename = "asymmetricKeyType")]
17070    asymmetric_key_type: Option<String>,
17071    #[serde(
17072        skip_serializing_if = "Option::is_none",
17073        rename = "asymmetricKeyDetails"
17074    )]
17075    asymmetric_key_details: Option<Map<String, Value>>,
17076    #[serde(skip_serializing_if = "Option::is_none")]
17077    jwk: Option<Value>,
17078}
17079
17080#[derive(Debug, Clone)]
17081struct JavascriptDirectKeyInput {
17082    key: JavascriptCryptoKeyMaterial,
17083    padding: Option<Padding>,
17084}
17085
17086fn service_javascript_crypto_cipheriv_sync_rpc(
17087    request: &JavascriptSyncRpcRequest,
17088) -> Result<Value, SidecarError> {
17089    service_javascript_crypto_cipheriv_inner(request, false)
17090}
17091
17092fn service_javascript_crypto_decipheriv_sync_rpc(
17093    request: &JavascriptSyncRpcRequest,
17094) -> Result<Value, SidecarError> {
17095    service_javascript_crypto_cipheriv_inner(request, true)
17096}
17097
17098fn service_javascript_crypto_cipheriv_create_sync_rpc(
17099    process: &mut ActiveProcess,
17100    request: &JavascriptSyncRpcRequest,
17101) -> Result<Value, SidecarError> {
17102    ensure_per_process_state_handle_capacity(process.cipher_sessions.len(), "cipher session")?;
17103    let mode = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.cipherivCreate mode")?;
17104    let decrypt = mode == "decipher";
17105    let algorithm =
17106        javascript_sync_rpc_arg_str(&request.args, 1, "crypto.cipherivCreate algorithm")?;
17107    let key = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.cipherivCreate key")?;
17108    let iv = javascript_sync_rpc_base64_arg_optional(&request.args, 3, "crypto.cipherivCreate iv")?;
17109    let options =
17110        javascript_sync_rpc_json_arg_optional(&request.args, 4, "crypto.cipherivCreate options")?;
17111    let context = javascript_crypto_build_cipher_session(
17112        algorithm,
17113        &key,
17114        iv.as_deref(),
17115        decrypt,
17116        options.as_ref(),
17117    )?;
17118    process.next_cipher_session_id += 1;
17119    let session_id = process.next_cipher_session_id;
17120    process
17121        .cipher_sessions
17122        .insert(session_id, ActiveCipherSession { context });
17123    Ok(json!(session_id))
17124}
17125
17126fn service_javascript_crypto_cipheriv_update_sync_rpc(
17127    process: &mut ActiveProcess,
17128    request: &JavascriptSyncRpcRequest,
17129) -> Result<Value, SidecarError> {
17130    let session_id =
17131        javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.cipherivUpdate session id")?;
17132    let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.cipherivUpdate data")?;
17133    let session = process
17134        .cipher_sessions
17135        .get_mut(&session_id)
17136        .ok_or_else(|| {
17137            SidecarError::InvalidState(format!("Cipher session {session_id} not found"))
17138        })?;
17139    let result = javascript_crypto_cipher_update(&mut session.context, &data)?;
17140    Ok(Value::String(
17141        base64::engine::general_purpose::STANDARD.encode(result),
17142    ))
17143}
17144
17145fn service_javascript_crypto_cipheriv_final_sync_rpc(
17146    process: &mut ActiveProcess,
17147    request: &JavascriptSyncRpcRequest,
17148) -> Result<Value, SidecarError> {
17149    let session_id =
17150        javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.cipherivFinal session id")?;
17151    let session = process.cipher_sessions.remove(&session_id).ok_or_else(|| {
17152        SidecarError::InvalidState(format!("Cipher session {session_id} not found"))
17153    })?;
17154    let outcome = session
17155        .context
17156        .finalize()
17157        .map_err(javascript_crypto_cipher_error)?;
17158    let mut response = Map::new();
17159    response.insert(
17160        String::from("data"),
17161        Value::String(base64::engine::general_purpose::STANDARD.encode(outcome.data)),
17162    );
17163    if let Some(auth_tag) = outcome.auth_tag {
17164        response.insert(
17165            String::from("authTag"),
17166            Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
17167        );
17168    }
17169    Ok(Value::String(serde_json::to_string(&response).map_err(
17170        |error| SidecarError::InvalidState(format!("serialize cipher final response: {error}")),
17171    )?))
17172}
17173
17174fn service_javascript_crypto_sign_sync_rpc(
17175    request: &JavascriptSyncRpcRequest,
17176) -> Result<Value, SidecarError> {
17177    let algorithm = request.args.first().and_then(Value::as_str);
17178    let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.sign data")?;
17179    let key_json = javascript_sync_rpc_arg_str(&request.args, 2, "crypto.sign key")?;
17180    let key_input =
17181        javascript_crypto_parse_direct_key_input(key_json, Some("private"), "crypto.sign key")?;
17182    let private_key = javascript_crypto_expect_private_key(key_input.key, "crypto.sign key")?;
17183    let mut signer = javascript_crypto_new_signer(algorithm, &private_key)?;
17184    if let Some(padding) = key_input.padding {
17185        signer
17186            .set_rsa_padding(padding)
17187            .map_err(javascript_crypto_openssl_error)?;
17188    }
17189    signer
17190        .update(&data)
17191        .map_err(javascript_crypto_openssl_error)?;
17192    Ok(Value::String(
17193        base64::engine::general_purpose::STANDARD.encode(
17194            signer
17195                .sign_to_vec()
17196                .map_err(javascript_crypto_openssl_error)?,
17197        ),
17198    ))
17199}
17200
17201fn service_javascript_crypto_verify_sync_rpc(
17202    request: &JavascriptSyncRpcRequest,
17203) -> Result<Value, SidecarError> {
17204    let algorithm = request.args.first().and_then(Value::as_str);
17205    let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.verify data")?;
17206    let key_json = javascript_sync_rpc_arg_str(&request.args, 2, "crypto.verify key")?;
17207    let signature = javascript_sync_rpc_base64_arg(&request.args, 3, "crypto.verify signature")?;
17208    let key_input =
17209        javascript_crypto_parse_direct_key_input(key_json, Some("public"), "crypto.verify key")?;
17210    let public_key = javascript_crypto_expect_public_key(key_input.key, "crypto.verify key")?;
17211    let mut verifier = javascript_crypto_new_verifier(algorithm, &public_key)?;
17212    if let Some(padding) = key_input.padding {
17213        verifier
17214            .set_rsa_padding(padding)
17215            .map_err(javascript_crypto_openssl_error)?;
17216    }
17217    verifier
17218        .update(&data)
17219        .map_err(javascript_crypto_openssl_error)?;
17220    Ok(json!(verifier
17221        .verify(&signature)
17222        .map_err(javascript_crypto_openssl_error)?))
17223}
17224
17225fn service_javascript_crypto_asymmetric_op_sync_rpc(
17226    request: &JavascriptSyncRpcRequest,
17227) -> Result<Value, SidecarError> {
17228    let operation = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.asymmetricOp operation")?;
17229    let key_json = javascript_sync_rpc_arg_str(&request.args, 1, "crypto.asymmetricOp key")?;
17230    let data = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.asymmetricOp data")?;
17231    let expect_kind = match operation {
17232        "publicEncrypt" | "publicDecrypt" => Some("public"),
17233        "privateEncrypt" | "privateDecrypt" => Some("private"),
17234        other => {
17235            return Err(SidecarError::InvalidState(format!(
17236                "Unsupported asymmetric crypto operation: {other}"
17237            )));
17238        }
17239    };
17240    let key_input =
17241        javascript_crypto_parse_direct_key_input(key_json, expect_kind, "crypto.asymmetricOp key")?;
17242    let padding = key_input.padding.unwrap_or(Padding::PKCS1);
17243    let mut output = vec![0_u8; javascript_crypto_rsa_output_size(&key_input.key)?];
17244    let written = match (operation, key_input.key) {
17245        ("publicEncrypt", JavascriptCryptoKeyMaterial::Public(key))
17246        | ("publicDecrypt", JavascriptCryptoKeyMaterial::Public(key)) => {
17247            let rsa = key.rsa().map_err(javascript_crypto_openssl_error)?;
17248            if operation == "publicEncrypt" {
17249                rsa.public_encrypt(&data, &mut output, padding)
17250                    .map_err(javascript_crypto_openssl_error)?
17251            } else {
17252                rsa.public_decrypt(&data, &mut output, padding)
17253                    .map_err(javascript_crypto_openssl_error)?
17254            }
17255        }
17256        ("privateEncrypt", JavascriptCryptoKeyMaterial::Private(key))
17257        | ("privateDecrypt", JavascriptCryptoKeyMaterial::Private(key)) => {
17258            let rsa = key.rsa().map_err(javascript_crypto_openssl_error)?;
17259            if operation == "privateEncrypt" {
17260                rsa.private_encrypt(&data, &mut output, padding)
17261                    .map_err(javascript_crypto_openssl_error)?
17262            } else {
17263                rsa.private_decrypt(&data, &mut output, padding)
17264                    .map_err(javascript_crypto_openssl_error)?
17265            }
17266        }
17267        _ => {
17268            return Err(SidecarError::InvalidState(format!(
17269                "{operation} requires an RSA {} key",
17270                expect_kind.unwrap_or("asymmetric")
17271            )));
17272        }
17273    };
17274    output.truncate(written);
17275    Ok(Value::String(
17276        base64::engine::general_purpose::STANDARD.encode(output),
17277    ))
17278}
17279
17280fn service_javascript_crypto_create_key_object_sync_rpc(
17281    request: &JavascriptSyncRpcRequest,
17282) -> Result<Value, SidecarError> {
17283    let operation =
17284        javascript_sync_rpc_arg_str(&request.args, 0, "crypto.createKeyObject operation")?;
17285    let key_json = javascript_sync_rpc_arg_str(&request.args, 1, "crypto.createKeyObject key")?;
17286    let expected = match operation {
17287        "createPrivateKey" => Some("private"),
17288        "createPublicKey" => Some("public"),
17289        other => {
17290            return Err(SidecarError::InvalidState(format!(
17291                "Unsupported key creation operation: {other}"
17292            )));
17293        }
17294    };
17295    let key_input =
17296        javascript_crypto_parse_direct_key_input(key_json, expected, "crypto.createKeyObject key")?;
17297    Ok(Value::String(
17298        serde_json::to_string(&javascript_crypto_serialize_sandbox_key_object(
17299            &key_input.key,
17300        )?)
17301        .map_err(|error| {
17302            SidecarError::InvalidState(format!("serialize crypto key object: {error}"))
17303        })?,
17304    ))
17305}
17306
17307fn service_javascript_crypto_generate_key_pair_sync_rpc(
17308    request: &JavascriptSyncRpcRequest,
17309) -> Result<Value, SidecarError> {
17310    let key_type =
17311        javascript_sync_rpc_arg_str(&request.args, 0, "crypto.generateKeyPairSync type")?;
17312    let options = javascript_crypto_parse_serialized_options_arg(
17313        &request.args,
17314        1,
17315        "crypto.generateKeyPairSync options",
17316    )?
17317    .unwrap_or(Value::Object(Map::new()));
17318    let public_encoding = options.get("publicKeyEncoding").cloned();
17319    let private_encoding = options.get("privateKeyEncoding").cloned();
17320
17321    let private_key = match key_type {
17322        "rsa" => {
17323            let bits = options
17324                .get("modulusLength")
17325                .and_then(Value::as_u64)
17326                .unwrap_or(2048) as u32;
17327            let exponent = options
17328                .get("publicExponent")
17329                .map(|value| javascript_crypto_u32_from_bridge_value(value, "rsa publicExponent"))
17330                .transpose()?
17331                .unwrap_or(65_537);
17332            let exponent = BigNum::from_u32(exponent).map_err(javascript_crypto_openssl_error)?;
17333            let rsa =
17334                Rsa::generate_with_e(bits, &exponent).map_err(javascript_crypto_openssl_error)?;
17335            PKey::from_rsa(rsa).map_err(javascript_crypto_openssl_error)?
17336        }
17337        "ec" => {
17338            let named_curve = options
17339                .get("namedCurve")
17340                .and_then(Value::as_str)
17341                .ok_or_else(|| {
17342                    SidecarError::InvalidState(String::from(
17343                        "crypto.generateKeyPairSync ec requires namedCurve",
17344                    ))
17345                })?;
17346            let group = EcGroup::from_curve_name(javascript_crypto_curve_nid(named_curve)?)
17347                .map_err(javascript_crypto_openssl_error)?;
17348            let key = EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?;
17349            PKey::from_ec_key(key).map_err(javascript_crypto_openssl_error)?
17350        }
17351        "ed25519" => PKey::generate_ed25519().map_err(javascript_crypto_openssl_error)?,
17352        "x25519" => PKey::generate_x25519().map_err(javascript_crypto_openssl_error)?,
17353        other => {
17354            return Err(SidecarError::InvalidState(format!(
17355                "unsupported crypto key pair type {other}"
17356            )));
17357        }
17358    };
17359    let public_key = PKey::public_key_from_pem(
17360        &private_key
17361            .public_key_to_pem()
17362            .map_err(javascript_crypto_openssl_error)?,
17363    )
17364    .map_err(javascript_crypto_openssl_error)?;
17365    let response = if public_encoding.is_some() || private_encoding.is_some() {
17366        json!({
17367            "publicKey": javascript_crypto_serialize_encoded_key_value_public(&public_key, public_encoding.as_ref())?,
17368            "privateKey": javascript_crypto_serialize_encoded_key_value_private(&private_key, private_encoding.as_ref())?,
17369        })
17370    } else {
17371        json!({
17372            "publicKey": javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Public(public_key))?,
17373            "privateKey": javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Private(private_key))?,
17374        })
17375    };
17376    Ok(Value::String(serde_json::to_string(&response).map_err(
17377        |error| SidecarError::InvalidState(format!("serialize generated key pair: {error}")),
17378    )?))
17379}
17380
17381fn service_javascript_crypto_generate_key_sync_rpc(
17382    request: &JavascriptSyncRpcRequest,
17383) -> Result<Value, SidecarError> {
17384    let key_type = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.generateKeySync type")?;
17385    let options = javascript_crypto_parse_serialized_options_arg(
17386        &request.args,
17387        1,
17388        "crypto.generateKeySync options",
17389    )?
17390    .unwrap_or(Value::Object(Map::new()));
17391    let bit_length = options
17392        .get("length")
17393        .and_then(Value::as_u64)
17394        .ok_or_else(|| {
17395            SidecarError::InvalidState(String::from(
17396                "crypto.generateKeySync options.length is required",
17397            ))
17398        })? as usize;
17399    let mut raw = vec![0_u8; bit_length.div_ceil(8)];
17400    rand_bytes(&mut raw).map_err(javascript_crypto_openssl_error)?;
17401    let serialized = match key_type {
17402        "hmac" => javascript_crypto_serialize_sandbox_key_object(
17403            &JavascriptCryptoKeyMaterial::Secret(raw),
17404        )?,
17405        "aes" => javascript_crypto_serialize_sandbox_key_object(
17406            &JavascriptCryptoKeyMaterial::Secret(raw),
17407        )?,
17408        other => {
17409            return Err(SidecarError::InvalidState(format!(
17410                "unsupported crypto.generateKeySync type {other}"
17411            )));
17412        }
17413    };
17414    Ok(Value::String(serde_json::to_string(&serialized).map_err(
17415        |error| SidecarError::InvalidState(format!("serialize generated key: {error}")),
17416    )?))
17417}
17418
17419fn service_javascript_crypto_generate_prime_sync_rpc(
17420    request: &JavascriptSyncRpcRequest,
17421) -> Result<Value, SidecarError> {
17422    let bits =
17423        javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.generatePrimeSync size")? as i32;
17424    let options = javascript_crypto_parse_serialized_options_arg(
17425        &request.args,
17426        1,
17427        "crypto.generatePrimeSync options",
17428    )?
17429    .unwrap_or(Value::Object(Map::new()));
17430    let safe = options
17431        .get("safe")
17432        .and_then(Value::as_bool)
17433        .unwrap_or(false);
17434    let add = options
17435        .get("add")
17436        .map(|value| javascript_crypto_bignum_from_bridge_value(value, "prime add"))
17437        .transpose()?;
17438    let rem = options
17439        .get("rem")
17440        .map(|value| javascript_crypto_bignum_from_bridge_value(value, "prime rem"))
17441        .transpose()?;
17442    let mut prime = BigNum::new().map_err(javascript_crypto_openssl_error)?;
17443    prime
17444        .generate_prime(bits, safe, add.as_deref(), rem.as_deref())
17445        .map_err(javascript_crypto_openssl_error)?;
17446    let payload = if options
17447        .get("bigint")
17448        .and_then(Value::as_bool)
17449        .unwrap_or(false)
17450    {
17451        json!({
17452            "__type": "bigint",
17453            "value": prime.to_dec_str().map_err(javascript_crypto_openssl_error)?.to_string(),
17454        })
17455    } else {
17456        json!({
17457            "__type": "buffer",
17458            "value": base64::engine::general_purpose::STANDARD.encode(prime.to_vec()),
17459        })
17460    };
17461    Ok(Value::String(serde_json::to_string(&payload).map_err(
17462        |error| SidecarError::InvalidState(format!("serialize generated prime: {error}")),
17463    )?))
17464}
17465
17466fn service_javascript_crypto_diffie_hellman_sync_rpc(
17467    request: &JavascriptSyncRpcRequest,
17468) -> Result<Value, SidecarError> {
17469    let options = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.diffieHellman options")?;
17470    let parsed: Value = serde_json::from_str(options).map_err(|error| {
17471        SidecarError::InvalidState(format!(
17472            "crypto.diffieHellman options must be valid JSON: {error}"
17473        ))
17474    })?;
17475    let private_key = javascript_crypto_parse_key_material_value(
17476        parsed.get("privateKey").ok_or_else(|| {
17477            SidecarError::InvalidState(String::from("crypto.diffieHellman missing privateKey"))
17478        })?,
17479        Some("private"),
17480        "crypto.diffieHellman privateKey",
17481    )?;
17482    let public_key = javascript_crypto_parse_key_material_value(
17483        parsed.get("publicKey").ok_or_else(|| {
17484            SidecarError::InvalidState(String::from("crypto.diffieHellman missing publicKey"))
17485        })?,
17486        Some("public"),
17487        "crypto.diffieHellman publicKey",
17488    )?;
17489    let private_key =
17490        javascript_crypto_expect_private_key(private_key, "crypto.diffieHellman privateKey")?;
17491    let public_key =
17492        javascript_crypto_expect_public_key(public_key, "crypto.diffieHellman publicKey")?;
17493    let mut deriver = Deriver::new(&private_key).map_err(javascript_crypto_openssl_error)?;
17494    deriver
17495        .set_peer(&public_key)
17496        .map_err(javascript_crypto_openssl_error)?;
17497    let secret = deriver
17498        .derive_to_vec()
17499        .map_err(javascript_crypto_openssl_error)?;
17500    Ok(Value::String(
17501        serde_json::to_string(&json!({
17502            "__type": "buffer",
17503            "value": base64::engine::general_purpose::STANDARD.encode(secret),
17504        }))
17505        .map_err(|error| {
17506            SidecarError::InvalidState(format!("serialize derived secret: {error}"))
17507        })?,
17508    ))
17509}
17510
17511fn service_javascript_crypto_diffie_hellman_group_sync_rpc(
17512    request: &JavascriptSyncRpcRequest,
17513) -> Result<Value, SidecarError> {
17514    let name = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.diffieHellmanGroup name")?;
17515    let params = javascript_crypto_named_dh_group(name)?;
17516    let response = json!({
17517        "prime": {
17518            "__type": "buffer",
17519            "value": base64::engine::general_purpose::STANDARD.encode(params.prime_p().to_vec()),
17520        },
17521        "generator": {
17522            "__type": "buffer",
17523            "value": base64::engine::general_purpose::STANDARD.encode(params.generator().to_vec()),
17524        },
17525    });
17526    Ok(Value::String(serde_json::to_string(&response).map_err(
17527        |error| {
17528            SidecarError::InvalidState(format!("serialize diffieHellmanGroup response: {error}"))
17529        },
17530    )?))
17531}
17532
17533fn service_javascript_crypto_diffie_hellman_session_create_sync_rpc(
17534    process: &mut ActiveProcess,
17535    request: &JavascriptSyncRpcRequest,
17536) -> Result<Value, SidecarError> {
17537    ensure_per_process_state_handle_capacity(
17538        process.diffie_hellman_sessions.len(),
17539        "diffie-hellman session",
17540    )?;
17541    let raw = javascript_sync_rpc_arg_str(
17542        &request.args,
17543        0,
17544        "crypto.diffieHellmanSessionCreate request",
17545    )?;
17546    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
17547        SidecarError::InvalidState(format!(
17548            "crypto.diffieHellmanSessionCreate request must be valid JSON: {error}"
17549        ))
17550    })?;
17551    let session = match parsed.get("type").and_then(Value::as_str) {
17552        Some("group") => {
17553            let name = parsed.get("name").and_then(Value::as_str).ok_or_else(|| {
17554                SidecarError::InvalidState(String::from(
17555                    "crypto.diffieHellmanSessionCreate group requires name",
17556                ))
17557            })?;
17558            ActiveDiffieHellmanSession::Dh(ActiveDhSession {
17559                params: javascript_crypto_named_dh_group(name)?,
17560                key_pair: None,
17561            })
17562        }
17563        Some("dh") => {
17564            let args = parsed
17565                .get("args")
17566                .and_then(Value::as_array)
17567                .ok_or_else(|| {
17568                    SidecarError::InvalidState(String::from(
17569                        "crypto.diffieHellmanSessionCreate dh requires args",
17570                    ))
17571                })?;
17572            let params = javascript_crypto_build_dh_params(args)?;
17573            ActiveDiffieHellmanSession::Dh(ActiveDhSession {
17574                params,
17575                key_pair: None,
17576            })
17577        }
17578        Some("ecdh") => {
17579            let curve = parsed.get("name").and_then(Value::as_str).ok_or_else(|| {
17580                SidecarError::InvalidState(String::from(
17581                    "crypto.diffieHellmanSessionCreate ecdh requires name",
17582                ))
17583            })?;
17584            ActiveDiffieHellmanSession::Ecdh(ActiveEcdhSession {
17585                curve: curve.to_string(),
17586                key_pair: None,
17587            })
17588        }
17589        other => {
17590            return Err(SidecarError::InvalidState(format!(
17591                "Unsupported Diffie-Hellman session type: {}",
17592                other.unwrap_or("<missing>")
17593            )));
17594        }
17595    };
17596    process.next_diffie_hellman_session_id += 1;
17597    let session_id = process.next_diffie_hellman_session_id;
17598    process.diffie_hellman_sessions.insert(session_id, session);
17599    Ok(json!(session_id))
17600}
17601
17602fn service_javascript_crypto_diffie_hellman_session_call_sync_rpc(
17603    process: &mut ActiveProcess,
17604    request: &JavascriptSyncRpcRequest,
17605) -> Result<Value, SidecarError> {
17606    let session_id = javascript_sync_rpc_arg_u64(
17607        &request.args,
17608        0,
17609        "crypto.diffieHellmanSessionCall session id",
17610    )?;
17611    let raw =
17612        javascript_sync_rpc_arg_str(&request.args, 1, "crypto.diffieHellmanSessionCall request")?;
17613    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
17614        SidecarError::InvalidState(format!(
17615            "crypto.diffieHellmanSessionCall request must be valid JSON: {error}"
17616        ))
17617    })?;
17618    let method = parsed
17619        .get("method")
17620        .and_then(Value::as_str)
17621        .ok_or_else(|| {
17622            SidecarError::InvalidState(String::from(
17623                "crypto.diffieHellmanSessionCall request missing method",
17624            ))
17625        })?;
17626    let args = parsed
17627        .get("args")
17628        .and_then(Value::as_array)
17629        .cloned()
17630        .unwrap_or_default();
17631    let session = process
17632        .diffie_hellman_sessions
17633        .get_mut(&session_id)
17634        .ok_or_else(|| {
17635            SidecarError::InvalidState(format!("Diffie-Hellman session {session_id} not found"))
17636        })?;
17637    let (result, has_result) = match session {
17638        ActiveDiffieHellmanSession::Dh(session) => {
17639            javascript_crypto_call_dh_session(session, method, &args)?
17640        }
17641        ActiveDiffieHellmanSession::Ecdh(session) => {
17642            javascript_crypto_call_ecdh_session(session, method, &args)?
17643        }
17644    };
17645    Ok(Value::String(
17646        serde_json::to_string(&json!({
17647            "result": result,
17648            "hasResult": has_result,
17649        }))
17650        .map_err(|error| {
17651            SidecarError::InvalidState(format!("serialize diffie session result: {error}"))
17652        })?,
17653    ))
17654}
17655
17656fn service_javascript_crypto_diffie_hellman_session_destroy_sync_rpc(
17657    process: &mut ActiveProcess,
17658    request: &JavascriptSyncRpcRequest,
17659) -> Result<Value, SidecarError> {
17660    let session_id = javascript_sync_rpc_arg_u64(
17661        &request.args,
17662        0,
17663        "crypto.diffieHellmanSessionDestroy session id",
17664    )?;
17665    process
17666        .diffie_hellman_sessions
17667        .remove(&session_id)
17668        .ok_or_else(|| {
17669            SidecarError::InvalidState(format!("Diffie-Hellman session {session_id} not found"))
17670        })?;
17671    Ok(Value::Null)
17672}
17673
17674fn service_javascript_crypto_subtle_sync_rpc(
17675    request: &JavascriptSyncRpcRequest,
17676) -> Result<Value, SidecarError> {
17677    let raw = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.subtle request")?;
17678    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
17679        SidecarError::InvalidState(format!("crypto.subtle request must be valid JSON: {error}"))
17680    })?;
17681    let op = parsed.get("op").and_then(Value::as_str).ok_or_else(|| {
17682        SidecarError::InvalidState(String::from("crypto.subtle request missing op"))
17683    })?;
17684    match op {
17685        "digest" => {
17686            let algorithm = parsed
17687                .get("algorithm")
17688                .and_then(Value::as_str)
17689                .ok_or_else(|| {
17690                    SidecarError::InvalidState(String::from(
17691                        "crypto.subtle.digest missing algorithm",
17692                    ))
17693                })?;
17694            let data = parsed.get("data").and_then(Value::as_str).ok_or_else(|| {
17695                SidecarError::InvalidState(String::from("crypto.subtle.digest missing data"))
17696            })?;
17697            let bytes = base64::engine::general_purpose::STANDARD
17698                .decode(data)
17699                .map_err(|error| {
17700                    SidecarError::InvalidState(format!("crypto.subtle.digest data base64: {error}"))
17701                })?;
17702            let digest = JavascriptCryptoDigestAlgorithm::parse(algorithm)?.digest(&bytes);
17703            Ok(Value::String(
17704                serde_json::to_string(&json!({
17705                    "data": base64::engine::general_purpose::STANDARD.encode(digest),
17706                }))
17707                .map_err(|error| {
17708                    SidecarError::InvalidState(format!("serialize crypto.subtle digest: {error}"))
17709                })?,
17710            ))
17711        }
17712        "generateKey" => {
17713            let algorithm = parsed.get("algorithm").ok_or_else(|| {
17714                SidecarError::InvalidState(String::from(
17715                    "crypto.subtle.generateKey missing algorithm",
17716                ))
17717            })?;
17718            let name =
17719                javascript_crypto_subtle_algorithm_name(algorithm, "crypto.subtle.generateKey")?;
17720            if !matches!(name, "AES-GCM" | "AES-CBC" | "AES-CTR" | "AES-KW") {
17721                return Err(SidecarError::InvalidState(format!(
17722                    "Unsupported key algorithm: {name}"
17723                )));
17724            }
17725            let length_bits = algorithm
17726                .get("length")
17727                .and_then(Value::as_u64)
17728                .ok_or_else(|| {
17729                    SidecarError::InvalidState(String::from(
17730                        "crypto.subtle.generateKey AES algorithm requires length",
17731                    ))
17732                })?;
17733            if length_bits % 8 != 0 {
17734                return Err(SidecarError::InvalidState(String::from(
17735                    "crypto.subtle.generateKey length must be byte-aligned",
17736                )));
17737            }
17738            let length_bytes = usize::try_from(length_bits / 8).map_err(|_| {
17739                SidecarError::InvalidState(String::from(
17740                    "crypto.subtle.generateKey length is too large",
17741                ))
17742            })?;
17743            let mut raw = vec![0_u8; length_bytes];
17744            rand_bytes(&mut raw).map_err(javascript_crypto_openssl_error)?;
17745            let key = javascript_crypto_serialize_subtle_secret_key(
17746                &raw,
17747                javascript_crypto_normalize_subtle_secret_algorithm(algorithm.clone(), &raw)?,
17748                parsed
17749                    .get("extractable")
17750                    .and_then(Value::as_bool)
17751                    .unwrap_or(false),
17752                parsed.get("usages").cloned().unwrap_or_else(|| json!([])),
17753            )?;
17754            Ok(Value::String(
17755                serde_json::to_string(&json!({ "key": key })).map_err(|error| {
17756                    SidecarError::InvalidState(format!(
17757                        "serialize crypto.subtle generated key: {error}"
17758                    ))
17759                })?,
17760            ))
17761        }
17762        "importKey" => {
17763            let format = parsed
17764                .get("format")
17765                .and_then(Value::as_str)
17766                .ok_or_else(|| {
17767                    SidecarError::InvalidState(String::from(
17768                        "crypto.subtle.importKey missing format",
17769                    ))
17770                })?;
17771            if format != "raw" {
17772                return Err(SidecarError::InvalidState(format!(
17773                    "Unsupported import format: {format}"
17774                )));
17775            }
17776            let key_data = parsed
17777                .get("keyData")
17778                .and_then(Value::as_str)
17779                .ok_or_else(|| {
17780                    SidecarError::InvalidState(String::from(
17781                        "crypto.subtle.importKey missing keyData",
17782                    ))
17783                })?;
17784            let raw = base64::engine::general_purpose::STANDARD
17785                .decode(key_data)
17786                .map_err(|error| {
17787                    SidecarError::InvalidState(format!(
17788                        "crypto.subtle.importKey keyData base64: {error}"
17789                    ))
17790                })?;
17791            let algorithm = parsed.get("algorithm").ok_or_else(|| {
17792                SidecarError::InvalidState(String::from(
17793                    "crypto.subtle.importKey missing algorithm",
17794                ))
17795            })?;
17796            let key = javascript_crypto_serialize_subtle_secret_key(
17797                &raw,
17798                javascript_crypto_normalize_subtle_secret_algorithm(algorithm.clone(), &raw)?,
17799                parsed
17800                    .get("extractable")
17801                    .and_then(Value::as_bool)
17802                    .unwrap_or(false),
17803                parsed.get("usages").cloned().unwrap_or_else(|| json!([])),
17804            )?;
17805            Ok(Value::String(
17806                serde_json::to_string(&json!({ "key": key })).map_err(|error| {
17807                    SidecarError::InvalidState(format!(
17808                        "serialize crypto.subtle imported key: {error}"
17809                    ))
17810                })?,
17811            ))
17812        }
17813        "exportKey" => {
17814            let format = parsed
17815                .get("format")
17816                .and_then(Value::as_str)
17817                .ok_or_else(|| {
17818                    SidecarError::InvalidState(String::from(
17819                        "crypto.subtle.exportKey missing format",
17820                    ))
17821                })?;
17822            if format != "raw" {
17823                return Err(SidecarError::InvalidState(format!(
17824                    "Unsupported export format: {format}"
17825                )));
17826            }
17827            let raw = javascript_crypto_subtle_key_raw(
17828                parsed.get("key").ok_or_else(|| {
17829                    SidecarError::InvalidState(String::from("crypto.subtle.exportKey missing key"))
17830                })?,
17831                "crypto.subtle.exportKey key",
17832            )?;
17833            Ok(Value::String(
17834                serde_json::to_string(&json!({
17835                    "data": base64::engine::general_purpose::STANDARD.encode(raw),
17836                }))
17837                .map_err(|error| {
17838                    SidecarError::InvalidState(format!("serialize crypto.subtle export: {error}"))
17839                })?,
17840            ))
17841        }
17842        "encrypt" | "decrypt" => service_javascript_crypto_subtle_aes_crypt_sync_rpc(op, &parsed),
17843        _ => Err(SidecarError::InvalidState(format!(
17844            "Unsupported subtle operation: {op}"
17845        ))),
17846    }
17847}
17848
17849fn javascript_crypto_subtle_algorithm_name<'a>(
17850    algorithm: &'a Value,
17851    label: &str,
17852) -> Result<&'a str, SidecarError> {
17853    if let Some(name) = algorithm.as_str() {
17854        return Ok(name);
17855    }
17856    algorithm
17857        .get("name")
17858        .and_then(Value::as_str)
17859        .ok_or_else(|| SidecarError::InvalidState(format!("{label} algorithm missing name")))
17860}
17861
17862fn javascript_crypto_normalize_subtle_secret_algorithm(
17863    algorithm: Value,
17864    raw: &[u8],
17865) -> Result<Value, SidecarError> {
17866    let mut object = match algorithm {
17867        Value::String(name) => {
17868            let mut object = Map::new();
17869            object.insert(String::from("name"), Value::String(name));
17870            object
17871        }
17872        Value::Object(object) => object,
17873        _ => {
17874            return Err(SidecarError::InvalidState(String::from(
17875                "crypto.subtle secret algorithm must be a string or object",
17876            )));
17877        }
17878    };
17879    let name = object
17880        .get("name")
17881        .and_then(Value::as_str)
17882        .ok_or_else(|| {
17883            SidecarError::InvalidState(String::from("crypto.subtle secret algorithm missing name"))
17884        })?
17885        .to_string();
17886    if matches!(name.as_str(), "AES-GCM" | "AES-CBC" | "AES-CTR" | "AES-KW")
17887        && !object.contains_key("length")
17888    {
17889        object.insert(String::from("length"), json!(raw.len() * 8));
17890    }
17891    Ok(Value::Object(object))
17892}
17893
17894fn javascript_crypto_serialize_subtle_secret_key(
17895    raw: &[u8],
17896    algorithm: Value,
17897    extractable: bool,
17898    usages: Value,
17899) -> Result<Value, SidecarError> {
17900    let raw_base64 = base64::engine::general_purpose::STANDARD.encode(raw);
17901    let source_key_object_data = javascript_crypto_serialize_sandbox_key_object(
17902        &JavascriptCryptoKeyMaterial::Secret(raw.to_vec()),
17903    )?;
17904    Ok(json!({
17905        "type": "secret",
17906        "algorithm": algorithm,
17907        "extractable": extractable,
17908        "usages": usages,
17909        "_raw": raw_base64,
17910        "_sourceKeyObjectData": source_key_object_data,
17911    }))
17912}
17913
17914fn javascript_crypto_subtle_key_raw(key: &Value, label: &str) -> Result<Vec<u8>, SidecarError> {
17915    let raw = key.get("_raw").and_then(Value::as_str).ok_or_else(|| {
17916        SidecarError::InvalidState(format!("{label} must be a raw secret CryptoKey"))
17917    })?;
17918    base64::engine::general_purpose::STANDARD
17919        .decode(raw)
17920        .map_err(|error| SidecarError::InvalidState(format!("{label} raw base64: {error}")))
17921}
17922
17923fn service_javascript_crypto_subtle_aes_crypt_sync_rpc(
17924    op: &str,
17925    parsed: &Value,
17926) -> Result<Value, SidecarError> {
17927    let algorithm = parsed.get("algorithm").ok_or_else(|| {
17928        SidecarError::InvalidState(format!("crypto.subtle.{op} missing algorithm"))
17929    })?;
17930    let name = javascript_crypto_subtle_algorithm_name(algorithm, &format!("crypto.subtle.{op}"))?;
17931    if name != "AES-GCM" {
17932        return Err(SidecarError::InvalidState(format!(
17933            "Unsupported subtle AES operation algorithm: {name}"
17934        )));
17935    }
17936    let key = javascript_crypto_subtle_key_raw(
17937        parsed
17938            .get("key")
17939            .ok_or_else(|| SidecarError::InvalidState(format!("crypto.subtle.{op} missing key")))?,
17940        &format!("crypto.subtle.{op} key"),
17941    )?;
17942    let iv = algorithm.get("iv").and_then(Value::as_str).ok_or_else(|| {
17943        SidecarError::InvalidState(format!("crypto.subtle.{op} AES-GCM missing iv"))
17944    })?;
17945    let iv = base64::engine::general_purpose::STANDARD
17946        .decode(iv)
17947        .map_err(|error| {
17948            SidecarError::InvalidState(format!("crypto.subtle.{op} iv base64: {error}"))
17949        })?;
17950    let data = parsed
17951        .get("data")
17952        .and_then(Value::as_str)
17953        .ok_or_else(|| SidecarError::InvalidState(format!("crypto.subtle.{op} missing data")))?;
17954    let mut data = base64::engine::general_purpose::STANDARD
17955        .decode(data)
17956        .map_err(|error| {
17957            SidecarError::InvalidState(format!("crypto.subtle.{op} data base64: {error}"))
17958        })?;
17959    let tag_len = javascript_crypto_subtle_aes_gcm_tag_len(algorithm)?;
17960    let mut options = Map::new();
17961    options.insert(String::from("authTagLength"), json!(tag_len));
17962    if let Some(additional_data) = algorithm.get("additionalData").and_then(Value::as_str) {
17963        options.insert(
17964            String::from("aad"),
17965            Value::String(additional_data.to_string()),
17966        );
17967    }
17968    let decrypt = op == "decrypt";
17969    if decrypt {
17970        if data.len() < tag_len {
17971            return Err(SidecarError::InvalidState(String::from(
17972                "crypto.subtle.decrypt AES-GCM data shorter than auth tag",
17973            )));
17974        }
17975        let auth_tag = data.split_off(data.len() - tag_len);
17976        options.insert(
17977            String::from("authTag"),
17978            Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
17979        );
17980    }
17981    let cipher_name = format!("aes-{}-gcm", key.len() * 8);
17982    let mut session = javascript_crypto_build_cipher_session(
17983        &cipher_name,
17984        &key,
17985        Some(&iv),
17986        decrypt,
17987        Some(&Value::Object(options)),
17988    )?;
17989    let mut output = javascript_crypto_cipher_update(&mut session, &data)?;
17990    let outcome = session.finalize().map_err(javascript_crypto_cipher_error)?;
17991    output.extend(outcome.data);
17992    if !decrypt {
17993        if let Some(auth_tag) = outcome.auth_tag {
17994            output.extend(auth_tag);
17995        }
17996    }
17997    Ok(Value::String(
17998        serde_json::to_string(&json!({
17999            "data": base64::engine::general_purpose::STANDARD.encode(output),
18000        }))
18001        .map_err(|error| {
18002            SidecarError::InvalidState(format!("serialize crypto.subtle {op}: {error}"))
18003        })?,
18004    ))
18005}
18006
18007fn javascript_crypto_subtle_aes_gcm_tag_len(algorithm: &Value) -> Result<usize, SidecarError> {
18008    let tag_bits = algorithm
18009        .get("tagLength")
18010        .and_then(Value::as_u64)
18011        .unwrap_or(128);
18012    if !tag_bits.is_multiple_of(8) {
18013        return Err(SidecarError::InvalidState(String::from(
18014            "crypto.subtle AES-GCM tagLength must be byte-aligned",
18015        )));
18016    }
18017    usize::try_from(tag_bits / 8).map_err(|_| {
18018        SidecarError::InvalidState(String::from("crypto.subtle AES-GCM tagLength too large"))
18019    })
18020}
18021
18022fn service_javascript_crypto_cipheriv_inner(
18023    request: &JavascriptSyncRpcRequest,
18024    decrypt: bool,
18025) -> Result<Value, SidecarError> {
18026    let label = if decrypt {
18027        "crypto.decipheriv"
18028    } else {
18029        "crypto.cipheriv"
18030    };
18031    let algorithm = javascript_sync_rpc_arg_str(&request.args, 0, &format!("{label} algorithm"))?;
18032    let key = javascript_sync_rpc_base64_arg(&request.args, 1, &format!("{label} key"))?;
18033    let iv = javascript_sync_rpc_base64_arg_optional(&request.args, 2, &format!("{label} iv"))?;
18034    let data = javascript_sync_rpc_base64_arg(&request.args, 3, &format!("{label} data"))?;
18035    let options =
18036        javascript_sync_rpc_json_arg_optional(&request.args, 4, &format!("{label} options"))?;
18037    let mut session = javascript_crypto_build_cipher_session(
18038        algorithm,
18039        &key,
18040        iv.as_deref(),
18041        decrypt,
18042        options.as_ref(),
18043    )?;
18044    let payload = javascript_crypto_cipher_update(&mut session, &data)?;
18045    let outcome = session.finalize().map_err(javascript_crypto_cipher_error)?;
18046    if decrypt {
18047        let mut output = payload;
18048        output.extend(outcome.data);
18049        return Ok(Value::String(
18050            base64::engine::general_purpose::STANDARD.encode(output),
18051        ));
18052    }
18053
18054    let mut response = Map::new();
18055    let mut encrypted = payload;
18056    encrypted.extend(outcome.data);
18057    response.insert(
18058        String::from("data"),
18059        Value::String(base64::engine::general_purpose::STANDARD.encode(encrypted)),
18060    );
18061    if let Some(auth_tag) = outcome.auth_tag {
18062        response.insert(
18063            String::from("authTag"),
18064            Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
18065        );
18066    }
18067    Ok(Value::String(serde_json::to_string(&response).map_err(
18068        |error| SidecarError::InvalidState(format!("serialize {label} response: {error}")),
18069    )?))
18070}
18071
18072fn javascript_sync_rpc_base64_arg_optional(
18073    args: &[Value],
18074    index: usize,
18075    label: &str,
18076) -> Result<Option<Vec<u8>>, SidecarError> {
18077    if args.get(index).is_none() || args[index].is_null() {
18078        return Ok(None);
18079    }
18080    javascript_sync_rpc_base64_arg(args, index, label).map(Some)
18081}
18082
18083fn javascript_sync_rpc_json_arg_optional(
18084    args: &[Value],
18085    index: usize,
18086    label: &str,
18087) -> Result<Option<Value>, SidecarError> {
18088    if args.get(index).is_none() || args[index].is_null() {
18089        return Ok(None);
18090    }
18091    let raw = javascript_sync_rpc_arg_str(args, index, label)?;
18092    serde_json::from_str(raw)
18093        .map(Some)
18094        .map_err(|error| SidecarError::InvalidState(format!("{label} must be valid JSON: {error}")))
18095}
18096
18097fn javascript_crypto_parse_direct_key_input(
18098    raw: &str,
18099    expected: Option<&str>,
18100    label: &str,
18101) -> Result<JavascriptDirectKeyInput, SidecarError> {
18102    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
18103        SidecarError::InvalidState(format!("{label} must be valid JSON: {error}"))
18104    })?;
18105    let padding = match parsed.as_object().and_then(|value| value.get("padding")) {
18106        Some(value) => javascript_crypto_padding_from_value(value)?,
18107        None => None,
18108    };
18109    Ok(JavascriptDirectKeyInput {
18110        key: javascript_crypto_parse_key_material_value(&parsed, expected, label)?,
18111        padding,
18112    })
18113}
18114
18115fn javascript_crypto_parse_key_material_value(
18116    value: &Value,
18117    expected: Option<&str>,
18118    label: &str,
18119) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
18120    if let Some(object) = value.as_object() {
18121        if object.get("__type").and_then(Value::as_str) == Some("keyObject") {
18122            let serialized = object.get("value").ok_or_else(|| {
18123                SidecarError::InvalidState(format!("{label} keyObject is missing a value"))
18124            })?;
18125            return javascript_crypto_parse_serialized_key_object(serialized, expected, label);
18126        }
18127        if object.contains_key("type") && (object.contains_key("pem") || object.contains_key("raw"))
18128        {
18129            return javascript_crypto_parse_serialized_key_object(value, expected, label);
18130        }
18131        if let Some(source) = object.get("key") {
18132            return javascript_crypto_parse_key_source(
18133                source,
18134                object.get("format").and_then(Value::as_str),
18135                object.get("type").and_then(Value::as_str),
18136                expected,
18137                label,
18138            );
18139        }
18140    }
18141    javascript_crypto_parse_key_source(value, None, None, expected, label)
18142}
18143
18144fn javascript_crypto_parse_key_source(
18145    source: &Value,
18146    format: Option<&str>,
18147    kind: Option<&str>,
18148    expected: Option<&str>,
18149    label: &str,
18150) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
18151    match source {
18152        Value::String(pem) => javascript_crypto_parse_key_from_pem(pem.as_bytes(), expected, label),
18153        Value::Object(object) if object.get("__type").and_then(Value::as_str) == Some("buffer") => {
18154            let data = javascript_crypto_decode_bridge_buffer(source, label)?;
18155            javascript_crypto_parse_key_from_bytes(&data, format, kind, expected, label)
18156        }
18157        Value::Object(_) => {
18158            if format == Some("jwk") {
18159                return Err(SidecarError::InvalidState(format!(
18160                    "{label} jwk inputs are not supported yet"
18161                )));
18162            }
18163            Err(SidecarError::InvalidState(format!(
18164                "{label} has an unsupported key shape"
18165            )))
18166        }
18167        _ => Err(SidecarError::InvalidState(format!(
18168            "{label} has an unsupported key value"
18169        ))),
18170    }
18171}
18172
18173fn javascript_crypto_parse_key_from_pem(
18174    pem: &[u8],
18175    expected: Option<&str>,
18176    label: &str,
18177) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
18178    match expected {
18179        Some("private") => PKey::private_key_from_pem(pem)
18180            .map(JavascriptCryptoKeyMaterial::Private)
18181            .map_err(|error| {
18182                SidecarError::InvalidState(format!("{label} private key is invalid: {error}"))
18183            }),
18184        Some("public") => PKey::public_key_from_pem(pem)
18185            .map(JavascriptCryptoKeyMaterial::Public)
18186            .map_err(|error| {
18187                SidecarError::InvalidState(format!("{label} public key is invalid: {error}"))
18188            }),
18189        _ => PKey::private_key_from_pem(pem)
18190            .map(JavascriptCryptoKeyMaterial::Private)
18191            .or_else(|_| PKey::public_key_from_pem(pem).map(JavascriptCryptoKeyMaterial::Public))
18192            .map_err(|error| {
18193                SidecarError::InvalidState(format!("{label} PEM key is invalid: {error}"))
18194            }),
18195    }
18196}
18197
18198fn javascript_crypto_parse_key_from_bytes(
18199    der: &[u8],
18200    format: Option<&str>,
18201    kind: Option<&str>,
18202    expected: Option<&str>,
18203    label: &str,
18204) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
18205    match (format.unwrap_or("der"), kind.or(expected)) {
18206        ("der", Some("pkcs8")) | ("der", Some("private")) => PKey::private_key_from_der(der)
18207            .map(JavascriptCryptoKeyMaterial::Private)
18208            .map_err(|error| {
18209                SidecarError::InvalidState(format!("{label} private key DER is invalid: {error}"))
18210            }),
18211        ("der", Some("spki")) | ("der", Some("public")) => PKey::public_key_from_der(der)
18212            .map(JavascriptCryptoKeyMaterial::Public)
18213            .map_err(|error| {
18214                SidecarError::InvalidState(format!("{label} public key DER is invalid: {error}"))
18215            }),
18216        _ => Err(SidecarError::InvalidState(format!(
18217            "{label} unsupported key bytes format"
18218        ))),
18219    }
18220}
18221
18222fn javascript_crypto_parse_serialized_key_object(
18223    value: &Value,
18224    expected: Option<&str>,
18225    label: &str,
18226) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
18227    let serialized: JavascriptSerializedSandboxKeyObject = serde_json::from_value(value.clone())
18228        .map_err(|error| {
18229            SidecarError::InvalidState(format!("{label} keyObject is invalid: {error}"))
18230        })?;
18231    match serialized.kind.as_str() {
18232        "secret" => {
18233            if expected == Some("public") || expected == Some("private") {
18234                return Err(SidecarError::InvalidState(format!(
18235                    "{label} expected an asymmetric key"
18236                )));
18237            }
18238            Ok(JavascriptCryptoKeyMaterial::Secret(
18239                base64::engine::general_purpose::STANDARD
18240                    .decode(serialized.raw.unwrap_or_default())
18241                    .map_err(|error| {
18242                        SidecarError::InvalidState(format!(
18243                            "{label} secret key contains invalid base64: {error}"
18244                        ))
18245                    })?,
18246            ))
18247        }
18248        "private" => {
18249            let pem = serialized.pem.ok_or_else(|| {
18250                SidecarError::InvalidState(format!("{label} private keyObject is missing pem"))
18251            })?;
18252            javascript_crypto_parse_key_from_pem(pem.as_bytes(), Some("private"), label)
18253        }
18254        "public" => {
18255            let pem = serialized.pem.ok_or_else(|| {
18256                SidecarError::InvalidState(format!("{label} public keyObject is missing pem"))
18257            })?;
18258            javascript_crypto_parse_key_from_pem(pem.as_bytes(), Some("public"), label)
18259        }
18260        other => Err(SidecarError::InvalidState(format!(
18261            "{label} has unsupported keyObject type {other}"
18262        ))),
18263    }
18264}
18265
18266fn javascript_crypto_expect_private_key(
18267    key: JavascriptCryptoKeyMaterial,
18268    label: &str,
18269) -> Result<PKey<Private>, SidecarError> {
18270    match key {
18271        JavascriptCryptoKeyMaterial::Private(key) => Ok(key),
18272        _ => Err(SidecarError::InvalidState(format!(
18273            "{label} requires a private key"
18274        ))),
18275    }
18276}
18277
18278fn javascript_crypto_expect_public_key(
18279    key: JavascriptCryptoKeyMaterial,
18280    label: &str,
18281) -> Result<PKey<Public>, SidecarError> {
18282    match key {
18283        JavascriptCryptoKeyMaterial::Public(key) => Ok(key),
18284        JavascriptCryptoKeyMaterial::Private(key) => {
18285            let pem = key
18286                .public_key_to_pem()
18287                .map_err(javascript_crypto_openssl_error)?;
18288            PKey::public_key_from_pem(&pem).map_err(javascript_crypto_openssl_error)
18289        }
18290        _ => Err(SidecarError::InvalidState(format!(
18291            "{label} requires a public key"
18292        ))),
18293    }
18294}
18295
18296fn javascript_crypto_new_signer<'a>(
18297    algorithm: Option<&'a str>,
18298    key: &'a PKey<Private>,
18299) -> Result<Signer<'a>, SidecarError> {
18300    if matches!(key.id(), PKeyId::ED25519 | PKeyId::ED448) || algorithm.is_none() {
18301        return Signer::new_without_digest(key).map_err(javascript_crypto_openssl_error);
18302    }
18303    Signer::new(
18304        javascript_crypto_message_digest_from_name(algorithm.ok_or_else(|| {
18305            SidecarError::InvalidState(String::from("crypto.sign requires a digest algorithm"))
18306        })?)?,
18307        key,
18308    )
18309    .map_err(javascript_crypto_openssl_error)
18310}
18311
18312fn javascript_crypto_new_verifier<'a>(
18313    algorithm: Option<&'a str>,
18314    key: &'a PKey<Public>,
18315) -> Result<Verifier<'a>, SidecarError> {
18316    if matches!(key.id(), PKeyId::ED25519 | PKeyId::ED448) || algorithm.is_none() {
18317        return Verifier::new_without_digest(key).map_err(javascript_crypto_openssl_error);
18318    }
18319    Verifier::new(
18320        javascript_crypto_message_digest_from_name(algorithm.ok_or_else(|| {
18321            SidecarError::InvalidState(String::from("crypto.verify requires a digest algorithm"))
18322        })?)?,
18323        key,
18324    )
18325    .map_err(javascript_crypto_openssl_error)
18326}
18327
18328fn javascript_crypto_message_digest_from_name(name: &str) -> Result<MessageDigest, SidecarError> {
18329    match name.trim().to_ascii_lowercase().replace('-', "").as_str() {
18330        "md5" => Ok(MessageDigest::md5()),
18331        "sha1" => Ok(MessageDigest::sha1()),
18332        "sha256" => Ok(MessageDigest::sha256()),
18333        "sha384" => Ok(MessageDigest::sha384()),
18334        "sha512" => Ok(MessageDigest::sha512()),
18335        other => Err(SidecarError::InvalidState(format!(
18336            "unsupported crypto digest algorithm {other}"
18337        ))),
18338    }
18339}
18340
18341fn javascript_crypto_padding_from_value(value: &Value) -> Result<Option<Padding>, SidecarError> {
18342    let Some(number) = value.as_i64() else {
18343        return Ok(None);
18344    };
18345    let padding = match number {
18346        1 => Padding::PKCS1,
18347        3 => Padding::NONE,
18348        4 => Padding::PKCS1_OAEP,
18349        6 => Padding::PKCS1_PSS,
18350        other => {
18351            return Err(SidecarError::InvalidState(format!(
18352                "unsupported RSA padding constant {other}"
18353            )));
18354        }
18355    };
18356    Ok(Some(padding))
18357}
18358
18359fn javascript_crypto_decode_bridge_buffer(
18360    value: &Value,
18361    label: &str,
18362) -> Result<Vec<u8>, SidecarError> {
18363    decode_bridge_buffer_value(value)
18364        .map_err(|error| SidecarError::InvalidState(format!("{label} {error}")))
18365}
18366
18367fn javascript_crypto_serialize_sandbox_key_object(
18368    key: &JavascriptCryptoKeyMaterial,
18369) -> Result<Value, SidecarError> {
18370    let serialized = match key {
18371        JavascriptCryptoKeyMaterial::Private(key) => JavascriptSerializedSandboxKeyObject {
18372            kind: String::from("private"),
18373            pem: Some(
18374                String::from_utf8(
18375                    key.private_key_to_pem_pkcs8()
18376                        .map_err(javascript_crypto_openssl_error)?,
18377                )
18378                .map_err(|error| {
18379                    SidecarError::InvalidState(format!("private key PEM is not utf8: {error}"))
18380                })?,
18381            ),
18382            raw: None,
18383            asymmetric_key_type: javascript_crypto_pkey_type_name(key.id()),
18384            asymmetric_key_details: None,
18385            jwk: None,
18386        },
18387        JavascriptCryptoKeyMaterial::Public(key) => JavascriptSerializedSandboxKeyObject {
18388            kind: String::from("public"),
18389            pem: Some(
18390                String::from_utf8(
18391                    key.public_key_to_pem()
18392                        .map_err(javascript_crypto_openssl_error)?,
18393                )
18394                .map_err(|error| {
18395                    SidecarError::InvalidState(format!("public key PEM is not utf8: {error}"))
18396                })?,
18397            ),
18398            raw: None,
18399            asymmetric_key_type: javascript_crypto_pkey_type_name(key.id()),
18400            asymmetric_key_details: None,
18401            jwk: None,
18402        },
18403        JavascriptCryptoKeyMaterial::Secret(raw) => JavascriptSerializedSandboxKeyObject {
18404            kind: String::from("secret"),
18405            pem: None,
18406            raw: Some(base64::engine::general_purpose::STANDARD.encode(raw)),
18407            asymmetric_key_type: None,
18408            asymmetric_key_details: None,
18409            jwk: None,
18410        },
18411    };
18412    serde_json::to_value(serialized)
18413        .map_err(|error| SidecarError::InvalidState(format!("serialize key object: {error}")))
18414}
18415
18416fn javascript_crypto_pkey_type_name(id: PKeyId) -> Option<String> {
18417    match id {
18418        PKeyId::RSA => Some(String::from("rsa")),
18419        PKeyId::EC => Some(String::from("ec")),
18420        PKeyId::ED25519 => Some(String::from("ed25519")),
18421        PKeyId::ED448 => Some(String::from("ed448")),
18422        PKeyId::X25519 => Some(String::from("x25519")),
18423        PKeyId::X448 => Some(String::from("x448")),
18424        PKeyId::DH => Some(String::from("dh")),
18425        _ => None,
18426    }
18427}
18428
18429fn javascript_crypto_rsa_output_size(
18430    key: &JavascriptCryptoKeyMaterial,
18431) -> Result<usize, SidecarError> {
18432    match key {
18433        JavascriptCryptoKeyMaterial::Private(key) => key
18434            .rsa()
18435            .map(|rsa| rsa.size() as usize)
18436            .map_err(javascript_crypto_openssl_error),
18437        JavascriptCryptoKeyMaterial::Public(key) => key
18438            .rsa()
18439            .map(|rsa| rsa.size() as usize)
18440            .map_err(javascript_crypto_openssl_error),
18441        JavascriptCryptoKeyMaterial::Secret(_) => Err(SidecarError::InvalidState(String::from(
18442            "RSA operations require an asymmetric key",
18443        ))),
18444    }
18445}
18446
18447fn javascript_crypto_parse_serialized_options_arg(
18448    args: &[Value],
18449    index: usize,
18450    label: &str,
18451) -> Result<Option<Value>, SidecarError> {
18452    let Some(raw) = args.get(index).and_then(Value::as_str) else {
18453        return Ok(None);
18454    };
18455    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
18456        SidecarError::InvalidState(format!("{label} must be valid JSON: {error}"))
18457    })?;
18458    if parsed.get("hasOptions").and_then(Value::as_bool) == Some(true) {
18459        Ok(parsed.get("options").cloned())
18460    } else {
18461        Ok(None)
18462    }
18463}
18464
18465fn javascript_crypto_u32_from_bridge_value(
18466    value: &Value,
18467    label: &str,
18468) -> Result<u32, SidecarError> {
18469    if let Some(number) = value.as_u64() {
18470        return u32::try_from(number)
18471            .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")));
18472    }
18473    let bytes = javascript_crypto_decode_bridge_buffer(value, label)?;
18474    if bytes.len() > 4 {
18475        return Err(SidecarError::InvalidState(format!(
18476            "{label} buffer is too large for u32"
18477        )));
18478    }
18479    Ok(bytes
18480        .into_iter()
18481        .fold(0_u32, |acc, byte| (acc << 8) | u32::from(byte)))
18482}
18483
18484fn javascript_crypto_bignum_from_bridge_value(
18485    value: &Value,
18486    label: &str,
18487) -> Result<BigNum, SidecarError> {
18488    if let Some(object) = value.as_object() {
18489        if object.get("__type").and_then(Value::as_str) == Some("bigint") {
18490            let decimal = object.get("value").and_then(Value::as_str).ok_or_else(|| {
18491                SidecarError::InvalidState(format!("{label} bigint is missing a value"))
18492            })?;
18493            return BigNum::from_dec_str(decimal).map_err(javascript_crypto_openssl_error);
18494        }
18495    }
18496    let bytes = javascript_crypto_decode_bridge_buffer(value, label)?;
18497    BigNum::from_slice(&bytes).map_err(javascript_crypto_openssl_error)
18498}
18499
18500fn javascript_crypto_curve_nid(name: &str) -> Result<Nid, SidecarError> {
18501    match name {
18502        "prime256v1" | "P-256" => Ok(Nid::X9_62_PRIME256V1),
18503        "secp384r1" | "P-384" => Ok(Nid::SECP384R1),
18504        "secp521r1" | "P-521" => Ok(Nid::SECP521R1),
18505        "secp256k1" => Ok(Nid::SECP256K1),
18506        other => Err(SidecarError::InvalidState(format!(
18507            "unsupported EC curve {other}"
18508        ))),
18509    }
18510}
18511
18512fn javascript_crypto_named_dh_group(name: &str) -> Result<Dh<Params>, SidecarError> {
18513    match name {
18514        "modp2" => Dh::get_1024_160().map_err(javascript_crypto_openssl_error),
18515        "modp14" | "modp15" | "modp16" | "modp17" | "modp18" => {
18516            Dh::get_2048_256().map_err(javascript_crypto_openssl_error)
18517        }
18518        other => Err(SidecarError::InvalidState(format!(
18519            "unsupported Diffie-Hellman group {other}"
18520        ))),
18521    }
18522}
18523
18524fn javascript_crypto_clone_dh_params(params: &Dh<Params>) -> Result<Dh<Params>, SidecarError> {
18525    Dh::from_pqg(
18526        params
18527            .prime_p()
18528            .to_owned()
18529            .map_err(javascript_crypto_openssl_error)?,
18530        params
18531            .prime_q()
18532            .map(|value| value.to_owned().map_err(javascript_crypto_openssl_error))
18533            .transpose()?,
18534        params
18535            .generator()
18536            .to_owned()
18537            .map_err(javascript_crypto_openssl_error)?,
18538    )
18539    .map_err(javascript_crypto_openssl_error)
18540}
18541
18542fn javascript_crypto_build_dh_params(args: &[Value]) -> Result<Dh<Params>, SidecarError> {
18543    let Some(first) = args.first() else {
18544        return Err(SidecarError::InvalidState(String::from(
18545            "Diffie-Hellman session args are required",
18546        )));
18547    };
18548    if let Some(bits) = first.as_u64() {
18549        let generator = args
18550            .get(1)
18551            .map(|value| javascript_crypto_u32_from_bridge_value(value, "Diffie-Hellman generator"))
18552            .transpose()?
18553            .unwrap_or(2);
18554        return Dh::generate_params(bits as u32, generator)
18555            .map_err(javascript_crypto_openssl_error);
18556    }
18557    let prime = javascript_crypto_bignum_from_bridge_value(first, "Diffie-Hellman prime")?;
18558    let generator = args
18559        .get(1)
18560        .map(|value| javascript_crypto_bignum_from_bridge_value(value, "Diffie-Hellman generator"))
18561        .transpose()?
18562        .unwrap_or(BigNum::from_u32(2).map_err(javascript_crypto_openssl_error)?);
18563    Dh::from_pqg(prime, None, generator).map_err(javascript_crypto_openssl_error)
18564}
18565
18566fn javascript_crypto_call_dh_session(
18567    session: &mut ActiveDhSession,
18568    method: &str,
18569    args: &[Value],
18570) -> Result<(Value, bool), SidecarError> {
18571    match method {
18572        "verifyError" => Ok((Value::Null, false)),
18573        "generateKeys" => {
18574            if session.key_pair.is_none() {
18575                session.key_pair = Some(
18576                    javascript_crypto_clone_dh_params(&session.params)?
18577                        .generate_key()
18578                        .map_err(javascript_crypto_openssl_error)?,
18579                );
18580            }
18581            let public = session
18582                .key_pair
18583                .as_ref()
18584                .expect("dh key pair")
18585                .public_key()
18586                .to_vec();
18587            Ok((javascript_crypto_bridge_buffer_value(&public), true))
18588        }
18589        "computeSecret" => {
18590            if session.key_pair.is_none() {
18591                session.key_pair = Some(
18592                    javascript_crypto_clone_dh_params(&session.params)?
18593                        .generate_key()
18594                        .map_err(javascript_crypto_openssl_error)?,
18595                );
18596            }
18597            let peer = javascript_crypto_bignum_from_bridge_value(
18598                args.first().ok_or_else(|| {
18599                    SidecarError::InvalidState(String::from(
18600                        "computeSecret requires peer public key",
18601                    ))
18602                })?,
18603                "Diffie-Hellman peer public key",
18604            )?;
18605            let private_key = session
18606                .key_pair
18607                .as_ref()
18608                .expect("dh key pair")
18609                .private_key();
18610            let mut secret = BigNum::new().map_err(javascript_crypto_openssl_error)?;
18611            let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
18612            secret
18613                .mod_exp(&peer, private_key, session.params.prime_p(), &mut ctx)
18614                .map_err(javascript_crypto_openssl_error)?;
18615            Ok((
18616                javascript_crypto_bridge_buffer_value(
18617                    &secret
18618                        .to_vec_padded(session.params.prime_p().num_bytes())
18619                        .map_err(javascript_crypto_openssl_error)?,
18620                ),
18621                true,
18622            ))
18623        }
18624        "getPrime" => Ok((
18625            javascript_crypto_bridge_buffer_value(&session.params.prime_p().to_vec()),
18626            true,
18627        )),
18628        "getGenerator" => Ok((
18629            javascript_crypto_bridge_buffer_value(&session.params.generator().to_vec()),
18630            true,
18631        )),
18632        "getPublicKey" => {
18633            if session.key_pair.is_none() {
18634                session.key_pair = Some(
18635                    javascript_crypto_clone_dh_params(&session.params)?
18636                        .generate_key()
18637                        .map_err(javascript_crypto_openssl_error)?,
18638                );
18639            }
18640            Ok((
18641                javascript_crypto_bridge_buffer_value(
18642                    &session
18643                        .key_pair
18644                        .as_ref()
18645                        .expect("dh key pair")
18646                        .public_key()
18647                        .to_vec(),
18648                ),
18649                true,
18650            ))
18651        }
18652        "getPrivateKey" => {
18653            if session.key_pair.is_none() {
18654                session.key_pair = Some(
18655                    javascript_crypto_clone_dh_params(&session.params)?
18656                        .generate_key()
18657                        .map_err(javascript_crypto_openssl_error)?,
18658                );
18659            }
18660            Ok((
18661                javascript_crypto_bridge_buffer_value(
18662                    &session
18663                        .key_pair
18664                        .as_ref()
18665                        .expect("dh key pair")
18666                        .private_key()
18667                        .to_vec(),
18668                ),
18669                true,
18670            ))
18671        }
18672        "setPrivateKey" => {
18673            let private_key = javascript_crypto_bignum_from_bridge_value(
18674                args.first().ok_or_else(|| {
18675                    SidecarError::InvalidState(String::from("setPrivateKey requires private key"))
18676                })?,
18677                "Diffie-Hellman private key",
18678            )?;
18679            let mut public_key = BigNum::new().map_err(javascript_crypto_openssl_error)?;
18680            let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
18681            public_key
18682                .mod_exp(
18683                    session.params.generator(),
18684                    &private_key,
18685                    session.params.prime_p(),
18686                    &mut ctx,
18687                )
18688                .map_err(javascript_crypto_openssl_error)?;
18689            session.key_pair = Some(
18690                javascript_crypto_clone_dh_params(&session.params)?
18691                    .set_key(public_key, private_key)
18692                    .map_err(javascript_crypto_openssl_error)?,
18693            );
18694            Ok((Value::Null, false))
18695        }
18696        "setPublicKey" => {
18697            let public_key = javascript_crypto_bignum_from_bridge_value(
18698                args.first().ok_or_else(|| {
18699                    SidecarError::InvalidState(String::from("setPublicKey requires public key"))
18700                })?,
18701                "Diffie-Hellman public key",
18702            )?;
18703            let private_key = session
18704                .key_pair
18705                .as_ref()
18706                .ok_or_else(|| {
18707                    SidecarError::InvalidState(String::from(
18708                        "setPublicKey requires private key to be set first",
18709                    ))
18710                })?
18711                .private_key()
18712                .to_owned()
18713                .map_err(javascript_crypto_openssl_error)?;
18714            session.key_pair = Some(
18715                javascript_crypto_clone_dh_params(&session.params)?
18716                    .set_key(public_key, private_key)
18717                    .map_err(javascript_crypto_openssl_error)?,
18718            );
18719            Ok((Value::Null, false))
18720        }
18721        other => Err(SidecarError::InvalidState(format!(
18722            "Unsupported Diffie-Hellman method: {other}"
18723        ))),
18724    }
18725}
18726
18727fn javascript_crypto_call_ecdh_session(
18728    session: &mut ActiveEcdhSession,
18729    method: &str,
18730    args: &[Value],
18731) -> Result<(Value, bool), SidecarError> {
18732    let nid = javascript_crypto_curve_nid(&session.curve)?;
18733    let group = EcGroup::from_curve_name(nid).map_err(javascript_crypto_openssl_error)?;
18734    match method {
18735        "verifyError" => Ok((Value::Null, false)),
18736        "generateKeys" => {
18737            if session.key_pair.is_none() {
18738                session.key_pair =
18739                    Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
18740            }
18741            let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
18742            let bytes = session
18743                .key_pair
18744                .as_ref()
18745                .expect("ecdh key pair")
18746                .public_key()
18747                .to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx)
18748                .map_err(javascript_crypto_openssl_error)?;
18749            Ok((javascript_crypto_bridge_buffer_value(&bytes), true))
18750        }
18751        "computeSecret" => {
18752            if session.key_pair.is_none() {
18753                session.key_pair =
18754                    Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
18755            }
18756            let peer_bytes = javascript_crypto_decode_bridge_buffer(
18757                args.first().ok_or_else(|| {
18758                    SidecarError::InvalidState(String::from(
18759                        "computeSecret requires peer public key",
18760                    ))
18761                })?,
18762                "ECDH peer public key",
18763            )?;
18764            let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
18765            let peer_point = EcPoint::from_bytes(&group, &peer_bytes, &mut ctx)
18766                .map_err(javascript_crypto_openssl_error)?;
18767            let peer_key = EcKey::from_public_key(&group, &peer_point)
18768                .map_err(javascript_crypto_openssl_error)?;
18769            let private =
18770                PKey::from_ec_key(session.key_pair.as_ref().expect("ecdh key pair").to_owned())
18771                    .map_err(javascript_crypto_openssl_error)?;
18772            let peer = PKey::from_ec_key(peer_key).map_err(javascript_crypto_openssl_error)?;
18773            let mut deriver = Deriver::new(&private).map_err(javascript_crypto_openssl_error)?;
18774            deriver
18775                .set_peer(&peer)
18776                .map_err(javascript_crypto_openssl_error)?;
18777            let secret = deriver
18778                .derive_to_vec()
18779                .map_err(javascript_crypto_openssl_error)?;
18780            Ok((javascript_crypto_bridge_buffer_value(&secret), true))
18781        }
18782        "getPublicKey" => {
18783            if session.key_pair.is_none() {
18784                session.key_pair =
18785                    Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
18786            }
18787            let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
18788            let bytes = session
18789                .key_pair
18790                .as_ref()
18791                .expect("ecdh key pair")
18792                .public_key()
18793                .to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx)
18794                .map_err(javascript_crypto_openssl_error)?;
18795            Ok((javascript_crypto_bridge_buffer_value(&bytes), true))
18796        }
18797        "getPrivateKey" => {
18798            if session.key_pair.is_none() {
18799                session.key_pair =
18800                    Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
18801            }
18802            Ok((
18803                javascript_crypto_bridge_buffer_value(
18804                    &session
18805                        .key_pair
18806                        .as_ref()
18807                        .expect("ecdh key pair")
18808                        .private_key()
18809                        .to_vec(),
18810                ),
18811                true,
18812            ))
18813        }
18814        "setPrivateKey" => {
18815            let private_key = javascript_crypto_bignum_from_bridge_value(
18816                args.first().ok_or_else(|| {
18817                    SidecarError::InvalidState(String::from("setPrivateKey requires private key"))
18818                })?,
18819                "ECDH private key",
18820            )?;
18821            let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
18822            let mut public_key = EcPoint::new(&group).map_err(javascript_crypto_openssl_error)?;
18823            public_key
18824                .mul_generator2(&group, &private_key, &mut ctx)
18825                .map_err(javascript_crypto_openssl_error)?;
18826            session.key_pair = Some(
18827                EcKey::from_private_components(&group, &private_key, &public_key)
18828                    .map_err(javascript_crypto_openssl_error)?,
18829            );
18830            Ok((Value::Null, false))
18831        }
18832        "setPublicKey" => {
18833            let public_key_bytes = javascript_crypto_decode_bridge_buffer(
18834                args.first().ok_or_else(|| {
18835                    SidecarError::InvalidState(String::from("setPublicKey requires public key"))
18836                })?,
18837                "ECDH public key",
18838            )?;
18839            let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
18840            let public_key = EcPoint::from_bytes(&group, &public_key_bytes, &mut ctx)
18841                .map_err(javascript_crypto_openssl_error)?;
18842            let private_key = session
18843                .key_pair
18844                .as_ref()
18845                .ok_or_else(|| {
18846                    SidecarError::InvalidState(String::from(
18847                        "setPublicKey requires private key to be set first",
18848                    ))
18849                })?
18850                .private_key()
18851                .to_owned()
18852                .map_err(javascript_crypto_openssl_error)?;
18853            session.key_pair = Some(
18854                EcKey::from_private_components(&group, &private_key, &public_key)
18855                    .map_err(javascript_crypto_openssl_error)?,
18856            );
18857            Ok((Value::Null, false))
18858        }
18859        other => Err(SidecarError::InvalidState(format!(
18860            "Unsupported Diffie-Hellman method: {other}"
18861        ))),
18862    }
18863}
18864
18865fn javascript_crypto_serialize_encoded_key_value_public(
18866    key: &PKey<Public>,
18867    encoding: Option<&Value>,
18868) -> Result<Value, SidecarError> {
18869    if let Some(encoding) = encoding {
18870        let format = encoding
18871            .get("format")
18872            .and_then(Value::as_str)
18873            .unwrap_or("pem");
18874        return Ok(match format {
18875            "der" => json!({
18876                "kind": "buffer",
18877                "value": base64::engine::general_purpose::STANDARD
18878                    .encode(key.public_key_to_der().map_err(javascript_crypto_openssl_error)?),
18879            }),
18880            _ => json!({
18881                "kind": "string",
18882                "value": String::from_utf8(
18883                    key.public_key_to_pem().map_err(javascript_crypto_openssl_error)?,
18884                )
18885                .map_err(|error| SidecarError::InvalidState(format!("public key PEM utf8: {error}")))?,
18886            }),
18887        });
18888    }
18889    javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Public(
18890        key.to_owned(),
18891    ))
18892}
18893
18894fn javascript_crypto_serialize_encoded_key_value_private(
18895    key: &PKey<Private>,
18896    encoding: Option<&Value>,
18897) -> Result<Value, SidecarError> {
18898    if let Some(encoding) = encoding {
18899        let format = encoding
18900            .get("format")
18901            .and_then(Value::as_str)
18902            .unwrap_or("pem");
18903        return Ok(match format {
18904            "der" => json!({
18905                "kind": "buffer",
18906                "value": base64::engine::general_purpose::STANDARD
18907                    .encode(key.private_key_to_der().map_err(javascript_crypto_openssl_error)?),
18908            }),
18909            _ => json!({
18910                "kind": "string",
18911                "value": String::from_utf8(
18912                    key.private_key_to_pem_pkcs8().map_err(javascript_crypto_openssl_error)?,
18913                )
18914                .map_err(|error| SidecarError::InvalidState(format!("private key PEM utf8: {error}")))?,
18915            }),
18916        });
18917    }
18918    javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Private(
18919        key.to_owned(),
18920    ))
18921}
18922
18923fn javascript_crypto_bridge_buffer_value(bytes: &[u8]) -> Value {
18924    bridge_buffer_value(bytes)
18925}
18926
18927fn javascript_crypto_cipher_error(error: AesCipherError) -> SidecarError {
18928    SidecarError::InvalidState(error.0)
18929}
18930
18931fn javascript_crypto_decode_cipher_option_b64(
18932    options: Option<&Value>,
18933    field: &str,
18934) -> Result<Option<Vec<u8>>, SidecarError> {
18935    let Some(encoded) = options
18936        .and_then(|value| value.get(field))
18937        .and_then(Value::as_str)
18938    else {
18939        return Ok(None);
18940    };
18941    base64::engine::general_purpose::STANDARD
18942        .decode(encoded)
18943        .map(Some)
18944        .map_err(|error| {
18945            SidecarError::InvalidState(format!("cipher {field} contains invalid base64: {error}"))
18946        })
18947}
18948
18949fn javascript_crypto_build_cipher_session(
18950    algorithm: &str,
18951    key: &[u8],
18952    iv: Option<&[u8]>,
18953    decrypt: bool,
18954    options: Option<&Value>,
18955) -> Result<StreamCipherSession, SidecarError> {
18956    let pad = options
18957        .and_then(|value| value.get("autoPadding"))
18958        .and_then(Value::as_bool)
18959        .unwrap_or(true);
18960    let aad = if javascript_crypto_is_aead(algorithm) {
18961        javascript_crypto_decode_cipher_option_b64(options, "aad")?
18962    } else {
18963        None
18964    };
18965    let auth_tag = if decrypt && javascript_crypto_is_aead(algorithm) {
18966        javascript_crypto_decode_cipher_option_b64(options, "authTag")?
18967    } else {
18968        None
18969    };
18970    let tag_len = javascript_crypto_requested_aead_tag_len(algorithm, options)?;
18971    StreamCipherSession::new(
18972        algorithm,
18973        key,
18974        iv,
18975        decrypt,
18976        pad,
18977        aad.as_deref(),
18978        auth_tag.as_deref(),
18979        tag_len,
18980    )
18981    .map_err(javascript_crypto_cipher_error)
18982}
18983
18984fn javascript_crypto_requested_aead_tag_len(
18985    algorithm: &str,
18986    options: Option<&Value>,
18987) -> Result<usize, SidecarError> {
18988    if !javascript_crypto_is_aead(algorithm) {
18989        return Ok(0);
18990    }
18991    let requested = options
18992        .and_then(|value| value.get("authTagLength"))
18993        .and_then(Value::as_u64)
18994        .unwrap_or(javascript_crypto_aead_tag_len(algorithm) as u64);
18995    usize::try_from(requested).map_err(|_| {
18996        SidecarError::InvalidState(String::from("cipher authTagLength must fit within usize"))
18997    })
18998}
18999
19000fn javascript_crypto_cipher_update(
19001    session: &mut StreamCipherSession,
19002    data: &[u8],
19003) -> Result<Vec<u8>, SidecarError> {
19004    session.update(data).map_err(javascript_crypto_cipher_error)
19005}
19006
19007fn javascript_crypto_is_aead(algorithm: &str) -> bool {
19008    crate::crypto_cipher::is_aead(algorithm)
19009}
19010
19011fn javascript_crypto_aead_tag_len(_algorithm: &str) -> usize {
19012    crate::crypto_cipher::default_aead_tag_len()
19013}
19014
19015fn javascript_crypto_openssl_error(error: openssl::error::ErrorStack) -> SidecarError {
19016    SidecarError::Execution(format!("crypto operation failed: {error}"))
19017}
19018
19019fn service_javascript_kernel_stdin_sync_rpc(
19020    kernel: &mut SidecarKernel,
19021    process: &mut ActiveProcess,
19022    request: &JavascriptSyncRpcRequest,
19023) -> Result<Value, SidecarError> {
19024    let (max_bytes, timeout_ms) = parse_kernel_stdin_read_args(request)?;
19025    kernel_stdin_read_response(
19026        kernel,
19027        process.kernel_pid,
19028        max_bytes,
19029        Duration::from_millis(timeout_ms),
19030    )
19031}
19032
19033/// Parse `__kernel_stdin_read` args: (max bytes, requested timeout ms).
19034pub(crate) fn parse_kernel_stdin_read_args(
19035    request: &JavascriptSyncRpcRequest,
19036) -> Result<(usize, u64), SidecarError> {
19037    let max_bytes =
19038        javascript_sync_rpc_arg_u64_optional(&request.args, 0, "__kernel_stdin_read max bytes")?
19039            .map(|value| value.clamp(1, DEFAULT_KERNEL_STDIN_READ_MAX_BYTES as u64) as usize)
19040            .unwrap_or(DEFAULT_KERNEL_STDIN_READ_MAX_BYTES);
19041    let timeout_ms =
19042        javascript_sync_rpc_arg_u64_optional(&request.args, 1, "__kernel_stdin_read timeout ms")?
19043            .unwrap_or(DEFAULT_KERNEL_STDIN_READ_TIMEOUT_MS);
19044    Ok((max_bytes, timeout_ms))
19045}
19046
19047/// One bounded stdin read against the kernel. `Duration::ZERO` = non-blocking
19048/// probe (deferred servicing re-checks readiness with this before replying).
19049pub(crate) fn kernel_stdin_read_response(
19050    kernel: &mut SidecarKernel,
19051    kernel_pid: u32,
19052    max_bytes: usize,
19053    timeout: Duration,
19054) -> Result<Value, SidecarError> {
19055    match kernel
19056        .fd_read_with_timeout_result(
19057            EXECUTION_DRIVER_NAME,
19058            kernel_pid,
19059            0,
19060            max_bytes,
19061            Some(timeout),
19062        )
19063        .map_err(kernel_error)
19064    {
19065        Ok(Some(chunk)) if !chunk.is_empty() => Ok(json!({
19066            "dataBase64": base64::engine::general_purpose::STANDARD.encode(chunk),
19067        })),
19068        Ok(Some(_)) => Ok(Value::Null),
19069        Ok(None) => Ok(json!({
19070            "done": true,
19071        })),
19072        Err(SidecarError::Kernel(error)) if error.starts_with("EAGAIN:") => Ok(Value::Null),
19073        Err(error) => Err(error),
19074    }
19075}
19076
19077fn service_javascript_pty_set_raw_mode_sync_rpc(
19078    kernel: &mut SidecarKernel,
19079    process: &mut ActiveProcess,
19080    request: &JavascriptSyncRpcRequest,
19081) -> Result<Value, SidecarError> {
19082    let enabled = javascript_sync_rpc_arg_bool(&request.args, 0, "__pty_set_raw_mode enabled")?;
19083    kernel
19084        .pty_set_discipline(
19085            EXECUTION_DRIVER_NAME,
19086            process.kernel_pid,
19087            0,
19088            // Match cfmakeraw: raw mode also disables output post-processing
19089            // (OPOST/ONLCR) so a full-screen app (vim) that drives its own
19090            // cursor/CRLF is not mangled by the line discipline; cooked mode
19091            // re-enables it so the line shell gets LF->CRLF.
19092            LineDisciplineConfig {
19093                canonical: Some(!enabled),
19094                echo: Some(!enabled),
19095                isig: Some(!enabled),
19096                opost: Some(!enabled),
19097                onlcr: Some(!enabled),
19098            },
19099        )
19100        .map_err(kernel_error)?;
19101    Ok(Value::Null)
19102}
19103
19104fn service_javascript_kernel_isatty_sync_rpc(
19105    kernel: &mut SidecarKernel,
19106    process: &ActiveProcess,
19107    request: &JavascriptSyncRpcRequest,
19108) -> Result<Value, SidecarError> {
19109    let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_isatty fd")?;
19110    let is_tty = kernel
19111        .isatty(EXECUTION_DRIVER_NAME, process.kernel_pid, fd)
19112        .map_err(kernel_error)?;
19113    Ok(json!(is_tty))
19114}
19115
19116fn service_javascript_kernel_tty_size_sync_rpc(
19117    kernel: &mut SidecarKernel,
19118    process: &ActiveProcess,
19119    request: &JavascriptSyncRpcRequest,
19120) -> Result<Value, SidecarError> {
19121    let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_tty_size fd")?;
19122    let size = kernel
19123        .pty_window_size(EXECUTION_DRIVER_NAME, process.kernel_pid, fd)
19124        .map_err(kernel_error)?;
19125    Ok(json!({
19126        "cols": size.cols,
19127        "rows": size.rows,
19128    }))
19129}
19130
19131// TODO(double-print): The recovered pre-convergence work (94e935d0) added a
19132// guard here because in THAT model the sidecar owned a per-process
19133// `tty_master_fd`, drained the PTY master into `Stdout`/`ProcessOutput` events,
19134// and therefore had to suppress a TTY child's own `process_output` event to
19135// avoid surfacing the same bytes twice (once via the child's Stdout event, once
19136// via the drained master).
19137//
19138// The current "real terminal client" model on main is different and does NOT
19139// exhibit that bug as far as I can determine:
19140//   * `tty_master_fd` here is only a LOCAL used to derive
19141//     `kernel_stdin_writer_fd`; there is no `ActiveProcess::tty_master_fd`
19142//     field and no `drain_tty_master_output` — the sidecar never surfaces the
19143//     PTY master as host `ProcessOutput` at all. The in-guest terminal client
19144//     reads the master itself.
19145//   * A wasm/python CHILD of a cooked-TTY JS shell is spawned WITHOUT the
19146//     parent's PTY slave dup2'd onto its fds (the child spawn path does no
19147//     `open_pty`/slave dup2), so the child's stdout does not flow through the
19148//     parent's master. It is surfaced exactly once, via the `Stdout` event
19149//     queued below.
19150// So there is no second surfacing path to suppress, and porting 94e935d0's
19151// master-drain guard would guard against a drain that no longer exists.
19152//
19153// UNRESOLVED: I could not exercise the live JS-shell-spawns-wasm-child scenario
19154// end-to-end to positively confirm the host sees exactly one copy; the analysis
19155// above is from the code paths only. If a real double-print is later observed
19156// for a TTY child, the fix belongs here (or at the `ProcessOutput` emission in
19157// `event_frame_for_execution_event`): suppress this child `Stdout` event when
19158// the same bytes already reach the host through a PTY master owned by an
19159// ancestor. Deliberately NOT fabricating that guard now, per instructions.
19160/// A TTY in raw mode (no echo, no canonical) — like cfmakeraw. Full-screen apps
19161/// (vim) run raw and drive their own cursor/CRLF, so their output must be passed
19162/// through untouched, NOT round-tripped through the slave->process_output->master
19163/// path (which buffers/reorders escape sequences and corrupts the screen).
19164fn tty_is_raw_mode(kernel: &SidecarKernel, process: &ActiveProcess) -> bool {
19165    let Some(master_fd) = process.tty_master_fd else {
19166        return false;
19167    };
19168    match kernel.tcgetattr(EXECUTION_DRIVER_NAME, process.kernel_pid, master_fd) {
19169        Ok(termios) => !termios.echo && !termios.icanon,
19170        Err(_) => false,
19171    }
19172}
19173
19174/// Non-blocking drain of the PTY master output buffer for a TTY process.
19175///
19176/// For a TTY (PTY-backed) process the master output buffer is the single
19177/// ordered output stream: it carries cooked-mode echo plus ONLCR-processed
19178/// guest output, already merged FIFO. A zero-timeout master read returns the
19179/// whole current buffer (so echo and guest output stay grouped) or EAGAIN when
19180/// empty, which is mapped to `Ok(None)`. Returns `Ok(None)` for non-TTY
19181/// processes (no master fd).
19182pub(crate) fn drain_tty_master_output(
19183    kernel: &mut SidecarKernel,
19184    process: &mut ActiveProcess,
19185) -> Result<Option<Vec<u8>>, SidecarError> {
19186    let Some(master_fd) = process.tty_master_fd else {
19187        return Ok(None);
19188    };
19189    match kernel.fd_read_with_timeout_result(
19190        EXECUTION_DRIVER_NAME,
19191        process.kernel_pid,
19192        master_fd,
19193        MAX_PTY_BUFFER_BYTES,
19194        Some(Duration::ZERO),
19195    ) {
19196        Ok(Some(bytes)) if !bytes.is_empty() => Ok(Some(bytes)),
19197        Ok(_) => Ok(None),
19198        Err(error) if error.code() == "EAGAIN" => Ok(None),
19199        Err(error) => Err(kernel_error(error)),
19200    }
19201}
19202
19203fn service_javascript_kernel_stdio_write_sync_rpc(
19204    kernel: &mut SidecarKernel,
19205    process: &mut ActiveProcess,
19206    request: &JavascriptSyncRpcRequest,
19207) -> Result<Value, SidecarError> {
19208    let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_stdio_write fd")?;
19209    let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "__kernel_stdio_write chunk")?;
19210    if fd != 1 && fd != 2 {
19211        return Err(SidecarError::InvalidState(format!(
19212            "__kernel_stdio_write only supports fd 1/2, got {fd}"
19213        )));
19214    }
19215
19216    // COOKED TTY (line shell): route the write through the PTY slave so it flows
19217    // through process_output (ONLCR) into the master output buffer interleaved
19218    // with cooked-mode echo, then surface that single ordered master stream so
19219    // ONLCR + echo reach the host. stderr shares the master, merging onto Stdout.
19220    let raw_mode = tty_is_raw_mode(kernel, process);
19221    if process.tty_master_fd.is_some() && !raw_mode {
19222        let written = if fd == 1 {
19223            kernel
19224                .write_process_stdout(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
19225                .map_err(kernel_error)?
19226        } else {
19227            kernel
19228                .write_process_stderr(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
19229                .map_err(kernel_error)?
19230        };
19231        if let Some(master_bytes) = drain_tty_master_output(kernel, process)? {
19232            process.queue_pending_execution_event(ActiveExecutionEvent::Stdout(master_bytes))?;
19233        }
19234        return Ok(json!(written));
19235    }
19236
19237    // RAW TTY (full-screen app) or non-TTY: emit the guest's bytes unmodified.
19238    // For a raw TTY we must NOT write through the slave (that would fill the
19239    // never-drained master and corrupt rendering); for non-TTY we write to the
19240    // underlying fd so pipes/files actually receive it.
19241    let written = if process.tty_master_fd.is_some() {
19242        chunk.len()
19243    } else if fd == 1 {
19244        kernel
19245            .write_process_stdout(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
19246            .map_err(kernel_error)?
19247    } else {
19248        kernel
19249            .write_process_stderr(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
19250            .map_err(kernel_error)?
19251    };
19252
19253    let event = if fd == 1 {
19254        ActiveExecutionEvent::Stdout(chunk)
19255    } else {
19256        ActiveExecutionEvent::Stderr(chunk)
19257    };
19258    process.queue_pending_execution_event(event)?;
19259
19260    Ok(json!(written))
19261}
19262
19263fn service_javascript_kernel_poll_sync_rpc(
19264    kernel: &mut SidecarKernel,
19265    process: &ActiveProcess,
19266    request: &JavascriptSyncRpcRequest,
19267) -> Result<Value, SidecarError> {
19268    let (fd_requests, timeout_ms) = parse_kernel_poll_args(request)?;
19269    kernel_poll_response(kernel, process.kernel_pid, &fd_requests, timeout_ms)
19270}
19271
19272/// Parse `__kernel_poll` args: (fd list, requested timeout ms).
19273pub(crate) fn parse_kernel_poll_args(
19274    request: &JavascriptSyncRpcRequest,
19275) -> Result<(Vec<KernelPollFdRequest>, i32), SidecarError> {
19276    let fd_requests: Vec<KernelPollFdRequest> = serde_json::from_value(
19277        request
19278            .args
19279            .first()
19280            .cloned()
19281            .unwrap_or_else(|| Value::Array(Vec::new())),
19282    )
19283    .map_err(|error| {
19284        SidecarError::InvalidState(format!(
19285            "__kernel_poll fd list must be a JSON array of {{ fd, events }} objects: {error}"
19286        ))
19287    })?;
19288    let timeout_ms =
19289        javascript_sync_rpc_arg_u64_optional(&request.args, 1, "__kernel_poll timeout ms")?
19290            .unwrap_or_default();
19291    let timeout_ms = i32::try_from(timeout_ms).map_err(|_| {
19292        SidecarError::InvalidState(String::from("__kernel_poll timeout ms must fit within i32"))
19293    })?;
19294    Ok((fd_requests, timeout_ms))
19295}
19296
19297/// One bounded kernel poll. Timeout `0` = non-blocking probe (deferred
19298/// servicing re-checks readiness with this before replying).
19299pub(crate) fn kernel_poll_response(
19300    kernel: &SidecarKernel,
19301    kernel_pid: u32,
19302    fd_requests: &[KernelPollFdRequest],
19303    timeout_ms: i32,
19304) -> Result<Value, SidecarError> {
19305    let poll_fds = fd_requests
19306        .iter()
19307        .map(|entry| PollFd {
19308            fd: entry.fd,
19309            events: PollEvents::from_bits(entry.events),
19310            revents: PollEvents::empty(),
19311        })
19312        .collect::<Vec<_>>();
19313    let result = kernel
19314        .poll_fds(EXECUTION_DRIVER_NAME, kernel_pid, poll_fds, timeout_ms)
19315        .map_err(kernel_error)?;
19316    Ok(json!({
19317        "readyCount": result.ready_count,
19318        "fds": result
19319            .fds
19320            .into_iter()
19321            .map(|entry| KernelPollFdResponse {
19322                fd: entry.fd,
19323                events: entry.events.bits(),
19324                revents: entry.revents.bits(),
19325            })
19326            .collect::<Vec<_>>(),
19327    }))
19328}
19329
19330fn install_kernel_stdin_pipe(kernel: &mut SidecarKernel, pid: u32) -> Result<u32, SidecarError> {
19331    let (read_fd, write_fd) = kernel
19332        .open_pipe(EXECUTION_DRIVER_NAME, pid)
19333        .map_err(kernel_error)?;
19334    kernel
19335        .fd_dup2(EXECUTION_DRIVER_NAME, pid, read_fd, 0)
19336        .map_err(kernel_error)?;
19337    kernel
19338        .fd_close(EXECUTION_DRIVER_NAME, pid, read_fd)
19339        .map_err(kernel_error)?;
19340    Ok(write_fd)
19341}
19342
19343fn requested_pty_window_size(env: &BTreeMap<String, String>) -> Option<(u16, u16)> {
19344    let cols = env
19345        .get("COLUMNS")
19346        .and_then(|value| value.parse::<u16>().ok())
19347        .filter(|value| *value > 0)?;
19348    let rows = env
19349        .get("LINES")
19350        .and_then(|value| value.parse::<u16>().ok())
19351        .filter(|value| *value > 0)?;
19352    Some((cols, rows))
19353}
19354
19355fn javascript_child_process_stdin_mode(request: &JavascriptChildProcessSpawnRequest) -> &str {
19356    request
19357        .options
19358        .stdio
19359        .first()
19360        .map(String::as_str)
19361        .unwrap_or("pipe")
19362}
19363
19364pub(crate) fn write_kernel_process_stdin(
19365    kernel: &mut SidecarKernel,
19366    process: &mut ActiveProcess,
19367    chunk: &[u8],
19368) -> Result<(), SidecarError> {
19369    // Non-TTY JavaScript uses the in-process local stdin bridge, not a kernel
19370    // fd; a TTY JavaScript process (tty_master_fd set) DOES route through the
19371    // kernel PTY master, exactly like wasm/python, so line discipline + echo
19372    // apply.
19373    if process.runtime == GuestRuntimeKind::JavaScript && process.tty_master_fd.is_none() {
19374        return Ok(());
19375    }
19376    let Some(writer_fd) = process.kernel_stdin_writer_fd else {
19377        return Ok(());
19378    };
19379    kernel
19380        .fd_write(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd, chunk)
19381        .map_err(kernel_error)?;
19382    // For a TTY process the master write above drives line-discipline echo into
19383    // the master output buffer. Drain it now and surface it as the single
19384    // ordered Stdout stream so typed-character echo reaches the host even while
19385    // the guest is blocked in read() and not producing output of its own.
19386    if let Some(echo) = drain_tty_master_output(kernel, process)? {
19387        process.queue_pending_execution_event(ActiveExecutionEvent::Stdout(echo))?;
19388    }
19389    forward_tty_slave_input_to_javascript(kernel, process)?;
19390    Ok(())
19391}
19392
19393/// For a TTY JavaScript guest, cooked input becomes readable on the PTY slave
19394/// only after line discipline runs (on newline/VEOF in canonical mode; every
19395/// byte in raw mode). The V8 isolate has no kernel-fd read loop of its own —
19396/// its stdin is the stream-stdin dispatch fed by `execution.write_stdin` — so
19397/// right after each master write, drain whatever the discipline released on
19398/// the slave (fd 0) and forward it to the isolate. A `None` read is the
19399/// discipline's VEOF: propagate it as end-of-stdin so `process.stdin` emits
19400/// `end`. Wasm/python guests read the slave themselves and are skipped.
19401fn forward_tty_slave_input_to_javascript(
19402    kernel: &mut SidecarKernel,
19403    process: &mut ActiveProcess,
19404) -> Result<(), SidecarError> {
19405    if process.tty_master_fd.is_none()
19406        || !matches!(process.execution, ActiveExecution::Javascript(_))
19407    {
19408        return Ok(());
19409    }
19410    loop {
19411        match kernel.fd_read_with_timeout_result(
19412            EXECUTION_DRIVER_NAME,
19413            process.kernel_pid,
19414            0,
19415            MAX_PTY_BUFFER_BYTES,
19416            Some(Duration::ZERO),
19417        ) {
19418            Ok(Some(bytes)) if !bytes.is_empty() => {
19419                process.execution.write_stdin(&bytes)?;
19420            }
19421            Ok(Some(_)) => return Ok(()),
19422            Ok(None) => {
19423                process.execution.close_stdin()?;
19424                return Ok(());
19425            }
19426            Err(error) if error.code() == "EAGAIN" => return Ok(()),
19427            Err(error) => return Err(kernel_error(error)),
19428        }
19429    }
19430}
19431
19432pub(crate) fn close_kernel_process_stdin(
19433    kernel: &mut SidecarKernel,
19434    process: &mut ActiveProcess,
19435) -> Result<(), SidecarError> {
19436    let Some(writer_fd) = process.kernel_stdin_writer_fd.take() else {
19437        return Ok(());
19438    };
19439    kernel
19440        .fd_close(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd)
19441        .map_err(kernel_error)
19442}
19443
19444fn parse_http_header_collection(
19445    headers: &BTreeMap<String, Value>,
19446    label: &str,
19447) -> Result<HttpHeaderCollection, SidecarError> {
19448    let mut normalized = BTreeMap::<String, Vec<String>>::new();
19449    let mut raw_pairs = Vec::new();
19450
19451    for (raw_name, value) in headers {
19452        let normalized_name = raw_name.to_ascii_lowercase();
19453        let values = match value {
19454            Value::String(text) => vec![text.clone()],
19455            Value::Array(values) => values
19456                .iter()
19457                .map(|entry| {
19458                    entry.as_str().map(str::to_owned).ok_or_else(|| {
19459                        SidecarError::InvalidState(format!(
19460                            "{label} header {raw_name} must contain only strings"
19461                        ))
19462                    })
19463                })
19464                .collect::<Result<Vec<_>, _>>()?,
19465            other => {
19466                return Err(SidecarError::InvalidState(format!(
19467                    "{label} header {raw_name} must be a string or string array, received {other}"
19468                )));
19469            }
19470        };
19471        raw_pairs.extend(
19472            values
19473                .iter()
19474                .cloned()
19475                .map(|entry| (raw_name.clone(), entry)),
19476        );
19477        normalized
19478            .entry(normalized_name)
19479            .or_default()
19480            .extend(values);
19481    }
19482
19483    Ok(HttpHeaderCollection {
19484        normalized,
19485        raw_pairs,
19486    })
19487}
19488
19489fn http_headers_json(headers: &HttpHeaderCollection) -> Value {
19490    let map = headers
19491        .normalized
19492        .iter()
19493        .map(|(name, values)| {
19494            let value = if values.len() == 1 {
19495                Value::String(values[0].clone())
19496            } else {
19497                Value::Array(values.iter().cloned().map(Value::String).collect())
19498            };
19499            (name.clone(), value)
19500        })
19501        .collect::<Map<String, Value>>();
19502    Value::Object(map)
19503}
19504
19505fn http_raw_headers_json(headers: &HttpHeaderCollection) -> Value {
19506    Value::Array(
19507        headers
19508            .raw_pairs
19509            .iter()
19510            .flat_map(|(name, value)| [Value::String(name.clone()), Value::String(value.clone())])
19511            .collect(),
19512    )
19513}
19514
19515fn is_loopback_request_host(host: &str) -> bool {
19516    let bare = host
19517        .strip_prefix('[')
19518        .and_then(|value| value.strip_suffix(']'))
19519        .unwrap_or(host);
19520    matches!(bare, "localhost" | "127.0.0.1" | "::1")
19521}
19522
19523fn serialize_http_loopback_request(
19524    url: &Url,
19525    options: &JavascriptHttpRequestOptions,
19526    headers: &HttpHeaderCollection,
19527) -> Result<String, SidecarError> {
19528    let body_base64 = options
19529        .body
19530        .as_ref()
19531        .map(|body| base64::engine::general_purpose::STANDARD.encode(body.as_bytes()));
19532    serde_json::to_string(&json!({
19533        "method": options.method.clone().unwrap_or_else(|| String::from("GET")),
19534        "url": http_request_target(url),
19535        "headers": http_headers_json(headers),
19536        "rawHeaders": http_raw_headers_json(headers),
19537        "bodyBase64": body_base64,
19538    }))
19539    .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
19540}
19541
19542fn http_request_target(url: &Url) -> String {
19543    let path = if url.path().is_empty() {
19544        "/"
19545    } else {
19546        url.path()
19547    };
19548    format!(
19549        "{path}{}",
19550        url.query()
19551            .map(|query| format!("?{query}"))
19552            .unwrap_or_default()
19553    )
19554}
19555
19556fn find_kernel_http_listener_process(vm: &VmState, port: u16) -> Option<String> {
19557    vm.active_processes
19558        .iter()
19559        .find_map(|(process_id, process)| {
19560            process.tcp_listeners.values().find_map(|listener| {
19561                let socket_id = listener.kernel_socket_id?;
19562                let record = vm.kernel.socket_get(socket_id)?;
19563                let local_addr = record
19564                    .local_address()
19565                    .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
19566                    .unwrap_or_else(|| listener.guest_local_addr());
19567                if local_addr.port() == port && is_vm_local_http_listener_addr(local_addr.ip()) {
19568                    Some(process_id.to_owned())
19569                } else {
19570                    None
19571                }
19572            })
19573        })
19574}
19575
19576fn is_vm_local_http_listener_addr(ip: IpAddr) -> bool {
19577    ip.is_loopback() || ip.is_unspecified()
19578}
19579
19580fn serialize_kernel_http_fetch_request(
19581    port: u16,
19582    path: &str,
19583    options: &JavascriptHttpRequestOptions,
19584    headers: &HttpHeaderCollection,
19585) -> Vec<u8> {
19586    let method = options.method.as_deref().unwrap_or("GET");
19587    let mut lines = vec![format!("{method} {path} HTTP/1.1")];
19588    let mut has_host = false;
19589    let mut has_connection = false;
19590    let mut has_content_length = false;
19591    for (name, values) in &headers.normalized {
19592        match name.as_str() {
19593            "host" => has_host = true,
19594            "connection" => has_connection = true,
19595            "content-length" => has_content_length = true,
19596            _ => {}
19597        }
19598        lines.push(format!("{name}: {}", values.join(", ")));
19599    }
19600    if !has_host {
19601        lines.push(format!("Host: 127.0.0.1:{port}"));
19602    }
19603    if !has_connection {
19604        lines.push(String::from("Connection: close"));
19605    }
19606    let body = options.body.as_deref().unwrap_or("").as_bytes();
19607    if !has_content_length && !body.is_empty() {
19608        lines.push(format!("Content-Length: {}", body.len()));
19609    }
19610    lines.push(String::new());
19611    lines.push(String::new());
19612
19613    let mut request = lines.join("\r\n").into_bytes();
19614    request.extend_from_slice(body);
19615    request
19616}
19617
19618fn kernel_http_fetch_target_exit_code(error: &SidecarError) -> Option<i32> {
19619    let SidecarError::Execution(message) = error else {
19620        return None;
19621    };
19622    message
19623        .strip_prefix("vm.fetch target exited before responding (exit code ")?
19624        .strip_suffix(')')?
19625        .parse()
19626        .ok()
19627}
19628
19629#[allow(clippy::too_many_arguments)]
19630fn service_host_fetch_target_event<B>(
19631    bridge: &SharedBridge<B>,
19632    vm_id: &str,
19633    dns: &VmDnsConfig,
19634    socket_paths: &JavascriptSocketPathContext,
19635    kernel: &mut SidecarKernel,
19636    kernel_readiness: &KernelSocketReadinessRegistry,
19637    process: &mut ActiveProcess,
19638    resource_limits: &ResourceLimits,
19639    wait: Duration,
19640) -> Result<bool, SidecarError>
19641where
19642    B: NativeSidecarBridge + Send + 'static,
19643    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19644{
19645    let Some(event) = process
19646        .execution
19647        .poll_event_blocking(wait)
19648        .map_err(|error| SidecarError::Execution(error.to_string()))?
19649    else {
19650        return Ok(false);
19651    };
19652
19653    match event {
19654        ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
19655            let network_counts = process.network_resource_counts();
19656            let response = service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
19657                bridge,
19658                vm_id,
19659                dns,
19660                socket_paths,
19661                kernel,
19662                kernel_readiness: Arc::clone(kernel_readiness),
19663                process,
19664                sync_request: &request,
19665                resource_limits,
19666                network_counts,
19667            });
19668            match response {
19669                Ok(result) => process
19670                    .execution
19671                    .respond_javascript_sync_rpc_response(request.id, result)
19672                    .or_else(ignore_stale_javascript_sync_rpc_response)?,
19673                Err(error) => process
19674                    .execution
19675                    .respond_javascript_sync_rpc_error(
19676                        request.id,
19677                        javascript_sync_rpc_error_code(&error),
19678                        error.to_string(),
19679                    )
19680                    .or_else(ignore_stale_javascript_sync_rpc_response)?,
19681            }
19682        }
19683        ActiveExecutionEvent::Exited(code) => {
19684            return Err(SidecarError::Execution(format!(
19685                "vm.fetch target exited before responding (exit code {code})"
19686            )));
19687        }
19688        other => {
19689            process.queue_pending_execution_event(other)?;
19690        }
19691    }
19692    Ok(true)
19693}
19694
19695fn drain_host_fetch_target_events<B>(
19696    bridge: &SharedBridge<B>,
19697    vm_id: &str,
19698    vm: &mut VmState,
19699    target_process_id: &str,
19700    socket_paths: &JavascriptSocketPathContext,
19701    resource_limits: &ResourceLimits,
19702) -> Result<(), SidecarError>
19703where
19704    B: NativeSidecarBridge + Send + 'static,
19705    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19706{
19707    for _ in 0..32 {
19708        let dns = vm.dns.clone();
19709        let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness);
19710        let Some(process) = vm.active_processes.get_mut(target_process_id) else {
19711            break;
19712        };
19713        let serviced = service_host_fetch_target_event(
19714            bridge,
19715            vm_id,
19716            &dns,
19717            socket_paths,
19718            &mut vm.kernel,
19719            &kernel_readiness,
19720            process,
19721            resource_limits,
19722            Duration::from_millis(1),
19723        )?;
19724        if !serviced {
19725            break;
19726        }
19727    }
19728    Ok(())
19729}
19730
19731#[allow(clippy::too_many_arguments)]
19732fn dispatch_kernel_http_fetch<B>(
19733    bridge: &SharedBridge<B>,
19734    vm_id: &str,
19735    vm: &mut VmState,
19736    target_process_id: &str,
19737    port: u16,
19738    path: &str,
19739    options: &JavascriptHttpRequestOptions,
19740    headers: &HttpHeaderCollection,
19741    max_fetch_response_bytes: usize,
19742) -> Result<String, SidecarError>
19743where
19744    B: NativeSidecarBridge + Send + 'static,
19745    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19746{
19747    let socket_paths = build_javascript_socket_path_context(vm)?;
19748    let family = JavascriptSocketFamily::Ipv4;
19749    let local_port = allocate_guest_listen_port(
19750        0,
19751        family,
19752        &socket_paths.used_tcp_guest_ports,
19753        socket_paths.listen_policy,
19754    )?;
19755    let resource_limits = vm.kernel.resource_limits().clone();
19756    let network_counts = vm_network_resource_counts(vm);
19757    check_network_resource_limit(
19758        resource_limits.max_sockets,
19759        network_counts.sockets,
19760        2,
19761        "socket",
19762    )?;
19763    check_network_resource_limit(
19764        resource_limits.max_connections,
19765        network_counts.connections,
19766        2,
19767        "connection",
19768    )?;
19769
19770    let kernel_pid = vm
19771        .active_processes
19772        .get(target_process_id)
19773        .ok_or_else(|| {
19774            SidecarError::InvalidState(format!(
19775                "vm.fetch target process disappeared: {target_process_id}"
19776            ))
19777        })?
19778        .kernel_pid;
19779    let socket_id = vm
19780        .kernel
19781        .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, SocketSpec::tcp())
19782        .map_err(kernel_error)?;
19783
19784    let result = dispatch_kernel_http_fetch_with_socket(
19785        bridge,
19786        vm_id,
19787        vm,
19788        target_process_id,
19789        kernel_pid,
19790        socket_id,
19791        local_port,
19792        port,
19793        path,
19794        options,
19795        headers,
19796        &socket_paths,
19797        &resource_limits,
19798        max_fetch_response_bytes,
19799    );
19800    let close_result = vm
19801        .kernel
19802        .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id)
19803        .map_err(kernel_error);
19804    let cleanup_result = if result.is_err() {
19805        drain_host_fetch_target_events(
19806            bridge,
19807            vm_id,
19808            vm,
19809            target_process_id,
19810            &socket_paths,
19811            &resource_limits,
19812        )
19813    } else {
19814        Ok(())
19815    };
19816    match (result, close_result) {
19817        (Ok(response), Ok(())) => cleanup_result.map(|()| response),
19818        (Err(error), _) => Err(error),
19819        (Ok(_), Err(error)) => Err(error),
19820    }
19821}
19822
19823#[allow(clippy::too_many_arguments)]
19824fn dispatch_kernel_http_fetch_with_socket<B>(
19825    bridge: &SharedBridge<B>,
19826    vm_id: &str,
19827    vm: &mut VmState,
19828    target_process_id: &str,
19829    kernel_pid: u32,
19830    socket_id: SocketId,
19831    local_port: u16,
19832    port: u16,
19833    path: &str,
19834    options: &JavascriptHttpRequestOptions,
19835    headers: &HttpHeaderCollection,
19836    socket_paths: &JavascriptSocketPathContext,
19837    resource_limits: &ResourceLimits,
19838    max_fetch_response_bytes: usize,
19839) -> Result<String, SidecarError>
19840where
19841    B: NativeSidecarBridge + Send + 'static,
19842    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19843{
19844    vm.kernel
19845        .socket_bind_inet(
19846            EXECUTION_DRIVER_NAME,
19847            kernel_pid,
19848            socket_id,
19849            InetSocketAddress::new("127.0.0.1", local_port),
19850        )
19851        .map_err(kernel_error)?;
19852    vm.kernel
19853        .socket_connect_inet_loopback(
19854            EXECUTION_DRIVER_NAME,
19855            kernel_pid,
19856            socket_id,
19857            InetSocketAddress::new("127.0.0.1", port),
19858        )
19859        .map_err(kernel_error)?;
19860
19861    let request_bytes = serialize_kernel_http_fetch_request(port, path, options, headers);
19862    vm.kernel
19863        .socket_write(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, &request_bytes)
19864        .map_err(kernel_error)?;
19865
19866    let mut response_buffer = Vec::new();
19867    let mut peer_closed = false;
19868    let url = format!("http://127.0.0.1:{port}{path}");
19869    let deadline = Instant::now() + http_loopback_request_timeout();
19870    loop {
19871        if let Some(response) =
19872            parse_kernel_http_fetch_response(&response_buffer, peer_closed, &url)
19873                .map_err(sidecar_core_execution_error)?
19874        {
19875            ensure_vm_fetch_response_within_limit(&response, "vm.fetch", max_fetch_response_bytes)
19876                .map_err(sidecar_core_execution_error)?;
19877            return Ok(response);
19878        }
19879        if Instant::now() >= deadline {
19880            let preview = String::from_utf8_lossy(&response_buffer);
19881            return Err(SidecarError::Execution(format!(
19882                "vm.fetch timed out waiting for kernel TCP HTTP response ({} buffered bytes: {:?})",
19883                response_buffer.len(),
19884                preview.chars().take(200).collect::<String>()
19885            )));
19886        }
19887
19888        {
19889            let dns = vm.dns.clone();
19890            let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness);
19891            let process = vm
19892                .active_processes
19893                .get_mut(target_process_id)
19894                .ok_or_else(|| {
19895                    SidecarError::InvalidState(format!(
19896                        "vm.fetch target process disappeared: {target_process_id}"
19897                    ))
19898                })?;
19899            service_host_fetch_target_event(
19900                bridge,
19901                vm_id,
19902                &dns,
19903                socket_paths,
19904                &mut vm.kernel,
19905                &kernel_readiness,
19906                process,
19907                resource_limits,
19908                Duration::from_millis(5),
19909            )?;
19910        }
19911
19912        let poll = vm
19913            .kernel
19914            .poll_targets(
19915                EXECUTION_DRIVER_NAME,
19916                kernel_pid,
19917                vec![PollTargetEntry::socket(
19918                    socket_id,
19919                    POLLIN | POLLHUP | POLLERR,
19920                )],
19921                5,
19922            )
19923            .map_err(kernel_error)?;
19924        let revents = poll
19925            .targets
19926            .first()
19927            .map(|entry| entry.revents)
19928            .unwrap_or_else(PollEvents::empty);
19929        if revents.intersects(POLLERR) {
19930            return Err(SidecarError::Execution(String::from(
19931                "vm.fetch kernel TCP socket reported POLLERR",
19932            )));
19933        }
19934        if revents.intersects(POLLIN) {
19935            loop {
19936                match vm
19937                    .kernel
19938                    .socket_read(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, 64 * 1024)
19939                {
19940                    Ok(Some(bytes)) if !bytes.is_empty() => {
19941                        response_buffer.extend(bytes);
19942                        ensure_vm_fetch_raw_response_buffer_within_limit(
19943                            response_buffer.len(),
19944                            "vm.fetch",
19945                        )
19946                        .map_err(sidecar_core_execution_error)?;
19947                    }
19948                    Ok(Some(_)) => break,
19949                    Ok(None) => {
19950                        peer_closed = true;
19951                        break;
19952                    }
19953                    Err(error) if error.code() == "EAGAIN" => break,
19954                    Err(error) => return Err(kernel_error(error)),
19955                }
19956            }
19957        }
19958        if revents.intersects(POLLHUP) {
19959            peer_closed = true;
19960        }
19961    }
19962}
19963
19964fn outbound_http_response_json(url: &Url, response: ureq::Response) -> Result<Value, SidecarError> {
19965    let status = response.status();
19966    let status_text = response.status_text().to_owned();
19967    let mut header_pairs = Vec::new();
19968    let mut raw_headers = Vec::new();
19969    for raw_name in response.headers_names() {
19970        for value in response.all(&raw_name) {
19971            header_pairs.push(json!([raw_name.to_ascii_lowercase(), value]));
19972            raw_headers.push(Value::String(raw_name.clone()));
19973            raw_headers.push(Value::String(value.to_owned()));
19974        }
19975    }
19976    let mut reader = response.into_reader();
19977    let mut body = Vec::new();
19978    reader.read_to_end(&mut body).map_err(|error| {
19979        SidecarError::Execution(format!("failed to read HTTP response: {error}"))
19980    })?;
19981    serde_json::to_string(&json!({
19982        "status": status,
19983        "statusText": status_text,
19984        "headers": header_pairs,
19985        "rawHeaders": raw_headers,
19986        "body": base64::engine::general_purpose::STANDARD.encode(body),
19987        "bodyEncoding": "base64",
19988        "url": url.as_str(),
19989    }))
19990    .map(Value::String)
19991    .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
19992}
19993
19994/// Split a ureq resolver `netloc` (`host:port`, with optional `[..]` IPv6
19995/// brackets) into its host and port components. Returns `None` if the port is
19996/// missing or unparseable.
19997fn split_netloc(netloc: &str) -> Option<(&str, u16)> {
19998    let (host, port) = netloc.rsplit_once(':')?;
19999    let port: u16 = port.parse().ok()?;
20000    let host = host
20001        .strip_prefix('[')
20002        .and_then(|rest| rest.strip_suffix(']'))
20003        .unwrap_or(host);
20004    Some((host, port))
20005}
20006
20007fn issue_outbound_http_request(
20008    url: &Url,
20009    options: &JavascriptHttpRequestOptions,
20010    headers: &HttpHeaderCollection,
20011    pinned_addresses: &[IpAddr],
20012) -> Result<Value, SidecarError> {
20013    let method = options.method.as_deref().unwrap_or("GET");
20014    // Pin the underlying resolver to the egress-vetted addresses. ureq performs
20015    // its own DNS resolution for the TCP/TLS connect; without this override an
20016    // https:// request would re-resolve the hostname through the host resolver
20017    // (a rebinding DNS server could then return a private/metadata IP that the
20018    // earlier range check would have rejected). The pinned resolver returns only
20019    // the vetted addresses and refuses any host it was not vetted for, while the
20020    // request URL keeps the original hostname so TLS SNI and the Host header stay
20021    // correct.
20022    let pinned_host = url.host_str().map(str::to_owned);
20023    let pinned: Vec<IpAddr> = pinned_addresses.to_vec();
20024    let resolver = move |netloc: &str| -> std::io::Result<Vec<SocketAddr>> {
20025        let (host, port) = split_netloc(netloc).ok_or_else(|| {
20026            std::io::Error::new(
20027                std::io::ErrorKind::InvalidInput,
20028                format!("invalid network location: {netloc}"),
20029            )
20030        })?;
20031        let expected_host = pinned_host.as_deref();
20032        if expected_host != Some(host) {
20033            return Err(std::io::Error::new(
20034                std::io::ErrorKind::PermissionDenied,
20035                format!(
20036                    "EACCES: outbound HTTP resolver pinned to {expected_host:?}, refusing {host}"
20037                ),
20038            ));
20039        }
20040        if pinned.is_empty() {
20041            return Err(std::io::Error::new(
20042                std::io::ErrorKind::PermissionDenied,
20043                "EACCES: no egress-vetted address available for outbound HTTP request",
20044            ));
20045        }
20046        Ok(pinned.iter().map(|ip| SocketAddr::new(*ip, port)).collect())
20047    };
20048    let mut agent_builder = ureq::AgentBuilder::new()
20049        .resolver(resolver)
20050        .timeout_connect(Duration::from_secs(5))
20051        .timeout_read(Duration::from_secs(15))
20052        .timeout_write(Duration::from_secs(15));
20053    if url.scheme() == "https" {
20054        let tls_options = JavascriptTlsBridgeOptions {
20055            is_server: false,
20056            servername: url.host_str().map(str::to_owned),
20057            alpn_protocols: Some(vec![String::from("http/1.1")]),
20058            reject_unauthorized: options.reject_unauthorized,
20059            ..JavascriptTlsBridgeOptions::default()
20060        };
20061        agent_builder = agent_builder.tls_config(Arc::new(build_client_tls_config(&tls_options)?));
20062    }
20063    let agent = agent_builder.build();
20064    let mut request = agent.request_url(method, url);
20065    for (name, values) in &headers.normalized {
20066        if name == "host" {
20067            continue;
20068        }
20069        let header_value = values.join(", ");
20070        request = request.set(name, &header_value);
20071    }
20072    let response = match options.body.as_deref() {
20073        Some(body) => request.send_string(body),
20074        None => request.call(),
20075    };
20076
20077    match response {
20078        Ok(response) => outbound_http_response_json(url, response),
20079        Err(ureq::Error::Status(_, response)) => outbound_http_response_json(url, response),
20080        Err(ureq::Error::Transport(error)) => Err(SidecarError::Execution(format!(
20081            "ERR_HTTP_REQUEST_FAILED: {error}"
20082        ))),
20083    }
20084}
20085
20086fn wait_for_loopback_http_response<B>(
20087    request: LoopbackHttpResponseWaitRequest<'_, B>,
20088) -> Result<String, SidecarError>
20089where
20090    B: NativeSidecarBridge + Send + 'static,
20091    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
20092{
20093    let LoopbackHttpResponseWaitRequest {
20094        bridge,
20095        vm_id,
20096        dns,
20097        socket_paths,
20098        kernel,
20099        kernel_readiness,
20100        process,
20101        resource_limits,
20102        request_key,
20103    } = request;
20104    let deadline = Instant::now() + http_loopback_request_timeout();
20105    loop {
20106        if let Some(response) = process
20107            .pending_http_requests
20108            .get(&request_key)
20109            .and_then(|response| response.clone())
20110        {
20111            process.pending_http_requests.remove(&request_key);
20112            return Ok(response);
20113        }
20114
20115        if Instant::now() >= deadline {
20116            process.pending_http_requests.remove(&request_key);
20117            return Err(SidecarError::Execution(String::from(
20118                "HTTP loopback request timed out waiting for net.http_respond",
20119            )));
20120        }
20121
20122        let Some(event) = process
20123            .execution
20124            .poll_event_blocking(Duration::from_millis(10))
20125            .map_err(|error| SidecarError::Execution(error.to_string()))?
20126        else {
20127            continue;
20128        };
20129
20130        match event {
20131            ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
20132                let network_counts = process.network_resource_counts();
20133                let response = service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
20134                    bridge,
20135                    vm_id,
20136                    dns,
20137                    socket_paths,
20138                    kernel,
20139                    kernel_readiness: Arc::clone(&kernel_readiness),
20140                    process,
20141                    sync_request: &request,
20142                    resource_limits,
20143                    network_counts,
20144                });
20145                match response {
20146                    Ok(result) => process
20147                        .execution
20148                        .respond_javascript_sync_rpc_response(request.id, result)
20149                        .or_else(ignore_stale_javascript_sync_rpc_response)?,
20150                    Err(error) => process
20151                        .execution
20152                        .respond_javascript_sync_rpc_error(
20153                            request.id,
20154                            javascript_sync_rpc_error_code(&error),
20155                            error.to_string(),
20156                        )
20157                        .or_else(ignore_stale_javascript_sync_rpc_response)?,
20158                }
20159            }
20160            ActiveExecutionEvent::Exited(code) => {
20161                process.pending_http_requests.remove(&request_key);
20162                return Err(SidecarError::Execution(format!(
20163                    "HTTP loopback server exited before responding (exit code {code})"
20164                )));
20165            }
20166            ActiveExecutionEvent::Stdout(_)
20167            | ActiveExecutionEvent::Stderr(_)
20168            | ActiveExecutionEvent::PythonVfsRpcRequest(_)
20169            | ActiveExecutionEvent::SignalState { .. } => {}
20170        }
20171    }
20172}
20173
20174pub(crate) fn dispatch_loopback_http_request<B>(
20175    request: LoopbackHttpDispatchRequest<'_, B>,
20176) -> Result<String, SidecarError>
20177where
20178    B: NativeSidecarBridge + Send + 'static,
20179    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
20180{
20181    let LoopbackHttpDispatchRequest {
20182        bridge,
20183        vm_id,
20184        dns,
20185        socket_paths,
20186        kernel,
20187        kernel_readiness,
20188        process,
20189        resource_limits,
20190        server_id,
20191        request_json,
20192    } = request;
20193    let request_id = {
20194        let server = process.http_servers.get_mut(&server_id).ok_or_else(|| {
20195            SidecarError::InvalidState(format!("HTTP target server disappeared: {server_id}"))
20196        })?;
20197        server.next_request_id += 1;
20198        server.next_request_id
20199    };
20200    process
20201        .pending_http_requests
20202        .insert((server_id, request_id), None);
20203    process.execution.send_javascript_stream_event(
20204        "http_request",
20205        json!({
20206            "serverId": server_id,
20207            "requestId": request_id,
20208            "request": request_json,
20209        }),
20210    )?;
20211    wait_for_loopback_http_response(LoopbackHttpResponseWaitRequest {
20212        bridge,
20213        vm_id,
20214        dns,
20215        socket_paths,
20216        kernel,
20217        kernel_readiness,
20218        process,
20219        resource_limits,
20220        request_key: (server_id, request_id),
20221    })
20222}
20223
20224fn sidecar_core_execution_error(error: SidecarCoreError) -> SidecarError {
20225    SidecarError::Execution(error.to_string())
20226}
20227
20228pub(crate) fn ensure_vm_fetch_response_frame_within_limit(
20229    response: &ResponseFrame,
20230    max_frame_bytes: usize,
20231) -> Result<(), SidecarError> {
20232    let max_frame_bytes = max_frame_bytes.min(VM_FETCH_BUFFER_LIMIT_BYTES);
20233    let frame = crate::protocol::to_generated_protocol_frame(
20234        &crate::protocol::ProtocolFrame::Response(response.clone()),
20235    )
20236    .map_err(|error| SidecarError::FrameTooLarge(error.to_string()))?;
20237    let WireProtocolFrame::ResponseFrame(_) = &frame else {
20238        return Err(SidecarError::FrameTooLarge(String::from(
20239            "vm fetch response converted to non-response wire frame",
20240        )));
20241    };
20242    WireFrameCodec::new(max_frame_bytes)
20243        .encode(&frame)
20244        .map(|_| ())
20245        .map_err(|error| SidecarError::FrameTooLarge(error.to_string()))
20246}
20247
20248fn service_javascript_dns_sync_rpc<B>(
20249    bridge: &SharedBridge<B>,
20250    kernel: &SidecarKernel,
20251    vm_id: &str,
20252    dns: &VmDnsConfig,
20253    request: &JavascriptSyncRpcRequest,
20254) -> Result<Value, SidecarError>
20255where
20256    B: NativeSidecarBridge + Send + 'static,
20257    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
20258{
20259    match request.method.as_str() {
20260        "dns.lookup" => {
20261            let payload = request
20262                .args
20263                .first()
20264                .cloned()
20265                .ok_or_else(|| {
20266                    SidecarError::InvalidState(String::from(
20267                        "dns.lookup requires a request payload",
20268                    ))
20269                })
20270                .and_then(|value| {
20271                    serde_json::from_value::<JavascriptDnsLookupRequest>(value).map_err(|error| {
20272                        SidecarError::InvalidState(format!("invalid dns.lookup payload: {error}"))
20273                    })
20274                })?;
20275            let addresses = filter_dns_ip_addrs(
20276                resolve_dns_ip_addrs(
20277                    bridge,
20278                    kernel,
20279                    vm_id,
20280                    dns,
20281                    &payload.hostname,
20282                    DnsLookupPolicy::CheckPermissions,
20283                )?,
20284                payload.family,
20285            )?;
20286            let addresses = filter_dns_safe_ip_addrs(addresses, &payload.hostname)?;
20287            Ok(Value::Array(
20288                addresses
20289                    .into_iter()
20290                    .map(|ip| {
20291                        json!({
20292                            "address": ip.to_string(),
20293                            "family": if ip.is_ipv6() { 6 } else { 4 },
20294                        })
20295                    })
20296                    .collect(),
20297            ))
20298        }
20299        "dns.resolve" | "dns.resolve4" | "dns.resolve6" => {
20300            let payload = request
20301                .args
20302                .first()
20303                .cloned()
20304                .ok_or_else(|| {
20305                    SidecarError::InvalidState(String::from(
20306                        "dns.resolve requires a request payload",
20307                    ))
20308                })
20309                .and_then(|value| {
20310                    serde_json::from_value::<JavascriptDnsResolveRequest>(value).map_err(|error| {
20311                        SidecarError::InvalidState(format!("invalid dns.resolve payload: {error}"))
20312                    })
20313                })?;
20314            let requested_type = match request.method.as_str() {
20315                "dns.resolve4" => String::from("A"),
20316                "dns.resolve6" => String::from("AAAA"),
20317                _ => payload
20318                    .rrtype
20319                    .as_deref()
20320                    .unwrap_or("A")
20321                    .to_ascii_uppercase(),
20322            };
20323            let record_type = parse_dns_record_type(&requested_type)?;
20324            let resolution = resolve_dns_records(
20325                bridge,
20326                kernel,
20327                vm_id,
20328                dns,
20329                &payload.hostname,
20330                record_type,
20331                DnsLookupPolicy::CheckPermissions,
20332            )?;
20333            dns_resolution_to_node_value(&resolution, &requested_type)
20334        }
20335        other => Err(SidecarError::InvalidState(format!(
20336            "unsupported JavaScript dns sync RPC method {other}"
20337        ))),
20338    }
20339}
20340
20341fn service_javascript_dgram_sync_rpc<B>(
20342    request: JavascriptDgramSyncRpcServiceRequest<'_, B>,
20343) -> Result<Value, SidecarError>
20344where
20345    B: NativeSidecarBridge + Send + 'static,
20346    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
20347{
20348    let JavascriptDgramSyncRpcServiceRequest {
20349        bridge,
20350        kernel,
20351        vm_id,
20352        dns,
20353        socket_paths,
20354        process,
20355        kernel_readiness,
20356        sync_request: request,
20357        resource_limits,
20358        network_counts,
20359    } = request;
20360    match request.method.as_str() {
20361        "dgram.createSocket" => {
20362            check_network_resource_limit(
20363                resource_limits.max_sockets,
20364                network_counts.sockets,
20365                1,
20366                "socket",
20367            )?;
20368            let payload = request
20369                .args
20370                .first()
20371                .cloned()
20372                .ok_or_else(|| {
20373                    SidecarError::InvalidState(String::from(
20374                        "dgram.createSocket requires a request payload",
20375                    ))
20376                })
20377                .and_then(|value| {
20378                    serde_json::from_value::<JavascriptDgramCreateSocketRequest>(value).map_err(
20379                        |error| {
20380                            SidecarError::InvalidState(format!(
20381                                "invalid dgram.createSocket payload: {error}"
20382                            ))
20383                        },
20384                    )
20385                })?;
20386            let family = JavascriptUdpFamily::from_socket_type(&payload.socket_type)?;
20387            let socket_id = process.allocate_udp_socket_id();
20388            let socket = ActiveUdpSocket::new(kernel, process.kernel_pid, family)?;
20389            register_kernel_readiness_target(
20390                &kernel_readiness,
20391                socket.kernel_socket_id,
20392                process.execution.javascript_v8_session_handle(),
20393                socket_id.clone(),
20394                KernelSocketReadinessEvent::Datagram,
20395            );
20396            process.udp_sockets.insert(socket_id.clone(), socket);
20397            Ok(json!({
20398                "socketId": socket_id,
20399                "type": family.socket_type(),
20400            }))
20401        }
20402        "dgram.bind" => {
20403            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.bind socket id")?;
20404            let payload = request
20405                .args
20406                .get(1)
20407                .cloned()
20408                .ok_or_else(|| {
20409                    SidecarError::InvalidState(String::from(
20410                        "dgram.bind requires a request payload",
20411                    ))
20412                })
20413                .and_then(|value| {
20414                    serde_json::from_value::<JavascriptDgramBindRequest>(value).map_err(|error| {
20415                        SidecarError::InvalidState(format!("invalid dgram.bind payload: {error}"))
20416                    })
20417                })?;
20418            let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
20419                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
20420            })?;
20421            let local_addr = socket.bind(
20422                kernel,
20423                process.kernel_pid,
20424                payload.address.as_deref(),
20425                payload.port,
20426                socket_paths,
20427            )?;
20428            Ok(local_endpoint_value(&local_addr))
20429        }
20430        "dgram.send" => {
20431            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.send socket id")?;
20432            let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "dgram.send payload")?;
20433            let payload = request
20434                .args
20435                .get(2)
20436                .cloned()
20437                .ok_or_else(|| {
20438                    SidecarError::InvalidState(String::from(
20439                        "dgram.send requires a request payload",
20440                    ))
20441                })
20442                .and_then(|value| {
20443                    serde_json::from_value::<JavascriptDgramSendRequest>(value).map_err(|error| {
20444                        SidecarError::InvalidState(format!("invalid dgram.send payload: {error}"))
20445                    })
20446                })?;
20447            let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
20448                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
20449            })?;
20450            let (written, local_addr) = socket.send_to(ActiveUdpSendToRequest {
20451                bridge,
20452                kernel,
20453                kernel_pid: process.kernel_pid,
20454                vm_id,
20455                dns,
20456                host: payload.address.as_deref().unwrap_or("localhost"),
20457                port: payload.port,
20458                context: socket_paths,
20459                contents: &chunk,
20460            })?;
20461            Ok(json!({
20462                "bytes": written,
20463                "localAddress": local_addr.ip().to_string(),
20464                "localPort": local_addr.port(),
20465                "family": socket_addr_family(&local_addr),
20466            }))
20467        }
20468        "dgram.poll" => {
20469            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.poll socket id")?;
20470            let wait_ms =
20471                javascript_sync_rpc_arg_u64_optional(&request.args, 1, "dgram.poll wait ms")?
20472                    .unwrap_or_default();
20473            let event = {
20474                let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
20475                    SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
20476                })?;
20477                socket.poll(kernel, process.kernel_pid, Duration::from_millis(wait_ms))?
20478            };
20479
20480            match event {
20481                Some(JavascriptUdpSocketEvent::Message { data, remote_addr }) => {
20482                    let family = JavascriptSocketFamily::from_ip(remote_addr.ip());
20483                    let guest_remote_port = if is_loopback_ip(remote_addr.ip()) {
20484                        socket_paths
20485                            .guest_udp_port_for_host_port(family, remote_addr.port())
20486                            .unwrap_or(remote_addr.port())
20487                    } else {
20488                        remote_addr.port()
20489                    };
20490                    let mut response = remote_endpoint_value(&remote_addr, guest_remote_port);
20491                    if let Value::Object(fields) = &mut response {
20492                        fields.insert(String::from("type"), Value::String(String::from("message")));
20493                        fields.insert(String::from("data"), javascript_sync_rpc_bytes_value(&data));
20494                    }
20495                    Ok(response)
20496                }
20497                Some(JavascriptUdpSocketEvent::Error { code, message }) => Ok(json!({
20498                    "type": "error",
20499                    "code": code,
20500                    "message": message,
20501                })),
20502                None => Ok(Value::Null),
20503            }
20504        }
20505        "dgram.close" => {
20506            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.close socket id")?;
20507            let mut socket = process.udp_sockets.remove(socket_id).ok_or_else(|| {
20508                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
20509            })?;
20510            unregister_kernel_readiness_target(&kernel_readiness, socket.kernel_socket_id);
20511            socket.close(kernel, process.kernel_pid);
20512            Ok(Value::Null)
20513        }
20514        "dgram.address" => {
20515            let socket_id =
20516                javascript_sync_rpc_arg_str(&request.args, 0, "dgram.address socket id")?;
20517            let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
20518                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
20519            })?;
20520            let local_addr = socket.local_addr().ok_or_else(|| {
20521                SidecarError::Execution(String::from("EBADF: bad file descriptor"))
20522            })?;
20523            javascript_net_json_string(
20524                json!({
20525                    "address": local_addr.ip().to_string(),
20526                    "port": local_addr.port(),
20527                    "family": socket_addr_family(&local_addr),
20528                }),
20529                "dgram.address",
20530            )
20531        }
20532        "dgram.setBufferSize" => {
20533            let socket_id =
20534                javascript_sync_rpc_arg_str(&request.args, 0, "dgram.setBufferSize socket id")?;
20535            let which =
20536                javascript_sync_rpc_arg_str(&request.args, 1, "dgram.setBufferSize buffer kind")?;
20537            let size = javascript_sync_rpc_arg_u64(&request.args, 2, "dgram.setBufferSize size")?;
20538            let size = usize::try_from(size).map_err(|_| {
20539                SidecarError::InvalidState(String::from(
20540                    "dgram.setBufferSize size must fit within usize",
20541                ))
20542            })?;
20543            let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
20544                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
20545            })?;
20546            socket.set_buffer_size(which, size)?;
20547            Ok(Value::Null)
20548        }
20549        "dgram.getBufferSize" => {
20550            let socket_id =
20551                javascript_sync_rpc_arg_str(&request.args, 0, "dgram.getBufferSize socket id")?;
20552            let which =
20553                javascript_sync_rpc_arg_str(&request.args, 1, "dgram.getBufferSize buffer kind")?;
20554            let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
20555                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
20556            })?;
20557            let size = socket.get_buffer_size(which)?;
20558            Ok(json!(size))
20559        }
20560        other => Err(SidecarError::InvalidState(format!(
20561            "unsupported JavaScript dgram sync RPC method {other}"
20562        ))),
20563    }
20564}
20565
20566#[derive(Debug)]
20567struct ClientHttp2StreamState {
20568    send_stream: Option<h2::SendStream<Bytes>>,
20569}
20570
20571#[derive(Debug)]
20572struct ServerHttp2StreamState {
20573    send_response: Option<ServerHttp2Responder>,
20574    send_stream: Option<h2::SendStream<Bytes>>,
20575}
20576
20577#[derive(Debug)]
20578enum ServerHttp2Responder {
20579    Regular(server::SendResponse<Bytes>),
20580    Pushed(server::SendPushedResponse<Bytes>),
20581}
20582
20583const HTTP2_DEFAULT_WINDOW_SIZE: u32 = 65_535;
20584const HTTP2_POLL_DELAY: Duration = Duration::from_millis(10);
20585
20586fn http2_runtime_snapshot() -> Http2RuntimeSnapshot {
20587    Http2RuntimeSnapshot {
20588        effective_local_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
20589        local_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
20590        remote_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
20591        next_stream_id: 1,
20592        outbound_queue_size: 1,
20593        deflate_dynamic_table_size: 0,
20594        inflate_dynamic_table_size: 0,
20595    }
20596}
20597
20598fn http2_snapshot_json(snapshot: &Http2SessionSnapshot) -> Result<String, SidecarError> {
20599    serde_json::to_string(snapshot)
20600        .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
20601}
20602
20603fn http2_event_value(event: &Http2BridgeEvent) -> Result<Value, SidecarError> {
20604    serde_json::to_string(event)
20605        .map(Value::String)
20606        .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
20607}
20608
20609fn push_http2_server_event(
20610    shared: &Arc<Mutex<crate::state::Http2SharedState>>,
20611    server_id: u64,
20612    event: Http2BridgeEvent,
20613) {
20614    let (ready, wake_session, should_wake) = if let Ok(mut state) = shared.lock() {
20615        let queue = state.server_events.entry(server_id).or_default();
20616        let should_wake = queue.is_empty();
20617        queue.push_back(event);
20618        (
20619            Some(Arc::clone(&state.ready)),
20620            state.event_session.clone(),
20621            should_wake,
20622        )
20623    } else {
20624        (None, None, false)
20625    };
20626    if let Some(ready) = ready {
20627        ready.notify_all();
20628    }
20629    if should_wake {
20630        push_http2_retain_wake(wake_session, "server", server_id);
20631    }
20632}
20633
20634fn push_http2_session_event(
20635    shared: &Arc<Mutex<crate::state::Http2SharedState>>,
20636    session_id: u64,
20637    event: Http2BridgeEvent,
20638) {
20639    let (ready, wake_session, should_wake) = if let Ok(mut state) = shared.lock() {
20640        let queue = state.session_events.entry(session_id).or_default();
20641        let should_wake = queue.is_empty();
20642        queue.push_back(event);
20643        (
20644            Some(Arc::clone(&state.ready)),
20645            state.event_session.clone(),
20646            should_wake,
20647        )
20648    } else {
20649        (None, None, false)
20650    };
20651    if let Some(ready) = ready {
20652        ready.notify_all();
20653    }
20654    if should_wake {
20655        push_http2_retain_wake(wake_session, "session", session_id);
20656    }
20657}
20658
20659fn push_http2_retain_wake(session: Option<V8SessionHandle>, kind: &'static str, id: u64) {
20660    let Some(session) = session else {
20661        return;
20662    };
20663    let payload = v8_runtime::json_to_cbor_payload(&json!({
20664        "event": "http2",
20665        "kind": kind,
20666        "id": id,
20667    }))
20668    .unwrap_or_default();
20669    let _ = session.send_stream_event("net_socket", payload);
20670}
20671
20672fn pop_http2_event(
20673    queue: &mut BTreeMap<u64, VecDeque<Http2BridgeEvent>>,
20674    id: u64,
20675) -> Option<Http2BridgeEvent> {
20676    queue.get_mut(&id).and_then(VecDeque::pop_front)
20677}
20678
20679fn wait_for_http2_event(
20680    shared: &Arc<Mutex<crate::state::Http2SharedState>>,
20681    id: u64,
20682    is_server: bool,
20683    wait_ms: u64,
20684) -> Option<Http2BridgeEvent> {
20685    let deadline = Instant::now() + Duration::from_millis(wait_ms);
20686    let mut state = shared.lock().ok()?;
20687    loop {
20688        let queue = if is_server {
20689            &mut state.server_events
20690        } else {
20691            &mut state.session_events
20692        };
20693        if let Some(event) = pop_http2_event(queue, id) {
20694            return Some(event);
20695        }
20696        if wait_ms == 0 {
20697            return None;
20698        }
20699        let now = Instant::now();
20700        if now >= deadline {
20701            return None;
20702        }
20703        let remaining = deadline.saturating_duration_since(now);
20704        if remaining.is_zero() {
20705            return None;
20706        }
20707        let ready = Arc::clone(&state.ready);
20708        state = ready.wait_timeout(state, remaining).ok()?.0;
20709    }
20710}
20711
20712fn next_http2_session_id(shared: &mut crate::state::Http2SharedState) -> u64 {
20713    shared.next_session_id += 1;
20714    shared.next_session_id
20715}
20716
20717fn next_http2_stream_id(shared: &mut crate::state::Http2SharedState) -> u64 {
20718    shared.next_stream_id += 1;
20719    shared.next_stream_id
20720}
20721
20722fn http2_reason(code: Option<u32>) -> Reason {
20723    code.unwrap_or(Reason::NO_ERROR.into()).into()
20724}
20725
20726fn http2_error_payload(message: impl Into<String>) -> String {
20727    serde_json::to_string(&json!({
20728        "name": "Error",
20729        "code": "ERR_HTTP2_ERROR",
20730        "message": message.into(),
20731    }))
20732    .unwrap_or_else(|_| {
20733        String::from(
20734            "{\"name\":\"Error\",\"code\":\"ERR_HTTP2_ERROR\",\"message\":\"HTTP/2 bridge error\"}",
20735        )
20736    })
20737}
20738
20739fn http2_socket_snapshot(local_addr: SocketAddr, remote_addr: SocketAddr) -> Http2SocketSnapshot {
20740    Http2SocketSnapshot {
20741        encrypted: false,
20742        allow_half_open: false,
20743        local_address: Some(local_addr.ip().to_string()),
20744        local_port: Some(local_addr.port()),
20745        local_family: Some(socket_addr_family(&local_addr).to_string()),
20746        remote_address: Some(remote_addr.ip().to_string()),
20747        remote_port: Some(remote_addr.port()),
20748        remote_family: Some(socket_addr_family(&remote_addr).to_string()),
20749        servername: None,
20750        alpn_protocol: Some(String::from("h2c")),
20751    }
20752}
20753
20754fn http2_wait_result(kind: &str, id: u64) -> Value {
20755    json!({
20756        "kind": kind,
20757        "id": id,
20758    })
20759}
20760
20761fn is_http2_terminal_event(event: &Http2BridgeEvent, is_server: bool, id: u64) -> bool {
20762    if is_server {
20763        event.kind == "serverClose" && event.id == id
20764    } else {
20765        event.kind == "sessionClose" && event.id == id
20766    }
20767}
20768
20769fn dispatch_http2_wait_loop(
20770    process: &ActiveProcess,
20771    id: u64,
20772    is_server: bool,
20773) -> Result<Value, SidecarError> {
20774    loop {
20775        if let Some(event) = wait_for_http2_event(&process.http2.shared, id, is_server, 50) {
20776            let payload = serde_json::to_value(&event).map_err(|error| {
20777                SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}"))
20778            })?;
20779            process
20780                .execution
20781                .send_javascript_stream_event("http2", payload.clone())?;
20782            if is_http2_terminal_event(&event, is_server, id) {
20783                return Ok(payload);
20784            }
20785            continue;
20786        }
20787
20788        let exists = process
20789            .http2
20790            .shared
20791            .lock()
20792            .map(|state| {
20793                if is_server {
20794                    state.servers.contains_key(&id)
20795                } else {
20796                    state.sessions.contains_key(&id)
20797                }
20798            })
20799            .unwrap_or(false);
20800        if !exists {
20801            return Ok(if is_server {
20802                http2_wait_result("serverClose", id)
20803            } else {
20804                http2_wait_result("sessionClose", id)
20805            });
20806        }
20807    }
20808}
20809
20810fn dispatch_http_wait_loop(process: &ActiveProcess, server_id: u64) -> Result<Value, SidecarError> {
20811    loop {
20812        if !process.http_servers.contains_key(&server_id) {
20813            return Ok(json!({
20814                "kind": "serverClose",
20815                "id": server_id,
20816            }));
20817        }
20818        thread::sleep(Duration::from_millis(25));
20819    }
20820}
20821
20822fn http2_settings_from_value(settings: &BTreeMap<String, Value>) -> BTreeMap<String, Value> {
20823    settings.clone()
20824}
20825
20826fn parse_http2_headers_json(
20827    headers_json: &str,
20828    label: &str,
20829) -> Result<BTreeMap<String, Value>, SidecarError> {
20830    serde_json::from_str::<BTreeMap<String, Value>>(headers_json)
20831        .map_err(|error| SidecarError::InvalidState(format!("{label} must be valid JSON: {error}")))
20832}
20833
20834fn apply_http2_header_values(
20835    header_map: &mut HeaderMap,
20836    name: &str,
20837    value: &Value,
20838) -> Result<(), SidecarError> {
20839    let header_name = HeaderName::from_bytes(name.as_bytes()).map_err(|error| {
20840        SidecarError::InvalidState(format!("invalid HTTP/2 header name {name:?}: {error}"))
20841    })?;
20842    match value {
20843        Value::Array(values) => {
20844            for value in values {
20845                apply_http2_header_values(header_map, name, value)?;
20846            }
20847        }
20848        Value::String(text) => {
20849            let value = HeaderValue::from_str(text).map_err(|error| {
20850                SidecarError::InvalidState(format!(
20851                    "invalid HTTP/2 header value for {name}: {error}"
20852                ))
20853            })?;
20854            header_map.append(header_name.clone(), value);
20855        }
20856        Value::Number(number) => {
20857            let value = HeaderValue::from_str(&number.to_string()).map_err(|error| {
20858                SidecarError::InvalidState(format!(
20859                    "invalid HTTP/2 numeric header value for {name}: {error}"
20860                ))
20861            })?;
20862            header_map.append(header_name.clone(), value);
20863        }
20864        Value::Bool(boolean) => {
20865            let value = HeaderValue::from_str(if *boolean { "true" } else { "false" }).map_err(
20866                |error| {
20867                    SidecarError::InvalidState(format!(
20868                        "invalid HTTP/2 boolean header value for {name}: {error}"
20869                    ))
20870                },
20871            )?;
20872            header_map.append(header_name.clone(), value);
20873        }
20874        Value::Null => {}
20875        Value::Object(_) => {
20876            return Err(SidecarError::InvalidState(format!(
20877                "unsupported HTTP/2 header object value for {name}"
20878            )));
20879        }
20880    }
20881    Ok(())
20882}
20883
20884fn build_http2_request(headers_json: &str) -> Result<Request<()>, SidecarError> {
20885    let headers = parse_http2_headers_json(headers_json, "HTTP/2 request headers")?;
20886    let method = headers
20887        .get(":method")
20888        .and_then(Value::as_str)
20889        .unwrap_or("GET");
20890    let path = headers.get(":path").and_then(Value::as_str).unwrap_or("/");
20891    let mut builder = Request::builder()
20892        .method(Method::from_bytes(method.as_bytes()).map_err(|error| {
20893            SidecarError::InvalidState(format!("invalid HTTP/2 method {method:?}: {error}"))
20894        })?)
20895        .uri(path.parse::<Uri>().map_err(|error| {
20896            SidecarError::InvalidState(format!("invalid HTTP/2 path {path:?}: {error}"))
20897        })?);
20898    {
20899        let header_map = builder.headers_mut().expect("request header map");
20900        for (name, value) in &headers {
20901            if name.starts_with(':') {
20902                continue;
20903            }
20904            apply_http2_header_values(header_map, name, value)?;
20905        }
20906    }
20907    builder
20908        .body(())
20909        .map_err(|error| SidecarError::InvalidState(format!("invalid HTTP/2 request: {error}")))
20910}
20911
20912fn build_http2_response(headers_json: &str) -> Result<Response<()>, SidecarError> {
20913    let headers = parse_http2_headers_json(headers_json, "HTTP/2 response headers")?;
20914    let status = headers
20915        .get(":status")
20916        .and_then(Value::as_u64)
20917        .or_else(|| {
20918            headers
20919                .get(":status")
20920                .and_then(Value::as_str)
20921                .and_then(|value| value.parse::<u16>().ok().map(u64::from))
20922        })
20923        .unwrap_or(200);
20924    let mut builder = Response::builder().status(status as u16);
20925    {
20926        let header_map = builder.headers_mut().expect("response header map");
20927        for (name, value) in &headers {
20928            if name.starts_with(':') {
20929                continue;
20930            }
20931            apply_http2_header_values(header_map, name, value)?;
20932        }
20933    }
20934    builder.body(()).map_err(|error| {
20935        SidecarError::InvalidState(format!("invalid HTTP/2 response headers: {error}"))
20936    })
20937}
20938
20939fn serialize_http2_headers_map(
20940    pseudo: BTreeMap<String, Value>,
20941    headers: &HeaderMap,
20942) -> Result<String, SidecarError> {
20943    let mut serialized = pseudo;
20944    for (name, value) in headers {
20945        let name = name.as_str().to_string();
20946        let value = Value::String(
20947            value
20948                .to_str()
20949                .map_err(|error| {
20950                    SidecarError::Execution(format!("invalid HTTP/2 header value: {error}"))
20951                })?
20952                .to_owned(),
20953        );
20954        match serialized.get_mut(&name) {
20955            Some(Value::Array(values)) => values.push(value),
20956            Some(existing) => {
20957                let first = existing.clone();
20958                *existing = Value::Array(vec![first, value]);
20959            }
20960            None => {
20961                serialized.insert(name, value);
20962            }
20963        }
20964    }
20965    serde_json::to_string(&serialized)
20966        .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
20967}
20968
20969fn serialize_http2_request_headers(
20970    request: &Request<h2::RecvStream>,
20971) -> Result<String, SidecarError> {
20972    let mut pseudo = BTreeMap::new();
20973    pseudo.insert(
20974        String::from(":method"),
20975        Value::String(request.method().as_str().to_string()),
20976    );
20977    pseudo.insert(
20978        String::from(":path"),
20979        Value::String(
20980            request
20981                .uri()
20982                .path_and_query()
20983                .map(|value| value.as_str().to_string())
20984                .unwrap_or_else(|| String::from("/")),
20985        ),
20986    );
20987    serialize_http2_headers_map(pseudo, request.headers())
20988}
20989
20990fn serialize_http2_response_headers(
20991    response: &Response<h2::RecvStream>,
20992) -> Result<String, SidecarError> {
20993    let mut pseudo = BTreeMap::new();
20994    pseudo.insert(
20995        String::from(":status"),
20996        Value::Number(serde_json::Number::from(response.status().as_u16())),
20997    );
20998    serialize_http2_headers_map(pseudo, response.headers())
20999}
21000
21001fn remove_http2_session_resources(
21002    shared: &Arc<Mutex<crate::state::Http2SharedState>>,
21003    session_id: u64,
21004) {
21005    if let Ok(mut state) = shared.lock() {
21006        state.sessions.remove(&session_id);
21007        state.session_events.remove(&session_id);
21008        let stream_ids = state
21009            .streams
21010            .iter()
21011            .filter_map(|(stream_id, stream)| {
21012                (stream.session_id == session_id).then_some(*stream_id)
21013            })
21014            .collect::<Vec<_>>();
21015        for stream_id in stream_ids {
21016            state.streams.remove(&stream_id);
21017        }
21018    }
21019}
21020
21021fn spawn_http2_client_session(
21022    shared: Arc<Mutex<crate::state::Http2SharedState>>,
21023    session_id: u64,
21024    remote_addr: SocketAddr,
21025    tls: Option<JavascriptTlsBridgeOptions>,
21026    snapshot: Arc<Mutex<Http2SessionSnapshot>>,
21027    mut command_rx: UnboundedReceiver<Http2SessionCommand>,
21028) {
21029    thread::spawn(move || {
21030        let runtime = match TokioRuntimeBuilder::new_current_thread()
21031            .enable_all()
21032            .build()
21033        {
21034            Ok(runtime) => runtime,
21035            Err(error) => {
21036                push_http2_session_event(
21037                    &shared,
21038                    session_id,
21039                    Http2BridgeEvent {
21040                        kind: String::from("sessionError"),
21041                        id: session_id,
21042                        data: Some(http2_error_payload(error.to_string())),
21043                        ..Http2BridgeEvent::default()
21044                    },
21045                );
21046                remove_http2_session_resources(&shared, session_id);
21047                return;
21048            }
21049        };
21050
21051        runtime.block_on(async move {
21052            let stream = match tokio::net::TcpStream::connect(remote_addr).await {
21053                Ok(stream) => stream,
21054                Err(error) => {
21055                    push_http2_session_event(
21056                        &shared,
21057                        session_id,
21058                        Http2BridgeEvent {
21059                            kind: String::from("sessionError"),
21060                            id: session_id,
21061                            data: Some(http2_error_payload(error.to_string())),
21062                            ..Http2BridgeEvent::default()
21063                        },
21064                    );
21065                    remove_http2_session_resources(&shared, session_id);
21066                    return;
21067                }
21068            };
21069
21070            let local_addr = match stream.local_addr() {
21071                Ok(addr) => addr,
21072                Err(error) => {
21073                    push_http2_session_event(
21074                        &shared,
21075                        session_id,
21076                        Http2BridgeEvent {
21077                            kind: String::from("sessionError"),
21078                            id: session_id,
21079                            data: Some(http2_error_payload(error.to_string())),
21080                            ..Http2BridgeEvent::default()
21081                        },
21082                    );
21083                    remove_http2_session_resources(&shared, session_id);
21084                    return;
21085                }
21086            };
21087
21088            {
21089                let mut snapshot_guard = snapshot.lock().expect("http2 snapshot lock");
21090                snapshot_guard.socket = http2_socket_snapshot(local_addr, remote_addr);
21091                if let Some(options) = tls.as_ref() {
21092                    snapshot_guard.encrypted = true;
21093                    snapshot_guard.alpn_protocol = Some(String::from("h2"));
21094                    snapshot_guard.socket.encrypted = true;
21095                    snapshot_guard.socket.servername = options.servername.clone();
21096                    snapshot_guard.socket.alpn_protocol = Some(String::from("h2"));
21097                }
21098                snapshot_guard.state = http2_runtime_snapshot();
21099            }
21100            if let Ok(snapshot_json) =
21101                http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())
21102            {
21103                push_http2_session_event(
21104                    &shared,
21105                    session_id,
21106                    Http2BridgeEvent {
21107                        kind: String::from("sessionConnect"),
21108                        id: session_id,
21109                        data: Some(snapshot_json),
21110                        ..Http2BridgeEvent::default()
21111                    },
21112                );
21113            }
21114
21115            let io: Pin<Box<dyn Http2AsyncIo>> = if let Some(options) = tls.as_ref() {
21116                let server_name = match ServerName::try_from(
21117                    options
21118                        .servername
21119                        .clone()
21120                        .unwrap_or_else(|| String::from("localhost")),
21121                ) {
21122                    Ok(server_name) => server_name,
21123                    Err(_) => {
21124                        push_http2_session_event(
21125                            &shared,
21126                            session_id,
21127                            Http2BridgeEvent {
21128                                kind: String::from("sessionError"),
21129                                id: session_id,
21130                                data: Some(http2_error_payload("invalid TLS servername")),
21131                                ..Http2BridgeEvent::default()
21132                            },
21133                        );
21134                        remove_http2_session_resources(&shared, session_id);
21135                        return;
21136                    }
21137                };
21138                let connector = match build_client_tls_config(options) {
21139                    Ok(config) => TlsConnector::from(Arc::new(config)),
21140                    Err(error) => {
21141                        push_http2_session_event(
21142                            &shared,
21143                            session_id,
21144                            Http2BridgeEvent {
21145                                kind: String::from("sessionError"),
21146                                id: session_id,
21147                                data: Some(http2_error_payload(error.to_string())),
21148                                ..Http2BridgeEvent::default()
21149                            },
21150                        );
21151                        remove_http2_session_resources(&shared, session_id);
21152                        return;
21153                    }
21154                };
21155                match connector.connect(server_name, stream).await {
21156                    Ok(tls_stream) => Box::pin(tls_stream),
21157                    Err(error) => {
21158                        push_http2_session_event(
21159                            &shared,
21160                            session_id,
21161                            Http2BridgeEvent {
21162                                kind: String::from("sessionError"),
21163                                id: session_id,
21164                                data: Some(http2_error_payload(error.to_string())),
21165                                ..Http2BridgeEvent::default()
21166                            },
21167                        );
21168                        remove_http2_session_resources(&shared, session_id);
21169                        return;
21170                    }
21171                }
21172            } else {
21173                Box::pin(stream)
21174            };
21175
21176            let (mut sender, connection) = match client::handshake(io).await {
21177                Ok(parts) => parts,
21178                Err(error) => {
21179                    push_http2_session_event(
21180                        &shared,
21181                        session_id,
21182                        Http2BridgeEvent {
21183                            kind: String::from("sessionError"),
21184                            id: session_id,
21185                            data: Some(http2_error_payload(error.to_string())),
21186                            ..Http2BridgeEvent::default()
21187                        },
21188                    );
21189                    remove_http2_session_resources(&shared, session_id);
21190                    return;
21191                }
21192            };
21193
21194            let (status_tx, mut status_rx) = unbounded_channel::<Result<(), String>>();
21195            tokio::spawn(async move {
21196                let _ = status_tx.send(connection.await.map_err(|error| error.to_string()));
21197            });
21198
21199            let streams: Arc<Mutex<BTreeMap<u64, ClientHttp2StreamState>>> =
21200                Arc::new(Mutex::new(BTreeMap::new()));
21201
21202            loop {
21203                tokio::select! {
21204                    Some(result) = status_rx.recv() => {
21205                        if let Err(message) = result {
21206                            push_http2_session_event(
21207                                &shared,
21208                                session_id,
21209                                Http2BridgeEvent {
21210                                    kind: String::from("sessionError"),
21211                                    id: session_id,
21212                                    data: Some(http2_error_payload(message)),
21213                                    ..Http2BridgeEvent::default()
21214                                },
21215                            );
21216                        }
21217                        push_http2_session_event(
21218                            &shared,
21219                            session_id,
21220                            Http2BridgeEvent {
21221                                kind: String::from("sessionClose"),
21222                                id: session_id,
21223                                ..Http2BridgeEvent::default()
21224                            },
21225                        );
21226                        remove_http2_session_resources(&shared, session_id);
21227                        break;
21228                    }
21229                    Some(command) = command_rx.recv() => {
21230                        match command {
21231                            Http2SessionCommand::Request { headers_json, options_json, respond_to } => {
21232                                let request = match build_http2_request(&headers_json) {
21233                                    Ok(request) => request,
21234                                    Err(error) => {
21235                                        let _ = respond_to.send(Err(error.to_string()));
21236                                        continue;
21237                                    }
21238                                };
21239                                let options: JavascriptHttp2RequestOptions =
21240                                    serde_json::from_str(&options_json).unwrap_or_default();
21241                                let stream_id = {
21242                                    let mut state = shared.lock().expect("http2 shared state");
21243                                    let stream_id = next_http2_stream_id(&mut state);
21244                                    state.streams.insert(
21245                                        stream_id,
21246                                        ActiveHttp2Stream {
21247                                            session_id,
21248                                            paused: Arc::new(AtomicBool::new(false)),
21249                                            resume_notify: Arc::new(tokio::sync::Notify::new()),
21250                                        },
21251                                    );
21252                                    stream_id
21253                                };
21254                                match sender.send_request(request, options.end_stream) {
21255                                    Ok((response_future, send_stream)) => {
21256                                        if !options.end_stream {
21257                                            streams
21258                                                .lock()
21259                                                .expect("http2 client streams")
21260                                                .insert(stream_id, ClientHttp2StreamState { send_stream: Some(send_stream) });
21261                                        }
21262                                        let shared_clone = Arc::clone(&shared);
21263                                        let snapshot_clone = Arc::clone(&snapshot);
21264                                        tokio::spawn(async move {
21265                                            match response_future.await {
21266                                                Ok(response) => {
21267                                                    if let Ok(headers_json) = serialize_http2_response_headers(&response) {
21268                                                        push_http2_session_event(
21269                                                            &shared_clone,
21270                                                            session_id,
21271                                                            Http2BridgeEvent {
21272                                                                kind: String::from("clientResponseHeaders"),
21273                                                                id: stream_id,
21274                                                                data: Some(headers_json),
21275                                                                ..Http2BridgeEvent::default()
21276                                                            },
21277                                                        );
21278                                                    }
21279                                                    let mut body = response.into_body();
21280                                                    while let Some(chunk) = body.data().await {
21281                                                        match chunk {
21282                                                            Ok(bytes) => {
21283                                                                let pause_state = {
21284                                                                    let state = shared_clone.lock().expect("http2 shared state");
21285                                                                    state.streams.get(&stream_id).map(|stream| {
21286                                                                        (
21287                                                                            Arc::clone(&stream.paused),
21288                                                                            Arc::clone(&stream.resume_notify),
21289                                                                        )
21290                                                                    })
21291                                                                };
21292                                                                if let Some((paused, resume_notify)) = pause_state {
21293                                                                    while paused.load(Ordering::SeqCst) {
21294                                                                        let notified = resume_notify.notified();
21295                                                                        tokio::pin!(notified);
21296                                                                        notified.as_mut().enable();
21297                                                                        if !paused.load(Ordering::SeqCst) { break; }
21298                                                                        let _ = tokio::time::timeout(Duration::from_millis(250), notified).await;
21299                                                                    }
21300                                                                }
21301                                                                let _ = body.flow_control().release_capacity(bytes.len());
21302                                                                push_http2_session_event(
21303                                                                    &shared_clone,
21304                                                                    session_id,
21305                                                                    Http2BridgeEvent {
21306                                                                        kind: String::from("clientData"),
21307                                                                        id: stream_id,
21308                                                                        data: Some(base64::engine::general_purpose::STANDARD.encode(bytes)),
21309                                                                        ..Http2BridgeEvent::default()
21310                                                                    },
21311                                                                );
21312                                                            }
21313                                                            Err(error) => {
21314                                                                push_http2_session_event(
21315                                                                    &shared_clone,
21316                                                                    session_id,
21317                                                                    Http2BridgeEvent {
21318                                                                        kind: String::from("clientError"),
21319                                                                        id: stream_id,
21320                                                                        data: Some(http2_error_payload(error.to_string())),
21321                                                                        ..Http2BridgeEvent::default()
21322                                                                    },
21323                                                                );
21324                                                                break;
21325                                                            }
21326                                                        }
21327                                                    }
21328                                                    {
21329                                                        let mut snapshot = snapshot_clone.lock().expect("http2 snapshot lock");
21330                                                        snapshot.state.next_stream_id =
21331                                                            snapshot.state.next_stream_id.saturating_add(2);
21332                                                    }
21333                                                    push_http2_session_event(
21334                                                        &shared_clone,
21335                                                        session_id,
21336                                                        Http2BridgeEvent {
21337                                                            kind: String::from("clientEnd"),
21338                                                            id: stream_id,
21339                                                            ..Http2BridgeEvent::default()
21340                                                        },
21341                                                    );
21342                                                    push_http2_session_event(
21343                                                        &shared_clone,
21344                                                        session_id,
21345                                                        Http2BridgeEvent {
21346                                                            kind: String::from("clientClose"),
21347                                                            id: stream_id,
21348                                                            extra_number: Some(0),
21349                                                            ..Http2BridgeEvent::default()
21350                                                        },
21351                                                    );
21352                                                    if let Ok(mut state) = shared_clone.lock() {
21353                                                        state.streams.remove(&stream_id);
21354                                                    }
21355                                                }
21356                                                Err(error) => {
21357                                                    push_http2_session_event(
21358                                                        &shared_clone,
21359                                                        session_id,
21360                                                        Http2BridgeEvent {
21361                                                            kind: String::from("clientError"),
21362                                                            id: stream_id,
21363                                                            data: Some(http2_error_payload(error.to_string())),
21364                                                            ..Http2BridgeEvent::default()
21365                                                        },
21366                                                    );
21367                                                    push_http2_session_event(
21368                                                        &shared_clone,
21369                                                        session_id,
21370                                                        Http2BridgeEvent {
21371                                                            kind: String::from("clientClose"),
21372                                                            id: stream_id,
21373                                                            extra_number: Some(u32::from(Reason::INTERNAL_ERROR) as u64),
21374                                                            ..Http2BridgeEvent::default()
21375                                                        },
21376                                                    );
21377                                                    if let Ok(mut state) = shared_clone.lock() {
21378                                                        state.streams.remove(&stream_id);
21379                                                    }
21380                                                }
21381                                            }
21382                                        });
21383                                        let _ = respond_to.send(Ok(json!(stream_id)));
21384                                    }
21385                                    Err(error) => {
21386                                        if let Ok(mut state) = shared.lock() {
21387                                            state.streams.remove(&stream_id);
21388                                        }
21389                                        let _ = respond_to.send(Err(error.to_string()));
21390                                    }
21391                                }
21392                            }
21393                            Http2SessionCommand::Settings { settings_json, respond_to } => {
21394                                let settings = serde_json::from_str::<BTreeMap<String, Value>>(&settings_json)
21395                                    .unwrap_or_default();
21396                                {
21397                                    let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
21398                                    snapshot.local_settings = http2_settings_from_value(&settings);
21399                                }
21400                                if let Ok(headers_json) = serde_json::to_string(&settings) {
21401                                    push_http2_session_event(
21402                                        &shared,
21403                                        session_id,
21404                                        Http2BridgeEvent {
21405                                            kind: String::from("sessionLocalSettings"),
21406                                            id: session_id,
21407                                            data: Some(headers_json.clone()),
21408                                            ..Http2BridgeEvent::default()
21409                                        },
21410                                    );
21411                                    push_http2_session_event(
21412                                        &shared,
21413                                        session_id,
21414                                        Http2BridgeEvent {
21415                                            kind: String::from("sessionSettingsAck"),
21416                                            id: session_id,
21417                                            ..Http2BridgeEvent::default()
21418                                        },
21419                                    );
21420                                }
21421                                let _ = respond_to.send(Ok(Value::Null));
21422                            }
21423                            Http2SessionCommand::SetLocalWindowSize { size, respond_to } => {
21424                                {
21425                                    let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
21426                                    snapshot.state.local_window_size = size;
21427                                    snapshot.state.effective_local_window_size = size;
21428                                }
21429                                let value = snapshot
21430                                    .lock()
21431                                    .ok()
21432                                    .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok())
21433                                    .map(Value::String)
21434                                    .unwrap_or(Value::Null);
21435                                let _ = respond_to.send(Ok(value));
21436                            }
21437                            Http2SessionCommand::Goaway { error_code, last_stream_id, opaque_data, respond_to } => {
21438                                push_http2_session_event(
21439                                    &shared,
21440                                    session_id,
21441                                    Http2BridgeEvent {
21442                                        kind: String::from("sessionGoaway"),
21443                                        id: session_id,
21444                                        data: opaque_data.map(|value| {
21445                                            base64::engine::general_purpose::STANDARD.encode(value)
21446                                        }),
21447                                        extra_number: Some(error_code as u64),
21448                                        flags: Some(last_stream_id as u64),
21449                                        ..Http2BridgeEvent::default()
21450                                    },
21451                                );
21452                                let _ = respond_to.send(Ok(Value::Null));
21453                            }
21454                            Http2SessionCommand::Close { respond_to, .. } => {
21455                                let _ = respond_to.send(Ok(Value::Null));
21456                                push_http2_session_event(
21457                                    &shared,
21458                                    session_id,
21459                                    Http2BridgeEvent {
21460                                        kind: String::from("sessionClose"),
21461                                        id: session_id,
21462                                        ..Http2BridgeEvent::default()
21463                                    },
21464                                );
21465                                remove_http2_session_resources(&shared, session_id);
21466                                break;
21467                            }
21468                            Http2SessionCommand::StreamWrite { stream_id, chunk, end_stream, respond_to } => {
21469                                let result = streams
21470                                    .lock()
21471                                    .expect("http2 client streams")
21472                                    .get_mut(&stream_id)
21473                                    .and_then(|stream| stream.send_stream.as_mut())
21474                                    .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 client stream {stream_id}")))
21475                                    .and_then(|stream| stream.send_data(Bytes::from(chunk), end_stream).map_err(|error| SidecarError::Execution(error.to_string())));
21476                                match result {
21477                                    Ok(()) => {
21478                                        if end_stream {
21479                                            streams.lock().expect("http2 client streams").remove(&stream_id);
21480                                        }
21481                                        let _ = respond_to.send(Ok(Value::Bool(true)));
21482                                    }
21483                                    Err(error) => {
21484                                        let _ = respond_to.send(Err(error.to_string()));
21485                                    }
21486                                }
21487                            }
21488                            Http2SessionCommand::StreamClose { stream_id, error_code, respond_to } => {
21489                                let mut streams = streams.lock().expect("http2 client streams");
21490                                let Some(mut state) = streams.remove(&stream_id) else {
21491                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 client stream {stream_id}")));
21492                                    continue;
21493                                };
21494                                if let Some(stream) = state.send_stream.as_mut() {
21495                                    stream.send_reset(http2_reason(error_code));
21496                                }
21497                                if let Ok(mut state) = shared.lock() {
21498                                    state.streams.remove(&stream_id);
21499                                }
21500                                push_http2_session_event(
21501                                    &shared,
21502                                    session_id,
21503                                    Http2BridgeEvent {
21504                                        kind: String::from("clientClose"),
21505                                        id: stream_id,
21506                                        extra_number: Some(u32::from(http2_reason(error_code)) as u64),
21507                                        ..Http2BridgeEvent::default()
21508                                    },
21509                                );
21510                                let _ = respond_to.send(Ok(Value::Null));
21511                            }
21512                            Http2SessionCommand::StreamRespond { respond_to, .. }
21513                            | Http2SessionCommand::StreamPush { respond_to, .. }
21514                            | Http2SessionCommand::StreamRespondWithFile { respond_to, .. } => {
21515                                let _ = respond_to.send(Err(String::from("HTTP/2 client streams cannot send server responses")));
21516                            }
21517                        }
21518                    }
21519                    else => break,
21520                }
21521            }
21522        });
21523    });
21524}
21525
21526fn spawn_http2_server_session(
21527    shared: Arc<Mutex<crate::state::Http2SharedState>>,
21528    server_id: u64,
21529    session_id: u64,
21530    stream: TcpStream,
21531    tls: Option<JavascriptTlsBridgeOptions>,
21532    snapshot: Arc<Mutex<Http2SessionSnapshot>>,
21533    mut command_rx: UnboundedReceiver<Http2SessionCommand>,
21534) {
21535    thread::spawn(move || {
21536        let runtime = match TokioRuntimeBuilder::new_current_thread()
21537            .enable_all()
21538            .build()
21539        {
21540            Ok(runtime) => runtime,
21541            Err(error) => {
21542                push_http2_server_event(
21543                    &shared,
21544                    server_id,
21545                    Http2BridgeEvent {
21546                        kind: String::from("serverStreamError"),
21547                        id: session_id,
21548                        data: Some(http2_error_payload(error.to_string())),
21549                        ..Http2BridgeEvent::default()
21550                    },
21551                );
21552                remove_http2_session_resources(&shared, session_id);
21553                return;
21554            }
21555        };
21556
21557        runtime.block_on(async move {
21558            if let Err(error) = stream.set_nonblocking(true) {
21559                push_http2_server_event(
21560                    &shared,
21561                    server_id,
21562                    Http2BridgeEvent {
21563                        kind: String::from("serverStreamError"),
21564                        id: session_id,
21565                        data: Some(http2_error_payload(error.to_string())),
21566                        ..Http2BridgeEvent::default()
21567                    },
21568                );
21569                remove_http2_session_resources(&shared, session_id);
21570                return;
21571            }
21572            let stream = match tokio::net::TcpStream::from_std(stream) {
21573                Ok(stream) => stream,
21574                Err(error) => {
21575                    push_http2_server_event(
21576                        &shared,
21577                        server_id,
21578                        Http2BridgeEvent {
21579                            kind: String::from("serverStreamError"),
21580                            id: session_id,
21581                            data: Some(http2_error_payload(error.to_string())),
21582                            ..Http2BridgeEvent::default()
21583                        },
21584                    );
21585                    remove_http2_session_resources(&shared, session_id);
21586                    return;
21587                }
21588            };
21589            let local_addr = match stream.local_addr() {
21590                Ok(addr) => addr,
21591                Err(error) => {
21592                    push_http2_server_event(
21593                        &shared,
21594                        server_id,
21595                        Http2BridgeEvent {
21596                            kind: String::from("serverStreamError"),
21597                            id: session_id,
21598                            data: Some(http2_error_payload(error.to_string())),
21599                            ..Http2BridgeEvent::default()
21600                        },
21601                    );
21602                    remove_http2_session_resources(&shared, session_id);
21603                    return;
21604                }
21605            };
21606            let remote_addr = match stream.peer_addr() {
21607                Ok(addr) => addr,
21608                Err(error) => {
21609                    push_http2_server_event(
21610                        &shared,
21611                        server_id,
21612                        Http2BridgeEvent {
21613                            kind: String::from("serverStreamError"),
21614                            id: session_id,
21615                            data: Some(http2_error_payload(error.to_string())),
21616                            ..Http2BridgeEvent::default()
21617                        },
21618                    );
21619                    remove_http2_session_resources(&shared, session_id);
21620                    return;
21621                }
21622            };
21623            {
21624                let mut snapshot_guard = snapshot.lock().expect("http2 snapshot lock");
21625                snapshot_guard.socket = http2_socket_snapshot(local_addr, remote_addr);
21626                if tls.is_some() {
21627                    snapshot_guard.encrypted = true;
21628                    snapshot_guard.alpn_protocol = Some(String::from("h2"));
21629                    snapshot_guard.socket.encrypted = true;
21630                    snapshot_guard.socket.alpn_protocol = Some(String::from("h2"));
21631                }
21632                snapshot_guard.state = http2_runtime_snapshot();
21633            }
21634            if let Ok(snapshot_json) =
21635                http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())
21636            {
21637                push_http2_server_event(
21638                    &shared,
21639                    server_id,
21640                    Http2BridgeEvent {
21641                        kind: String::from(if tls.is_some() {
21642                            "serverSecureConnection"
21643                        } else {
21644                            "serverConnection"
21645                        }),
21646                        id: server_id,
21647                        data: Some(serde_json::to_string(&http2_socket_snapshot(local_addr, remote_addr)).unwrap_or_default()),
21648                        ..Http2BridgeEvent::default()
21649                    },
21650                );
21651                push_http2_server_event(
21652                    &shared,
21653                    server_id,
21654                    Http2BridgeEvent {
21655                        kind: String::from("serverSession"),
21656                        id: server_id,
21657                        data: Some(snapshot_json),
21658                        extra_number: Some(session_id),
21659                        ..Http2BridgeEvent::default()
21660                    },
21661                );
21662            }
21663
21664            let io: Pin<Box<dyn Http2AsyncIo>> = if let Some(options) = tls.as_ref() {
21665                let acceptor = match build_server_tls_config(options) {
21666                    Ok(config) => TlsAcceptor::from(Arc::new(config)),
21667                    Err(error) => {
21668                        push_http2_server_event(
21669                            &shared,
21670                            server_id,
21671                            Http2BridgeEvent {
21672                                kind: String::from("serverStreamError"),
21673                                id: session_id,
21674                                data: Some(http2_error_payload(error.to_string())),
21675                                ..Http2BridgeEvent::default()
21676                            },
21677                        );
21678                        remove_http2_session_resources(&shared, session_id);
21679                        return;
21680                    }
21681                };
21682                match acceptor.accept(stream).await {
21683                    Ok(tls_stream) => Box::pin(tls_stream),
21684                    Err(error) => {
21685                        push_http2_server_event(
21686                            &shared,
21687                            server_id,
21688                            Http2BridgeEvent {
21689                                kind: String::from("serverStreamError"),
21690                                id: session_id,
21691                                data: Some(http2_error_payload(error.to_string())),
21692                                ..Http2BridgeEvent::default()
21693                            },
21694                        );
21695                        remove_http2_session_resources(&shared, session_id);
21696                        return;
21697                    }
21698                }
21699            } else {
21700                Box::pin(stream)
21701            };
21702
21703            let mut connection = match server::handshake(io).await {
21704                Ok(connection) => connection,
21705                Err(error) => {
21706                    push_http2_server_event(
21707                        &shared,
21708                        server_id,
21709                        Http2BridgeEvent {
21710                            kind: String::from("serverStreamError"),
21711                            id: session_id,
21712                            data: Some(http2_error_payload(error.to_string())),
21713                            ..Http2BridgeEvent::default()
21714                        },
21715                    );
21716                    remove_http2_session_resources(&shared, session_id);
21717                    return;
21718                }
21719            };
21720
21721            let streams: Arc<Mutex<BTreeMap<u64, ServerHttp2StreamState>>> =
21722                Arc::new(Mutex::new(BTreeMap::new()));
21723
21724            loop {
21725                tokio::select! {
21726                    incoming = connection.accept() => {
21727                        match incoming {
21728                            Some(Ok((request, respond))) => {
21729                                let headers_json = match serialize_http2_request_headers(&request) {
21730                                    Ok(headers) => headers,
21731                                    Err(error) => {
21732                                        push_http2_server_event(
21733                                            &shared,
21734                                            server_id,
21735                                            Http2BridgeEvent {
21736                                                kind: String::from("serverStreamError"),
21737                                                id: server_id,
21738                                                data: Some(http2_error_payload(error.to_string())),
21739                                                ..Http2BridgeEvent::default()
21740                                            },
21741                                        );
21742                                        continue;
21743                                    }
21744                                };
21745                                let stream_id = {
21746                                    let mut state = shared.lock().expect("http2 shared state");
21747                                    let stream_id = next_http2_stream_id(&mut state);
21748                                    state.streams.insert(
21749                                        stream_id,
21750                                        ActiveHttp2Stream {
21751                                            session_id,
21752                                            paused: Arc::new(AtomicBool::new(false)),
21753                                            resume_notify: Arc::new(tokio::sync::Notify::new()),
21754                                        },
21755                                    );
21756                                    stream_id
21757                                };
21758                                streams.lock().expect("http2 server streams").insert(
21759                                    stream_id,
21760                                    ServerHttp2StreamState {
21761                                        send_response: Some(ServerHttp2Responder::Regular(respond)),
21762                                        send_stream: None,
21763                                    },
21764                                );
21765                                let snapshot_json = snapshot
21766                                    .lock()
21767                                    .ok()
21768                                    .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok());
21769                                push_http2_server_event(
21770                                    &shared,
21771                                    server_id,
21772                                    Http2BridgeEvent {
21773                                        kind: String::from("serverStream"),
21774                                        id: server_id,
21775                                        data: Some(stream_id.to_string()),
21776                                        extra: snapshot_json,
21777                                        extra_number: Some(session_id),
21778                                        extra_headers: Some(headers_json),
21779                                        flags: Some(0),
21780                                    },
21781                                );
21782                                let shared_clone = Arc::clone(&shared);
21783                                tokio::spawn(async move {
21784                                    let mut body = request.into_body();
21785                                    while let Some(chunk) = body.data().await {
21786                                        match chunk {
21787                                            Ok(bytes) => {
21788                                                let pause_state = {
21789                                                    let state = shared_clone.lock().expect("http2 shared state");
21790                                                    state.streams.get(&stream_id).map(|stream| {
21791                                                        (
21792                                                            Arc::clone(&stream.paused),
21793                                                            Arc::clone(&stream.resume_notify),
21794                                                        )
21795                                                    })
21796                                                };
21797                                                if let Some((paused, resume_notify)) = pause_state {
21798                                                    while paused.load(Ordering::SeqCst) {
21799                                                        let notified = resume_notify.notified();
21800                                                        tokio::pin!(notified);
21801                                                        notified.as_mut().enable();
21802                                                        if !paused.load(Ordering::SeqCst) { break; }
21803                                                        let _ = tokio::time::timeout(Duration::from_millis(250), notified).await;
21804                                                    }
21805                                                }
21806                                                let _ = body.flow_control().release_capacity(bytes.len());
21807                                                push_http2_server_event(
21808                                                    &shared_clone,
21809                                                    server_id,
21810                                                    Http2BridgeEvent {
21811                                                        kind: String::from("serverStreamData"),
21812                                                        id: stream_id,
21813                                                        data: Some(base64::engine::general_purpose::STANDARD.encode(bytes)),
21814                                                        ..Http2BridgeEvent::default()
21815                                                    },
21816                                                );
21817                                            }
21818                                            Err(error) => {
21819                                                push_http2_server_event(
21820                                                    &shared_clone,
21821                                                    server_id,
21822                                                    Http2BridgeEvent {
21823                                                        kind: String::from("serverStreamError"),
21824                                                        id: stream_id,
21825                                                        data: Some(http2_error_payload(error.to_string())),
21826                                                        ..Http2BridgeEvent::default()
21827                                                    },
21828                                                );
21829                                                break;
21830                                            }
21831                                        }
21832                                    }
21833                                    push_http2_server_event(
21834                                        &shared_clone,
21835                                        server_id,
21836                                        Http2BridgeEvent {
21837                                            kind: String::from("serverStreamEnd"),
21838                                            id: stream_id,
21839                                            ..Http2BridgeEvent::default()
21840                                        },
21841                                    );
21842                                });
21843                            }
21844                            Some(Err(error)) => {
21845                                push_http2_server_event(
21846                                    &shared,
21847                                    server_id,
21848                                    Http2BridgeEvent {
21849                                        kind: String::from("serverStreamError"),
21850                                        id: server_id,
21851                                        data: Some(http2_error_payload(error.to_string())),
21852                                        ..Http2BridgeEvent::default()
21853                                    },
21854                                );
21855                                break;
21856                            }
21857                            None => {
21858                                push_http2_server_event(
21859                                    &shared,
21860                                    server_id,
21861                                    Http2BridgeEvent {
21862                                        kind: String::from("sessionClose"),
21863                                        id: session_id,
21864                                        ..Http2BridgeEvent::default()
21865                                    },
21866                                );
21867                                remove_http2_session_resources(&shared, session_id);
21868                                break;
21869                            }
21870                        }
21871                    }
21872                    Some(command) = command_rx.recv() => {
21873                        match command {
21874                            Http2SessionCommand::Settings { settings_json, respond_to } => {
21875                                let settings = serde_json::from_str::<BTreeMap<String, Value>>(&settings_json)
21876                                    .unwrap_or_default();
21877                                if let Some(initial_window_size) = settings
21878                                    .get("initialWindowSize")
21879                                    .and_then(Value::as_u64)
21880                                {
21881                                    let _ = connection.set_initial_window_size(initial_window_size as u32);
21882                                }
21883                                {
21884                                    let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
21885                                    snapshot.local_settings = http2_settings_from_value(&settings);
21886                                }
21887                                if let Ok(headers_json) = serde_json::to_string(&settings) {
21888                                    push_http2_session_event(
21889                                        &shared,
21890                                        session_id,
21891                                        Http2BridgeEvent {
21892                                            kind: String::from("sessionLocalSettings"),
21893                                            id: session_id,
21894                                            data: Some(headers_json),
21895                                            ..Http2BridgeEvent::default()
21896                                        },
21897                                    );
21898                                }
21899                                let _ = respond_to.send(Ok(Value::Null));
21900                            }
21901                            Http2SessionCommand::SetLocalWindowSize { size, respond_to } => {
21902                                connection.set_target_window_size(size);
21903                                {
21904                                    let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
21905                                    snapshot.state.local_window_size = size;
21906                                    snapshot.state.effective_local_window_size = size;
21907                                }
21908                                let value = snapshot
21909                                    .lock()
21910                                    .ok()
21911                                    .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok())
21912                                    .map(Value::String)
21913                                    .unwrap_or(Value::Null);
21914                                let _ = respond_to.send(Ok(value));
21915                            }
21916                            Http2SessionCommand::Goaway { error_code, last_stream_id, opaque_data, respond_to } => {
21917                                connection.abrupt_shutdown(http2_reason(Some(error_code)));
21918                                push_http2_session_event(
21919                                    &shared,
21920                                    session_id,
21921                                    Http2BridgeEvent {
21922                                        kind: String::from("sessionGoaway"),
21923                                        id: session_id,
21924                                        data: opaque_data.map(|value| {
21925                                            base64::engine::general_purpose::STANDARD.encode(value)
21926                                        }),
21927                                        extra_number: Some(error_code as u64),
21928                                        flags: Some(last_stream_id as u64),
21929                                        ..Http2BridgeEvent::default()
21930                                    },
21931                                );
21932                                let _ = respond_to.send(Ok(Value::Null));
21933                            }
21934                            Http2SessionCommand::Close { abrupt, respond_to } => {
21935                                if abrupt {
21936                                    connection.abrupt_shutdown(Reason::NO_ERROR);
21937                                } else {
21938                                    connection.graceful_shutdown();
21939                                }
21940                                let _ = respond_to.send(Ok(Value::Null));
21941                                push_http2_session_event(
21942                                    &shared,
21943                                    session_id,
21944                                    Http2BridgeEvent {
21945                                        kind: String::from("sessionClose"),
21946                                        id: session_id,
21947                                        ..Http2BridgeEvent::default()
21948                                    },
21949                                );
21950                                remove_http2_session_resources(&shared, session_id);
21951                                break;
21952                            }
21953                            Http2SessionCommand::StreamRespond { stream_id, headers_json, respond_to } => {
21954                                let response = match build_http2_response(&headers_json) {
21955                                    Ok(response) => response,
21956                                    Err(error) => {
21957                                        let _ = respond_to.send(Err(error.to_string()));
21958                                        continue;
21959                                    }
21960                                };
21961                                let mut streams = streams.lock().expect("http2 server streams");
21962                                let Some(state) = streams.get_mut(&stream_id) else {
21963                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
21964                                    continue;
21965                                };
21966                                let Some(send_response) = state.send_response.as_mut() else {
21967                                    let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} already responded")));
21968                                    continue;
21969                                };
21970                                match match send_response {
21971                                    ServerHttp2Responder::Regular(send_response) => {
21972                                        send_response.send_response(response, false)
21973                                    }
21974                                    ServerHttp2Responder::Pushed(send_response) => {
21975                                        send_response.send_response(response, false)
21976                                    }
21977                                } {
21978                                    Ok(send_stream) => {
21979                                        state.send_stream = Some(send_stream);
21980                                        state.send_response = None;
21981                                        let _ = respond_to.send(Ok(Value::Null));
21982                                    }
21983                                    Err(error) => {
21984                                        let _ = respond_to.send(Err(error.to_string()));
21985                                    }
21986                                }
21987                            }
21988                            Http2SessionCommand::StreamPush { stream_id, headers_json, respond_to } => {
21989                                let request = match build_http2_request(&headers_json) {
21990                                    Ok(request) => request,
21991                                    Err(error) => {
21992                                        let _ = respond_to.send(Err(error.to_string()));
21993                                        continue;
21994                                    }
21995                                };
21996                                let mut streams_guard = streams.lock().expect("http2 server streams");
21997                                let Some(state) = streams_guard.get_mut(&stream_id) else {
21998                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
21999                                    continue;
22000                                };
22001                                let Some(send_response) = state.send_response.as_mut() else {
22002                                    let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} cannot push after responding")));
22003                                    continue;
22004                                };
22005                                let ServerHttp2Responder::Regular(send_response) = send_response else {
22006                                    let _ = respond_to.send(Err(format!("HTTP/2 pushed stream {stream_id} cannot create nested push promises")));
22007                                    continue;
22008                                };
22009                                match send_response.push_request(request) {
22010                                    Ok(pushed) => {
22011                                        let pushed_stream_id = {
22012                                            let mut state = shared.lock().expect("http2 shared state");
22013                                            let pushed_stream_id = next_http2_stream_id(&mut state);
22014                                            state.streams.insert(
22015                                                pushed_stream_id,
22016                                                ActiveHttp2Stream {
22017                                                    session_id,
22018                                                    paused: Arc::new(AtomicBool::new(false)),
22019                                                    resume_notify: Arc::new(tokio::sync::Notify::new()),
22020                                                },
22021                                            );
22022                                            pushed_stream_id
22023                                        };
22024                                        streams_guard.insert(
22025                                            pushed_stream_id,
22026                                            ServerHttp2StreamState {
22027                                                send_response: Some(ServerHttp2Responder::Pushed(pushed)),
22028                                                send_stream: None,
22029                                            },
22030                                        );
22031                                        let _ = respond_to.send(Ok(json!({
22032                                            "streamId": pushed_stream_id,
22033                                            "headers": headers_json,
22034                                        }).to_string().into()));
22035                                    }
22036                                    Err(error) => {
22037                                        let _ = respond_to.send(Err(error.to_string()));
22038                                    }
22039                                }
22040                            }
22041                            Http2SessionCommand::StreamWrite { stream_id, chunk, end_stream, respond_to } => {
22042                                let mut streams = streams.lock().expect("http2 server streams");
22043                                let Some(state) = streams.get_mut(&stream_id) else {
22044                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
22045                                    continue;
22046                                };
22047                                let Some(send_stream) = state.send_stream.as_mut() else {
22048                                    let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} has not sent response headers")));
22049                                    continue;
22050                                };
22051                                match send_stream.send_data(Bytes::from(chunk), end_stream) {
22052                                    Ok(()) => {
22053                                        if end_stream {
22054                                            streams.remove(&stream_id);
22055                                            if let Ok(mut state) = shared.lock() {
22056                                                state.streams.remove(&stream_id);
22057                                            }
22058                                            push_http2_server_event(
22059                                                &shared,
22060                                                server_id,
22061                                                Http2BridgeEvent {
22062                                                    kind: String::from("serverStreamClose"),
22063                                                    id: stream_id,
22064                                                    extra_number: Some(0),
22065                                                    ..Http2BridgeEvent::default()
22066                                                },
22067                                            );
22068                                        }
22069                                        let _ = respond_to.send(Ok(Value::Bool(true)));
22070                                    }
22071                                    Err(error) => {
22072                                        let _ = respond_to.send(Err(error.to_string()));
22073                                    }
22074                                }
22075                            }
22076                            Http2SessionCommand::StreamClose { stream_id, error_code, respond_to } => {
22077                                let mut streams_guard = streams.lock().expect("http2 server streams");
22078                                let Some(mut state) = streams_guard.remove(&stream_id) else {
22079                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
22080                                    continue;
22081                                };
22082                                let reason = http2_reason(error_code);
22083                                if let Some(send_stream) = state.send_stream.as_mut() {
22084                                    send_stream.send_reset(reason);
22085                                }
22086                                if let Some(send_response) = state.send_response.as_mut() {
22087                                    match send_response {
22088                                        ServerHttp2Responder::Regular(send_response) => {
22089                                            send_response.send_reset(reason)
22090                                        }
22091                                        ServerHttp2Responder::Pushed(send_response) => {
22092                                            send_response.send_reset(reason)
22093                                        }
22094                                    }
22095                                }
22096                                if let Ok(mut shared_guard) = shared.lock() {
22097                                    shared_guard.streams.remove(&stream_id);
22098                                }
22099                                push_http2_server_event(
22100                                    &shared,
22101                                    server_id,
22102                                    Http2BridgeEvent {
22103                                        kind: String::from("serverStreamClose"),
22104                                        id: stream_id,
22105                                        extra_number: Some(u32::from(reason) as u64),
22106                                        ..Http2BridgeEvent::default()
22107                                    },
22108                                );
22109                                let _ = respond_to.send(Ok(Value::Null));
22110                            }
22111                            Http2SessionCommand::StreamRespondWithFile { stream_id, body, headers_json, options_json, respond_to } => {
22112                                let options: JavascriptHttp2FileResponseOptions =
22113                                    serde_json::from_str(&options_json).unwrap_or_default();
22114                                let response = match build_http2_response(&headers_json) {
22115                                    Ok(response) => response,
22116                                    Err(error) => {
22117                                        let _ = respond_to.send(Err(error.to_string()));
22118                                        continue;
22119                                    }
22120                                };
22121                                let offset = usize::try_from(options.offset.unwrap_or_default()).unwrap_or(0);
22122                                let body = if offset >= body.len() {
22123                                    Vec::new()
22124                                } else {
22125                                    let body = &body[offset..];
22126                                    match options.length {
22127                                        Some(length) if length >= 0 => {
22128                                            body[..body.len().min(length as usize)].to_vec()
22129                                        }
22130                                        _ => body.to_vec(),
22131                                    }
22132                                };
22133                                let mut streams_guard = streams.lock().expect("http2 server streams");
22134                                let Some(state) = streams_guard.get_mut(&stream_id) else {
22135                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
22136                                    continue;
22137                                };
22138                                let Some(send_response) = state.send_response.as_mut() else {
22139                                    let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} already responded")));
22140                                    continue;
22141                                };
22142                                match match send_response {
22143                                    ServerHttp2Responder::Regular(send_response) => {
22144                                        send_response.send_response(response, body.is_empty())
22145                                    }
22146                                    ServerHttp2Responder::Pushed(send_response) => {
22147                                        send_response.send_response(response, body.is_empty())
22148                                    }
22149                                } {
22150                                    Ok(mut send_stream) => {
22151                                        state.send_response = None;
22152                                        if body.is_empty() {
22153                                            streams_guard.remove(&stream_id);
22154                                            if let Ok(mut shared_guard) = shared.lock() {
22155                                                shared_guard.streams.remove(&stream_id);
22156                                            }
22157                                        } else {
22158                                            if let Err(error) = send_stream.send_data(Bytes::from(body), true) {
22159                                                let _ = respond_to.send(Err(error.to_string()));
22160                                                continue;
22161                                            }
22162                                            streams_guard.remove(&stream_id);
22163                                            if let Ok(mut shared_guard) = shared.lock() {
22164                                                shared_guard.streams.remove(&stream_id);
22165                                            }
22166                                        }
22167                                        push_http2_server_event(
22168                                            &shared,
22169                                            server_id,
22170                                            Http2BridgeEvent {
22171                                                kind: String::from("serverStreamClose"),
22172                                                id: stream_id,
22173                                                extra_number: Some(0),
22174                                                ..Http2BridgeEvent::default()
22175                                            },
22176                                        );
22177                                        let _ = respond_to.send(Ok(Value::Null));
22178                                    }
22179                                    Err(error) => {
22180                                        let _ = respond_to.send(Err(error.to_string()));
22181                                    }
22182                                }
22183                            }
22184                            Http2SessionCommand::Request { respond_to, .. } => {
22185                                let _ = respond_to.send(Err(String::from("HTTP/2 server sessions cannot initiate client requests")));
22186                            }
22187                        }
22188                    }
22189                    else => break,
22190                }
22191            }
22192        });
22193    });
22194}
22195
22196fn spawn_http2_server_accept_loop(
22197    shared: Arc<Mutex<crate::state::Http2SharedState>>,
22198    server_id: u64,
22199    listener: TcpListener,
22200) {
22201    thread::spawn(move || {
22202        let listener = listener;
22203        loop {
22204            let closed = shared
22205                .lock()
22206                .ok()
22207                .and_then(|state| {
22208                    state
22209                        .servers
22210                        .get(&server_id)
22211                        .map(|server| server.closed.load(Ordering::SeqCst))
22212                })
22213                .unwrap_or(true);
22214            if closed {
22215                break;
22216            }
22217            match listener.accept() {
22218                Ok((stream, _)) => {
22219                    let (command_tx, command_rx) = unbounded_channel();
22220                    let (guest_local_addr, secure, tls) = {
22221                        let state = shared.lock().expect("http2 shared state");
22222                        let server = state.servers.get(&server_id).expect("http2 server state");
22223                        (server.guest_local_addr, server.secure, server.tls.clone())
22224                    };
22225                    let (local_addr, remote_addr) = match (stream.local_addr(), stream.peer_addr())
22226                    {
22227                        (Ok(local_addr), Ok(remote_addr)) => (local_addr, remote_addr),
22228                        _ => continue,
22229                    };
22230                    let session_snapshot = Arc::new(Mutex::new(Http2SessionSnapshot {
22231                        encrypted: secure,
22232                        alpn_protocol: Some(if secure {
22233                            String::from("h2")
22234                        } else {
22235                            String::from("h2c")
22236                        }),
22237                        local_settings: BTreeMap::new(),
22238                        remote_settings: BTreeMap::new(),
22239                        state: http2_runtime_snapshot(),
22240                        socket: Http2SocketSnapshot {
22241                            local_address: Some(guest_local_addr.ip().to_string()),
22242                            local_port: Some(guest_local_addr.port()),
22243                            local_family: Some(socket_addr_family(&guest_local_addr).to_string()),
22244                            remote_address: Some(remote_addr.ip().to_string()),
22245                            remote_port: Some(remote_addr.port()),
22246                            remote_family: Some(socket_addr_family(&remote_addr).to_string()),
22247                            ..http2_socket_snapshot(local_addr, remote_addr)
22248                        },
22249                        ..Http2SessionSnapshot::default()
22250                    }));
22251                    let session_id = {
22252                        let mut state = shared.lock().expect("http2 shared state");
22253                        let session_id = next_http2_session_id(&mut state);
22254                        state
22255                            .sessions
22256                            .insert(session_id, ActiveHttp2Session { command_tx });
22257                        session_id
22258                    };
22259                    spawn_http2_server_session(
22260                        Arc::clone(&shared),
22261                        server_id,
22262                        session_id,
22263                        stream,
22264                        tls,
22265                        session_snapshot,
22266                        command_rx,
22267                    );
22268                }
22269                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
22270                    wait_fd_readable_until(
22271                        listener.as_fd(),
22272                        Instant::now() + Duration::from_millis(100),
22273                    );
22274                }
22275                Err(error) => {
22276                    push_http2_server_event(
22277                        &shared,
22278                        server_id,
22279                        Http2BridgeEvent {
22280                            kind: String::from("serverStreamError"),
22281                            id: server_id,
22282                            data: Some(http2_error_payload(error.to_string())),
22283                            ..Http2BridgeEvent::default()
22284                        },
22285                    );
22286                    // Persistent accept errors (e.g. EMFILE) leave the fd readable, so a
22287                    // readiness wait would return immediately and spin; keep the throttle.
22288                    thread::sleep(HTTP2_POLL_DELAY);
22289                }
22290            }
22291        }
22292    });
22293}
22294
22295fn send_http2_command(
22296    session: &ActiveHttp2Session,
22297    command: impl FnOnce(Sender<Result<Value, String>>) -> Http2SessionCommand,
22298) -> Result<Value, SidecarError> {
22299    let (respond_to, response_rx) = mpsc::channel();
22300    session.command_tx.send(command(respond_to)).map_err(|_| {
22301        SidecarError::InvalidState(String::from("HTTP/2 session command channel closed"))
22302    })?;
22303    response_rx
22304        .recv_timeout(Duration::from_secs(30))
22305        .map_err(|_| {
22306            SidecarError::Execution(String::from("timed out waiting for HTTP/2 session command"))
22307        })?
22308        .map_err(SidecarError::Execution)
22309}
22310
22311fn parse_http2_server_listen_payload(
22312    request: &JavascriptSyncRpcRequest,
22313) -> Result<JavascriptHttp2ServerListenRequest, SidecarError> {
22314    let payload_json =
22315        javascript_sync_rpc_arg_str(&request.args, 0, "net.http2_server_listen payload")?;
22316    serde_json::from_str(payload_json).map_err(|error| {
22317        SidecarError::InvalidState(format!(
22318            "net.http2_server_listen payload must be valid JSON: {error}"
22319        ))
22320    })
22321}
22322
22323fn parse_http2_connect_payload(
22324    request: &JavascriptSyncRpcRequest,
22325) -> Result<JavascriptHttp2SessionConnectRequest, SidecarError> {
22326    let payload_json =
22327        javascript_sync_rpc_arg_str(&request.args, 0, "net.http2_session_connect payload")?;
22328    serde_json::from_str(payload_json).map_err(|error| {
22329        SidecarError::InvalidState(format!(
22330            "net.http2_session_connect payload must be valid JSON: {error}"
22331        ))
22332    })
22333}
22334
22335fn http2_session_for_id(
22336    process: &ActiveProcess,
22337    session_id: u64,
22338) -> Result<ActiveHttp2Session, SidecarError> {
22339    let shared = process
22340        .http2
22341        .shared
22342        .lock()
22343        .map_err(|_| SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned")))?;
22344    shared
22345        .sessions
22346        .get(&session_id)
22347        .cloned()
22348        .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 session {session_id}")))
22349}
22350
22351fn http2_stream_for_id(
22352    process: &ActiveProcess,
22353    stream_id: u64,
22354) -> Result<ActiveHttp2Stream, SidecarError> {
22355    let shared = process
22356        .http2
22357        .shared
22358        .lock()
22359        .map_err(|_| SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned")))?;
22360    shared
22361        .streams
22362        .get(&stream_id)
22363        .cloned()
22364        .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 stream {stream_id}")))
22365}
22366
22367fn service_javascript_http2_sync_rpc<B>(
22368    request: JavascriptHttp2SyncRpcServiceRequest<'_, B>,
22369) -> Result<Value, SidecarError>
22370where
22371    B: NativeSidecarBridge + Send + 'static,
22372    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
22373{
22374    let JavascriptHttp2SyncRpcServiceRequest {
22375        bridge,
22376        kernel,
22377        vm_id,
22378        dns,
22379        socket_paths,
22380        process,
22381        sync_request: request,
22382        resource_limits,
22383        network_counts,
22384    } = request;
22385    match request.method.as_str() {
22386        "net.http2_server_listen" => {
22387            check_network_resource_limit(
22388                resource_limits.max_sockets,
22389                network_counts.sockets,
22390                1,
22391                "socket",
22392            )?;
22393            let payload = parse_http2_server_listen_payload(request)?;
22394            let (family, bind_host, guest_host) =
22395                normalize_tcp_listen_host(payload.host.as_deref())?;
22396            let requested_port = payload.port.unwrap_or(0);
22397            bridge.require_network_access(
22398                vm_id,
22399                NetworkOperation::Listen,
22400                format_tcp_resource(bind_host, requested_port),
22401            )?;
22402            let port = allocate_guest_listen_port(
22403                requested_port,
22404                family,
22405                &socket_paths.used_tcp_guest_ports,
22406                socket_paths.listen_policy,
22407            )?;
22408            let mut listener =
22409                ActiveTcpListener::bind(bind_host, guest_host, port, payload.backlog)?;
22410            let guest_local_addr = listener.guest_local_addr();
22411            let closed = Arc::new(AtomicBool::new(false));
22412            {
22413                let mut state = process.http2.shared.lock().map_err(|_| {
22414                    SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
22415                })?;
22416                state.servers.insert(
22417                    payload.server_id,
22418                    ActiveHttp2Server {
22419                        actual_local_addr: listener.local_addr(),
22420                        guest_local_addr,
22421                        secure: payload.secure,
22422                        tls: payload.tls.clone().map(|mut tls| {
22423                            tls.is_server = payload.secure;
22424                            if payload.secure && tls.alpn_protocols.is_none() {
22425                                tls.alpn_protocols = Some(vec![String::from("h2")]);
22426                            }
22427                            tls
22428                        }),
22429                        closed: Arc::clone(&closed),
22430                    },
22431                );
22432                if state.event_session.is_none() {
22433                    state.event_session = process.execution.javascript_v8_session_handle();
22434                }
22435                state.server_events.entry(payload.server_id).or_default();
22436            }
22437            spawn_http2_server_accept_loop(
22438                Arc::clone(&process.http2.shared),
22439                payload.server_id,
22440                listener.listener.take().ok_or_else(|| {
22441                    SidecarError::InvalidState(String::from(
22442                        "HTTP/2 listener missing host TCP socket",
22443                    ))
22444                })?,
22445            );
22446            javascript_net_json_string(
22447                json!({
22448                    "address": socket_address_value(&guest_local_addr)
22449                }),
22450                "net.http2_server_listen",
22451            )
22452        }
22453        "net.http2_server_poll" => {
22454            let server_id =
22455                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_poll server id")?;
22456            let wait_ms = javascript_sync_rpc_arg_u64_optional(
22457                &request.args,
22458                1,
22459                "net.http2_server_poll wait ms",
22460            )?
22461            .unwrap_or_default();
22462            match wait_for_http2_event(&process.http2.shared, server_id, true, wait_ms) {
22463                Some(event) => http2_event_value(&event),
22464                None => Ok(Value::Null),
22465            }
22466        }
22467        "net.http2_server_wait" => {
22468            let server_id =
22469                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_wait server id")?;
22470            dispatch_http2_wait_loop(process, server_id, true)
22471        }
22472        "net.http2_server_close" => {
22473            let server_id =
22474                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_close server id")?;
22475            let server = {
22476                let mut state = process.http2.shared.lock().map_err(|_| {
22477                    SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
22478                })?;
22479                state.servers.remove(&server_id)
22480            }
22481            .ok_or_else(|| {
22482                SidecarError::InvalidState(format!("unknown HTTP/2 server {server_id}"))
22483            })?;
22484            server.closed.store(true, Ordering::SeqCst);
22485            push_http2_server_event(
22486                &process.http2.shared,
22487                server_id,
22488                Http2BridgeEvent {
22489                    kind: String::from("serverClose"),
22490                    id: server_id,
22491                    ..Http2BridgeEvent::default()
22492                },
22493            );
22494            Ok(Value::Null)
22495        }
22496        "net.http2_server_respond" => {
22497            let server_id = javascript_sync_rpc_arg_u64(
22498                &request.args,
22499                0,
22500                "net.http2_server_respond server id",
22501            )?;
22502            let request_id = javascript_sync_rpc_arg_u64(
22503                &request.args,
22504                1,
22505                "net.http2_server_respond request id",
22506            )?;
22507            let response_json =
22508                javascript_sync_rpc_arg_str(&request.args, 2, "net.http2_server_respond payload")?;
22509            ensure_vm_fetch_response_within_limit(
22510                response_json,
22511                "net.http2_server_respond",
22512                VM_FETCH_BUFFER_LIMIT_BYTES,
22513            )
22514            .map_err(sidecar_core_execution_error)?;
22515            serde_json::from_str::<Value>(response_json).map_err(|error| {
22516                SidecarError::Execution(format!(
22517                    "net.http2_server_respond payload must be valid JSON: {error}"
22518                ))
22519            })?;
22520            let Some(pending) = process
22521                .pending_http_requests
22522                .get_mut(&(server_id, request_id))
22523            else {
22524                return Err(SidecarError::InvalidState(format!(
22525                    "unknown pending HTTP/2 request {request_id} for server {server_id}"
22526                )));
22527            };
22528            *pending = Some(response_json.to_owned());
22529            Ok(Value::Bool(true))
22530        }
22531        "net.http2_session_connect" => {
22532            check_network_resource_limit(
22533                resource_limits.max_sockets,
22534                network_counts.sockets,
22535                1,
22536                "socket",
22537            )?;
22538            check_network_resource_limit(
22539                resource_limits.max_connections,
22540                network_counts.connections,
22541                1,
22542                "connection",
22543            )?;
22544            let payload = parse_http2_connect_payload(request)?;
22545            let authority = payload.authority.clone().unwrap_or_else(|| {
22546                format!(
22547                    "{}://{}:{}",
22548                    payload.protocol.as_deref().unwrap_or("http"),
22549                    payload.host.as_deref().unwrap_or("localhost"),
22550                    payload.port.unwrap_or(80)
22551                )
22552            });
22553            let url = Url::parse(&authority).map_err(|error| {
22554                SidecarError::InvalidState(format!(
22555                    "invalid HTTP/2 authority {authority:?}: {error}"
22556                ))
22557            })?;
22558            let secure = url.scheme() == "https" || payload.protocol.as_deref() == Some("https:");
22559            let host = payload
22560                .host
22561                .as_deref()
22562                .or_else(|| url.host_str())
22563                .unwrap_or("localhost");
22564            let port = payload.port.or_else(|| url.port()).unwrap_or(80);
22565            bridge.require_network_access(
22566                vm_id,
22567                NetworkOperation::Http,
22568                format_tcp_resource(host, port),
22569            )?;
22570            let resolved = {
22571                let shared = process.http2.shared.lock().map_err(|_| {
22572                    SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
22573                })?;
22574                shared
22575                    .servers
22576                    .values()
22577                    .find(|server| {
22578                        is_loopback_request_host(host) && server.guest_local_addr.port() == port
22579                    })
22580                    .map(|server| ResolvedTcpConnectAddr {
22581                        actual_addr: server.actual_local_addr,
22582                        guest_remote_addr: server.guest_local_addr,
22583                        use_kernel_loopback: false,
22584                    })
22585            };
22586            let resolved = match resolved {
22587                Some(resolved) => resolved,
22588                None => {
22589                    resolve_tcp_connect_addr(bridge, kernel, vm_id, dns, host, port, socket_paths)?
22590                }
22591            };
22592            let (command_tx, command_rx) = unbounded_channel();
22593            let snapshot = Arc::new(Mutex::new(Http2SessionSnapshot {
22594                encrypted: secure,
22595                alpn_protocol: Some(String::from(if secure { "h2" } else { "h2c" })),
22596                local_settings: http2_settings_from_value(&payload.settings),
22597                remote_settings: BTreeMap::new(),
22598                state: http2_runtime_snapshot(),
22599                socket: Http2SocketSnapshot {
22600                    encrypted: secure,
22601                    remote_address: Some(resolved.guest_remote_addr.ip().to_string()),
22602                    remote_port: Some(resolved.guest_remote_addr.port()),
22603                    remote_family: Some(
22604                        socket_addr_family(&resolved.guest_remote_addr).to_string(),
22605                    ),
22606                    servername: if secure {
22607                        payload
22608                            .tls
22609                            .as_ref()
22610                            .and_then(|tls| tls.servername.clone())
22611                            .or_else(|| Some(host.to_string()))
22612                    } else {
22613                        None
22614                    },
22615                    alpn_protocol: Some(String::from(if secure { "h2" } else { "h2c" })),
22616                    ..Http2SocketSnapshot::default()
22617                },
22618                ..Http2SessionSnapshot::default()
22619            }));
22620            let session_id = {
22621                let mut state = process.http2.shared.lock().map_err(|_| {
22622                    SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
22623                })?;
22624                let session_id = next_http2_session_id(&mut state);
22625                state
22626                    .sessions
22627                    .insert(session_id, ActiveHttp2Session { command_tx });
22628                if state.event_session.is_none() {
22629                    state.event_session = process.execution.javascript_v8_session_handle();
22630                }
22631                state.session_events.entry(session_id).or_default();
22632                session_id
22633            };
22634            spawn_http2_client_session(
22635                Arc::clone(&process.http2.shared),
22636                session_id,
22637                resolved.actual_addr,
22638                if secure {
22639                    Some(payload.tls.unwrap_or(JavascriptTlsBridgeOptions {
22640                        is_server: false,
22641                        servername: Some(host.to_string()),
22642                        alpn_protocols: Some(vec![String::from("h2")]),
22643                        ..JavascriptTlsBridgeOptions::default()
22644                    }))
22645                } else {
22646                    None
22647                },
22648                Arc::clone(&snapshot),
22649                command_rx,
22650            );
22651            let snapshot_json =
22652                http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())?;
22653            javascript_net_json_string(
22654                json!({
22655                    "sessionId": session_id,
22656                    "state": snapshot_json,
22657                }),
22658                "net.http2_session_connect",
22659            )
22660        }
22661        "net.http2_session_request" => {
22662            let session_id = javascript_sync_rpc_arg_u64(
22663                &request.args,
22664                0,
22665                "net.http2_session_request session id",
22666            )?;
22667            let headers_json =
22668                javascript_sync_rpc_arg_str(&request.args, 1, "net.http2_session_request headers")?;
22669            let options_json =
22670                javascript_sync_rpc_arg_str(&request.args, 2, "net.http2_session_request options")?;
22671            let session = http2_session_for_id(process, session_id)?;
22672            send_http2_command(&session, |respond_to| Http2SessionCommand::Request {
22673                headers_json: headers_json.to_owned(),
22674                options_json: options_json.to_owned(),
22675                respond_to,
22676            })
22677        }
22678        "net.http2_session_settings" => {
22679            let session_id = javascript_sync_rpc_arg_u64(
22680                &request.args,
22681                0,
22682                "net.http2_session_settings session id",
22683            )?;
22684            let settings_json = javascript_sync_rpc_arg_str(
22685                &request.args,
22686                1,
22687                "net.http2_session_settings settings",
22688            )?;
22689            let session = http2_session_for_id(process, session_id)?;
22690            send_http2_command(&session, |respond_to| Http2SessionCommand::Settings {
22691                settings_json: settings_json.to_owned(),
22692                respond_to,
22693            })
22694        }
22695        "net.http2_session_set_local_window_size" => {
22696            let session_id = javascript_sync_rpc_arg_u64(
22697                &request.args,
22698                0,
22699                "net.http2_session_set_local_window_size session id",
22700            )?;
22701            let window_size = javascript_sync_rpc_arg_u64(
22702                &request.args,
22703                1,
22704                "net.http2_session_set_local_window_size window size",
22705            )?;
22706            let session = http2_session_for_id(process, session_id)?;
22707            send_http2_command(&session, |respond_to| {
22708                Http2SessionCommand::SetLocalWindowSize {
22709                    size: window_size as u32,
22710                    respond_to,
22711                }
22712            })
22713        }
22714        "net.http2_session_goaway" => {
22715            let session_id = javascript_sync_rpc_arg_u64(
22716                &request.args,
22717                0,
22718                "net.http2_session_goaway session id",
22719            )?;
22720            let error_code = javascript_sync_rpc_arg_u64(
22721                &request.args,
22722                1,
22723                "net.http2_session_goaway error code",
22724            )?;
22725            let last_stream_id = javascript_sync_rpc_arg_u64(
22726                &request.args,
22727                2,
22728                "net.http2_session_goaway last stream id",
22729            )?;
22730            let opaque_data = request
22731                .args
22732                .get(3)
22733                .and_then(Value::as_str)
22734                .map(|value| {
22735                    base64::engine::general_purpose::STANDARD
22736                        .decode(value)
22737                        .map_err(|error| {
22738                            SidecarError::InvalidState(format!("invalid GOAWAY payload: {error}"))
22739                        })
22740                })
22741                .transpose()?;
22742            let session = http2_session_for_id(process, session_id)?;
22743            send_http2_command(&session, |respond_to| Http2SessionCommand::Goaway {
22744                error_code: error_code as u32,
22745                last_stream_id: last_stream_id as u32,
22746                opaque_data,
22747                respond_to,
22748            })
22749        }
22750        "net.http2_session_close" | "net.http2_session_destroy" => {
22751            let session_id = javascript_sync_rpc_arg_u64(
22752                &request.args,
22753                0,
22754                "net.http2_session_close session id",
22755            )?;
22756            let session = http2_session_for_id(process, session_id)?;
22757            send_http2_command(&session, |respond_to| Http2SessionCommand::Close {
22758                abrupt: request.method == "net.http2_session_destroy",
22759                respond_to,
22760            })
22761        }
22762        "net.http2_session_poll" => {
22763            let session_id =
22764                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_session_poll session id")?;
22765            let wait_ms = javascript_sync_rpc_arg_u64_optional(
22766                &request.args,
22767                1,
22768                "net.http2_session_poll wait ms",
22769            )?
22770            .unwrap_or_default();
22771            match wait_for_http2_event(&process.http2.shared, session_id, false, wait_ms) {
22772                Some(event) => http2_event_value(&event),
22773                None => Ok(Value::Null),
22774            }
22775        }
22776        "net.http2_session_wait" => {
22777            let session_id =
22778                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_session_wait session id")?;
22779            dispatch_http2_wait_loop(process, session_id, false)
22780        }
22781        "net.http2_stream_respond" => {
22782            let stream_id = javascript_sync_rpc_arg_u64(
22783                &request.args,
22784                0,
22785                "net.http2_stream_respond stream id",
22786            )?;
22787            let headers_json =
22788                javascript_sync_rpc_arg_str(&request.args, 1, "net.http2_stream_respond headers")?;
22789            let stream = http2_stream_for_id(process, stream_id)?;
22790            let session = http2_session_for_id(process, stream.session_id)?;
22791            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamRespond {
22792                stream_id,
22793                headers_json: headers_json.to_owned(),
22794                respond_to,
22795            })
22796        }
22797        "net.http2_stream_push_stream" => {
22798            let stream_id = javascript_sync_rpc_arg_u64(
22799                &request.args,
22800                0,
22801                "net.http2_stream_push_stream stream id",
22802            )?;
22803            let headers_json = javascript_sync_rpc_arg_str(
22804                &request.args,
22805                1,
22806                "net.http2_stream_push_stream headers",
22807            )?;
22808            let _options_json = javascript_sync_rpc_arg_str(
22809                &request.args,
22810                2,
22811                "net.http2_stream_push_stream options",
22812            )?;
22813            let stream = http2_stream_for_id(process, stream_id)?;
22814            let session = http2_session_for_id(process, stream.session_id)?;
22815            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamPush {
22816                stream_id,
22817                headers_json: headers_json.to_owned(),
22818                respond_to,
22819            })
22820        }
22821        "net.http2_stream_write" => {
22822            let stream_id =
22823                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_write stream id")?;
22824            let chunk =
22825                javascript_sync_rpc_base64_arg(&request.args, 1, "net.http2_stream_write data")?;
22826            let stream = http2_stream_for_id(process, stream_id)?;
22827            let session = http2_session_for_id(process, stream.session_id)?;
22828            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamWrite {
22829                stream_id,
22830                chunk,
22831                end_stream: false,
22832                respond_to,
22833            })
22834        }
22835        "net.http2_stream_end" => {
22836            let stream_id =
22837                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_end stream id")?;
22838            let chunk = request
22839                .args
22840                .get(1)
22841                .and_then(Value::as_str)
22842                .map(|value| {
22843                    base64::engine::general_purpose::STANDARD
22844                        .decode(value)
22845                        .map_err(|error| {
22846                            SidecarError::InvalidState(format!(
22847                                "invalid HTTP/2 stream payload: {error}"
22848                            ))
22849                        })
22850                })
22851                .transpose()?
22852                .unwrap_or_default();
22853            let stream = http2_stream_for_id(process, stream_id)?;
22854            let session = http2_session_for_id(process, stream.session_id)?;
22855            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamWrite {
22856                stream_id,
22857                chunk,
22858                end_stream: true,
22859                respond_to,
22860            })
22861        }
22862        "net.http2_stream_close" => {
22863            let stream_id =
22864                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_close stream id")?;
22865            let code = javascript_sync_rpc_arg_u64_optional(
22866                &request.args,
22867                1,
22868                "net.http2_stream_close error code",
22869            )?
22870            .map(|value| value as u32);
22871            let stream = http2_stream_for_id(process, stream_id)?;
22872            let session = http2_session_for_id(process, stream.session_id)?;
22873            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamClose {
22874                stream_id,
22875                error_code: code,
22876                respond_to,
22877            })
22878        }
22879        "net.http2_stream_pause" => {
22880            let stream_id =
22881                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_pause stream id")?;
22882            let stream = http2_stream_for_id(process, stream_id)?;
22883            stream.paused.store(true, Ordering::SeqCst);
22884            Ok(Value::Null)
22885        }
22886        "net.http2_stream_resume" => {
22887            let stream_id =
22888                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_resume stream id")?;
22889            let stream = http2_stream_for_id(process, stream_id)?;
22890            stream.paused.store(false, Ordering::SeqCst);
22891            stream.resume_notify.notify_waiters();
22892            Ok(Value::Null)
22893        }
22894        "net.http2_stream_respond_with_file" => {
22895            let stream_id = javascript_sync_rpc_arg_u64(
22896                &request.args,
22897                0,
22898                "net.http2_stream_respond_with_file stream id",
22899            )?;
22900            let path = javascript_sync_rpc_arg_str(
22901                &request.args,
22902                1,
22903                "net.http2_stream_respond_with_file path",
22904            )?;
22905            let headers_json = javascript_sync_rpc_arg_str(
22906                &request.args,
22907                2,
22908                "net.http2_stream_respond_with_file headers",
22909            )?;
22910            let options_json = javascript_sync_rpc_arg_str(
22911                &request.args,
22912                3,
22913                "net.http2_stream_respond_with_file options",
22914            )?;
22915            let stream = http2_stream_for_id(process, stream_id)?;
22916            let session = http2_session_for_id(process, stream.session_id)?;
22917            let guest_path = resolve_http2_file_response_guest_path(process, path);
22918            let body = kernel.read_file(&guest_path).map_err(kernel_error)?;
22919            send_http2_command(&session, |respond_to| {
22920                Http2SessionCommand::StreamRespondWithFile {
22921                    stream_id,
22922                    body,
22923                    headers_json: headers_json.to_owned(),
22924                    options_json: options_json.to_owned(),
22925                    respond_to,
22926                }
22927            })
22928        }
22929        other => Err(SidecarError::InvalidState(format!(
22930            "unsupported JavaScript HTTP/2 sync RPC method {other}"
22931        ))),
22932    }
22933}
22934
22935const JAVASCRIPT_NET_POLL_MAX_WAIT: Duration = Duration::from_millis(50);
22936const EXITED_PROCESS_SNAPSHOT_RETENTION: Duration = Duration::from_secs(2);
22937
22938fn resolve_http2_file_response_guest_path(process: &ActiveProcess, path: &str) -> String {
22939    if Path::new(path).is_absolute() {
22940        normalize_path(path)
22941    } else {
22942        normalize_path(&format!("{}/{}", process.guest_cwd, path))
22943    }
22944}
22945
22946pub(crate) fn clamp_javascript_net_poll_wait(wait_ms: u64) -> Duration {
22947    // WASM net.poll runs on the sidecar's sync-RPC main thread. Guest-controlled waits
22948    // must stay bounded so one VM cannot stall dispose/shutdown or unrelated VM work.
22949    if wait_ms == 0 {
22950        Duration::ZERO
22951    } else {
22952        Duration::from_millis(wait_ms).min(JAVASCRIPT_NET_POLL_MAX_WAIT)
22953    }
22954}
22955
22956fn service_javascript_net_sync_rpc_response<B>(
22957    request: JavascriptNetSyncRpcServiceRequest<'_, B>,
22958) -> Result<JavascriptSyncRpcServiceResponse, SidecarError>
22959where
22960    B: NativeSidecarBridge + Send + 'static,
22961    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
22962{
22963    if request.sync_request.method != "net.socket_read" {
22964        return service_javascript_net_sync_rpc(request).map(Into::into);
22965    }
22966
22967    let JavascriptNetSyncRpcServiceRequest {
22968        kernel,
22969        process,
22970        sync_request: request,
22971        ..
22972    } = request;
22973    let trace_enabled = net_tcp_trace_enabled(&process.env);
22974    let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_read socket id")?;
22975    if trace_enabled {
22976        NET_TCP_TRACE_COUNTERS
22977            .socket_read_calls
22978            .fetch_add(1, Ordering::Relaxed);
22979        NET_TCP_TRACE_COUNTERS
22980            .socket_read_zero_wait_calls
22981            .fetch_add(1, Ordering::Relaxed);
22982    }
22983
22984    let event = if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
22985        socket.poll(kernel, process.kernel_pid, Duration::ZERO, trace_enabled)?
22986    } else {
22987        let socket = process
22988            .unix_sockets
22989            .get_mut(socket_id)
22990            .ok_or_else(|| SidecarError::InvalidState(format!("unknown net socket {socket_id}")))?;
22991        socket.poll(Duration::ZERO)?
22992    };
22993
22994    match event {
22995        Some(JavascriptTcpSocketEvent::Data(chunk)) => {
22996            Ok(JavascriptSyncRpcServiceResponse::Raw(chunk))
22997        }
22998        other => javascript_net_read_value(other).map(Into::into),
22999    }
23000}
23001
23002pub(crate) fn service_javascript_net_sync_rpc<B>(
23003    request: JavascriptNetSyncRpcServiceRequest<'_, B>,
23004) -> Result<Value, SidecarError>
23005where
23006    B: NativeSidecarBridge + Send + 'static,
23007    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
23008{
23009    let JavascriptNetSyncRpcServiceRequest {
23010        bridge,
23011        vm_id,
23012        dns,
23013        socket_paths,
23014        kernel,
23015        kernel_readiness,
23016        process,
23017        sync_request: request,
23018        resource_limits,
23019        network_counts,
23020    } = request;
23021    let trace_enabled = net_tcp_trace_enabled(&process.env);
23022    match request.method.as_str() {
23023        "net.http_listen" => {
23024            check_network_resource_limit(
23025                resource_limits.max_sockets,
23026                network_counts.sockets,
23027                1,
23028                "socket",
23029            )?;
23030            let payload_json =
23031                javascript_sync_rpc_arg_str(&request.args, 0, "net.http_listen payload")?;
23032            let payload: JavascriptHttpListenRequest =
23033                serde_json::from_str(payload_json).map_err(|error| {
23034                    SidecarError::InvalidState(format!(
23035                        "net.http_listen payload must be valid JSON: {error}"
23036                    ))
23037                })?;
23038            let (family, bind_host, guest_host) =
23039                normalize_tcp_listen_host(payload.hostname.as_deref())?;
23040            let requested_port = payload.port.unwrap_or(0);
23041            bridge.require_network_access(
23042                vm_id,
23043                NetworkOperation::Listen,
23044                format_tcp_resource(bind_host, requested_port),
23045            )?;
23046            let port = allocate_guest_listen_port(
23047                requested_port,
23048                family,
23049                &socket_paths.used_tcp_guest_ports,
23050                socket_paths.listen_policy,
23051            )?;
23052            let mut listener = ActiveTcpListener::bind(
23053                bind_host,
23054                guest_host,
23055                port,
23056                Some(DEFAULT_JAVASCRIPT_NET_BACKLOG),
23057            )?;
23058            let guest_local_addr = listener.guest_local_addr();
23059            process.http_servers.insert(
23060                payload.server_id,
23061                ActiveHttpServer {
23062                    listener: listener.listener.take().ok_or_else(|| {
23063                        SidecarError::InvalidState(String::from(
23064                            "HTTP listener missing host TCP socket",
23065                        ))
23066                    })?,
23067                    guest_local_addr,
23068                    next_request_id: 0,
23069                },
23070            );
23071            serde_json::to_string(&json!({
23072                "address": socket_address_value(&guest_local_addr)
23073            }))
23074            .map(Value::String)
23075            .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
23076        }
23077        "net.http_close" => {
23078            let server_id =
23079                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_close server id")?;
23080            let server = process.http_servers.remove(&server_id).ok_or_else(|| {
23081                SidecarError::InvalidState(format!("unknown HTTP server {server_id}"))
23082            })?;
23083            drop(server.listener);
23084            process
23085                .pending_http_requests
23086                .retain(|(pending_server_id, _), _| *pending_server_id != server_id);
23087            Ok(Value::Null)
23088        }
23089        "net.http_wait" => {
23090            let server_id =
23091                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_wait server id")?;
23092            dispatch_http_wait_loop(process, server_id)
23093        }
23094        "net.http_respond" => {
23095            let server_id =
23096                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_respond server id")?;
23097            let request_id =
23098                javascript_sync_rpc_arg_u64(&request.args, 1, "net.http_respond request id")?;
23099            let response_json =
23100                javascript_sync_rpc_arg_str(&request.args, 2, "net.http_respond payload")?;
23101            ensure_vm_fetch_response_within_limit(
23102                response_json,
23103                "net.http_respond",
23104                VM_FETCH_BUFFER_LIMIT_BYTES,
23105            )
23106            .map_err(sidecar_core_execution_error)?;
23107            serde_json::from_str::<Value>(response_json).map_err(|error| {
23108                SidecarError::Execution(format!(
23109                    "net.http_respond payload must be valid JSON: {error}"
23110                ))
23111            })?;
23112            let Some(pending) = process
23113                .pending_http_requests
23114                .get_mut(&(server_id, request_id))
23115            else {
23116                return Err(SidecarError::InvalidState(format!(
23117                    "unknown pending HTTP request {request_id} for server {server_id}"
23118                )));
23119            };
23120            *pending = Some(response_json.to_owned());
23121            Ok(Value::Null)
23122        }
23123        "net.reserve_tcp_port" => {
23124            let payload = request
23125                .args
23126                .first()
23127                .cloned()
23128                .ok_or_else(|| {
23129                    SidecarError::InvalidState(String::from(
23130                        "net.reserve_tcp_port requires a request payload",
23131                    ))
23132                })
23133                .and_then(|value| {
23134                    serde_json::from_value::<JavascriptNetReserveTcpPortRequest>(value).map_err(
23135                        |error| {
23136                            SidecarError::InvalidState(format!(
23137                                "invalid net.reserve_tcp_port payload: {error}"
23138                            ))
23139                        },
23140                    )
23141                })?;
23142            let (family, _bind_host, guest_host) =
23143                normalize_tcp_listen_host(payload.host.as_deref())?;
23144            let requested_port = payload.port.unwrap_or(0);
23145            let port = allocate_guest_listen_port(
23146                requested_port,
23147                family,
23148                &socket_paths.used_tcp_guest_ports,
23149                socket_paths.listen_policy,
23150            )?;
23151            let reservation_id = process.allocate_tcp_port_reservation_id();
23152            process
23153                .tcp_port_reservations
23154                .insert(reservation_id.clone(), (family, port));
23155            Ok(json!({
23156                "reservationId": reservation_id,
23157                "localAddress": guest_host,
23158                "localPort": port,
23159                "family": match family {
23160                    JavascriptSocketFamily::Ipv4 => "IPv4",
23161                    JavascriptSocketFamily::Ipv6 => "IPv6",
23162                },
23163            }))
23164        }
23165        "net.release_tcp_port" => {
23166            let reservation_id =
23167                javascript_sync_rpc_arg_str(&request.args, 0, "net.release_tcp_port reservation")?;
23168            process.tcp_port_reservations.remove(reservation_id);
23169            Ok(Value::Null)
23170        }
23171        "net.connect" => {
23172            check_network_resource_limit(
23173                resource_limits.max_sockets,
23174                network_counts.sockets,
23175                1,
23176                "socket",
23177            )?;
23178            check_network_resource_limit(
23179                resource_limits.max_connections,
23180                network_counts.connections,
23181                1,
23182                "connection",
23183            )?;
23184            let payload = request
23185                .args
23186                .first()
23187                .cloned()
23188                .ok_or_else(|| {
23189                    SidecarError::InvalidState(String::from(
23190                        "net.connect requires a request payload",
23191                    ))
23192                })
23193                .and_then(|value| {
23194                    serde_json::from_value::<JavascriptNetConnectRequest>(value).map_err(|error| {
23195                        SidecarError::InvalidState(format!("invalid net.connect payload: {error}"))
23196                    })
23197                })?;
23198            if let Some(path) = payload.path.as_deref() {
23199                let guest_path = normalize_path(path);
23200                let host_path = resolve_guest_socket_host_path(socket_paths, &guest_path);
23201                let socket = ActiveUnixSocket::connect(&host_path, &guest_path)?;
23202                let socket_id = process.allocate_unix_socket_id();
23203                socket.set_event_pusher(
23204                    process.execution.javascript_v8_session_handle(),
23205                    socket_id.clone(),
23206                );
23207                process.unix_sockets.insert(socket_id.clone(), socket);
23208                Ok(json!({
23209                    "socketId": socket_id,
23210                    "remotePath": guest_path,
23211                }))
23212            } else {
23213                let port = payload.port.ok_or_else(|| {
23214                    SidecarError::InvalidState(String::from(
23215                        "net.connect requires either a path or port",
23216                    ))
23217                })?;
23218                let host = payload.host.as_deref().unwrap_or("localhost");
23219                let local_reservation = payload.local_reservation.as_deref().and_then(|id| {
23220                    process
23221                        .tcp_port_reservations
23222                        .remove(id)
23223                        .map(|reservation| (id.to_owned(), reservation))
23224                });
23225                bridge.require_network_access(
23226                    vm_id,
23227                    NetworkOperation::Http,
23228                    format_tcp_resource(host, port),
23229                )?;
23230                if is_loopback_socket_host(host) {
23231                    let families = [JavascriptSocketFamily::Ipv4, JavascriptSocketFamily::Ipv6];
23232                    if let Some((family, target)) = families.iter().find_map(|family| {
23233                        socket_paths
23234                            .http_loopback_target(*family, port)
23235                            .map(|target| (*family, target))
23236                    }) {
23237                        if let Some((reservation_id, reservation)) = local_reservation {
23238                            process
23239                                .tcp_port_reservations
23240                                .insert(reservation_id, reservation);
23241                        }
23242                        let remote_address = match family {
23243                            JavascriptSocketFamily::Ipv4 => "127.0.0.1",
23244                            JavascriptSocketFamily::Ipv6 => "::1",
23245                        };
23246                        return Ok(json!({
23247                            "loopbackHttpTarget": {
23248                                "processId": target.process_id.clone(),
23249                                "serverId": target.server_id,
23250                                "host": remote_address,
23251                                "port": port,
23252                            },
23253                            "localAddress": match family {
23254                                JavascriptSocketFamily::Ipv4 => "127.0.0.1",
23255                                JavascriptSocketFamily::Ipv6 => "::1",
23256                            },
23257                            "localPort": payload.local_port.unwrap_or(0),
23258                            "remoteAddress": remote_address,
23259                            "remotePort": port,
23260                            "remoteFamily": match family {
23261                                JavascriptSocketFamily::Ipv4 => "IPv4",
23262                                JavascriptSocketFamily::Ipv6 => "IPv6",
23263                            },
23264                        }));
23265                    }
23266                }
23267                let connect_result = ActiveTcpSocket::connect(ActiveTcpConnectRequest {
23268                    bridge,
23269                    kernel,
23270                    kernel_pid: process.kernel_pid,
23271                    vm_id,
23272                    dns,
23273                    host,
23274                    port,
23275                    local_address: payload.local_address.as_deref(),
23276                    local_port: payload.local_port,
23277                    local_reservation: local_reservation
23278                        .as_ref()
23279                        .map(|(_, reservation)| *reservation),
23280                    context: socket_paths,
23281                });
23282                if let Err(error) = connect_result {
23283                    if let Some((reservation_id, reservation)) = local_reservation {
23284                        process
23285                            .tcp_port_reservations
23286                            .insert(reservation_id, reservation);
23287                    }
23288                    return Err(error);
23289                }
23290                let socket = connect_result?;
23291                let socket_id = process.allocate_tcp_socket_id();
23292                let local_addr = socket.guest_local_addr;
23293                let remote_addr = socket.guest_remote_addr;
23294                socket.set_event_pusher(
23295                    process.execution.javascript_v8_session_handle(),
23296                    socket_id.clone(),
23297                );
23298                register_kernel_readiness_target(
23299                    &kernel_readiness,
23300                    socket.kernel_socket_id,
23301                    process.execution.javascript_v8_session_handle(),
23302                    socket_id.clone(),
23303                    KernelSocketReadinessEvent::Data,
23304                );
23305                process.tcp_sockets.insert(socket_id.clone(), socket);
23306                Ok(json!({
23307                    "socketId": socket_id,
23308                    "localAddress": local_addr.ip().to_string(),
23309                    "localPort": local_addr.port(),
23310                    "remoteAddress": remote_addr.ip().to_string(),
23311                    "remotePort": remote_addr.port(),
23312                    "remoteFamily": socket_addr_family(&remote_addr),
23313                }))
23314            }
23315        }
23316        "net.listen" => {
23317            check_network_resource_limit(
23318                resource_limits.max_sockets,
23319                network_counts.sockets,
23320                1,
23321                "socket",
23322            )?;
23323            let payload = request
23324                .args
23325                .first()
23326                .cloned()
23327                .ok_or_else(|| {
23328                    SidecarError::InvalidState(String::from(
23329                        "net.listen requires a request payload",
23330                    ))
23331                })
23332                .and_then(|value| match value {
23333                    Value::String(json) => {
23334                        serde_json::from_str::<JavascriptNetListenRequest>(&json).map_err(|error| {
23335                            SidecarError::InvalidState(format!(
23336                                "invalid net.listen payload: {error}"
23337                            ))
23338                        })
23339                    }
23340                    other => serde_json::from_value::<JavascriptNetListenRequest>(other).map_err(
23341                        |error| {
23342                            SidecarError::InvalidState(format!(
23343                                "invalid net.listen payload: {error}"
23344                            ))
23345                        },
23346                    ),
23347                })?;
23348            if let Some(path) = payload.path.as_deref() {
23349                let guest_path = normalize_path(path);
23350                if kernel.exists(&guest_path).map_err(kernel_error)? {
23351                    return Err(sidecar_net_error(std::io::Error::from_raw_os_error(
23352                        libc::EADDRINUSE,
23353                    )));
23354                }
23355
23356                let host_path = resolve_guest_socket_host_path(socket_paths, &guest_path);
23357                let on_host_mount =
23358                    host_mount_path_for_guest_path_from_mounts(&socket_paths.mounts, &guest_path)
23359                        .is_some();
23360                let listener = ActiveUnixListener::bind(&host_path, &guest_path, payload.backlog)?;
23361                if !on_host_mount {
23362                    ensure_kernel_parent_directories(kernel, &guest_path)?;
23363                    kernel
23364                        .write_file(&guest_path, Vec::new())
23365                        .map_err(kernel_error)?;
23366                }
23367                let listener_id = process.allocate_unix_listener_id();
23368                process.unix_listeners.insert(listener_id.clone(), listener);
23369                Ok(json!({
23370                    "serverId": listener_id,
23371                    "path": guest_path,
23372                }))
23373            } else {
23374                let (family, bind_host, guest_host) =
23375                    normalize_tcp_listen_host(payload.host.as_deref())?;
23376                let requested_port = payload.port.unwrap_or(0);
23377                bridge.require_network_access(
23378                    vm_id,
23379                    NetworkOperation::Listen,
23380                    format_tcp_resource(bind_host, requested_port),
23381                )?;
23382                let local_reservation = payload.local_reservation.as_deref().and_then(|id| {
23383                    process
23384                        .tcp_port_reservations
23385                        .remove(id)
23386                        .map(|reservation| (id.to_owned(), reservation))
23387                });
23388                let port = if requested_port != 0
23389                    && local_reservation
23390                        .as_ref()
23391                        .map(|(_, reservation)| *reservation)
23392                        == Some((family, requested_port))
23393                {
23394                    requested_port
23395                } else {
23396                    allocate_guest_listen_port(
23397                        requested_port,
23398                        family,
23399                        &socket_paths.used_tcp_guest_ports,
23400                        socket_paths.listen_policy,
23401                    )?
23402                };
23403                let listener_result = ActiveTcpListener::bind_kernel(
23404                    kernel,
23405                    process.kernel_pid,
23406                    guest_host,
23407                    port,
23408                    payload.backlog,
23409                );
23410                if let Err(error) = listener_result {
23411                    if let Some((reservation_id, reservation)) = local_reservation {
23412                        process
23413                            .tcp_port_reservations
23414                            .insert(reservation_id, reservation);
23415                    }
23416                    return Err(error);
23417                }
23418                let listener = listener_result?;
23419                let listener_id = process.allocate_tcp_listener_id();
23420                let local_addr = listener.guest_local_addr();
23421                register_kernel_readiness_target(
23422                    &kernel_readiness,
23423                    listener.kernel_socket_id,
23424                    process.execution.javascript_v8_session_handle(),
23425                    listener_id.clone(),
23426                    KernelSocketReadinessEvent::Accept,
23427                );
23428                process.tcp_listeners.insert(listener_id.clone(), listener);
23429                Ok(json!({
23430                    "serverId": listener_id,
23431                    "localAddress": local_addr.ip().to_string(),
23432                    "localPort": local_addr.port(),
23433                    "family": socket_addr_family(&local_addr),
23434                }))
23435            }
23436        }
23437        "net.poll" => {
23438            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.poll socket id")?;
23439            let wait_ms =
23440                javascript_sync_rpc_arg_u64_optional(&request.args, 1, "net.poll wait ms")?
23441                    .unwrap_or_default();
23442            let wait = clamp_javascript_net_poll_wait(wait_ms);
23443            let event = if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
23444                socket.poll(kernel, process.kernel_pid, wait, trace_enabled)?
23445            } else if let Some(socket) = process.unix_sockets.get_mut(socket_id) {
23446                socket.poll(wait)?
23447            } else {
23448                return Err(SidecarError::InvalidState(format!(
23449                    "unknown net socket {socket_id}"
23450                )));
23451            };
23452
23453            match event {
23454                Some(JavascriptTcpSocketEvent::Data(chunk)) => Ok(json!({
23455                    "type": "data",
23456                    "data": javascript_sync_rpc_bytes_value(&chunk),
23457                })),
23458                Some(JavascriptTcpSocketEvent::End) => Ok(json!({
23459                    "type": "end",
23460                })),
23461                Some(JavascriptTcpSocketEvent::Error { code, message }) => Ok(json!({
23462                    "type": "error",
23463                    "code": code,
23464                    "message": message,
23465                })),
23466                Some(JavascriptTcpSocketEvent::Close { had_error }) => {
23467                    if let Some(socket) = process.tcp_sockets.remove(socket_id) {
23468                        release_tcp_socket_handle(
23469                            process,
23470                            socket_id,
23471                            socket,
23472                            kernel,
23473                            &kernel_readiness,
23474                        );
23475                    } else if let Some(socket) = process.unix_sockets.remove(socket_id) {
23476                        release_unix_socket_handle(process, socket_id, socket);
23477                    }
23478                    Ok(json!({
23479                        "type": "close",
23480                        "hadError": had_error,
23481                    }))
23482                }
23483                None => Ok(Value::Null),
23484            }
23485        }
23486        "net.socket_wait_connect" => {
23487            let socket_id =
23488                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_wait_connect socket id")?;
23489            if let Some(socket) = process.tcp_sockets.get(socket_id) {
23490                javascript_net_json_string(socket.socket_info(), "net.socket_wait_connect")
23491            } else {
23492                let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
23493                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
23494                })?;
23495                javascript_net_json_string(socket.socket_info(), "net.socket_wait_connect")
23496            }
23497        }
23498        "net.socket_read" => {
23499            let socket_id =
23500                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_read socket id")?;
23501            if trace_enabled {
23502                NET_TCP_TRACE_COUNTERS
23503                    .socket_read_calls
23504                    .fetch_add(1, Ordering::Relaxed);
23505                NET_TCP_TRACE_COUNTERS
23506                    .socket_read_zero_wait_calls
23507                    .fetch_add(1, Ordering::Relaxed);
23508            }
23509            if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
23510                javascript_net_read_value(socket.poll(
23511                    kernel,
23512                    process.kernel_pid,
23513                    Duration::ZERO,
23514                    trace_enabled,
23515                )?)
23516            } else {
23517                let socket = process.unix_sockets.get_mut(socket_id).ok_or_else(|| {
23518                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
23519                })?;
23520                javascript_net_read_value(socket.poll(Duration::ZERO)?)
23521            }
23522        }
23523        "net.socket_set_no_delay" => {
23524            let socket_id =
23525                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_set_no_delay socket id")?;
23526            let enable =
23527                javascript_sync_rpc_arg_bool(&request.args, 1, "net.socket_set_no_delay enabled")?;
23528            if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
23529                socket.set_no_delay(enable)?;
23530            } else if !process.unix_sockets.contains_key(socket_id) {
23531                return Err(SidecarError::InvalidState(format!(
23532                    "unknown net socket {socket_id}"
23533                )));
23534            }
23535            Ok(Value::Null)
23536        }
23537        "net.socket_set_keep_alive" => {
23538            let socket_id = javascript_sync_rpc_arg_str(
23539                &request.args,
23540                0,
23541                "net.socket_set_keep_alive socket id",
23542            )?;
23543            let enable = javascript_sync_rpc_arg_bool(
23544                &request.args,
23545                1,
23546                "net.socket_set_keep_alive enabled",
23547            )?;
23548            let initial_delay_secs = javascript_sync_rpc_arg_u64_optional(
23549                &request.args,
23550                2,
23551                "net.socket_set_keep_alive initial delay seconds",
23552            )?;
23553            if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
23554                socket.set_keep_alive(enable, initial_delay_secs)?;
23555            } else if !process.unix_sockets.contains_key(socket_id) {
23556                return Err(SidecarError::InvalidState(format!(
23557                    "unknown net socket {socket_id}"
23558                )));
23559            }
23560            Ok(Value::Null)
23561        }
23562        "net.socket_upgrade_tls" => {
23563            let socket_id =
23564                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_upgrade_tls socket id")?;
23565            let options_json =
23566                javascript_sync_rpc_arg_str(&request.args, 1, "net.socket_upgrade_tls options")?;
23567            let options: JavascriptTlsBridgeOptions =
23568                serde_json::from_str(options_json).map_err(|error| {
23569                    SidecarError::InvalidState(format!(
23570                        "net.socket_upgrade_tls options must be valid JSON: {error}"
23571                    ))
23572                })?;
23573            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
23574                SidecarError::InvalidState(format!(
23575                    "unknown TCP socket {socket_id} for TLS upgrade"
23576                ))
23577            })?;
23578            socket.upgrade_tls(vm_id, kernel, options)?;
23579            Ok(Value::Null)
23580        }
23581        "net.socket_get_tls_client_hello" => {
23582            let socket_id = javascript_sync_rpc_arg_str(
23583                &request.args,
23584                0,
23585                "net.socket_get_tls_client_hello socket id",
23586            )?;
23587            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
23588                SidecarError::InvalidState(format!(
23589                    "unknown TCP socket {socket_id} for TLS client hello query"
23590                ))
23591            })?;
23592            socket.tls_client_hello_json(vm_id, kernel)
23593        }
23594        "net.socket_tls_query" => {
23595            let socket_id =
23596                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_tls_query socket id")?;
23597            let query =
23598                javascript_sync_rpc_arg_str(&request.args, 1, "net.socket_tls_query query")?;
23599            let detailed = request
23600                .args
23601                .get(2)
23602                .and_then(Value::as_bool)
23603                .unwrap_or(false);
23604            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
23605                SidecarError::InvalidState(format!("unknown TCP socket {socket_id} for TLS query"))
23606            })?;
23607            socket.tls_query(query, detailed)
23608        }
23609        "net.server_poll" => {
23610            let listener_id =
23611                javascript_sync_rpc_arg_str(&request.args, 0, "net.server_poll listener id")?;
23612            let wait_ms =
23613                javascript_sync_rpc_arg_u64_optional(&request.args, 1, "net.server_poll wait ms")?
23614                    .unwrap_or_default();
23615            let tcp_event = if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
23616                Some(listener.poll(
23617                    kernel,
23618                    process.kernel_pid,
23619                    Duration::from_millis(wait_ms),
23620                    trace_enabled,
23621                )?)
23622            } else {
23623                None
23624            };
23625
23626            if let Some(event) = tcp_event {
23627                return match event {
23628                    Some(JavascriptTcpListenerEvent::Connection(pending)) => {
23629                        let PendingTcpSocket {
23630                            stream,
23631                            kernel_socket_id,
23632                            preallocated,
23633                            guest_local_addr,
23634                            guest_remote_addr,
23635                        } = pending;
23636                        if !preallocated {
23637                            if let Err(error) = check_network_resource_limit(
23638                                resource_limits.max_sockets,
23639                                network_counts.sockets,
23640                                1,
23641                                "socket",
23642                            )
23643                            .and_then(|()| {
23644                                check_network_resource_limit(
23645                                    resource_limits.max_connections,
23646                                    network_counts.connections,
23647                                    1,
23648                                    "connection",
23649                                )
23650                            }) {
23651                                if let Some(stream) = stream {
23652                                    let _ = stream.shutdown(Shutdown::Both);
23653                                }
23654                                return Ok(json!({
23655                                    "type": "error",
23656                                    "code": "EAGAIN",
23657                                    "message": error.to_string(),
23658                                }));
23659                            }
23660                        }
23661                        let socket = if let Some(stream) = stream {
23662                            ActiveTcpSocket::from_stream(
23663                                stream,
23664                                Some(listener_id.to_string()),
23665                                guest_local_addr,
23666                                guest_remote_addr,
23667                            )?
23668                        } else {
23669                            ActiveTcpSocket::from_kernel(
23670                                kernel_socket_id.ok_or_else(|| {
23671                                    SidecarError::InvalidState(String::from(
23672                                        "kernel TCP accept missing socket id",
23673                                    ))
23674                                })?,
23675                                Some(listener_id.to_string()),
23676                                guest_local_addr,
23677                                guest_remote_addr,
23678                            )
23679                        };
23680                        let socket_id = process.allocate_tcp_socket_id();
23681                        socket.set_event_pusher(
23682                            process.execution.javascript_v8_session_handle(),
23683                            socket_id.clone(),
23684                        );
23685                        register_kernel_readiness_target(
23686                            &kernel_readiness,
23687                            socket.kernel_socket_id,
23688                            process.execution.javascript_v8_session_handle(),
23689                            socket_id.clone(),
23690                            KernelSocketReadinessEvent::Data,
23691                        );
23692                        if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
23693                            listener.register_connection(&socket_id);
23694                        }
23695                        process.tcp_sockets.insert(socket_id.clone(), socket);
23696                        Ok(json!({
23697                            "type": "connection",
23698                            "socketId": socket_id,
23699                            "localAddress": guest_local_addr.ip().to_string(),
23700                            "localPort": guest_local_addr.port(),
23701                            "remoteAddress": guest_remote_addr.ip().to_string(),
23702                            "remotePort": guest_remote_addr.port(),
23703                            "remoteFamily": socket_addr_family(&guest_remote_addr),
23704                        }))
23705                    }
23706                    Some(JavascriptTcpListenerEvent::Error { code, message }) => Ok(json!({
23707                        "type": "error",
23708                        "code": code,
23709                        "message": message,
23710                    })),
23711                    None => Ok(Value::Null),
23712                };
23713            }
23714
23715            let event = {
23716                let listener = process.unix_listeners.get_mut(listener_id).ok_or_else(|| {
23717                    SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
23718                })?;
23719                listener.poll(Duration::from_millis(wait_ms))?
23720            };
23721
23722            match event {
23723                Some(JavascriptUnixListenerEvent::Connection(pending)) => {
23724                    if let Err(error) = check_network_resource_limit(
23725                        resource_limits.max_sockets,
23726                        network_counts.sockets,
23727                        1,
23728                        "socket",
23729                    )
23730                    .and_then(|()| {
23731                        check_network_resource_limit(
23732                            resource_limits.max_connections,
23733                            network_counts.connections,
23734                            1,
23735                            "connection",
23736                        )
23737                    }) {
23738                        let _ = pending.stream.shutdown(Shutdown::Both);
23739                        return Ok(json!({
23740                            "type": "error",
23741                            "code": "EAGAIN",
23742                            "message": error.to_string(),
23743                        }));
23744                    }
23745                    let socket = ActiveUnixSocket::from_stream(
23746                        pending.stream,
23747                        Some(listener_id.to_string()),
23748                        pending.local_path.clone(),
23749                        pending.remote_path.clone(),
23750                    )?;
23751                    let socket_id = process.allocate_unix_socket_id();
23752                    socket.set_event_pusher(
23753                        process.execution.javascript_v8_session_handle(),
23754                        socket_id.clone(),
23755                    );
23756                    if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
23757                        listener.register_connection(&socket_id);
23758                    }
23759                    process.unix_sockets.insert(socket_id.clone(), socket);
23760                    Ok(json!({
23761                        "type": "connection",
23762                        "socketId": socket_id,
23763                        "localPath": pending.local_path,
23764                        "remotePath": pending.remote_path,
23765                    }))
23766                }
23767                Some(JavascriptUnixListenerEvent::Error { code, message }) => Ok(json!({
23768                    "type": "error",
23769                    "code": code,
23770                    "message": message,
23771                })),
23772                None => Ok(Value::Null),
23773            }
23774        }
23775        "net.server_accept" => {
23776            let listener_id =
23777                javascript_sync_rpc_arg_str(&request.args, 0, "net.server_accept listener id")?;
23778            if trace_enabled {
23779                NET_TCP_TRACE_COUNTERS
23780                    .server_accept_calls
23781                    .fetch_add(1, Ordering::Relaxed);
23782                NET_TCP_TRACE_COUNTERS
23783                    .server_accept_zero_wait_calls
23784                    .fetch_add(1, Ordering::Relaxed);
23785            }
23786            if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
23787                return match listener.poll(
23788                    kernel,
23789                    process.kernel_pid,
23790                    Duration::ZERO,
23791                    trace_enabled,
23792                )? {
23793                    Some(JavascriptTcpListenerEvent::Connection(pending)) => {
23794                        let PendingTcpSocket {
23795                            stream,
23796                            kernel_socket_id,
23797                            preallocated,
23798                            guest_local_addr,
23799                            guest_remote_addr,
23800                        } = pending;
23801                        if !preallocated {
23802                            check_network_resource_limit(
23803                                resource_limits.max_sockets,
23804                                network_counts.sockets,
23805                                1,
23806                                "socket",
23807                            )?;
23808                            check_network_resource_limit(
23809                                resource_limits.max_connections,
23810                                network_counts.connections,
23811                                1,
23812                                "connection",
23813                            )?;
23814                        }
23815                        let info = tcp_socket_info_value(&guest_local_addr, &guest_remote_addr);
23816                        let socket = if let Some(stream) = stream {
23817                            ActiveTcpSocket::from_stream(
23818                                stream,
23819                                Some(listener_id.to_string()),
23820                                guest_local_addr,
23821                                guest_remote_addr,
23822                            )?
23823                        } else {
23824                            ActiveTcpSocket::from_kernel(
23825                                kernel_socket_id.ok_or_else(|| {
23826                                    SidecarError::InvalidState(String::from(
23827                                        "kernel TCP accept missing socket id",
23828                                    ))
23829                                })?,
23830                                Some(listener_id.to_string()),
23831                                guest_local_addr,
23832                                guest_remote_addr,
23833                            )
23834                        };
23835                        let socket_id = process.allocate_tcp_socket_id();
23836                        socket.set_event_pusher(
23837                            process.execution.javascript_v8_session_handle(),
23838                            socket_id.clone(),
23839                        );
23840                        register_kernel_readiness_target(
23841                            &kernel_readiness,
23842                            socket.kernel_socket_id,
23843                            process.execution.javascript_v8_session_handle(),
23844                            socket_id.clone(),
23845                            KernelSocketReadinessEvent::Data,
23846                        );
23847                        if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
23848                            listener.register_connection(&socket_id);
23849                        }
23850                        process.tcp_sockets.insert(socket_id.clone(), socket);
23851                        javascript_net_json_string(
23852                            json!({
23853                                "socketId": socket_id,
23854                                "info": info,
23855                            }),
23856                            "net.server_accept",
23857                        )
23858                    }
23859                    Some(JavascriptTcpListenerEvent::Error { code, message }) => {
23860                        let detail = code.unwrap_or_else(|| String::from("server accept"));
23861                        Err(SidecarError::Execution(format!("{detail}: {message}")))
23862                    }
23863                    None => Ok(javascript_net_timeout_value()),
23864                };
23865            }
23866
23867            let listener = process.unix_listeners.get_mut(listener_id).ok_or_else(|| {
23868                SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
23869            })?;
23870            match listener.poll(Duration::ZERO)? {
23871                Some(JavascriptUnixListenerEvent::Connection(pending)) => {
23872                    check_network_resource_limit(
23873                        resource_limits.max_sockets,
23874                        network_counts.sockets,
23875                        1,
23876                        "socket",
23877                    )?;
23878                    check_network_resource_limit(
23879                        resource_limits.max_connections,
23880                        network_counts.connections,
23881                        1,
23882                        "connection",
23883                    )?;
23884                    let info = json!({
23885                        "localPath": pending.local_path.clone(),
23886                        "remotePath": pending.remote_path.clone(),
23887                    });
23888                    let socket = ActiveUnixSocket::from_stream(
23889                        pending.stream,
23890                        Some(listener_id.to_string()),
23891                        pending.local_path,
23892                        pending.remote_path,
23893                    )?;
23894                    let socket_id = process.allocate_unix_socket_id();
23895                    socket.set_event_pusher(
23896                        process.execution.javascript_v8_session_handle(),
23897                        socket_id.clone(),
23898                    );
23899                    if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
23900                        listener.register_connection(&socket_id);
23901                    }
23902                    process.unix_sockets.insert(socket_id.clone(), socket);
23903                    javascript_net_json_string(
23904                        json!({
23905                            "socketId": socket_id,
23906                            "info": info,
23907                        }),
23908                        "net.server_accept",
23909                    )
23910                }
23911                Some(JavascriptUnixListenerEvent::Error { code, message }) => {
23912                    let detail = code.unwrap_or_else(|| String::from("server accept"));
23913                    Err(SidecarError::Execution(format!("{detail}: {message}")))
23914                }
23915                None => Ok(javascript_net_timeout_value()),
23916            }
23917        }
23918        "net.server_connections" => {
23919            let listener_id = javascript_sync_rpc_arg_str(
23920                &request.args,
23921                0,
23922                "net.server_connections listener id",
23923            )?;
23924            if let Some(listener) = process.tcp_listeners.get(listener_id) {
23925                Ok(json!(listener.active_connection_count()))
23926            } else {
23927                let listener = process.unix_listeners.get(listener_id).ok_or_else(|| {
23928                    SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
23929                })?;
23930                Ok(json!(listener.active_connection_count()))
23931            }
23932        }
23933        "net.upgrade_socket_write" => {
23934            let socket_id = javascript_sync_rpc_arg_str(
23935                &request.args,
23936                0,
23937                "net.upgrade_socket_write socket id",
23938            )?;
23939            let chunk =
23940                javascript_sync_rpc_base64_arg(&request.args, 1, "net.upgrade_socket_write chunk")?;
23941            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
23942                SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
23943            })?;
23944            socket
23945                .write_all(kernel, process.kernel_pid, &chunk)
23946                .map(|written| json!(written))
23947        }
23948        "net.upgrade_socket_end" => {
23949            let socket_id =
23950                javascript_sync_rpc_arg_str(&request.args, 0, "net.upgrade_socket_end socket id")?;
23951            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
23952                SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
23953            })?;
23954            socket.shutdown_write(kernel, process.kernel_pid)?;
23955            Ok(Value::Null)
23956        }
23957        "net.upgrade_socket_destroy" => {
23958            let socket_id = javascript_sync_rpc_arg_str(
23959                &request.args,
23960                0,
23961                "net.upgrade_socket_destroy socket id",
23962            )?;
23963            let socket = process.tcp_sockets.remove(socket_id).ok_or_else(|| {
23964                SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
23965            })?;
23966            release_tcp_socket_handle(process, socket_id, socket, kernel, &kernel_readiness);
23967            Ok(Value::Null)
23968        }
23969        "net.write" => {
23970            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.write socket id")?;
23971            let chunk = if let Some(bytes) = request.raw_bytes_args.get(&1) {
23972                bytes.clone()
23973            } else {
23974                javascript_sync_rpc_bytes_arg(&request.args, 1, "net.write chunk")?
23975            };
23976            if trace_enabled {
23977                NET_TCP_TRACE_COUNTERS
23978                    .socket_write_calls
23979                    .fetch_add(1, Ordering::Relaxed);
23980                NET_TCP_TRACE_COUNTERS.socket_write_bytes.fetch_add(
23981                    u64::try_from(chunk.len()).unwrap_or(u64::MAX),
23982                    Ordering::Relaxed,
23983                );
23984            }
23985            if let Some(socket) = process.tcp_sockets.get(socket_id) {
23986                let write_started = trace_enabled.then(Instant::now);
23987                let write_result = socket.write_all(kernel, process.kernel_pid, &chunk);
23988                if let Some(write_started) = write_started {
23989                    NET_TCP_TRACE_COUNTERS.socket_write_kernel_us.fetch_add(
23990                        duration_micros_u64(write_started.elapsed()),
23991                        Ordering::Relaxed,
23992                    );
23993                }
23994                match write_result {
23995                    Ok(written) => Ok(json!(written)),
23996                    Err(error) => {
23997                        if trace_enabled {
23998                            NET_TCP_TRACE_COUNTERS
23999                                .socket_write_errors
24000                                .fetch_add(1, Ordering::Relaxed);
24001                        }
24002                        Err(error)
24003                    }
24004                }
24005            } else {
24006                let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
24007                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
24008                })?;
24009                socket.write_all(&chunk).map(|written| json!(written))
24010            }
24011        }
24012        "net.shutdown" => {
24013            let socket_id =
24014                javascript_sync_rpc_arg_str(&request.args, 0, "net.shutdown socket id")?;
24015            if let Some(socket) = process.tcp_sockets.get(socket_id) {
24016                socket.shutdown_write(kernel, process.kernel_pid)?;
24017            } else {
24018                let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
24019                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
24020                })?;
24021                socket.shutdown_write()?;
24022            }
24023            Ok(Value::Null)
24024        }
24025        "net.destroy" => {
24026            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.destroy socket id")?;
24027            if let Some(socket) = process.tcp_sockets.remove(socket_id) {
24028                release_tcp_socket_handle(process, socket_id, socket, kernel, &kernel_readiness);
24029                Ok(Value::Null)
24030            } else if let Some(socket) = process.unix_sockets.remove(socket_id) {
24031                release_unix_socket_handle(process, socket_id, socket);
24032                Ok(Value::Null)
24033            } else {
24034                Ok(Value::Null)
24035            }
24036        }
24037        "net.server_close" => {
24038            let listener_id =
24039                javascript_sync_rpc_arg_str(&request.args, 0, "net.server_close listener id")?;
24040            if let Some(listener) = process.tcp_listeners.remove(listener_id) {
24041                unregister_kernel_readiness_target(&kernel_readiness, listener.kernel_socket_id);
24042                listener.close(kernel, process.kernel_pid)?;
24043                Ok(Value::Null)
24044            } else {
24045                let listener = process.unix_listeners.remove(listener_id).ok_or_else(|| {
24046                    SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
24047                })?;
24048                listener.close()?;
24049                Ok(Value::Null)
24050            }
24051        }
24052        "tls.get_ciphers" => javascript_net_json_string(
24053            Value::Array(
24054                tls_provider()
24055                    .cipher_suites
24056                    .iter()
24057                    .filter_map(|suite| {
24058                        suite
24059                            .suite()
24060                            .as_str()
24061                            .map(|value| Value::String(value.to_owned()))
24062                    })
24063                    .collect(),
24064            ),
24065            "tls.get_ciphers",
24066        ),
24067        _ => Err(SidecarError::InvalidState(format!(
24068            "unsupported JavaScript net sync RPC method {}",
24069            request.method
24070        ))),
24071    }
24072}
24073
24074fn signal_name_for_stream_event(signal: i32) -> Option<&'static str> {
24075    match signal {
24076        libc::SIGHUP => Some("SIGHUP"),
24077        libc::SIGINT => Some("SIGINT"),
24078        libc::SIGUSR1 => Some("SIGUSR1"),
24079        libc::SIGALRM => Some("SIGALRM"),
24080        libc::SIGCONT => Some("SIGCONT"),
24081        libc::SIGTERM => Some("SIGTERM"),
24082        libc::SIGCHLD => Some("SIGCHLD"),
24083        libc::SIGWINCH => Some("SIGWINCH"),
24084        _ => None,
24085    }
24086}
24087
24088pub(crate) fn canonical_signal_name(signal: i32) -> Option<&'static str> {
24089    agentos_native_sidecar_core::canonical_signal_name(signal)
24090}
24091
24092fn dispatch_v8_process_signal(process: &ActiveProcess, signal: i32) -> Result<bool, SidecarError> {
24093    let Some(signal_name) = signal_name_for_stream_event(signal) else {
24094        return Ok(false);
24095    };
24096    process.execution.send_javascript_stream_event(
24097        "signal",
24098        json!({
24099            "signal": signal_name,
24100            "number": signal,
24101            "action": "default",
24102        }),
24103    )?;
24104    Ok(true)
24105}
24106
24107fn dispatch_v8_session_signal_async(session: V8SessionHandle, signal: i32) {
24108    let Some(signal_name) = signal_name_for_stream_event(signal).map(str::to_owned) else {
24109        return;
24110    };
24111    thread::spawn(move || {
24112        thread::sleep(Duration::from_millis(1));
24113        let payload = v8_runtime::json_to_cbor_payload(&json!({
24114            "signal": signal_name,
24115            "number": signal,
24116            "action": "default",
24117        }))
24118        .unwrap_or_default();
24119        let _ = session.send_stream_event("signal", payload);
24120    });
24121}
24122
24123pub(crate) fn parse_signal(signal: &str) -> Result<i32, SidecarError> {
24124    let trimmed = signal.trim();
24125    if trimmed.is_empty() {
24126        return Err(SidecarError::InvalidState(String::from(
24127            "kill_process requires a non-empty signal",
24128        )));
24129    }
24130
24131    if let Ok(value) = trimmed.parse::<i32>() {
24132        return match value {
24133            0..=31 => Ok(value),
24134            _ => Err(SidecarError::InvalidState(format!(
24135                "unsupported kill_process signal {signal}"
24136            ))),
24137        };
24138    }
24139
24140    agentos_native_sidecar_core::parse_posix_signal(trimmed).ok_or_else(|| {
24141        SidecarError::InvalidState(format!("unsupported kill_process signal {signal}"))
24142    })
24143}
24144
24145pub(crate) fn runtime_child_is_alive(child_pid: u32) -> Result<bool, SidecarError> {
24146    Ok(runtime_child_exit_status(child_pid)?.is_none())
24147}
24148
24149#[cfg(not(target_os = "macos"))]
24150fn runtime_child_exit_status(child_pid: u32) -> Result<Option<i32>, SidecarError> {
24151    if child_pid == 0 {
24152        return Ok(Some(0));
24153    }
24154
24155    let wait_flags = WaitPidFlag::WNOHANG
24156        | WaitPidFlag::WNOWAIT
24157        | WaitPidFlag::WEXITED
24158        | WaitPidFlag::WUNTRACED
24159        | WaitPidFlag::WCONTINUED;
24160    match wait_on_child(WaitId::Pid(Pid::from_raw(child_pid as i32)), wait_flags) {
24161        Ok(WaitStatus::StillAlive)
24162        | Ok(WaitStatus::Stopped(_, _))
24163        | Ok(WaitStatus::Continued(_)) => Ok(None),
24164        Ok(WaitStatus::Exited(_, status)) => Ok(Some(status)),
24165        Ok(WaitStatus::Signaled(_, signal, _)) => Ok(Some(128 + signal as i32)),
24166        #[cfg(any(target_os = "linux", target_os = "android"))]
24167        Ok(WaitStatus::PtraceEvent(_, _, _) | WaitStatus::PtraceSyscall(_)) => Ok(None),
24168        Err(nix::errno::Errno::ECHILD) => Ok(Some(0)),
24169        Err(error) => Err(SidecarError::Execution(format!(
24170            "failed to inspect guest runtime process {child_pid}: {error}"
24171        ))),
24172    }
24173}
24174
24175// macOS nix exposes no `waitid`/`WNOWAIT`, so we poll with `waitpid(WNOHANG)`.
24176// NOTE: unlike Linux's `waitid(WNOWAIT)`, `waitpid` REAPS an exited child rather
24177// than leaving it waitable. That is correct for this poll (the sidecar is the
24178// reaping parent), but a second status query after exit returns ECHILD → treated
24179// as "exited(0)" below.
24180#[cfg(target_os = "macos")]
24181fn runtime_child_exit_status(child_pid: u32) -> Result<Option<i32>, SidecarError> {
24182    if child_pid == 0 {
24183        return Ok(Some(0));
24184    }
24185
24186    match waitpid(Pid::from_raw(child_pid as i32), Some(WaitPidFlag::WNOHANG)) {
24187        Ok(WaitStatus::StillAlive)
24188        | Ok(WaitStatus::Stopped(_, _))
24189        | Ok(WaitStatus::Continued(_)) => Ok(None),
24190        Ok(WaitStatus::Exited(_, status)) => Ok(Some(status)),
24191        Ok(WaitStatus::Signaled(_, signal, _)) => Ok(Some(128 + signal as i32)),
24192        Err(nix::errno::Errno::ECHILD) => Ok(Some(0)),
24193        Err(error) => Err(SidecarError::Execution(format!(
24194            "failed to inspect guest runtime process {child_pid}: {error}"
24195        ))),
24196    }
24197}
24198
24199pub(crate) fn signal_runtime_process(child_pid: u32, signal: i32) -> Result<(), SidecarError> {
24200    if child_pid == 0 {
24201        return Ok(());
24202    }
24203
24204    if !runtime_child_is_alive(child_pid)? {
24205        return Ok(());
24206    }
24207
24208    if signal == 0 {
24209        return Ok(());
24210    }
24211
24212    let parsed = Signal::try_from(signal).map_err(|_| {
24213        SidecarError::InvalidState(format!("unsupported kill_process signal {signal}"))
24214    })?;
24215    let result = send_signal(Pid::from_raw(child_pid as i32), Some(parsed));
24216
24217    match result {
24218        Ok(()) => Ok(()),
24219        Err(nix::errno::Errno::ESRCH) => Ok(()),
24220        Err(error) => Err(SidecarError::Execution(format!(
24221            "failed to signal guest runtime process {child_pid}: {error}"
24222        ))),
24223    }
24224}
24225
24226pub(crate) fn error_code(error: &SidecarError) -> &'static str {
24227    match error {
24228        SidecarError::InvalidState(_) => "invalid_state",
24229        SidecarError::ProtocolVersionMismatch(_) => "protocol_version_mismatch",
24230        SidecarError::BridgeVersionMismatch(_) => "bridge_version_mismatch",
24231        SidecarError::Conflict(_) => "conflict",
24232        SidecarError::Unauthorized(_) => "unauthorized",
24233        SidecarError::Unsupported(_) => "unsupported",
24234        SidecarError::FrameTooLarge(_) => "frame_too_large",
24235        SidecarError::Kernel(_) => "kernel_error",
24236        SidecarError::Plugin(_) => "plugin_error",
24237        SidecarError::Execution(_) => "execution_error",
24238        SidecarError::Bridge(_) => "bridge_error",
24239        SidecarError::Io(_) => "io_error",
24240    }
24241}
24242
24243fn guest_errno_code(message: &str) -> Option<&str> {
24244    const TRUSTED_PREFIXES: &[&str] = &[
24245        "ERR_AGENTOS_NODE_SYNC_RPC",
24246        "ERR_AGENTOS_PYTHON_VFS_RPC",
24247        "ERR_AGENTOS_BRIDGE",
24248    ];
24249
24250    let mut segments = message.split(':').map(str::trim);
24251    let first = segments.next()?;
24252    if is_guest_errno_segment(first) {
24253        return Some(first);
24254    }
24255
24256    if TRUSTED_PREFIXES.contains(&first) {
24257        let second = segments.next()?;
24258        if is_guest_errno_segment(second) {
24259            return Some(second);
24260        }
24261    }
24262
24263    None
24264}
24265
24266fn is_guest_errno_segment(segment: &str) -> bool {
24267    segment.len() >= 2
24268        && segment.starts_with('E')
24269        && !segment.starts_with("ERR_")
24270        && segment[1..]
24271            .bytes()
24272            .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_')
24273}
24274
24275pub(crate) fn javascript_sync_rpc_error_code(error: &SidecarError) -> String {
24276    let message = error.to_string();
24277    if let Some(code) = guest_errno_code(&message) {
24278        return code.to_owned();
24279    }
24280    if message.starts_with("ERR_NATIVE_BINARY_NOT_SUPPORTED:") {
24281        return String::from("ERR_NATIVE_BINARY_NOT_SUPPORTED");
24282    }
24283
24284    let lower = message.to_ascii_lowercase();
24285    if lower.contains("no such file or directory")
24286        || lower.contains("entry not found")
24287        || lower.contains("not found")
24288    {
24289        return String::from("ENOENT");
24290    }
24291    if lower.contains("permission denied") {
24292        return String::from("EACCES");
24293    }
24294    if lower.contains("already exists")
24295        || lower.contains("already registered")
24296        || lower.contains("file exists")
24297    {
24298        return String::from("EEXIST");
24299    }
24300    if lower.contains("invalid argument") {
24301        return String::from("EINVAL");
24302    }
24303
24304    String::from("ERR_AGENTOS_NODE_SYNC_RPC")
24305}
24306
24307pub(crate) fn ignore_stale_javascript_sync_rpc_response(
24308    error: SidecarError,
24309) -> Result<(), SidecarError> {
24310    match error {
24311        SidecarError::Execution(message)
24312            if message.ends_with("is no longer pending")
24313                && message.starts_with("sync RPC request ") =>
24314        {
24315            Ok(())
24316        }
24317        SidecarError::Execution(message) => {
24318            let lower = message.to_ascii_lowercase();
24319            if lower.contains("sync rpc response")
24320                && (lower.contains("broken pipe") || lower.contains("channel closed unexpectedly"))
24321            {
24322                Ok(())
24323            } else {
24324                Err(SidecarError::Execution(message))
24325            }
24326        }
24327        other => Err(other),
24328    }
24329}
24330
24331#[cfg(test)]
24332mod error_code_tests {
24333    use super::{guest_errno_code, javascript_sync_rpc_error_code, SidecarError};
24334
24335    #[test]
24336    fn guest_errno_code_rejects_guest_controlled_errno_segments() {
24337        assert_eq!(guest_errno_code("user said 'EACCES: denied'"), None);
24338        assert_eq!(
24339            guest_errno_code("prefix: user said 'EPERM': more text"),
24340            None
24341        );
24342        assert_eq!(guest_errno_code("ERR_AGENTOS_FAKE: EACCES: denied"), None);
24343    }
24344
24345    #[test]
24346    fn guest_errno_code_accepts_trusted_secure_exec_prefixes() {
24347        assert_eq!(
24348            guest_errno_code("ERR_AGENTOS_NODE_SYNC_RPC: EACCES: permission denied on /foo"),
24349            Some("EACCES")
24350        );
24351        assert_eq!(
24352            guest_errno_code("ERR_AGENTOS_PYTHON_VFS_RPC: ENOENT: missing file"),
24353            Some("ENOENT")
24354        );
24355        assert_eq!(guest_errno_code("EEXIST: already exists"), Some("EEXIST"));
24356    }
24357
24358    #[test]
24359    fn javascript_sync_rpc_error_code_ignores_spoofed_errnos() {
24360        let error = SidecarError::Execution(String::from("user said 'EACCES: denied'"));
24361        assert_eq!(
24362            javascript_sync_rpc_error_code(&error),
24363            "ERR_AGENTOS_NODE_SYNC_RPC"
24364        );
24365    }
24366
24367    #[test]
24368    fn javascript_sync_rpc_error_code_preserves_real_sidecar_errnos() {
24369        let error = SidecarError::Execution(String::from(
24370            "ERR_AGENTOS_NODE_SYNC_RPC: EACCES: permission denied on /foo",
24371        ));
24372        assert_eq!(javascript_sync_rpc_error_code(&error), "EACCES");
24373    }
24374
24375    #[test]
24376    fn javascript_sync_rpc_error_code_maps_file_exists_messages() {
24377        let error = SidecarError::Io(String::from(
24378            "failed to create mapped guest directory /.next/server: File exists (os error 17)",
24379        ));
24380        assert_eq!(javascript_sync_rpc_error_code(&error), "EEXIST");
24381    }
24382
24383    #[test]
24384    fn javascript_sync_rpc_error_code_preserves_native_binary_rejections() {
24385        let error = SidecarError::Execution(String::from(
24386            "ERR_NATIVE_BINARY_NOT_SUPPORTED: refused to execute native ELF guest binary at /tmp/fake-rg inside the VM",
24387        ));
24388        assert_eq!(
24389            javascript_sync_rpc_error_code(&error),
24390            "ERR_NATIVE_BINARY_NOT_SUPPORTED"
24391        );
24392    }
24393}
24394#[cfg(test)]
24395mod ssrf_egress_classifier_tests {
24396    // F-005/006/007 (sec-sidecar T1/T7/T11): the egress classifier must treat the
24397    // unspecified address (0.0.0.0 / ::), CGNAT (100.64.0.0/10), IPv6 spellings of
24398    // restricted IPv4 targets (::a.b.c.d), and reserved/multicast (240/4, 224/4) as
24399    // restricted. 0.0.0.0 routes to 127.0.0.1 on connect(), so leaving it
24400    // unclassified let a guest bypass the loopback port-ownership gate.
24401    //
24402    // These are bounded SAFEGUARD tests: they exercise the classifier and the DNS
24403    // egress filter directly (no network I/O, no Node), so they run fast and
24404    // deterministically. See FAILURES.md#F-005, #F-006, #F-007.
24405    use super::{
24406        filter_dns_safe_ip_addrs, is_loopback_ip, restricted_non_loopback_ip_range, SidecarError,
24407    };
24408    use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
24409
24410    fn assert_restricted(ip: IpAddr, expected_label: &str) {
24411        let classification = restricted_non_loopback_ip_range(ip);
24412        assert!(
24413            classification.is_some(),
24414            "{ip} must be classified as a restricted egress target"
24415        );
24416        let (_cidr, label) = classification.unwrap();
24417        assert_eq!(
24418            label, expected_label,
24419            "{ip} should be labelled {expected_label}, got {label}"
24420        );
24421    }
24422
24423    fn assert_dns_denied(ip: IpAddr, label: &str) {
24424        match filter_dns_safe_ip_addrs(vec![ip], "attacker.example") {
24425            Err(SidecarError::Execution(message)) => assert!(
24426                message.starts_with("EACCES:"),
24427                "{label}: egress filter must deny with EACCES, got: {message}"
24428            ),
24429            other => panic!("{label}: expected EACCES denial, got {other:?}"),
24430        }
24431    }
24432
24433    // F-005 (sec-sidecar T1).
24434    #[test]
24435    fn classifier_denies_unspecified_and_cgnat_targets() {
24436        // 0.0.0.0 (IPv4 unspecified) -> would route to host loopback.
24437        assert_restricted(IpAddr::V4(Ipv4Addr::UNSPECIFIED), "unspecified");
24438        // :: (IPv6 unspecified).
24439        assert_restricted(IpAddr::V6(Ipv6Addr::UNSPECIFIED), "unspecified");
24440
24441        // CGNAT 100.64.0.0/10 spans 100.64.x.x .. 100.127.x.x.
24442        assert_restricted(
24443            IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1)),
24444            "carrier-grade-nat",
24445        );
24446        assert_restricted(
24447            IpAddr::V4(Ipv4Addr::new(100, 127, 255, 254)),
24448            "carrier-grade-nat",
24449        );
24450
24451        // Guard against over-blocking: addresses just outside 100.64/10 stay allowed.
24452        assert!(
24453            restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(100, 63, 255, 255)))
24454                .is_none(),
24455            "100.63.255.255 is outside CGNAT and must remain allowed"
24456        );
24457        assert!(
24458            restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(100, 128, 0, 0))).is_none(),
24459            "100.128.0.0 is outside CGNAT and must remain allowed"
24460        );
24461
24462        // The DNS egress filter must also deny these via EACCES.
24463        assert_dns_denied(IpAddr::V4(Ipv4Addr::UNSPECIFIED), "0.0.0.0 (unspecified)");
24464        assert_dns_denied(IpAddr::V6(Ipv6Addr::UNSPECIFIED), ":: (unspecified)");
24465        assert_dns_denied(
24466            IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1)),
24467            "100.64.0.1 (CGNAT)",
24468        );
24469    }
24470
24471    // F-006 (sec-sidecar T7).
24472    #[test]
24473    fn classifier_denies_ipv6_spelled_metadata_addresses() {
24474        // The IPv4-mapped form (::ffff:169.254.169.254) was already handled; the
24475        // IPv4-compatible form (::169.254.169.254) is the gap this fixes.
24476        let mapped = "::ffff:169.254.169.254".parse::<Ipv6Addr>().unwrap();
24477        assert_restricted(IpAddr::V6(mapped), "link-local");
24478
24479        let compat = "::169.254.169.254".parse::<Ipv6Addr>().unwrap();
24480        assert_restricted(IpAddr::V6(compat), "link-local");
24481
24482        // Other IPv4-compatible private/CGNAT spellings must also be canonicalized.
24483        assert_restricted(
24484            IpAddr::V6("::10.0.0.1".parse::<Ipv6Addr>().unwrap()),
24485            "private",
24486        );
24487        assert_restricted(
24488            IpAddr::V6("::100.64.0.1".parse::<Ipv6Addr>().unwrap()),
24489            "carrier-grade-nat",
24490        );
24491
24492        // Guard against over-blocking: the IPv6 unspecified/loopback addresses
24493        // are not IPv4-compatible host targets, and a public IPv4-compatible
24494        // address must remain allowed.
24495        assert_eq!(
24496            restricted_non_loopback_ip_range(IpAddr::V6(Ipv6Addr::UNSPECIFIED)),
24497            Some(("::/128", "unspecified")),
24498            ":: must classify as unspecified, not via the IPv4-compat path"
24499        );
24500        assert!(
24501            restricted_non_loopback_ip_range(IpAddr::V6(Ipv6Addr::LOCALHOST)).is_none()
24502                || is_loopback_ip(IpAddr::V6(Ipv6Addr::LOCALHOST)),
24503            "::1 must not be classified as a restricted IPv4-compatible target"
24504        );
24505        assert!(
24506            restricted_non_loopback_ip_range(IpAddr::V6("::8.8.8.8".parse::<Ipv6Addr>().unwrap()))
24507                .is_none(),
24508            "::8.8.8.8 (public IPv4-compatible) must remain allowed"
24509        );
24510
24511        // The DNS egress filter must deny the IPv4-compat metadata spelling.
24512        assert_dns_denied(
24513            IpAddr::V6("::169.254.169.254".parse::<Ipv6Addr>().unwrap()),
24514            "::169.254.169.254 (IPv4-compat metadata)",
24515        );
24516    }
24517
24518    // F-007 (sec-sidecar T11).
24519    #[test]
24520    fn classifier_denies_reserved_and_multicast_targets() {
24521        // 224.0.0.0/4 (multicast) and 240.0.0.0/4 (reserved / future use) are not
24522        // legitimate unicast egress targets; a guest connect to them must be
24523        // classified as restricted and denied.
24524        assert_restricted(IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)), "multicast");
24525        assert_restricted(IpAddr::V4(Ipv4Addr::new(239, 255, 255, 255)), "multicast");
24526        assert_restricted(IpAddr::V4(Ipv4Addr::new(240, 0, 0, 1)), "reserved");
24527        // 255.255.255.255 (limited broadcast) falls in 240.0.0.0/4.
24528        assert_restricted(IpAddr::V4(Ipv4Addr::BROADCAST), "reserved");
24529
24530        // IPv4-compatible IPv6 spellings must canonicalize and be denied too.
24531        assert_restricted(
24532            IpAddr::V6("::224.0.0.1".parse::<Ipv6Addr>().unwrap()),
24533            "multicast",
24534        );
24535        assert_restricted(
24536            IpAddr::V6("::240.0.0.1".parse::<Ipv6Addr>().unwrap()),
24537            "reserved",
24538        );
24539
24540        // Guard against over-blocking: addresses just outside 224/4 stay allowed.
24541        assert!(
24542            restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(223, 255, 255, 255)))
24543                .is_none(),
24544            "223.255.255.255 is outside 224/4 and must remain allowed"
24545        );
24546
24547        // The DNS egress filter must also deny these via EACCES.
24548        assert_dns_denied(
24549            IpAddr::V4(Ipv4Addr::new(240, 0, 0, 1)),
24550            "240.0.0.1 (reserved)",
24551        );
24552        assert_dns_denied(
24553            IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)),
24554            "224.0.0.1 (multicast)",
24555        );
24556    }
24557}
24558
24559/// Adversarial coverage for the DNS-rebinding gap (VECTORS.md D.3) on the
24560/// Python/Pyodide `httpRequestSync` outbound HTTP path. The egress range guard
24561/// (`filter_dns_safe_ip_addrs`) runs at resolution time, but `ureq` performs its
24562/// own DNS resolution for the TCP/TLS connect, so a rebinding DNS server could
24563/// previously make the second lookup land on a private/link-local/metadata IP
24564/// the first check rejected. The fix pins `ureq`'s resolver to the vetted
24565/// address set; these tests prove the connect is pinned and refuses any other
24566/// host or an empty (fully-rejected) address set.
24567#[cfg(test)]
24568mod dns_rebinding_pin_tests {
24569    use super::{issue_outbound_http_request, split_netloc, JavascriptHttpRequestOptions};
24570    use std::collections::BTreeMap;
24571    use std::io::{Read, Write};
24572    use std::net::{IpAddr, Ipv4Addr, TcpListener};
24573    use std::thread;
24574    use url::Url;
24575
24576    fn empty_headers() -> super::HttpHeaderCollection {
24577        super::parse_http_header_collection(&BTreeMap::new(), "test headers")
24578            .expect("empty header collection")
24579    }
24580
24581    fn options() -> JavascriptHttpRequestOptions {
24582        JavascriptHttpRequestOptions {
24583            method: Some(String::from("GET")),
24584            headers: BTreeMap::new(),
24585            body: None,
24586            reject_unauthorized: None,
24587        }
24588    }
24589
24590    #[test]
24591    fn split_netloc_handles_hostnames_and_bracketed_ipv6() {
24592        assert_eq!(
24593            split_netloc("attacker.example:80"),
24594            Some(("attacker.example", 80))
24595        );
24596        assert_eq!(split_netloc("[::1]:443"), Some(("::1", 443)));
24597        assert_eq!(split_netloc("10.0.0.1:8080"), Some(("10.0.0.1", 8080)));
24598        assert_eq!(split_netloc("no-port"), None);
24599        assert_eq!(split_netloc("host:notaport"), None);
24600    }
24601
24602    /// A loopback HTTP server stands in for the egress-vetted target. The
24603    /// request URL uses a *different* hostname (`attacker.example`) whose real
24604    /// DNS would resolve elsewhere; pinning forces the connect onto the vetted
24605    /// IP only. If the resolver were unpinned, the request would fail to reach
24606    /// this server (and on a real host could land on a private/metadata IP).
24607    #[test]
24608    fn outbound_http_connect_is_pinned_to_vetted_ip() {
24609        let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).expect("bind loopback server");
24610        let port = listener.local_addr().expect("local addr").port();
24611        let server = thread::spawn(move || {
24612            let (mut stream, _) = listener.accept().expect("accept");
24613            let mut buf = [0u8; 1024];
24614            let _ = stream.read(&mut buf);
24615            stream
24616                .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi")
24617                .expect("write response");
24618            let _ = stream.flush();
24619        });
24620
24621        let url = Url::parse(&format!("http://attacker.example:{port}/")).expect("url");
24622        let pinned = vec![IpAddr::V4(Ipv4Addr::LOCALHOST)];
24623        let result = issue_outbound_http_request(&url, &options(), &empty_headers(), &pinned)
24624            .expect("pinned request should reach the vetted loopback target");
24625        let payload = result.as_str().expect("string payload");
24626        assert!(
24627            payload.contains("\"status\":200"),
24628            "expected 200 from pinned target, got: {payload}"
24629        );
24630        server.join().expect("server thread");
24631    }
24632
24633    /// With no vetted address (every resolved IP was rejected by the range
24634    /// guard, or the literal IP was a blocked range), the pinned resolver must
24635    /// refuse rather than fall back to the host resolver.
24636    #[test]
24637    fn outbound_http_refuses_when_no_vetted_address() {
24638        let url = Url::parse("https://attacker.example/").expect("url");
24639        let error = issue_outbound_http_request(&url, &options(), &empty_headers(), &[])
24640            .expect_err("empty pinned set must be refused");
24641        let message = error.to_string();
24642        assert!(
24643            message.contains("EACCES") || message.contains("ERR_HTTP_REQUEST_FAILED"),
24644            "expected an egress refusal, got: {message}"
24645        );
24646    }
24647}