Watch
0
0
Fork
You've already forked hyperhive
0

subagent headers: leave out a model or effort a header cannot hold

The values come from the agent's start request. One with a line break made
async-nats panic inside the publish task, which would have stopped every
subagent's rows. They are now trimmed, and skipped when empty, over 64
characters, or not a valid header value.
This commit is contained in:
iris 2026-10-28 21:10:37 +01:00 • committed by mara
commit 0999cffdaf

View file

@ -84,19 +84,36 @@ pub struct RunInfo {
} }
impl RunInfo { impl RunInfo {
/// The headers for one row: only the values there are. /// The headers for one row: only the values there are, and only ones a
/// header can hold. The values come from the agent's start request, so one
/// with a line break (which `async_nats` panics on) or an absurd length is
/// left out rather than published.
fn headers(&self) -> async_nats::HeaderMap { fn headers(&self) -> async_nats::HeaderMap {
let mut headers = async_nats::HeaderMap::new(); let mut headers = async_nats::HeaderMap::new();
if let Some(model) = self.model.as_deref().filter(|v| !v.is_empty()) { if let Some(model) = header_value(self.model.as_deref()) {
headers.insert(subagent_term::HEADER_MODEL, model); headers.insert(subagent_term::HEADER_MODEL, model);
} }
if let Some(effort) = self.effort.as_deref().filter(|v| !v.is_empty()) { if let Some(effort) = header_value(self.effort.as_deref()) {
headers.insert(subagent_term::HEADER_EFFORT, effort); headers.insert(subagent_term::HEADER_EFFORT, effort);
} }
headers headers
} }
} }
/// The longest model or effort name published. Real ones are a few
/// characters; the value rides on every row and is shown in the UI.
const MAX_VALUE_LEN: usize = 64;
/// `value` trimmed, as a header value, or `None` when it is empty, too long, or
/// not one a header can carry.
fn header_value(value: Option<&str>) -> Option<async_nats::HeaderValue> {
let value = value?.trim();
if value.is_empty() || value.len() > MAX_VALUE_LEN {
return None;
}
value.parse().ok()
}
struct Line { struct Line {
subagent: String, subagent: String,
info: Arc<RunInfo>, info: Arc<RunInfo>,
@ -486,6 +503,30 @@ mod tests {
assert!(info.headers().is_empty()); assert!(info.headers().is_empty());
} }
/// The values are the caller's. A line break would panic `async_nats`, in
/// the task that publishes every subagent's rows.
#[test]
fn a_value_a_header_cannot_hold_is_left_out_and_never_panics() {
for bad in ["a\nb", "high\r\nX: y", &"x".repeat(MAX_VALUE_LEN + 1)] {
let info = RunInfo {
model: Some(bad.to_owned()),
effort: Some(bad.to_owned()),
};
assert!(info.headers().is_empty(), "{bad:?}");
}
let padded = RunInfo {
model: Some(" opus\n".to_owned()),
effort: None,
};
assert_eq!(
padded
.headers()
.get(subagent_term::HEADER_MODEL)
.map(ToString::to_string),
Some("opus".to_owned())
);
}
#[test] #[test]
fn every_kind_of_line_renders_as_stamped_rows() { fn every_kind_of_line_renders_as_stamped_rows() {
let mut ctx = ClassifyCtx::default(); let mut ctx = ClassifyCtx::default();