bxctl: escape error details, time out on admin.sock, AdminError::Io
M3a review findings 9, 10, 12, 13. A retrying error and every error detail can carry the inference server's body, so they are escaped like model text. Admin requests wait at most 30 s, so a stuck brokerd cannot hang bxctl or a chat turn. A failed write is AdminError::Io and stops handle_pending instead of being answered with another write. The usage line says what audit verify checks. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,10 +12,27 @@ use proto::{
|
||||
use crate::chat::code_name;
|
||||
use crate::escape::escape_json_text;
|
||||
|
||||
// Ask brokerd for one message and read one answer.
|
||||
/// How long `bxctl` waits for `brokerd` to take or answer an admin request.
|
||||
pub const ADMIN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
|
||||
|
||||
// Ask brokerd for one message and read one answer, waiting at most `ADMIN_TIMEOUT`.
|
||||
pub fn request(socket: &Path, msg: Message) -> Result<Message, AdminError> {
|
||||
request_with_timeout(socket, msg, ADMIN_TIMEOUT)
|
||||
}
|
||||
|
||||
/// `request` with another limit on each read and write, so a `brokerd` that accepts and never
|
||||
/// answers cannot hang `bxctl`, or a `chat` turn waiting on an approval.
|
||||
pub fn request_with_timeout(
|
||||
socket: &Path,
|
||||
msg: Message,
|
||||
timeout: std::time::Duration,
|
||||
) -> Result<Message, AdminError> {
|
||||
let mut stream =
|
||||
UnixStream::connect(socket).map_err(|e| AdminError::Connect(socket.to_path_buf(), e))?;
|
||||
stream
|
||||
.set_read_timeout(Some(timeout))
|
||||
.and_then(|()| stream.set_write_timeout(Some(timeout)))
|
||||
.map_err(|e| AdminError::Connect(socket.to_path_buf(), e))?;
|
||||
let env = Envelope {
|
||||
v: PROTOCOL_VERSION,
|
||||
id: 1,
|
||||
@@ -51,6 +68,8 @@ pub enum AdminError {
|
||||
Frame(FrameError),
|
||||
Refused(WireError),
|
||||
Protocol(String),
|
||||
/// Writing the output failed; the caller stops rather than write again.
|
||||
Io(std::io::Error),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AdminError {
|
||||
@@ -58,16 +77,20 @@ impl std::fmt::Display for AdminError {
|
||||
match self {
|
||||
AdminError::Connect(p, e) => write!(f, "cannot reach brokerd at {}: {e}", p.display()),
|
||||
AdminError::Frame(e) => write!(f, "{e}"),
|
||||
AdminError::Refused(w) => write!(f, "{}: {}", code_name(w.code), w.detail),
|
||||
// The detail may carry text from the inference server or a tool: escape it.
|
||||
AdminError::Refused(w) => {
|
||||
write!(f, "{}: {}", code_name(w.code), escape_json_text(&w.detail))
|
||||
}
|
||||
AdminError::Protocol(s) => write!(f, "{s}"),
|
||||
AdminError::Io(e) => write!(f, "{e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// An error writing the output is reported like any other failure, with the message preserved.
|
||||
// An error writing the output is its own kind, so the caller can stop instead of writing again.
|
||||
impl From<std::io::Error> for AdminError {
|
||||
fn from(e: std::io::Error) -> Self {
|
||||
AdminError::Protocol(e.to_string())
|
||||
AdminError::Io(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user