Skip to main content

secure_exec_sidecar/
state.rs

1//! Shared state types used across sidecar domain modules.
2//!
3//! Contains VM state, session state, configuration types, active process/socket
4//! types, and other shared data structures extracted from service.rs.
5
6use crate::protocol::{
7    EventFrame, GuestRuntimeKind, MountDescriptor, PermissionsPolicy, ProjectedModuleDescriptor,
8    RegisterHostCallbacksRequest, ResponseFrame, SidecarRequestFrame, SidecarRequestPayload,
9    SidecarResponseFrame, SidecarResponsePayload, SignalHandlerRegistration, SoftwareDescriptor,
10    WasmPermissionTier,
11};
12use crate::wire::DEFAULT_MAX_FRAME_BYTES;
13use rusqlite::Connection;
14use rustls::{ClientConnection, ServerConnection, StreamOwned};
15use secure_exec_bridge::{BridgeTypes, FilesystemSnapshot};
16use secure_exec_execution::{
17    JavascriptExecution, JavascriptSyncRpcRequest, PythonExecution, PythonVfsRpcRequest,
18    WasmExecution,
19};
20use secure_exec_kernel::kernel::{KernelProcessHandle, KernelVm};
21use secure_exec_kernel::mount_table::MountTable;
22use secure_exec_kernel::root_fs::{RootFileSystem, RootFilesystemMode, RootFilesystemSnapshot};
23use secure_exec_kernel::socket_table::SocketId;
24use secure_exec_vm_config as vm_config;
25use serde::{Deserialize, Serialize};
26use serde_json::Value;
27use std::collections::{BTreeMap, BTreeSet, VecDeque};
28use std::error::Error;
29use std::fmt;
30use std::fs::File;
31use std::net::{IpAddr, SocketAddr, TcpListener, TcpStream, UdpSocket};
32use std::os::unix::net::{UnixListener, UnixStream};
33use std::path::PathBuf;
34use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
35use std::sync::mpsc::{Receiver, Sender};
36use std::sync::{Arc, Condvar, Mutex};
37use std::time::{Duration, Instant};
38use tokio::sync::mpsc::UnboundedSender;
39
40// ---------------------------------------------------------------------------
41// Type aliases
42// ---------------------------------------------------------------------------
43
44pub(crate) type BridgeError<B> = <B as BridgeTypes>::Error;
45pub(crate) type SidecarKernel = KernelVm<MountTable>;
46
47// ---------------------------------------------------------------------------
48// Constants
49// ---------------------------------------------------------------------------
50
51pub(crate) const EXECUTION_DRIVER_NAME: &str = "secure-exec-sidecar-execution";
52pub(crate) const JAVASCRIPT_COMMAND: &str = "node";
53pub(crate) const PYTHON_COMMAND: &str = "python";
54pub(crate) const WASM_COMMAND: &str = "wasm";
55pub(crate) const PYTHON_VFS_RPC_GUEST_ROOT: &str = "/workspace";
56pub(crate) const EXECUTION_SANDBOX_ROOT_ENV: &str = "AGENTOS_SANDBOX_ROOT";
57pub(crate) const WASM_STDIO_SYNC_RPC_ENV: &str = "AGENTOS_WASI_STDIO_SYNC_RPC";
58#[cfg(test)]
59#[allow(dead_code)]
60pub(crate) const HOST_REALPATH_MAX_SYMLINK_DEPTH: usize = 40;
61pub(crate) const DISPOSE_VM_SIGTERM_GRACE: std::time::Duration =
62    std::time::Duration::from_millis(100);
63pub(crate) const DISPOSE_VM_SIGKILL_GRACE: std::time::Duration =
64    std::time::Duration::from_millis(100);
65pub(crate) const VM_DNS_SERVERS_METADATA_KEY: &str = "network.dns.servers";
66#[cfg(test)]
67#[allow(dead_code)]
68pub(crate) const VM_LISTEN_PORT_MIN_METADATA_KEY: &str = "network.listen.port_min";
69#[cfg(test)]
70#[allow(dead_code)]
71pub(crate) const VM_LISTEN_PORT_MAX_METADATA_KEY: &str = "network.listen.port_max";
72pub(crate) const VM_LISTEN_ALLOW_PRIVILEGED_METADATA_KEY: &str = "network.listen.allow_privileged";
73pub(crate) const DEFAULT_JAVASCRIPT_NET_BACKLOG: u32 = 511;
74pub(crate) const LOOPBACK_EXEMPT_PORTS_ENV: &str = "AGENTOS_LOOPBACK_EXEMPT_PORTS";
75pub(crate) const TOOL_DRIVER_NAME: &str = "secure-exec-host-callbacks";
76pub(crate) const MAPPED_HOST_FD_START: u32 = 1_000_000_000;
77
78// ---------------------------------------------------------------------------
79// Public API types
80// ---------------------------------------------------------------------------
81
82#[derive(Debug, Clone)]
83pub struct NativeSidecarConfig {
84    pub sidecar_id: String,
85    pub max_frame_bytes: usize,
86    pub compile_cache_root: Option<PathBuf>,
87    pub expected_auth_token: Option<String>,
88    pub acp_termination_grace: Duration,
89}
90
91impl Default for NativeSidecarConfig {
92    fn default() -> Self {
93        Self {
94            sidecar_id: String::from("secure-exec-sidecar"),
95            max_frame_bytes: DEFAULT_MAX_FRAME_BYTES,
96            compile_cache_root: None,
97            expected_auth_token: None,
98            acp_termination_grace: Duration::from_secs(3),
99        }
100    }
101}
102
103#[derive(Debug, Clone)]
104pub struct DispatchResult {
105    pub response: ResponseFrame,
106    pub events: Vec<EventFrame>,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq)]
110pub enum SidecarError {
111    InvalidState(String),
112    ProtocolVersionMismatch(String),
113    BridgeVersionMismatch(String),
114    Conflict(String),
115    Unauthorized(String),
116    Unsupported(String),
117    FrameTooLarge(String),
118    Kernel(String),
119    Plugin(String),
120    Execution(String),
121    Bridge(String),
122    Io(String),
123}
124
125impl fmt::Display for SidecarError {
126    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127        match self {
128            Self::InvalidState(message)
129            | Self::ProtocolVersionMismatch(message)
130            | Self::BridgeVersionMismatch(message)
131            | Self::Conflict(message)
132            | Self::Unauthorized(message)
133            | Self::Unsupported(message)
134            | Self::FrameTooLarge(message)
135            | Self::Kernel(message)
136            | Self::Plugin(message)
137            | Self::Execution(message)
138            | Self::Bridge(message)
139            | Self::Io(message) => f.write_str(message),
140        }
141    }
142}
143
144impl Error for SidecarError {}
145
146pub trait SidecarRequestTransport: Send + Sync {
147    fn send_request(
148        &self,
149        request: SidecarRequestFrame,
150        timeout: Duration,
151    ) -> Result<SidecarResponseFrame, SidecarError>;
152}
153
154#[derive(Clone)]
155pub(crate) struct SharedSidecarRequestClient {
156    transport: Option<Arc<dyn SidecarRequestTransport>>,
157    next_request_id: Arc<AtomicI64>,
158}
159
160impl Default for SharedSidecarRequestClient {
161    fn default() -> Self {
162        Self {
163            transport: None,
164            next_request_id: Arc::new(AtomicI64::new(-1)),
165        }
166    }
167}
168
169impl SharedSidecarRequestClient {
170    pub(crate) fn set_transport(&mut self, transport: Arc<dyn SidecarRequestTransport>) {
171        self.transport = Some(transport);
172    }
173
174    pub(crate) fn invoke(
175        &self,
176        ownership: crate::protocol::OwnershipScope,
177        payload: SidecarRequestPayload,
178        timeout: Duration,
179    ) -> Result<SidecarResponsePayload, SidecarError> {
180        let transport = self.transport.as_ref().ok_or_else(|| {
181            SidecarError::Unsupported(String::from("sidecar request transport is not configured"))
182        })?;
183        let request_id = self.next_request_id.fetch_sub(1, Ordering::Relaxed);
184        let request = SidecarRequestFrame::new(request_id, ownership.clone(), payload);
185        let response = transport.send_request(request, timeout)?;
186        if response.request_id != request_id {
187            return Err(SidecarError::InvalidState(format!(
188                "sidecar response {} did not match request {request_id}",
189                response.request_id
190            )));
191        }
192        if response.ownership != ownership {
193            return Err(SidecarError::InvalidState(String::from(
194                "sidecar response ownership did not match request ownership",
195            )));
196        }
197        Ok(response.payload)
198    }
199}
200
201/// Fire-and-forget live event sink. Lets an extension emit a `session/update`
202/// (or any other) event frame to the host *mid-dispatch*, instead of having to
203/// return it from the dispatch and wait for the whole request to resolve before
204/// the stdio loop flushes it. Mirrors `SidecarRequestTransport`, but events have
205/// no response, no request id, and no timeout — they are written to the same
206/// outbound stdout channel the batch path uses.
207pub trait EventSinkTransport: Send + Sync {
208    fn emit_event(&self, event: crate::wire::EventFrame) -> Result<(), SidecarError>;
209}
210
211#[derive(Clone, Default)]
212pub(crate) struct SharedEventSink {
213    transport: Option<Arc<dyn EventSinkTransport>>,
214}
215
216impl SharedEventSink {
217    pub(crate) fn set_transport(&mut self, transport: Arc<dyn EventSinkTransport>) {
218        self.transport = Some(transport);
219    }
220
221    /// Emit `event` live if a transport is configured (the stdio path). Returns
222    /// `Ok(None)` when the event was handed to the live transport, or
223    /// `Ok(Some(event))` when no transport is configured (e.g. an in-process
224    /// `NativeSidecar` with no stdout loop) so the caller can fall back to the
225    /// batch path and still deliver the event when the dispatch resolves.
226    pub(crate) fn try_emit(
227        &self,
228        event: crate::wire::EventFrame,
229    ) -> Result<Option<crate::wire::EventFrame>, SidecarError> {
230        match self.transport.as_ref() {
231            Some(transport) => {
232                transport.emit_event(event)?;
233                Ok(None)
234            }
235            None => Ok(Some(event)),
236        }
237    }
238}
239
240// ---------------------------------------------------------------------------
241// Bridge wrapper
242// ---------------------------------------------------------------------------
243
244pub(crate) struct SharedBridge<B> {
245    pub(crate) inner: Arc<Mutex<B>>,
246    pub(crate) permissions: Arc<Mutex<BTreeMap<String, PermissionsPolicy>>>,
247    #[cfg(test)]
248    pub(crate) set_vm_permissions_outcomes: Arc<Mutex<VecDeque<Option<SidecarError>>>>,
249}
250
251impl<B> Clone for SharedBridge<B> {
252    fn clone(&self) -> Self {
253        Self {
254            inner: Arc::clone(&self.inner),
255            permissions: Arc::clone(&self.permissions),
256            #[cfg(test)]
257            set_vm_permissions_outcomes: Arc::clone(&self.set_vm_permissions_outcomes),
258        }
259    }
260}
261
262// ---------------------------------------------------------------------------
263// Connection / session / VM state
264// ---------------------------------------------------------------------------
265
266#[allow(dead_code)]
267#[derive(Debug)]
268pub(crate) struct ConnectionState {
269    pub(crate) auth_token: String,
270    pub(crate) sessions: BTreeSet<String>,
271}
272
273#[allow(dead_code)]
274#[derive(Debug)]
275pub(crate) struct SessionState {
276    pub(crate) connection_id: String,
277    pub(crate) placement: crate::protocol::SidecarPlacement,
278    pub(crate) metadata: BTreeMap<String, String>,
279    pub(crate) vm_ids: BTreeSet<String>,
280}
281
282#[allow(dead_code)]
283#[derive(Debug, Default, Clone)]
284pub(crate) struct VmConfiguration {
285    pub(crate) mounts: Vec<MountDescriptor>,
286    pub(crate) software: Vec<SoftwareDescriptor>,
287    pub(crate) permissions: PermissionsPolicy,
288    pub(crate) module_access_cwd: Option<String>,
289    pub(crate) instructions: Vec<String>,
290    pub(crate) projected_modules: Vec<ProjectedModuleDescriptor>,
291    pub(crate) command_permissions: BTreeMap<String, WasmPermissionTier>,
292    /// Guest JavaScript host-environment config (platform / module resolution /
293    /// builtin allow-list). Set at `create_vm` from `CreateVmConfig.jsRuntime`
294    /// and preserved across `configure_vm`. `None` => full Node.js emulation.
295    pub(crate) js_runtime: Option<vm_config::JsRuntimeConfig>,
296    pub(crate) loopback_exempt_ports: Vec<u16>,
297}
298
299#[allow(dead_code)]
300pub(crate) struct VmLayerStore {
301    pub(crate) next_layer_id: u64,
302    pub(crate) layers: BTreeMap<String, VmLayer>,
303}
304
305impl Default for VmLayerStore {
306    fn default() -> Self {
307        Self {
308            next_layer_id: 1,
309            layers: BTreeMap::new(),
310        }
311    }
312}
313
314#[allow(dead_code)]
315#[derive(Debug)]
316pub(crate) enum VmLayer {
317    Writable(RootFileSystem),
318    Snapshot(RootFilesystemSnapshot),
319    Overlay(VmOverlayLayer),
320}
321
322#[allow(dead_code)]
323#[derive(Debug, Clone)]
324pub(crate) struct VmOverlayLayer {
325    pub(crate) mode: RootFilesystemMode,
326    pub(crate) upper_layer_id: Option<String>,
327    pub(crate) lower_layer_ids: Vec<String>,
328}
329
330#[allow(dead_code)]
331pub(crate) struct VmState {
332    pub(crate) connection_id: String,
333    pub(crate) session_id: String,
334    /// Operator-tunable VM-scoped runtime limits. Immutable for the VM's lifetime;
335    /// `ConfigureVm` does not mutate limits.
336    pub(crate) limits: crate::limits::VmLimits,
337    pub(crate) dns: VmDnsConfig,
338    pub(crate) listen_policy: VmListenPolicy,
339    pub(crate) create_loopback_exempt_ports: BTreeSet<u16>,
340    pub(crate) guest_env: BTreeMap<String, String>,
341    pub(crate) requested_runtime: GuestRuntimeKind,
342    pub(crate) root_filesystem_mode: RootFilesystemMode,
343    pub(crate) guest_cwd: String,
344    pub(crate) cwd: PathBuf,
345    pub(crate) host_cwd: PathBuf,
346    pub(crate) kernel: SidecarKernel,
347    pub(crate) loaded_snapshot: Option<FilesystemSnapshot>,
348    pub(crate) configuration: VmConfiguration,
349    pub(crate) layers: VmLayerStore,
350    pub(crate) command_guest_paths: BTreeMap<String, String>,
351    pub(crate) command_permissions: BTreeMap<String, WasmPermissionTier>,
352    pub(crate) toolkits: BTreeMap<String, RegisterHostCallbacksRequest>,
353    pub(crate) active_processes: BTreeMap<String, ActiveProcess>,
354    pub(crate) exited_process_snapshots: VecDeque<ExitedProcessSnapshot>,
355    pub(crate) detached_child_processes: BTreeSet<String>,
356    pub(crate) signal_states: BTreeMap<String, BTreeMap<u32, SignalHandlerRegistration>>,
357}
358
359#[derive(Debug, Clone)]
360pub(crate) struct ExitedProcessSnapshot {
361    pub(crate) captured_at: Instant,
362    pub(crate) process: crate::protocol::ProcessSnapshotEntry,
363}
364
365// ---------------------------------------------------------------------------
366// DNS configuration
367// ---------------------------------------------------------------------------
368
369#[derive(Debug, Clone, Default)]
370pub(crate) struct VmDnsConfig {
371    pub(crate) name_servers: Vec<SocketAddr>,
372    pub(crate) overrides: BTreeMap<String, Vec<IpAddr>>,
373}
374
375#[derive(Debug, Clone)]
376pub(crate) struct JavascriptSocketPathContext {
377    pub(crate) sandbox_root: PathBuf,
378    pub(crate) mounts: Vec<MountDescriptor>,
379    pub(crate) listen_policy: VmListenPolicy,
380    pub(crate) loopback_exempt_ports: BTreeSet<u16>,
381    pub(crate) tcp_loopback_guest_to_host_ports: BTreeMap<(JavascriptSocketFamily, u16), u16>,
382    pub(crate) http_loopback_targets:
383        BTreeMap<(JavascriptSocketFamily, u16), JavascriptHttpLoopbackTarget>,
384    pub(crate) udp_loopback_guest_to_host_ports: BTreeMap<(JavascriptSocketFamily, u16), u16>,
385    pub(crate) udp_loopback_host_to_guest_ports: BTreeMap<(JavascriptSocketFamily, u16), u16>,
386    pub(crate) used_tcp_guest_ports: BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
387    pub(crate) used_udp_guest_ports: BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
388}
389
390#[derive(Debug, Clone)]
391pub(crate) struct JavascriptHttpLoopbackTarget {
392    pub(crate) process_id: String,
393    pub(crate) server_id: u64,
394}
395
396#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
397pub(crate) enum JavascriptSocketFamily {
398    Ipv4,
399    Ipv6,
400}
401
402impl JavascriptSocketFamily {
403    pub(crate) fn from_ip(ip: IpAddr) -> Self {
404        match ip {
405            IpAddr::V4(_) => Self::Ipv4,
406            IpAddr::V6(_) => Self::Ipv6,
407        }
408    }
409}
410
411impl From<JavascriptUdpFamily> for JavascriptSocketFamily {
412    fn from(value: JavascriptUdpFamily) -> Self {
413        match value {
414            JavascriptUdpFamily::Ipv4 => Self::Ipv4,
415            JavascriptUdpFamily::Ipv6 => Self::Ipv6,
416        }
417    }
418}
419
420#[derive(Debug, Clone, Copy)]
421pub(crate) struct VmListenPolicy {
422    pub(crate) port_min: u16,
423    pub(crate) port_max: u16,
424    pub(crate) allow_privileged: bool,
425}
426
427impl Default for VmListenPolicy {
428    fn default() -> Self {
429        Self {
430            port_min: 1,
431            port_max: u16::MAX,
432            allow_privileged: false,
433        }
434    }
435}
436
437// ---------------------------------------------------------------------------
438// Active process state
439// ---------------------------------------------------------------------------
440
441#[allow(dead_code)]
442pub(crate) struct ActiveProcess {
443    pub(crate) kernel_pid: u32,
444    pub(crate) kernel_handle: KernelProcessHandle,
445    pub(crate) kernel_stdin_writer_fd: Option<u32>,
446    pub(crate) runtime: GuestRuntimeKind,
447    pub(crate) detached: bool,
448    pub(crate) execution: ActiveExecution,
449    pub(crate) guest_cwd: String,
450    pub(crate) env: BTreeMap<String, String>,
451    pub(crate) host_cwd: PathBuf,
452    pub(crate) mapped_host_fds: BTreeMap<u32, ActiveMappedHostFd>,
453    pub(crate) next_mapped_host_fd: u32,
454    pub(crate) pending_execution_events: VecDeque<ActiveExecutionEvent>,
455    pub(crate) pending_self_signal_exit: Option<i32>,
456    pub(crate) child_processes: BTreeMap<String, ActiveProcess>,
457    pub(crate) next_child_process_id: usize,
458    pub(crate) http_servers: BTreeMap<u64, ActiveHttpServer>,
459    pub(crate) pending_http_requests: BTreeMap<(u64, u64), Option<String>>,
460    pub(crate) http2: ActiveHttp2State,
461    pub(crate) tcp_listeners: BTreeMap<String, ActiveTcpListener>,
462    pub(crate) next_tcp_listener_id: usize,
463    pub(crate) tcp_sockets: BTreeMap<String, ActiveTcpSocket>,
464    pub(crate) next_tcp_socket_id: usize,
465    pub(crate) tcp_port_reservations: BTreeMap<String, (JavascriptSocketFamily, u16)>,
466    pub(crate) next_tcp_port_reservation_id: usize,
467    pub(crate) unix_listeners: BTreeMap<String, ActiveUnixListener>,
468    pub(crate) next_unix_listener_id: usize,
469    pub(crate) unix_sockets: BTreeMap<String, ActiveUnixSocket>,
470    pub(crate) next_unix_socket_id: usize,
471    pub(crate) udp_sockets: BTreeMap<String, ActiveUdpSocket>,
472    pub(crate) next_udp_socket_id: usize,
473    pub(crate) cipher_sessions: BTreeMap<u64, ActiveCipherSession>,
474    pub(crate) next_cipher_session_id: u64,
475    pub(crate) diffie_hellman_sessions: BTreeMap<u64, ActiveDiffieHellmanSession>,
476    pub(crate) next_diffie_hellman_session_id: u64,
477    pub(crate) sqlite_databases: BTreeMap<u64, ActiveSqliteDatabase>,
478    pub(crate) next_sqlite_database_id: u64,
479    pub(crate) sqlite_statements: BTreeMap<u64, ActiveSqliteStatement>,
480    pub(crate) next_sqlite_statement_id: u64,
481    /// Per-process module resolution cache, persisted across module sync-RPCs
482    /// (`__resolve_module` / `__load_file` / `__module_format` /
483    /// `__batch_resolve_modules`) for the lifetime of this process so cold-start
484    /// resolution does not rebuild it on every dispatch. The resolver reads the
485    /// kernel VFS; the node_modules tree is mounted read-only, so cached
486    /// stat/exists/package.json results under it stay valid for the process run.
487    pub(crate) module_resolution_cache: secure_exec_execution::LocalModuleResolutionCache,
488}
489
490pub(crate) struct ActiveMappedHostFd {
491    pub(crate) file: File,
492    pub(crate) path: PathBuf,
493}
494
495pub(crate) struct ActiveCipherSession {
496    pub(crate) algorithm: String,
497    pub(crate) auth_tag_len: usize,
498    pub(crate) context: openssl::symm::Crypter,
499}
500
501pub(crate) struct ActiveSqliteDatabase {
502    pub(crate) connection: Connection,
503    pub(crate) host_path: Option<PathBuf>,
504    pub(crate) vm_path: Option<String>,
505    pub(crate) dirty: bool,
506    pub(crate) transaction_depth: usize,
507    pub(crate) read_only: bool,
508}
509
510#[derive(Clone)]
511pub(crate) struct ActiveSqliteStatement {
512    pub(crate) database_id: u64,
513    pub(crate) sql: String,
514    pub(crate) return_arrays: bool,
515    pub(crate) read_bigints: bool,
516    pub(crate) allow_bare_named_parameters: bool,
517    pub(crate) allow_unknown_named_parameters: bool,
518}
519
520pub(crate) enum ActiveDiffieHellmanSession {
521    Dh(ActiveDhSession),
522    Ecdh(ActiveEcdhSession),
523}
524
525pub(crate) struct ActiveDhSession {
526    pub(crate) params: openssl::dh::Dh<openssl::pkey::Params>,
527    pub(crate) key_pair: Option<openssl::dh::Dh<openssl::pkey::Private>>,
528}
529
530pub(crate) struct ActiveEcdhSession {
531    pub(crate) curve: String,
532    pub(crate) key_pair: Option<openssl::ec::EcKey<openssl::pkey::Private>>,
533}
534
535#[derive(Debug, Clone, Copy, Default)]
536pub(crate) struct NetworkResourceCounts {
537    pub(crate) sockets: usize,
538    pub(crate) connections: usize,
539}
540
541#[derive(Debug)]
542pub(crate) struct ActiveHttpServer {
543    pub(crate) listener: TcpListener,
544    pub(crate) guest_local_addr: SocketAddr,
545    pub(crate) next_request_id: u64,
546}
547
548#[derive(Clone, Default)]
549pub(crate) struct ActiveHttp2State {
550    pub(crate) shared: Arc<Mutex<Http2SharedState>>,
551}
552
553#[derive(Default)]
554pub(crate) struct Http2SharedState {
555    pub(crate) next_session_id: u64,
556    pub(crate) next_stream_id: u64,
557    pub(crate) servers: BTreeMap<u64, ActiveHttp2Server>,
558    pub(crate) sessions: BTreeMap<u64, ActiveHttp2Session>,
559    pub(crate) streams: BTreeMap<u64, ActiveHttp2Stream>,
560    pub(crate) server_events: BTreeMap<u64, VecDeque<Http2BridgeEvent>>,
561    pub(crate) session_events: BTreeMap<u64, VecDeque<Http2BridgeEvent>>,
562}
563
564#[derive(Debug)]
565pub(crate) struct ActiveHttp2Server {
566    pub(crate) actual_local_addr: SocketAddr,
567    pub(crate) guest_local_addr: SocketAddr,
568    pub(crate) secure: bool,
569    pub(crate) tls: Option<JavascriptTlsBridgeOptions>,
570    pub(crate) closed: Arc<AtomicBool>,
571}
572
573#[derive(Debug, Clone)]
574pub(crate) struct ActiveHttp2Session {
575    pub(crate) command_tx: UnboundedSender<Http2SessionCommand>,
576}
577
578#[derive(Debug, Clone)]
579pub(crate) struct ActiveHttp2Stream {
580    pub(crate) session_id: u64,
581    pub(crate) paused: Arc<AtomicBool>,
582}
583
584#[derive(Debug, Clone, Default, Serialize, Deserialize)]
585#[serde(default, rename_all = "camelCase")]
586pub(crate) struct Http2SocketSnapshot {
587    pub(crate) encrypted: bool,
588    pub(crate) allow_half_open: bool,
589    pub(crate) local_address: Option<String>,
590    pub(crate) local_port: Option<u16>,
591    pub(crate) local_family: Option<String>,
592    pub(crate) remote_address: Option<String>,
593    pub(crate) remote_port: Option<u16>,
594    pub(crate) remote_family: Option<String>,
595    pub(crate) servername: Option<String>,
596    pub(crate) alpn_protocol: Option<String>,
597}
598
599#[derive(Debug, Clone, Default, Serialize, Deserialize)]
600#[serde(default, rename_all = "camelCase")]
601pub(crate) struct Http2RuntimeSnapshot {
602    pub(crate) effective_local_window_size: u32,
603    pub(crate) local_window_size: u32,
604    pub(crate) remote_window_size: u32,
605    pub(crate) next_stream_id: u32,
606    pub(crate) outbound_queue_size: u32,
607    pub(crate) deflate_dynamic_table_size: u32,
608    pub(crate) inflate_dynamic_table_size: u32,
609}
610
611#[derive(Debug, Clone, Default, Serialize, Deserialize)]
612#[serde(default, rename_all = "camelCase")]
613pub(crate) struct Http2SessionSnapshot {
614    pub(crate) encrypted: bool,
615    pub(crate) alpn_protocol: Option<String>,
616    pub(crate) origin_set: Vec<String>,
617    pub(crate) local_settings: BTreeMap<String, Value>,
618    pub(crate) remote_settings: BTreeMap<String, Value>,
619    pub(crate) state: Http2RuntimeSnapshot,
620    pub(crate) socket: Http2SocketSnapshot,
621}
622
623#[derive(Debug, Clone, Default, Serialize, Deserialize)]
624#[serde(default, rename_all = "camelCase")]
625pub(crate) struct Http2BridgeEvent {
626    pub(crate) kind: String,
627    pub(crate) id: u64,
628    #[serde(skip_serializing_if = "Option::is_none")]
629    pub(crate) data: Option<String>,
630    #[serde(skip_serializing_if = "Option::is_none")]
631    pub(crate) extra: Option<String>,
632    #[serde(skip_serializing_if = "Option::is_none")]
633    pub(crate) extra_number: Option<u64>,
634    #[serde(skip_serializing_if = "Option::is_none")]
635    pub(crate) extra_headers: Option<String>,
636    #[serde(skip_serializing_if = "Option::is_none")]
637    pub(crate) flags: Option<u64>,
638}
639
640pub(crate) enum Http2SessionCommand {
641    Request {
642        headers_json: String,
643        options_json: String,
644        respond_to: Sender<Result<Value, String>>,
645    },
646    Settings {
647        settings_json: String,
648        respond_to: Sender<Result<Value, String>>,
649    },
650    SetLocalWindowSize {
651        size: u32,
652        respond_to: Sender<Result<Value, String>>,
653    },
654    Goaway {
655        error_code: u32,
656        last_stream_id: u32,
657        opaque_data: Option<Vec<u8>>,
658        respond_to: Sender<Result<Value, String>>,
659    },
660    Close {
661        abrupt: bool,
662        respond_to: Sender<Result<Value, String>>,
663    },
664    StreamRespond {
665        stream_id: u64,
666        headers_json: String,
667        respond_to: Sender<Result<Value, String>>,
668    },
669    StreamPush {
670        stream_id: u64,
671        headers_json: String,
672        respond_to: Sender<Result<Value, String>>,
673    },
674    StreamWrite {
675        stream_id: u64,
676        chunk: Vec<u8>,
677        end_stream: bool,
678        respond_to: Sender<Result<Value, String>>,
679    },
680    StreamClose {
681        stream_id: u64,
682        error_code: Option<u32>,
683        respond_to: Sender<Result<Value, String>>,
684    },
685    StreamRespondWithFile {
686        stream_id: u64,
687        body: Vec<u8>,
688        headers_json: String,
689        options_json: String,
690        respond_to: Sender<Result<Value, String>>,
691    },
692}
693
694// ---------------------------------------------------------------------------
695// TCP types
696// ---------------------------------------------------------------------------
697
698#[derive(Debug)]
699pub(crate) enum JavascriptTcpListenerEvent {
700    Connection(PendingTcpSocket),
701    Error {
702        code: Option<String>,
703        message: String,
704    },
705}
706
707#[derive(Debug)]
708pub(crate) struct PendingTcpSocket {
709    pub(crate) stream: Option<TcpStream>,
710    pub(crate) kernel_socket_id: Option<SocketId>,
711    pub(crate) preallocated: bool,
712    pub(crate) guest_local_addr: SocketAddr,
713    pub(crate) guest_remote_addr: SocketAddr,
714}
715
716#[derive(Debug)]
717pub(crate) enum JavascriptTcpSocketEvent {
718    Data(Vec<u8>),
719    End,
720    Close {
721        had_error: bool,
722    },
723    Error {
724        code: Option<String>,
725        message: String,
726    },
727}
728
729#[derive(Debug)]
730pub(crate) struct ActiveTcpSocket {
731    pub(crate) stream: Option<Arc<Mutex<TcpStream>>>,
732    pub(crate) pending_read_stream: Option<Arc<Mutex<Option<TcpStream>>>>,
733    pub(crate) events: Option<Receiver<JavascriptTcpSocketEvent>>,
734    pub(crate) event_sender: Option<Sender<JavascriptTcpSocketEvent>>,
735    pub(crate) kernel_socket_id: Option<SocketId>,
736    pub(crate) no_delay: bool,
737    pub(crate) keep_alive: bool,
738    pub(crate) keep_alive_initial_delay_secs: Option<u64>,
739    pub(crate) guest_local_addr: SocketAddr,
740    pub(crate) guest_remote_addr: SocketAddr,
741    pub(crate) listener_id: Option<String>,
742    pub(crate) tls_mode: Arc<AtomicBool>,
743    pub(crate) tls_stream: Arc<Mutex<Option<ActiveTlsStream>>>,
744    pub(crate) tls_state: Arc<Mutex<Option<ActiveTlsState>>>,
745    pub(crate) saw_local_shutdown: Arc<AtomicBool>,
746    pub(crate) saw_remote_end: Arc<AtomicBool>,
747    pub(crate) close_notified: Arc<AtomicBool>,
748}
749
750pub(crate) struct LoopbackTlsTransportPair {
751    pub(crate) state: Mutex<LoopbackTlsTransportPairState>,
752    pub(crate) ready: Condvar,
753}
754
755#[derive(Debug, Default)]
756pub(crate) struct LoopbackTlsTransportPairState {
757    pub(crate) lower_to_higher: VecDeque<u8>,
758    pub(crate) higher_to_lower: VecDeque<u8>,
759    pub(crate) lower_write_closed: bool,
760    pub(crate) higher_write_closed: bool,
761    pub(crate) lower_closed: bool,
762    pub(crate) higher_closed: bool,
763}
764
765pub(crate) struct LoopbackTlsEndpoint {
766    pub(crate) pair: Arc<LoopbackTlsTransportPair>,
767    pub(crate) is_lower_socket: bool,
768    /// Registry key (`vm_id:lower:higher`) under which this endpoint's transport
769    /// pair is registered in the loopback-TLS transport registry. Stored so the
770    /// endpoint's `Drop` can eagerly prune its own registry entry once it is the
771    /// last owner of the pair, instead of leaking a dead `Weak` entry until the
772    /// next lazy `retain()` in `loopback_tls_endpoint()`. `None` means the
773    /// endpoint was not registered (e.g. test-constructed) and Drop skips pruning.
774    pub(crate) registry_key: Option<String>,
775}
776
777impl fmt::Debug for LoopbackTlsEndpoint {
778    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
779        f.debug_struct("LoopbackTlsEndpoint")
780            .field("is_lower_socket", &self.is_lower_socket)
781            .finish()
782    }
783}
784
785#[derive(Debug)]
786pub(crate) enum ActiveTlsStream {
787    Client(StreamOwned<ClientConnection, TcpStream>),
788    Server(StreamOwned<ServerConnection, TcpStream>),
789    LoopbackClient(StreamOwned<ClientConnection, LoopbackTlsEndpoint>),
790    LoopbackServer(StreamOwned<ServerConnection, LoopbackTlsEndpoint>),
791}
792
793#[derive(Debug, Clone, Default, Serialize, Deserialize)]
794#[serde(default, rename_all = "camelCase")]
795pub(crate) struct JavascriptTlsClientHello {
796    #[serde(skip_serializing_if = "Option::is_none")]
797    pub(crate) servername: Option<String>,
798    #[serde(
799        rename = "ALPNProtocols",
800        alias = "ALPNProtocols",
801        skip_serializing_if = "Option::is_none"
802    )]
803    pub(crate) alpn_protocols: Option<Vec<String>>,
804}
805
806#[derive(Debug, Clone, Default, Deserialize)]
807#[serde(default, rename_all = "camelCase")]
808pub(crate) struct JavascriptTlsBridgeOptions {
809    pub(crate) is_server: bool,
810    pub(crate) servername: Option<String>,
811    pub(crate) reject_unauthorized: Option<bool>,
812    pub(crate) request_cert: Option<bool>,
813    pub(crate) session: Option<String>,
814    pub(crate) key: Option<JavascriptTlsMaterial>,
815    pub(crate) cert: Option<JavascriptTlsMaterial>,
816    pub(crate) ca: Option<JavascriptTlsMaterial>,
817    pub(crate) passphrase: Option<String>,
818    pub(crate) ciphers: Option<String>,
819    #[serde(alias = "ALPNProtocols")]
820    pub(crate) alpn_protocols: Option<Vec<String>>,
821    pub(crate) min_version: Option<String>,
822    pub(crate) max_version: Option<String>,
823}
824
825#[derive(Debug, Clone, Deserialize)]
826#[serde(untagged)]
827pub(crate) enum JavascriptTlsMaterial {
828    Single(JavascriptTlsDataValue),
829    Many(Vec<JavascriptTlsDataValue>),
830}
831
832#[derive(Debug, Clone, Deserialize)]
833#[serde(tag = "kind", rename_all = "camelCase")]
834pub(crate) enum JavascriptTlsDataValue {
835    Buffer { data: String },
836    String { data: String },
837}
838
839#[derive(Debug, Clone, Default)]
840pub(crate) struct ActiveTlsState {
841    pub(crate) client_hello: Option<JavascriptTlsClientHello>,
842    pub(crate) local_certificates: Vec<Vec<u8>>,
843    pub(crate) session_reused: bool,
844}
845
846#[derive(Debug, Clone, Copy)]
847pub(crate) struct ResolvedTcpConnectAddr {
848    pub(crate) actual_addr: SocketAddr,
849    pub(crate) guest_remote_addr: SocketAddr,
850    pub(crate) use_kernel_loopback: bool,
851}
852
853#[derive(Debug)]
854pub(crate) struct ActiveTcpListener {
855    pub(crate) listener: Option<TcpListener>,
856    pub(crate) kernel_socket_id: Option<SocketId>,
857    pub(crate) local_addr: Option<SocketAddr>,
858    pub(crate) guest_local_addr: SocketAddr,
859    pub(crate) backlog: usize,
860    pub(crate) active_connection_ids: BTreeSet<String>,
861}
862
863// ---------------------------------------------------------------------------
864// Unix socket types
865// ---------------------------------------------------------------------------
866
867#[derive(Debug)]
868pub(crate) enum JavascriptUnixListenerEvent {
869    Connection(PendingUnixSocket),
870    Error {
871        code: Option<String>,
872        message: String,
873    },
874}
875
876#[derive(Debug)]
877pub(crate) struct PendingUnixSocket {
878    pub(crate) stream: UnixStream,
879    pub(crate) local_path: Option<String>,
880    pub(crate) remote_path: Option<String>,
881}
882
883#[derive(Debug)]
884pub(crate) struct ActiveUnixSocket {
885    pub(crate) stream: Arc<Mutex<UnixStream>>,
886    pub(crate) events: Receiver<JavascriptTcpSocketEvent>,
887    pub(crate) event_sender: Sender<JavascriptTcpSocketEvent>,
888    pub(crate) listener_id: Option<String>,
889    pub(crate) local_path: Option<String>,
890    pub(crate) remote_path: Option<String>,
891    pub(crate) saw_local_shutdown: Arc<AtomicBool>,
892    pub(crate) saw_remote_end: Arc<AtomicBool>,
893    pub(crate) close_notified: Arc<AtomicBool>,
894}
895
896#[derive(Debug)]
897pub(crate) struct ActiveUnixListener {
898    pub(crate) listener: UnixListener,
899    pub(crate) path: String,
900    pub(crate) backlog: usize,
901    pub(crate) active_connection_ids: BTreeSet<String>,
902}
903
904// ---------------------------------------------------------------------------
905// UDP types
906// ---------------------------------------------------------------------------
907
908#[derive(Debug, Clone, Copy, PartialEq, Eq)]
909pub(crate) enum JavascriptUdpFamily {
910    Ipv4,
911    Ipv6,
912}
913
914impl JavascriptUdpFamily {
915    pub(crate) fn from_socket_type(value: &str) -> Result<Self, SidecarError> {
916        match value {
917            "udp4" => Ok(Self::Ipv4),
918            "udp6" => Ok(Self::Ipv6),
919            other => Err(SidecarError::InvalidState(format!(
920                "unsupported dgram socket type {other}"
921            ))),
922        }
923    }
924
925    pub(crate) fn socket_type(self) -> &'static str {
926        match self {
927            Self::Ipv4 => "udp4",
928            Self::Ipv6 => "udp6",
929        }
930    }
931
932    pub(crate) fn matches_addr(self, addr: &SocketAddr) -> bool {
933        matches!(
934            (self, addr),
935            (Self::Ipv4, SocketAddr::V4(_)) | (Self::Ipv6, SocketAddr::V6(_))
936        )
937    }
938}
939
940#[derive(Debug)]
941pub(crate) enum JavascriptUdpSocketEvent {
942    Message {
943        data: Vec<u8>,
944        remote_addr: SocketAddr,
945    },
946    Error {
947        code: Option<String>,
948        message: String,
949    },
950}
951
952#[derive(Debug)]
953pub(crate) struct ActiveUdpSocket {
954    pub(crate) family: JavascriptUdpFamily,
955    pub(crate) socket: Option<UdpSocket>,
956    pub(crate) kernel_socket_id: Option<SocketId>,
957    pub(crate) guest_local_addr: Option<SocketAddr>,
958    pub(crate) recv_buffer_size: usize,
959    pub(crate) send_buffer_size: usize,
960}
961
962// ---------------------------------------------------------------------------
963// Execution types
964// ---------------------------------------------------------------------------
965
966#[derive(Debug)]
967pub(crate) enum ActiveExecution {
968    Javascript(JavascriptExecution),
969    Python(PythonExecution),
970    Wasm(Box<WasmExecution>),
971    Tool(ToolExecution),
972}
973
974#[derive(Debug, Clone)]
975pub(crate) struct ToolExecution {
976    pub(crate) cancelled: Arc<AtomicBool>,
977    pub(crate) pending_events: Arc<Mutex<VecDeque<ActiveExecutionEvent>>>,
978    pub(crate) events_overflowed: Arc<AtomicBool>,
979}
980
981impl Default for ToolExecution {
982    fn default() -> Self {
983        Self {
984            cancelled: Arc::new(AtomicBool::new(false)),
985            pending_events: Arc::new(Mutex::new(VecDeque::new())),
986            events_overflowed: Arc::new(AtomicBool::new(false)),
987        }
988    }
989}
990
991#[derive(Debug)]
992pub(crate) enum ActiveExecutionEvent {
993    Stdout(Vec<u8>),
994    Stderr(Vec<u8>),
995    JavascriptSyncRpcRequest(JavascriptSyncRpcRequest),
996    PythonVfsRpcRequest(Box<PythonVfsRpcRequest>),
997    SignalState {
998        signal: u32,
999        registration: SignalHandlerRegistration,
1000    },
1001    Exited(i32),
1002}
1003
1004#[derive(Debug)]
1005pub(crate) struct ProcessEventEnvelope {
1006    pub(crate) connection_id: String,
1007    pub(crate) session_id: String,
1008    pub(crate) vm_id: String,
1009    pub(crate) process_id: String,
1010    pub(crate) event: ActiveExecutionEvent,
1011}
1012
1013#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1014pub(crate) enum SocketQueryKind {
1015    TcpListener,
1016    UdpBound,
1017}
1018
1019// ---------------------------------------------------------------------------
1020// Command resolution
1021// ---------------------------------------------------------------------------
1022
1023#[derive(Debug)]
1024pub(crate) struct ResolvedChildProcessExecution {
1025    pub(crate) command: String,
1026    pub(crate) process_args: Vec<String>,
1027    pub(crate) runtime: GuestRuntimeKind,
1028    pub(crate) entrypoint: String,
1029    pub(crate) execution_args: Vec<String>,
1030    pub(crate) env: BTreeMap<String, String>,
1031    pub(crate) guest_cwd: String,
1032    pub(crate) host_cwd: PathBuf,
1033    pub(crate) wasm_permission_tier: Option<WasmPermissionTier>,
1034    pub(crate) tool_command: bool,
1035}
1036
1037// ---------------------------------------------------------------------------
1038// Utility types
1039// ---------------------------------------------------------------------------
1040
1041#[derive(Debug)]
1042pub(crate) struct ProcNetEntry {
1043    pub(crate) local_host: String,
1044    pub(crate) local_port: u16,
1045    pub(crate) state: String,
1046    pub(crate) inode: u64,
1047}