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 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 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 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 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}