Files
boxmaker/crates/brokerd/src/main.rs
T
kyle e69ba632e6 brokerd: escape container errors in the log; prefix and quote two messages
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
2026-09-23 16:10:54 -07:00

150 lines
5.0 KiB
Rust

//! `brokerd`: the broker role. `serve` takes the audit lock, binds both sockets, and serves.
use std::ffi::OsString;
use std::path::PathBuf;
use std::process::ExitCode;
use std::sync::Arc;
use brokerd::audit::{AuditError, RECOVERED_NOTICE};
use brokerd::config::Config;
use brokerd::container::Podman;
use brokerd::runner::{Refusing, Runtime};
use brokerd::serve::{self, ServeError};
const USAGE: &str = "usage: brokerd serve --config <path> [--accept-break]";
/// Where the owner looks when brokerd cannot start: its config, a directory or a socket.
const START_FAILED: &str = "see docs/runbook.md#brokerd-start-failed";
/// Where the owner looks when brokerd stops serving after it started.
const LISTENER_LOST: &str = "see docs/runbook.md#brokerd-listener-lost";
fn main() -> ExitCode {
// The first argument must be `serve`.
// `args_os`: a path need not be UTF-8, and `args` would panic on one that is not.
let mut args = std::env::args_os().skip(1);
match args.next() {
Some(command) if command == "serve" => {}
_ => return usage(),
}
// `--config <path>` exactly once and `--accept-break` at most once, in any order.
let (config_path, accept_break) = match parse_serve(args) {
Ok(pair) => pair,
Err(()) => return usage(),
};
// Read the config before touching the filesystem: a bad config must make nothing.
let cfg = match Config::load(&config_path) {
Ok(cfg) => cfg,
Err(e) => {
eprintln!("brokerd: {e}\n{START_FAILED}");
return ExitCode::from(1);
}
};
// Keep the socket paths for the serving notice, before `cfg` is moved into `start`.
let broker_path = cfg.broker_socket();
let admin_path = cfg.admin_socket();
// The runtime and the line that says which. A `[runner]` runs calls in containers; without it,
// every call is refused, as in M3a.
let log: Arc<dyn Fn(&str) + Send + Sync> = Arc::new(|line: &str| eprintln!("{line}"));
let runtime: Box<dyn Runtime>;
let runtime_notice: String;
match &cfg.runner {
Some(runner) => {
let image = runner.image.clone();
runtime = Box::new(Podman::new(
runner.clone(),
cfg.egress_dir(),
Arc::clone(&log),
));
runtime_notice = format!("tools run in containers from {image}");
}
None => {
runtime = Box::new(Refusing);
runtime_notice = "no [runner] section: every tool call is refused".to_string();
}
}
let started = match serve::start(cfg, accept_break, runtime, log) {
Ok(started) => started,
Err(ServeError::Audit(e @ AuditError::NothingToAccept)) => {
eprintln!("brokerd: {e}");
return ExitCode::from(2);
}
// The audit errors carry their own entry; the rest are this one.
Err(ServeError::Audit(e)) => {
eprintln!("brokerd: {e}");
return ExitCode::from(1);
}
Err(e) => {
eprintln!("brokerd: {e}\n{START_FAILED}");
return ExitCode::from(1);
}
};
if started.recovered {
eprintln!("{RECOVERED_NOTICE}");
}
if let Some(failure) = &started.accepted {
eprintln!(
"audit: accepted the break at {}:{}: {}",
failure.file, failure.line, failure.what
);
}
eprintln!(
"brokerd: serving tools on {} and approvals on {}",
broker_path.display(),
admin_path.display()
);
eprintln!("brokerd: {runtime_notice}");
match started.run() {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("brokerd: stopped serving: {e}\n{LISTENER_LOST}");
ExitCode::from(1)
}
}
}
/// The argument list was not `serve --config <path> [--accept-break]`.
fn usage() -> ExitCode {
eprintln!("{USAGE}");
ExitCode::from(2)
}
/// The `--config` and `--accept-break` flags after `serve`, or `Err(())` for a bad list.
/// Flags are compared as `OsStr`, so one that is not UTF-8 is simply unknown; the path is kept as
/// given.
fn parse_serve<I: Iterator<Item = OsString>>(mut args: I) -> Result<(PathBuf, bool), ()> {
let mut config: Option<OsString> = None;
let mut accept_break = false;
loop {
match args.next() {
None => {
return match config {
Some(path) => Ok((PathBuf::from(path), accept_break)),
None => Err(()),
};
}
Some(arg) if arg == "--config" => match args.next() {
Some(path) => {
if config.is_some() {
return Err(());
}
config = Some(path);
}
None => return Err(()),
},
Some(arg) if arg == "--accept-break" => {
if accept_break {
return Err(());
}
accept_break = true;
}
Some(_) => return Err(()),
}
}
}