brokerd serve: run tools in containers when [runner] is set

Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
2026-09-23 09:35:46 -07:00
parent e6081a2177
commit feb50abc88
3 changed files with 169 additions and 7 deletions
+25 -7
View File
@@ -7,7 +7,8 @@ use std::sync::Arc;
use brokerd::audit::{AuditError, RECOVERED_NOTICE};
use brokerd::config::Config;
use brokerd::runner::Refusing;
use brokerd::container::Podman;
use brokerd::runner::{Refusing, Runtime};
use brokerd::serve::{self, ServeError};
const USAGE: &str = "usage: brokerd serve --config <path> [--accept-break]";
@@ -44,12 +45,28 @@ fn main() -> ExitCode {
let broker_path = cfg.broker_socket();
let admin_path = cfg.admin_socket();
let started = match serve::start(
cfg,
accept_break,
Box::new(Refusing),
Arc::new(|line: &str| eprintln!("{line}")),
) {
// 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}");
@@ -80,6 +97,7 @@ fn main() -> ExitCode {
broker_path.display(),
admin_path.display()
);
eprintln!("{runtime_notice}");
match started.run() {
Ok(()) => ExitCode::SUCCESS,
+143
View File
@@ -0,0 +1,143 @@
//! `brokerd serve` with a `[runner]` section runs allowed calls through Podman (here a fake), and
//! says which runtime it uses. Without the section it refuses every call, as in M3a. Do not edit.
#[path = "support/fake_podman.rs"]
mod fake_podman;
use std::io::Read;
use std::os::unix::net::UnixStream;
use std::path::Path;
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};
use fake_podman::{Fake, IMAGE, serial};
use proto::{
CallId, DataClass, Envelope, Message, PROTOCOL_VERSION, SessionId, ToolRequest, ToolResponse,
read_frame, write_frame,
};
struct Running(Child);
impl Drop for Running {
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}
impl Running {
fn stop(mut self) -> String {
let _ = self.0.kill();
let _ = self.0.wait();
let mut err = String::new();
if let Some(mut stderr) = self.0.stderr.take() {
let _ = stderr.read_to_string(&mut err);
}
err
}
}
fn start(home: &Path, runner: &str) -> Running {
std::fs::create_dir_all(home.join("grants")).unwrap();
std::fs::write(
home.join("grants/notes.toml"),
"tool = \"read_file\"\nmode = \"auto\"\nmax_taint = \"secret\"\n[constraints]\npaths = [\"/n\"]\n",
)
.unwrap();
let config = home.join("brokerd.toml");
std::fs::write(
&config,
format!(
"[paths]\nhome = \"{0}\"\ngrants = \"{0}/grants\"\n{runner}\n",
home.display()
),
)
.unwrap();
let child = Command::new(env!("CARGO_BIN_EXE_brokerd"))
.args(["serve", "--config"])
.arg(&config)
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()
.unwrap();
let running = Running(child);
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));
}
running
}
fn read_call(home: &Path) -> ToolResponse {
let mut stream = UnixStream::connect(home.join("run/loop-broker/broker.sock")).unwrap();
stream
.set_read_timeout(Some(Duration::from_secs(10)))
.unwrap();
let request = ToolRequest {
session: SessionId::new("s1").unwrap(),
call: CallId(1),
tool: "read_file".to_string(),
arguments: r#"{"path":"/n/a.md"}"#.to_string(),
};
let envelope = Envelope {
v: PROTOCOL_VERSION,
id: 1,
r#final: true,
msg: Message::ToolRequest(request),
};
write_frame(&mut stream, &envelope).unwrap();
match read_frame(&mut stream).unwrap().msg {
Message::ToolResponse(response) => response,
other => panic!("{other:?}"),
}
}
#[test]
fn with_a_runner_an_allowed_call_runs_in_a_container() {
let _s = serial();
let fake = Fake::new(
"serve",
"cat > /dev/null; printf 'from the container'; exit 0",
);
let home = fake.dir.join("home");
let running = start(
&home,
&format!(
"[runner]\npodman = \"{}\"\nimage = \"{IMAGE}\"\n",
fake.script.display()
),
);
assert_eq!(
read_call(&home),
ToolResponse::Result {
content: "from the container".to_string(),
class: DataClass::Private,
untrusted: true,
truncated: false,
}
);
let printed = running.stop();
assert!(
printed.contains(&format!("tools run in containers from {IMAGE}")),
"{printed}"
);
assert_eq!(fake.calls().len(), 1);
}
#[test]
fn without_a_runner_every_call_is_refused_and_it_says_so() {
let _s = serial();
let fake = Fake::new("serve-none", "exit 0");
let home = fake.dir.join("home");
let running = start(&home, "");
assert_eq!(
read_call(&home),
ToolResponse::Failed {
message: brokerd::runner::REFUSING.to_string()
}
);
let printed = running.stop();
assert!(printed.contains("no [runner] section"), "{printed}");
assert!(fake.calls().is_empty());
}