Skip to main content

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