From 70d1b542e37165bd38ba41150f4bf5bd0c6b0d5d Mon Sep 17 00:00:00 2001 From: damocles Date: Wed, 21 Oct 2026 21:50:57 +0200 Subject: [PATCH] agent mcp: bound the in-agent socket round trip so a stalled harness cannot hang a turn --- hive-agent-mcp/src/mcp/render.rs | 68 +++++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/hive-agent-mcp/src/mcp/render.rs b/hive-agent-mcp/src/mcp/render.rs index 08186002..275e65d8 100644 --- a/hive-agent-mcp/src/mcp/render.rs +++ b/hive-agent-mcp/src/mcp/render.rs @@ -343,19 +343,38 @@ pub(super) async fn matrix_unread_summary() -> Option> { pub(super) async fn dial_agent_socket( req: &hive_agent_sock::Request, ) -> Option { - use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; - use tokio::net::UnixStream; let socket = std::env::var_os("HIVE_AGENT_SOCKET").map(std::path::PathBuf::from)?; if !socket.exists() { return None; } - let mut stream = UnixStream::connect(&socket).await.ok()?; - let mut line = serde_json::to_string(req).ok()?; - line.push('\n'); - stream.write_all(line.as_bytes()).await.ok()?; - let mut lines = BufReader::new(stream).lines(); - let resp_line = lines.next_line().await.ok()??; - serde_json::from_str(&resp_line).ok() + dial_socket(&socket, req, AGENT_SOCKET_DEADLINE).await +} + +/// Longest a tool call waits on the in-agent socket, connect through reply. +/// Matches the server's own per-connection request-line deadline: a harness +/// that accepts and then stalls must not hang the model's turn. +const AGENT_SOCKET_DEADLINE: std::time::Duration = std::time::Duration::from_secs(30); + +/// One round trip on the socket at `socket`, abandoned (as `None`) after +/// `deadline`. +async fn dial_socket( + socket: &std::path::Path, + req: &hive_agent_sock::Request, + deadline: std::time::Duration, +) -> Option { + use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; + use tokio::net::UnixStream; + tokio::time::timeout(deadline, async { + let mut stream = UnixStream::connect(socket).await.ok()?; + let mut line = serde_json::to_string(req).ok()?; + line.push('\n'); + stream.write_all(line.as_bytes()).await.ok()?; + let mut lines = BufReader::new(stream).lines(); + let resp_line = lines.next_line().await.ok()??; + serde_json::from_str(&resp_line).ok() + }) + .await + .ok()? } /// Query the harness's in-agent socket for this agent's local todos @@ -540,7 +559,36 @@ pub fn annotate_retries(mut s: String, retries: u32) -> String { #[cfg(test)] mod tests { - use super::{format_ack, format_recv}; + use super::{dial_socket, format_ack, format_recv}; + + #[tokio::test] + async fn a_socket_that_accepts_and_never_answers_is_given_up_on() { + let path = std::env::temp_dir().join(format!( + "hive-agent-mcp-dial-test-{}-{}.sock", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + let listener = tokio::net::UnixListener::bind(&path).unwrap(); + // Accept, then hold the connection open without ever replying. + let _stalled = tokio::spawn(async move { + let conn = listener.accept().await; + std::future::pending::<()>().await; + drop(conn); + }); + + let resp = dial_socket( + &path, + &hive_agent_sock::Request::ListReminders, + std::time::Duration::from_millis(100), + ) + .await; + let _ = std::fs::remove_file(&path); + + assert!(resp.is_none()); + } fn msg(id: i64, from: &str, body: &str) -> hive_sh4re::inbox::DeliveredMessage { hive_sh4re::inbox::DeliveredMessage {