1use crate::wire::{
2 self, AuthenticatedResponse, ExtEnvelope, OwnershipScope, ProtocolCodecError, ProtocolFrame,
3 RequestFrame, RequestId, RequestPayload, ResponseFrame, ResponsePayload, SessionOpenedResponse,
4 SidecarResponseFrame, WireDispatchResult, WireFrameCodec,
5};
6use crate::{
7 EventSinkTransport, Extension, ExtensionInterruptRequest, NativeSidecar, NativeSidecarConfig,
8 SidecarError, SidecarRequestTransport,
9};
10use agentos_bridge::queue_tracker::{tracked_sync_channel, TrackedLimit, TrackedSyncSender};
11use agentos_bridge::{
12 BridgeTypes, ChmodRequest, ClockBridge, ClockRequest, CommandPermissionRequest,
13 CreateDirRequest, CreateJavascriptContextRequest, CreateWasmContextRequest, DiagnosticRecord,
14 DirectoryEntry, EnvironmentPermissionRequest, EventBridge, ExecutionBridge, ExecutionEvent,
15 ExecutionHandleRequest, FileMetadata, FilesystemBridge, FilesystemPermissionRequest,
16 FilesystemSnapshot, FlushFilesystemStateRequest, GuestContextHandle, KillExecutionRequest,
17 LifecycleEventRecord, LoadFilesystemStateRequest, LogRecord, NetworkPermissionRequest,
18 PathRequest, PermissionBridge, PermissionDecision, PersistenceBridge,
19 PollExecutionEventRequest, RandomBridge, RandomBytesRequest, ReadDirRequest, ReadFileRequest,
20 RenameRequest, ScheduleTimerRequest, ScheduledTimer, StartExecutionRequest, StartedExecution,
21 StructuredEventRecord, SymlinkRequest, TruncateRequest, WriteExecutionStdinRequest,
22 WriteFileRequest,
23};
24use agentos_native_sidecar_core::{
25 generated_wire_blocking_extension_interrupt, BlockingExtensionInterrupt,
26};
27use std::collections::{BTreeMap, BTreeSet};
28use std::error::Error;
29use std::fmt;
30use std::fs::{self, OpenOptions};
31use std::io::{self, Read, Write};
32use std::os::unix::fs::{symlink as create_symlink, MetadataExt, PermissionsExt};
33use std::path::{Path, PathBuf};
34use std::sync::{mpsc, Arc, Mutex};
35use std::thread;
36use std::time::{Duration, Instant, SystemTime};
37use tokio::sync::{
38 mpsc::{channel, unbounded_channel, Receiver},
39 Notify,
40};
41use tokio::time;
42
43const EVENT_PUMP_INTERVAL: Duration = Duration::from_micros(250);
50const MAX_STDIN_FRAME_QUEUE: usize = 128;
51const MAX_EVENT_READY_QUEUE: usize = 1;
52const MAX_STDOUT_FRAME_QUEUE: usize = 4096;
56
57#[cfg(test)]
58fn request_frame(
59 request_id: RequestId,
60 ownership: OwnershipScope,
61 payload: RequestPayload,
62) -> RequestFrame {
63 RequestFrame {
64 schema: wire::protocol_schema(),
65 request_id,
66 ownership,
67 payload,
68 }
69}
70
71fn response_frame(
72 request_id: RequestId,
73 ownership: OwnershipScope,
74 payload: ResponsePayload,
75) -> ResponseFrame {
76 ResponseFrame {
77 schema: wire::protocol_schema(),
78 request_id,
79 ownership,
80 payload,
81 }
82}
83
84#[cfg(test)]
85fn connection_ownership(connection_id: &str) -> OwnershipScope {
86 OwnershipScope::ConnectionOwnership(wire::ConnectionOwnership {
87 connection_id: connection_id.to_owned(),
88 })
89}
90
91fn session_ownership(connection_id: &str, session_id: &str) -> OwnershipScope {
92 OwnershipScope::SessionOwnership(wire::SessionOwnership {
93 connection_id: connection_id.to_owned(),
94 session_id: session_id.to_owned(),
95 })
96}
97
98#[cfg(test)]
99fn vm_ownership(connection_id: &str, session_id: &str, vm_id: &str) -> OwnershipScope {
100 OwnershipScope::VmOwnership(wire::VmOwnership {
101 connection_id: connection_id.to_owned(),
102 session_id: session_id.to_owned(),
103 vm_id: vm_id.to_owned(),
104 })
105}
106
107fn wire_protocol_error(error: ProtocolCodecError) -> SidecarError {
108 SidecarError::InvalidState(format!("invalid generated wire protocol frame: {error}"))
109}
110
111pub fn run() -> Result<(), Box<dyn Error>> {
112 run_with_extensions(Vec::new())
113}
114
115pub fn run_with_extensions(extensions: Vec<Box<dyn Extension>>) -> Result<(), Box<dyn Error>> {
116 if let Err(error) = agentos_execution::v8_host::ensure_runtime_initialized() {
121 eprintln!("embedded V8 runtime init failed at startup: {error}");
122 }
123 tokio::runtime::Builder::new_current_thread()
124 .enable_all()
125 .build()?
126 .block_on(run_async(extensions))
127}
128
129async fn run_async(extensions: Vec<Box<dyn Extension>>) -> Result<(), Box<dyn Error>> {
130 let config = NativeSidecarConfig {
131 compile_cache_root: Some(default_compile_cache_root()),
132 ..NativeSidecarConfig::default()
133 };
134 let codec = WireFrameCodec::new(config.max_frame_bytes);
135 let mut sidecar =
136 NativeSidecar::with_config_and_extensions(LocalBridge::default(), config, extensions)?;
137 let mut active_sessions = BTreeSet::<SessionScope>::new();
138 let mut active_connections = BTreeSet::<String>::new();
139 let (stdin_tx, mut stdin_rx) =
140 channel::<Result<Option<ProtocolFrame>, String>>(MAX_STDIN_FRAME_QUEUE);
141 let stdin_gauge = agentos_bridge::queue_tracker::register_queue(
142 TrackedLimit::SidecarStdinFrames,
143 MAX_STDIN_FRAME_QUEUE,
144 );
145 let (event_ready_tx, mut event_ready_rx) = channel::<()>(MAX_EVENT_READY_QUEUE);
146 let (write_tx, write_rx) = tracked_sync_channel::<ProtocolFrame>(
147 TrackedLimit::SidecarStdoutFrames,
148 MAX_STDOUT_FRAME_QUEUE,
149 );
150 let (write_error_tx, mut write_error_rx) = unbounded_channel::<String>();
151
152 let (limit_warning_tx, mut limit_warning_rx) =
158 unbounded_channel::<agentos_bridge::queue_tracker::LimitWarning>();
159 agentos_bridge::queue_tracker::set_limit_warning_handler(Box::new(move |warning| {
160 let _ = limit_warning_tx.send(warning.clone());
161 }));
162 let callback_transport = Arc::new(FrameSidecarRequestTransport::new(write_tx.clone()));
163 sidecar.set_sidecar_request_transport(callback_transport.clone());
164 let event_transport = Arc::new(FrameEventTransport::new(write_tx.clone()));
169 sidecar.set_event_transport(event_transport);
170 let mut event_pump = time::interval(EVENT_PUMP_INTERVAL);
171 let process_event_notify = Arc::new(Notify::new());
172 sidecar
173 .javascript_engine
174 .set_event_notify(Some(process_event_notify.clone()));
175 let writer_codec = codec.clone();
176 let reader_codec = codec.clone();
177 let writer_error_tx = write_error_tx.clone();
178 thread::spawn(move || {
179 let mut writer = io::BufWriter::new(io::stdout());
180 while let Ok(frame) = write_rx.recv() {
181 if let Err(error) = write_frame(&writer_codec, &mut writer, &frame) {
182 let _ = writer_error_tx.send(error.to_string());
183 break;
184 }
185 }
186 });
187
188 thread::spawn({
189 let callback_transport = callback_transport.clone();
190 let read_error_tx = write_error_tx.clone();
191 move || {
192 let mut stdin = io::stdin();
193 loop {
194 let frame = match read_frame(&reader_codec, &mut stdin) {
195 Ok(Some(ProtocolFrame::SidecarResponseFrame(response))) => {
196 if callback_transport.accept_response(response.clone()) {
197 continue;
198 }
199 Ok(Some(ProtocolFrame::SidecarResponseFrame(response)))
200 }
201 Ok(Some(frame)) => Ok(Some(frame)),
202 other => other,
203 }
204 .map_err(|error: Box<dyn Error>| error.to_string());
205 let should_stop = matches!(frame, Ok(None) | Err(_));
206 match enqueue_stdin_frame(&stdin_tx, frame) {
207 Ok(()) => {
208 stdin_gauge.observe_depth(
211 stdin_tx.max_capacity().saturating_sub(stdin_tx.capacity()),
212 );
213 }
214 Err(StdinFrameQueueError::Full(message)) => {
215 let _ = read_error_tx.send(message);
216 break;
217 }
218 Err(StdinFrameQueueError::Closed) => break,
219 }
220 if should_stop {
221 break;
222 }
223 }
224 }
225 });
226
227 flush_sidecar_requests(&mut sidecar, &write_tx)?;
228 let mut pending_frame: Option<ProtocolFrame> = None;
229 let mut limit_warning_closed = false;
230
231 loop {
232 if let Some(frame) = pending_frame.take() {
233 handle_protocol_frame(
234 frame,
235 &mut sidecar,
236 &mut stdin_rx,
237 &mut pending_frame,
238 &write_tx,
239 &mut active_sessions,
240 &mut active_connections,
241 )
242 .await?;
243 continue;
244 }
245
246 tokio::select! {
247 maybe_frame = stdin_rx.recv() => {
248 let Some(frame) = maybe_frame else {
249 break;
250 };
251 let Some(frame) = frame.map_err(io::Error::other)? else {
252 break;
253 };
254 handle_protocol_frame(
255 frame,
256 &mut sidecar,
257 &mut stdin_rx,
258 &mut pending_frame,
259 &write_tx,
260 &mut active_sessions,
261 &mut active_connections,
262 ).await?;
263 }
264 maybe_warning = limit_warning_rx.recv(), if !limit_warning_closed => {
265 match maybe_warning {
266 Some(warning) => {
267 if let Some(connection_id) = active_connections.iter().next() {
273 let mut detail = std::collections::HashMap::new();
274 detail.insert(String::from("limit"), warning.name.as_str().to_string());
275 detail.insert(
276 String::from("category"),
277 warning.category.as_str().to_string(),
278 );
279 detail.insert(String::from("observed"), warning.observed.to_string());
280 detail.insert(String::from("capacity"), warning.capacity.to_string());
281 detail.insert(
282 String::from("fillPercent"),
283 warning.fill_percent.to_string(),
284 );
285 let frame = crate::service::structured_event_frame(
286 connection_id,
287 "limit_warning",
288 detail,
289 )?;
290 send_output_frame(&write_tx, ProtocolFrame::EventFrame(frame))?;
291 }
292 }
293 None => {
294 limit_warning_closed = true;
299 }
300 }
301 }
302 maybe_ready = event_ready_rx.recv() => {
303 let Some(()) = maybe_ready else {
304 break;
305 };
306 loop {
307 let mut emitted_frame = false;
308 for session in active_sessions.iter().cloned().collect::<Vec<_>>() {
309 if let Some(frame) = sidecar
310 .poll_event_wire(&session.ownership_scope(), Duration::ZERO)
311 .await?
312 {
313 send_output_frame(&write_tx, ProtocolFrame::EventFrame(frame))?;
314 emitted_frame = true;
315 }
316 }
317
318 if !emitted_frame {
319 break;
320 }
321 }
322 flush_sidecar_requests(&mut sidecar, &write_tx)?;
323 }
324 _ = process_event_notify.notified() => {
325 for session in active_sessions.iter().cloned().collect::<Vec<_>>() {
326 if sidecar.pump_process_events(&session.compat_ownership_scope()).await? {
327 let _ = event_ready_tx.try_send(());
328 }
329 }
330 flush_sidecar_requests(&mut sidecar, &write_tx)?;
331 }
332 _ = event_pump.tick() => {
333 for session in active_sessions.iter().cloned().collect::<Vec<_>>() {
334 if sidecar.pump_process_events(&session.compat_ownership_scope()).await? {
335 let _ = event_ready_tx.try_send(());
336 }
337 }
338 flush_sidecar_requests(&mut sidecar, &write_tx)?;
339 }
340 maybe_write_error = write_error_rx.recv() => {
341 if let Some(error) = maybe_write_error {
342 return Err(io::Error::new(io::ErrorKind::BrokenPipe, error).into());
343 }
344 }
345 }
346 }
347
348 cleanup_connections(&mut sidecar, &active_connections, &mut active_sessions).await;
349 Ok(())
350}
351
352async fn handle_protocol_frame(
353 frame: ProtocolFrame,
354 sidecar: &mut NativeSidecar<LocalBridge>,
355 stdin_rx: &mut Receiver<Result<Option<ProtocolFrame>, String>>,
356 pending_frame: &mut Option<ProtocolFrame>,
357 write_tx: &TrackedSyncSender<ProtocolFrame>,
358 active_sessions: &mut BTreeSet<SessionScope>,
359 active_connections: &mut BTreeSet<String>,
360) -> Result<(), Box<dyn Error>> {
361 match frame {
362 ProtocolFrame::RequestFrame(request) => {
363 let (dispatch, extra_responses) =
364 dispatch_with_prompt_interrupt(sidecar, request.clone(), stdin_rx, pending_frame)
365 .await?;
366 track_session_state(
367 &dispatch.response.payload,
368 active_sessions,
369 active_connections,
370 );
371
372 send_output_frame(write_tx, ProtocolFrame::ResponseFrame(dispatch.response))?;
373 for response in extra_responses {
374 send_output_frame(write_tx, ProtocolFrame::ResponseFrame(response))?;
375 }
376 for event in dispatch.events {
377 send_output_frame(write_tx, ProtocolFrame::EventFrame(event))?;
378 }
379 flush_sidecar_requests(sidecar, write_tx)?;
380 }
381 ProtocolFrame::SidecarResponseFrame(response) => {
382 sidecar.accept_wire_sidecar_response(response)?;
383 flush_sidecar_requests(sidecar, write_tx)?;
384 }
385 other => {
386 return Err(format!(
387 "expected request or sidecar_response frame on stdin, received {}",
388 frame_kind(&other)
389 )
390 .into());
391 }
392 }
393 untrack_disposed_sessions(&sidecar.take_disposed_sessions(), active_sessions);
396 Ok(())
397}
398
399fn untrack_disposed_sessions(
404 disposed: &[(String, String)],
405 active_sessions: &mut BTreeSet<SessionScope>,
406) {
407 for (connection_id, session_id) in disposed {
408 active_sessions.remove(&SessionScope {
409 connection_id: connection_id.clone(),
410 session_id: session_id.clone(),
411 });
412 }
413}
414
415async fn dispatch_with_prompt_interrupt(
416 sidecar: &mut NativeSidecar<LocalBridge>,
417 request: RequestFrame,
418 stdin_rx: &mut Receiver<Result<Option<ProtocolFrame>, String>>,
419 pending_frame: &mut Option<ProtocolFrame>,
420) -> Result<(WireDispatchResult, Vec<ResponseFrame>), Box<dyn Error>> {
421 let Some(blocking_request) = blocking_extension_request(sidecar, &request) else {
422 return Ok((sidecar.dispatch_wire(request).await?, Vec::new()));
423 };
424
425 let mut dispatch = Box::pin(sidecar.dispatch_wire(request.clone()));
426 tokio::select! {
427 result = dispatch.as_mut() => Ok((result?, Vec::new())),
428 maybe_frame = stdin_rx.recv() => {
429 let frame = decode_stdin_frame(maybe_frame)?;
430 if let Some(frame) = frame {
431 if let Some(interrupt) = extension_interrupt_response(&blocking_request, &request, &frame) {
432 drop(dispatch);
433 let mut extra_responses = Vec::new();
434 if let Some(response) = interrupt.interrupting_response {
435 extra_responses.push(response);
436 } else {
437 *pending_frame = Some(frame);
438 }
439 return Ok((interrupt.interrupted_dispatch, extra_responses));
440 }
441 *pending_frame = Some(frame);
442 }
443 Ok((dispatch.await?, Vec::new()))
444 }
445 }
446}
447
448fn decode_stdin_frame(
449 maybe_frame: Option<Result<Option<ProtocolFrame>, String>>,
450) -> Result<Option<ProtocolFrame>, Box<dyn Error>> {
451 let Some(frame) = maybe_frame else {
452 return Ok(None);
453 };
454 Ok(frame.map_err(io::Error::other)?)
455}
456
457struct BlockingExtensionRequest {
458 namespace: String,
459 payload: Vec<u8>,
460 extension: Arc<dyn Extension>,
461}
462
463struct ExtensionInterruptDispatch {
464 interrupted_dispatch: WireDispatchResult,
465 interrupting_response: Option<ResponseFrame>,
466}
467
468fn blocking_extension_request(
469 sidecar: &NativeSidecar<LocalBridge>,
470 request: &RequestFrame,
471) -> Option<BlockingExtensionRequest> {
472 let RequestPayload::ExtEnvelope(envelope) = &request.payload else {
473 return None;
474 };
475 let extension = sidecar.extensions.get(&envelope.namespace)?.clone();
476 if !extension.is_blocking_request(&envelope.payload) {
477 return None;
478 }
479 Some(BlockingExtensionRequest {
480 namespace: envelope.namespace.clone(),
481 payload: envelope.payload.clone(),
482 extension,
483 })
484}
485
486fn extension_interrupt_response(
487 blocking_request: &BlockingExtensionRequest,
488 active_request: &RequestFrame,
489 frame: &ProtocolFrame,
490) -> Option<ExtensionInterruptDispatch> {
491 match frame {
492 ProtocolFrame::RequestFrame(request) => {
493 let interrupt = generated_wire_blocking_extension_interrupt(
494 active_request,
495 &blocking_request.namespace,
496 request,
497 )?;
498 let interrupt = blocking_request.extension.interrupt_blocking_request(
499 &blocking_request.payload,
500 match interrupt {
501 BlockingExtensionInterrupt::ExtensionPayload(payload) => {
502 ExtensionInterruptRequest::ExtensionPayload(payload)
503 }
504 BlockingExtensionInterrupt::KillProcess => {
505 ExtensionInterruptRequest::KillProcess
506 }
507 },
508 )?;
509 let interrupted_dispatch = interrupted_extension_dispatch(
510 active_request,
511 &blocking_request.namespace,
512 interrupt.interrupted_response_payload,
513 );
514 let interrupting_response = interrupt.interrupting_response_payload.map(|payload| {
515 response_frame(
516 request.request_id,
517 request.ownership.clone(),
518 ResponsePayload::ExtEnvelope(ExtEnvelope {
519 namespace: blocking_request.namespace.clone(),
520 payload,
521 }),
522 )
523 });
524 Some(ExtensionInterruptDispatch {
525 interrupted_dispatch,
526 interrupting_response,
527 })
528 }
529 ProtocolFrame::ResponseFrame(_)
535 | ProtocolFrame::EventFrame(_)
536 | ProtocolFrame::SidecarRequestFrame(_)
537 | ProtocolFrame::SidecarResponseFrame(_) => None,
538 }
539}
540
541fn interrupted_extension_dispatch(
542 request: &RequestFrame,
543 namespace: &str,
544 payload: Vec<u8>,
545) -> WireDispatchResult {
546 if !matches!(request.payload, RequestPayload::ExtEnvelope(_)) {
547 unreachable!("interrupted extension dispatch requires an extension request");
548 }
549
550 let response = ResponsePayload::ExtEnvelope(ExtEnvelope {
551 namespace: namespace.to_string(),
552 payload,
553 });
554 WireDispatchResult {
555 response: response_frame(request.request_id, request.ownership.clone(), response),
556 events: Vec::new(),
557 }
558}
559
560async fn cleanup_connections(
561 sidecar: &mut NativeSidecar<LocalBridge>,
562 active_connections: &BTreeSet<String>,
563 active_sessions: &mut BTreeSet<SessionScope>,
564) {
565 for connection_id in active_connections {
566 let _ = sidecar.remove_connection(connection_id).await;
567 }
568 untrack_disposed_sessions(&sidecar.take_disposed_sessions(), active_sessions);
569}
570
571fn track_session_state(
572 payload: &ResponsePayload,
573 active_sessions: &mut BTreeSet<SessionScope>,
574 active_connections: &mut BTreeSet<String>,
575) {
576 match payload {
577 ResponsePayload::AuthenticatedResponse(AuthenticatedResponse { connection_id, .. }) => {
578 active_connections.insert(connection_id.clone());
579 }
580 ResponsePayload::SessionOpenedResponse(SessionOpenedResponse {
581 session_id,
582 owner_connection_id,
583 }) => {
584 active_sessions.insert(SessionScope {
585 connection_id: owner_connection_id.clone(),
586 session_id: session_id.clone(),
587 });
588 }
589 _ => {}
590 }
591}
592
593fn read_frame(
594 codec: &WireFrameCodec,
595 reader: &mut impl Read,
596) -> Result<Option<ProtocolFrame>, Box<dyn Error>> {
597 let mut prefix = [0u8; 4];
598 match reader.read_exact(&mut prefix) {
599 Ok(()) => {}
600 Err(error) if error.kind() == io::ErrorKind::UnexpectedEof => {
601 return Ok(None);
602 }
603 Err(error) => return Err(error.into()),
604 }
605
606 let declared_len = u32::from_be_bytes(prefix) as usize;
607 if declared_len > codec.max_frame_bytes() {
608 return Err(ProtocolCodecError::FrameTooLarge {
609 size: declared_len,
610 max: codec.max_frame_bytes(),
611 }
612 .into());
613 }
614 let total_len = prefix.len().saturating_add(declared_len);
615 let mut bytes = Vec::with_capacity(total_len);
616 bytes.extend_from_slice(&prefix);
617 bytes.resize(total_len, 0);
618 reader.read_exact(&mut bytes[prefix.len()..])?;
619
620 Ok(Some(codec.decode(&bytes)?))
621}
622
623fn write_frame(
624 codec: &WireFrameCodec,
625 writer: &mut impl Write,
626 frame: &ProtocolFrame,
627) -> Result<(), Box<dyn Error>> {
628 let bytes = codec.encode(frame)?;
629 writer.write_all(&bytes)?;
630 writer.flush()?;
631 Ok(())
632}
633
634fn frame_kind(frame: &ProtocolFrame) -> &'static str {
635 match frame {
636 ProtocolFrame::RequestFrame(_) => "request",
637 ProtocolFrame::ResponseFrame(_) => "response",
638 ProtocolFrame::EventFrame(_) => "event",
639 ProtocolFrame::SidecarRequestFrame(_) => "sidecar_request",
640 ProtocolFrame::SidecarResponseFrame(_) => "sidecar_response",
641 }
642}
643
644#[derive(Debug, Clone, PartialEq, Eq)]
645enum StdinFrameQueueError {
646 Full(String),
647 Closed,
648}
649
650fn enqueue_stdin_frame(
651 sender: &tokio::sync::mpsc::Sender<Result<Option<ProtocolFrame>, String>>,
652 frame: Result<Option<ProtocolFrame>, String>,
653) -> Result<(), StdinFrameQueueError> {
654 sender.try_send(frame).map_err(|error| match error {
655 tokio::sync::mpsc::error::TrySendError::Full(_) => StdinFrameQueueError::Full(format!(
656 "stdin frame queue exceeded {MAX_STDIN_FRAME_QUEUE} pending frames"
657 )),
658 tokio::sync::mpsc::error::TrySendError::Closed(_) => StdinFrameQueueError::Closed,
659 })
660}
661
662fn flush_sidecar_requests(
663 sidecar: &mut NativeSidecar<LocalBridge>,
664 writer: &TrackedSyncSender<ProtocolFrame>,
665) -> Result<(), Box<dyn Error>> {
666 while let Some(request) = sidecar.pop_wire_sidecar_request()? {
667 send_output_frame(writer, ProtocolFrame::SidecarRequestFrame(request))?;
668 }
669 Ok(())
670}
671
672fn send_output_frame(
673 writer: &TrackedSyncSender<ProtocolFrame>,
674 frame: ProtocolFrame,
675) -> Result<(), io::Error> {
676 writer.send(frame).map_err(|_disconnected| {
687 io::Error::new(io::ErrorKind::BrokenPipe, "stdout writer disconnected")
688 })
689}
690
691fn default_compile_cache_root() -> PathBuf {
692 std::env::temp_dir().join("agentos-native-sidecar-compile-cache")
699}
700
701#[cfg(test)]
702mod tests {
703 use super::*;
704 use crate::wire::{AuthenticateRequest, KillProcessRequest};
705 use crate::{ExtensionContext, ExtensionFuture, ExtensionInterruptResponse, ExtensionResponse};
706 use std::io::Cursor;
707
708 const TEST_EXTENSION_NAMESPACE: &str = "dev.rivet.secure-exec.test.blocking";
709
710 #[test]
711 fn read_frame_rejects_oversized_prefix_before_allocating_payload() {
712 let codec = WireFrameCodec::new(16);
713 let mut reader = Cursor::new((32_u32).to_be_bytes().to_vec());
714
715 let error = read_frame(&codec, &mut reader).expect_err("oversized frame should fail");
716 let error = error
717 .downcast::<ProtocolCodecError>()
718 .expect("protocol codec error");
719 assert!(matches!(
720 *error,
721 ProtocolCodecError::FrameTooLarge { size: 32, max: 16 }
722 ));
723 }
724
725 #[test]
726 fn stdio_work_queues_are_bounded() {
727 let (stdin_tx, _stdin_rx) =
728 channel::<Result<Option<ProtocolFrame>, String>>(MAX_STDIN_FRAME_QUEUE);
729 for _ in 0..MAX_STDIN_FRAME_QUEUE {
730 enqueue_stdin_frame(&stdin_tx, Ok(None))
731 .expect("stdin frame queue should accept capacity");
732 }
733 assert!(matches!(
734 enqueue_stdin_frame(&stdin_tx, Ok(None)),
735 Err(StdinFrameQueueError::Full(_))
736 ));
737
738 let (event_ready_tx, _event_ready_rx) = channel::<()>(MAX_EVENT_READY_QUEUE);
739 event_ready_tx
740 .try_send(())
741 .expect("event-ready queue should accept capacity");
742 assert!(matches!(
743 event_ready_tx.try_send(()),
744 Err(tokio::sync::mpsc::error::TrySendError::Full(_))
745 ));
746 }
747
748 #[test]
755 fn stdout_frame_queue_applies_backpressure_instead_of_crashing() {
756 let queue_frame = |request_id: RequestId| {
757 ProtocolFrame::RequestFrame(request_frame(
758 request_id,
759 connection_ownership("conn-queue"),
760 RequestPayload::AuthenticateRequest(AuthenticateRequest {
761 client_name: String::from("queue-test"),
762 auth_token: String::from("token"),
763 protocol_version: wire::PROTOCOL_VERSION,
764 bridge_version: agentos_bridge::bridge_contract().version,
765 }),
766 ))
767 };
768
769 let queue_cap = 8usize;
774 let total_frames = queue_cap * 3;
775 let (stdout_tx, stdout_rx) =
776 tracked_sync_channel::<ProtocolFrame>(TrackedLimit::SidecarStdoutFrames, queue_cap);
777 let drainer = std::thread::spawn(move || {
778 let mut drained = 0usize;
779 while stdout_rx.recv().is_ok() {
780 drained += 1;
781 std::thread::sleep(std::time::Duration::from_millis(1));
782 }
783 drained
784 });
785
786 for request_id in 0..total_frames {
787 send_output_frame(&stdout_tx, queue_frame(request_id as RequestId))
788 .expect("backpressured stdout queue must accept frames, not crash");
789 }
790 drop(stdout_tx);
791 let drained = drainer.join().expect("drainer thread panicked");
792 assert_eq!(
793 drained, total_frames,
794 "every frame must survive the backpressured queue"
795 );
796
797 let (closed_tx, closed_rx) =
800 tracked_sync_channel::<ProtocolFrame>(TrackedLimit::SidecarStdoutFrames, queue_cap);
801 drop(closed_rx);
802 let error = send_output_frame(&closed_tx, queue_frame(0))
803 .expect_err("send to a dropped writer must error");
804 assert_eq!(error.kind(), io::ErrorKind::BrokenPipe);
805 }
806
807 #[test]
813 fn disposed_sessions_are_untracked_from_active_sessions() {
814 let mut active_sessions = BTreeSet::<SessionScope>::new();
815 let mut active_connections = BTreeSet::<String>::new();
816 track_session_state(
817 &ResponsePayload::SessionOpenedResponse(SessionOpenedResponse {
818 session_id: String::from("session-1"),
819 owner_connection_id: String::from("conn-1"),
820 }),
821 &mut active_sessions,
822 &mut active_connections,
823 );
824 assert_eq!(
825 active_sessions.len(),
826 1,
827 "opening a session should track it for the event pump"
828 );
829
830 untrack_disposed_sessions(
831 &[(String::from("conn-1"), String::from("session-1"))],
832 &mut active_sessions,
833 );
834 assert!(
835 active_sessions.is_empty(),
836 "a disposed session must be removed from the active-session set"
837 );
838 }
839
840 #[test]
841 fn read_frame_decodes_wire_authenticate_request() {
842 let codec = WireFrameCodec::new(wire::DEFAULT_MAX_FRAME_BYTES);
843 let frame = ProtocolFrame::RequestFrame(request_frame(
844 1,
845 connection_ownership("client-hint"),
846 RequestPayload::AuthenticateRequest(AuthenticateRequest {
847 client_name: "probe".to_string(),
848 auth_token: "probe-token".to_string(),
849 protocol_version: wire::PROTOCOL_VERSION,
850 bridge_version: agentos_bridge::bridge_contract().version,
851 }),
852 ));
853 let encoded = codec.encode(&frame).expect("encode wire frame");
854 let mut reader = Cursor::new(encoded);
855
856 let decoded = read_frame(&codec, &mut reader)
857 .expect("decode bare frame")
858 .expect("frame present");
859
860 assert_eq!(decoded, frame);
861 }
862
863 #[test]
864 fn extension_close_interrupts_matching_blocking_request() {
865 let ownership = vm_ownership("conn-1", "session-1", "vm-1");
866 let prompt = test_extension_request_frame(10, ownership.clone(), "prompt:ext-session-1");
867 let close = ProtocolFrame::RequestFrame(test_extension_request_frame(
868 11,
869 ownership,
870 "close:ext-session-1",
871 ));
872
873 let blocking_request = blocking_extension_request(&prompt);
874 let interrupt = extension_interrupt_response(&blocking_request, &prompt, &close)
875 .expect("close should interrupt prompt");
876
877 assert_eq!(interrupt.interrupted_dispatch.response.request_id, 10);
878 let ResponsePayload::ExtEnvelope(envelope) =
879 interrupt.interrupted_dispatch.response.payload
880 else {
881 panic!("expected extension response");
882 };
883 assert_eq!(envelope.namespace, TEST_EXTENSION_NAMESPACE);
884 assert_eq!(envelope.payload, b"prompt-cancelled:ext-session-1");
885 }
886
887 #[test]
888 fn extension_cancel_interrupt_gets_synthetic_response() {
889 let ownership = vm_ownership("conn-1", "session-1", "vm-1");
890 let prompt = test_extension_request_frame(10, ownership.clone(), "prompt:ext-session-1");
891 let cancel = ProtocolFrame::RequestFrame(test_extension_request_frame(
892 11,
893 ownership,
894 "cancel:ext-session-1",
895 ));
896
897 let blocking_request = blocking_extension_request(&prompt);
898 let interrupt = extension_interrupt_response(&blocking_request, &prompt, &cancel)
899 .expect("cancel should interrupt prompt");
900 let response = interrupt
901 .interrupting_response
902 .expect("cancel should get a response");
903
904 assert_eq!(response.request_id, 11);
905 let ResponsePayload::ExtEnvelope(envelope) = response.payload else {
906 panic!("expected extension response");
907 };
908 assert_eq!(envelope.namespace, TEST_EXTENSION_NAMESPACE);
909 assert_eq!(envelope.payload, b"cancelled:ext-session-1");
910 }
911
912 #[test]
913 fn kill_process_interrupts_blocking_extension_request() {
914 let ownership = vm_ownership("conn-1", "session-1", "vm-1");
915 let prompt = test_extension_request_frame(10, ownership.clone(), "prompt:ext-session-1");
916 let kill = ProtocolFrame::RequestFrame(request_frame(
917 11,
918 ownership,
919 RequestPayload::KillProcessRequest(KillProcessRequest {
920 process_id: "adapter-process".to_string(),
921 signal: "SIGTERM".to_string(),
922 }),
923 ));
924
925 let blocking_request = blocking_extension_request(&prompt);
926 let interrupt = extension_interrupt_response(&blocking_request, &prompt, &kill)
927 .expect("kill should interrupt prompt");
928
929 assert_eq!(interrupt.interrupted_dispatch.response.request_id, 10);
930 assert!(interrupt.interrupting_response.is_none());
931 }
932
933 fn test_extension_request_frame(
934 request_id: RequestId,
935 ownership: OwnershipScope,
936 payload: &str,
937 ) -> RequestFrame {
938 request_frame(
939 request_id,
940 ownership,
941 RequestPayload::ExtEnvelope(ExtEnvelope {
942 namespace: TEST_EXTENSION_NAMESPACE.to_string(),
943 payload: payload.as_bytes().to_vec(),
944 }),
945 )
946 }
947
948 fn blocking_extension_request(request: &RequestFrame) -> BlockingExtensionRequest {
949 let RequestPayload::ExtEnvelope(envelope) = &request.payload else {
950 panic!("expected extension request");
951 };
952 BlockingExtensionRequest {
953 namespace: TEST_EXTENSION_NAMESPACE.to_string(),
954 payload: envelope.payload.clone(),
955 extension: Arc::new(TestBlockingInterruptExtension),
956 }
957 }
958
959 struct TestBlockingInterruptExtension;
960
961 impl Extension for TestBlockingInterruptExtension {
962 fn namespace(&self) -> &str {
963 TEST_EXTENSION_NAMESPACE
964 }
965
966 fn handle_request<'a>(
967 &'a self,
968 _ctx: ExtensionContext<'a>,
969 _payload: Vec<u8>,
970 ) -> ExtensionFuture<'a, ExtensionResponse> {
971 Box::pin(async { Ok(ExtensionResponse::new(Vec::new())) })
972 }
973
974 fn is_blocking_request(&self, payload: &[u8]) -> bool {
975 parse_test_payload(payload).is_some_and(|(kind, _session_id)| kind == "prompt")
976 }
977
978 fn interrupt_blocking_request(
979 &self,
980 blocking_payload: &[u8],
981 interrupt: ExtensionInterruptRequest<'_>,
982 ) -> Option<ExtensionInterruptResponse> {
983 let (blocking_kind, blocking_session_id) = parse_test_payload(blocking_payload)?;
984 if blocking_kind != "prompt" {
985 return None;
986 }
987
988 let interrupted_response_payload =
989 encode_test_response("prompt-cancelled", blocking_session_id);
990 match interrupt {
991 ExtensionInterruptRequest::KillProcess => Some(ExtensionInterruptResponse {
992 interrupted_response_payload,
993 interrupting_response_payload: None,
994 }),
995 ExtensionInterruptRequest::ExtensionPayload(payload) => {
996 let (interrupt_kind, interrupt_session_id) = parse_test_payload(payload)?;
997 match interrupt_kind {
998 "close" if interrupt_session_id == blocking_session_id => {
999 Some(ExtensionInterruptResponse {
1000 interrupted_response_payload,
1001 interrupting_response_payload: None,
1002 })
1003 }
1004 "cancel" if interrupt_session_id == blocking_session_id => {
1005 Some(ExtensionInterruptResponse {
1006 interrupted_response_payload,
1007 interrupting_response_payload: Some(encode_test_response(
1008 "cancelled",
1009 interrupt_session_id,
1010 )),
1011 })
1012 }
1013 "prompt" | "close" | "cancel" => None,
1014 _ => None,
1015 }
1016 }
1017 }
1018 }
1019 }
1020
1021 fn parse_test_payload(payload: &[u8]) -> Option<(&str, &str)> {
1022 let payload = std::str::from_utf8(payload).ok()?;
1023 payload.split_once(':')
1024 }
1025
1026 fn encode_test_response(kind: &str, session_id: &str) -> Vec<u8> {
1027 format!("{kind}:{session_id}").into_bytes()
1028 }
1029}
1030
1031#[derive(Debug, Clone)]
1032pub(crate) struct LocalBridge {
1033 started_at: Instant,
1034 next_timer_id: usize,
1035 snapshots: BTreeMap<String, FilesystemSnapshot>,
1036}
1037
1038impl Default for LocalBridge {
1039 fn default() -> Self {
1040 Self {
1041 started_at: Instant::now(),
1042 next_timer_id: 0,
1043 snapshots: BTreeMap::new(),
1044 }
1045 }
1046}
1047
1048impl BridgeTypes for LocalBridge {
1049 type Error = LocalBridgeError;
1050}
1051
1052impl FilesystemBridge for LocalBridge {
1053 fn read_file(&mut self, request: ReadFileRequest) -> Result<Vec<u8>, Self::Error> {
1054 fs::read(Self::host_path(&request.path))
1055 .map_err(|error| LocalBridgeError::io("read", &request.path, error))
1056 }
1057
1058 fn write_file(&mut self, request: WriteFileRequest) -> Result<(), Self::Error> {
1059 let host_path = Self::host_path(&request.path);
1060 if let Some(parent) = host_path.parent() {
1061 fs::create_dir_all(parent)
1062 .map_err(|error| LocalBridgeError::io("mkdir", &request.path, error))?;
1063 }
1064 fs::write(host_path, request.contents)
1065 .map_err(|error| LocalBridgeError::io("write", &request.path, error))
1066 }
1067
1068 fn stat(&mut self, request: PathRequest) -> Result<FileMetadata, Self::Error> {
1069 fs::metadata(Self::host_path(&request.path))
1070 .map(Self::file_metadata)
1071 .map_err(|error| LocalBridgeError::io("stat", &request.path, error))
1072 }
1073
1074 fn lstat(&mut self, request: PathRequest) -> Result<FileMetadata, Self::Error> {
1075 fs::symlink_metadata(Self::host_path(&request.path))
1076 .map(Self::file_metadata)
1077 .map_err(|error| LocalBridgeError::io("lstat", &request.path, error))
1078 }
1079
1080 fn read_dir(&mut self, request: ReadDirRequest) -> Result<Vec<DirectoryEntry>, Self::Error> {
1081 let mut entries = fs::read_dir(Self::host_path(&request.path))
1082 .map_err(|error| LocalBridgeError::io("readdir", &request.path, error))?
1083 .map(|entry| {
1084 let entry =
1085 entry.map_err(|error| LocalBridgeError::io("readdir", &request.path, error))?;
1086 let kind = entry
1087 .file_type()
1088 .map(Self::file_kind)
1089 .map_err(|error| LocalBridgeError::io("readdir", &request.path, error))?;
1090 Ok(DirectoryEntry {
1091 name: entry.file_name().to_string_lossy().into_owned(),
1092 kind,
1093 })
1094 })
1095 .collect::<Result<Vec<_>, LocalBridgeError>>()?;
1096 entries.sort_by(|left, right| left.name.cmp(&right.name));
1097 Ok(entries)
1098 }
1099
1100 fn create_dir(&mut self, request: CreateDirRequest) -> Result<(), Self::Error> {
1101 let host_path = Self::host_path(&request.path);
1102 if request.recursive {
1103 fs::create_dir_all(host_path)
1104 } else {
1105 fs::create_dir(host_path)
1106 }
1107 .map_err(|error| LocalBridgeError::io("mkdir", &request.path, error))
1108 }
1109
1110 fn remove_file(&mut self, request: PathRequest) -> Result<(), Self::Error> {
1111 fs::remove_file(Self::host_path(&request.path))
1112 .map_err(|error| LocalBridgeError::io("unlink", &request.path, error))
1113 }
1114
1115 fn remove_dir(&mut self, request: PathRequest) -> Result<(), Self::Error> {
1116 fs::remove_dir(Self::host_path(&request.path))
1117 .map_err(|error| LocalBridgeError::io("rmdir", &request.path, error))
1118 }
1119
1120 fn rename(&mut self, request: RenameRequest) -> Result<(), Self::Error> {
1121 let from_path = Self::host_path(&request.from_path);
1122 let to_path = Self::host_path(&request.to_path);
1123 if let Some(parent) = to_path.parent() {
1124 fs::create_dir_all(parent)
1125 .map_err(|error| LocalBridgeError::io("mkdir", &request.to_path, error))?;
1126 }
1127 fs::rename(from_path, to_path).map_err(|error| {
1128 LocalBridgeError::unsupported(format!(
1129 "rename {} -> {}: {}",
1130 request.from_path, request.to_path, error
1131 ))
1132 })
1133 }
1134
1135 fn symlink(&mut self, request: SymlinkRequest) -> Result<(), Self::Error> {
1136 let link_path = Self::host_path(&request.link_path);
1137 if let Some(parent) = link_path.parent() {
1138 fs::create_dir_all(parent)
1139 .map_err(|error| LocalBridgeError::io("mkdir", &request.link_path, error))?;
1140 }
1141 create_symlink(&request.target_path, link_path)
1142 .map_err(|error| LocalBridgeError::io("symlink", &request.link_path, error))
1143 }
1144
1145 fn read_link(&mut self, request: PathRequest) -> Result<String, Self::Error> {
1146 fs::read_link(Self::host_path(&request.path))
1147 .map(|target| target.to_string_lossy().into_owned())
1148 .map_err(|error| LocalBridgeError::io("readlink", &request.path, error))
1149 }
1150
1151 fn chmod(&mut self, request: ChmodRequest) -> Result<(), Self::Error> {
1152 let permissions = fs::Permissions::from_mode(request.mode);
1153 fs::set_permissions(Self::host_path(&request.path), permissions)
1154 .map_err(|error| LocalBridgeError::io("chmod", &request.path, error))
1155 }
1156
1157 fn truncate(&mut self, request: TruncateRequest) -> Result<(), Self::Error> {
1158 OpenOptions::new()
1159 .write(true)
1160 .create(false)
1161 .open(Self::host_path(&request.path))
1162 .and_then(|file| file.set_len(request.len))
1163 .map_err(|error| LocalBridgeError::io("truncate", &request.path, error))
1164 }
1165
1166 fn exists(&mut self, request: PathRequest) -> Result<bool, Self::Error> {
1167 Ok(fs::symlink_metadata(Self::host_path(&request.path)).is_ok())
1168 }
1169}
1170
1171impl PermissionBridge for LocalBridge {
1172 fn check_filesystem_access(
1173 &mut self,
1174 request: FilesystemPermissionRequest,
1175 ) -> Result<PermissionDecision, Self::Error> {
1176 Ok(PermissionDecision::deny(format!(
1177 "no static filesystem policy registered for {}:{}",
1178 request.vm_id, request.path
1179 )))
1180 }
1181
1182 fn check_network_access(
1183 &mut self,
1184 request: NetworkPermissionRequest,
1185 ) -> Result<PermissionDecision, Self::Error> {
1186 Ok(PermissionDecision::deny(format!(
1187 "no static network policy registered for {}:{}",
1188 request.vm_id, request.resource
1189 )))
1190 }
1191
1192 fn check_command_execution(
1193 &mut self,
1194 request: CommandPermissionRequest,
1195 ) -> Result<PermissionDecision, Self::Error> {
1196 Ok(PermissionDecision::deny(format!(
1197 "no static child_process policy registered for {}:{}",
1198 request.vm_id, request.command
1199 )))
1200 }
1201
1202 fn check_environment_access(
1203 &mut self,
1204 request: EnvironmentPermissionRequest,
1205 ) -> Result<PermissionDecision, Self::Error> {
1206 Ok(PermissionDecision::deny(format!(
1207 "no static env policy registered for {}:{}",
1208 request.vm_id, request.key
1209 )))
1210 }
1211}
1212
1213impl PersistenceBridge for LocalBridge {
1214 fn load_filesystem_state(
1215 &mut self,
1216 request: LoadFilesystemStateRequest,
1217 ) -> Result<Option<FilesystemSnapshot>, Self::Error> {
1218 Ok(self.snapshots.get(&request.vm_id).cloned())
1219 }
1220
1221 fn flush_filesystem_state(
1222 &mut self,
1223 request: FlushFilesystemStateRequest,
1224 ) -> Result<(), Self::Error> {
1225 self.snapshots.insert(request.vm_id, request.snapshot);
1226 Ok(())
1227 }
1228}
1229
1230impl ClockBridge for LocalBridge {
1231 fn wall_clock(&mut self, _request: ClockRequest) -> Result<SystemTime, Self::Error> {
1232 Ok(SystemTime::now())
1233 }
1234
1235 fn monotonic_clock(&mut self, _request: ClockRequest) -> Result<Duration, Self::Error> {
1236 Ok(self.started_at.elapsed())
1237 }
1238
1239 fn schedule_timer(
1240 &mut self,
1241 request: ScheduleTimerRequest,
1242 ) -> Result<ScheduledTimer, Self::Error> {
1243 self.next_timer_id += 1;
1244 Ok(ScheduledTimer {
1245 timer_id: format!("timer-{}", self.next_timer_id),
1246 delay: request.delay,
1247 })
1248 }
1249}
1250
1251impl RandomBridge for LocalBridge {
1252 fn fill_random_bytes(&mut self, request: RandomBytesRequest) -> Result<Vec<u8>, Self::Error> {
1253 Ok(vec![0u8; request.len])
1254 }
1255}
1256
1257impl EventBridge for LocalBridge {
1258 fn emit_structured_event(&mut self, _event: StructuredEventRecord) -> Result<(), Self::Error> {
1259 Ok(())
1260 }
1261
1262 fn emit_diagnostic(&mut self, _event: DiagnosticRecord) -> Result<(), Self::Error> {
1263 Ok(())
1264 }
1265
1266 fn emit_log(&mut self, _event: LogRecord) -> Result<(), Self::Error> {
1267 Ok(())
1268 }
1269
1270 fn emit_lifecycle(&mut self, _event: LifecycleEventRecord) -> Result<(), Self::Error> {
1271 Ok(())
1272 }
1273}
1274
1275impl ExecutionBridge for LocalBridge {
1276 fn create_javascript_context(
1277 &mut self,
1278 _request: CreateJavascriptContextRequest,
1279 ) -> Result<GuestContextHandle, Self::Error> {
1280 Err(LocalBridgeError::unsupported(
1281 "execution bridge is handled internally by the native sidecar",
1282 ))
1283 }
1284
1285 fn create_wasm_context(
1286 &mut self,
1287 _request: CreateWasmContextRequest,
1288 ) -> Result<GuestContextHandle, Self::Error> {
1289 Err(LocalBridgeError::unsupported(
1290 "execution bridge is handled internally by the native sidecar",
1291 ))
1292 }
1293
1294 fn start_execution(
1295 &mut self,
1296 _request: StartExecutionRequest,
1297 ) -> Result<StartedExecution, Self::Error> {
1298 Err(LocalBridgeError::unsupported(
1299 "execution bridge is handled internally by the native sidecar",
1300 ))
1301 }
1302
1303 fn write_stdin(&mut self, _request: WriteExecutionStdinRequest) -> Result<(), Self::Error> {
1304 Err(LocalBridgeError::unsupported(
1305 "execution bridge is handled internally by the native sidecar",
1306 ))
1307 }
1308
1309 fn close_stdin(&mut self, _request: ExecutionHandleRequest) -> Result<(), Self::Error> {
1310 Err(LocalBridgeError::unsupported(
1311 "execution bridge is handled internally by the native sidecar",
1312 ))
1313 }
1314
1315 fn kill_execution(&mut self, _request: KillExecutionRequest) -> Result<(), Self::Error> {
1316 Err(LocalBridgeError::unsupported(
1317 "execution bridge is handled internally by the native sidecar",
1318 ))
1319 }
1320
1321 fn poll_execution_event(
1322 &mut self,
1323 _request: PollExecutionEventRequest,
1324 ) -> Result<Option<ExecutionEvent>, Self::Error> {
1325 Ok(None)
1326 }
1327}
1328
1329#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
1330struct SessionScope {
1331 connection_id: String,
1332 session_id: String,
1333}
1334
1335impl SessionScope {
1336 fn ownership_scope(&self) -> OwnershipScope {
1337 session_ownership(&self.connection_id, &self.session_id)
1338 }
1339
1340 fn compat_ownership_scope(&self) -> crate::protocol::OwnershipScope {
1341 wire::ownership_scope_to_compat(self.ownership_scope())
1342 }
1343}
1344
1345struct FrameEventTransport {
1351 writer: TrackedSyncSender<ProtocolFrame>,
1352}
1353
1354impl FrameEventTransport {
1355 fn new(writer: TrackedSyncSender<ProtocolFrame>) -> Self {
1356 Self { writer }
1357 }
1358}
1359
1360impl EventSinkTransport for FrameEventTransport {
1361 fn emit_event(&self, event: crate::wire::EventFrame) -> Result<(), SidecarError> {
1362 send_output_frame(&self.writer, ProtocolFrame::EventFrame(event))
1363 .map_err(|error| SidecarError::Bridge(error.to_string()))
1364 }
1365}
1366
1367struct FrameSidecarRequestTransport {
1368 writer: TrackedSyncSender<ProtocolFrame>,
1369 pending: Arc<Mutex<BTreeMap<RequestId, mpsc::SyncSender<SidecarResponseFrame>>>>,
1370}
1371
1372impl FrameSidecarRequestTransport {
1373 fn new(writer: TrackedSyncSender<ProtocolFrame>) -> Self {
1374 Self {
1375 writer,
1376 pending: Arc::new(Mutex::new(BTreeMap::new())),
1377 }
1378 }
1379
1380 fn accept_response(&self, response: SidecarResponseFrame) -> bool {
1381 let sender = {
1382 let mut pending = match self.pending.lock() {
1383 Ok(pending) => pending,
1384 Err(_) => return false,
1385 };
1386 pending.remove(&response.request_id)
1387 };
1388 let Some(sender) = sender else {
1389 return false;
1390 };
1391 let _ = sender.send(response);
1392 true
1393 }
1394}
1395
1396impl SidecarRequestTransport for FrameSidecarRequestTransport {
1397 fn send_request(
1398 &self,
1399 request: crate::protocol::SidecarRequestFrame,
1400 timeout: Duration,
1401 ) -> Result<crate::protocol::SidecarResponseFrame, SidecarError> {
1402 let request =
1403 wire::sidecar_request_frame_from_compat(request).map_err(wire_protocol_error)?;
1404 let (sender, receiver) = mpsc::sync_channel(1);
1405 self.pending
1406 .lock()
1407 .map_err(|_| {
1408 SidecarError::Bridge(String::from("sidecar callback waiter map lock poisoned"))
1409 })?
1410 .insert(request.request_id, sender);
1411 let write_deadline = Instant::now() + timeout;
1418 let mut frame = ProtocolFrame::SidecarRequestFrame(request.clone());
1419 let write_result = loop {
1420 match self.writer.try_send(frame) {
1421 Ok(()) => break Ok(()),
1422 Err(mpsc::TrySendError::Disconnected(_)) => {
1423 break Err(String::from("stdout writer disconnected"));
1424 }
1425 Err(mpsc::TrySendError::Full(returned)) => {
1426 if Instant::now() >= write_deadline {
1427 break Err(format!(
1428 "timed out writing sidecar request frame after {}s",
1429 timeout.as_secs()
1430 ));
1431 }
1432 frame = returned;
1433 thread::sleep(Duration::from_millis(1));
1434 }
1435 }
1436 };
1437 if let Err(message) = write_result {
1438 let _ = self
1439 .pending
1440 .lock()
1441 .map(|mut pending| pending.remove(&request.request_id));
1442 return Err(SidecarError::Io(format!(
1443 "failed to write sidecar request frame: {message}"
1444 )));
1445 }
1446 match receiver.recv_timeout(timeout) {
1447 Ok(response) => {
1448 wire::sidecar_response_frame_to_compat(response).map_err(wire_protocol_error)
1449 }
1450 Err(mpsc::RecvTimeoutError::Timeout) => {
1451 let _ = self
1452 .pending
1453 .lock()
1454 .map(|mut pending| pending.remove(&request.request_id));
1455 Err(SidecarError::Io(format!(
1456 "timed out waiting for sidecar response after {}s",
1457 timeout.as_secs()
1458 )))
1459 }
1460 Err(mpsc::RecvTimeoutError::Disconnected) => Err(SidecarError::Io(String::from(
1461 "sidecar response waiter disconnected",
1462 ))),
1463 }
1464 }
1465}
1466
1467#[derive(Debug, Clone, PartialEq, Eq)]
1468pub(crate) struct LocalBridgeError {
1469 message: String,
1470}
1471
1472impl LocalBridgeError {
1473 fn unsupported(message: impl Into<String>) -> Self {
1474 Self {
1475 message: message.into(),
1476 }
1477 }
1478
1479 fn io(operation: &str, path: &str, error: io::Error) -> Self {
1480 Self::unsupported(format!("{operation} {path}: {error}"))
1481 }
1482}
1483
1484impl fmt::Display for LocalBridgeError {
1485 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1486 f.write_str(&self.message)
1487 }
1488}
1489
1490impl Error for LocalBridgeError {}
1491
1492impl LocalBridge {
1493 fn host_path(path: &str) -> PathBuf {
1494 let candidate = Path::new(path);
1495 if candidate.is_absolute() {
1496 candidate.to_path_buf()
1497 } else {
1498 std::env::current_dir()
1499 .unwrap_or_else(|_| PathBuf::from("."))
1500 .join(candidate)
1501 }
1502 }
1503
1504 fn file_metadata(metadata: fs::Metadata) -> FileMetadata {
1505 FileMetadata {
1506 mode: metadata.permissions().mode(),
1507 size: metadata.size(),
1508 kind: Self::file_kind(metadata.file_type()),
1509 }
1510 }
1511
1512 fn file_kind(file_type: fs::FileType) -> agentos_bridge::FileKind {
1513 if file_type.is_file() {
1514 agentos_bridge::FileKind::File
1515 } else if file_type.is_dir() {
1516 agentos_bridge::FileKind::Directory
1517 } else if file_type.is_symlink() {
1518 agentos_bridge::FileKind::SymbolicLink
1519 } else {
1520 agentos_bridge::FileKind::Other
1521 }
1522 }
1523}