agent mcp: bound the in-agent socket round trip so a stalled harness cannot hang a turn
This commit is contained in:
parent
220b51d0a3
commit
70d1b542e3
1 changed files with 58 additions and 10 deletions
|
|
@ -343,19 +343,38 @@ pub(super) async fn matrix_unread_summary() -> Option<Vec<MatrixRoomUnread>> {
|
|||
pub(super) async fn dial_agent_socket(
|
||||
req: &hive_agent_sock::Request,
|
||||
) -> Option<hive_agent_sock::Response> {
|
||||
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<hive_agent_sock::Response> {
|
||||
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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue