1use crate::bridge::{build_mount_plugin_registry, MountPluginContext};
2pub(crate) use crate::execution::{
3 build_javascript_socket_path_context, canonical_signal_name, dispatch_loopback_http_request,
4 error_code, format_tcp_resource, ignore_stale_javascript_sync_rpc_response,
5 javascript_sync_rpc_arg_i32, javascript_sync_rpc_arg_str, javascript_sync_rpc_arg_u32,
6 javascript_sync_rpc_arg_u32_optional, javascript_sync_rpc_arg_u64,
7 javascript_sync_rpc_arg_u64_optional, javascript_sync_rpc_bytes_arg,
8 javascript_sync_rpc_bytes_value, javascript_sync_rpc_encoding, javascript_sync_rpc_error_code,
9 javascript_sync_rpc_option_bool, javascript_sync_rpc_option_u32, kernel_poll_response,
10 kernel_stdin_read_response, mark_execute_exit_event_queued, parse_kernel_poll_args,
11 parse_kernel_stdin_read_args, parse_signal, record_execute_exit_event_queue_wait,
12 record_execute_phase, sanitize_javascript_child_process_internal_bootstrap_env,
13 service_javascript_sync_rpc, vm_network_resource_counts, JavascriptSyncRpcServiceRequest,
14 LoopbackHttpDispatchRequest,
15};
16use crate::extension::{
17 Extension, ExtensionBufferedProcessOutput, ExtensionContext, ExtensionFuture, ExtensionHost,
18 ExtensionSnapshot,
19};
20use crate::filesystem::guest_filesystem_call as filesystem_guest_filesystem_call;
21use crate::limits::DEFAULT_ACP_STDOUT_BUFFER_BYTE_LIMIT;
22use crate::protocol::{
23 CloseStdinRequest, DisposeReason, EventFrame, EventPayload, ExecuteRequest, ExtEnvelope,
24 GuestFilesystemCallRequest, GuestFilesystemResultResponse, JavascriptChildProcessSpawnOptions,
25 JavascriptChildProcessSpawnRequest, KillProcessRequest, OpenSessionRequest, OwnershipScope,
26 ProcessKilledResponse, ProcessStartedResponse, RequestFrame, RequestId, RequestPayload,
27 ResponseFrame, ResponsePayload, SidecarRequestFrame, SidecarRequestPayload,
28 SidecarResponseFrame, SidecarResponsePayload, SidecarResponseTracker,
29 SidecarResponseTrackerError, SignalDispositionAction, StdinClosedResponse,
30 StdinWrittenResponse, VmLifecycleState, WriteStdinRequest,
31};
32use crate::state::{
33 ActiveExecutionEvent, BridgeError, ConnectionState, EventSinkTransport, JavascriptSocketFamily,
34 JavascriptSocketPathContext, ProcessEventEnvelope, SessionState, SharedBridge, SharedEventSink,
35 SharedSidecarRequestClient, SidecarRequestTransport, VmState, EXECUTION_DRIVER_NAME,
36};
37use crate::tools::register_host_callbacks;
38use crate::NativeSidecarBridge;
39use agentos_bridge::queue_tracker::{register_queue, QueueGauge, TrackedLimit};
40use agentos_bridge::{
41 CommandPermissionRequest, EnvironmentAccess, EnvironmentPermissionRequest, FilesystemAccess,
42 FilesystemPermissionRequest, LifecycleEventRecord, LifecycleState, LogLevel, LogRecord,
43 NetworkAccess, NetworkPermissionRequest, StructuredEventRecord,
44};
45use agentos_execution::{
46 record_sync_bridge_request_observed, JavascriptExecutionEngine, JavascriptExecutionError,
47 JavascriptSyncRpcRequest, PythonExecutionEngine, PythonExecutionError, WasmExecutionEngine,
48 WasmExecutionError,
49};
50use agentos_kernel::kernel::KernelError;
51use agentos_kernel::mount_plugin::{FileSystemPluginRegistry, PluginError};
52use agentos_kernel::permissions::{
53 CommandAccessRequest, EnvAccessRequest, EnvironmentOperation, NetworkAccessRequest,
54 NetworkOperation, PermissionDecision,
55};
56use agentos_native_sidecar_core::permissions::{
57 deny_all_policy, environment_permission_capability, evaluate_permissions_policy,
58 filesystem_permission_capability, network_permission_capability,
59 permission_mode_to_kernel_decision,
60};
61use agentos_native_sidecar_core::{
62 apply_process_signal_state_update, authenticated_response as shared_authenticated_response,
63 parse_process_signal_state_request, reject as shared_reject, request_dispatch_mode,
64 respond as shared_respond, route_request_payload, session_opened_response,
65 unsupported_host_callback_direction_dispatch, validate_authenticate_versions,
66 vm_lifecycle_event as shared_vm_lifecycle_event, AuthenticateVersionError, RequestDispatchMode,
67 RequestRoute,
68};
69use agentos_vm_config::PermissionsPolicy;
70use agentos_kernel::vfs::VfsError;
72use serde::Deserialize;
73use serde_json::{json, Value};
74use std::collections::{BTreeMap, BTreeSet, VecDeque};
75use std::fmt;
76use std::fs;
77use std::os::unix::fs::PermissionsExt;
78use std::path::{Component, Path, PathBuf};
79use std::sync::{Arc, Mutex, OnceLock};
80use std::task::{Context, Poll, Waker};
81use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
82use tokio::sync::mpsc::{channel, Receiver, Sender};
83use tokio::time;
84
85const INTERNAL_JAVASCRIPT_ENTRYPOINT_ENV_KEYS: &[&str] =
88 &["AGENTOS_ENTRYPOINT", "AGENTOS_BOOTSTRAP_MODULE"];
89const INTERNAL_WASM_ENTRYPOINT_ENV_KEYS: &[&str] =
90 &["AGENTOS_WASM_MODULE_PATH", "AGENTOS_WASM_MODULE_BASE64"];
91const INTERNAL_PYTHON_ENTRYPOINT_ENV_PREFIXES: &[&str] = &["AGENTOS_PYTHON_"];
92pub(crate) const MAX_PROCESS_EVENT_QUEUE: usize = 10_000;
93pub(crate) const MAX_PENDING_SIDECAR_RESPONSES: usize = 10_000;
94pub(crate) const MAX_OUTBOUND_SIDECAR_REQUESTS: usize = 10_000;
95pub(crate) const MAX_COMPLETED_SIDECAR_RESPONSES: usize = 10_000;
96static BLOCKING_DISPATCH_RUNTIME: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
97
98fn blocking_dispatch_runtime() -> &'static tokio::runtime::Runtime {
99 BLOCKING_DISPATCH_RUNTIME.get_or_init(|| {
100 tokio::runtime::Builder::new_current_thread()
101 .enable_all()
102 .build()
103 .expect("sidecar blocking dispatch runtime")
104 })
105}
106
107pub(crate) fn process_event_queue_overflow_error() -> SidecarError {
108 SidecarError::InvalidState(format!(
109 "process event queue exceeded {MAX_PROCESS_EVENT_QUEUE} pending events"
110 ))
111}
112
113fn sidecar_response_pending_overflow_error() -> SidecarError {
114 SidecarError::InvalidState(format!(
115 "sidecar response tracker exceeded {MAX_PENDING_SIDECAR_RESPONSES} pending responses"
116 ))
117}
118
119fn outbound_sidecar_request_queue_overflow_error() -> SidecarError {
120 SidecarError::InvalidState(format!(
121 "outbound sidecar request queue exceeded {MAX_OUTBOUND_SIDECAR_REQUESTS} pending requests"
122 ))
123}
124
125fn wire_protocol_error(error: crate::wire::ProtocolCodecError) -> SidecarError {
126 SidecarError::InvalidState(format!("invalid generated wire protocol frame: {error}"))
127}
128
129fn wire_dispatch_result(
130 result: DispatchResult,
131) -> Result<crate::wire::WireDispatchResult, SidecarError> {
132 crate::wire::dispatch_result_from_compat(crate::wire::CompatDispatchResult {
133 response: result.response,
134 events: result.events,
135 })
136 .map_err(wire_protocol_error)
137}
138
139pub use agentos_native_sidecar_core::DispatchResult;
140pub use crate::state::{NativeSidecarConfig, SidecarError};
142
143#[derive(Debug, Default, Deserialize)]
146struct LegacyJavascriptChildProcessSpawnOptions {
147 #[serde(default)]
148 cwd: Option<String>,
149 #[serde(default)]
150 env: BTreeMap<String, String>,
151 #[serde(default)]
152 input: Option<Value>,
153 #[serde(default)]
154 shell: bool,
155 #[serde(default)]
156 detached: bool,
157 #[serde(default)]
158 stdio: Vec<String>,
159 #[serde(default, rename = "maxBuffer")]
160 max_buffer: Option<usize>,
161 #[serde(default)]
162 timeout: Option<u64>,
163 #[serde(default, rename = "killSignal")]
164 kill_signal: Option<String>,
165}
166
167#[derive(Debug, Deserialize)]
168#[serde(rename_all = "camelCase")]
169struct JavascriptHttpLoopbackRequest {
170 process_id: String,
171 server_id: u64,
172 host: String,
173 port: u16,
174 request: String,
175}
176
177fn is_javascript_loopback_host(host: &str) -> bool {
178 host == "127.0.0.1" || host == "::1" || host.eq_ignore_ascii_case("localhost")
179}
180
181pub(crate) fn parse_javascript_child_process_spawn_request(
182 vm: &VmState,
183 args: &[Value],
184) -> Result<(JavascriptChildProcessSpawnRequest, Option<usize>), SidecarError> {
185 if let Some(value) = args.first().cloned() {
186 if let Ok(request) = serde_json::from_value::<JavascriptChildProcessSpawnRequest>(value) {
187 return Ok((request, None));
188 }
189 }
190
191 let command = javascript_sync_rpc_arg_str(args, 0, "child_process.spawn command")?.to_owned();
192 let raw_args = javascript_sync_rpc_arg_str(args, 1, "child_process.spawn args")?;
193 let raw_options = javascript_sync_rpc_arg_str(args, 2, "child_process.spawn options")?;
194
195 let parsed_args = serde_json::from_str::<Vec<String>>(raw_args).map_err(|error| {
196 SidecarError::InvalidState(format!("invalid child_process.spawn args payload: {error}"))
197 })?;
198 let parsed_options = serde_json::from_str::<LegacyJavascriptChildProcessSpawnOptions>(
199 raw_options,
200 )
201 .map_err(|error| {
202 SidecarError::InvalidState(format!(
203 "invalid child_process.spawn options payload: {error}"
204 ))
205 })?;
206
207 Ok((
208 JavascriptChildProcessSpawnRequest {
209 command,
210 args: parsed_args,
211 options: JavascriptChildProcessSpawnOptions {
212 cwd: parsed_options.cwd,
213 env: parsed_options.env,
214 internal_bootstrap_env: sanitize_javascript_child_process_internal_bootstrap_env(
215 &vm.guest_env,
216 ),
217 input: parsed_options.input,
218 shell: parsed_options.shell,
219 detached: parsed_options.detached,
220 stdio: parsed_options.stdio,
221 timeout: parsed_options.timeout,
222 kill_signal: parsed_options.kill_signal,
223 },
224 },
225 parsed_options.max_buffer,
226 ))
227}
228
229impl<B> SharedBridge<B> {
230 fn new(bridge: B) -> Self {
231 Self {
232 inner: Arc::new(Mutex::new(bridge)),
233 permissions: Arc::new(Mutex::new(BTreeMap::new())),
234 #[cfg(test)]
235 set_vm_permissions_outcomes: Arc::new(Mutex::new(VecDeque::new())),
236 }
237 }
238}
239
240impl<B> SharedBridge<B>
241where
242 B: NativeSidecarBridge + Send + 'static,
243 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
244{
245 pub(crate) fn with_mut<T>(
246 &self,
247 operation: impl FnOnce(&mut B) -> Result<T, BridgeError<B>>,
248 ) -> Result<T, SidecarError> {
249 let mut bridge = self.inner.lock().map_err(|_| {
250 SidecarError::Bridge(String::from("native sidecar bridge lock poisoned"))
251 })?;
252 operation(&mut bridge).map_err(|error| SidecarError::Bridge(format!("{error:?}")))
253 }
254
255 fn inspect<T>(&self, operation: impl FnOnce(&mut B) -> T) -> Result<T, SidecarError> {
256 let mut bridge = self.inner.lock().map_err(|_| {
257 SidecarError::Bridge(String::from("native sidecar bridge lock poisoned"))
258 })?;
259 Ok(operation(&mut bridge))
260 }
261
262 #[cfg(test)]
263 #[allow(dead_code)]
264 pub(crate) fn queue_set_vm_permissions_result(
265 &self,
266 result: Result<(), SidecarError>,
267 ) -> Result<(), SidecarError> {
268 let mut outcomes = self.set_vm_permissions_outcomes.lock().map_err(|_| {
269 SidecarError::Bridge(String::from(
270 "native sidecar test set_vm_permissions outcome lock poisoned",
271 ))
272 })?;
273 outcomes.push_back(result.err());
274 Ok(())
275 }
276
277 pub(crate) fn emit_lifecycle(
278 &self,
279 vm_id: &str,
280 state: LifecycleState,
281 ) -> Result<(), SidecarError> {
282 self.with_mut(|bridge| {
283 bridge.emit_lifecycle(LifecycleEventRecord {
284 vm_id: vm_id.to_owned(),
285 state,
286 detail: None,
287 })
288 })
289 }
290
291 pub(crate) fn emit_log(
292 &self,
293 vm_id: &str,
294 message: impl Into<String>,
295 ) -> Result<(), SidecarError> {
296 self.with_mut(|bridge| {
297 bridge.emit_log(LogRecord {
298 vm_id: vm_id.to_owned(),
299 level: LogLevel::Info,
300 message: message.into(),
301 })
302 })
303 }
304
305 pub(crate) fn filesystem_decision(
306 &self,
307 vm_id: &str,
308 path: &str,
309 access: FilesystemAccess,
310 ) -> PermissionDecision {
311 if let Some(decision) = self.static_permission_decision(
312 vm_id,
313 filesystem_permission_capability(access),
314 "fs",
315 Some(path),
316 ) {
317 return decision;
318 }
319 match self.with_mut(|bridge| {
320 bridge.check_filesystem_access(FilesystemPermissionRequest {
321 vm_id: vm_id.to_owned(),
322 path: path.to_owned(),
323 access,
324 })
325 }) {
326 Ok(decision) => map_bridge_permission(decision),
327 Err(error) => PermissionDecision::deny(error.to_string()),
328 }
329 }
330
331 pub(crate) fn command_decision(
332 &self,
333 vm_id: &str,
334 request: &CommandAccessRequest,
335 ) -> PermissionDecision {
336 if is_internal_runtime_command_request(request) {
337 return PermissionDecision::allow();
338 }
339 if let Some(decision) = self.static_permission_decision(
340 vm_id,
341 "child_process.spawn",
342 "child_process",
343 Some(&request.command),
344 ) {
345 return decision;
346 }
347 match self.with_mut(|bridge| {
348 bridge.check_command_execution(CommandPermissionRequest {
349 vm_id: vm_id.to_owned(),
350 command: request.command.clone(),
351 args: request.args.clone(),
352 cwd: request.cwd.clone(),
353 env: request.env.clone(),
354 })
355 }) {
356 Ok(decision) => map_bridge_permission(decision),
357 Err(error) => PermissionDecision::deny(error.to_string()),
358 }
359 }
360
361 pub(crate) fn environment_decision(
362 &self,
363 vm_id: &str,
364 request: &EnvAccessRequest,
365 ) -> PermissionDecision {
366 if let Some(decision) = self.static_permission_decision(
367 vm_id,
368 environment_permission_capability(request.op),
369 "env",
370 Some(&request.key),
371 ) {
372 return decision;
373 }
374 match self.with_mut(|bridge| {
375 bridge.check_environment_access(EnvironmentPermissionRequest {
376 vm_id: vm_id.to_owned(),
377 access: match request.op {
378 EnvironmentOperation::Read => EnvironmentAccess::Read,
379 EnvironmentOperation::Write => EnvironmentAccess::Write,
380 },
381 key: request.key.clone(),
382 value: request.value.clone(),
383 })
384 }) {
385 Ok(decision) => map_bridge_permission(decision),
386 Err(error) => PermissionDecision::deny(error.to_string()),
387 }
388 }
389
390 pub(crate) fn network_decision(
391 &self,
392 vm_id: &str,
393 request: &NetworkAccessRequest,
394 ) -> PermissionDecision {
395 if let Some(decision) = self.static_permission_decision(
396 vm_id,
397 network_permission_capability(request.op),
398 "network",
399 Some(&request.resource),
400 ) {
401 return decision;
402 }
403 match self.with_mut(|bridge| {
404 bridge.check_network_access(NetworkPermissionRequest {
405 vm_id: vm_id.to_owned(),
406 access: match request.op {
407 NetworkOperation::Fetch => NetworkAccess::Fetch,
408 NetworkOperation::Http => NetworkAccess::Http,
409 NetworkOperation::Dns => NetworkAccess::Dns,
410 NetworkOperation::Listen => NetworkAccess::Listen,
411 },
412 resource: request.resource.clone(),
413 })
414 }) {
415 Ok(decision) => map_bridge_permission(decision),
416 Err(error) => PermissionDecision::deny(error.to_string()),
417 }
418 }
419
420 pub(crate) fn require_network_access(
421 &self,
422 vm_id: &str,
423 op: NetworkOperation,
424 resource: impl Into<String>,
425 ) -> Result<(), SidecarError> {
426 let resource = resource.into();
427 let decision = self.network_decision(
428 vm_id,
429 &NetworkAccessRequest {
430 vm_id: vm_id.to_owned(),
431 op,
432 resource: resource.clone(),
433 },
434 );
435 if decision.allow {
436 return Ok(());
437 }
438
439 let message = match decision.reason.as_deref() {
440 Some(reason) => format!("EACCES: permission denied, {resource}: {reason}"),
441 None => format!("EACCES: permission denied, {resource}"),
442 };
443 Err(SidecarError::Execution(message))
444 }
445
446 pub(crate) fn set_vm_permissions(
447 &self,
448 vm_id: &str,
449 permissions: &PermissionsPolicy,
450 ) -> Result<(), SidecarError> {
451 #[cfg(test)]
452 {
453 let mut outcomes = self.set_vm_permissions_outcomes.lock().map_err(|_| {
454 SidecarError::Bridge(String::from(
455 "native sidecar test set_vm_permissions outcome lock poisoned",
456 ))
457 })?;
458 if let Some(Some(error)) = outcomes.pop_front() {
459 return Err(error);
460 }
461 }
462
463 let mut stored = self.permissions.lock().map_err(|_| {
464 SidecarError::Bridge(String::from(
465 "native sidecar permission policy lock poisoned",
466 ))
467 })?;
468 stored.insert(vm_id.to_owned(), permissions.clone());
469 Ok(())
470 }
471
472 pub(crate) fn restore_vm_permissions_fail_closed(
473 &self,
474 vm_id: &str,
475 original_permissions: &PermissionsPolicy,
476 context: &str,
477 operation_error: &SidecarError,
478 ) -> Result<(), SidecarError> {
479 match self.set_vm_permissions(vm_id, original_permissions) {
480 Ok(()) => Ok(()),
481 Err(restore_error) => {
482 let deny_all = deny_all_policy();
483 match self.set_vm_permissions(vm_id, &deny_all) {
484 Ok(()) => Err(SidecarError::InvalidState(format!(
485 "{context} failed: {operation_error}; restoring original permissions failed: {restore_error}; applied deny-all fallback"
486 ))),
487 Err(deny_all_error) => panic!(
488 "{context} failed: {operation_error}; restoring original permissions failed: {restore_error}; deny-all fallback failed: {deny_all_error}"
489 ),
490 }
491 }
492 }
493 }
494
495 pub(crate) fn clear_vm_permissions(&self, vm_id: &str) -> Result<(), SidecarError> {
496 let mut stored = self.permissions.lock().map_err(|_| {
497 SidecarError::Bridge(String::from(
498 "native sidecar permission policy lock poisoned",
499 ))
500 })?;
501 stored.remove(vm_id);
502 Ok(())
503 }
504
505 pub(crate) fn static_permission_decision(
506 &self,
507 vm_id: &str,
508 capability: &str,
509 domain: &str,
510 resource: Option<&str>,
511 ) -> Option<PermissionDecision> {
512 let stored = self.permissions.lock().ok()?;
513 let permissions = stored.get(vm_id)?;
514 let mode = evaluate_permissions_policy(permissions, domain, capability, resource);
515 Some(permission_mode_to_kernel_decision(mode, capability))
516 }
517}
518
519pub(crate) fn validate_permissions_policy(
520 permissions: &PermissionsPolicy,
521) -> Result<(), SidecarError> {
522 agentos_native_sidecar_core::permissions::validate_permissions_policy(permissions)
523 .map_err(|error| SidecarError::InvalidState(error.to_string()))
524}
525
526fn is_internal_runtime_command_request(request: &CommandAccessRequest) -> bool {
527 match request.command.as_str() {
528 "node" => request
529 .env
530 .keys()
531 .any(|key| INTERNAL_JAVASCRIPT_ENTRYPOINT_ENV_KEYS.contains(&key.as_str())),
532 "wasm" => request
533 .env
534 .keys()
535 .any(|key| INTERNAL_WASM_ENTRYPOINT_ENV_KEYS.contains(&key.as_str())),
536 "python" => request.env.keys().any(|key| {
537 INTERNAL_PYTHON_ENTRYPOINT_ENV_PREFIXES
538 .iter()
539 .any(|prefix| key.starts_with(prefix))
540 }),
541 _ => false,
542 }
543}
544
545fn ownership_matches_process_event(
546 ownership: &OwnershipScope,
547 event: &ProcessEventEnvelope,
548) -> bool {
549 match ownership {
550 OwnershipScope::ConnectionOwnership(inner) => inner.connection_id == event.connection_id,
551 OwnershipScope::SessionOwnership(inner) => {
552 inner.connection_id == event.connection_id && inner.session_id == event.session_id
553 }
554 OwnershipScope::VmOwnership(inner) => {
555 inner.connection_id == event.connection_id
556 && inner.session_id == event.session_id
557 && inner.vm_id == event.vm_id
558 }
559 }
560}
561
562fn public_process_event_matches_ownership<B>(
563 sidecar: &NativeSidecar<B>,
564 ownership: &OwnershipScope,
565 event: &ProcessEventEnvelope,
566) -> bool
567where
568 B: NativeSidecarBridge + Send + 'static,
569 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
570{
571 if !ownership_matches_process_event(ownership, event) {
572 return false;
573 }
574
575 if event.process_id.contains('/') {
576 return false;
577 }
578
579 let _ = sidecar;
582 true
583}
584
585fn poll_future_once<F: std::future::Future>(future: std::pin::Pin<&mut F>) -> Option<F::Output> {
586 let mut context = Context::from_waker(Waker::noop());
587 match future.poll(&mut context) {
588 Poll::Ready(output) => Some(output),
589 Poll::Pending => None,
590 }
591}
592
593impl JavascriptSocketPathContext {
598 pub(crate) fn loopback_port_allowed(&self, port: u16) -> bool {
599 self.loopback_exempt_ports.contains(&port)
600 || self
601 .tcp_loopback_guest_to_host_ports
602 .keys()
603 .any(|(_, guest_port)| *guest_port == port)
604 }
605
606 pub(crate) fn translate_tcp_loopback_port(
607 &self,
608 family: JavascriptSocketFamily,
609 port: u16,
610 ) -> Option<u16> {
611 self.tcp_loopback_guest_to_host_ports
612 .get(&(family, port))
613 .copied()
614 }
615
616 pub(crate) fn http_loopback_target(
617 &self,
618 family: JavascriptSocketFamily,
619 port: u16,
620 ) -> Option<&crate::state::JavascriptHttpLoopbackTarget> {
621 self.http_loopback_targets.get(&(family, port))
622 }
623
624 pub(crate) fn translate_udp_loopback_port(
625 &self,
626 family: JavascriptSocketFamily,
627 port: u16,
628 ) -> Option<u16> {
629 self.udp_loopback_guest_to_host_ports
630 .get(&(family, port))
631 .copied()
632 }
633
634 pub(crate) fn guest_udp_port_for_host_port(
635 &self,
636 family: JavascriptSocketFamily,
637 port: u16,
638 ) -> Option<u16> {
639 self.udp_loopback_host_to_guest_ports
640 .get(&(family, port))
641 .copied()
642 }
643}
644
645pub struct NativeSidecar<B> {
648 pub(crate) config: NativeSidecarConfig,
649 pub(crate) bridge: SharedBridge<B>,
650 pub(crate) mount_plugins: FileSystemPluginRegistry<MountPluginContext<B>>,
651 pub(crate) cache_root: PathBuf,
652 pub(crate) javascript_engine: JavascriptExecutionEngine,
653 pub(crate) python_engine: PythonExecutionEngine,
654 pub(crate) wasm_engine: WasmExecutionEngine,
655 pub(crate) next_connection_id: usize,
656 pub(crate) next_session_id: usize,
657 pub(crate) next_vm_id: usize,
658 pub(crate) next_sidecar_request_id: RequestId,
659 pub(crate) connections: BTreeMap<String, ConnectionState>,
660 pub(crate) sessions: BTreeMap<String, SessionState>,
661 pub(crate) vms: BTreeMap<String, VmState>,
662 #[allow(dead_code)]
663 pub(crate) process_event_sender: Sender<ProcessEventEnvelope>,
664 pub(crate) process_event_receiver: Option<Receiver<ProcessEventEnvelope>>,
665 pub(crate) pending_process_events: VecDeque<ProcessEventEnvelope>,
666 pub(crate) pending_sidecar_responses: SidecarResponseTracker,
667 pub(crate) outbound_sidecar_requests: VecDeque<SidecarRequestFrame>,
668 pub(crate) completed_sidecar_responses: BTreeMap<RequestId, SidecarResponseFrame>,
669 pub(crate) completed_sidecar_response_order: VecDeque<RequestId>,
670 pub(crate) completed_sidecar_responses_gauge: Arc<QueueGauge>,
671 pub(crate) pending_process_events_gauge: Arc<QueueGauge>,
672 pub(crate) pending_sidecar_responses_gauge: Arc<QueueGauge>,
673 pub(crate) outbound_sidecar_requests_gauge: Arc<QueueGauge>,
674 pub(crate) sidecar_requests: SharedSidecarRequestClient,
675 pub(crate) event_sink: SharedEventSink,
676 pub(crate) extensions: BTreeMap<String, Arc<dyn Extension>>,
677 pub(crate) extension_sessions: BTreeMap<(String, String), ExtensionSessionResources>,
678 pub(crate) extension_process_output_buffers:
679 BTreeMap<(String, String), ExtensionBufferedProcessOutput>,
680 pub(crate) disposed_sessions: Vec<(String, String)>,
684}
685
686#[derive(Debug)]
687pub(crate) struct ExtensionSessionResources {
688 pub(crate) ownership: OwnershipScope,
689 pub(crate) process_ids: BTreeSet<String>,
690 pub(crate) vm_ids: BTreeSet<String>,
691}
692
693impl<B> fmt::Debug for NativeSidecar<B> {
694 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
695 f.debug_struct("NativeSidecar")
696 .field("config", &self.config)
697 .field("cache_root", &self.cache_root)
698 .field("next_connection_id", &self.next_connection_id)
699 .field("next_session_id", &self.next_session_id)
700 .field("next_vm_id", &self.next_vm_id)
701 .field("connection_count", &self.connections.len())
702 .field("session_count", &self.sessions.len())
703 .field("vm_count", &self.vms.len())
704 .field("extension_session_count", &self.extension_sessions.len())
705 .field(
706 "extension_process_output_buffer_count",
707 &self.extension_process_output_buffers.len(),
708 )
709 .finish()
710 }
711}
712
713impl<B> NativeSidecar<B>
714where
715 B: NativeSidecarBridge + Send + 'static,
716 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
717{
718 pub fn new(bridge: B) -> Result<Self, SidecarError> {
719 Self::with_config(bridge, NativeSidecarConfig::default())
720 }
721
722 pub fn with_config(bridge: B, config: NativeSidecarConfig) -> Result<Self, SidecarError> {
723 if matches!(config.expected_auth_token.as_deref(), Some("")) {
724 return Err(SidecarError::InvalidState(String::from(
725 "native sidecar expected_auth_token must not be empty",
726 )));
727 }
728
729 let cache_root = config.compile_cache_root.clone().unwrap_or_else(|| {
730 std::env::temp_dir().join(format!(
731 "{}-{}",
732 config.sidecar_id,
733 SystemTime::now()
734 .duration_since(UNIX_EPOCH)
735 .expect("system time before unix epoch")
736 .as_nanos()
737 ))
738 });
739 fs::create_dir_all(&cache_root).map_err(|error| {
740 SidecarError::Io(format!("failed to prepare sidecar cache root: {error}"))
741 })?;
742
743 let bridge = SharedBridge::new(bridge);
744 let mount_plugins = build_mount_plugin_registry::<B>()?;
745 let (process_event_sender, process_event_receiver) = channel(MAX_PROCESS_EVENT_QUEUE);
746
747 Ok(Self {
748 config,
749 bridge,
750 mount_plugins,
751 cache_root,
752 javascript_engine: JavascriptExecutionEngine::default(),
753 python_engine: PythonExecutionEngine::default(),
754 wasm_engine: WasmExecutionEngine::default(),
755 next_connection_id: 0,
756 next_session_id: 0,
757 next_vm_id: 0,
758 next_sidecar_request_id: -1,
759 connections: BTreeMap::new(),
760 sessions: BTreeMap::new(),
761 vms: BTreeMap::new(),
762 process_event_sender,
763 process_event_receiver: Some(process_event_receiver),
764 pending_process_events: VecDeque::new(),
765 pending_sidecar_responses: SidecarResponseTracker::default(),
766 outbound_sidecar_requests: VecDeque::new(),
767 completed_sidecar_responses: BTreeMap::new(),
768 completed_sidecar_response_order: VecDeque::new(),
769 completed_sidecar_responses_gauge: register_queue(
770 TrackedLimit::CompletedSidecarResponses,
771 MAX_COMPLETED_SIDECAR_RESPONSES,
772 ),
773 pending_process_events_gauge: register_queue(
774 TrackedLimit::PendingProcessEvents,
775 MAX_PROCESS_EVENT_QUEUE,
776 ),
777 pending_sidecar_responses_gauge: register_queue(
778 TrackedLimit::PendingSidecarResponses,
779 MAX_PENDING_SIDECAR_RESPONSES,
780 ),
781 outbound_sidecar_requests_gauge: register_queue(
782 TrackedLimit::OutboundSidecarRequests,
783 MAX_OUTBOUND_SIDECAR_REQUESTS,
784 ),
785 sidecar_requests: SharedSidecarRequestClient::default(),
786 event_sink: SharedEventSink::default(),
787 extensions: BTreeMap::new(),
788 extension_sessions: BTreeMap::new(),
789 extension_process_output_buffers: BTreeMap::new(),
790 disposed_sessions: Vec::new(),
791 })
792 }
793
794 pub fn with_config_and_extensions(
795 bridge: B,
796 config: NativeSidecarConfig,
797 extensions: Vec<Box<dyn Extension>>,
798 ) -> Result<Self, SidecarError> {
799 let mut sidecar = Self::with_config(bridge, config)?;
800 for extension in extensions {
801 sidecar.register_extension(extension)?;
802 }
803 Ok(sidecar)
804 }
805
806 pub(crate) fn prune_extension_process_resource(&mut self, process_id: &str) {
807 self.extension_sessions.retain(|_, resources| {
808 resources.process_ids.remove(process_id);
809 !resources.process_ids.is_empty() || !resources.vm_ids.is_empty()
810 });
811 }
812
813 pub(crate) fn prune_extension_vm_resource(&mut self, vm_id: &str) {
814 self.extension_sessions.retain(|_, resources| {
815 if matches!(
816 &resources.ownership,
817 OwnershipScope::VmOwnership(inner) if inner.vm_id == vm_id
818 ) {
819 resources.process_ids.clear();
820 }
821 resources.vm_ids.remove(vm_id);
822 !resources.process_ids.is_empty() || !resources.vm_ids.is_empty()
823 });
824 }
825
826 pub(crate) fn reclaim_vm_tracking(&mut self, session_id: &str, vm_id: &str) {
835 self.javascript_engine.dispose_vm(vm_id);
836 self.python_engine.dispose_vm(vm_id);
837 self.wasm_engine.dispose_vm(vm_id);
838 self.prune_extension_vm_resource(vm_id);
839 self.extension_process_output_buffers
840 .retain(|(buffer_vm_id, _process_id), _| buffer_vm_id != vm_id);
841 if let Some(session) = self.sessions.get_mut(session_id) {
842 session.vm_ids.remove(vm_id);
843 }
844 }
845
846 pub(crate) fn capture_extension_process_output_event(
847 &mut self,
848 vm_id: &str,
849 process_id: &str,
850 event: &ActiveExecutionEvent,
851 ) -> bool {
852 let Some(buffer) = self
853 .extension_process_output_buffers
854 .get_mut(&(vm_id.to_string(), process_id.to_string()))
855 else {
856 return false;
857 };
858 match event {
859 ActiveExecutionEvent::Stdout(chunk) => {
860 buffer.append_stdout(chunk, DEFAULT_ACP_STDOUT_BUFFER_BYTE_LIMIT);
861 true
862 }
863 ActiveExecutionEvent::Stderr(chunk) => {
864 buffer.append_stderr(chunk, DEFAULT_ACP_STDOUT_BUFFER_BYTE_LIMIT);
865 true
866 }
867 ActiveExecutionEvent::JavascriptSyncRpcRequest(_)
868 | ActiveExecutionEvent::PythonVfsRpcRequest(_)
869 | ActiveExecutionEvent::SignalState { .. }
870 | ActiveExecutionEvent::Exited(_) => false,
871 }
872 }
873
874 fn bind_extension_process_resource(
875 &mut self,
876 ownership: OwnershipScope,
877 namespace: String,
878 ext_session_id: String,
879 process_id: String,
880 ) -> Result<(), SidecarError> {
881 if ext_session_id.is_empty() {
882 return Err(SidecarError::InvalidState(String::from(
883 "extension session id must not be empty",
884 )));
885 }
886 let (connection_id, session_id, vm_id) = self.vm_scope_for(&ownership)?;
887 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
888 let process_exists = self
889 .vms
890 .get(&vm_id)
891 .is_some_and(|vm| vm.active_processes.contains_key(&process_id));
892 if !process_exists {
893 return Err(SidecarError::InvalidState(format!(
894 "VM {vm_id} has no active process {process_id}"
895 )));
896 }
897
898 let key = (namespace, ext_session_id);
899 if let Some(resources) = self.extension_sessions.get_mut(&key) {
900 if resources.ownership != ownership {
901 return Err(SidecarError::InvalidState(String::from(
902 "extension session ownership did not match existing resources",
903 )));
904 }
905 resources.process_ids.insert(process_id);
906 } else {
907 self.extension_sessions.insert(
908 key,
909 ExtensionSessionResources {
910 ownership,
911 process_ids: BTreeSet::from([process_id]),
912 vm_ids: BTreeSet::new(),
913 },
914 );
915 }
916 Ok(())
917 }
918
919 fn bind_extension_vm_resource(
920 &mut self,
921 ownership: OwnershipScope,
922 namespace: String,
923 ext_session_id: String,
924 ) -> Result<(), SidecarError> {
925 if ext_session_id.is_empty() {
926 return Err(SidecarError::InvalidState(String::from(
927 "extension session id must not be empty",
928 )));
929 }
930 let (connection_id, session_id, vm_id) = self.vm_scope_for(&ownership)?;
931 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
932
933 let key = (namespace, ext_session_id);
934 if let Some(resources) = self.extension_sessions.get_mut(&key) {
935 if resources.ownership != ownership {
936 return Err(SidecarError::InvalidState(String::from(
937 "extension session ownership did not match existing resources",
938 )));
939 }
940 resources.vm_ids.insert(vm_id);
941 } else {
942 self.extension_sessions.insert(
943 key,
944 ExtensionSessionResources {
945 ownership,
946 process_ids: BTreeSet::new(),
947 vm_ids: BTreeSet::from([vm_id]),
948 },
949 );
950 }
951 Ok(())
952 }
953
954 pub fn sidecar_id(&self) -> &str {
955 &self.config.sidecar_id
956 }
957
958 pub fn with_bridge_mut<T>(
959 &self,
960 operation: impl FnOnce(&mut B) -> T,
961 ) -> Result<T, SidecarError> {
962 self.bridge.inspect(operation)
963 }
964
965 pub fn set_sidecar_request_transport(&mut self, transport: Arc<dyn SidecarRequestTransport>) {
966 self.sidecar_requests.set_transport(transport);
967 }
968
969 pub fn set_event_transport(&mut self, transport: Arc<dyn EventSinkTransport>) {
970 self.event_sink.set_transport(transport);
971 }
972
973 pub fn register_extension(
974 &mut self,
975 extension: Box<dyn Extension>,
976 ) -> Result<(), SidecarError> {
977 let namespace = extension.namespace().to_owned();
978 if namespace.is_empty() {
979 return Err(SidecarError::InvalidState(String::from(
980 "extension namespace must not be empty",
981 )));
982 }
983 if self.extensions.contains_key(&namespace) {
984 return Err(SidecarError::Conflict(format!(
985 "extension namespace {namespace} is already registered",
986 )));
987 }
988 self.extensions.insert(namespace, Arc::from(extension));
989 Ok(())
990 }
991
992 pub fn set_sidecar_request_handler<F>(&mut self, handler: F)
993 where
994 F: Fn(SidecarRequestFrame) -> Result<SidecarResponsePayload, SidecarError>
995 + Send
996 + Sync
997 + 'static,
998 {
999 struct HandlerTransport<F>(F);
1000
1001 impl<F> SidecarRequestTransport for HandlerTransport<F>
1002 where
1003 F: Fn(SidecarRequestFrame) -> Result<SidecarResponsePayload, SidecarError>
1004 + Send
1005 + Sync
1006 + 'static,
1007 {
1008 fn send_request(
1009 &self,
1010 request: SidecarRequestFrame,
1011 _timeout: Duration,
1012 ) -> Result<SidecarResponseFrame, SidecarError> {
1013 let payload = (self.0)(request.clone())?;
1014 Ok(SidecarResponseFrame::new(
1015 request.request_id,
1016 request.ownership,
1017 payload,
1018 ))
1019 }
1020 }
1021
1022 self.set_sidecar_request_transport(Arc::new(HandlerTransport(handler)));
1023 }
1024
1025 pub fn set_wire_sidecar_request_handler<F>(&mut self, handler: F)
1026 where
1027 F: Fn(
1028 crate::wire::SidecarRequestFrame,
1029 ) -> Result<crate::wire::SidecarResponseFrame, SidecarError>
1030 + Send
1031 + Sync
1032 + 'static,
1033 {
1034 self.set_sidecar_request_handler(move |request| {
1035 let request = crate::wire::sidecar_request_frame_from_compat(request)
1036 .map_err(wire_protocol_error)?;
1037 let response = handler(request)?;
1038 let response = crate::wire::sidecar_response_frame_to_compat(response)
1039 .map_err(wire_protocol_error)?;
1040 Ok(response.payload)
1041 });
1042 }
1043
1044 pub(crate) fn queue_pending_process_event(
1045 &mut self,
1046 envelope: ProcessEventEnvelope,
1047 ) -> Result<(), SidecarError> {
1048 if self.pending_process_events.len() >= MAX_PROCESS_EVENT_QUEUE {
1049 return Err(process_event_queue_overflow_error());
1050 }
1051 if matches!(&envelope.event, ActiveExecutionEvent::Exited(_)) {
1052 mark_execute_exit_event_queued(&envelope.vm_id, &envelope.process_id);
1053 }
1054 self.pending_process_events.push_back(envelope);
1055 self.pending_process_events_gauge
1056 .observe_depth(self.pending_process_events.len());
1057 Ok(())
1058 }
1059
1060 pub(crate) fn queue_front_pending_process_event(
1061 &mut self,
1062 envelope: ProcessEventEnvelope,
1063 ) -> Result<(), SidecarError> {
1064 if self.pending_process_events.len() >= MAX_PROCESS_EVENT_QUEUE {
1065 return Err(process_event_queue_overflow_error());
1066 }
1067 if matches!(&envelope.event, ActiveExecutionEvent::Exited(_)) {
1068 mark_execute_exit_event_queued(&envelope.vm_id, &envelope.process_id);
1069 }
1070 self.pending_process_events.push_front(envelope);
1071 self.pending_process_events_gauge
1072 .observe_depth(self.pending_process_events.len());
1073 Ok(())
1074 }
1075
1076 pub(crate) fn pending_process_event_capacity(&self) -> usize {
1077 MAX_PROCESS_EVENT_QUEUE.saturating_sub(self.pending_process_events.len())
1078 }
1079
1080 pub fn dispatch_blocking(
1081 &mut self,
1082 request: RequestFrame,
1083 ) -> Result<DispatchResult, SidecarError> {
1084 let inside_runtime = tokio::runtime::Handle::try_current().is_ok();
1085 if request_dispatch_mode(&request) == RequestDispatchMode::Async && !inside_runtime {
1086 return blocking_dispatch_runtime().block_on(self.dispatch(request));
1087 }
1088
1089 let mut future = std::pin::pin!(self.dispatch(request));
1090 match poll_future_once(future.as_mut()) {
1091 Some(result) => result,
1092 None if inside_runtime => Err(SidecarError::InvalidState(String::from(
1093 "dispatch_blocking cannot wait for an async sidecar request inside a Tokio runtime; use dispatch().await",
1094 ))),
1095 None => blocking_dispatch_runtime().block_on(future),
1096 }
1097 }
1098
1099 pub fn dispatch_wire_blocking(
1100 &mut self,
1101 request: crate::wire::RequestFrame,
1102 ) -> Result<crate::wire::WireDispatchResult, SidecarError> {
1103 let request = crate::wire::request_frame_to_compat(request).map_err(wire_protocol_error)?;
1104 let result = self.dispatch_blocking(request)?;
1105 wire_dispatch_result(result)
1106 }
1107
1108 pub fn poll_event_blocking(
1109 &mut self,
1110 ownership: &OwnershipScope,
1111 timeout: Duration,
1112 ) -> Result<Option<EventFrame>, SidecarError> {
1113 blocking_dispatch_runtime().block_on(self.poll_event(ownership, timeout))
1114 }
1115
1116 pub fn poll_event_wire_blocking(
1117 &mut self,
1118 ownership: &crate::wire::OwnershipScope,
1119 timeout: Duration,
1120 ) -> Result<Option<crate::wire::EventFrame>, SidecarError> {
1121 let ownership = crate::wire::ownership_scope_to_compat(ownership.clone());
1122 self.poll_event_blocking(&ownership, timeout)?
1123 .map(crate::wire::event_frame_from_compat)
1124 .transpose()
1125 .map_err(wire_protocol_error)
1126 }
1127
1128 pub fn close_session_blocking(
1129 &mut self,
1130 connection_id: &str,
1131 session_id: &str,
1132 ) -> Result<Vec<EventFrame>, SidecarError> {
1133 blocking_dispatch_runtime().block_on(self.close_session(connection_id, session_id))
1134 }
1135
1136 pub fn remove_connection_blocking(
1137 &mut self,
1138 connection_id: &str,
1139 ) -> Result<Vec<EventFrame>, SidecarError> {
1140 blocking_dispatch_runtime().block_on(self.remove_connection(connection_id))
1141 }
1142
1143 pub fn dispose_vm_internal_blocking(
1144 &mut self,
1145 connection_id: &str,
1146 session_id: &str,
1147 vm_id: &str,
1148 reason: DisposeReason,
1149 ) -> Result<Vec<EventFrame>, SidecarError> {
1150 blocking_dispatch_runtime().block_on(self.dispose_vm_internal(
1151 connection_id,
1152 session_id,
1153 vm_id,
1154 reason,
1155 ))
1156 }
1157
1158 pub async fn dispatch(
1159 &mut self,
1160 request: RequestFrame,
1161 ) -> Result<DispatchResult, SidecarError> {
1162 if let Err(error) = self.ensure_request_within_frame_limit(&request) {
1163 return Ok(DispatchResult {
1164 response: self.reject(&request, error_code(&error), &error.to_string()),
1165 events: Vec::new(),
1166 });
1167 }
1168
1169 let result = match route_request_payload(&request) {
1170 RequestRoute::Authenticate(payload) => {
1171 self.authenticate_connection(&request, payload).await
1172 }
1173 RequestRoute::OpenSession(payload) => self.open_session(&request, payload).await,
1174 RequestRoute::CreateVm(payload) => self.create_vm(&request, payload).await,
1175 RequestRoute::DisposeVm(payload) => self.dispose_vm(&request, payload).await,
1176 RequestRoute::BootstrapRootFilesystem(payload) => {
1177 self.bootstrap_root_filesystem(&request, payload.entries)
1178 .await
1179 }
1180 RequestRoute::ConfigureVm(payload) => self.configure_vm(&request, payload).await,
1181 RequestRoute::RegisterHostCallbacks(payload) => {
1182 register_host_callbacks(self, &request, payload)
1183 }
1184 RequestRoute::CreateLayer(payload) => self.create_layer(&request, payload).await,
1185 RequestRoute::SealLayer(payload) => self.seal_layer(&request, payload).await,
1186 RequestRoute::ImportSnapshot(payload) => self.import_snapshot(&request, payload).await,
1187 RequestRoute::ExportSnapshot(payload) => self.export_snapshot(&request, payload).await,
1188 RequestRoute::CreateOverlay(payload) => self.create_overlay(&request, payload).await,
1189 RequestRoute::GuestFilesystemCall(payload) => {
1190 self.guest_filesystem_call(&request, payload).await
1191 }
1192 RequestRoute::GuestKernelCall(payload) => {
1193 self.guest_kernel_call(&request, payload).await
1194 }
1195 RequestRoute::SnapshotRootFilesystem(payload) => {
1196 self.snapshot_root_filesystem(&request, payload).await
1197 }
1198 RequestRoute::Execute(payload) => self.execute(&request, payload).await,
1199 RequestRoute::WriteStdin(payload) => self.write_stdin(&request, payload).await,
1200 RequestRoute::ResizePty(payload) => self.resize_pty(&request, payload).await,
1201 RequestRoute::CloseStdin(payload) => self.close_stdin(&request, payload).await,
1202 RequestRoute::KillProcess(payload) => self.kill_process(&request, payload).await,
1203 RequestRoute::GetProcessSnapshot(payload) => {
1204 self.get_process_snapshot(&request, payload).await
1205 }
1206 RequestRoute::GetResourceSnapshot(payload) => {
1207 self.get_resource_snapshot(&request, payload).await
1208 }
1209 RequestRoute::FindListener(payload) => self.find_listener(&request, payload).await,
1210 RequestRoute::FindBoundUdp(payload) => self.find_bound_udp(&request, payload).await,
1211 RequestRoute::VmFetch(payload) => self.vm_fetch(&request, payload).await,
1212 RequestRoute::GetSignalState(payload) => self.get_signal_state(&request, payload).await,
1213 RequestRoute::GetZombieTimerCount(payload) => {
1214 self.get_zombie_timer_count(&request, payload).await
1215 }
1216 RequestRoute::LinkPackage(payload) => self.link_package(&request, payload).await,
1217 RequestRoute::ProvidedCommands(payload) => {
1218 self.provided_commands(&request, payload).await
1219 }
1220 RequestRoute::UnsupportedHostCallbackDirection => {
1221 Ok(unsupported_host_callback_direction_dispatch(&request))
1222 }
1223 RequestRoute::Ext(payload) => self.dispatch_extension_request(&request, payload).await,
1224 };
1225
1226 match result {
1227 Ok(dispatch) => Ok(dispatch),
1228 Err(error @ SidecarError::Io(_)) => Err(error),
1229 Err(error) => Ok(DispatchResult {
1230 response: self.reject(&request, error_code(&error), &error.to_string()),
1231 events: Vec::new(),
1232 }),
1233 }
1234 }
1235
1236 pub async fn dispatch_wire(
1237 &mut self,
1238 request: crate::wire::RequestFrame,
1239 ) -> Result<crate::wire::WireDispatchResult, SidecarError> {
1240 let request = crate::wire::request_frame_to_compat(request).map_err(wire_protocol_error)?;
1241 let result = self.dispatch(request).await?;
1242 wire_dispatch_result(result)
1243 }
1244
1245 pub async fn poll_event_wire(
1246 &mut self,
1247 ownership: &crate::wire::OwnershipScope,
1248 timeout: Duration,
1249 ) -> Result<Option<crate::wire::EventFrame>, SidecarError> {
1250 let ownership = crate::wire::ownership_scope_to_compat(ownership.clone());
1251 self.poll_event(&ownership, timeout)
1252 .await?
1253 .map(crate::wire::event_frame_from_compat)
1254 .transpose()
1255 .map_err(wire_protocol_error)
1256 }
1257
1258 async fn dispatch_extension_request(
1259 &mut self,
1260 request: &RequestFrame,
1261 envelope: ExtEnvelope,
1262 ) -> Result<DispatchResult, SidecarError> {
1263 let namespace = envelope.namespace;
1264 let Some(extension) = self.extensions.get(&namespace).cloned() else {
1265 return Ok(DispatchResult {
1266 response: self.reject(
1267 request,
1268 "unknown_extension",
1269 &format!("no extension registered for namespace {namespace}"),
1270 ),
1271 events: Vec::new(),
1272 });
1273 };
1274 let snapshot = ExtensionSnapshot::new(
1275 namespace.clone(),
1276 request.ownership.clone(),
1277 self.sidecar_requests.clone(),
1278 self.event_sink.clone(),
1279 );
1280 let ctx = ExtensionContext::new(snapshot, self);
1281 let response = extension.handle_request(ctx, envelope.payload).await?;
1282 Ok(DispatchResult {
1283 response: self.respond(
1284 request,
1285 ResponsePayload::ExtResult(ExtEnvelope {
1286 namespace,
1287 payload: response.payload,
1288 }),
1289 ),
1290 events: response.events,
1291 })
1292 }
1293
1294 pub async fn poll_event(
1295 &mut self,
1296 ownership: &OwnershipScope,
1297 timeout: Duration,
1298 ) -> Result<Option<EventFrame>, SidecarError> {
1299 let deadline = Instant::now() + timeout;
1300 loop {
1301 if let Some(index) = self
1302 .pending_process_events
1303 .iter()
1304 .position(|event| public_process_event_matches_ownership(self, ownership, event))
1305 {
1306 let Some(envelope) = self.pending_process_events.remove(index) else {
1307 continue;
1308 };
1309 if let Some(frame) = self.handle_process_event_envelope(envelope)? {
1310 return Ok(Some(frame));
1311 }
1312 continue;
1313 }
1314
1315 if !timeout.is_zero() {
1316 let _ = self.pump_process_events(ownership).await?;
1317 }
1318
1319 let queued_envelopes = {
1320 let pending_capacity = self.pending_process_event_capacity();
1321 let receiver = self.process_event_receiver.as_mut().ok_or_else(|| {
1322 SidecarError::InvalidState(String::from("process event receiver unavailable"))
1323 })?;
1324 let mut queued = Vec::new();
1325 loop {
1326 if queued.len() >= pending_capacity {
1327 if receiver.is_empty() {
1328 break;
1329 }
1330 return Err(process_event_queue_overflow_error());
1331 }
1332 match receiver.try_recv() {
1333 Ok(envelope) => queued.push(envelope),
1334 Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
1335 Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
1336 }
1337 }
1338 queued
1339 };
1340
1341 let mut matching_envelope = None;
1342 for envelope in queued_envelopes {
1343 if matching_envelope.is_none()
1344 && public_process_event_matches_ownership(self, ownership, &envelope)
1345 {
1346 matching_envelope = Some(envelope);
1347 } else {
1348 self.queue_pending_process_event(envelope)?;
1349 }
1350 }
1351
1352 if let Some(envelope) = matching_envelope {
1353 if let Some(frame) = self.handle_process_event_envelope(envelope)? {
1354 return Ok(Some(frame));
1355 }
1356 continue;
1357 }
1358
1359 if Instant::now() >= deadline {
1360 return Ok(None);
1361 }
1362
1363 let remaining = deadline.saturating_duration_since(Instant::now());
1364 time::sleep(remaining.min(Duration::from_millis(10))).await;
1365 }
1366 }
1367
1368 pub(crate) fn handle_process_event_envelope(
1369 &mut self,
1370 envelope: ProcessEventEnvelope,
1371 ) -> Result<Option<EventFrame>, SidecarError> {
1372 let handle_start = Instant::now();
1373 let ProcessEventEnvelope {
1374 connection_id,
1375 session_id,
1376 vm_id,
1377 process_id,
1378 event,
1379 } = envelope;
1380
1381 let is_exit_event = matches!(event, ActiveExecutionEvent::Exited(_));
1382
1383 if is_exit_event {
1384 record_execute_exit_event_queue_wait(
1385 "process_exit_event_queue_wait",
1386 &vm_id,
1387 &process_id,
1388 );
1389 let mut trailing = Vec::new();
1390 let mut deferred = VecDeque::new();
1391 let phase_start = Instant::now();
1392 while let Some(pending) = self.pending_process_events.pop_front() {
1393 if pending.vm_id == vm_id
1394 && pending.process_id == process_id
1395 && !matches!(pending.event, ActiveExecutionEvent::Exited(_))
1396 {
1397 trailing.push(pending.event);
1398 } else {
1399 deferred.push_back(pending);
1400 }
1401 }
1402 self.pending_process_events = deferred;
1403 record_execute_phase("process_exit_trailing_pending_scan", phase_start.elapsed());
1404 let drain_limit = self
1405 .pending_process_event_capacity()
1406 .saturating_sub(trailing.len().saturating_add(1));
1407 let phase_start = Instant::now();
1408 trailing.extend(
1409 self.drain_process_events_blocking_with_limit(&vm_id, &process_id, drain_limit)?
1410 .into_iter()
1411 .filter(|event| !matches!(event, ActiveExecutionEvent::Exited(_))),
1412 );
1413 record_execute_phase(
1414 "process_exit_trailing_blocking_drain",
1415 phase_start.elapsed(),
1416 );
1417
1418 if !trailing.is_empty() {
1419 if self.pending_process_event_capacity() < trailing.len() {
1420 return Err(process_event_queue_overflow_error());
1421 }
1422 let emit_now = if self.pending_process_event_capacity() == trailing.len() {
1423 Some(trailing.remove(0))
1424 } else {
1425 None
1426 };
1427 let phase_start = Instant::now();
1428 mark_execute_exit_event_queued(&vm_id, &process_id);
1429 self.queue_front_pending_process_event(ProcessEventEnvelope {
1430 connection_id: connection_id.clone(),
1431 session_id: session_id.clone(),
1432 vm_id: vm_id.clone(),
1433 process_id: process_id.clone(),
1434 event,
1435 })?;
1436 for event in trailing.into_iter().rev() {
1437 self.queue_front_pending_process_event(ProcessEventEnvelope {
1438 connection_id: connection_id.clone(),
1439 session_id: session_id.clone(),
1440 vm_id: vm_id.clone(),
1441 process_id: process_id.clone(),
1442 event,
1443 })?;
1444 }
1445 record_execute_phase("process_exit_trailing_requeue", phase_start.elapsed());
1446 if let Some(event) = emit_now {
1447 let result = self.handle_execution_event(&vm_id, &process_id, event);
1448 record_execute_phase(
1449 "process_exit_event_handle_envelope_total",
1450 handle_start.elapsed(),
1451 );
1452 return result;
1453 }
1454 record_execute_phase(
1455 "process_exit_event_handle_envelope_total",
1456 handle_start.elapsed(),
1457 );
1458 return Ok(None);
1459 }
1460 }
1461
1462 let result = self.handle_execution_event(&vm_id, &process_id, event);
1463 if is_exit_event {
1464 record_execute_phase(
1465 "process_exit_event_handle_envelope_total",
1466 handle_start.elapsed(),
1467 );
1468 }
1469 result
1470 }
1471
1472 pub async fn close_session(
1475 &mut self,
1476 connection_id: &str,
1477 session_id: &str,
1478 ) -> Result<Vec<EventFrame>, SidecarError> {
1479 self.dispose_session(connection_id, session_id, DisposeReason::Requested)
1480 .await
1481 }
1482
1483 pub async fn remove_connection(
1484 &mut self,
1485 connection_id: &str,
1486 ) -> Result<Vec<EventFrame>, SidecarError> {
1487 self.require_authenticated_connection(connection_id)?;
1488
1489 let session_ids = self
1490 .connections
1491 .get(connection_id)
1492 .expect("authenticated connection should exist")
1493 .sessions
1494 .iter()
1495 .cloned()
1496 .collect::<Vec<_>>();
1497
1498 let mut events = Vec::new();
1499 let mut first_error: Option<SidecarError> = None;
1500 for session_id in session_ids {
1501 match self
1504 .dispose_session(connection_id, &session_id, DisposeReason::ConnectionClosed)
1505 .await
1506 {
1507 Ok(session_events) => events.extend(session_events),
1508 Err(error) => {
1509 if first_error.is_none() {
1510 first_error = Some(error);
1511 }
1512 }
1513 }
1514 }
1515
1516 self.connections.remove(connection_id);
1517 if let Some(error) = first_error {
1518 return Err(error);
1519 }
1520 Ok(events)
1521 }
1522
1523 async fn authenticate_connection(
1524 &mut self,
1525 request: &RequestFrame,
1526 payload: crate::protocol::AuthenticateRequest,
1527 ) -> Result<DispatchResult, SidecarError> {
1528 let _ = self.connection_id_for(&request.ownership)?;
1529 if let Err(error) = self.validate_auth_token(&payload.auth_token) {
1530 let mut fields = audit_fields([
1531 (String::from("source"), payload.client_name.clone()),
1532 (String::from("reason"), error.to_string()),
1533 ]);
1534 if let OwnershipScope::ConnectionOwnership(inner) = &request.ownership {
1535 fields.insert(String::from("connection_id"), inner.connection_id.clone());
1536 }
1537 emit_security_audit_event(
1538 &self.bridge,
1539 &self.config.sidecar_id,
1540 "security.auth.failed",
1541 fields,
1542 );
1543 return Err(error);
1544 }
1545
1546 if let Err(error) = validate_authenticate_versions(&payload) {
1547 return Err(match error {
1548 AuthenticateVersionError::ProtocolVersionMismatch(message) => {
1549 SidecarError::ProtocolVersionMismatch(message)
1550 }
1551 AuthenticateVersionError::BridgeVersionMismatch(message) => {
1552 SidecarError::BridgeVersionMismatch(message)
1553 }
1554 });
1555 }
1556
1557 let connection_id = self.allocate_connection_id();
1558 self.connections.insert(
1559 connection_id.clone(),
1560 ConnectionState {
1561 auth_token: payload.auth_token,
1562 sessions: BTreeSet::new(),
1563 },
1564 );
1565
1566 let response = shared_authenticated_response(
1567 request.request_id,
1568 self.config.sidecar_id.clone(),
1569 connection_id,
1570 self.config.max_frame_bytes as u32,
1571 );
1572 Ok(DispatchResult {
1573 response,
1574 events: Vec::new(),
1575 })
1576 }
1577
1578 async fn open_session(
1579 &mut self,
1580 request: &RequestFrame,
1581 payload: OpenSessionRequest,
1582 ) -> Result<DispatchResult, SidecarError> {
1583 let connection_id = self.connection_id_for(&request.ownership)?;
1584 self.require_authenticated_connection(&connection_id)?;
1585
1586 self.next_session_id += 1;
1587 let session_id = format!("session-{}", self.next_session_id);
1588 self.sessions.insert(
1589 session_id.clone(),
1590 SessionState {
1591 connection_id: connection_id.clone(),
1592 placement: payload.placement,
1593 metadata: payload.metadata.into_iter().collect(),
1594 vm_ids: BTreeSet::new(),
1595 },
1596 );
1597 self.connections
1598 .get_mut(&connection_id)
1599 .expect("authenticated connection should exist")
1600 .sessions
1601 .insert(session_id.clone());
1602
1603 Ok(DispatchResult {
1604 response: session_opened_response(request.request_id, connection_id, session_id),
1605 events: Vec::new(),
1606 })
1607 }
1608
1609 async fn guest_filesystem_call(
1612 &mut self,
1613 request: &RequestFrame,
1614 payload: GuestFilesystemCallRequest,
1615 ) -> Result<DispatchResult, SidecarError> {
1616 filesystem_guest_filesystem_call(self, request, payload).await
1617 }
1618
1619 async fn dispose_session(
1625 &mut self,
1626 connection_id: &str,
1627 session_id: &str,
1628 reason: DisposeReason,
1629 ) -> Result<Vec<EventFrame>, SidecarError> {
1630 self.require_owned_session(connection_id, session_id)?;
1631
1632 let vm_ids = self
1633 .sessions
1634 .get(session_id)
1635 .expect("owned session should exist")
1636 .vm_ids
1637 .iter()
1638 .cloned()
1639 .collect::<Vec<_>>();
1640
1641 let mut events = Vec::new();
1642 let mut first_error: Option<SidecarError> = None;
1643 for vm_id in vm_ids {
1644 match self
1648 .dispose_vm_internal(connection_id, session_id, &vm_id, reason.clone())
1649 .await
1650 {
1651 Ok(vm_events) => events.extend(vm_events),
1652 Err(error) => {
1653 if first_error.is_none() {
1654 first_error = Some(error);
1655 }
1656 }
1657 }
1658 }
1659
1660 if matches!(reason, DisposeReason::ConnectionClosed) {
1664 if let Err(error) = self
1665 .dispose_extension_session_state(connection_id, session_id)
1666 .await
1667 {
1668 if first_error.is_none() {
1669 first_error = Some(error);
1670 }
1671 }
1672 }
1673
1674 self.sessions.remove(session_id);
1675 if let Some(connection) = self.connections.get_mut(connection_id) {
1676 connection.sessions.remove(session_id);
1677 }
1678 self.disposed_sessions
1681 .push((connection_id.to_owned(), session_id.to_owned()));
1682
1683 if let Some(error) = first_error {
1684 return Err(error);
1685 }
1686 Ok(events)
1687 }
1688
1689 async fn dispose_extension_session_state(
1693 &mut self,
1694 connection_id: &str,
1695 session_id: &str,
1696 ) -> Result<(), SidecarError> {
1697 let ownership = OwnershipScope::session(connection_id, session_id);
1698 let extensions = self
1699 .extensions
1700 .values()
1701 .cloned()
1702 .collect::<Vec<Arc<dyn Extension>>>();
1703 let mut first_error: Option<SidecarError> = None;
1704 for extension in extensions {
1705 let snapshot = ExtensionSnapshot::new(
1706 extension.namespace().to_owned(),
1707 ownership.clone(),
1708 self.sidecar_requests.clone(),
1709 self.event_sink.clone(),
1710 );
1711 if let Err(error) = extension.on_session_disposed(snapshot).await {
1712 if first_error.is_none() {
1713 first_error = Some(error);
1714 }
1715 }
1716 }
1717 match first_error {
1718 Some(error) => Err(error),
1719 None => Ok(()),
1720 }
1721 }
1722
1723 pub(crate) fn take_disposed_sessions(&mut self) -> Vec<(String, String)> {
1726 std::mem::take(&mut self.disposed_sessions)
1727 }
1728
1729 fn kernel_wait_rpc_is_deferrable(
1740 &self,
1741 vm_id: &str,
1742 process_id: &str,
1743 request: &JavascriptSyncRpcRequest,
1744 ) -> bool {
1745 let Some(vm) = self.vms.get(vm_id) else {
1746 return false;
1747 };
1748 let Some(process) = vm.active_processes.get(process_id) else {
1749 return false;
1750 };
1751 if request.method == "__kernel_stdin_read"
1752 && matches!(
1753 process.execution,
1754 crate::state::ActiveExecution::Javascript(_)
1755 )
1756 && process.tty_master_fd.is_none()
1757 {
1758 return false;
1759 }
1760 true
1761 }
1762
1763 fn service_deferrable_kernel_wait_rpc(
1773 &mut self,
1774 vm_id: &str,
1775 process_id: &str,
1776 request: &JavascriptSyncRpcRequest,
1777 ) -> Result<Option<crate::execution::JavascriptSyncRpcServiceResponse>, SidecarError> {
1778 let requested_timeout_ms = match request.method.as_str() {
1779 "__kernel_stdin_read" => parse_kernel_stdin_read_args(request)?.1,
1780 _ => u64::try_from(parse_kernel_poll_args(request)?.1).unwrap_or(0),
1781 };
1782 let now = Instant::now();
1783
1784 let Some(vm) = self.vms.get_mut(vm_id) else {
1785 log_stale_process_event(&self.bridge, vm_id, process_id, "deferred kernel wait RPC");
1786 return Ok(None);
1787 };
1788 let wait_handle = vm.kernel.poll_wait_handle();
1789 let generation = wait_handle.snapshot();
1793 let Some(process) = vm.active_processes.get_mut(process_id) else {
1794 log_stale_process_event(&self.bridge, vm_id, process_id, "deferred kernel wait RPC");
1795 return Ok(None);
1796 };
1797 let kernel_pid = process.kernel_pid;
1798 let deadline = match &process.deferred_kernel_wait_rpc {
1799 Some((parked, parked_deadline)) if parked.id == request.id => *parked_deadline,
1800 _ => now + Duration::from_millis(requested_timeout_ms),
1801 };
1802 let probe = match request.method.as_str() {
1803 "__kernel_stdin_read" => {
1804 let (max_bytes, _) = parse_kernel_stdin_read_args(request)?;
1805 kernel_stdin_read_response(&mut vm.kernel, kernel_pid, max_bytes, Duration::ZERO)
1806 }
1807 _ => {
1808 let (fd_requests, _) = parse_kernel_poll_args(request)?;
1809 kernel_poll_response(&vm.kernel, kernel_pid, &fd_requests, 0)
1810 }
1811 };
1812 let Some(process) = vm.active_processes.get_mut(process_id) else {
1813 return Ok(None);
1814 };
1815 let probe = match probe {
1816 Ok(value) => value,
1817 Err(error) => {
1818 process.deferred_kernel_wait_rpc = None;
1819 return Err(error);
1820 }
1821 };
1822 let ready = match request.method.as_str() {
1823 "__kernel_stdin_read" => !probe.is_null(),
1824 _ => probe.get("readyCount").and_then(Value::as_u64).unwrap_or(0) > 0,
1825 };
1826 if ready || requested_timeout_ms == 0 || now >= deadline {
1827 process.deferred_kernel_wait_rpc = None;
1828 return Ok(Some(probe.into()));
1829 }
1830
1831 let connection_id = vm.connection_id.clone();
1832 let session_id = vm.session_id.clone();
1833 let remaining = deadline.saturating_duration_since(now);
1834 let sender = self.process_event_sender.clone();
1835 let waiter_request = request.clone();
1836 let envelope_vm_id = vm_id.to_owned();
1837 let envelope_process_id = process_id.to_owned();
1838 let spawned = std::thread::Builder::new()
1839 .name(String::from("kernel-wait-rpc"))
1840 .spawn(move || {
1841 let _ = wait_handle.wait_for_change(generation, Some(remaining));
1845 let _ = sender.blocking_send(ProcessEventEnvelope {
1846 connection_id,
1847 session_id,
1848 vm_id: envelope_vm_id,
1849 process_id: envelope_process_id,
1850 event: ActiveExecutionEvent::JavascriptSyncRpcRequest(waiter_request),
1851 });
1852 });
1853 let Some(vm) = self.vms.get_mut(vm_id) else {
1854 return Ok(None);
1855 };
1856 let Some(process) = vm.active_processes.get_mut(process_id) else {
1857 return Ok(None);
1858 };
1859 if spawned.is_err() {
1860 process.deferred_kernel_wait_rpc = None;
1863 return Ok(Some(probe.into()));
1864 }
1865 process.deferred_kernel_wait_rpc = Some((request.clone(), deadline));
1866 Ok(None)
1867 }
1868
1869 pub(crate) fn handle_javascript_sync_rpc_request(
1870 &mut self,
1871 vm_id: &str,
1872 process_id: &str,
1873 request: JavascriptSyncRpcRequest,
1874 ) -> Result<(), SidecarError> {
1875 record_sync_bridge_request_observed(request.id, &request.method);
1876 let Some(vm) = self.vms.get(vm_id) else {
1877 log_stale_process_event(&self.bridge, vm_id, process_id, "javascript sync RPC");
1878 return Ok(());
1879 };
1880 if !vm.active_processes.contains_key(process_id) {
1881 log_stale_process_event(&self.bridge, vm_id, process_id, "javascript sync RPC");
1882 return Ok(());
1883 }
1884
1885 let response: Result<crate::execution::JavascriptSyncRpcServiceResponse, SidecarError> =
1886 match request.method.as_str() {
1887 "child_process.spawn" => {
1888 let Some(vm) = self.vms.get(vm_id) else {
1889 log_stale_process_event(
1890 &self.bridge,
1891 vm_id,
1892 process_id,
1893 "javascript sync RPC child_process.spawn",
1894 );
1895 return Ok(());
1896 };
1897 let (payload, _) =
1898 parse_javascript_child_process_spawn_request(vm, &request.args)?;
1899 self.spawn_javascript_child_process(vm_id, process_id, payload)
1900 .map(Into::into)
1901 }
1902 "child_process.spawn_sync" => {
1903 let Some(vm) = self.vms.get(vm_id) else {
1904 log_stale_process_event(
1905 &self.bridge,
1906 vm_id,
1907 process_id,
1908 "javascript sync RPC child_process.spawn_sync",
1909 );
1910 return Ok(());
1911 };
1912 let (payload, max_buffer) =
1913 parse_javascript_child_process_spawn_request(vm, &request.args)?;
1914 self.spawn_javascript_child_process_sync(vm_id, process_id, payload, max_buffer)
1915 .map(Into::into)
1916 }
1917 "child_process.poll" => {
1918 let child_process_id = javascript_sync_rpc_arg_str(
1919 &request.args,
1920 0,
1921 "child_process.poll child id",
1922 )?;
1923 let wait_ms = javascript_sync_rpc_arg_u64_optional(
1924 &request.args,
1925 1,
1926 "child_process.poll wait ms",
1927 )?
1928 .unwrap_or_default();
1929 self.poll_javascript_child_process(vm_id, process_id, child_process_id, wait_ms)
1930 .map(Into::into)
1931 }
1932 "child_process.write_stdin" => {
1933 let child_process_id = javascript_sync_rpc_arg_str(
1934 &request.args,
1935 0,
1936 "child_process.write_stdin child id",
1937 )?;
1938 let chunk = javascript_sync_rpc_bytes_arg(
1939 &request.args,
1940 1,
1941 "child_process.write_stdin chunk",
1942 )?;
1943 self.write_javascript_child_process_stdin(
1944 vm_id,
1945 process_id,
1946 child_process_id,
1947 &chunk,
1948 )?;
1949 Ok(Value::Null.into())
1950 }
1951 "child_process.close_stdin" => {
1952 let child_process_id = javascript_sync_rpc_arg_str(
1953 &request.args,
1954 0,
1955 "child_process.close_stdin child id",
1956 )?;
1957 self.close_javascript_child_process_stdin(vm_id, process_id, child_process_id)?;
1958 Ok(Value::Null.into())
1959 }
1960 "child_process.kill" => {
1961 let child_process_id = javascript_sync_rpc_arg_str(
1962 &request.args,
1963 0,
1964 "child_process.kill child id",
1965 )?;
1966 let signal =
1967 javascript_sync_rpc_arg_str(&request.args, 1, "child_process.kill signal")?;
1968 self.kill_javascript_child_process(
1969 vm_id,
1970 process_id,
1971 child_process_id,
1972 signal,
1973 )?;
1974 Ok(Value::Null.into())
1975 }
1976 "process.kill" => {
1977 let target_pid =
1978 javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?;
1979 let signal =
1980 javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?;
1981 let parsed_signal = parse_signal(signal)?;
1982 if parsed_signal == 0 {
1983 let Some(vm) = self.vms.get(vm_id) else {
1984 log_stale_process_event(
1985 &self.bridge,
1986 vm_id,
1987 process_id,
1988 "javascript sync RPC process.kill",
1989 );
1990 return Ok(());
1991 };
1992 if !vm.active_processes.contains_key(process_id) {
1993 log_stale_process_event(
1994 &self.bridge,
1995 vm_id,
1996 process_id,
1997 "javascript sync RPC process.kill",
1998 );
1999 return Ok(());
2000 }
2001 vm.kernel
2002 .signal_process(EXECUTION_DRIVER_NAME, target_pid, parsed_signal)
2003 .map(|()| Value::Null.into())
2004 .map_err(kernel_error)
2005 } else if target_pid < 0 {
2006 let caller_kernel_pid = {
2007 let Some(vm) = self.vms.get(vm_id) else {
2008 log_stale_process_event(
2009 &self.bridge,
2010 vm_id,
2011 process_id,
2012 "javascript sync RPC process.kill",
2013 );
2014 return Ok(());
2015 };
2016 let Some(caller) = vm.active_processes.get(process_id) else {
2017 log_stale_process_event(
2018 &self.bridge,
2019 vm_id,
2020 process_id,
2021 "javascript sync RPC process.kill",
2022 );
2023 return Ok(());
2024 };
2025 caller.kernel_pid
2026 };
2027 let pgid = target_pid.unsigned_abs();
2028 match self.signal_vm_process_group(vm_id, caller_kernel_pid, pgid, signal) {
2029 Ok(true) => Ok(self
2030 .apply_self_process_kill(vm_id, process_id, parsed_signal)
2031 .into()),
2032 Ok(false) => Ok(Value::Null.into()),
2033 Err(error) => Err(error),
2034 }
2035 } else {
2036 enum ProcessKillTarget {
2037 SelfProcess,
2038 Child(String),
2039 TopLevel(String),
2040 KernelPid(u32),
2041 }
2042 let target = {
2043 let Some(vm) = self.vms.get(vm_id) else {
2044 log_stale_process_event(
2045 &self.bridge,
2046 vm_id,
2047 process_id,
2048 "javascript sync RPC process.kill",
2049 );
2050 return Ok(());
2051 };
2052 let Some(caller) = vm.active_processes.get(process_id) else {
2053 log_stale_process_event(
2054 &self.bridge,
2055 vm_id,
2056 process_id,
2057 "javascript sync RPC process.kill",
2058 );
2059 return Ok(());
2060 };
2061 let caller_pid = i32::try_from(caller.kernel_pid).map_err(|_| {
2062 SidecarError::InvalidState("caller pid exceeds i32".into())
2063 })?;
2064 if caller_pid == target_pid {
2065 ProcessKillTarget::SelfProcess
2066 } else if let Some((child_process_id, _)) =
2067 caller.child_processes.iter().find(|(_, child)| {
2068 i32::try_from(child.kernel_pid) == Ok(target_pid)
2069 })
2070 {
2071 ProcessKillTarget::Child(child_process_id.clone())
2072 } else if let Some((target_process_id, _)) =
2073 vm.active_processes.iter().find(|(_, process)| {
2074 i32::try_from(process.kernel_pid) == Ok(target_pid)
2075 })
2076 {
2077 ProcessKillTarget::TopLevel(target_process_id.clone())
2078 } else {
2079 let target_kernel_pid =
2080 u32::try_from(target_pid).map_err(|_| {
2081 SidecarError::InvalidState(format!(
2082 "EINVAL: invalid process pid {target_pid}"
2083 ))
2084 })?;
2085 ProcessKillTarget::KernelPid(target_kernel_pid)
2086 }
2087 };
2088 match target {
2089 ProcessKillTarget::SelfProcess => Ok(self
2090 .apply_self_process_kill(vm_id, process_id, parsed_signal)
2091 .into()),
2092 ProcessKillTarget::Child(child_process_id) => {
2093 self.kill_javascript_child_process(
2094 vm_id,
2095 process_id,
2096 &child_process_id,
2097 signal,
2098 )?;
2099 Ok(Value::Null.into())
2100 }
2101 ProcessKillTarget::TopLevel(target_process_id) => {
2102 self.kill_process_internal(vm_id, &target_process_id, signal)?;
2103 Ok(Value::Null.into())
2104 }
2105 ProcessKillTarget::KernelPid(target_kernel_pid) => {
2106 self.signal_vm_kernel_pid(vm_id, target_kernel_pid, signal)
2110 .map(|()| Value::Null.into())
2111 }
2112 }
2113 }
2114 }
2115 "process.signal_state" => {
2116 let (signal, registration) = parse_process_signal_state_request(&request.args)
2117 .map_err(|error| SidecarError::InvalidState(error.to_string()))?;
2118 let Some(vm) = self.vms.get_mut(vm_id) else {
2119 log_stale_process_event(
2120 &self.bridge,
2121 vm_id,
2122 process_id,
2123 "javascript sync RPC process.signal_state",
2124 );
2125 return Ok(());
2126 };
2127 apply_process_signal_state_update(
2128 &mut vm.signal_states,
2129 process_id,
2130 signal,
2131 registration,
2132 );
2133 Ok(Value::Null.into())
2134 }
2135 "net.http_request" => {
2136 let payload = request
2137 .args
2138 .first()
2139 .cloned()
2140 .ok_or_else(|| {
2141 SidecarError::InvalidState(String::from(
2142 "net.http_request requires a request payload",
2143 ))
2144 })
2145 .and_then(|value| {
2146 serde_json::from_value::<JavascriptHttpLoopbackRequest>(value).map_err(
2147 |error| {
2148 SidecarError::InvalidState(format!(
2149 "invalid net.http_request payload: {error}"
2150 ))
2151 },
2152 )
2153 })?;
2154 if !is_javascript_loopback_host(&payload.host) {
2155 return Err(SidecarError::Execution(format!(
2156 "EACCES: HTTP loopback request requires a loopback host, got {}",
2157 payload.host
2158 )));
2159 }
2160 self.bridge.require_network_access(
2161 vm_id,
2162 NetworkOperation::Http,
2163 format_tcp_resource(&payload.host, payload.port),
2164 )?;
2165 let Some(vm) = self.vms.get_mut(vm_id) else {
2166 log_stale_process_event(
2167 &self.bridge,
2168 vm_id,
2169 process_id,
2170 "javascript sync RPC net.http_request",
2171 );
2172 return Ok(());
2173 };
2174 let resource_limits = vm.kernel.resource_limits().clone();
2175 let socket_paths = build_javascript_socket_path_context(vm)?;
2176 let target_is_current =
2177 [JavascriptSocketFamily::Ipv4, JavascriptSocketFamily::Ipv6]
2178 .iter()
2179 .any(|family| {
2180 socket_paths
2181 .http_loopback_target(*family, payload.port)
2182 .is_some_and(|target| {
2183 target.process_id == payload.process_id
2184 && target.server_id == payload.server_id
2185 })
2186 });
2187 if !target_is_current {
2188 return Err(SidecarError::InvalidState(format!(
2189 "unknown HTTP loopback target {}:{} for server {} in process {}",
2190 payload.host, payload.port, payload.server_id, payload.process_id
2191 )));
2192 }
2193 let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness);
2194 let Some(target_process) = vm.active_processes.get_mut(&payload.process_id)
2195 else {
2196 return Err(SidecarError::InvalidState(format!(
2197 "unknown HTTP loopback process {}",
2198 payload.process_id
2199 )));
2200 };
2201 dispatch_loopback_http_request(LoopbackHttpDispatchRequest {
2202 bridge: &self.bridge,
2203 vm_id,
2204 dns: &vm.dns,
2205 socket_paths: &socket_paths,
2206 kernel: &mut vm.kernel,
2207 kernel_readiness,
2208 process: target_process,
2209 resource_limits: &resource_limits,
2210 server_id: payload.server_id,
2211 request_json: &payload.request,
2212 })
2213 .map(Value::String)
2214 .map(Into::into)
2215 }
2216 "__kernel_stdio_write"
2217 if self
2218 .vms
2219 .get(vm_id)
2220 .and_then(|vm| vm.active_processes.get(process_id))
2221 .is_some_and(|process| process.tty_master_owner.is_some()) =>
2222 {
2223 let (writer_kernel_pid, owner) = {
2224 let process = self
2225 .vms
2226 .get(vm_id)
2227 .and_then(|vm| vm.active_processes.get(process_id))
2228 .expect("guarded by match arm");
2229 (
2230 process.kernel_pid,
2231 process.tty_master_owner.expect("guarded by match arm"),
2232 )
2233 };
2234 self.service_shared_tty_stdio_write(vm_id, writer_kernel_pid, owner, &request)
2235 .map(Into::into)
2236 }
2237 "__kernel_stdin_read" | "__kernel_poll"
2238 if self.kernel_wait_rpc_is_deferrable(vm_id, process_id, &request) =>
2239 {
2240 match self.service_deferrable_kernel_wait_rpc(vm_id, process_id, &request) {
2241 Ok(Some(response)) => Ok(response),
2242 Ok(None) => return Ok(()),
2245 Err(error) => Err(error),
2246 }
2247 }
2248 _ => {
2249 let Some(vm) = self.vms.get_mut(vm_id) else {
2250 log_stale_process_event(
2251 &self.bridge,
2252 vm_id,
2253 process_id,
2254 "javascript sync RPC bridge dispatch",
2255 );
2256 return Ok(());
2257 };
2258 let resource_limits = vm.kernel.resource_limits().clone();
2259 let network_counts = vm_network_resource_counts(vm);
2260 let socket_paths = build_javascript_socket_path_context(vm)?;
2261 let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness);
2262 let Some(process) = vm.active_processes.get_mut(process_id) else {
2263 log_stale_process_event(
2264 &self.bridge,
2265 vm_id,
2266 process_id,
2267 "javascript sync RPC bridge dispatch",
2268 );
2269 return Ok(());
2270 };
2271 service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
2272 bridge: &self.bridge,
2273 vm_id,
2274 dns: &vm.dns,
2275 socket_paths: &socket_paths,
2276 kernel: &mut vm.kernel,
2277 kernel_readiness,
2278 process,
2279 sync_request: &request,
2280 resource_limits: &resource_limits,
2281 network_counts,
2282 })
2283 }
2284 };
2285
2286 let Some(vm) = self.vms.get_mut(vm_id) else {
2287 log_stale_process_event(
2288 &self.bridge,
2289 vm_id,
2290 process_id,
2291 "javascript sync RPC response delivery",
2292 );
2293 return Ok(());
2294 };
2295 let shadow_root = vm.cwd.clone();
2296 let Some(process) = vm.active_processes.get_mut(process_id) else {
2297 log_stale_process_event(
2298 &self.bridge,
2299 vm_id,
2300 process_id,
2301 "javascript sync RPC response delivery",
2302 );
2303 return Ok(());
2304 };
2305
2306 if response.is_ok()
2307 && matches!(
2308 request.method.as_str(),
2309 "fs.chmodSync" | "fs.promises.chmod"
2310 )
2311 {
2312 let guest_path =
2313 javascript_sync_rpc_arg_str(&request.args, 0, "filesystem chmod path")?;
2314 let mode =
2315 javascript_sync_rpc_arg_u32(&request.args, 1, "filesystem chmod mode")? & 0o7777;
2316 let host_path =
2317 shadow_host_path_for_process(&shadow_root, &process.guest_cwd, guest_path);
2318 if host_path.exists() {
2319 fs::set_permissions(&host_path, fs::Permissions::from_mode(mode)).map_err(
2320 |error| {
2321 SidecarError::Io(format!(
2322 "failed to mirror chmod to shadow path {}: {error}",
2323 host_path.display()
2324 ))
2325 },
2326 )?;
2327 }
2328 }
2329
2330 match response {
2331 Ok(result) => process
2332 .execution
2333 .respond_javascript_sync_rpc_response(request.id, result)
2334 .or_else(ignore_stale_javascript_sync_rpc_response),
2335 Err(error) => process
2336 .execution
2337 .respond_javascript_sync_rpc_error(
2338 request.id,
2339 javascript_sync_rpc_error_code(&error),
2340 error.to_string(),
2341 )
2342 .or_else(ignore_stale_javascript_sync_rpc_response),
2343 }
2344 }
2345
2346 fn apply_self_process_kill(
2349 &mut self,
2350 vm_id: &str,
2351 process_id: &str,
2352 parsed_signal: i32,
2353 ) -> Value {
2354 let action = self
2355 .vms
2356 .get(vm_id)
2357 .and_then(|vm| vm.signal_states.get(process_id))
2358 .and_then(|handlers| handlers.get(&(parsed_signal as u32)))
2359 .map(|registration| registration.action.clone())
2360 .unwrap_or(SignalDispositionAction::Default);
2361 if action == SignalDispositionAction::Default
2362 && parsed_signal != 0
2363 && !matches!(
2364 canonical_signal_name(parsed_signal),
2365 Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
2366 )
2367 {
2368 if let Some(vm) = self.vms.get_mut(vm_id) {
2369 if let Some(process) = vm.active_processes.get_mut(process_id) {
2370 process.pending_self_signal_exit = Some(parsed_signal);
2371 }
2372 }
2373 }
2374 json!({
2375 "self": true,
2376 "action": match action {
2377 SignalDispositionAction::Default => "default",
2378 SignalDispositionAction::Ignore => "ignore",
2379 SignalDispositionAction::User => "user",
2380 },
2381 })
2382 }
2383
2384 pub(crate) fn vm_ids_for_scope(
2385 &self,
2386 ownership: &OwnershipScope,
2387 ) -> Result<Vec<String>, SidecarError> {
2388 match ownership {
2389 OwnershipScope::SessionOwnership(inner) => {
2390 self.require_owned_session(&inner.connection_id, &inner.session_id)?;
2391 Ok(self
2392 .sessions
2393 .get(&inner.session_id)
2394 .expect("owned session should exist")
2395 .vm_ids
2396 .iter()
2397 .cloned()
2398 .collect())
2399 }
2400 OwnershipScope::VmOwnership(inner) => {
2401 self.require_owned_vm(&inner.connection_id, &inner.session_id, &inner.vm_id)?;
2402 Ok(vec![inner.vm_id.clone()])
2403 }
2404 OwnershipScope::ConnectionOwnership(..) => Err(SidecarError::InvalidState(
2405 String::from("event polling requires session or VM ownership scope"),
2406 )),
2407 }
2408 }
2409
2410 pub(crate) fn vm_ownership(&self, vm_id: &str) -> Result<OwnershipScope, SidecarError> {
2411 let vm = self
2412 .vms
2413 .get(vm_id)
2414 .ok_or_else(|| SidecarError::InvalidState(format!("unknown sidecar VM {vm_id}")))?;
2415 Ok(OwnershipScope::vm(&vm.connection_id, &vm.session_id, vm_id))
2416 }
2417
2418 pub(crate) fn vm_has_active_processes(&self, vm_id: &str) -> bool {
2419 self.vms
2420 .get(vm_id)
2421 .is_some_and(|vm| !vm.active_processes.is_empty())
2422 }
2423
2424 fn require_authenticated_connection(&self, connection_id: &str) -> Result<(), SidecarError> {
2425 if self.connections.contains_key(connection_id) {
2426 Ok(())
2427 } else {
2428 Err(SidecarError::InvalidState(format!(
2429 "connection {connection_id} has not authenticated"
2430 )))
2431 }
2432 }
2433
2434 pub(crate) fn require_owned_session(
2435 &self,
2436 connection_id: &str,
2437 session_id: &str,
2438 ) -> Result<(), SidecarError> {
2439 self.require_authenticated_connection(connection_id)?;
2440 let session = self.sessions.get(session_id).ok_or_else(|| {
2441 SidecarError::InvalidState(format!("unknown sidecar session {session_id}"))
2442 })?;
2443 if session.connection_id == connection_id {
2444 Ok(())
2445 } else {
2446 Err(SidecarError::InvalidState(format!(
2447 "session {session_id} is not owned by connection {connection_id}"
2448 )))
2449 }
2450 }
2451
2452 pub(crate) fn require_owned_vm(
2453 &self,
2454 connection_id: &str,
2455 session_id: &str,
2456 vm_id: &str,
2457 ) -> Result<(), SidecarError> {
2458 self.require_owned_session(connection_id, session_id)?;
2459 let vm = self
2460 .vms
2461 .get(vm_id)
2462 .ok_or_else(|| SidecarError::InvalidState(format!("unknown sidecar VM {vm_id}")))?;
2463 if vm.connection_id != connection_id || vm.session_id != session_id {
2464 return Err(SidecarError::InvalidState(format!(
2465 "VM {vm_id} is not owned by {connection_id}/{session_id}"
2466 )));
2467 }
2468 Ok(())
2469 }
2470
2471 fn connection_id_for(&self, ownership: &OwnershipScope) -> Result<String, SidecarError> {
2472 match ownership {
2473 OwnershipScope::ConnectionOwnership(inner) => Ok(inner.connection_id.clone()),
2474 OwnershipScope::SessionOwnership(..) | OwnershipScope::VmOwnership(..) => {
2475 Err(SidecarError::InvalidState(String::from(
2476 "request requires connection ownership scope",
2477 )))
2478 }
2479 }
2480 }
2481
2482 fn validate_auth_token(&self, auth_token: &str) -> Result<(), SidecarError> {
2483 let Some(expected_auth_token) = self.config.expected_auth_token.as_deref() else {
2484 return Ok(());
2485 };
2486
2487 if auth_token == expected_auth_token {
2488 Ok(())
2489 } else {
2490 Err(SidecarError::Unauthorized(String::from(
2491 "authenticate request provided an invalid auth token",
2492 )))
2493 }
2494 }
2495
2496 fn allocate_connection_id(&mut self) -> String {
2497 self.next_connection_id += 1;
2498 format!("conn-{}", self.next_connection_id)
2499 }
2500
2501 fn take_matching_process_event_envelope(
2502 &mut self,
2503 vm_id: &str,
2504 process_id: &str,
2505 ) -> Result<Option<ProcessEventEnvelope>, SidecarError> {
2506 if let Some(index) = self
2507 .pending_process_events
2508 .iter()
2509 .position(|event| event.vm_id == vm_id && event.process_id == process_id)
2510 {
2511 return Ok(self.pending_process_events.remove(index));
2512 }
2513
2514 let mut matching_envelope = None;
2515 let mut deferred = Vec::new();
2516 {
2517 let pending_capacity = self.pending_process_event_capacity();
2518 let receiver = self.process_event_receiver.as_mut().ok_or_else(|| {
2519 SidecarError::InvalidState(String::from("process event receiver unavailable"))
2520 })?;
2521 loop {
2522 if deferred.len() >= pending_capacity {
2523 if receiver.is_empty() {
2524 break;
2525 }
2526 return Err(process_event_queue_overflow_error());
2527 }
2528 let envelope = match receiver.try_recv() {
2529 Ok(envelope) => envelope,
2530 Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
2531 Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
2532 };
2533 if matching_envelope.is_none()
2534 && envelope.vm_id == vm_id
2535 && envelope.process_id == process_id
2536 {
2537 matching_envelope = Some(envelope);
2538 break;
2539 }
2540 deferred.push(envelope);
2541 }
2542 }
2543 for envelope in deferred {
2544 self.queue_pending_process_event(envelope)?;
2545 }
2546
2547 Ok(matching_envelope)
2548 }
2549
2550 fn allocate_sidecar_request_id(&mut self) -> RequestId {
2551 let request_id = self.next_sidecar_request_id;
2552 self.next_sidecar_request_id -= 1;
2553 request_id
2554 }
2555
2556 pub(crate) fn session_scope_for(
2557 &self,
2558 ownership: &OwnershipScope,
2559 ) -> Result<(String, String), SidecarError> {
2560 match ownership {
2561 OwnershipScope::SessionOwnership(inner) => {
2562 Ok((inner.connection_id.clone(), inner.session_id.clone()))
2563 }
2564 OwnershipScope::ConnectionOwnership(..) | OwnershipScope::VmOwnership(..) => {
2565 Err(SidecarError::InvalidState(String::from(
2566 "request requires session ownership scope",
2567 )))
2568 }
2569 }
2570 }
2571
2572 pub(crate) fn vm_scope_for(
2573 &self,
2574 ownership: &OwnershipScope,
2575 ) -> Result<(String, String, String), SidecarError> {
2576 match ownership {
2577 OwnershipScope::VmOwnership(inner) => Ok((
2578 inner.connection_id.clone(),
2579 inner.session_id.clone(),
2580 inner.vm_id.clone(),
2581 )),
2582 OwnershipScope::ConnectionOwnership(..) | OwnershipScope::SessionOwnership(..) => Err(
2583 SidecarError::InvalidState(String::from("request requires VM ownership scope")),
2584 ),
2585 }
2586 }
2587
2588 pub(crate) fn respond(
2589 &self,
2590 request: &RequestFrame,
2591 payload: ResponsePayload,
2592 ) -> ResponseFrame {
2593 shared_respond(request, payload)
2594 }
2595
2596 fn reject(&self, request: &RequestFrame, code: &str, message: &str) -> ResponseFrame {
2597 shared_reject(request, code, message)
2598 }
2599
2600 pub fn queue_sidecar_request(
2601 &mut self,
2602 ownership: OwnershipScope,
2603 payload: SidecarRequestPayload,
2604 ) -> Result<RequestId, SidecarError> {
2605 if self.outbound_sidecar_requests.len() >= MAX_OUTBOUND_SIDECAR_REQUESTS {
2606 return Err(outbound_sidecar_request_queue_overflow_error());
2607 }
2608 if self.pending_sidecar_responses.pending_count() >= MAX_PENDING_SIDECAR_RESPONSES {
2609 return Err(sidecar_response_pending_overflow_error());
2610 }
2611 let request_id = self.allocate_sidecar_request_id();
2612 let request = SidecarRequestFrame::new(request_id, ownership, payload);
2613 self.pending_sidecar_responses
2614 .register_request(&request)
2615 .map_err(sidecar_response_tracker_error)?;
2616 self.outbound_sidecar_requests.push_back(request);
2617 self.outbound_sidecar_requests_gauge
2618 .observe_depth(self.outbound_sidecar_requests.len());
2619 self.pending_sidecar_responses_gauge
2620 .observe_depth(self.pending_sidecar_responses.pending_count());
2621 Ok(request_id)
2622 }
2623
2624 pub fn queue_wire_sidecar_request(
2625 &mut self,
2626 ownership: crate::wire::OwnershipScope,
2627 payload: crate::wire::SidecarRequestPayload,
2628 ) -> Result<crate::wire::RequestId, SidecarError> {
2629 let ownership = crate::wire::ownership_scope_to_compat(ownership);
2630 let payload = crate::wire::sidecar_request_payload_to_compat(&ownership, payload)
2631 .map_err(wire_protocol_error)?;
2632 self.queue_sidecar_request(ownership, payload)
2633 }
2634
2635 pub fn pop_sidecar_request(&mut self) -> Option<SidecarRequestFrame> {
2636 let request = self.outbound_sidecar_requests.pop_front();
2637 self.outbound_sidecar_requests_gauge
2638 .observe_depth(self.outbound_sidecar_requests.len());
2639 request
2640 }
2641
2642 pub fn pop_wire_sidecar_request(
2643 &mut self,
2644 ) -> Result<Option<crate::wire::SidecarRequestFrame>, SidecarError> {
2645 self.pop_sidecar_request()
2646 .map(crate::wire::sidecar_request_frame_from_compat)
2647 .transpose()
2648 .map_err(wire_protocol_error)
2649 }
2650
2651 pub fn accept_sidecar_response(
2652 &mut self,
2653 response: SidecarResponseFrame,
2654 ) -> Result<(), SidecarError> {
2655 match self.pending_sidecar_responses.accept_response(&response) {
2656 Ok(()) => {}
2657 Err(
2664 error @ (SidecarResponseTrackerError::UnmatchedResponse { .. }
2665 | SidecarResponseTrackerError::DuplicateResponse { .. }),
2666 ) => {
2667 tracing::warn!(
2668 request_id = response.request_id,
2669 "dropping stale sidecar response with no matching pending request: {error}"
2670 );
2671 return Ok(());
2672 }
2673 Err(error) => return Err(sidecar_response_tracker_error(error)),
2674 }
2675 self.pending_sidecar_responses_gauge
2676 .observe_depth(self.pending_sidecar_responses.pending_count());
2677 self.completed_sidecar_response_order
2678 .push_back(response.request_id);
2679 self.completed_sidecar_responses
2680 .insert(response.request_id, response);
2681 self.completed_sidecar_responses_gauge
2682 .observe_depth(self.completed_sidecar_responses.len());
2683 while self.completed_sidecar_responses.len() > MAX_COMPLETED_SIDECAR_RESPONSES {
2684 match self.completed_sidecar_response_order.pop_front() {
2685 Some(evicted) => {
2689 if self.completed_sidecar_responses.remove(&evicted).is_some() {
2690 tracing::warn!(
2691 queue = "completed_sidecar_responses",
2692 evicted_request_id = evicted,
2693 capacity = MAX_COMPLETED_SIDECAR_RESPONSES,
2694 "dropping an unretrieved completed sidecar response to stay within cap; the host can no longer fetch it (response lost)"
2695 );
2696 self.completed_sidecar_responses_gauge
2697 .observe_depth(self.completed_sidecar_responses.len());
2698 }
2699 }
2700 None => break,
2701 }
2702 }
2703 Ok(())
2704 }
2705
2706 pub fn accept_wire_sidecar_response(
2707 &mut self,
2708 response: crate::wire::SidecarResponseFrame,
2709 ) -> Result<(), SidecarError> {
2710 let response =
2711 crate::wire::sidecar_response_frame_to_compat(response).map_err(wire_protocol_error)?;
2712 self.accept_sidecar_response(response)
2713 }
2714
2715 pub fn take_sidecar_response(&mut self, request_id: RequestId) -> Option<SidecarResponseFrame> {
2716 let response = self.completed_sidecar_responses.remove(&request_id);
2717 if response.is_some() {
2718 self.completed_sidecar_response_order
2719 .retain(|completed_id| completed_id != &request_id);
2720 self.completed_sidecar_responses_gauge
2721 .observe_depth(self.completed_sidecar_responses.len());
2722 }
2723 response
2724 }
2725
2726 pub fn take_wire_sidecar_response(
2727 &mut self,
2728 request_id: crate::wire::RequestId,
2729 ) -> Result<Option<crate::wire::SidecarResponseFrame>, SidecarError> {
2730 self.take_sidecar_response(request_id)
2731 .map(|response| {
2732 crate::wire::sidecar_response_frame_from_compat(response)
2733 .map_err(wire_protocol_error)
2734 })
2735 .transpose()
2736 }
2737
2738 pub(crate) fn vm_lifecycle_event(
2739 &self,
2740 connection_id: &str,
2741 session_id: &str,
2742 vm_id: &str,
2743 state: VmLifecycleState,
2744 ) -> EventFrame {
2745 shared_vm_lifecycle_event(connection_id, session_id, vm_id, state)
2746 }
2747
2748 fn ensure_request_within_frame_limit(
2749 &self,
2750 request: &RequestFrame,
2751 ) -> Result<(), SidecarError> {
2752 let frame = crate::protocol::to_generated_protocol_frame(
2753 &crate::protocol::ProtocolFrame::Request(request.clone()),
2754 )
2755 .map_err(|error| {
2756 SidecarError::InvalidState(format!("failed to convert request frame: {error}"))
2757 })?;
2758 let crate::wire::ProtocolFrame::RequestFrame(_) = &frame else {
2759 return Err(SidecarError::InvalidState(String::from(
2760 "request converted to non-request wire frame",
2761 )));
2762 };
2763
2764 crate::wire::WireFrameCodec::new(self.config.max_frame_bytes)
2765 .encode(&frame)
2766 .map(|_| ())
2767 .map_err(|error| SidecarError::FrameTooLarge(error.to_string()))
2768 }
2769}
2770
2771impl<B> ExtensionHost for NativeSidecar<B>
2772where
2773 B: NativeSidecarBridge + Send + 'static,
2774 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
2775{
2776 fn spawn_process<'a>(
2777 &'a mut self,
2778 ownership: OwnershipScope,
2779 payload: ExecuteRequest,
2780 ) -> ExtensionFuture<'a, ProcessStartedResponse> {
2781 Box::pin(async move {
2782 let request = RequestFrame::new(0, ownership, RequestPayload::Execute(payload.clone()));
2783 let dispatch = NativeSidecar::execute(self, &request, payload).await?;
2784 match dispatch.response.payload {
2785 ResponsePayload::ProcessStarted(response) => Ok(response),
2786 other => Err(unexpected_extension_host_response("execute", other)),
2787 }
2788 })
2789 }
2790
2791 fn write_stdin<'a>(
2792 &'a mut self,
2793 ownership: OwnershipScope,
2794 payload: WriteStdinRequest,
2795 ) -> ExtensionFuture<'a, StdinWrittenResponse> {
2796 Box::pin(async move {
2797 let request =
2798 RequestFrame::new(0, ownership, RequestPayload::WriteStdin(payload.clone()));
2799 let dispatch = NativeSidecar::write_stdin(self, &request, payload).await?;
2800 match dispatch.response.payload {
2801 ResponsePayload::StdinWritten(response) => Ok(response),
2802 other => Err(unexpected_extension_host_response("write_stdin", other)),
2803 }
2804 })
2805 }
2806
2807 fn close_stdin<'a>(
2808 &'a mut self,
2809 ownership: OwnershipScope,
2810 payload: CloseStdinRequest,
2811 ) -> ExtensionFuture<'a, StdinClosedResponse> {
2812 Box::pin(async move {
2813 let request =
2814 RequestFrame::new(0, ownership, RequestPayload::CloseStdin(payload.clone()));
2815 let dispatch = NativeSidecar::close_stdin(self, &request, payload).await?;
2816 match dispatch.response.payload {
2817 ResponsePayload::StdinClosed(response) => Ok(response),
2818 other => Err(unexpected_extension_host_response("close_stdin", other)),
2819 }
2820 })
2821 }
2822
2823 fn kill_process<'a>(
2824 &'a mut self,
2825 ownership: OwnershipScope,
2826 payload: KillProcessRequest,
2827 ) -> ExtensionFuture<'a, ProcessKilledResponse> {
2828 Box::pin(async move {
2829 let request =
2830 RequestFrame::new(0, ownership, RequestPayload::KillProcess(payload.clone()));
2831 let dispatch = NativeSidecar::kill_process(self, &request, payload).await?;
2832 match dispatch.response.payload {
2833 ResponsePayload::ProcessKilled(response) => Ok(response),
2834 other => Err(unexpected_extension_host_response("kill_process", other)),
2835 }
2836 })
2837 }
2838
2839 fn poll_event<'a>(
2840 &'a mut self,
2841 ownership: OwnershipScope,
2842 timeout: Duration,
2843 ) -> ExtensionFuture<'a, Option<EventFrame>> {
2844 Box::pin(async move { NativeSidecar::poll_event(self, &ownership, timeout).await })
2845 }
2846
2847 fn projected_agents<'a>(
2848 &'a mut self,
2849 ownership: OwnershipScope,
2850 ) -> ExtensionFuture<'a, Vec<crate::extension::ProjectedAgentLaunchEntry>> {
2851 Box::pin(async move {
2852 let (connection_id, session_id, vm_id) = self.vm_scope_for(&ownership)?;
2853 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
2854 let vm = self.vms.get(&vm_id).ok_or_else(|| {
2855 SidecarError::InvalidState(format!("unknown VM {vm_id}"))
2856 })?;
2857 Ok(vm
2858 .projected_agent_launch
2859 .iter()
2860 .map(|(id, launch): (&String, &crate::state::ProjectedAgentLaunch)| crate::extension::ProjectedAgentLaunchEntry {
2861 id: id.clone(),
2862 acp_entrypoint: launch.acp_entrypoint.clone(),
2863 env: launch.env.clone(),
2864 launch_args: launch.launch_args.clone(),
2865 })
2866 .collect())
2867 })
2868 }
2869
2870 fn guest_filesystem_call<'a>(
2871 &'a mut self,
2872 ownership: OwnershipScope,
2873 payload: GuestFilesystemCallRequest,
2874 ) -> ExtensionFuture<'a, GuestFilesystemResultResponse> {
2875 Box::pin(async move {
2876 let request = RequestFrame::new(
2877 0,
2878 ownership,
2879 RequestPayload::GuestFilesystemCall(payload.clone()),
2880 );
2881 let dispatch = NativeSidecar::guest_filesystem_call(self, &request, payload).await?;
2882 match dispatch.response.payload {
2883 ResponsePayload::GuestFilesystemResult(response) => Ok(response),
2884 other => Err(unexpected_extension_host_response(
2885 "guest_filesystem_call",
2886 other,
2887 )),
2888 }
2889 })
2890 }
2891
2892 fn bind_process_to_session<'a>(
2893 &'a mut self,
2894 ownership: OwnershipScope,
2895 namespace: String,
2896 ext_session_id: String,
2897 process_id: String,
2898 ) -> ExtensionFuture<'a, ()> {
2899 Box::pin(async move {
2900 self.bind_extension_process_resource(ownership, namespace, ext_session_id, process_id)
2901 })
2902 }
2903
2904 fn bind_vm_to_session<'a>(
2905 &'a mut self,
2906 ownership: OwnershipScope,
2907 namespace: String,
2908 ext_session_id: String,
2909 ) -> ExtensionFuture<'a, ()> {
2910 Box::pin(
2911 async move { self.bind_extension_vm_resource(ownership, namespace, ext_session_id) },
2912 )
2913 }
2914
2915 fn dispose_session_resources<'a>(
2916 &'a mut self,
2917 ownership: OwnershipScope,
2918 namespace: String,
2919 ext_session_id: String,
2920 ) -> ExtensionFuture<'a, Vec<EventFrame>> {
2921 Box::pin(async move {
2922 let key = (namespace, ext_session_id);
2923 let Some(resources) = self.extension_sessions.get(&key) else {
2924 return Ok(Vec::new());
2925 };
2926 if resources.ownership != ownership {
2927 return Err(SidecarError::InvalidState(String::from(
2928 "extension session ownership did not match dispose request",
2929 )));
2930 }
2931 let resources = self
2932 .extension_sessions
2933 .remove(&key)
2934 .expect("extension resources existed before removal");
2935 let (connection_id, session_id, vm_id) = self.vm_scope_for(&ownership)?;
2936 for process_id in resources.process_ids {
2937 if self
2938 .vms
2939 .get(&vm_id)
2940 .is_some_and(|vm| vm.active_processes.contains_key(&process_id))
2941 {
2942 self.kill_process_internal(&vm_id, &process_id, "SIGTERM")?;
2943 }
2944 }
2945 let mut events = Vec::new();
2946 for resource_vm_id in resources.vm_ids {
2947 if self.vms.contains_key(&resource_vm_id) {
2948 events.extend(
2949 self.dispose_vm_internal(
2950 &connection_id,
2951 &session_id,
2952 &resource_vm_id,
2953 DisposeReason::Requested,
2954 )
2955 .await?,
2956 );
2957 }
2958 }
2959 Ok(events)
2960 })
2961 }
2962
2963 fn start_buffering_process_output<'a>(
2964 &'a mut self,
2965 ownership: OwnershipScope,
2966 process_id: String,
2967 ) -> ExtensionFuture<'a, ()> {
2968 Box::pin(async move {
2969 let (connection_id, session_id, vm_id) = self.vm_scope_for(&ownership)?;
2970 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
2971 let key = (vm_id, process_id);
2972 if self.extension_process_output_buffers.contains_key(&key) {
2973 return Err(SidecarError::Conflict(String::from(
2974 "extension process output buffering already started",
2975 )));
2976 }
2977 self.extension_process_output_buffers
2978 .insert(key, ExtensionBufferedProcessOutput::default());
2979 Ok(())
2980 })
2981 }
2982
2983 fn handoff_buffered_process_output<'a>(
2984 &'a mut self,
2985 ownership: OwnershipScope,
2986 namespace: String,
2987 ext_session_id: String,
2988 process_id: String,
2989 timeout: Duration,
2990 ) -> ExtensionFuture<'a, ExtensionBufferedProcessOutput> {
2991 Box::pin(async move {
2992 let (connection_id, session_id, vm_id) = self.vm_scope_for(&ownership)?;
2993 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
2994 let key = (vm_id.clone(), process_id.clone());
2995 let deadline = Instant::now() + timeout;
2996 loop {
2997 self.pump_process_events(&ownership).await?;
2998 while let Some(envelope) =
2999 self.take_matching_process_event_envelope(&vm_id, &process_id)?
3000 {
3001 if self.capture_extension_process_output_event(
3002 &vm_id,
3003 &process_id,
3004 &envelope.event,
3005 ) {
3006 continue;
3007 }
3008 self.queue_pending_process_event(envelope)?;
3009 break;
3010 }
3011 let buffered = self
3012 .extension_process_output_buffers
3013 .get(&key)
3014 .is_some_and(|buffer| !buffer.stdout.is_empty() || !buffer.stderr.is_empty());
3015 if buffered || timeout.is_zero() || Instant::now() >= deadline {
3016 break;
3017 }
3018 let remaining = deadline.saturating_duration_since(Instant::now());
3019 time::sleep(remaining.min(Duration::from_millis(10))).await;
3020 }
3021 self.bind_extension_process_resource(
3022 ownership,
3023 namespace,
3024 ext_session_id,
3025 process_id.clone(),
3026 )?;
3027 self.extension_process_output_buffers
3028 .remove(&key)
3029 .ok_or_else(|| {
3030 SidecarError::InvalidState(String::from(
3031 "extension process output buffering was not started",
3032 ))
3033 })
3034 })
3035 }
3036}
3037
3038fn unexpected_extension_host_response(operation: &str, payload: ResponsePayload) -> SidecarError {
3039 match payload {
3040 ResponsePayload::Rejected(response) => SidecarError::InvalidState(format!(
3041 "extension {operation} rejected with {}: {}",
3042 response.code, response.message
3043 )),
3044 other => SidecarError::InvalidState(format!(
3045 "extension {operation} returned unexpected response: {other:?}"
3046 )),
3047 }
3048}
3049
3050fn shadow_host_path_for_process(
3051 shadow_root: &Path,
3052 process_guest_cwd: &str,
3053 guest_path: &str,
3054) -> PathBuf {
3055 let normalized_guest_path = if guest_path.starts_with('/') {
3056 normalize_path(guest_path)
3057 } else {
3058 normalize_path(&format!(
3059 "{}/{}",
3060 process_guest_cwd.trim_end_matches('/'),
3061 guest_path
3062 ))
3063 };
3064 if normalized_guest_path == "/" {
3065 shadow_root.to_path_buf()
3066 } else {
3067 shadow_root.join(normalized_guest_path.trim_start_matches('/'))
3068 }
3069}
3070
3071fn sidecar_response_tracker_error(error: SidecarResponseTrackerError) -> SidecarError {
3072 SidecarError::InvalidState(format!(
3073 "invalid sidecar response correlation state: {error}"
3074 ))
3075}
3076
3077fn map_bridge_permission(decision: agentos_bridge::PermissionDecision) -> PermissionDecision {
3078 match decision.verdict {
3079 agentos_bridge::PermissionVerdict::Allow => PermissionDecision::allow(),
3080 agentos_bridge::PermissionVerdict::Deny => PermissionDecision::deny(
3081 decision
3082 .reason
3083 .unwrap_or_else(|| String::from("denied by host")),
3084 ),
3085 agentos_bridge::PermissionVerdict::Prompt => PermissionDecision::deny(
3086 decision
3087 .reason
3088 .unwrap_or_else(|| String::from("permission prompt required")),
3089 ),
3090 }
3091}
3092
3093fn audit_timestamp() -> String {
3094 SystemTime::now()
3095 .duration_since(UNIX_EPOCH)
3096 .expect("system time before unix epoch")
3097 .as_millis()
3098 .to_string()
3099}
3100
3101pub(crate) fn audit_fields<I, K, V>(fields: I) -> BTreeMap<String, String>
3102where
3103 I: IntoIterator<Item = (K, V)>,
3104 K: Into<String>,
3105 V: Into<String>,
3106{
3107 let mut mapped = BTreeMap::from([(String::from("timestamp"), audit_timestamp())]);
3108 for (key, value) in fields {
3109 mapped.insert(key.into(), value.into());
3110 }
3111 mapped
3112}
3113
3114pub(crate) fn emit_structured_event<B>(
3115 bridge: &SharedBridge<B>,
3116 vm_id: &str,
3117 name: &str,
3118 fields: BTreeMap<String, String>,
3119) -> Result<(), SidecarError>
3120where
3121 B: NativeSidecarBridge + Send + 'static,
3122 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
3123{
3124 bridge.with_mut(|bridge| {
3125 bridge.emit_structured_event(StructuredEventRecord {
3126 vm_id: vm_id.to_owned(),
3127 name: name.to_owned(),
3128 fields,
3129 })
3130 })
3131}
3132
3133pub(crate) fn emit_security_audit_event<B>(
3134 bridge: &SharedBridge<B>,
3135 vm_id: &str,
3136 name: &str,
3137 fields: BTreeMap<String, String>,
3138) where
3139 B: NativeSidecarBridge + Send + 'static,
3140 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
3141{
3142 let _ = emit_structured_event(bridge, vm_id, name, fields);
3143}
3144
3145pub(crate) fn structured_event_frame(
3152 connection_id: &str,
3153 name: &str,
3154 detail: std::collections::HashMap<String, String>,
3155) -> Result<crate::wire::EventFrame, SidecarError> {
3156 let event = EventFrame::new(
3157 OwnershipScope::connection(connection_id),
3158 EventPayload::Structured(crate::protocol::StructuredEvent {
3159 name: name.to_owned(),
3160 detail,
3161 }),
3162 );
3163 crate::wire::event_frame_from_compat(event).map_err(|error| {
3164 SidecarError::InvalidState(format!("invalid structured event frame: {error}"))
3165 })
3166}
3167
3168pub(crate) fn log_stale_process_event<B>(
3169 bridge: &SharedBridge<B>,
3170 vm_id: &str,
3171 process_id: &str,
3172 context: &str,
3173) where
3174 B: NativeSidecarBridge + Send + 'static,
3175 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
3176{
3177 let _ = bridge.emit_log(
3178 vm_id,
3179 format!(
3180 "Ignoring stale process event during {context}: VM {vm_id} process {process_id} was already reaped"
3181 ),
3182 );
3183}
3184
3185pub(crate) fn root_filesystem_error(error: impl std::fmt::Display) -> SidecarError {
3188 SidecarError::InvalidState(format!("root filesystem: {error}"))
3189}
3190
3191pub(crate) fn normalize_path(path: &str) -> String {
3192 let mut segments = Vec::new();
3193 for component in Path::new(path).components() {
3194 match component {
3195 Component::RootDir => segments.clear(),
3196 Component::ParentDir => {
3197 segments.pop();
3198 }
3199 Component::CurDir => {}
3200 Component::Normal(value) => segments.push(value.to_string_lossy().into_owned()),
3201 Component::Prefix(prefix) => {
3202 segments.push(prefix.as_os_str().to_string_lossy().into_owned());
3203 }
3204 }
3205 }
3206
3207 let normalized = format!("/{}", segments.join("/"));
3208 if normalized.is_empty() {
3209 String::from("/")
3210 } else {
3211 normalized
3212 }
3213}
3214
3215pub(crate) fn normalize_host_path(path: &Path) -> PathBuf {
3216 let mut normalized = PathBuf::new();
3217
3218 for component in path.components() {
3219 match component {
3220 Component::Prefix(prefix) => normalized.push(prefix.as_os_str()),
3221 Component::RootDir => normalized.push(Path::new("/")),
3222 Component::CurDir => {}
3223 Component::ParentDir => {
3224 if normalized != Path::new("/") {
3225 normalized.pop();
3226 }
3227 }
3228 Component::Normal(part) => normalized.push(part),
3229 }
3230 }
3231
3232 if normalized.as_os_str().is_empty() {
3233 if path.is_absolute() {
3234 PathBuf::from("/")
3235 } else {
3236 PathBuf::from(".")
3237 }
3238 } else {
3239 normalized
3240 }
3241}
3242
3243pub(crate) fn path_is_within_root(path: &Path, root: &Path) -> bool {
3244 path == root || path.starts_with(root)
3245}
3246
3247pub(crate) fn dirname(path: &str) -> String {
3248 let normalized = normalize_path(path);
3249 let parent = Path::new(&normalized)
3250 .parent()
3251 .unwrap_or_else(|| Path::new("/"));
3252 let value = parent.to_string_lossy();
3253 if value.is_empty() {
3254 String::from("/")
3255 } else {
3256 value.into_owned()
3257 }
3258}
3259
3260pub(crate) fn kernel_error(error: KernelError) -> SidecarError {
3261 SidecarError::Kernel(error.to_string())
3262}
3263
3264pub(crate) fn plugin_error(error: PluginError) -> SidecarError {
3265 SidecarError::Plugin(error.to_string())
3266}
3267
3268pub(crate) fn javascript_error(error: JavascriptExecutionError) -> SidecarError {
3269 SidecarError::Execution(error.to_string())
3270}
3271
3272pub(crate) fn wasm_error(error: WasmExecutionError) -> SidecarError {
3273 SidecarError::Execution(error.to_string())
3274}
3275
3276pub(crate) fn python_error(error: PythonExecutionError) -> SidecarError {
3277 SidecarError::Execution(error.to_string())
3278}
3279
3280pub(crate) fn vfs_error(error: VfsError) -> SidecarError {
3281 SidecarError::Kernel(error.to_string())
3282}
3283
3284#[allow(dead_code)]
3294const HOISTED_NODE_MODULES_GUIDANCE: &str = "secure-exec can't load mounted node_modules: the directory uses a non-flat layout (pnpm / bun / yarn workspaces store, or yarn Plug'n'Play) whose package store isn't visible inside the VM. A flat (hoisted) node_modules is required.\n - pnpm -> add `node-linker=hoisted` to .npmrc, then reinstall\n - yarn berry -> set `nodeLinker: node-modules` in .yarnrc.yml (not pnp/pnpm)\n - bun -> install dependencies outside a workspace (workspaces use a .bun store)\n - npm / yarn classic -> already flat, no change needed";
3295
3296#[allow(dead_code)]
3306fn symlinked_node_modules_hint(stderr: &str) -> Option<&'static str> {
3307 const STORE_MARKERS: &[&str] = &[
3312 "node_modules/.pnpm/",
3313 "node_modules/.bun/",
3314 "node_modules/.store/",
3315 "/__virtual__/",
3316 ];
3317 const PNP_STRICT_MARKERS: &[&str] = &["isn't declared in your dependencies"];
3321 const PNP_PATH_MARKERS: &[&str] = &[".pnp.cjs", ".pnp.loader.mjs", "/.yarn/cache/"];
3322
3323 if PNP_STRICT_MARKERS.iter().any(|m| stderr.contains(m)) {
3324 return Some(HOISTED_NODE_MODULES_GUIDANCE);
3325 }
3326
3327 let missing = stderr.contains("ENOENT")
3328 || stderr.contains("no such file or directory")
3329 || stderr.contains("Cannot find module")
3330 || stderr.contains("MODULE_NOT_FOUND");
3331 if !missing {
3332 return None;
3333 }
3334 if STORE_MARKERS.iter().any(|m| stderr.contains(m))
3335 || PNP_PATH_MARKERS.iter().any(|m| stderr.contains(m))
3336 {
3337 return Some(HOISTED_NODE_MODULES_GUIDANCE);
3338 }
3339 None
3340}
3341
3342#[cfg(test)]
3343mod symlinked_node_modules_hint_tests {
3344 use super::symlinked_node_modules_hint;
3345
3346 #[test]
3348 fn matches_pnpm_store_enoent() {
3349 let stderr = "Error: ENOENT: no such file or directory, open '/root/node_modules/.pnpm/@mariozechner+pi-coding-agent@0.60.0_x/node_modules/@mariozechner/pi-coding-agent/dist/package.json'";
3352 let hint = symlinked_node_modules_hint(stderr).expect("expected hoisted guidance");
3353 assert!(hint.contains("secure-exec can't load mounted node_modules"));
3354 assert!(!hint.contains("agentos"));
3355 }
3356
3357 #[test]
3358 fn matches_bun_store_enoent() {
3359 let stderr = "Error: ENOENT: no such file or directory, open '/root/node_modules/.bun/is-odd@3.0.1/node_modules/is-odd/package.json'";
3360 assert!(symlinked_node_modules_hint(stderr).is_some());
3361 }
3362
3363 #[test]
3364 fn matches_yarn_pnpm_store_enoent() {
3365 let stderr = "Error: ENOENT: no such file or directory, open '/root/node_modules/.store/is-odd-npm-3.0.1-93c3c3f41b/package/package.json'";
3366 assert!(symlinked_node_modules_hint(stderr).is_some());
3367 }
3368
3369 #[test]
3370 fn matches_pnp_declared_error() {
3371 let stderr = "Error: Your application tried to access is-number, but it isn't declared in your dependencies; this makes the require call ambiguous and unsound.";
3373 assert!(symlinked_node_modules_hint(stderr).is_some());
3374 }
3375
3376 #[test]
3377 fn matches_pnp_cjs_module_not_found() {
3378 let stderr = "Error: Cannot find module 'is-odd'\n at /root/.pnp.cjs:12345:18\n code: 'MODULE_NOT_FOUND'";
3379 assert!(symlinked_node_modules_hint(stderr).is_some());
3380 }
3381
3382 #[test]
3383 fn matches_virtual_instance() {
3384 let stderr = "Error: ENOENT: no such file or directory, open '/root/.yarn/__virtual__/is-odd-abc/1/node_modules/is-odd/package.json'";
3385 assert!(symlinked_node_modules_hint(stderr).is_some());
3386 }
3387
3388 #[test]
3390 fn ignores_enoent_outside_a_store() {
3391 let stderr = "Error: ENOENT: no such file or directory, open '/tmp/scratch/config.json'";
3392 assert!(symlinked_node_modules_hint(stderr).is_none());
3393 }
3394
3395 #[test]
3396 fn ignores_store_path_without_missing_file() {
3397 let stderr =
3398 "loaded /root/node_modules/.pnpm/some-pkg@1.0.0/node_modules/some-pkg/index.js";
3399 assert!(symlinked_node_modules_hint(stderr).is_none());
3400 }
3401
3402 #[test]
3403 fn ignores_flat_node_modules_enoent() {
3404 let stderr = "Error: ENOENT: no such file or directory, open '/root/node_modules/is-odd/missing-asset.json'";
3406 assert!(symlinked_node_modules_hint(stderr).is_none());
3407 }
3408
3409 #[test]
3410 fn ignores_unrelated_failure() {
3411 let stderr = "Error: connect ECONNREFUSED 127.0.0.1:443";
3412 assert!(symlinked_node_modules_hint(stderr).is_none());
3413 }
3414}
3415
3416#[cfg(test)]
3417mod structured_event_frame_tests {
3418 use super::*;
3419
3420 #[test]
3421 fn structured_event_frame_round_trips_limit_warning() {
3422 let mut detail = std::collections::HashMap::new();
3423 let limit_name = TrackedLimit::JavascriptEventChannel.as_str();
3425 detail.insert(String::from("limit"), String::from(limit_name));
3426 detail.insert(String::from("fillPercent"), String::from("82"));
3427
3428 let wire = structured_event_frame("conn-1", "limit_warning", detail)
3429 .expect("build structured event frame");
3430 let compat = crate::wire::event_frame_to_compat(wire).expect("convert to compat");
3431
3432 match compat.payload {
3433 EventPayload::Structured(event) => {
3434 assert_eq!(event.name, "limit_warning");
3435 assert_eq!(
3436 event.detail.get("limit").map(String::as_str),
3437 Some(limit_name)
3438 );
3439 assert_eq!(
3440 event.detail.get("fillPercent").map(String::as_str),
3441 Some("82")
3442 );
3443 }
3444 other => panic!("expected structured payload, got {other:?}"),
3445 }
3446 match compat.ownership {
3447 OwnershipScope::ConnectionOwnership(inner) => {
3448 assert_eq!(inner.connection_id, "conn-1");
3449 }
3450 other => panic!("expected connection ownership, got {other:?}"),
3451 }
3452 }
3453}
3454
3455#[cfg(test)]
3456mod dispose_lifecycle_tests {
3457 use super::*;
3458 use crate::extension::ExtensionResponse;
3459 use crate::stdio::LocalBridge;
3460 use std::sync::atomic::{AtomicUsize, Ordering};
3461
3462 fn block_on<F: std::future::Future>(future: F) -> F::Output {
3463 tokio::runtime::Builder::new_current_thread()
3464 .enable_all()
3465 .build()
3466 .expect("dispose lifecycle test runtime")
3467 .block_on(future)
3468 }
3469
3470 fn test_sidecar() -> NativeSidecar<LocalBridge> {
3471 NativeSidecar::new(LocalBridge::default()).expect("build test sidecar")
3472 }
3473
3474 fn insert_session(
3477 sidecar: &mut NativeSidecar<LocalBridge>,
3478 connection_id: &str,
3479 session_id: &str,
3480 vm_ids: BTreeSet<String>,
3481 ) {
3482 sidecar.connections.insert(
3483 connection_id.to_string(),
3484 ConnectionState {
3485 auth_token: String::new(),
3486 sessions: BTreeSet::from([session_id.to_string()]),
3487 },
3488 );
3489 sidecar.sessions.insert(
3490 session_id.to_string(),
3491 SessionState {
3492 connection_id: connection_id.to_string(),
3493 placement: crate::protocol::SidecarPlacement::SidecarPlacementShared(
3494 crate::protocol::SidecarPlacementShared { pool: None },
3495 ),
3496 metadata: BTreeMap::new(),
3497 vm_ids,
3498 },
3499 );
3500 }
3501
3502 struct RecordingExtension {
3503 namespace: String,
3504 session_disposed: Arc<AtomicUsize>,
3505 }
3506
3507 impl Extension for RecordingExtension {
3508 fn namespace(&self) -> &str {
3509 &self.namespace
3510 }
3511
3512 fn handle_request<'a>(
3513 &'a self,
3514 _ctx: ExtensionContext<'a>,
3515 _payload: Vec<u8>,
3516 ) -> ExtensionFuture<'a, ExtensionResponse> {
3517 Box::pin(async { Ok(ExtensionResponse::new(Vec::new())) })
3518 }
3519
3520 fn on_session_disposed<'a>(&'a self, _ctx: ExtensionSnapshot) -> ExtensionFuture<'a, ()> {
3521 let counter = self.session_disposed.clone();
3522 Box::pin(async move {
3523 counter.fetch_add(1, Ordering::SeqCst);
3524 Ok(())
3525 })
3526 }
3527 }
3528
3529 fn register_recording_extension(sidecar: &mut NativeSidecar<LocalBridge>) -> Arc<AtomicUsize> {
3530 let counter = Arc::new(AtomicUsize::new(0));
3531 sidecar
3532 .register_extension(Box::new(RecordingExtension {
3533 namespace: String::from("dev.test.dispose"),
3534 session_disposed: counter.clone(),
3535 }))
3536 .expect("register recording extension");
3537 counter
3538 }
3539
3540 #[test]
3543 fn connection_closed_dispose_invokes_extension_session_teardown() {
3544 let mut sidecar = test_sidecar();
3545 let counter = register_recording_extension(&mut sidecar);
3546 insert_session(&mut sidecar, "conn-1", "session-1", BTreeSet::new());
3547
3548 block_on(sidecar.dispose_session("conn-1", "session-1", DisposeReason::ConnectionClosed))
3549 .expect("dispose session on connection close");
3550
3551 assert_eq!(
3552 counter.load(Ordering::SeqCst),
3553 1,
3554 "extension session-teardown hook must fire on ConnectionClosed"
3555 );
3556 assert!(
3557 !sidecar.sessions.contains_key("session-1"),
3558 "the disposed session must be reclaimed"
3559 );
3560 }
3561
3562 #[test]
3565 fn requested_dispose_does_not_invoke_extension_session_teardown() {
3566 let mut sidecar = test_sidecar();
3567 let counter = register_recording_extension(&mut sidecar);
3568 insert_session(&mut sidecar, "conn-1", "session-1", BTreeSet::new());
3569
3570 block_on(sidecar.dispose_session("conn-1", "session-1", DisposeReason::Requested))
3571 .expect("dispose session on request");
3572
3573 assert_eq!(
3574 counter.load(Ordering::SeqCst),
3575 0,
3576 "the teardown hook is reserved for client disconnect"
3577 );
3578 }
3579
3580 #[test]
3582 fn dispose_session_records_disposed_scope() {
3583 let mut sidecar = test_sidecar();
3584 insert_session(&mut sidecar, "conn-1", "session-1", BTreeSet::new());
3585
3586 block_on(sidecar.dispose_session("conn-1", "session-1", DisposeReason::Requested))
3587 .expect("dispose session");
3588
3589 assert_eq!(
3590 sidecar.take_disposed_sessions(),
3591 vec![(String::from("conn-1"), String::from("session-1"))],
3592 "dispose must publish the session scope so stdio can untrack it"
3593 );
3594 }
3595
3596 #[test]
3601 fn reclaim_vm_tracking_clears_every_per_vm_map() {
3602 let mut sidecar = test_sidecar();
3603 insert_session(
3604 &mut sidecar,
3605 "conn-1",
3606 "session-1",
3607 BTreeSet::from([String::from("vm-1")]),
3608 );
3609 sidecar.extension_process_output_buffers.insert(
3610 (String::from("vm-1"), String::from("proc-1")),
3611 ExtensionBufferedProcessOutput::default(),
3612 );
3613 sidecar.extension_sessions.insert(
3614 (String::from("ns"), String::from("ext-sess-1")),
3615 ExtensionSessionResources {
3616 ownership: OwnershipScope::vm("conn-1", "session-1", "vm-1"),
3617 process_ids: BTreeSet::new(),
3618 vm_ids: BTreeSet::from([String::from("vm-1")]),
3619 },
3620 );
3621
3622 sidecar.reclaim_vm_tracking("session-1", "vm-1");
3623
3624 assert!(
3625 sidecar.extension_process_output_buffers.is_empty(),
3626 "M6: the output-buffer map must be reclaimed on VM disposal"
3627 );
3628 assert!(
3629 sidecar.extension_sessions.is_empty(),
3630 "H1: an extension session bound only to the VM must be reclaimed"
3631 );
3632 assert!(
3633 !sidecar
3634 .sessions
3635 .get("session-1")
3636 .expect("session present")
3637 .vm_ids
3638 .contains("vm-1"),
3639 "the VM id must be removed from its session"
3640 );
3641 }
3642
3643 #[test]
3647 fn dispose_session_reclaims_session_even_when_a_vm_dispose_fails() {
3648 let mut sidecar = test_sidecar();
3649 insert_session(
3650 &mut sidecar,
3651 "conn-1",
3652 "session-1",
3653 BTreeSet::from([String::from("vm-a"), String::from("vm-b")]),
3654 );
3655
3656 let result =
3657 block_on(sidecar.dispose_session("conn-1", "session-1", DisposeReason::Requested));
3658
3659 assert!(
3660 result.is_err(),
3661 "a failing VM dispose must still surface an error"
3662 );
3663 assert!(
3664 !sidecar.sessions.contains_key("session-1"),
3665 "the session must be reclaimed even though VM dispose failed"
3666 );
3667 }
3668}