A draft spec for the owner's review and 13 offline tasks with their given tests: shared tool arguments and host rules in proto, the sealed fetch target (M3a finding 14), the toolkit tools and SOCKS5 egress proxy, and brokerd's [runner], podman argument lists, runtime and proxy lifecycle. Each task's tests were run against a reference at that task's end state (560 to 638 tests, clippy clean); the reference is not in the repository. Adds the runner-unavailable runbook entry and tip T23 (ETXTBSY in script tests). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
144 lines
4.1 KiB
Rust
144 lines
4.1 KiB
Rust
//! `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());
|
|
}
|