Files
kyleandClaude Opus 5.5 bb4d7c0919 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>
2026-09-22 21:48:24 -07:00

76 lines
2.2 KiB
Rust

//! Text from `brokerd` or the inference server is escaped like model text before it reaches the
//! terminal: a `retrying` error and every error detail (M3a review finding 10).
use bxctl::admin::AdminError;
use bxctl::chat::{ChatError, Printer};
use proto::{ErrorCode, TurnEvent, WireError};
const ESC: char = '\u{1b}';
fn hostile() -> String {
format!("the server responded with 503: {ESC}[2J{ESC}]0;owned\u{7}\u{202e}")
}
fn clean(text: &str) {
for c in text.chars() {
let cp = u32::from(c);
assert!(
cp >= 0x20 && !(0x7f..=0x9f).contains(&cp) && !(0x2028..=0x202e).contains(&cp)
|| c == '\n',
"raw {cp:#x} in {text:?}"
);
}
}
#[test]
fn a_retrying_error_is_escaped() {
let mut out = Vec::new();
let mut printer = Printer::new(true, false);
printer
.event(
&mut out,
&TurnEvent::Retrying {
attempt: 1,
after_ms: 2000,
error: hostile(),
},
)
.unwrap();
let text = String::from_utf8(out).unwrap();
clean(&text);
assert!(text.contains("\\u001b[2J"), "{text}");
}
#[test]
fn an_error_detail_is_escaped_in_chat_and_admin_errors() {
let wire = WireError {
code: ErrorCode::Inference,
detail: hostile(),
};
for text in [
ChatError::Refused(wire.clone()).to_string(),
AdminError::Refused(wire).to_string(),
] {
clean(&text);
assert!(text.contains("\\u202e"), "{text}");
}
}
/// A frame that does not decode is reported with serde's message, which quotes the offending
/// text after JSON decoding: from a compromised peer that can be raw escape sequences.
#[test]
fn a_frame_error_is_escaped() {
let bad = r#"{"v":1,"id":1,"final":true,"msg":{"kind":"\u001b[2J\u202e"}}"#;
let error = serde_json::from_str::<proto::Envelope>(bad).unwrap_err();
for text in [
ChatError::Frame(proto::FrameError::Json(
serde_json::from_str::<proto::Envelope>(bad).unwrap_err(),
))
.to_string(),
AdminError::Frame(proto::FrameError::Json(error)).to_string(),
] {
clean(&text);
assert!(text.contains("\\u001b"), "{text}");
}
}