bxctl: escape frame errors; say the outcome is unknown after a timeout

From the independent review of task 23. serde quotes a bad frame's text after
decoding, so a compromised peer could put escape sequences in it. A timed-out
admin request now says whether brokerd acted is unknown, since it may have.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-22 21:48:24 -07:00
co-authored by Claude Opus 5.5
parent e08deb39a6
commit bb4d7c0919
4 changed files with 45 additions and 4 deletions
+20 -2
View File
@@ -40,7 +40,17 @@ pub fn request_with_timeout(
msg,
};
write_frame(&mut stream, &env).map_err(AdminError::Frame)?;
let answer = read_frame(&mut stream).map_err(AdminError::Frame)?;
let answer = read_frame(&mut stream).map_err(|e| match e {
FrameError::Io(io)
if matches!(
io.kind(),
io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut
) =>
{
AdminError::NoAnswer(timeout)
}
other => AdminError::Frame(other),
})?;
if answer.id != 1 || !answer.r#final {
return Err(AdminError::Protocol(
"expected an answer for request 1".to_string(),
@@ -70,19 +80,27 @@ pub enum AdminError {
Protocol(String),
/// Writing the output failed; the caller stops rather than write again.
Io(std::io::Error),
/// The request was sent and no answer came in time, so what brokerd did is not known.
NoAnswer(std::time::Duration),
}
impl std::fmt::Display for AdminError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AdminError::Connect(p, e) => write!(f, "cannot reach brokerd at {}: {e}", p.display()),
AdminError::Frame(e) => write!(f, "{e}"),
// serde quotes the offending text of a bad frame, decoded: escape it.
AdminError::Frame(e) => write!(f, "{}", escape_json_text(&e.to_string())),
// 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}"),
AdminError::NoAnswer(t) => write!(
f,
"brokerd did not answer within {} s; whether it acted is unknown: check `bxctl approvals` and `bxctl audit verify`",
t.as_secs_f64()
),
}
}
}
+2 -1
View File
@@ -24,7 +24,8 @@ impl std::fmt::Display for ChatError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ChatError::Connect(e) => write!(f, "{e}"),
ChatError::Frame(e) => write!(f, "{e}"),
// serde quotes the offending text of a bad frame, decoded: escape it.
ChatError::Frame(e) => write!(f, "{}", escape_json_text(&e.to_string())),
// The detail may carry the inference server's body: escape it like model text.
ChatError::Refused(w) => {
write!(f, "{}: {}", code_name(w.code), escape_json_text(&w.detail))