M3b plan: follow-up tasks 14 to 17 for the review's lower findings
14 moves the pipe handling out of container.rs (a pure move, replayed on its own); 15 starts threads with Builder and bounds output collection with a 2 s grace period; 16 escapes container errors in the log and fixes two texts; 17 fixes toolkit's thread start, casts and the egress-proxy form. Each checked against a reference, which is not kept. Tips T24 to T26 from this run. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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:?}");
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
//! What a tool or Podman writes on standard error reaches `brokerd`'s log escaped, one entry per
|
||||
//! event: it cannot start a line of its own or forge a runbook pointer (M3b review finding 5).
|
||||
//! Do not edit.
|
||||
|
||||
#[path = "support/build.rs"]
|
||||
mod build;
|
||||
#[path = "support/fake_podman.rs"]
|
||||
mod fake_podman;
|
||||
|
||||
use brokerd::container::{CANNOT_START, COULD_NOT_RUN, Podman, UNEXPECTED};
|
||||
use brokerd::policy::{Outcome, SessionState, decide};
|
||||
use brokerd::runner::run;
|
||||
use build::{fetch, grant, now, read, set};
|
||||
use fake_podman::{Fake, Lines, serial};
|
||||
use proto::{Mode, ToolRequest, ToolResponse};
|
||||
|
||||
const FORGED: &str = "real line\nbrokerd: forged\nsee docs/runbook.md#grants-invalid";
|
||||
|
||||
fn call(fake: &Fake, req: ToolRequest, grants: Vec<build::Build>, log: &Lines) -> ToolResponse {
|
||||
let podman = Podman::new(fake.runner(""), fake.dir.join("egress"), log.sink());
|
||||
let decision = match decide(req, &set(grants), SessionState::default(), now()) {
|
||||
Outcome::Allowed(d) => d,
|
||||
other => panic!("not allowed: {other:?}"),
|
||||
};
|
||||
run(decision, &podman)
|
||||
}
|
||||
|
||||
/// No entry holds the forged text as lines of its own; the one that carries it has it escaped.
|
||||
fn escaped(log: &Lines) {
|
||||
let entries = log.0.lock().unwrap().clone();
|
||||
for entry in &entries {
|
||||
assert!(!entry.contains("\nbrokerd: forged"), "raw: {entry:?}");
|
||||
assert!(
|
||||
!entry.contains("\nsee docs/runbook.md#grants-invalid"),
|
||||
"raw: {entry:?}"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
entries
|
||||
.iter()
|
||||
.any(|e| e.contains(r"real line\nbrokerd: forged")),
|
||||
"the error is still logged, escaped: {entries:?}"
|
||||
);
|
||||
}
|
||||
|
||||
fn notes() -> Vec<build::Build> {
|
||||
vec![grant("notes", "read_file", Mode::Auto).paths(&["/n"])]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tool_that_could_not_run() {
|
||||
let _s = serial();
|
||||
let fake = Fake::new(
|
||||
"log-2",
|
||||
&format!("cat > /dev/null; printf '{FORGED}' >&2; exit 2"),
|
||||
);
|
||||
let log = Lines::default();
|
||||
assert_eq!(
|
||||
call(&fake, read("/n/a"), notes(), &log),
|
||||
ToolResponse::Failed {
|
||||
message: COULD_NOT_RUN.to_string()
|
||||
}
|
||||
);
|
||||
escaped(&log);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_container_podman_could_not_start_keeps_its_one_real_pointer() {
|
||||
let _s = serial();
|
||||
let fake = Fake::new(
|
||||
"log-125",
|
||||
&format!("cat > /dev/null; printf '{FORGED}' >&2; exit 125"),
|
||||
);
|
||||
let log = Lines::default();
|
||||
assert_eq!(
|
||||
call(&fake, read("/n/a"), notes(), &log),
|
||||
ToolResponse::Failed {
|
||||
message: CANNOT_START.to_string()
|
||||
}
|
||||
);
|
||||
escaped(&log);
|
||||
let entries = log.0.lock().unwrap().clone();
|
||||
assert!(
|
||||
entries
|
||||
.iter()
|
||||
.any(|e| e.ends_with("\nsee docs/runbook.md#runner-unavailable")),
|
||||
"{entries:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unexpected_ending() {
|
||||
let _s = serial();
|
||||
let fake = Fake::new(
|
||||
"log-3",
|
||||
&format!("cat > /dev/null; printf '{FORGED}' >&2; exit 3"),
|
||||
);
|
||||
let log = Lines::default();
|
||||
assert_eq!(
|
||||
call(&fake, read("/n/a"), notes(), &log),
|
||||
ToolResponse::Failed {
|
||||
message: UNEXPECTED.to_string()
|
||||
}
|
||||
);
|
||||
escaped(&log);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_proxy_podman_could_not_start() {
|
||||
let _s = serial();
|
||||
let body =
|
||||
format!("if [ \"$2\" = -d ]; then printf '{FORGED}' >&2; exit 125; fi; cat > /dev/null");
|
||||
let fake = Fake::new("log-egress", &body);
|
||||
let log = Lines::default();
|
||||
let got = call(
|
||||
&fake,
|
||||
fetch("https://example.com/"),
|
||||
vec![grant("web", "http_fetch", Mode::Auto).hosts(&["example.com"])],
|
||||
&log,
|
||||
);
|
||||
assert_eq!(
|
||||
got,
|
||||
ToolResponse::Failed {
|
||||
message: CANNOT_START.to_string()
|
||||
}
|
||||
);
|
||||
escaped(&log);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
//! Two small texts from the M3b review: every line `brokerd serve` prints about its runtime starts
|
||||
//! with `brokerd:`, and a bad `[runner]` value is quoted in its error. Do not edit.
|
||||
|
||||
#[path = "support/fake_podman.rs"]
|
||||
mod fake_podman;
|
||||
|
||||
use std::io::Read;
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use brokerd::config::{Config, ConfigError};
|
||||
use fake_podman::{Fake, IMAGE, serial};
|
||||
|
||||
#[test]
|
||||
fn the_runtime_notice_starts_with_brokerd() {
|
||||
let _s = serial();
|
||||
let fake = Fake::new("notice", "exit 0");
|
||||
let home = fake.dir.join("home");
|
||||
std::fs::create_dir_all(home.join("grants")).unwrap();
|
||||
let config = fake.dir.join("brokerd.toml");
|
||||
std::fs::write(
|
||||
&config,
|
||||
format!(
|
||||
"[paths]\nhome = \"{0}\"\ngrants = \"{0}/grants\"\n[runner]\npodman = \"{1}\"\nimage = \"{IMAGE}\"\n",
|
||||
home.display(),
|
||||
fake.script.display()
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
let mut child = Command::new(env!("CARGO_BIN_EXE_brokerd"))
|
||||
.args(["serve", "--config"])
|
||||
.arg(&config)
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
let until = Instant::now() + Duration::from_secs(10);
|
||||
while UnixStream::connect(home.join("run/loop-broker/broker.sock")).is_err() {
|
||||
assert!(Instant::now() < until, "brokerd never listened");
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
child.kill().unwrap();
|
||||
child.wait().unwrap();
|
||||
let mut printed = String::new();
|
||||
child
|
||||
.stderr
|
||||
.take()
|
||||
.unwrap()
|
||||
.read_to_string(&mut printed)
|
||||
.unwrap();
|
||||
assert!(
|
||||
printed
|
||||
.lines()
|
||||
.any(|l| l == format!("brokerd: tools run in containers from {IMAGE}")),
|
||||
"{printed}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_bad_runner_value_is_quoted() {
|
||||
let dir = std::env::temp_dir().join(format!("bx-notice-cfg-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let cases = [
|
||||
("image = \"not by digest\"", "\"not by digest\""),
|
||||
(
|
||||
&*format!("image = \"{IMAGE}\"\nmemory = \"lots\""),
|
||||
"\"lots\"",
|
||||
),
|
||||
];
|
||||
for (n, (body, quoted)) in cases.iter().enumerate() {
|
||||
let path = dir.join(format!("q{n}.toml"));
|
||||
std::fs::write(&path, format!("[runner]\n{body}\n")).unwrap();
|
||||
match Config::load(&path) {
|
||||
Err(ConfigError::Invalid(_, why)) => assert!(why.contains(quoted), "{why}"),
|
||||
other => panic!("{body}: {other:?}"),
|
||||
}
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
Reference in New Issue
Block a user