1use std::process::Stdio;
12use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering};
13use std::sync::{Arc, Weak};
14
15#[cfg(unix)]
16use command_fds::{CommandFdExt, FdMapping};
17#[cfg(unix)]
18use std::os::unix::net::UnixStream as StdUnixStream;
19
20use scc::HashMap as SccHashMap;
21use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
22use tokio::process::{Child, Command};
23use tokio::sync::{broadcast, mpsc, oneshot};
24
25use crate::wire::{self, WireFrameCodec};
26use crate::TransportError;
27
28const EVENT_CHANNEL_CAPACITY: usize = 4096;
30
31const REQUEST_FRAME_QUEUE_CAPACITY: usize = 4096;
33
34const CONTROL_FRAME_QUEUE_CAPACITY: usize = 1024;
36
37const PENDING_REQUEST_LIMIT: usize = 4096;
39
40const SIDECAR_BIN_ENV: &str = "AGENTOS_SIDECAR_BIN";
43
44#[cfg(unix)]
46const CONTROL_FD: std::os::fd::RawFd = 3;
47
48const SIDECAR_SILENCE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
54
55pub type WireSidecarCallback = Arc<
57 dyn Fn(
58 wire::SidecarRequestPayload,
59 wire::OwnershipScope,
60 ) -> futures::future::BoxFuture<
61 'static,
62 Result<wire::SidecarResponsePayload, TransportError>,
63 > + Send
64 + Sync,
65>;
66
67pub struct SidecarTransport {
70 child: parking_lot::Mutex<Option<Child>>,
72 pending: SccHashMap<wire::RequestId, oneshot::Sender<wire::ResponsePayload>>,
74 pending_request_lock: parking_lot::Mutex<()>,
75 request_counter: AtomicI64,
77 max_frame_bytes: AtomicUsize,
79 event_tx: broadcast::Sender<(wire::OwnershipScope, wire::EventPayload)>,
81 callbacks: SccHashMap<&'static str, WireSidecarCallback>,
83 request_writer_tx: mpsc::Sender<Vec<u8>>,
85 control_writer_tx: mpsc::Sender<Vec<u8>>,
87 last_inbound_at: parking_lot::Mutex<std::time::Instant>,
89}
90
91impl SidecarTransport {
92 pub async fn spawn(binary_path: Option<String>) -> Result<Arc<Self>, TransportError> {
97 #[cfg(not(unix))]
98 {
99 let _ = binary_path;
100 return Err(TransportError::Sidecar(
101 "the native sidecar response/control transport is unsupported on this platform"
102 .to_string(),
103 ));
104 }
105
106 #[cfg(unix)]
107 {
108 Self::spawn_unix(binary_path).await
109 }
110 }
111
112 #[cfg(unix)]
113 async fn spawn_unix(binary_path: Option<String>) -> Result<Arc<Self>, TransportError> {
114 let bin = resolve_sidecar_binary_path(binary_path);
115 let (control_parent, control_child) = StdUnixStream::pair().map_err(|error| {
116 TransportError::Sidecar(format!(
117 "failed to create sidecar control socketpair: {error}"
118 ))
119 })?;
120 control_parent.set_nonblocking(true).map_err(|error| {
121 TransportError::Sidecar(format!(
122 "failed to configure sidecar control socket: {error}"
123 ))
124 })?;
125 let mut command = Command::new(&bin);
126 command
127 .stdin(Stdio::piped())
128 .stdout(Stdio::piped())
129 .stderr(Stdio::inherit())
130 .kill_on_drop(true);
131 map_control_fd(&mut command, control_child)?;
135 let mut child = command.spawn().map_err(|error| {
136 TransportError::Sidecar(format!("failed to spawn sidecar '{bin}': {error}"))
137 })?;
138 drop(command);
139 let stdin = child
140 .stdin
141 .take()
142 .ok_or_else(|| TransportError::Sidecar("sidecar stdin was not piped".to_string()))?;
143 let stdout = child
144 .stdout
145 .take()
146 .ok_or_else(|| TransportError::Sidecar("sidecar stdout was not piped".to_string()))?;
147 let control = tokio::net::UnixStream::from_std(control_parent).map_err(|error| {
148 TransportError::Sidecar(format!("failed to adopt sidecar control socket: {error}"))
149 })?;
150 let (control_reader, control_writer) = control.into_split();
151
152 let (request_writer_tx, request_writer_rx) = mpsc::channel(REQUEST_FRAME_QUEUE_CAPACITY);
153 let (control_writer_tx, control_writer_rx) = mpsc::channel(CONTROL_FRAME_QUEUE_CAPACITY);
154 let (event_tx, _) = broadcast::channel(EVENT_CHANNEL_CAPACITY);
155
156 let transport = Arc::new(Self {
157 child: parking_lot::Mutex::new(Some(child)),
158 pending: SccHashMap::new(),
159 pending_request_lock: parking_lot::Mutex::new(()),
160 request_counter: AtomicI64::new(1),
161 max_frame_bytes: AtomicUsize::new(wire::DEFAULT_MAX_FRAME_BYTES),
162 event_tx,
163 callbacks: SccHashMap::new(),
164 request_writer_tx,
165 control_writer_tx,
166 last_inbound_at: parking_lot::Mutex::new(std::time::Instant::now()),
167 });
168
169 tokio::spawn(run_writer(
170 Arc::downgrade(&transport),
171 "ordinary request",
172 stdin,
173 request_writer_rx,
174 ));
175 tokio::spawn(run_writer(
176 Arc::downgrade(&transport),
177 "response/control",
178 control_writer,
179 control_writer_rx,
180 ));
181 tokio::spawn(run_reader(
182 Arc::downgrade(&transport),
183 stdout,
184 InboundLane::Event,
185 ));
186 tokio::spawn(run_reader(
187 Arc::downgrade(&transport),
188 control_reader,
189 InboundLane::Control,
190 ));
191 tokio::spawn(run_silence_watchdog(
192 Arc::downgrade(&transport),
193 SIDECAR_SILENCE_TIMEOUT,
194 ));
195
196 Ok(transport)
197 }
198
199 pub fn next_request_id(&self) -> wire::RequestId {
201 self.request_counter.fetch_add(1, Ordering::SeqCst)
202 }
203
204 pub async fn request_wire(
206 &self,
207 ownership: wire::OwnershipScope,
208 payload: wire::RequestPayload,
209 ) -> Result<wire::ResponsePayload, TransportError> {
210 self.request_wire_with_frame_limit(ownership, payload, None)
211 .await
212 }
213
214 pub async fn request_wire_bounded(
216 &self,
217 ownership: wire::OwnershipScope,
218 payload: wire::RequestPayload,
219 max_frame_bytes: usize,
220 ) -> Result<wire::ResponsePayload, TransportError> {
221 self.request_wire_with_frame_limit(ownership, payload, Some(max_frame_bytes))
222 .await
223 }
224
225 async fn request_wire_with_frame_limit(
226 &self,
227 ownership: wire::OwnershipScope,
228 payload: wire::RequestPayload,
229 max_frame_bytes: Option<usize>,
230 ) -> Result<wire::ResponsePayload, TransportError> {
231 let request_id = self.next_request_id();
232 let frame = wire::ProtocolFrame::RequestFrame(wire::RequestFrame {
233 schema: wire::protocol_schema(),
234 request_id,
235 ownership,
236 payload,
237 });
238 let bytes = self.encode_wire_frame(&frame, max_frame_bytes)?;
239
240 let (tx, rx) = oneshot::channel();
241 self.register_pending_request(request_id, tx)?;
242 let _pending_guard = PendingRequestGuard::new(self, request_id);
243
244 if self.request_writer_tx.send(bytes).await.is_err() {
245 self.pending.remove(&request_id);
246 return Err(TransportError::Sidecar(
247 "sidecar transport closed".to_string(),
248 ));
249 }
250
251 rx.await
252 .map_err(|_| TransportError::Sidecar("sidecar transport disconnected".to_string()))
253 }
254
255 pub fn subscribe_wire_events(
257 &self,
258 ) -> broadcast::Receiver<(wire::OwnershipScope, wire::EventPayload)> {
259 self.event_tx.subscribe()
260 }
261
262 pub fn register_wire_callback(&self, key: &'static str, callback: WireSidecarCallback) {
265 let _ = self.callbacks.insert(key, callback);
266 }
267
268 pub fn max_frame_bytes(&self) -> usize {
270 self.max_frame_bytes.load(Ordering::Relaxed)
271 }
272
273 pub fn set_max_frame_bytes(&self, max_frame_bytes: usize) {
275 self.max_frame_bytes
276 .store(max_frame_bytes, Ordering::SeqCst);
277 }
278
279 pub async fn shutdown(&self, reason: impl Into<String>) -> Result<(), TransportError> {
283 let frame = wire::ProtocolFrame::ControlFrame(wire::ControlFrame {
284 schema: wire::protocol_schema(),
285 payload: wire::ControlPayload::ShutdownControl(wire::ShutdownControl {
286 reason: reason.into(),
287 }),
288 });
289 let bytes = self.encode_wire_frame(&frame, None)?;
290 self.control_writer_tx.send(bytes).await.map_err(|_| {
291 TransportError::Sidecar("sidecar response/control transport closed".to_string())
292 })
293 }
294
295 pub fn kill_child(&self) {
297 if let Some(mut child) = self.child.lock().take() {
298 let _ = child.start_kill();
299 }
300 }
301
302 fn encode_wire_frame(
303 &self,
304 frame: &wire::ProtocolFrame,
305 max_frame_bytes: Option<usize>,
306 ) -> Result<Vec<u8>, TransportError> {
307 let transport_limit = self.max_frame_bytes.load(Ordering::Relaxed);
308 let max_frame_bytes = max_frame_bytes
309 .map(|limit| limit.min(transport_limit))
310 .unwrap_or(transport_limit);
311 let codec = WireFrameCodec::new(max_frame_bytes);
312 Ok(codec.encode(frame)?)
313 }
314
315 async fn handle_wire_frame(self: &Arc<Self>, frame: wire::ProtocolFrame) {
318 match frame {
319 wire::ProtocolFrame::ResponseFrame(response) => {
320 match self.pending.remove(&response.request_id) {
321 Some((_, tx)) => {
322 let _ = tx.send(response.payload);
323 }
324 None => {
325 tracing::warn!(
326 request_id = response.request_id,
327 "response for unknown request id"
328 )
329 }
330 }
331 }
332 wire::ProtocolFrame::EventFrame(event) => {
333 if matches!(
338 &event.payload,
339 wire::EventPayload::StructuredEvent(structured)
340 if structured.name == "heartbeat"
341 ) {
342 return;
343 }
344 let _ = self.event_tx.send((event.ownership, event.payload));
345 }
346 wire::ProtocolFrame::SidecarRequestFrame(request) => {
347 self.dispatch_sidecar_request(request).await
348 }
349 wire::ProtocolFrame::SidecarResponseFrame(_)
350 | wire::ProtocolFrame::RequestFrame(_)
351 | wire::ProtocolFrame::ControlFrame(_) => {
352 tracing::warn!("unexpected inbound frame on host transport")
353 }
354 }
355 }
356
357 async fn dispatch_sidecar_request(self: &Arc<Self>, frame: wire::SidecarRequestFrame) {
362 let key = sidecar_request_key(&frame.payload);
363 let callback = self.callbacks.read(&key, |_, value| value.clone());
364 match callback {
365 Some(callback) => {
366 let transport = Arc::downgrade(self);
367 tokio::spawn(async move {
368 match callback(frame.payload, frame.ownership.clone()).await {
369 Ok(payload) => {
370 let response = wire::ProtocolFrame::SidecarResponseFrame(
371 wire::SidecarResponseFrame {
372 schema: wire::protocol_schema(),
373 request_id: frame.request_id,
374 ownership: frame.ownership,
375 payload,
376 },
377 );
378 let Some(transport) = transport.upgrade() else {
380 return;
381 };
382 if let Ok(bytes) = transport.encode_wire_frame(&response, None) {
383 let _ = transport.control_writer_tx.send(bytes).await;
384 }
385 }
386 Err(error) => tracing::warn!(?error, key, "sidecar callback failed"),
387 }
388 });
389 }
390 None => tracing::warn!(key, "no callback registered for sidecar request"),
391 }
392 }
393
394 fn fail_all_pending(&self) {
396 self.pending.clear();
397 }
398
399 fn disconnect(&self) {
400 self.kill_child();
401 self.fail_all_pending();
402 }
403
404 fn register_pending_request(
405 &self,
406 request_id: wire::RequestId,
407 tx: oneshot::Sender<wire::ResponsePayload>,
408 ) -> Result<(), TransportError> {
409 let _guard = self.pending_request_lock.lock();
410 if pending_request_count(self) >= PENDING_REQUEST_LIMIT {
411 return Err(TransportError::Sidecar(format!(
412 "sidecar pending request limit exceeded: at most {PENDING_REQUEST_LIMIT} requests can be in flight"
413 )));
414 }
415 let _ = self.pending.insert(request_id, tx);
416 Ok(())
417 }
418}
419
420#[cfg(unix)]
421fn map_control_fd(
422 command: &mut Command,
423 control_child: StdUnixStream,
424) -> Result<(), TransportError> {
425 command
426 .fd_mappings(vec![FdMapping {
427 parent_fd: control_child.into(),
428 child_fd: CONTROL_FD,
429 }])
430 .map_err(|error| {
431 TransportError::Sidecar(format!(
432 "failed to map sidecar response/control fd: {error}"
433 ))
434 })?;
435 Ok(())
436}
437
438struct PendingRequestGuard<'a> {
439 transport: &'a SidecarTransport,
440 request_id: wire::RequestId,
441}
442
443impl<'a> PendingRequestGuard<'a> {
444 fn new(transport: &'a SidecarTransport, request_id: wire::RequestId) -> Self {
445 Self {
446 transport,
447 request_id,
448 }
449 }
450}
451
452impl Drop for PendingRequestGuard<'_> {
453 fn drop(&mut self) {
454 let _ = self.transport.pending.remove(&self.request_id);
455 }
456}
457
458fn pending_request_count(transport: &SidecarTransport) -> usize {
459 let mut count = 0;
460 transport.pending.scan(|_, _| {
461 count += 1;
462 });
463 count
464}
465
466fn sidecar_request_key(payload: &wire::SidecarRequestPayload) -> &'static str {
468 match payload {
469 wire::SidecarRequestPayload::HostCallbackRequest(_) => "host_callback",
470 wire::SidecarRequestPayload::JsBridgeCallRequest(_) => "js_bridge_call",
471 wire::SidecarRequestPayload::ExtEnvelope(_) => "ext",
472 }
473}
474
475async fn run_writer<W>(
477 transport: Weak<SidecarTransport>,
478 lane: &'static str,
479 mut writer: W,
480 mut frames: mpsc::Receiver<Vec<u8>>,
481) where
482 W: AsyncWrite + Unpin,
483{
484 while let Some(bytes) = frames.recv().await {
485 let result = async {
486 writer.write_all(&bytes).await?;
487 writer.flush().await
488 }
489 .await;
490 if let Err(error) = result {
491 tracing::warn!(?error, lane, "sidecar writer failed");
492 if let Some(transport) = transport.upgrade() {
493 transport.disconnect();
494 }
495 return;
496 }
497 }
498}
499
500#[derive(Clone, Copy, Debug)]
501enum InboundLane {
502 Event,
503 Control,
504}
505
506impl InboundLane {
507 fn accepts(self, frame: &wire::ProtocolFrame) -> bool {
508 match self {
509 Self::Event => {
510 matches!(frame, wire::ProtocolFrame::EventFrame(event) if !is_heartbeat(event))
511 }
512 Self::Control => match frame {
513 wire::ProtocolFrame::ResponseFrame(_)
514 | wire::ProtocolFrame::SidecarRequestFrame(_) => true,
515 wire::ProtocolFrame::EventFrame(event) => is_heartbeat(event),
516 wire::ProtocolFrame::RequestFrame(_)
517 | wire::ProtocolFrame::SidecarResponseFrame(_)
518 | wire::ProtocolFrame::ControlFrame(_) => false,
519 },
520 }
521 }
522}
523
524fn is_heartbeat(event: &wire::EventFrame) -> bool {
525 matches!(
526 &event.payload,
527 wire::EventPayload::StructuredEvent(structured) if structured.name == "heartbeat"
528 )
529}
530
531async fn run_reader<R>(transport: Weak<SidecarTransport>, mut reader: R, lane: InboundLane)
533where
534 R: AsyncRead + Unpin,
535{
536 loop {
537 let mut length_buf = [0u8; 4];
538 if let Err(error) = reader.read_exact(&mut length_buf).await {
539 if let Some(transport) = transport.upgrade() {
540 tracing::warn!(?error, ?lane, "sidecar reader ended");
541 transport.disconnect();
542 }
543 return;
544 }
545 let length = u32::from_be_bytes(length_buf) as usize;
546
547 let Some(transport) = transport.upgrade() else {
548 break;
549 };
550 let max_frame_bytes = transport.max_frame_bytes.load(Ordering::Relaxed);
551 if frame_length_exceeds_limit(length, max_frame_bytes) {
552 tracing::warn!(
553 size = length,
554 max = max_frame_bytes,
555 "sidecar frame exceeds negotiated limit"
556 );
557 transport.disconnect();
558 return;
559 }
560
561 let mut frame_bytes = vec![0u8; 4 + length];
562 frame_bytes[..4].copy_from_slice(&length_buf);
563 if let Err(error) = reader.read_exact(&mut frame_bytes[4..]).await {
564 tracing::warn!(?error, ?lane, "sidecar reader ended mid-frame");
565 transport.disconnect();
566 return;
567 }
568 *transport.last_inbound_at.lock() = std::time::Instant::now();
571
572 let codec = WireFrameCodec::new(max_frame_bytes);
573 match codec.decode(&frame_bytes) {
574 Ok(frame) if lane.accepts(&frame) => transport.handle_wire_frame(frame).await,
575 Ok(frame) => {
576 tracing::warn!(?lane, frame = ?frame, "sidecar frame arrived on wrong transport lane");
577 transport.disconnect();
578 return;
579 }
580 Err(error) => {
581 tracing::warn!(?error, ?lane, "failed to decode sidecar frame");
582 transport.disconnect();
583 return;
584 }
585 }
586 }
587}
588
589fn frame_length_exceeds_limit(length: usize, max_frame_bytes: usize) -> bool {
590 length > max_frame_bytes
591}
592
593async fn run_silence_watchdog(transport: Weak<SidecarTransport>, timeout: std::time::Duration) {
597 let check_interval = (timeout / 4).min(std::time::Duration::from_secs(1));
598 loop {
599 tokio::time::sleep(check_interval).await;
600 let Some(transport) = transport.upgrade() else {
601 return;
602 };
603 let silence = transport.last_inbound_at.lock().elapsed();
604 if silence < timeout {
605 continue;
606 }
607 tracing::error!(
608 silence_ms = silence.as_millis() as u64,
609 "sidecar unresponsive: no protocol frames or heartbeats; killing sidecar",
610 );
611 transport.kill_child();
612 transport.fail_all_pending();
613 return;
614 }
615}
616
617fn resolve_sidecar_binary_path(binary_path: Option<String>) -> String {
618 binary_path
619 .or_else(|| std::env::var(SIDECAR_BIN_ENV).ok())
620 .unwrap_or_else(|| "agentos-native-sidecar".to_string())
621}
622
623#[cfg(test)]
624mod tests {
625 use super::*;
626 use std::sync::Mutex;
627
628 static ENV_LOCK: Mutex<()> = Mutex::new(());
629
630 fn test_transport() -> SidecarTransport {
631 let (request_writer_tx, _request_writer_rx) = mpsc::channel(REQUEST_FRAME_QUEUE_CAPACITY);
632 let (control_writer_tx, _control_writer_rx) = mpsc::channel(CONTROL_FRAME_QUEUE_CAPACITY);
633 let (event_tx, _) = broadcast::channel(EVENT_CHANNEL_CAPACITY);
634 SidecarTransport {
635 child: parking_lot::Mutex::new(None),
636 pending: SccHashMap::new(),
637 pending_request_lock: parking_lot::Mutex::new(()),
638 request_counter: AtomicI64::new(1),
639 max_frame_bytes: AtomicUsize::new(wire::DEFAULT_MAX_FRAME_BYTES),
640 event_tx,
641 callbacks: SccHashMap::new(),
642 request_writer_tx,
643 control_writer_tx,
644 last_inbound_at: parking_lot::Mutex::new(std::time::Instant::now()),
645 }
646 }
647
648 #[test]
649 fn binary_path_prefers_explicit_path_over_env() {
650 let _guard = ENV_LOCK.lock().expect("env lock");
651 let previous = std::env::var(SIDECAR_BIN_ENV).ok();
652 std::env::set_var(SIDECAR_BIN_ENV, "/tmp/from-env");
653
654 assert_eq!(
655 resolve_sidecar_binary_path(Some("/tmp/from-config".to_string())),
656 "/tmp/from-config"
657 );
658
659 restore_env(SIDECAR_BIN_ENV, previous);
660 }
661
662 #[test]
663 fn binary_path_uses_agentos_env_fallback() {
664 let _guard = ENV_LOCK.lock().expect("env lock");
665 let previous = std::env::var(SIDECAR_BIN_ENV).ok();
666 std::env::set_var(SIDECAR_BIN_ENV, "/tmp/agentos-native-sidecar");
667
668 assert_eq!(
669 resolve_sidecar_binary_path(None),
670 "/tmp/agentos-native-sidecar"
671 );
672
673 restore_env(SIDECAR_BIN_ENV, previous);
674 }
675
676 #[test]
677 fn binary_path_defaults_to_agentos_native_sidecar() {
678 let _guard = ENV_LOCK.lock().expect("env lock");
679 let previous = std::env::var(SIDECAR_BIN_ENV).ok();
680 std::env::remove_var(SIDECAR_BIN_ENV);
681
682 assert_eq!(resolve_sidecar_binary_path(None), "agentos-native-sidecar");
683
684 restore_env(SIDECAR_BIN_ENV, previous);
685 }
686
687 fn restore_env(key: &str, value: Option<String>) {
688 match value {
689 Some(value) => std::env::set_var(key, value),
690 None => std::env::remove_var(key),
691 }
692 }
693
694 #[test]
695 fn frame_length_limit_rejects_oversized_declared_length() {
696 assert!(!frame_length_exceeds_limit(1024, 1024));
697 assert!(frame_length_exceeds_limit(1025, 1024));
698 }
699
700 #[test]
701 fn transport_encodes_requests_with_generated_wire_codec() {
702 let transport = test_transport();
703 let frame = wire::ProtocolFrame::RequestFrame(wire::RequestFrame {
704 schema: wire::protocol_schema(),
705 request_id: 7,
706 ownership: wire::OwnershipScope::ConnectionOwnership(wire::ConnectionOwnership {
707 connection_id: "conn-1".to_string(),
708 }),
709 payload: wire::RequestPayload::AuthenticateRequest(wire::AuthenticateRequest {
710 client_name: "transport-test".to_string(),
711 auth_token: "token".to_string(),
712 protocol_version: wire::PROTOCOL_VERSION,
713 bridge_version: 1,
714 }),
715 });
716
717 let encoded = transport
718 .encode_wire_frame(&frame, None)
719 .expect("encode transport frame");
720 let decoded = WireFrameCodec::default()
721 .decode(&encoded)
722 .expect("decode generated wire frame");
723
724 assert!(matches!(
725 decoded,
726 wire::ProtocolFrame::RequestFrame(wire::RequestFrame {
727 payload: wire::RequestPayload::AuthenticateRequest(_),
728 ..
729 })
730 ));
731 }
732
733 #[tokio::test]
734 async fn transport_fans_out_generated_wire_events() {
735 let transport = Arc::new(test_transport());
736 let mut wire_events = transport.subscribe_wire_events();
737
738 transport
739 .handle_wire_frame(wire::ProtocolFrame::EventFrame(wire::EventFrame {
740 schema: wire::protocol_schema(),
741 ownership: wire::OwnershipScope::VmOwnership(wire::VmOwnership {
742 connection_id: "conn-1".to_string(),
743 session_id: "session-1".to_string(),
744 vm_id: "vm-1".to_string(),
745 }),
746 payload: wire::EventPayload::ProcessOutputEvent(wire::ProcessOutputEvent {
747 process_id: "proc-1".to_string(),
748 channel: wire::StreamChannel::Stdout,
749 chunk: b"hello".to_vec(),
750 }),
751 }))
752 .await;
753
754 let (ownership, payload) = wire_events.recv().await.expect("wire event");
755 assert!(matches!(
756 ownership,
757 wire::OwnershipScope::VmOwnership(wire::VmOwnership {
758 connection_id,
759 session_id,
760 vm_id,
761 }) if connection_id == "conn-1" && session_id == "session-1" && vm_id == "vm-1"
762 ));
763 assert!(matches!(
764 payload,
765 wire::EventPayload::ProcessOutputEvent(wire::ProcessOutputEvent {
766 process_id,
767 channel: wire::StreamChannel::Stdout,
768 chunk,
769 }) if process_id == "proc-1" && chunk == b"hello".to_vec()
770 ));
771 }
772
773 #[tokio::test]
774 async fn event_before_response_is_delivered_without_transport_history() {
775 let transport = Arc::new(test_transport());
776 let mut events = transport.subscribe_wire_events();
777 let (response_tx, response_rx) = oneshot::channel();
778 transport
779 .register_pending_request(7, response_tx)
780 .expect("register pending request");
781 let ownership = wire::OwnershipScope::ConnectionOwnership(wire::ConnectionOwnership {
782 connection_id: "conn-1".to_string(),
783 });
784
785 transport
786 .handle_wire_frame(wire::ProtocolFrame::EventFrame(wire::EventFrame {
787 schema: wire::protocol_schema(),
788 ownership: ownership.clone(),
789 payload: wire::EventPayload::StructuredEvent(wire::StructuredEvent {
790 name: "process_started_before_reply".to_string(),
791 detail: std::collections::HashMap::new(),
792 }),
793 }))
794 .await;
795 transport
796 .handle_wire_frame(wire::ProtocolFrame::ResponseFrame(wire::ResponseFrame {
797 schema: wire::protocol_schema(),
798 request_id: 7,
799 ownership,
800 payload: wire::ResponsePayload::ExtEnvelope(wire::ExtEnvelope {
801 namespace: "test".to_string(),
802 payload: Vec::new(),
803 }),
804 }))
805 .await;
806
807 let (_, event) = events.recv().await.expect("live event");
808 assert!(matches!(
809 event,
810 wire::EventPayload::StructuredEvent(wire::StructuredEvent { name, .. })
811 if name == "process_started_before_reply"
812 ));
813 assert!(matches!(
814 response_rx.await.expect("response"),
815 wire::ResponsePayload::ExtEnvelope(_)
816 ));
817 }
818
819 #[tokio::test]
820 async fn bounded_event_fanout_reports_lag_instead_of_retaining_history() {
821 let (event_tx, mut events) = broadcast::channel(2);
822 for index in 0..3 {
823 event_tx
824 .send((
825 wire::OwnershipScope::ConnectionOwnership(wire::ConnectionOwnership {
826 connection_id: "conn-1".to_string(),
827 }),
828 wire::EventPayload::StructuredEvent(wire::StructuredEvent {
829 name: format!("event-{index}"),
830 detail: std::collections::HashMap::new(),
831 }),
832 ))
833 .expect("active receiver");
834 }
835
836 assert!(matches!(
837 events.recv().await,
838 Err(broadcast::error::RecvError::Lagged(1))
839 ));
840 }
841
842 #[tokio::test]
843 async fn silence_watchdog_fails_pending_requests_after_sustained_silence() {
844 let transport = Arc::new(test_transport());
845 let (tx, rx) = oneshot::channel();
846 transport
847 .register_pending_request(1, tx)
848 .expect("register pending request");
849
850 tokio::spawn(run_silence_watchdog(
851 Arc::downgrade(&transport),
852 std::time::Duration::from_millis(40),
853 ));
854
855 rx.await
858 .expect_err("watchdog should drop the pending sender");
859 assert_eq!(pending_request_count(&transport), 0);
860 }
861
862 #[tokio::test]
863 async fn silence_watchdog_stays_quiet_while_frames_arrive() {
864 let transport = Arc::new(test_transport());
865 let (tx, mut rx) = oneshot::channel();
866 transport
867 .register_pending_request(1, tx)
868 .expect("register pending request");
869
870 tokio::spawn(run_silence_watchdog(
871 Arc::downgrade(&transport),
872 std::time::Duration::from_millis(120),
873 ));
874
875 for _ in 0..6 {
878 tokio::time::sleep(std::time::Duration::from_millis(40)).await;
879 *transport.last_inbound_at.lock() = std::time::Instant::now();
880 assert!(
881 rx.try_recv().is_err(),
882 "pending request must remain registered while frames arrive"
883 );
884 }
885 assert_eq!(pending_request_count(&transport), 1);
886 }
887
888 #[tokio::test]
889 async fn heartbeat_events_are_swallowed_before_the_event_fanout() {
890 let transport = Arc::new(test_transport());
891 let mut wire_events = transport.subscribe_wire_events();
892
893 transport
894 .handle_wire_frame(wire::ProtocolFrame::EventFrame(wire::EventFrame {
895 schema: wire::protocol_schema(),
896 ownership: wire::OwnershipScope::ConnectionOwnership(wire::ConnectionOwnership {
897 connection_id: "sidecar-transport".to_string(),
898 }),
899 payload: wire::EventPayload::StructuredEvent(wire::StructuredEvent {
900 name: "heartbeat".to_string(),
901 detail: std::collections::HashMap::new(),
902 }),
903 }))
904 .await;
905 transport
908 .handle_wire_frame(wire::ProtocolFrame::EventFrame(wire::EventFrame {
909 schema: wire::protocol_schema(),
910 ownership: wire::OwnershipScope::ConnectionOwnership(wire::ConnectionOwnership {
911 connection_id: "conn-1".to_string(),
912 }),
913 payload: wire::EventPayload::StructuredEvent(wire::StructuredEvent {
914 name: "limit_warning".to_string(),
915 detail: std::collections::HashMap::new(),
916 }),
917 }))
918 .await;
919
920 let (_, payload) = wire_events.recv().await.expect("structured event");
921 assert!(matches!(
922 payload,
923 wire::EventPayload::StructuredEvent(wire::StructuredEvent { name, .. })
924 if name == "limit_warning"
925 ));
926 assert!(
927 wire_events.try_recv().is_err(),
928 "heartbeat must not fan out"
929 );
930 }
931
932 #[test]
933 fn pending_request_guard_removes_registered_slot_on_drop() {
934 let transport = test_transport();
935 let (tx, _rx) = oneshot::channel();
936 transport
937 .register_pending_request(1, tx)
938 .expect("register pending request");
939
940 {
941 let _guard = PendingRequestGuard::new(&transport, 1);
942 assert_eq!(pending_request_count(&transport), 1);
943 }
944
945 assert_eq!(pending_request_count(&transport), 0);
946 }
947
948 #[test]
949 fn pending_request_limit_rejects_full_transport() {
950 let transport = test_transport();
951 for request_id in 1..=PENDING_REQUEST_LIMIT as wire::RequestId {
952 let (tx, _rx) = oneshot::channel();
953 transport
954 .register_pending_request(request_id, tx)
955 .expect("register pending request");
956 }
957 let (tx, _rx) = oneshot::channel();
958 let error = transport
959 .register_pending_request((PENDING_REQUEST_LIMIT + 1) as wire::RequestId, tx)
960 .expect_err("full pending map should reject");
961
962 assert!(
963 error
964 .to_string()
965 .contains("sidecar pending request limit exceeded"),
966 "unexpected error: {error}"
967 );
968 }
969
970 #[tokio::test]
971 async fn shutdown_uses_typed_control_frame() {
972 let (request_writer_tx, _request_writer_rx) = mpsc::channel(4);
973 let (control_writer_tx, mut control_writer_rx) = mpsc::channel(4);
974 let (event_tx, _) = broadcast::channel(4);
975 let transport = SidecarTransport {
976 child: parking_lot::Mutex::new(None),
977 pending: SccHashMap::new(),
978 pending_request_lock: parking_lot::Mutex::new(()),
979 request_counter: AtomicI64::new(1),
980 max_frame_bytes: AtomicUsize::new(wire::DEFAULT_MAX_FRAME_BYTES),
981 event_tx,
982 callbacks: SccHashMap::new(),
983 request_writer_tx,
984 control_writer_tx,
985 last_inbound_at: parking_lot::Mutex::new(std::time::Instant::now()),
986 };
987
988 transport
989 .shutdown("test complete")
990 .await
991 .expect("enqueue typed shutdown");
992 let bytes = control_writer_rx.recv().await.expect("control frame bytes");
993 let frame = WireFrameCodec::default()
994 .decode(&bytes)
995 .expect("decode shutdown frame");
996 assert!(matches!(
997 frame,
998 wire::ProtocolFrame::ControlFrame(wire::ControlFrame {
999 payload: wire::ControlPayload::ShutdownControl(wire::ShutdownControl { reason }),
1000 ..
1001 }) if reason == "test complete"
1002 ));
1003 }
1004
1005 #[cfg(unix)]
1006 #[tokio::test]
1007 async fn unix_spawn_maps_duplex_control_socket_to_fd_three() {
1008 let (mut parent, child) = StdUnixStream::pair().expect("control socketpair");
1009 parent
1010 .set_read_timeout(Some(std::time::Duration::from_secs(2)))
1011 .expect("control read timeout");
1012 let mut command = Command::new("sh");
1013 command.arg("-c").arg("printf mapped-control >&3");
1014 map_control_fd(&mut command, child).expect("map child control fd");
1015 let mut child = command.spawn().expect("spawn fd mapping probe");
1016 drop(command);
1017
1018 let mut received = [0_u8; 14];
1019 std::io::Read::read_exact(&mut parent, &mut received).expect("read mapped fd output");
1020 assert_eq!(&received, b"mapped-control");
1021 assert!(child
1022 .wait()
1023 .await
1024 .expect("wait for mapping probe")
1025 .success());
1026 }
1027
1028 #[tokio::test]
1029 async fn control_writer_progresses_while_ordinary_stream_is_blocked() {
1030 let transport = Arc::new(test_transport());
1031 let (ordinary_client, _ordinary_server) = tokio::io::duplex(1);
1032 let (control_client, mut control_server) = tokio::io::duplex(64);
1033 let (control_tx, control_rx) = mpsc::channel(CONTROL_FRAME_QUEUE_CAPACITY);
1034 let (request_tx, request_rx) = mpsc::channel(REQUEST_FRAME_QUEUE_CAPACITY);
1035 request_tx
1036 .send(vec![b'r'; 64])
1037 .await
1038 .expect("send request frame");
1039 control_tx
1040 .send(vec![b'c'])
1041 .await
1042 .expect("send control frame");
1043 let ordinary_writer = tokio::spawn(run_writer(
1044 Arc::downgrade(&transport),
1045 "ordinary",
1046 ordinary_client,
1047 request_rx,
1048 ));
1049 let control_writer = tokio::spawn(run_writer(
1050 Arc::downgrade(&transport),
1051 "control",
1052 control_client,
1053 control_rx,
1054 ));
1055 let mut first = [0u8; 1];
1056 tokio::time::timeout(
1057 std::time::Duration::from_secs(1),
1058 control_server.read_exact(&mut first),
1059 )
1060 .await
1061 .expect("control write must not wait for ordinary stream")
1062 .expect("read first byte");
1063 assert_eq!(first, [b'c']);
1064 ordinary_writer.abort();
1065 control_writer.abort();
1066 }
1067
1068 #[tokio::test]
1069 async fn eof_on_either_inbound_lane_fails_pending_requests() {
1070 for lane in [InboundLane::Event, InboundLane::Control] {
1071 let transport = Arc::new(test_transport());
1072 let (tx, rx) = oneshot::channel();
1073 transport
1074 .register_pending_request(1, tx)
1075 .expect("register pending request");
1076 let (reader, peer) = tokio::io::duplex(64);
1077 drop(peer);
1078
1079 tokio::time::timeout(
1080 std::time::Duration::from_secs(1),
1081 run_reader(Arc::downgrade(&transport), reader, lane),
1082 )
1083 .await
1084 .expect("reader must terminate on EOF");
1085
1086 rx.await.expect_err("EOF must fail pending requests");
1087 assert_eq!(pending_request_count(&transport), 0);
1088 }
1089 }
1090
1091 #[tokio::test]
1092 async fn writer_preserves_order_within_one_lane() {
1093 let transport = Arc::new(test_transport());
1094 let (client, mut server) = tokio::io::duplex(64);
1095 let (tx, rx) = mpsc::channel(CONTROL_FRAME_QUEUE_CAPACITY);
1096 tx.send(vec![b'c']).await.expect("control one");
1097 tx.send(vec![b'C']).await.expect("control two");
1098 drop(tx);
1099
1100 let writer = tokio::spawn(run_writer(
1101 Arc::downgrade(&transport),
1102 "control",
1103 client,
1104 rx,
1105 ));
1106 let mut output = [0u8; 2];
1107 server.read_exact(&mut output).await.expect("read output");
1108 writer.await.expect("writer task");
1109
1110 assert_eq!(output, [b'c', b'C']);
1111 }
1112}