1use crate::{
2 BrowserSidecarBridge, BrowserWorkerEntrypoint, BrowserWorkerHandle, BrowserWorkerHandleRequest,
3 BrowserWorkerOsConfig, BrowserWorkerProcessConfig, BrowserWorkerSpawnRequest,
4};
5use agentos_bridge::{
6 BridgeTypes, CreateJavascriptContextRequest, CreateWasmContextRequest, ExecutionEvent,
7 ExecutionHandleRequest, GuestContextHandle, GuestRuntime, KillExecutionRequest,
8 LifecycleEventRecord, LifecycleState, PollExecutionEventRequest, SignalDispositionAction,
9 SignalHandlerRegistration, StartExecutionRequest, StartedExecution, StructuredEventRecord,
10 WriteExecutionStdinRequest,
11};
12use agentos_kernel::bridge::LifecycleState as KernelLifecycleState;
13use agentos_kernel::kernel::{KernelError, KernelVm, KernelVmConfig, VirtualProcessOptions};
14use agentos_kernel::mount_table::MountTable;
15use agentos_kernel::poll::{PollTargetEntry, POLLERR, POLLHUP, POLLIN};
16use agentos_kernel::process_table::ProcessStatus;
17use agentos_kernel::root_fs::{
18 RootFilesystemMode as KernelRootFilesystemMode, RootFilesystemSnapshot,
19};
20use agentos_kernel::socket_table::{
21 InetSocketAddress, SocketId, SocketSpec, SocketState, SocketType,
22};
23use agentos_native_sidecar_core::{
24 apply_process_signal_state_update, build_root_mount_table,
25 ensure_vm_fetch_raw_response_buffer_within_limit, ensure_vm_fetch_response_within_limit,
26 handle_guest_filesystem_call, handle_guest_kernel_call, parse_kernel_http_fetch_response,
27 process_snapshot_entry_from_kernel, serialize_kernel_http_fetch_request,
28 unsupported_guest_kernel_call_detail, SharedProcessSnapshotEntry, VmLayerStore,
29 VM_FETCH_BUFFER_LIMIT_BYTES,
30};
31use agentos_native_sidecar_core::{
32 ensure_command_aliases_available, ensure_toolkit_name_available,
33 ensure_toolkit_registry_capacity, registered_tool_command_names, shared_guest_runtime_identity,
34 validate_toolkit_registration,
35};
36use agentos_sidecar_protocol::protocol::{
37 FindBoundUdpRequest, FindListenerRequest, GuestFilesystemCallRequest,
38 GuestFilesystemResultResponse, GuestKernelCallRequest, GuestKernelResultResponse,
39 ProjectedModuleDescriptor, RegisterHostCallbacksRequest, RootFilesystemEntry,
40 SignalDispositionAction as ProtocolSignalDispositionAction,
41 SignalHandlerRegistration as ProtocolSignalHandlerRegistration, SocketStateEntry,
42 WasmPermissionTier,
43};
44use agentos_vm_config::RootFilesystemConfig;
45use std::collections::{BTreeMap, BTreeSet};
46use std::error::Error;
47use std::fmt;
48use std::time::{Duration, Instant};
49
50type BridgeError<B> = <B as BridgeTypes>::Error;
51type BrowserKernel = KernelVm<MountTable>;
52const BROWSER_WORKER_DRIVER: &str = "browser.worker";
53const BROWSER_VM_FETCH_TIMEOUT: Duration = Duration::from_secs(30);
54#[cfg(not(target_arch = "wasm32"))]
55const BROWSER_VM_FETCH_TIMEOUT_MS_ENV: &str = "AGENTOS_TEST_BROWSER_VM_FETCH_TIMEOUT_MS";
56
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct BrowserSidecarConfig {
59 pub sidecar_id: String,
60}
61
62impl Default for BrowserSidecarConfig {
63 fn default() -> Self {
64 Self {
65 sidecar_id: String::from("agentos-native-sidecar-browser"),
66 }
67 }
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub enum BrowserSidecarError {
72 InvalidState(String),
73 Kernel(String),
74 Bridge(String),
75}
76
77impl fmt::Display for BrowserSidecarError {
78 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 match self {
80 Self::InvalidState(message) | Self::Kernel(message) | Self::Bridge(message) => {
81 f.write_str(message)
82 }
83 }
84 }
85}
86
87impl Error for BrowserSidecarError {}
88
89struct VmState {
90 kernel: BrowserKernel,
91 configuration: BrowserVmConfiguration,
92 layers: VmLayerStore,
93 toolkits: BTreeMap<String, RegisterHostCallbacksRequest>,
94 signal_states: BTreeMap<String, BTreeMap<u32, ProtocolSignalHandlerRegistration>>,
95 contexts: BTreeSet<String>,
96 active_executions: BTreeSet<String>,
97}
98
99#[derive(Debug, Clone, Default, PartialEq, Eq)]
100struct BrowserVmConfiguration {
101 instructions: Vec<String>,
102 projected_modules: Vec<ProjectedModuleDescriptor>,
103 command_permissions: BTreeMap<String, WasmPermissionTier>,
104 create_loopback_exempt_ports: BTreeSet<u16>,
105 loopback_exempt_ports: Vec<u16>,
106}
107
108#[derive(Debug, Clone, Default, PartialEq, Eq)]
109pub struct BrowserExecutionOptions {
110 pub command_name: Option<String>,
111 pub wasm_permission_tier: Option<WasmPermissionTier>,
112}
113
114#[derive(Debug, Clone, PartialEq, Eq)]
115struct ContextState {
116 vm_id: String,
117 runtime: GuestRuntime,
118 entrypoint: BrowserWorkerEntrypoint,
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
122struct ExecutionState {
123 vm_id: String,
124 worker: BrowserWorkerHandle,
125 kernel_pid: u32,
126 stdin_write_fd: u32,
127 cwd: String,
128}
129
130pub trait BrowserExtension: Send + Sync {
131 fn namespace(&self) -> &str;
132
133 fn handle_request(
134 &self,
135 context: &mut BrowserExtensionContext<'_>,
136 payload: &[u8],
137 ) -> Result<Vec<u8>, BrowserSidecarError> {
138 let _ = context;
139 let _ = payload;
140 Err(BrowserSidecarError::InvalidState(format!(
141 "browser extension {} does not handle requests",
142 self.namespace()
143 )))
144 }
145}
146
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct BrowserExtensionRequest {
149 pub namespace: String,
150 pub payload: Vec<u8>,
151 pub vm_id: Option<String>,
155 pub connection_id: Option<String>,
158}
159
160#[derive(Debug, Clone, PartialEq, Eq)]
161pub struct BrowserExtensionResponse {
162 pub namespace: String,
163 pub payload: Vec<u8>,
164}
165
166pub trait BrowserExtensionHost {
167 fn write_file(
168 &mut self,
169 vm_id: &str,
170 path: &str,
171 contents: Vec<u8>,
172 ) -> Result<(), BrowserSidecarError>;
173
174 fn read_file(&mut self, vm_id: &str, path: &str) -> Result<Vec<u8>, BrowserSidecarError>;
175
176 fn mkdir(
177 &mut self,
178 vm_id: &str,
179 path: &str,
180 recursive: bool,
181 ) -> Result<(), BrowserSidecarError>;
182
183 fn read_dir(&mut self, vm_id: &str, path: &str) -> Result<Vec<String>, BrowserSidecarError>;
184
185 fn create_javascript_context(
186 &mut self,
187 request: CreateJavascriptContextRequest,
188 ) -> Result<GuestContextHandle, BrowserSidecarError>;
189
190 fn create_wasm_context(
191 &mut self,
192 request: CreateWasmContextRequest,
193 ) -> Result<GuestContextHandle, BrowserSidecarError>;
194
195 fn start_execution(
196 &mut self,
197 request: StartExecutionRequest,
198 ) -> Result<StartedExecution, BrowserSidecarError>;
199
200 fn write_stdin(
201 &mut self,
202 request: WriteExecutionStdinRequest,
203 ) -> Result<(), BrowserSidecarError>;
204
205 fn close_stdin(&mut self, request: ExecutionHandleRequest) -> Result<(), BrowserSidecarError>;
206
207 fn kill_execution(&mut self, request: KillExecutionRequest) -> Result<(), BrowserSidecarError>;
208
209 fn poll_execution_event(
210 &mut self,
211 request: PollExecutionEventRequest,
212 ) -> Result<Option<ExecutionEvent>, BrowserSidecarError>;
213}
214
215pub struct BrowserExtensionContext<'a> {
216 host: &'a mut dyn BrowserExtensionHost,
217 vm_id: Option<String>,
218 connection_id: Option<String>,
219}
220
221impl<'a> BrowserExtensionContext<'a> {
222 pub fn new(host: &'a mut dyn BrowserExtensionHost) -> Self {
223 Self {
224 host,
225 vm_id: None,
226 connection_id: None,
227 }
228 }
229
230 pub fn with_ownership(
233 host: &'a mut dyn BrowserExtensionHost,
234 vm_id: Option<String>,
235 connection_id: Option<String>,
236 ) -> Self {
237 Self {
238 host,
239 vm_id,
240 connection_id,
241 }
242 }
243
244 pub fn vm_id(&self) -> Option<&str> {
246 self.vm_id.as_deref()
247 }
248
249 pub fn connection_id(&self) -> Option<&str> {
251 self.connection_id.as_deref()
252 }
253
254 pub fn write_file(
255 &mut self,
256 vm_id: &str,
257 path: &str,
258 contents: impl Into<Vec<u8>>,
259 ) -> Result<(), BrowserSidecarError> {
260 self.host.write_file(vm_id, path, contents.into())
261 }
262
263 pub fn read_file(&mut self, vm_id: &str, path: &str) -> Result<Vec<u8>, BrowserSidecarError> {
264 self.host.read_file(vm_id, path)
265 }
266
267 pub fn mkdir(
268 &mut self,
269 vm_id: &str,
270 path: &str,
271 recursive: bool,
272 ) -> Result<(), BrowserSidecarError> {
273 self.host.mkdir(vm_id, path, recursive)
274 }
275
276 pub fn read_dir(
277 &mut self,
278 vm_id: &str,
279 path: &str,
280 ) -> Result<Vec<String>, BrowserSidecarError> {
281 self.host.read_dir(vm_id, path)
282 }
283
284 pub fn create_javascript_context(
285 &mut self,
286 request: CreateJavascriptContextRequest,
287 ) -> Result<GuestContextHandle, BrowserSidecarError> {
288 self.host.create_javascript_context(request)
289 }
290
291 pub fn create_wasm_context(
292 &mut self,
293 request: CreateWasmContextRequest,
294 ) -> Result<GuestContextHandle, BrowserSidecarError> {
295 self.host.create_wasm_context(request)
296 }
297
298 pub fn start_execution(
299 &mut self,
300 request: StartExecutionRequest,
301 ) -> Result<StartedExecution, BrowserSidecarError> {
302 self.host.start_execution(request)
303 }
304
305 pub fn write_stdin(
306 &mut self,
307 request: WriteExecutionStdinRequest,
308 ) -> Result<(), BrowserSidecarError> {
309 self.host.write_stdin(request)
310 }
311
312 pub fn close_stdin(
313 &mut self,
314 request: ExecutionHandleRequest,
315 ) -> Result<(), BrowserSidecarError> {
316 self.host.close_stdin(request)
317 }
318
319 pub fn kill_execution(
320 &mut self,
321 request: KillExecutionRequest,
322 ) -> Result<(), BrowserSidecarError> {
323 self.host.kill_execution(request)
324 }
325
326 pub fn poll_execution_event(
327 &mut self,
328 request: PollExecutionEventRequest,
329 ) -> Result<Option<ExecutionEvent>, BrowserSidecarError> {
330 self.host.poll_execution_event(request)
331 }
332}
333
334pub struct BrowserSidecar<B> {
335 bridge: B,
336 config: BrowserSidecarConfig,
337 vms: BTreeMap<String, VmState>,
338 contexts: BTreeMap<String, ContextState>,
339 executions: BTreeMap<String, ExecutionState>,
340 extensions: BTreeMap<String, Box<dyn BrowserExtension>>,
341}
342
343impl<B> BrowserSidecar<B>
344where
345 B: BrowserSidecarBridge,
346 BridgeError<B>: fmt::Debug,
347{
348 pub fn new(bridge: B, config: BrowserSidecarConfig) -> Self {
349 Self::with_extensions(bridge, config, Vec::new())
350 .expect("empty browser extension registry should be valid")
351 }
352
353 pub fn with_extensions(
354 bridge: B,
355 config: BrowserSidecarConfig,
356 extensions: Vec<Box<dyn BrowserExtension>>,
357 ) -> Result<Self, BrowserSidecarError> {
358 let mut sidecar = Self {
359 bridge,
360 config,
361 vms: BTreeMap::new(),
362 contexts: BTreeMap::new(),
363 executions: BTreeMap::new(),
364 extensions: BTreeMap::new(),
365 };
366 for extension in extensions {
367 sidecar.register_extension(extension)?;
368 }
369 Ok(sidecar)
370 }
371
372 pub fn register_extension(
373 &mut self,
374 extension: Box<dyn BrowserExtension>,
375 ) -> Result<(), BrowserSidecarError> {
376 let namespace = extension.namespace();
377 if namespace.is_empty() {
378 return Err(BrowserSidecarError::InvalidState(String::from(
379 "browser extension namespace must not be empty",
380 )));
381 }
382 if self.extensions.contains_key(namespace) {
383 return Err(BrowserSidecarError::InvalidState(format!(
384 "browser extension namespace already registered: {namespace}",
385 )));
386 }
387 self.extensions.insert(namespace.to_string(), extension);
388 Ok(())
389 }
390
391 pub fn extension_count(&self) -> usize {
392 self.extensions.len()
393 }
394
395 pub fn has_extension(&self, namespace: &str) -> bool {
396 self.extensions.contains_key(namespace)
397 }
398
399 pub fn dispatch_extension_request(
400 &mut self,
401 request: BrowserExtensionRequest,
402 ) -> Result<BrowserExtensionResponse, BrowserSidecarError> {
403 let Some(extension) = self.extensions.remove(&request.namespace) else {
404 return Err(BrowserSidecarError::InvalidState(format!(
405 "no browser extension registered for namespace {}",
406 request.namespace
407 )));
408 };
409 let payload = {
410 let mut context = BrowserExtensionContext::with_ownership(
411 self,
412 request.vm_id.clone(),
413 request.connection_id.clone(),
414 );
415 extension.handle_request(&mut context, &request.payload)
416 };
417 self.extensions.insert(request.namespace.clone(), extension);
418 let payload = payload?;
419 Ok(BrowserExtensionResponse {
420 namespace: request.namespace,
421 payload,
422 })
423 }
424
425 pub fn sidecar_id(&self) -> &str {
426 &self.config.sidecar_id
427 }
428
429 pub fn bridge(&self) -> &B {
430 &self.bridge
431 }
432
433 pub fn bridge_mut(&mut self) -> &mut B {
434 &mut self.bridge
435 }
436
437 pub fn into_bridge(self) -> B {
438 self.bridge
439 }
440
441 pub fn vm_count(&self) -> usize {
442 self.vms.len()
443 }
444
445 pub fn context_count(&self, vm_id: &str) -> usize {
446 self.vms
447 .get(vm_id)
448 .map(|vm| vm.contexts.len())
449 .unwrap_or_default()
450 }
451
452 pub fn active_worker_count(&self, vm_id: &str) -> usize {
453 self.vms
454 .get(vm_id)
455 .map(|vm| vm.active_executions.len())
456 .unwrap_or_default()
457 }
458
459 pub fn create_vm(&mut self, config: KernelVmConfig) -> Result<(), BrowserSidecarError> {
460 self.create_vm_with_root_filesystem(config, RootFilesystemConfig::default())
461 }
462
463 pub fn configure_vm(
464 &mut self,
465 vm_id: &str,
466 permissions: Option<agentos_kernel::permissions::Permissions>,
467 instructions: Vec<String>,
468 projected_modules: Vec<ProjectedModuleDescriptor>,
469 command_permissions: BTreeMap<String, WasmPermissionTier>,
470 loopback_exempt_ports: impl IntoIterator<Item = u16>,
471 ) -> Result<(), BrowserSidecarError> {
472 let vm = self
473 .vms
474 .get_mut(vm_id)
475 .ok_or_else(|| BrowserSidecarError::InvalidState(format!("unknown VM {vm_id}")))?;
476 if let Some(permissions) = permissions {
477 vm.kernel.set_permissions(permissions);
478 }
479 let loopback_exempt_ports: Vec<u16> = loopback_exempt_ports.into_iter().collect();
480 vm.configuration.instructions = instructions;
481 vm.configuration.projected_modules = projected_modules;
482 vm.configuration.command_permissions = command_permissions;
483 vm.configuration.loopback_exempt_ports = loopback_exempt_ports.clone();
484 let mut effective_loopback_exempt_ports =
485 vm.configuration.create_loopback_exempt_ports.clone();
486 effective_loopback_exempt_ports.extend(loopback_exempt_ports);
487 vm.kernel
488 .set_loopback_exempt_ports(effective_loopback_exempt_ports);
489 Ok(())
490 }
491
492 pub fn create_vm_with_root_filesystem(
493 &mut self,
494 config: KernelVmConfig,
495 root_filesystem: RootFilesystemConfig,
496 ) -> Result<(), BrowserSidecarError> {
497 let vm_id = config.vm_id.clone();
498 if self.vms.contains_key(&vm_id) {
499 return Err(BrowserSidecarError::InvalidState(format!(
500 "browser sidecar VM already exists: {vm_id}"
501 )));
502 }
503
504 self.emit_lifecycle(
505 &vm_id,
506 LifecycleState::Starting,
507 Some(String::from(
508 "browser sidecar booting kernel on main thread",
509 )),
510 )?;
511 let create_loopback_exempt_ports = config.loopback_exempt_ports.clone();
512 self.vms.insert(
513 vm_id.clone(),
514 VmState {
515 kernel: KernelVm::new(
516 build_root_mount_table(&root_filesystem, &config.resources)
517 .map_err(Self::sidecar_core_error)?,
518 config,
519 ),
520 configuration: BrowserVmConfiguration {
521 create_loopback_exempt_ports,
522 ..BrowserVmConfiguration::default()
523 },
524 layers: VmLayerStore::default(),
525 toolkits: BTreeMap::new(),
526 signal_states: BTreeMap::new(),
527 contexts: BTreeSet::new(),
528 active_executions: BTreeSet::new(),
529 },
530 );
531 if let Some(root) = self
532 .vms
533 .get_mut(&vm_id)
534 .and_then(|vm| vm.kernel.root_filesystem_mut())
535 {
536 root.finish_bootstrap();
537 }
538 self.emit_lifecycle(
539 &vm_id,
540 LifecycleState::Ready,
541 Some(String::from(
542 "browser sidecar kernel is ready on the main thread",
543 )),
544 )?;
545 Ok(())
546 }
547
548 pub fn write_file(
549 &mut self,
550 vm_id: &str,
551 path: &str,
552 contents: impl Into<Vec<u8>>,
553 ) -> Result<(), BrowserSidecarError> {
554 let vm = self.vm_mut(vm_id)?;
555 vm.kernel
556 .write_file(path, contents)
557 .map_err(Self::kernel_error)
558 }
559
560 pub fn read_file(&mut self, vm_id: &str, path: &str) -> Result<Vec<u8>, BrowserSidecarError> {
561 let vm = self.vm_mut(vm_id)?;
562 vm.kernel.read_file(path).map_err(Self::kernel_error)
563 }
564
565 pub fn mkdir(
566 &mut self,
567 vm_id: &str,
568 path: &str,
569 recursive: bool,
570 ) -> Result<(), BrowserSidecarError> {
571 let vm = self.vm_mut(vm_id)?;
572 vm.kernel.mkdir(path, recursive).map_err(Self::kernel_error)
573 }
574
575 pub fn read_dir(
576 &mut self,
577 vm_id: &str,
578 path: &str,
579 ) -> Result<Vec<String>, BrowserSidecarError> {
580 let vm = self.vm_mut(vm_id)?;
581 vm.kernel.read_dir(path).map_err(Self::kernel_error)
582 }
583
584 pub fn snapshot_root_filesystem(
585 &mut self,
586 vm_id: &str,
587 ) -> Result<RootFilesystemSnapshot, BrowserSidecarError> {
588 let vm = self.vm_mut(vm_id)?;
589 vm.kernel
590 .snapshot_root_filesystem()
591 .map_err(Self::kernel_error)
592 }
593
594 pub fn create_layer(&mut self, vm_id: &str) -> Result<String, BrowserSidecarError> {
595 self.vm_mut(vm_id)?
596 .layers
597 .create_writable_layer()
598 .map_err(Self::sidecar_core_error)
599 }
600
601 pub fn seal_layer(
602 &mut self,
603 vm_id: &str,
604 layer_id: &str,
605 ) -> Result<String, BrowserSidecarError> {
606 self.vm_mut(vm_id)?
607 .layers
608 .seal_layer(layer_id)
609 .map_err(Self::sidecar_core_error)
610 }
611
612 pub fn import_snapshot(
613 &mut self,
614 vm_id: &str,
615 entries: &[RootFilesystemEntry],
616 ) -> Result<String, BrowserSidecarError> {
617 let snapshot = root_snapshot_from_entries(entries)?;
618 self.vm_mut(vm_id)?
619 .layers
620 .import_snapshot(snapshot)
621 .map_err(Self::sidecar_core_error)
622 }
623
624 pub fn export_snapshot(
625 &mut self,
626 vm_id: &str,
627 layer_id: &str,
628 ) -> Result<RootFilesystemSnapshot, BrowserSidecarError> {
629 self.vm_mut(vm_id)?
630 .layers
631 .export_snapshot(layer_id)
632 .map_err(Self::sidecar_core_error)
633 }
634
635 pub fn create_overlay(
636 &mut self,
637 vm_id: &str,
638 mode: KernelRootFilesystemMode,
639 upper_layer_id: Option<String>,
640 lower_layer_ids: Vec<String>,
641 ) -> Result<String, BrowserSidecarError> {
642 self.vm_mut(vm_id)?
643 .layers
644 .create_overlay_layer(mode, upper_layer_id, lower_layer_ids)
645 .map_err(Self::sidecar_core_error)
646 }
647
648 pub fn register_host_callbacks(
649 &mut self,
650 vm_id: &str,
651 payload: RegisterHostCallbacksRequest,
652 ) -> Result<(String, u32), BrowserSidecarError> {
653 validate_toolkit_registration(&payload)
654 .map_err(|error| BrowserSidecarError::InvalidState(error.to_string()))?;
655 let vm = self.vm_mut(vm_id)?;
656 ensure_toolkit_name_available(&vm.toolkits, &payload.name)
657 .map_err(|error| BrowserSidecarError::InvalidState(error.to_string()))?;
658 ensure_command_aliases_available(&vm.toolkits, &payload)
659 .map_err(|error| BrowserSidecarError::InvalidState(error.to_string()))?;
660 ensure_toolkit_registry_capacity(&vm.toolkits, &payload)
661 .map_err(|error| BrowserSidecarError::InvalidState(error.to_string()))?;
662
663 let registration = payload.name.clone();
664 vm.toolkits.insert(registration.clone(), payload);
665 let command_count = u32::try_from(registered_tool_command_names(&vm.toolkits).len())
666 .map_err(|_| {
667 BrowserSidecarError::InvalidState(String::from(
668 "registered host callback command count exceeds u32",
669 ))
670 })?;
671 Ok((registration, command_count))
672 }
673
674 pub fn bootstrap_root_filesystem_entries(
675 &mut self,
676 vm_id: &str,
677 entries: &[RootFilesystemEntry],
678 ) -> Result<u32, BrowserSidecarError> {
679 for entry in entries {
680 self.apply_root_filesystem_entry(vm_id, entry)?;
681 }
682 u32::try_from(entries.len()).map_err(|_| {
683 BrowserSidecarError::InvalidState(String::from(
684 "root filesystem bootstrap entry count exceeds u32",
685 ))
686 })
687 }
688
689 pub fn guest_filesystem_call(
690 &mut self,
691 vm_id: &str,
692 payload: GuestFilesystemCallRequest,
693 ) -> Result<GuestFilesystemResultResponse, BrowserSidecarError> {
694 let vm = self.vm_mut(vm_id)?;
695 handle_guest_filesystem_call(&mut vm.kernel, payload).map_err(Self::sidecar_core_error)
696 }
697
698 pub fn guest_kernel_call(
699 &mut self,
700 vm_id: &str,
701 payload: GuestKernelCallRequest,
702 ) -> Result<GuestKernelResultResponse, BrowserSidecarError> {
703 let execution = self.ensure_execution_state(vm_id, &payload.execution_id)?;
704 let kernel_pid = execution.kernel_pid;
705 let vm = self.vm_mut(vm_id)?;
706 let response = handle_guest_kernel_call(
707 &mut vm.kernel,
708 kernel_pid,
709 BROWSER_WORKER_DRIVER,
710 &payload.operation,
711 &payload.payload,
712 )
713 .map_err(Self::sidecar_core_error)?;
714 Ok(GuestKernelResultResponse { payload: response })
715 }
716
717 pub fn kernel_state(&self, vm_id: &str) -> Result<LifecycleState, BrowserSidecarError> {
718 let vm = self.vm(vm_id)?;
719 Ok(match vm.kernel.state() {
720 KernelLifecycleState::Starting => LifecycleState::Starting,
721 KernelLifecycleState::Ready => LifecycleState::Ready,
722 KernelLifecycleState::Busy => LifecycleState::Busy,
723 KernelLifecycleState::Terminated => LifecycleState::Terminated,
724 })
725 }
726
727 pub fn zombie_timer_count(&self, vm_id: &str) -> Result<u64, BrowserSidecarError> {
728 let vm = self.vm(vm_id)?;
729 Ok(vm.kernel.zombie_timer_count() as u64)
730 }
731
732 pub fn process_snapshot_entries(
733 &self,
734 vm_id: &str,
735 ) -> Result<Vec<SharedProcessSnapshotEntry>, BrowserSidecarError> {
736 let vm = self.vm(vm_id)?;
737 let process_table = vm.kernel.list_processes();
738 Ok(self
739 .executions
740 .iter()
741 .filter(|(_, execution)| execution.vm_id == vm_id)
742 .filter_map(|(execution_id, execution)| {
743 process_table.get(&execution.kernel_pid).map(|info| {
744 process_snapshot_entry_from_kernel(
745 execution_id,
746 info,
747 execution.cwd.clone(),
748 None,
749 )
750 })
751 })
752 .collect())
753 }
754
755 pub fn signal_state(
756 &self,
757 vm_id: &str,
758 execution_id: &str,
759 ) -> Result<BTreeMap<u32, ProtocolSignalHandlerRegistration>, BrowserSidecarError> {
760 let vm = self.vm(vm_id)?;
761 Ok(vm
762 .signal_states
763 .get(execution_id)
764 .cloned()
765 .unwrap_or_default())
766 }
767
768 pub fn find_listener(
769 &self,
770 vm_id: &str,
771 request: &FindListenerRequest,
772 ) -> Result<Option<SocketStateEntry>, BrowserSidecarError> {
773 let vm = self.vm(vm_id)?;
774 for (execution_id, execution) in &self.executions {
775 if execution.vm_id != vm_id {
776 continue;
777 }
778 for record in vm.kernel.socket_records_for_pid(execution.kernel_pid) {
779 if let Some(path) = request.path.as_deref() {
780 if record.state() == SocketState::Listening
781 && record.spec().socket_type == SocketType::Stream
782 && record.local_unix_path() == Some(path)
783 {
784 return Ok(Some(SocketStateEntry {
785 process_id: execution_id.clone(),
786 host: None,
787 port: None,
788 path: Some(path.to_string()),
789 }));
790 }
791 continue;
792 }
793
794 let Some(address) = record.local_address() else {
795 continue;
796 };
797 if record.state() != SocketState::Listening
798 || record.spec().socket_type != SocketType::Stream
799 || !socket_host_matches(request.host.as_deref(), address.host())
800 || request.port.is_some_and(|port| address.port() != port)
801 {
802 continue;
803 }
804 return Ok(Some(SocketStateEntry {
805 process_id: execution_id.clone(),
806 host: Some(address.host().to_string()),
807 port: Some(address.port()),
808 path: None,
809 }));
810 }
811 }
812 Ok(None)
813 }
814
815 pub fn find_bound_udp(
816 &self,
817 vm_id: &str,
818 request: &FindBoundUdpRequest,
819 ) -> Result<Option<SocketStateEntry>, BrowserSidecarError> {
820 let vm = self.vm(vm_id)?;
821 for (execution_id, execution) in &self.executions {
822 if execution.vm_id != vm_id {
823 continue;
824 }
825 for record in vm.kernel.socket_records_for_pid(execution.kernel_pid) {
826 let Some(address) = record.local_address() else {
827 continue;
828 };
829 if record.state() != SocketState::Bound
830 || record.spec().socket_type != SocketType::Datagram
831 || !socket_host_matches(request.host.as_deref(), address.host())
832 || request.port.is_some_and(|port| address.port() != port)
833 {
834 continue;
835 }
836 return Ok(Some(SocketStateEntry {
837 process_id: execution_id.clone(),
838 host: Some(address.host().to_string()),
839 port: Some(address.port()),
840 path: None,
841 }));
842 }
843 }
844 Ok(None)
845 }
846
847 pub fn vm_fetch(
848 &mut self,
849 vm_id: &str,
850 request: &agentos_sidecar_protocol::protocol::VmFetchRequest,
851 ) -> Result<String, BrowserSidecarError> {
852 let target_path = if request.path.starts_with('/') {
853 request.path.clone()
854 } else {
855 format!("/{}", request.path)
856 };
857 let listener = self
858 .find_listener(
859 vm_id,
860 &FindListenerRequest {
861 host: Some(String::from("127.0.0.1")),
862 port: Some(request.port),
863 path: None,
864 },
865 )?
866 .ok_or_else(|| {
867 BrowserSidecarError::InvalidState(format!(
868 "vm.fetch could not find a guest HTTP listener on port {}",
869 request.port
870 ))
871 })?;
872 let target_execution_id = listener.process_id;
873 let target = self.ensure_execution_state(vm_id, &target_execution_id)?;
874 let request_bytes = serialize_kernel_http_fetch_request(
875 request.port,
876 &target_path,
877 &request.method,
878 &request.headers_json,
879 request.body.as_deref(),
880 )
881 .map_err(|error| BrowserSidecarError::InvalidState(error.to_string()))?;
882 let url = format!("http://127.0.0.1:{}{}", request.port, target_path);
883
884 let socket_id = {
885 let vm = self.vm_mut(vm_id)?;
886 let socket_id = vm
887 .kernel
888 .socket_create(BROWSER_WORKER_DRIVER, target.kernel_pid, SocketSpec::tcp())
889 .map_err(Self::kernel_error)?;
890 let result = vm
891 .kernel
892 .socket_connect_inet_loopback(
893 BROWSER_WORKER_DRIVER,
894 target.kernel_pid,
895 socket_id,
896 InetSocketAddress::new("127.0.0.1", request.port),
897 )
898 .map_err(Self::kernel_error)
899 .and_then(|()| {
900 vm.kernel
901 .socket_write(
902 BROWSER_WORKER_DRIVER,
903 target.kernel_pid,
904 socket_id,
905 &request_bytes,
906 )
907 .map(|_| ())
908 .map_err(Self::kernel_error)
909 });
910 if let Err(error) = result {
911 let _ = vm
912 .kernel
913 .socket_close(BROWSER_WORKER_DRIVER, target.kernel_pid, socket_id);
914 return Err(error);
915 }
916 socket_id
917 };
918
919 let result = self.read_vm_fetch_response(
920 vm_id,
921 target.kernel_pid,
922 socket_id,
923 &url,
924 VM_FETCH_BUFFER_LIMIT_BYTES,
925 );
926 let close_result = {
927 let vm = self.vm_mut(vm_id)?;
928 vm.kernel
929 .socket_close(BROWSER_WORKER_DRIVER, target.kernel_pid, socket_id)
930 .map_err(Self::kernel_error)
931 };
932
933 match (result, close_result) {
934 (Ok(response), Ok(())) => Ok(response),
935 (Err(error), _) => Err(error),
936 (Ok(_), Err(error)) => Err(error),
937 }
938 }
939
940 fn read_vm_fetch_response(
941 &mut self,
942 vm_id: &str,
943 kernel_pid: u32,
944 socket_id: SocketId,
945 url: &str,
946 max_fetch_response_bytes: usize,
947 ) -> Result<String, BrowserSidecarError> {
948 let mut response_buffer = Vec::new();
949 let mut peer_closed = false;
950 let deadline = Instant::now() + browser_vm_fetch_timeout();
951
952 loop {
953 if let Some(response) =
954 parse_kernel_http_fetch_response(&response_buffer, peer_closed, url)
955 .map_err(|error| BrowserSidecarError::InvalidState(error.to_string()))?
956 {
957 ensure_vm_fetch_response_within_limit(
958 &response,
959 "vm.fetch",
960 max_fetch_response_bytes,
961 )
962 .map_err(|error| BrowserSidecarError::InvalidState(error.to_string()))?;
963 return Ok(response);
964 }
965 if Instant::now() >= deadline {
966 let preview = String::from_utf8_lossy(&response_buffer);
967 return Err(BrowserSidecarError::InvalidState(format!(
968 "vm.fetch timed out waiting for kernel TCP HTTP response ({} buffered bytes: {:?})",
969 response_buffer.len(),
970 preview.chars().take(200).collect::<String>()
971 )));
972 }
973
974 let poll = {
975 let vm = self.vm_mut(vm_id)?;
976 vm.kernel
977 .poll_targets(
978 BROWSER_WORKER_DRIVER,
979 kernel_pid,
980 vec![PollTargetEntry::socket(
981 socket_id,
982 POLLIN | POLLHUP | POLLERR,
983 )],
984 5,
985 )
986 .map_err(Self::kernel_error)?
987 };
988 let revents = poll
989 .targets
990 .first()
991 .map(|entry| entry.revents)
992 .unwrap_or_default();
993 if revents.intersects(POLLERR) {
994 return Err(BrowserSidecarError::InvalidState(String::from(
995 "vm.fetch kernel TCP socket reported POLLERR",
996 )));
997 }
998 if revents.intersects(POLLIN) {
999 let read_result = {
1000 let vm = self.vm_mut(vm_id)?;
1001 vm.kernel
1002 .socket_read(BROWSER_WORKER_DRIVER, kernel_pid, socket_id, 64 * 1024)
1003 };
1004 match read_result {
1005 Ok(Some(bytes)) if !bytes.is_empty() => {
1006 response_buffer.extend(bytes);
1007 ensure_vm_fetch_raw_response_buffer_within_limit(
1008 response_buffer.len(),
1009 "vm.fetch",
1010 )
1011 .map_err(|error| BrowserSidecarError::InvalidState(error.to_string()))?;
1012 }
1013 Ok(Some(_)) => {}
1014 Ok(None) => peer_closed = true,
1015 Err(error) if error.code() == "EAGAIN" => {}
1016 Err(error) => return Err(Self::kernel_error(error)),
1017 }
1018 }
1019 if revents.intersects(POLLHUP) {
1020 peer_closed = true;
1021 }
1022 }
1023 }
1024
1025 pub fn create_kernel_tcp_listener_for_execution(
1026 &mut self,
1027 vm_id: &str,
1028 execution_id: &str,
1029 host: &str,
1030 port: u16,
1031 backlog: usize,
1032 ) -> Result<SocketId, BrowserSidecarError> {
1033 let execution = self.ensure_execution_state(vm_id, execution_id)?;
1034 let vm = self.vm_mut(vm_id)?;
1035 let socket_id = vm
1036 .kernel
1037 .socket_create(
1038 BROWSER_WORKER_DRIVER,
1039 execution.kernel_pid,
1040 SocketSpec::tcp(),
1041 )
1042 .map_err(Self::kernel_error)?;
1043 vm.kernel
1044 .socket_bind_inet(
1045 BROWSER_WORKER_DRIVER,
1046 execution.kernel_pid,
1047 socket_id,
1048 InetSocketAddress::new(host, port),
1049 )
1050 .map_err(Self::kernel_error)?;
1051 vm.kernel
1052 .socket_listen(
1053 BROWSER_WORKER_DRIVER,
1054 execution.kernel_pid,
1055 socket_id,
1056 backlog,
1057 )
1058 .map_err(Self::kernel_error)?;
1059 Ok(socket_id)
1060 }
1061
1062 pub fn create_kernel_bound_udp_for_execution(
1063 &mut self,
1064 vm_id: &str,
1065 execution_id: &str,
1066 host: &str,
1067 port: u16,
1068 ) -> Result<SocketId, BrowserSidecarError> {
1069 let execution = self.ensure_execution_state(vm_id, execution_id)?;
1070 let vm = self.vm_mut(vm_id)?;
1071 let socket_id = vm
1072 .kernel
1073 .socket_create(
1074 BROWSER_WORKER_DRIVER,
1075 execution.kernel_pid,
1076 SocketSpec::udp(),
1077 )
1078 .map_err(Self::kernel_error)?;
1079 vm.kernel
1080 .socket_bind_inet(
1081 BROWSER_WORKER_DRIVER,
1082 execution.kernel_pid,
1083 socket_id,
1084 InetSocketAddress::new(host, port),
1085 )
1086 .map_err(Self::kernel_error)?;
1087 Ok(socket_id)
1088 }
1089
1090 pub fn read_execution_stdin(
1091 &mut self,
1092 vm_id: &str,
1093 execution_id: &str,
1094 length: usize,
1095 timeout: Duration,
1096 ) -> Result<Option<Vec<u8>>, BrowserSidecarError> {
1097 let execution = self.ensure_execution_state(vm_id, execution_id)?;
1098 let vm = self.vm_mut(vm_id)?;
1099 vm.kernel
1100 .read_process_stdin(
1101 BROWSER_WORKER_DRIVER,
1102 execution.kernel_pid,
1103 length,
1104 Some(timeout),
1105 )
1106 .map_err(Self::kernel_error)
1107 }
1108
1109 pub fn dispose_vm(&mut self, vm_id: &str) -> Result<(), BrowserSidecarError> {
1110 let Some(vm_state) = self.vms.remove(vm_id) else {
1116 return Err(BrowserSidecarError::InvalidState(format!(
1117 "unknown browser sidecar VM: {vm_id}"
1118 )));
1119 };
1120
1121 for context_id in &vm_state.contexts {
1125 self.contexts.remove(context_id);
1126 }
1127
1128 let mut first_error: Option<BrowserSidecarError> = None;
1135 for execution_id in &vm_state.active_executions {
1136 if let Err(error) = self.release_execution(execution_id, "browser.worker.disposed") {
1137 first_error.get_or_insert(error);
1138 }
1139 }
1140
1141 let terminated = self.emit_lifecycle(
1144 vm_id,
1145 LifecycleState::Terminated,
1146 Some(String::from(
1147 "browser sidecar VM disposed on the main thread",
1148 )),
1149 );
1150
1151 match first_error {
1152 Some(error) => Err(error),
1153 None => terminated,
1154 }
1155 }
1156
1157 pub fn create_javascript_context(
1158 &mut self,
1159 request: CreateJavascriptContextRequest,
1160 ) -> Result<GuestContextHandle, BrowserSidecarError> {
1161 self.ensure_vm(&request.vm_id)?;
1162
1163 let vm_id = request.vm_id.clone();
1164 let entrypoint = BrowserWorkerEntrypoint::JavaScript {
1165 bootstrap_module: request.bootstrap_module.clone(),
1166 };
1167 let handle = self
1168 .bridge
1169 .create_javascript_context(request)
1170 .map_err(Self::bridge_error)?;
1171
1172 self.register_context(vm_id, handle.clone(), entrypoint)?;
1173 Ok(handle)
1174 }
1175
1176 pub fn create_wasm_context(
1177 &mut self,
1178 request: CreateWasmContextRequest,
1179 ) -> Result<GuestContextHandle, BrowserSidecarError> {
1180 self.ensure_vm(&request.vm_id)?;
1181
1182 let vm_id = request.vm_id.clone();
1183 let entrypoint = BrowserWorkerEntrypoint::WebAssembly {
1184 module_path: request.module_path.clone(),
1185 };
1186 let handle = self
1187 .bridge
1188 .create_wasm_context(request)
1189 .map_err(Self::bridge_error)?;
1190
1191 self.register_context(vm_id, handle.clone(), entrypoint)?;
1192 Ok(handle)
1193 }
1194
1195 pub fn start_execution(
1196 &mut self,
1197 request: StartExecutionRequest,
1198 ) -> Result<StartedExecution, BrowserSidecarError> {
1199 self.start_execution_with_options(request, BrowserExecutionOptions::default())
1200 }
1201
1202 pub fn start_execution_with_options(
1203 &mut self,
1204 request: StartExecutionRequest,
1205 options: BrowserExecutionOptions,
1206 ) -> Result<StartedExecution, BrowserSidecarError> {
1207 self.ensure_vm(&request.vm_id)?;
1208
1209 let context = self
1210 .contexts
1211 .get(&request.context_id)
1212 .cloned()
1213 .ok_or_else(|| {
1214 BrowserSidecarError::InvalidState(format!(
1215 "unknown browser sidecar context: {}",
1216 request.context_id
1217 ))
1218 })?;
1219
1220 if context.vm_id != request.vm_id {
1221 return Err(BrowserSidecarError::InvalidState(format!(
1222 "browser sidecar context {} belongs to vm {}, not {}",
1223 request.context_id, context.vm_id, request.vm_id
1224 )));
1225 }
1226
1227 let guest_cwd = request.cwd.clone();
1228 let (kernel_pid, stdin_write_fd) = {
1229 let vm = self.vm_mut(&request.vm_id)?;
1230 let kernel_handle = vm
1231 .kernel
1232 .create_virtual_process(
1233 BROWSER_WORKER_DRIVER,
1234 BROWSER_WORKER_DRIVER,
1235 request
1236 .argv
1237 .first()
1238 .map(String::as_str)
1239 .unwrap_or("browser-worker"),
1240 request.argv.clone(),
1241 VirtualProcessOptions {
1242 env: request.env.clone(),
1243 cwd: Some(guest_cwd.clone()),
1244 ..VirtualProcessOptions::default()
1245 },
1246 )
1247 .map_err(Self::kernel_error)?;
1248 let kernel_pid = kernel_handle.pid();
1249 match Self::configure_process_stdio(&mut vm.kernel, kernel_pid) {
1250 Ok(stdin_write_fd) => (kernel_pid, stdin_write_fd),
1251 Err(error) => {
1252 Self::cleanup_pending_kernel_process(&mut vm.kernel, kernel_pid)?;
1253 return Err(error);
1254 }
1255 }
1256 };
1257
1258 let (process_config, os_config) = {
1259 let vm = self.vm(&request.vm_id)?;
1260 browser_worker_identity(&vm.kernel, &request, kernel_pid)
1261 };
1262 let wasm_permission_tier = match context.runtime {
1263 GuestRuntime::JavaScript => None,
1264 GuestRuntime::WebAssembly => Some(self.resolve_wasm_permission_tier(
1265 &request.vm_id,
1266 options.command_name.as_deref(),
1267 options.wasm_permission_tier,
1268 &context.entrypoint,
1269 )?),
1270 };
1271
1272 let started = match self.bridge.start_execution(request.clone()) {
1273 Ok(started) => started,
1274 Err(error) => {
1275 let vm = self.vm_mut(&request.vm_id)?;
1276 Self::cleanup_pending_kernel_process(&mut vm.kernel, kernel_pid)?;
1277 return Err(Self::bridge_error(error));
1278 }
1279 };
1280
1281 let worker = match self.bridge.create_worker(BrowserWorkerSpawnRequest {
1282 vm_id: request.vm_id.clone(),
1283 context_id: request.context_id.clone(),
1284 execution_id: started.execution_id.clone(),
1285 runtime: context.runtime,
1286 entrypoint: context.entrypoint.clone(),
1287 wasm_permission_tier,
1288 process_config,
1289 os_config,
1290 }) {
1291 Ok(worker) => worker,
1292 Err(error) => {
1293 let vm = self.vm_mut(&request.vm_id)?;
1294 Self::cleanup_pending_kernel_process(&mut vm.kernel, kernel_pid)?;
1295 self.bridge
1296 .kill_execution(KillExecutionRequest {
1297 vm_id: request.vm_id,
1298 execution_id: started.execution_id,
1299 signal: agentos_bridge::ExecutionSignal::Kill,
1300 })
1301 .map_err(Self::bridge_error)?;
1302 return Err(Self::bridge_error(error));
1303 }
1304 };
1305
1306 let worker_id = worker.worker_id.clone();
1307 self.executions.insert(
1308 started.execution_id.clone(),
1309 ExecutionState {
1310 vm_id: request.vm_id.clone(),
1311 worker: worker.clone(),
1312 kernel_pid,
1313 stdin_write_fd,
1314 cwd: guest_cwd,
1315 },
1316 );
1317 let vm_state = self
1318 .vms
1319 .get_mut(&request.vm_id)
1320 .expect("VM should exist after validation");
1321 vm_state
1322 .active_executions
1323 .insert(started.execution_id.clone());
1324
1325 self.emit_structured(
1326 &request.vm_id,
1327 "browser.worker.spawned",
1328 BTreeMap::from([
1329 (String::from("context_id"), request.context_id),
1330 (String::from("execution_id"), started.execution_id.clone()),
1331 (
1332 String::from("runtime"),
1333 runtime_label(context.runtime).to_string(),
1334 ),
1335 (String::from("worker_id"), worker_id),
1336 ]),
1337 )?;
1338 self.emit_lifecycle(
1339 &request.vm_id,
1340 LifecycleState::Busy,
1341 Some(String::from(
1342 "browser sidecar is coordinating guest execution on the main thread",
1343 )),
1344 )?;
1345
1346 Ok(started)
1347 }
1348
1349 fn resolve_wasm_permission_tier(
1350 &self,
1351 vm_id: &str,
1352 command_name: Option<&str>,
1353 explicit_tier: Option<WasmPermissionTier>,
1354 entrypoint: &BrowserWorkerEntrypoint,
1355 ) -> Result<WasmPermissionTier, BrowserSidecarError> {
1356 let vm = self.vm(vm_id)?;
1357 Ok(explicit_tier
1358 .or_else(|| {
1359 command_name
1360 .and_then(|command| vm.configuration.command_permissions.get(command).copied())
1361 })
1362 .or_else(|| {
1363 let BrowserWorkerEntrypoint::WebAssembly {
1364 module_path: Some(module_path),
1365 } = entrypoint
1366 else {
1367 return None;
1368 };
1369 module_path
1370 .rsplit('/')
1371 .next()
1372 .and_then(|name| vm.configuration.command_permissions.get(name).copied())
1373 })
1374 .unwrap_or(WasmPermissionTier::Full))
1375 }
1376
1377 fn configure_process_stdio(
1378 kernel: &mut BrowserKernel,
1379 kernel_pid: u32,
1380 ) -> Result<u32, BrowserSidecarError> {
1381 let (stdin_read_fd, stdin_write_fd) = kernel
1382 .open_pipe(BROWSER_WORKER_DRIVER, kernel_pid)
1383 .map_err(Self::kernel_error)?;
1384 kernel
1385 .fd_dup2(BROWSER_WORKER_DRIVER, kernel_pid, stdin_read_fd, 0)
1386 .map_err(Self::kernel_error)?;
1387 let (_stdout_read_fd, stdout_write_fd) = kernel
1388 .open_pipe(BROWSER_WORKER_DRIVER, kernel_pid)
1389 .map_err(Self::kernel_error)?;
1390 kernel
1391 .fd_dup2(BROWSER_WORKER_DRIVER, kernel_pid, stdout_write_fd, 1)
1392 .map_err(Self::kernel_error)?;
1393 let (_stderr_read_fd, stderr_write_fd) = kernel
1394 .open_pipe(BROWSER_WORKER_DRIVER, kernel_pid)
1395 .map_err(Self::kernel_error)?;
1396 kernel
1397 .fd_dup2(BROWSER_WORKER_DRIVER, kernel_pid, stderr_write_fd, 2)
1398 .map_err(Self::kernel_error)?;
1399 Ok(stdin_write_fd)
1400 }
1401
1402 fn cleanup_pending_kernel_process(
1403 kernel: &mut BrowserKernel,
1404 kernel_pid: u32,
1405 ) -> Result<(), BrowserSidecarError> {
1406 kernel
1407 .exit_process(BROWSER_WORKER_DRIVER, kernel_pid, 1)
1408 .map_err(Self::kernel_error)?;
1409 kernel.waitpid(kernel_pid).map_err(Self::kernel_error)?;
1410 Ok(())
1411 }
1412
1413 fn reap_execution_kernel_process(
1414 &mut self,
1415 vm_id: &str,
1416 kernel_pid: u32,
1417 ) -> Result<(), BrowserSidecarError> {
1418 let Some(vm) = self.vms.get_mut(vm_id) else {
1419 return Ok(());
1420 };
1421 let Some(process) = vm.kernel.list_processes().get(&kernel_pid).cloned() else {
1422 return Ok(());
1423 };
1424
1425 if process.status != ProcessStatus::Exited {
1426 vm.kernel
1427 .exit_process(BROWSER_WORKER_DRIVER, kernel_pid, 1)
1428 .map_err(Self::kernel_error)?;
1429 }
1430 vm.kernel.waitpid(kernel_pid).map_err(Self::kernel_error)?;
1431 Ok(())
1432 }
1433
1434 pub fn write_stdin(
1435 &mut self,
1436 request: WriteExecutionStdinRequest,
1437 ) -> Result<(), BrowserSidecarError> {
1438 self.ensure_execution(&request.vm_id, &request.execution_id)?;
1439 let execution = self.ensure_execution_state(&request.vm_id, &request.execution_id)?;
1440 {
1441 let vm = self.vm_mut(&request.vm_id)?;
1442 vm.kernel
1443 .fd_write(
1444 BROWSER_WORKER_DRIVER,
1445 execution.kernel_pid,
1446 execution.stdin_write_fd,
1447 &request.chunk,
1448 )
1449 .map_err(Self::kernel_error)?;
1450 }
1451 self.bridge.write_stdin(request).map_err(Self::bridge_error)
1452 }
1453
1454 pub fn close_stdin(
1455 &mut self,
1456 request: ExecutionHandleRequest,
1457 ) -> Result<(), BrowserSidecarError> {
1458 self.ensure_execution(&request.vm_id, &request.execution_id)?;
1459 let execution = self.ensure_execution_state(&request.vm_id, &request.execution_id)?;
1460 {
1461 let vm = self.vm_mut(&request.vm_id)?;
1462 vm.kernel
1463 .fd_close(
1464 BROWSER_WORKER_DRIVER,
1465 execution.kernel_pid,
1466 execution.stdin_write_fd,
1467 )
1468 .map_err(Self::kernel_error)?;
1469 }
1470 self.bridge.close_stdin(request).map_err(Self::bridge_error)
1471 }
1472
1473 pub fn kill_execution(
1474 &mut self,
1475 request: KillExecutionRequest,
1476 ) -> Result<(), BrowserSidecarError> {
1477 self.ensure_execution(&request.vm_id, &request.execution_id)?;
1478 self.signal_execution_kernel_process(
1479 &request.vm_id,
1480 &request.execution_id,
1481 agentos_native_sidecar_core::execution_signal_to_kernel(request.signal),
1482 )?;
1483 self.bridge
1484 .kill_execution(request)
1485 .map_err(Self::bridge_error)
1486 }
1487
1488 pub fn signal_execution_kernel_process(
1489 &mut self,
1490 vm_id: &str,
1491 execution_id: &str,
1492 signal: i32,
1493 ) -> Result<(), BrowserSidecarError> {
1494 self.ensure_execution(vm_id, execution_id)?;
1495 let execution = self.ensure_execution_state(vm_id, execution_id)?;
1496 {
1497 let vm = self.vm_mut(vm_id)?;
1498 vm.kernel
1499 .kill_process(BROWSER_WORKER_DRIVER, execution.kernel_pid, signal)
1500 .map_err(Self::kernel_error)?;
1501 }
1502 Ok(())
1503 }
1504
1505 pub fn poll_execution_event(
1506 &mut self,
1507 request: PollExecutionEventRequest,
1508 ) -> Result<Option<ExecutionEvent>, BrowserSidecarError> {
1509 self.ensure_vm(&request.vm_id)?;
1510
1511 let event = self
1512 .bridge
1513 .poll_execution_event(request)
1514 .map_err(Self::bridge_error)?;
1515
1516 match &event {
1517 Some(ExecutionEvent::Stdout(chunk)) => {
1518 let execution = self.ensure_execution_state(&chunk.vm_id, &chunk.execution_id)?;
1519 let vm = self.vm_mut(&chunk.vm_id)?;
1520 vm.kernel
1521 .write_process_stdout(BROWSER_WORKER_DRIVER, execution.kernel_pid, &chunk.chunk)
1522 .map_err(Self::kernel_error)?;
1523 }
1524 Some(ExecutionEvent::Stderr(chunk)) => {
1525 let execution = self.ensure_execution_state(&chunk.vm_id, &chunk.execution_id)?;
1526 let vm = self.vm_mut(&chunk.vm_id)?;
1527 vm.kernel
1528 .write_process_stderr(BROWSER_WORKER_DRIVER, execution.kernel_pid, &chunk.chunk)
1529 .map_err(Self::kernel_error)?;
1530 }
1531 Some(ExecutionEvent::Exited(exited)) => {
1532 let execution = self.ensure_execution_state(&exited.vm_id, &exited.execution_id)?;
1533 {
1534 let vm = self.vm_mut(&exited.vm_id)?;
1535 vm.kernel
1536 .exit_process(
1537 BROWSER_WORKER_DRIVER,
1538 execution.kernel_pid,
1539 exited.exit_code,
1540 )
1541 .map_err(Self::kernel_error)?;
1542 }
1543 self.release_execution(&exited.execution_id, "browser.worker.reaped")?;
1544 }
1545 Some(ExecutionEvent::GuestRequest(call)) => {
1546 let fields = unsupported_guest_kernel_call_detail(
1547 None,
1548 &call.execution_id,
1549 &call.operation,
1550 call.payload.len(),
1551 )
1552 .into_iter()
1553 .collect();
1554 self.emit_structured(
1555 &call.vm_id,
1556 agentos_native_sidecar_core::UNSUPPORTED_GUEST_KERNEL_CALL_EVENT,
1557 fields,
1558 )?;
1559 }
1560 Some(ExecutionEvent::SignalState(state)) => {
1561 self.ensure_execution_state(&state.vm_id, &state.execution_id)?;
1562 let registration = protocol_signal_registration(&state.registration);
1563 let vm = self.vm_mut(&state.vm_id)?;
1564 apply_process_signal_state_update(
1565 &mut vm.signal_states,
1566 &state.execution_id,
1567 state.signal,
1568 registration,
1569 );
1570 }
1571 None => {}
1572 }
1573
1574 Ok(event)
1575 }
1576
1577 fn register_context(
1578 &mut self,
1579 vm_id: String,
1580 handle: GuestContextHandle,
1581 entrypoint: BrowserWorkerEntrypoint,
1582 ) -> Result<(), BrowserSidecarError> {
1583 self.contexts.insert(
1584 handle.context_id.clone(),
1585 ContextState {
1586 vm_id: vm_id.clone(),
1587 runtime: handle.runtime,
1588 entrypoint,
1589 },
1590 );
1591 let vm_state = self
1592 .vms
1593 .get_mut(&vm_id)
1594 .expect("VM should exist while registering a guest context");
1595 vm_state.contexts.insert(handle.context_id.clone());
1596
1597 self.emit_structured(
1598 &vm_id,
1599 "browser.context.created",
1600 BTreeMap::from([
1601 (String::from("context_id"), handle.context_id),
1602 (
1603 String::from("runtime"),
1604 runtime_label(handle.runtime).to_string(),
1605 ),
1606 ]),
1607 )
1608 }
1609
1610 fn release_execution(
1611 &mut self,
1612 execution_id: &str,
1613 event_name: &'static str,
1614 ) -> Result<(), BrowserSidecarError> {
1615 let Some(execution) = self.executions.remove(execution_id) else {
1616 return Ok(());
1617 };
1618
1619 if let Some(vm_state) = self.vms.get_mut(&execution.vm_id) {
1620 vm_state.active_executions.remove(execution_id);
1621 vm_state.signal_states.remove(execution_id);
1622 }
1623
1624 let vm_id = execution.vm_id;
1625 self.reap_execution_kernel_process(&vm_id, execution.kernel_pid)?;
1626 let runtime = execution.worker.runtime;
1627 let worker_id = execution.worker.worker_id;
1628 self.bridge
1629 .terminate_worker(BrowserWorkerHandleRequest {
1630 vm_id: vm_id.clone(),
1631 execution_id: execution_id.to_string(),
1632 worker_id: worker_id.clone(),
1633 })
1634 .map_err(Self::bridge_error)?;
1635
1636 self.emit_structured(
1637 &vm_id,
1638 event_name,
1639 BTreeMap::from([
1640 (String::from("execution_id"), execution_id.to_string()),
1641 (String::from("runtime"), runtime_label(runtime).to_string()),
1642 (String::from("worker_id"), worker_id),
1643 ]),
1644 )?;
1645
1646 let next_state = if self.active_worker_count(&vm_id) == 0 {
1647 LifecycleState::Ready
1648 } else {
1649 LifecycleState::Busy
1650 };
1651 self.emit_lifecycle(
1652 &vm_id,
1653 next_state,
1654 Some(String::from(
1655 "browser sidecar worker bookkeeping was updated on the main thread",
1656 )),
1657 )
1658 }
1659
1660 fn ensure_vm(&self, vm_id: &str) -> Result<(), BrowserSidecarError> {
1661 if self.vms.contains_key(vm_id) {
1662 Ok(())
1663 } else {
1664 Err(BrowserSidecarError::InvalidState(format!(
1665 "unknown browser sidecar VM: {vm_id}"
1666 )))
1667 }
1668 }
1669
1670 fn ensure_execution(&self, vm_id: &str, execution_id: &str) -> Result<(), BrowserSidecarError> {
1671 let execution = self.executions.get(execution_id).ok_or_else(|| {
1672 BrowserSidecarError::InvalidState(format!(
1673 "unknown browser sidecar execution: {execution_id}"
1674 ))
1675 })?;
1676
1677 if execution.vm_id == vm_id {
1678 Ok(())
1679 } else {
1680 Err(BrowserSidecarError::InvalidState(format!(
1681 "browser sidecar execution {execution_id} belongs to vm {}, not {vm_id}",
1682 execution.vm_id
1683 )))
1684 }
1685 }
1686
1687 fn ensure_execution_state(
1688 &self,
1689 vm_id: &str,
1690 execution_id: &str,
1691 ) -> Result<ExecutionState, BrowserSidecarError> {
1692 let execution = self.executions.get(execution_id).cloned().ok_or_else(|| {
1693 BrowserSidecarError::InvalidState(format!(
1694 "unknown browser sidecar execution: {execution_id}"
1695 ))
1696 })?;
1697
1698 if execution.vm_id == vm_id {
1699 Ok(execution)
1700 } else {
1701 Err(BrowserSidecarError::InvalidState(format!(
1702 "browser sidecar execution {execution_id} belongs to vm {}, not {vm_id}",
1703 execution.vm_id
1704 )))
1705 }
1706 }
1707
1708 fn vm(&self, vm_id: &str) -> Result<&VmState, BrowserSidecarError> {
1709 self.vms.get(vm_id).ok_or_else(|| {
1710 BrowserSidecarError::InvalidState(format!("unknown browser sidecar VM: {vm_id}"))
1711 })
1712 }
1713
1714 fn vm_mut(&mut self, vm_id: &str) -> Result<&mut VmState, BrowserSidecarError> {
1715 self.vms.get_mut(vm_id).ok_or_else(|| {
1716 BrowserSidecarError::InvalidState(format!("unknown browser sidecar VM: {vm_id}"))
1717 })
1718 }
1719
1720 fn emit_lifecycle(
1721 &mut self,
1722 vm_id: &str,
1723 state: LifecycleState,
1724 detail: Option<String>,
1725 ) -> Result<(), BrowserSidecarError> {
1726 self.bridge
1727 .emit_lifecycle(LifecycleEventRecord {
1728 vm_id: vm_id.to_string(),
1729 state,
1730 detail,
1731 })
1732 .map_err(Self::bridge_error)
1733 }
1734
1735 fn emit_structured(
1736 &mut self,
1737 vm_id: &str,
1738 name: &str,
1739 fields: BTreeMap<String, String>,
1740 ) -> Result<(), BrowserSidecarError> {
1741 self.bridge
1742 .emit_structured_event(StructuredEventRecord {
1743 vm_id: vm_id.to_string(),
1744 name: name.to_string(),
1745 fields,
1746 })
1747 .map_err(Self::bridge_error)
1748 }
1749
1750 fn bridge_error(error: BridgeError<B>) -> BrowserSidecarError {
1751 BrowserSidecarError::Bridge(format!("{error:?}"))
1752 }
1753
1754 fn sidecar_core_error(
1755 error: agentos_native_sidecar_core::SidecarCoreError,
1756 ) -> BrowserSidecarError {
1757 BrowserSidecarError::InvalidState(error.to_string())
1758 }
1759
1760 fn kernel_error(error: KernelError) -> BrowserSidecarError {
1761 BrowserSidecarError::Kernel(error.to_string())
1762 }
1763
1764 fn apply_root_filesystem_entry(
1765 &mut self,
1766 vm_id: &str,
1767 entry: &RootFilesystemEntry,
1768 ) -> Result<(), BrowserSidecarError> {
1769 let filesystem = self.vm_mut(vm_id)?.kernel.filesystem_mut();
1773 agentos_native_sidecar_core::apply_root_filesystem_entry(filesystem, entry)
1774 .map_err(Self::sidecar_core_error)
1775 }
1776}
1777
1778fn root_snapshot_from_entries(
1779 entries: &[RootFilesystemEntry],
1780) -> Result<RootFilesystemSnapshot, BrowserSidecarError> {
1781 agentos_native_sidecar_core::root_snapshot_from_entries(entries)
1783 .map_err(|error| BrowserSidecarError::InvalidState(error.to_string()))
1784}
1785
1786#[cfg(not(target_arch = "wasm32"))]
1787fn browser_vm_fetch_timeout() -> Duration {
1788 std::env::var(BROWSER_VM_FETCH_TIMEOUT_MS_ENV)
1789 .ok()
1790 .and_then(|value| value.parse::<u64>().ok())
1791 .map(Duration::from_millis)
1792 .unwrap_or(BROWSER_VM_FETCH_TIMEOUT)
1793}
1794
1795#[cfg(target_arch = "wasm32")]
1796fn browser_vm_fetch_timeout() -> Duration {
1797 BROWSER_VM_FETCH_TIMEOUT
1798}
1799
1800fn socket_host_matches(requested: Option<&str>, actual: &str) -> bool {
1801 match requested {
1802 None => true,
1803 Some(requested) if requested == actual => true,
1804 Some(requested)
1805 if is_unspecified_socket_host(requested) && is_unspecified_socket_host(actual) =>
1806 {
1807 true
1808 }
1809 Some(requested) if is_unspecified_socket_host(requested) => is_loopback_socket_host(actual),
1810 Some(requested) if requested.eq_ignore_ascii_case("localhost") => {
1811 is_loopback_socket_host(actual)
1812 }
1813 _ => false,
1814 }
1815}
1816
1817fn is_unspecified_socket_host(host: &str) -> bool {
1818 host == "0.0.0.0" || host == "::"
1819}
1820
1821fn is_loopback_socket_host(host: &str) -> bool {
1822 host == "127.0.0.1" || host == "::1" || host.eq_ignore_ascii_case("localhost")
1823}
1824
1825impl<B> BrowserExtensionHost for BrowserSidecar<B>
1826where
1827 B: BrowserSidecarBridge,
1828 BridgeError<B>: fmt::Debug,
1829{
1830 fn write_file(
1831 &mut self,
1832 vm_id: &str,
1833 path: &str,
1834 contents: Vec<u8>,
1835 ) -> Result<(), BrowserSidecarError> {
1836 BrowserSidecar::write_file(self, vm_id, path, contents)
1837 }
1838
1839 fn read_file(&mut self, vm_id: &str, path: &str) -> Result<Vec<u8>, BrowserSidecarError> {
1840 BrowserSidecar::read_file(self, vm_id, path)
1841 }
1842
1843 fn mkdir(
1844 &mut self,
1845 vm_id: &str,
1846 path: &str,
1847 recursive: bool,
1848 ) -> Result<(), BrowserSidecarError> {
1849 BrowserSidecar::mkdir(self, vm_id, path, recursive)
1850 }
1851
1852 fn read_dir(&mut self, vm_id: &str, path: &str) -> Result<Vec<String>, BrowserSidecarError> {
1853 BrowserSidecar::read_dir(self, vm_id, path)
1854 }
1855
1856 fn create_javascript_context(
1857 &mut self,
1858 request: CreateJavascriptContextRequest,
1859 ) -> Result<GuestContextHandle, BrowserSidecarError> {
1860 BrowserSidecar::create_javascript_context(self, request)
1861 }
1862
1863 fn create_wasm_context(
1864 &mut self,
1865 request: CreateWasmContextRequest,
1866 ) -> Result<GuestContextHandle, BrowserSidecarError> {
1867 BrowserSidecar::create_wasm_context(self, request)
1868 }
1869
1870 fn start_execution(
1871 &mut self,
1872 request: StartExecutionRequest,
1873 ) -> Result<StartedExecution, BrowserSidecarError> {
1874 BrowserSidecar::start_execution(self, request)
1875 }
1876
1877 fn write_stdin(
1878 &mut self,
1879 request: WriteExecutionStdinRequest,
1880 ) -> Result<(), BrowserSidecarError> {
1881 BrowserSidecar::write_stdin(self, request)
1882 }
1883
1884 fn close_stdin(&mut self, request: ExecutionHandleRequest) -> Result<(), BrowserSidecarError> {
1885 BrowserSidecar::close_stdin(self, request)
1886 }
1887
1888 fn kill_execution(&mut self, request: KillExecutionRequest) -> Result<(), BrowserSidecarError> {
1889 BrowserSidecar::kill_execution(self, request)
1890 }
1891
1892 fn poll_execution_event(
1893 &mut self,
1894 request: PollExecutionEventRequest,
1895 ) -> Result<Option<ExecutionEvent>, BrowserSidecarError> {
1896 BrowserSidecar::poll_execution_event(self, request)
1897 }
1898}
1899
1900fn runtime_label(runtime: GuestRuntime) -> &'static str {
1901 match runtime {
1902 GuestRuntime::JavaScript => "javascript",
1903 GuestRuntime::WebAssembly => "webassembly",
1904 }
1905}
1906
1907fn protocol_signal_registration(
1908 registration: &SignalHandlerRegistration,
1909) -> ProtocolSignalHandlerRegistration {
1910 ProtocolSignalHandlerRegistration {
1911 action: match registration.action {
1912 SignalDispositionAction::Default => ProtocolSignalDispositionAction::Default,
1913 SignalDispositionAction::Ignore => ProtocolSignalDispositionAction::Ignore,
1914 SignalDispositionAction::User => ProtocolSignalDispositionAction::User,
1915 },
1916 mask: registration.mask.clone(),
1917 flags: registration.flags,
1918 }
1919}
1920
1921#[cfg(test)]
1922impl<B> BrowserSidecar<B>
1923where
1924 B: BrowserSidecarBridge,
1925 BridgeError<B>: fmt::Debug,
1926{
1927 pub(crate) fn test_total_context_count(&self) -> usize {
1929 self.contexts.len()
1930 }
1931
1932 pub(crate) fn test_total_execution_count(&self) -> usize {
1934 self.executions.len()
1935 }
1936
1937 pub(crate) fn test_insert_context(&mut self, vm_id: &str, context_id: &str) {
1941 self.contexts.insert(
1942 context_id.to_string(),
1943 ContextState {
1944 vm_id: vm_id.to_string(),
1945 runtime: GuestRuntime::JavaScript,
1946 entrypoint: BrowserWorkerEntrypoint::JavaScript {
1947 bootstrap_module: None,
1948 },
1949 },
1950 );
1951 if let Some(vm) = self.vms.get_mut(vm_id) {
1952 vm.contexts.insert(context_id.to_string());
1953 }
1954 }
1955
1956 pub(crate) fn test_insert_execution(&mut self, vm_id: &str, execution_id: &str) {
1959 self.executions.insert(
1960 execution_id.to_string(),
1961 ExecutionState {
1962 vm_id: vm_id.to_string(),
1963 worker: BrowserWorkerHandle {
1964 worker_id: format!("worker-{execution_id}"),
1965 runtime: GuestRuntime::JavaScript,
1966 },
1967 kernel_pid: 0,
1968 stdin_write_fd: 0,
1969 cwd: String::new(),
1970 },
1971 );
1972 if let Some(vm) = self.vms.get_mut(vm_id) {
1973 vm.active_executions.insert(execution_id.to_string());
1974 }
1975 }
1976}
1977
1978#[cfg(test)]
1979#[allow(clippy::items_after_test_module)] mod tests {
1981 use super::*;
1982 use agentos_bridge::{
1983 ChmodRequest, ClockRequest, CommandPermissionRequest, CreateDirRequest, DiagnosticRecord,
1984 DirectoryEntry, EnvironmentPermissionRequest, ExecutionHandleRequest, FileMetadata,
1985 FilesystemPermissionRequest, FilesystemSnapshot, FlushFilesystemStateRequest,
1986 LoadFilesystemStateRequest, LogRecord, NetworkPermissionRequest, PathRequest,
1987 PermissionDecision, RandomBytesRequest, ReadDirRequest, ReadFileRequest, RenameRequest,
1988 ScheduleTimerRequest, ScheduledTimer, SymlinkRequest, TruncateRequest, WriteFileRequest,
1989 };
1990 use agentos_bridge::{
1991 ClockBridge, EventBridge, ExecutionBridge, FilesystemBridge, PermissionBridge,
1992 PersistenceBridge, RandomBridge,
1993 };
1994 use agentos_kernel::kernel::KernelVmConfig;
1995 use std::time::SystemTime;
1996
1997 #[derive(Debug, Clone, PartialEq, Eq)]
1998 struct TestBridgeError(String);
1999
2000 #[derive(Default)]
2003 struct TerminateFailingBridge {
2004 fail_terminate: bool,
2005 }
2006
2007 impl BridgeTypes for TerminateFailingBridge {
2008 type Error = TestBridgeError;
2009 }
2010
2011 impl FilesystemBridge for TerminateFailingBridge {
2012 fn read_file(&mut self, _request: ReadFileRequest) -> Result<Vec<u8>, Self::Error> {
2013 unimplemented!("not exercised by dispose test")
2014 }
2015 fn write_file(&mut self, _request: WriteFileRequest) -> Result<(), Self::Error> {
2016 unimplemented!("not exercised by dispose test")
2017 }
2018 fn stat(&mut self, _request: PathRequest) -> Result<FileMetadata, Self::Error> {
2019 unimplemented!("not exercised by dispose test")
2020 }
2021 fn lstat(&mut self, _request: PathRequest) -> Result<FileMetadata, Self::Error> {
2022 unimplemented!("not exercised by dispose test")
2023 }
2024 fn read_dir(
2025 &mut self,
2026 _request: ReadDirRequest,
2027 ) -> Result<Vec<DirectoryEntry>, Self::Error> {
2028 unimplemented!("not exercised by dispose test")
2029 }
2030 fn create_dir(&mut self, _request: CreateDirRequest) -> Result<(), Self::Error> {
2031 unimplemented!("not exercised by dispose test")
2032 }
2033 fn remove_file(&mut self, _request: PathRequest) -> Result<(), Self::Error> {
2034 unimplemented!("not exercised by dispose test")
2035 }
2036 fn remove_dir(&mut self, _request: PathRequest) -> Result<(), Self::Error> {
2037 unimplemented!("not exercised by dispose test")
2038 }
2039 fn rename(&mut self, _request: RenameRequest) -> Result<(), Self::Error> {
2040 unimplemented!("not exercised by dispose test")
2041 }
2042 fn symlink(&mut self, _request: SymlinkRequest) -> Result<(), Self::Error> {
2043 unimplemented!("not exercised by dispose test")
2044 }
2045 fn read_link(&mut self, _request: PathRequest) -> Result<String, Self::Error> {
2046 unimplemented!("not exercised by dispose test")
2047 }
2048 fn chmod(&mut self, _request: ChmodRequest) -> Result<(), Self::Error> {
2049 unimplemented!("not exercised by dispose test")
2050 }
2051 fn truncate(&mut self, _request: TruncateRequest) -> Result<(), Self::Error> {
2052 unimplemented!("not exercised by dispose test")
2053 }
2054 fn exists(&mut self, _request: PathRequest) -> Result<bool, Self::Error> {
2055 unimplemented!("not exercised by dispose test")
2056 }
2057 }
2058
2059 impl PermissionBridge for TerminateFailingBridge {
2060 fn check_filesystem_access(
2061 &mut self,
2062 _request: FilesystemPermissionRequest,
2063 ) -> Result<PermissionDecision, Self::Error> {
2064 unimplemented!("not exercised by dispose test")
2065 }
2066 fn check_network_access(
2067 &mut self,
2068 _request: NetworkPermissionRequest,
2069 ) -> Result<PermissionDecision, Self::Error> {
2070 unimplemented!("not exercised by dispose test")
2071 }
2072 fn check_command_execution(
2073 &mut self,
2074 _request: CommandPermissionRequest,
2075 ) -> Result<PermissionDecision, Self::Error> {
2076 unimplemented!("not exercised by dispose test")
2077 }
2078 fn check_environment_access(
2079 &mut self,
2080 _request: EnvironmentPermissionRequest,
2081 ) -> Result<PermissionDecision, Self::Error> {
2082 unimplemented!("not exercised by dispose test")
2083 }
2084 }
2085
2086 impl PersistenceBridge for TerminateFailingBridge {
2087 fn load_filesystem_state(
2088 &mut self,
2089 _request: LoadFilesystemStateRequest,
2090 ) -> Result<Option<FilesystemSnapshot>, Self::Error> {
2091 unimplemented!("not exercised by dispose test")
2092 }
2093 fn flush_filesystem_state(
2094 &mut self,
2095 _request: FlushFilesystemStateRequest,
2096 ) -> Result<(), Self::Error> {
2097 unimplemented!("not exercised by dispose test")
2098 }
2099 }
2100
2101 impl ClockBridge for TerminateFailingBridge {
2102 fn wall_clock(&mut self, _request: ClockRequest) -> Result<SystemTime, Self::Error> {
2103 unimplemented!("not exercised by dispose test")
2104 }
2105 fn monotonic_clock(&mut self, _request: ClockRequest) -> Result<Duration, Self::Error> {
2106 unimplemented!("not exercised by dispose test")
2107 }
2108 fn schedule_timer(
2109 &mut self,
2110 _request: ScheduleTimerRequest,
2111 ) -> Result<ScheduledTimer, Self::Error> {
2112 unimplemented!("not exercised by dispose test")
2113 }
2114 }
2115
2116 impl RandomBridge for TerminateFailingBridge {
2117 fn fill_random_bytes(
2118 &mut self,
2119 _request: RandomBytesRequest,
2120 ) -> Result<Vec<u8>, Self::Error> {
2121 unimplemented!("not exercised by dispose test")
2122 }
2123 }
2124
2125 impl EventBridge for TerminateFailingBridge {
2126 fn emit_structured_event(
2127 &mut self,
2128 _event: StructuredEventRecord,
2129 ) -> Result<(), Self::Error> {
2130 Ok(())
2131 }
2132 fn emit_diagnostic(&mut self, _event: DiagnosticRecord) -> Result<(), Self::Error> {
2133 Ok(())
2134 }
2135 fn emit_log(&mut self, _event: LogRecord) -> Result<(), Self::Error> {
2136 Ok(())
2137 }
2138 fn emit_lifecycle(&mut self, _event: LifecycleEventRecord) -> Result<(), Self::Error> {
2139 Ok(())
2140 }
2141 }
2142
2143 impl ExecutionBridge for TerminateFailingBridge {
2144 fn create_javascript_context(
2145 &mut self,
2146 _request: CreateJavascriptContextRequest,
2147 ) -> Result<GuestContextHandle, Self::Error> {
2148 unimplemented!("not exercised by dispose test")
2149 }
2150 fn create_wasm_context(
2151 &mut self,
2152 _request: CreateWasmContextRequest,
2153 ) -> Result<GuestContextHandle, Self::Error> {
2154 unimplemented!("not exercised by dispose test")
2155 }
2156 fn start_execution(
2157 &mut self,
2158 _request: StartExecutionRequest,
2159 ) -> Result<StartedExecution, Self::Error> {
2160 unimplemented!("not exercised by dispose test")
2161 }
2162 fn write_stdin(&mut self, _request: WriteExecutionStdinRequest) -> Result<(), Self::Error> {
2163 unimplemented!("not exercised by dispose test")
2164 }
2165 fn close_stdin(&mut self, _request: ExecutionHandleRequest) -> Result<(), Self::Error> {
2166 unimplemented!("not exercised by dispose test")
2167 }
2168 fn kill_execution(&mut self, _request: KillExecutionRequest) -> Result<(), Self::Error> {
2169 unimplemented!("not exercised by dispose test")
2170 }
2171 fn poll_execution_event(
2172 &mut self,
2173 _request: PollExecutionEventRequest,
2174 ) -> Result<Option<ExecutionEvent>, Self::Error> {
2175 unimplemented!("not exercised by dispose test")
2176 }
2177 }
2178
2179 impl crate::BrowserWorkerBridge for TerminateFailingBridge {
2180 fn create_worker(
2181 &mut self,
2182 _request: BrowserWorkerSpawnRequest,
2183 ) -> Result<BrowserWorkerHandle, Self::Error> {
2184 unimplemented!("not exercised by dispose test")
2185 }
2186
2187 fn terminate_worker(
2188 &mut self,
2189 _request: BrowserWorkerHandleRequest,
2190 ) -> Result<(), Self::Error> {
2191 if self.fail_terminate {
2192 Err(TestBridgeError(String::from("forced terminate failure")))
2193 } else {
2194 Ok(())
2195 }
2196 }
2197 }
2198
2199 #[test]
2203 fn dispose_vm_drains_maps_even_when_worker_termination_fails() {
2204 let bridge = TerminateFailingBridge {
2205 fail_terminate: true,
2206 };
2207 let mut sidecar = BrowserSidecar::new(bridge, BrowserSidecarConfig::default());
2208
2209 sidecar
2210 .create_vm(KernelVmConfig::new("vm-leak"))
2211 .expect("create vm");
2212 sidecar.test_insert_context("vm-leak", "ctx-leak");
2213 sidecar.test_insert_execution("vm-leak", "exec-leak");
2214
2215 assert_eq!(sidecar.vm_count(), 1);
2216 assert_eq!(sidecar.test_total_context_count(), 1);
2217 assert_eq!(sidecar.test_total_execution_count(), 1);
2218
2219 let result = sidecar.dispose_vm("vm-leak");
2222 assert!(result.is_err(), "forced terminate failure should surface");
2223
2224 assert_eq!(sidecar.vm_count(), 0, "VmState leaked after failed dispose");
2225 assert_eq!(
2226 sidecar.test_total_context_count(),
2227 0,
2228 "ContextState leaked after failed dispose"
2229 );
2230 assert_eq!(
2231 sidecar.test_total_execution_count(),
2232 0,
2233 "ExecutionState leaked after failed dispose"
2234 );
2235 }
2236}
2237
2238fn browser_worker_identity(
2239 kernel: &BrowserKernel,
2240 request: &StartExecutionRequest,
2241 kernel_pid: u32,
2242) -> (BrowserWorkerProcessConfig, BrowserWorkerOsConfig) {
2243 let mut env = kernel.environment().clone();
2244 env.extend(request.env.clone());
2245 let user = kernel.user_profile();
2246 let resource_limits = kernel.resource_limits();
2247 let identity =
2248 shared_guest_runtime_identity(&user, resource_limits, Some(u64::from(kernel_pid)), Some(0));
2249
2250 (
2251 BrowserWorkerProcessConfig {
2252 cwd: request.cwd.clone(),
2253 env,
2254 argv: request.argv.clone(),
2255 platform: identity.process_platform.clone(),
2256 arch: identity.process_arch.clone(),
2257 version: String::from("v22.0.0"),
2258 pid: kernel_pid,
2259 ppid: 0,
2260 uid: identity.virtual_uid as u32,
2261 gid: identity.virtual_gid as u32,
2262 },
2263 BrowserWorkerOsConfig {
2264 platform: identity.process_platform,
2265 arch: identity.process_arch.clone(),
2266 r#type: identity.os_type,
2267 release: identity.os_release,
2268 version: identity.os_version,
2269 cpu_count: identity.os_cpu_count,
2270 totalmem: identity.os_totalmem,
2271 freemem: identity.os_freemem,
2272 hostname: identity.os_hostname,
2273 homedir: identity.os_homedir,
2274 tmpdir: identity.os_tmpdir,
2275 machine: identity.os_machine,
2276 user: identity.os_user,
2277 shell: identity.os_shell,
2278 uid: identity.virtual_uid as u32,
2279 gid: identity.virtual_gid as u32,
2280 },
2281 )
2282}