brokerd: start pipe threads safely and collect output within a grace period

Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
2026-09-23 16:07:15 -07:00
parent c87aff0793
commit 93539dbead
4 changed files with 200 additions and 51 deletions
+86
View File
@@ -0,0 +1,86 @@
//! After a container ends, its output is collected within a grace period, never waited on for
//! ever: something outside the container that still holds a pipe must not hold `brokerd` (M3b
//! review finding 4). Against a fake `podman` whose shell leaves a background `sleep` holding the
//! pipes, which real Podman does not do. Do not edit.
#[path = "support/build.rs"]
mod build;
#[path = "support/fake_podman.rs"]
mod fake_podman;
use std::time::{Duration, Instant};
use brokerd::container::{OUTPUT_OPEN, Podman, TIMED_OUT};
use brokerd::pipes::GRACE;
use brokerd::policy::{Outcome, SessionState, decide};
use brokerd::runner::run;
use build::{grant, now, read, set};
use fake_podman::{Fake, Lines, serial};
use proto::{Mode, ToolResponse};
fn call(fake: &Fake, extra: &str, log: &Lines) -> (ToolResponse, Duration) {
let podman = Podman::new(fake.runner(extra), fake.dir.join("egress"), log.sink());
let grants = set(vec![grant("notes", "read_file", Mode::Auto).paths(&["/n"])]);
let decision = match decide(read("/n/a"), &grants, SessionState::default(), now()) {
Outcome::Allowed(d) => d,
other => panic!("not allowed: {other:?}"),
};
let started = Instant::now();
let got = run(decision, &podman);
(got, started.elapsed())
}
#[test]
fn the_grace_period_is_two_seconds() {
assert_eq!(GRACE, Duration::from_secs(2));
}
#[test]
fn output_held_open_after_the_container_ended_is_abandoned_after_the_grace_period() {
let _s = serial();
// The shell exits at once; the background sleep keeps standard output and error open.
let fake = Fake::new("grace-open", "cat > /dev/null; printf ok; sleep 6 & exit 0");
let log = Lines::default();
let (got, took) = call(&fake, "", &log);
assert_eq!(
got,
ToolResponse::Failed {
message: OUTPUT_OPEN.to_string()
}
);
assert!(took >= GRACE, "{took:?}");
assert!(took < GRACE + Duration::from_secs(2), "{took:?}");
assert!(log.all().contains("abandoned"), "{}", log.all());
}
#[test]
fn a_tool_past_its_limit_is_answered_within_the_grace_period_even_if_its_pipes_stay_open() {
let _s = serial();
// No `exec`: killing the shell leaves the sleep holding the pipes.
let fake = Fake::new("grace-slow", "cat > /dev/null; sleep 6");
let log = Lines::default();
let (got, took) = call(&fake, "read_file_ms = 300", &log);
assert_eq!(
got,
ToolResponse::Failed {
message: TIMED_OUT.to_string()
}
);
assert!(
took < Duration::from_millis(300) + GRACE + Duration::from_secs(2),
"{took:?}"
);
}
#[test]
fn a_tool_that_ends_normally_is_not_slowed_by_the_grace_period() {
let _s = serial();
let fake = Fake::new("grace-ok", "cat > /dev/null; printf done; exit 0");
let log = Lines::default();
let (got, took) = call(&fake, "", &log);
assert!(
matches!(&got, ToolResponse::Result { content, .. } if content == "done"),
"{got:?}"
);
assert!(took < Duration::from_secs(1), "{took:?}");
}