1use 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
281fn 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
773fn 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 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 let Some(key) = self.registry_key.take() else {
1164 return;
1165 };
1166 let Ok(mut transports) = loopback_tls_transport_registry().lock() else {
1167 return;
1169 };
1170 let should_remove = match transports.get(&key) {
1171 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 #[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 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 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
1344struct 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
2498impl 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
2602impl 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
2922impl 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
3211impl 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 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 } 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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, ¤t_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 ¤t_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 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 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 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| ®istration.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 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 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 Err(error) if sidecar_error_is_esrch(&error) => {}
9031 Err(error) => return Err(error),
9032 }
9033 }
9034 Ok(caller_is_member)
9035 }
9036}
9037
9038fn 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
9156fn 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 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 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 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 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 Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
10044 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 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
10148fn 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 || 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
10294fn is_python_runtime_command(command: &str) -> bool {
10298 matches!(
10299 python_command_base_name(command),
10300 "python" | "python3" | "pip" | "pip3"
10301 )
10302}
10303
10304fn 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 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 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 env.insert(
10751 String::from("AGENTOS_JS_PLATFORM"),
10752 js_runtime_platform_env(vm).to_owned(),
10753 );
10754 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 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 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
10811fn 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
10827fn 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 snapshot_userland_code: vm.configuration.snapshot_userland_code.clone(),
10866 }
10867}
10868
10869fn 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
10882fn 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
10892fn 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
10909fn 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
10919fn 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
10930fn 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
10946fn 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 if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node {
10962 return Vec::new();
10963 }
10964 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
10997fn 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
11006fn 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
11096fn 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 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 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 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 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
11495fn 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 .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 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
11687fn 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
12374fn 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 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
13262fn 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
13327fn 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
13727pub(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
13751fn 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
13769const 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 .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 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
15000fn 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, ¤t)
15826 .map_err(kernel_error)?
15827 {
15828 kernel
15829 .mkdir_for_process(EXECUTION_DRIVER_NAME, kernel_pid, ¤t, 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
16132static 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 "_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 "_loadPolyfill" | "__load_polyfill" => {
16495 service_javascript_internal_bridge_sync_rpc(process, request)
16496 }
16497 "__kernel_stdin_read" => {
16498 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 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, ¶ms, &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
19033pub(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
19047pub(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 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
19131fn 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
19174pub(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 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 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
19272pub(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
19297pub(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 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 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
19393fn 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
19994fn 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 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 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 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#[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 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 #[test]
24435 fn classifier_denies_unspecified_and_cgnat_targets() {
24436 assert_restricted(IpAddr::V4(Ipv4Addr::UNSPECIFIED), "unspecified");
24438 assert_restricted(IpAddr::V6(Ipv6Addr::UNSPECIFIED), "unspecified");
24440
24441 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 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 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 #[test]
24473 fn classifier_denies_ipv6_spelled_metadata_addresses() {
24474 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 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 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 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 #[test]
24520 fn classifier_denies_reserved_and_multicast_targets() {
24521 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 assert_restricted(IpAddr::V4(Ipv4Addr::BROADCAST), "reserved");
24529
24530 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 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 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#[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 #[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 #[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}