Skip to main content

agentos_native_sidecar_browser/
service.rs

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    /// VM the request is scoped to (from the wire ownership scope), so extensions
152    /// that drive guest executions know which VM to target. `None` for connection/
153    /// session-scoped requests that carry no VM.
154    pub vm_id: Option<String>,
155    /// Owning connection (from the wire ownership scope), for per-connection
156    /// ownership enforcement inside extensions.
157    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    /// Construct with the wire ownership scope threaded in (VM + connection), so
231    /// extensions can target the right VM and enforce per-connection ownership.
232    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    /// VM this request is scoped to, if any.
245    pub fn vm_id(&self) -> Option<&str> {
246        self.vm_id.as_deref()
247    }
248
249    /// Owning connection of this request, if any.
250    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        // Remove the VM bookkeeping up front and take ownership of its state, so
1111        // that EVERY exit path below — including a mid-dispose `?` failure while
1112        // releasing executions or emitting lifecycle events — reclaims the
1113        // VmState (and the BrowserKernel it owns) instead of stranding it in the
1114        // `vms` map for the process lifetime.
1115        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        // Dropping per-context bookkeeping is infallible, so do it
1122        // unconditionally; `contexts` can never retain an entry for a VM that
1123        // has already been removed from `vms`.
1124        for context_id in &vm_state.contexts {
1125            self.contexts.remove(context_id);
1126        }
1127
1128        // Release every execution, attempting all of them and retaining only the
1129        // first error. A single worker-termination failure must not abandon the
1130        // remaining executions (their `ExecutionState`s would otherwise leak),
1131        // and `release_execution` already removes each entry from `executions`
1132        // before doing fallible bridge work, so the maps stay drained even when
1133        // the bridge reports an error.
1134        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        // Emit the terminal lifecycle event regardless of the outcome above; the
1142        // VM is already gone from the registry either way.
1143        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        // Shared with native: write the bootstrap entry through the VM's filesystem
1770        // (trusted, pre-guest; permissions default to allow at bootstrap time) with
1771        // deterministic mode/uid/gid defaults — see sidecar-core::apply_root_filesystem_entry.
1772        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    // Shared with native (sidecar-core): protocol entries -> kernel snapshot.
1782    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    /// Test-only: number of entries still tracked in the global `contexts` map.
1928    pub(crate) fn test_total_context_count(&self) -> usize {
1929        self.contexts.len()
1930    }
1931
1932    /// Test-only: number of entries still tracked in the global `executions` map.
1933    pub(crate) fn test_total_execution_count(&self) -> usize {
1934        self.executions.len()
1935    }
1936
1937    /// Test-only: inject a context directly into both the global `contexts` map
1938    /// and the owning VM's context set, bypassing the bridge round-trip so a
1939    /// dispose-path test can exercise cleanup at the smallest seam.
1940    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    /// Test-only: inject an active execution directly into both the global
1957    /// `executions` map and the owning VM's active-execution set.
1958    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)] // browser_worker_identity below is shared with the worker shim
1980mod 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    /// Minimal bridge whose `terminate_worker` can be forced to fail, used to
2001    /// drive a mid-dispose error through `release_execution`.
2002    #[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    // A mid-dispose worker-termination failure must still drain the VM, context,
2200    // and execution bookkeeping for that id — otherwise the VmState (holding a
2201    // BrowserKernel) and ContextState leak for the process lifetime.
2202    #[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        // The forced terminate_worker failure surfaces as an error, but the
2220        // dispose must still have reclaimed every entry for `vm-leak`.
2221        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}