Skip to main content

agentos_native_sidecar_browser/
wire_dispatch.rs

1use crate::{
2    BrowserExecutionOptions, BrowserExtensionRequest, BrowserSidecar, BrowserSidecarBridge,
3    BrowserSidecarConfig,
4};
5use agentos_bridge::{
6    CreateJavascriptContextRequest, CreateWasmContextRequest, ExecutionEvent,
7    ExecutionHandleRequest, GuestRuntime, KillExecutionRequest, PollExecutionEventRequest,
8    StartExecutionRequest, WriteExecutionStdinRequest,
9};
10use agentos_kernel::kernel::KernelVmConfig;
11use agentos_native_sidecar_core::{
12    authenticated_response, bound_udp_snapshot_response, connection_id_of,
13    execution_signal_from_number, layer_created_response, layer_sealed_response,
14    listener_snapshot_response, overlay_created_response, permissions_from_policy,
15    process_exited_event, process_killed_response, process_output_event, process_snapshot_response,
16    process_started_response, protocol_process_snapshot_entry, protocol_root_filesystem_mode,
17    reject, respond, root_filesystem_bootstrapped_response, root_filesystem_snapshot_response,
18    root_snapshot_entry, route_request_payload, session_opened_response, session_scope_of,
19    signal_state_response, snapshot_exported_response, snapshot_imported_response,
20    stdin_closed_response, stdin_written_response, unsupported_guest_kernel_call_event,
21    unsupported_host_callback_direction_dispatch, validate_authenticate_versions,
22    vm_configured_response, vm_created_response, vm_disposed_response, vm_id_of,
23    vm_lifecycle_event, zombie_timer_count_response, DispatchResult, RequestRoute,
24};
25use agentos_sidecar_protocol::protocol::{
26    AuthenticateRequest, BootstrapRootFilesystemRequest, CloseStdinRequest, ConfigureVmRequest,
27    CreateLayerRequest, CreateOverlayRequest, CreateVmRequest, DisposeVmRequest, EventFrame,
28    ExecuteRequest, ExportSnapshotRequest, ExtEnvelope, FindBoundUdpRequest, FindListenerRequest,
29    GetProcessSnapshotRequest, GetSignalStateRequest, GetZombieTimerCountRequest, GuestRuntimeKind,
30    HostCallbacksRegisteredResponse, ImportSnapshotRequest, KillProcessRequest, OpenSessionRequest,
31    OwnershipScope, RegisterHostCallbacksRequest, RequestFrame, ResponsePayload, SealLayerRequest,
32    SnapshotRootFilesystemRequest, SocketStateEntry, StreamChannel, VmFetchRequest,
33    VmFetchResponse, VmLifecycleState, WriteStdinRequest,
34};
35use agentos_sidecar_protocol::wire::{
36    request_frame_to_compat, CompatDispatchResult, ProtocolCodecError, ProtocolFrame,
37    WireFrameCodec,
38};
39use agentos_vm_config::CreateVmConfig;
40use std::collections::{BTreeMap, BTreeSet, VecDeque};
41use std::fmt;
42
43pub const BROWSER_SIDECAR_ID: &str = "agentos-native-sidecar-browser";
44pub const BROWSER_MAX_FRAME_BYTES: usize = 64 * 1024 * 1024;
45
46#[derive(Debug, Clone, PartialEq, Eq)]
47struct ExecutionRecord {
48    vm_id: String,
49    process_id: String,
50    ownership: OwnershipScope,
51}
52
53type ProcessExecutionKey = (String, String);
54
55pub struct BrowserWireDispatcher<B: BrowserSidecarBridge> {
56    codec: WireFrameCodec,
57    sidecar: BrowserSidecar<B>,
58    next_connection: usize,
59    next_session: usize,
60    next_vm: usize,
61    active_vms: BTreeSet<String>,
62    executions: BTreeMap<String, ExecutionRecord>,
63    process_executions: BTreeMap<ProcessExecutionKey, String>,
64    pending_events: VecDeque<EventFrame>,
65}
66
67impl<B> BrowserWireDispatcher<B>
68where
69    B: BrowserSidecarBridge,
70    <B as agentos_bridge::BridgeTypes>::Error: fmt::Debug,
71{
72    pub fn new(bridge: B) -> Self {
73        Self {
74            codec: WireFrameCodec::new(BROWSER_MAX_FRAME_BYTES),
75            sidecar: BrowserSidecar::new(bridge, BrowserSidecarConfig::default()),
76            next_connection: 0,
77            next_session: 0,
78            next_vm: 0,
79            active_vms: BTreeSet::new(),
80            executions: BTreeMap::new(),
81            process_executions: BTreeMap::new(),
82            pending_events: VecDeque::new(),
83        }
84    }
85
86    pub fn vm_count(&self) -> usize {
87        self.sidecar.vm_count()
88    }
89
90    pub fn sidecar_mut(&mut self) -> &mut BrowserSidecar<B> {
91        &mut self.sidecar
92    }
93
94    pub fn handle_request_bytes(&mut self, bytes: &[u8]) -> Result<Vec<u8>, ProtocolCodecError> {
95        let generated_request = match self.codec.decode_message(bytes)? {
96            ProtocolFrame::RequestFrame(request) => request,
97            _ => {
98                return Err(ProtocolCodecError::SerializeFailure(String::from(
99                    "browser sidecar expected a request frame",
100                )));
101            }
102        };
103        let request = request_frame_to_compat(generated_request)?;
104        let dispatch = self.dispatch(request);
105        for event in dispatch.events.iter().cloned() {
106            self.pending_events.push_back(event);
107        }
108        let generated =
109            agentos_sidecar_protocol::wire::dispatch_result_from_compat(CompatDispatchResult {
110                response: dispatch.response,
111                events: Vec::new(),
112            })?;
113        self.codec
114            .encode_message(&ProtocolFrame::ResponseFrame(generated.response))
115    }
116
117    pub fn poll_event_bytes(&mut self) -> Result<Option<Vec<u8>>, ProtocolCodecError> {
118        if self.pending_events.is_empty() {
119            self.pump_execution_events();
120        }
121        let Some(event) = self.pending_events.pop_front() else {
122            return Ok(None);
123        };
124        let generated = agentos_sidecar_protocol::wire::event_frame_from_compat(event)?;
125        self.codec
126            .encode_message(&ProtocolFrame::EventFrame(generated))
127            .map(Some)
128    }
129
130    fn dispatch(&mut self, request: RequestFrame) -> DispatchResult {
131        match route_request_payload(&request) {
132            RequestRoute::Authenticate(payload) => self.authenticate(&request, payload),
133            RequestRoute::OpenSession(payload) => self.open_session(&request, payload),
134            RequestRoute::CreateVm(payload) => self.create_vm(&request, payload),
135            RequestRoute::DisposeVm(payload) => self.dispose_vm(&request, payload),
136            RequestRoute::BootstrapRootFilesystem(payload) => {
137                self.bootstrap_root_filesystem(&request, payload)
138            }
139            RequestRoute::ConfigureVm(payload) => self.configure_vm(&request, payload),
140            RequestRoute::RegisterHostCallbacks(payload) => {
141                self.register_host_callbacks(&request, payload)
142            }
143            RequestRoute::CreateLayer(payload) => self.create_layer(&request, payload),
144            RequestRoute::SealLayer(payload) => self.seal_layer(&request, payload),
145            RequestRoute::ImportSnapshot(payload) => self.import_snapshot(&request, payload),
146            RequestRoute::ExportSnapshot(payload) => self.export_snapshot(&request, payload),
147            RequestRoute::CreateOverlay(payload) => self.create_overlay(&request, payload),
148            RequestRoute::GuestFilesystemCall(payload) => {
149                self.guest_filesystem_call(&request, payload)
150            }
151            RequestRoute::GuestKernelCall(payload) => self.guest_kernel_call(&request, payload),
152            RequestRoute::SnapshotRootFilesystem(payload) => {
153                self.snapshot_root_filesystem(&request, payload)
154            }
155            RequestRoute::GetProcessSnapshot(payload) => {
156                self.get_process_snapshot(&request, payload)
157            }
158            RequestRoute::GetResourceSnapshot(payload) => {
159                // Resource snapshots surface the native sidecar's queue/limit
160                // trackers, which the converged browser runtime does not run.
161                let _ = payload;
162                rejected(
163                    &request,
164                    "unsupported",
165                    "get_resource_snapshot is not available in the converged browser runtime",
166                )
167            }
168            RequestRoute::GetSignalState(payload) => self.get_signal_state(&request, payload),
169            RequestRoute::GetZombieTimerCount(payload) => {
170                self.get_zombie_timer_count(&request, payload)
171            }
172            RequestRoute::Execute(payload) => self.execute(&request, payload),
173            RequestRoute::WriteStdin(payload) => self.write_stdin(&request, payload),
174            RequestRoute::ResizePty(payload) => {
175                // The converged browser path resizes the PTY through the driver's
176                // master-side resize, not via a native wire ResizePty op, so this
177                // route is not exercised by the in-browser terminal.
178                let _ = payload;
179                rejected(
180                    &request,
181                    "unsupported",
182                    "resize_pty is handled by the converged browser driver, not the native wire op",
183                )
184            }
185            RequestRoute::CloseStdin(payload) => self.close_stdin(&request, payload),
186            RequestRoute::KillProcess(payload) => self.kill_process(&request, payload),
187            RequestRoute::FindListener(payload) => self.find_listener(&request, payload),
188            RequestRoute::FindBoundUdp(payload) => self.find_bound_udp(&request, payload),
189            RequestRoute::VmFetch(payload) => self.vm_fetch(&request, payload),
190            RequestRoute::Ext(payload) => self.ext(&request, payload),
191            RequestRoute::LinkPackage(payload) => {
192                // Package linking projects host-filesystem package trees into the
193                // VM, which the converged browser runtime does not provide.
194                let _ = payload;
195                rejected(
196                    &request,
197                    "unsupported",
198                    "link_package is not available in the converged browser runtime",
199                )
200            }
201            RequestRoute::ProvidedCommands(payload) => {
202                // Provided command metadata is retained by the native sidecar's
203                // host-backed package projection, which the converged browser
204                // runtime does not provide.
205                let _ = payload;
206                rejected(
207                    &request,
208                    "unsupported",
209                    "provided_commands is not available in the converged browser runtime",
210                )
211            }
212            RequestRoute::UnsupportedHostCallbackDirection => {
213                unsupported_host_callback_direction_dispatch(&request)
214            }
215        }
216    }
217
218    fn bootstrap_root_filesystem(
219        &mut self,
220        request: &RequestFrame,
221        payload: BootstrapRootFilesystemRequest,
222    ) -> DispatchResult {
223        let Some(vm_id) = vm_id_of(&request.ownership) else {
224            return rejected(
225                request,
226                "invalid_ownership",
227                "bootstrap_root_filesystem requires VM ownership",
228            );
229        };
230        let entry_count = match self
231            .sidecar
232            .bootstrap_root_filesystem_entries(&vm_id, &payload.entries)
233        {
234            Ok(entry_count) => entry_count,
235            Err(error) => return rejected(request, "bootstrap_root_failed", &error.to_string()),
236        };
237        DispatchResult {
238            response: root_filesystem_bootstrapped_response(request, entry_count),
239            events: Vec::new(),
240        }
241    }
242
243    fn configure_vm(
244        &mut self,
245        request: &RequestFrame,
246        payload: ConfigureVmRequest,
247    ) -> DispatchResult {
248        let Some(vm_id) = vm_id_of(&request.ownership) else {
249            return rejected(
250                request,
251                "invalid_ownership",
252                "configure_vm requires VM ownership",
253            );
254        };
255        if !payload.mounts.is_empty()
256            || !payload.software.is_empty()
257            || payload.module_access_cwd.is_some()
258        {
259            return rejected(
260                request,
261                "unsupported_request",
262                "browser ConfigureVm does not support host mounts, software installs, or moduleAccessCwd",
263            );
264        }
265
266        let permissions = match payload.permissions {
267            Some(policy) => {
268                let policy =
269                    agentos_sidecar_protocol::wire::permissions_policy_config_from_wire(policy);
270                if let Err(error) =
271                    agentos_native_sidecar_core::validate_permissions_policy(&policy)
272                {
273                    return rejected(request, "invalid_config", &error.to_string());
274                }
275                Some(permissions_from_policy(policy))
276            }
277            None => None,
278        };
279        if let Err(error) = self.sidecar.configure_vm(
280            &vm_id,
281            permissions,
282            payload.instructions,
283            payload.projected_modules,
284            payload.command_permissions.into_iter().collect(),
285            payload.loopback_exempt_ports,
286        ) {
287            return rejected(request, "configure_vm_failed", &error.to_string());
288        }
289        DispatchResult {
290            response: vm_configured_response(request, 0, 0, Vec::new(), Vec::new()),
291            events: Vec::new(),
292        }
293    }
294
295    fn register_host_callbacks(
296        &mut self,
297        request: &RequestFrame,
298        payload: RegisterHostCallbacksRequest,
299    ) -> DispatchResult {
300        let Some(vm_id) = vm_id_of(&request.ownership) else {
301            return rejected(
302                request,
303                "invalid_ownership",
304                "register_host_callbacks requires VM ownership",
305            );
306        };
307        let (registration, command_count) =
308            match self.sidecar.register_host_callbacks(&vm_id, payload) {
309                Ok(result) => result,
310                Err(error) => {
311                    return rejected(
312                        request,
313                        "register_host_callbacks_failed",
314                        &error.to_string(),
315                    )
316                }
317            };
318        DispatchResult {
319            response: respond(
320                request,
321                ResponsePayload::HostCallbacksRegistered(HostCallbacksRegisteredResponse {
322                    registration,
323                    command_count,
324                }),
325            ),
326            events: Vec::new(),
327        }
328    }
329
330    fn guest_filesystem_call(
331        &mut self,
332        request: &RequestFrame,
333        payload: agentos_sidecar_protocol::protocol::GuestFilesystemCallRequest,
334    ) -> DispatchResult {
335        let Some(vm_id) = vm_id_of(&request.ownership) else {
336            return rejected(
337                request,
338                "invalid_ownership",
339                "guest_filesystem_call requires VM ownership",
340            );
341        };
342        let result = match self.sidecar.guest_filesystem_call(&vm_id, payload) {
343            Ok(result) => result,
344            Err(error) => return rejected(request, "guest_filesystem_failed", &error.to_string()),
345        };
346        DispatchResult {
347            response: respond(request, ResponsePayload::GuestFilesystemResult(result)),
348            events: Vec::new(),
349        }
350    }
351
352    fn guest_kernel_call(
353        &mut self,
354        request: &RequestFrame,
355        payload: agentos_sidecar_protocol::protocol::GuestKernelCallRequest,
356    ) -> DispatchResult {
357        let Some(vm_id) = vm_id_of(&request.ownership) else {
358            return rejected(
359                request,
360                "invalid_ownership",
361                "guest_kernel_call requires VM ownership",
362            );
363        };
364        let result = match self.sidecar.guest_kernel_call(&vm_id, payload) {
365            Ok(result) => result,
366            Err(error) => return rejected(request, "guest_kernel_failed", &error.to_string()),
367        };
368        DispatchResult {
369            response: respond(request, ResponsePayload::GuestKernelResult(result)),
370            events: Vec::new(),
371        }
372    }
373
374    fn create_layer(
375        &mut self,
376        request: &RequestFrame,
377        _payload: CreateLayerRequest,
378    ) -> DispatchResult {
379        let Some(vm_id) = vm_id_of(&request.ownership) else {
380            return rejected(
381                request,
382                "invalid_ownership",
383                "create_layer requires VM ownership",
384            );
385        };
386        let layer_id = match self.sidecar.create_layer(&vm_id) {
387            Ok(layer_id) => layer_id,
388            Err(error) => return rejected(request, "create_layer_failed", &error.to_string()),
389        };
390        DispatchResult {
391            response: layer_created_response(request, layer_id),
392            events: Vec::new(),
393        }
394    }
395
396    fn seal_layer(&mut self, request: &RequestFrame, payload: SealLayerRequest) -> DispatchResult {
397        let Some(vm_id) = vm_id_of(&request.ownership) else {
398            return rejected(
399                request,
400                "invalid_ownership",
401                "seal_layer requires VM ownership",
402            );
403        };
404        let layer_id = match self.sidecar.seal_layer(&vm_id, &payload.layer_id) {
405            Ok(layer_id) => layer_id,
406            Err(error) => return rejected(request, "seal_layer_failed", &error.to_string()),
407        };
408        DispatchResult {
409            response: layer_sealed_response(request, layer_id),
410            events: Vec::new(),
411        }
412    }
413
414    fn import_snapshot(
415        &mut self,
416        request: &RequestFrame,
417        payload: ImportSnapshotRequest,
418    ) -> DispatchResult {
419        let Some(vm_id) = vm_id_of(&request.ownership) else {
420            return rejected(
421                request,
422                "invalid_ownership",
423                "import_snapshot requires VM ownership",
424            );
425        };
426        let layer_id = match self.sidecar.import_snapshot(&vm_id, &payload.entries) {
427            Ok(layer_id) => layer_id,
428            Err(error) => return rejected(request, "import_snapshot_failed", &error.to_string()),
429        };
430        DispatchResult {
431            response: snapshot_imported_response(request, layer_id),
432            events: Vec::new(),
433        }
434    }
435
436    fn export_snapshot(
437        &mut self,
438        request: &RequestFrame,
439        payload: ExportSnapshotRequest,
440    ) -> DispatchResult {
441        let Some(vm_id) = vm_id_of(&request.ownership) else {
442            return rejected(
443                request,
444                "invalid_ownership",
445                "export_snapshot requires VM ownership",
446            );
447        };
448        let snapshot = match self.sidecar.export_snapshot(&vm_id, &payload.layer_id) {
449            Ok(snapshot) => snapshot,
450            Err(error) => return rejected(request, "export_snapshot_failed", &error.to_string()),
451        };
452        DispatchResult {
453            response: snapshot_exported_response(
454                request,
455                payload.layer_id,
456                snapshot.entries.iter().map(root_snapshot_entry).collect(),
457            ),
458            events: Vec::new(),
459        }
460    }
461
462    fn create_overlay(
463        &mut self,
464        request: &RequestFrame,
465        payload: CreateOverlayRequest,
466    ) -> DispatchResult {
467        let Some(vm_id) = vm_id_of(&request.ownership) else {
468            return rejected(
469                request,
470                "invalid_ownership",
471                "create_overlay requires VM ownership",
472            );
473        };
474        let mode = protocol_root_filesystem_mode(payload.mode);
475        let layer_id = match self.sidecar.create_overlay(
476            &vm_id,
477            mode,
478            payload.upper_layer_id,
479            payload.lower_layer_ids,
480        ) {
481            Ok(layer_id) => layer_id,
482            Err(error) => return rejected(request, "create_overlay_failed", &error.to_string()),
483        };
484        DispatchResult {
485            response: overlay_created_response(request, layer_id),
486            events: Vec::new(),
487        }
488    }
489
490    fn snapshot_root_filesystem(
491        &mut self,
492        request: &RequestFrame,
493        _payload: SnapshotRootFilesystemRequest,
494    ) -> DispatchResult {
495        let Some(vm_id) = vm_id_of(&request.ownership) else {
496            return rejected(
497                request,
498                "invalid_ownership",
499                "snapshot_root_filesystem requires VM ownership",
500            );
501        };
502        let snapshot = match self.sidecar.snapshot_root_filesystem(&vm_id) {
503            Ok(snapshot) => snapshot,
504            Err(error) => return rejected(request, "snapshot_root_failed", &error.to_string()),
505        };
506        DispatchResult {
507            response: root_filesystem_snapshot_response(
508                request,
509                snapshot.entries.iter().map(root_snapshot_entry).collect(),
510            ),
511            events: Vec::new(),
512        }
513    }
514
515    fn get_process_snapshot(
516        &mut self,
517        request: &RequestFrame,
518        _payload: GetProcessSnapshotRequest,
519    ) -> DispatchResult {
520        let Some(vm_id) = vm_id_of(&request.ownership) else {
521            return rejected(
522                request,
523                "invalid_ownership",
524                "get_process_snapshot requires VM ownership",
525            );
526        };
527        let mut processes = match self.sidecar.process_snapshot_entries(&vm_id) {
528            Ok(processes) => processes,
529            Err(error) => return rejected(request, "process_snapshot_failed", &error.to_string()),
530        };
531        for process in &mut processes {
532            if let Some(record) = self.executions.get(&process.process_id) {
533                process.process_id = record.process_id.clone();
534            }
535        }
536        DispatchResult {
537            response: process_snapshot_response(
538                request,
539                processes
540                    .into_iter()
541                    .map(protocol_process_snapshot_entry)
542                    .collect(),
543            ),
544            events: Vec::new(),
545        }
546    }
547
548    fn get_signal_state(
549        &mut self,
550        request: &RequestFrame,
551        payload: GetSignalStateRequest,
552    ) -> DispatchResult {
553        let Some(vm_id) = vm_id_of(&request.ownership) else {
554            return rejected(
555                request,
556                "invalid_ownership",
557                "get_signal_state requires VM ownership",
558            );
559        };
560        let Some(execution_id) = self.execution_id_for(&vm_id, &payload.process_id) else {
561            return rejected(
562                request,
563                "unknown_process",
564                "get_signal_state process is not active",
565            );
566        };
567        let handlers = match self.sidecar.signal_state(&vm_id, &execution_id) {
568            Ok(handlers) => handlers,
569            Err(error) => return rejected(request, "signal_state_failed", &error.to_string()),
570        };
571        DispatchResult {
572            response: signal_state_response(request, payload.process_id, handlers),
573            events: Vec::new(),
574        }
575    }
576
577    fn get_zombie_timer_count(
578        &mut self,
579        request: &RequestFrame,
580        _payload: GetZombieTimerCountRequest,
581    ) -> DispatchResult {
582        let Some(vm_id) = vm_id_of(&request.ownership) else {
583            return rejected(
584                request,
585                "invalid_ownership",
586                "get_zombie_timer_count requires VM ownership",
587            );
588        };
589        let count = match self.sidecar.zombie_timer_count(&vm_id) {
590            Ok(count) => count,
591            Err(error) => {
592                return rejected(request, "zombie_timer_count_failed", &error.to_string())
593            }
594        };
595        DispatchResult {
596            response: zombie_timer_count_response(request, count),
597            events: Vec::new(),
598        }
599    }
600
601    fn find_listener(
602        &mut self,
603        request: &RequestFrame,
604        payload: FindListenerRequest,
605    ) -> DispatchResult {
606        let Some(vm_id) = vm_id_of(&request.ownership) else {
607            return rejected(
608                request,
609                "invalid_ownership",
610                "find_listener requires VM ownership",
611            );
612        };
613        let listener = match self.sidecar.find_listener(&vm_id, &payload) {
614            Ok(listener) => listener,
615            Err(error) => return rejected(request, "find_listener_failed", &error.to_string()),
616        }
617        .map(|entry| self.client_socket_state_entry(entry));
618        DispatchResult {
619            response: listener_snapshot_response(request, listener),
620            events: Vec::new(),
621        }
622    }
623
624    fn find_bound_udp(
625        &mut self,
626        request: &RequestFrame,
627        payload: FindBoundUdpRequest,
628    ) -> DispatchResult {
629        let Some(vm_id) = vm_id_of(&request.ownership) else {
630            return rejected(
631                request,
632                "invalid_ownership",
633                "find_bound_udp requires VM ownership",
634            );
635        };
636        let socket = match self.sidecar.find_bound_udp(&vm_id, &payload) {
637            Ok(socket) => socket,
638            Err(error) => return rejected(request, "find_bound_udp_failed", &error.to_string()),
639        }
640        .map(|entry| self.client_socket_state_entry(entry));
641        DispatchResult {
642            response: bound_udp_snapshot_response(request, socket),
643            events: Vec::new(),
644        }
645    }
646
647    fn vm_fetch(&mut self, request: &RequestFrame, payload: VmFetchRequest) -> DispatchResult {
648        let Some(vm_id) = vm_id_of(&request.ownership) else {
649            return rejected(
650                request,
651                "invalid_ownership",
652                "vm.fetch requires VM ownership",
653            );
654        };
655        if let Err(error) = serde_json::from_str::<serde_json::Value>(&payload.headers_json) {
656            return rejected(
657                request,
658                "invalid_request",
659                &format!("vm.fetch headers_json must be valid JSON: {error}"),
660            );
661        }
662
663        let response_json = match self.sidecar.vm_fetch(&vm_id, &payload) {
664            Ok(response_json) => response_json,
665            Err(error) => return rejected(request, "vm_fetch_failed", &error.to_string()),
666        };
667        DispatchResult {
668            response: respond(
669                request,
670                ResponsePayload::VmFetchResult(VmFetchResponse { response_json }),
671            ),
672            events: Vec::new(),
673        }
674    }
675
676    fn authenticate(
677        &mut self,
678        request: &RequestFrame,
679        payload: AuthenticateRequest,
680    ) -> DispatchResult {
681        if let Err(error) = validate_authenticate_versions(&payload) {
682            return rejected(request, error.code(), error.message());
683        }
684
685        self.next_connection += 1;
686        let connection_id = format!("browser-connection-{}", self.next_connection);
687        DispatchResult {
688            response: authenticated_response(
689                request.request_id,
690                BROWSER_SIDECAR_ID,
691                connection_id,
692                BROWSER_MAX_FRAME_BYTES as u32,
693            ),
694            events: Vec::new(),
695        }
696    }
697
698    fn client_socket_state_entry(&self, mut entry: SocketStateEntry) -> SocketStateEntry {
699        if let Some(record) = self.executions.get(&entry.process_id) {
700            entry.process_id = record.process_id.clone();
701        }
702        entry
703    }
704
705    fn open_session(
706        &mut self,
707        request: &RequestFrame,
708        _payload: OpenSessionRequest,
709    ) -> DispatchResult {
710        let Some(connection_id) = connection_id_of(&request.ownership) else {
711            return rejected(
712                request,
713                "invalid_ownership",
714                "open_session requires connection ownership",
715            );
716        };
717        self.next_session += 1;
718        let session_id = format!("browser-session-{}", self.next_session);
719        DispatchResult {
720            response: session_opened_response(request.request_id, connection_id, session_id),
721            events: Vec::new(),
722        }
723    }
724
725    fn create_vm(&mut self, request: &RequestFrame, payload: CreateVmRequest) -> DispatchResult {
726        let Some((connection_id, session_id)) = session_scope_of(&request.ownership) else {
727            return rejected(
728                request,
729                "invalid_ownership",
730                "create_vm requires session ownership",
731            );
732        };
733        let create_config: CreateVmConfig = match serde_json::from_str(&payload.config) {
734            Ok(config) => config,
735            Err(error) => {
736                return rejected(
737                    request,
738                    "invalid_config",
739                    &format!("invalid create VM config JSON: {error}"),
740                );
741            }
742        };
743        if let Err(error) = create_config.validate(BROWSER_MAX_FRAME_BYTES) {
744            return rejected(
745                request,
746                "invalid_config",
747                &format!("invalid create VM config: {error}"),
748            );
749        }
750
751        self.next_vm += 1;
752        let vm_id = format!("vm-{}", self.next_vm);
753        let mut kernel_config = KernelVmConfig::new(vm_id.clone());
754        kernel_config.env = create_config.env.clone();
755        if let Some(cwd) = create_config.cwd.clone() {
756            kernel_config.cwd = cwd;
757        }
758        kernel_config.loopback_exempt_ports = create_config
759            .loopback_exempt_ports
760            .iter()
761            .copied()
762            .collect();
763        let limits = match agentos_native_sidecar_core::vm_limits_from_config(
764            create_config.limits.as_ref(),
765            BROWSER_MAX_FRAME_BYTES,
766        ) {
767            Ok(limits) => limits,
768            Err(error) => {
769                return rejected(request, "invalid_config", &error.to_string());
770            }
771        };
772        kernel_config.resources = limits.resources;
773        let permissions = create_config
774            .permissions
775            .clone()
776            .unwrap_or_else(agentos_native_sidecar_core::deny_all_policy);
777        if let Err(error) = agentos_native_sidecar_core::validate_permissions_policy(&permissions) {
778            return rejected(request, "invalid_config", &error.to_string());
779        }
780        kernel_config.permissions = permissions_from_policy(permissions);
781
782        if let Err(error) = self
783            .sidecar
784            .create_vm_with_root_filesystem(kernel_config, create_config.root_filesystem)
785        {
786            return rejected(request, "create_vm_failed", &error.to_string());
787        }
788        self.active_vms.insert(vm_id.clone());
789
790        let ownership = OwnershipScope::vm(&connection_id, &session_id, &vm_id);
791        DispatchResult {
792            response: vm_created_response(request, vm_id.clone()),
793            events: vec![
794                vm_lifecycle_event(
795                    &connection_id,
796                    &session_id,
797                    &vm_id,
798                    VmLifecycleState::Creating,
799                ),
800                EventFrame::new(
801                    ownership,
802                    agentos_sidecar_protocol::protocol::EventPayload::VmLifecycle(
803                        agentos_sidecar_protocol::protocol::VmLifecycleEvent {
804                            state: VmLifecycleState::Ready,
805                        },
806                    ),
807                ),
808            ],
809        }
810    }
811
812    fn dispose_vm(&mut self, request: &RequestFrame, _payload: DisposeVmRequest) -> DispatchResult {
813        let Some(vm_id) = vm_id_of(&request.ownership) else {
814            return rejected(
815                request,
816                "invalid_ownership",
817                "dispose_vm requires VM ownership",
818            );
819        };
820        if let Err(error) = self.sidecar.dispose_vm(&vm_id) {
821            return rejected(request, "dispose_vm_failed", &error.to_string());
822        }
823        self.active_vms.remove(&vm_id);
824        self.executions.retain(|_, record| record.vm_id != vm_id);
825        self.process_executions
826            .retain(|(process_vm_id, _), _| process_vm_id != &vm_id);
827        DispatchResult {
828            response: vm_disposed_response(request, vm_id),
829            events: Vec::new(),
830        }
831    }
832
833    fn ext(&mut self, request: &RequestFrame, payload: ExtEnvelope) -> DispatchResult {
834        let response = match self
835            .sidecar
836            .dispatch_extension_request(BrowserExtensionRequest {
837                namespace: payload.namespace,
838                payload: payload.payload,
839                vm_id: vm_id_of(&request.ownership),
840                connection_id: connection_id_of(&request.ownership),
841            }) {
842            Ok(response) => response,
843            Err(error) => return rejected(request, "extension_failed", &error.to_string()),
844        };
845        DispatchResult {
846            response: respond(
847                request,
848                ResponsePayload::ExtResult(ExtEnvelope {
849                    namespace: response.namespace,
850                    payload: response.payload,
851                }),
852            ),
853            events: Vec::new(),
854        }
855    }
856
857    fn execute(&mut self, request: &RequestFrame, payload: ExecuteRequest) -> DispatchResult {
858        let Some(vm_id) = vm_id_of(&request.ownership) else {
859            return rejected(
860                request,
861                "invalid_ownership",
862                "execute requires VM ownership",
863            );
864        };
865        let process_key = (vm_id.clone(), payload.process_id.clone());
866        if self.process_executions.contains_key(&process_key) {
867            return rejected(
868                request,
869                "process_already_active",
870                "process_id is already active",
871            );
872        }
873        let runtime = match payload
874            .runtime
875            .clone()
876            .unwrap_or(GuestRuntimeKind::JavaScript)
877        {
878            GuestRuntimeKind::JavaScript | GuestRuntimeKind::Python => GuestRuntime::JavaScript,
879            GuestRuntimeKind::WebAssembly => GuestRuntime::WebAssembly,
880        };
881        let context = match runtime {
882            GuestRuntime::JavaScript => {
883                self.sidecar
884                    .create_javascript_context(CreateJavascriptContextRequest {
885                        vm_id: vm_id.clone(),
886                        bootstrap_module: payload.entrypoint.clone(),
887                    })
888            }
889            GuestRuntime::WebAssembly => {
890                self.sidecar.create_wasm_context(CreateWasmContextRequest {
891                    vm_id: vm_id.clone(),
892                    module_path: payload.entrypoint.clone(),
893                })
894            }
895        };
896        let context = match context {
897            Ok(context) => context,
898            Err(error) => return rejected(request, "execute_failed", &error.to_string()),
899        };
900
901        let mut argv = Vec::new();
902        if let Some(command) = payload.command.clone() {
903            argv.push(command);
904        }
905        argv.extend(payload.args.clone());
906        let started = match self.sidecar.start_execution_with_options(
907            StartExecutionRequest {
908                vm_id: vm_id.clone(),
909                context_id: context.context_id,
910                argv,
911                env: payload.env.clone().into_iter().collect(),
912                cwd: payload.cwd.clone().unwrap_or_else(|| String::from("/")),
913            },
914            BrowserExecutionOptions {
915                command_name: payload.command.clone(),
916                wasm_permission_tier: payload.wasm_permission_tier,
917            },
918        ) {
919            Ok(started) => started,
920            Err(error) => return rejected(request, "execute_failed", &error.to_string()),
921        };
922
923        self.executions.insert(
924            started.execution_id.clone(),
925            ExecutionRecord {
926                vm_id: vm_id.clone(),
927                process_id: payload.process_id.clone(),
928                ownership: request.ownership.clone(),
929            },
930        );
931        self.process_executions
932            .insert(process_key, started.execution_id.clone());
933        DispatchResult {
934            response: process_started_response(request, payload.process_id, None),
935            events: Vec::new(),
936        }
937    }
938
939    fn write_stdin(
940        &mut self,
941        request: &RequestFrame,
942        payload: WriteStdinRequest,
943    ) -> DispatchResult {
944        let Some(vm_id) = vm_id_of(&request.ownership) else {
945            return rejected(
946                request,
947                "invalid_ownership",
948                "write_stdin requires VM ownership",
949            );
950        };
951        let Some(execution_id) = self.execution_id_for(&vm_id, &payload.process_id) else {
952            return rejected(
953                request,
954                "unknown_process",
955                "write_stdin process is not active",
956            );
957        };
958        let accepted_bytes = payload.chunk.len() as u64;
959        if let Err(error) = self.sidecar.write_stdin(WriteExecutionStdinRequest {
960            vm_id,
961            execution_id,
962            chunk: payload.chunk,
963        }) {
964            return rejected(request, "write_stdin_failed", &error.to_string());
965        }
966        DispatchResult {
967            response: stdin_written_response(request, payload.process_id, accepted_bytes),
968            events: Vec::new(),
969        }
970    }
971
972    fn close_stdin(
973        &mut self,
974        request: &RequestFrame,
975        payload: CloseStdinRequest,
976    ) -> DispatchResult {
977        let Some(vm_id) = vm_id_of(&request.ownership) else {
978            return rejected(
979                request,
980                "invalid_ownership",
981                "close_stdin requires VM ownership",
982            );
983        };
984        let Some(execution_id) = self.execution_id_for(&vm_id, &payload.process_id) else {
985            return rejected(
986                request,
987                "unknown_process",
988                "close_stdin process is not active",
989            );
990        };
991        if let Err(error) = self.sidecar.close_stdin(ExecutionHandleRequest {
992            vm_id,
993            execution_id,
994        }) {
995            return rejected(request, "close_stdin_failed", &error.to_string());
996        }
997        DispatchResult {
998            response: stdin_closed_response(request, payload.process_id),
999            events: Vec::new(),
1000        }
1001    }
1002
1003    fn kill_process(
1004        &mut self,
1005        request: &RequestFrame,
1006        payload: KillProcessRequest,
1007    ) -> DispatchResult {
1008        let Some(vm_id) = vm_id_of(&request.ownership) else {
1009            return rejected(
1010                request,
1011                "invalid_ownership",
1012                "kill_process requires VM ownership",
1013            );
1014        };
1015        let Some(execution_id) = self.execution_id_for(&vm_id, &payload.process_id) else {
1016            return DispatchResult {
1017                response: process_killed_response(request, payload.process_id),
1018                events: Vec::new(),
1019            };
1020        };
1021        let signal = match agentos_native_sidecar_core::parse_posix_signal(&payload.signal) {
1022            Some(signal) => signal,
1023            None => {
1024                return rejected(
1025                    request,
1026                    "kill_process_failed",
1027                    &format!("unsupported kill_process signal {}", payload.signal),
1028                );
1029            }
1030        };
1031        if signal == 0 {
1032            return DispatchResult {
1033                response: process_killed_response(request, payload.process_id),
1034                events: Vec::new(),
1035            };
1036        }
1037        if let Err(error) =
1038            self.sidecar
1039                .signal_execution_kernel_process(&vm_id, &execution_id, signal)
1040        {
1041            return rejected(request, "kill_process_failed", &error.to_string());
1042        }
1043        if let Some(bridge_signal) = execution_signal_from_number(signal) {
1044            if let Err(error) = self
1045                .sidecar
1046                .bridge_mut()
1047                .kill_execution(KillExecutionRequest {
1048                    vm_id,
1049                    execution_id,
1050                    signal: bridge_signal,
1051                })
1052            {
1053                return rejected(request, "kill_process_failed", &format!("{error:?}"));
1054            }
1055        }
1056        DispatchResult {
1057            response: process_killed_response(request, payload.process_id),
1058            events: Vec::new(),
1059        }
1060    }
1061
1062    fn pump_execution_events(&mut self) {
1063        for vm_id in self.active_vms.iter().cloned().collect::<Vec<_>>() {
1064            while let Ok(Some(event)) =
1065                self.sidecar
1066                    .poll_execution_event(PollExecutionEventRequest {
1067                        vm_id: vm_id.clone(),
1068                    })
1069            {
1070                if let Some(frame) = self.execution_event_to_frame(event) {
1071                    self.pending_events.push_back(frame);
1072                }
1073            }
1074        }
1075    }
1076
1077    fn execution_event_to_frame(&mut self, event: ExecutionEvent) -> Option<EventFrame> {
1078        match event {
1079            ExecutionEvent::Stdout(chunk) => {
1080                let record = self.executions.get(&chunk.execution_id)?;
1081                Some(process_output_event(
1082                    record.ownership.clone(),
1083                    &record.process_id,
1084                    StreamChannel::Stdout,
1085                    chunk.chunk,
1086                ))
1087            }
1088            ExecutionEvent::Stderr(chunk) => {
1089                let record = self.executions.get(&chunk.execution_id)?;
1090                Some(process_output_event(
1091                    record.ownership.clone(),
1092                    &record.process_id,
1093                    StreamChannel::Stderr,
1094                    chunk.chunk,
1095                ))
1096            }
1097            ExecutionEvent::Exited(exited) => {
1098                let record = self.executions.remove(&exited.execution_id)?;
1099                self.process_executions
1100                    .remove(&(record.vm_id.clone(), record.process_id.clone()));
1101                Some(process_exited_event(
1102                    record.ownership,
1103                    &record.process_id,
1104                    exited.exit_code,
1105                ))
1106            }
1107            ExecutionEvent::GuestRequest(call) => {
1108                let record = self.executions.get(&call.execution_id)?;
1109                Some(unsupported_guest_kernel_call_event(
1110                    record.ownership.clone(),
1111                    &record.process_id,
1112                    &call.execution_id,
1113                    &call.operation,
1114                    call.payload.len(),
1115                ))
1116            }
1117            ExecutionEvent::SignalState(_) => None,
1118        }
1119    }
1120
1121    fn execution_id_for(&self, vm_id: &str, process_id: &str) -> Option<String> {
1122        self.process_executions
1123            .get(&(vm_id.to_string(), process_id.to_string()))
1124            .cloned()
1125    }
1126}
1127
1128fn rejected(request: &RequestFrame, code: &str, message: &str) -> DispatchResult {
1129    DispatchResult {
1130        response: reject(request, code, message),
1131        events: Vec::new(),
1132    }
1133}