Replace the runner stub with the Runtime seam and RunSpec
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
//! The runner seam: what `run` puts in the `RunSpec` for each tool, and what it answers. Do not
|
||||
//! edit.
|
||||
|
||||
#[path = "support/build.rs"]
|
||||
mod build;
|
||||
#[path = "support/runtime.rs"]
|
||||
mod runtime;
|
||||
|
||||
use brokerd::args::{ToolArgs, ToolName};
|
||||
use brokerd::policy::{Decision, Outcome, SessionState, decide};
|
||||
use brokerd::runner::{Mount, REFUSING, Refusing, RunError, RunOutput, run};
|
||||
use build::{grant, now, read, request, set};
|
||||
use proto::{DataClass, Mode, ToolRequest, ToolResponse};
|
||||
use runtime::Recording;
|
||||
|
||||
fn allowed(grants: Vec<build::Build>, request: ToolRequest) -> Decision {
|
||||
match decide(request, &set(grants), SessionState::default(), now()) {
|
||||
Outcome::Allowed(decision) => decision,
|
||||
other => panic!("the test's call is not allowed: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn mount(path: &str, writable: bool) -> Mount {
|
||||
Mount {
|
||||
path: path.to_string(),
|
||||
writable,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_file_mounts_the_matched_path_read_only_and_has_no_network() {
|
||||
let d = allowed(
|
||||
vec![grant("notes", "read_file", Mode::Auto).paths(&["/h/notes", "/h/notes/deep"])],
|
||||
read("/h/notes/deep/a.md"),
|
||||
);
|
||||
let rt = Recording::answering("text");
|
||||
run(d, rt.as_ref());
|
||||
let seen = rt.seen();
|
||||
assert_eq!(seen.len(), 1);
|
||||
assert_eq!(seen[0].tool, ToolName::ReadFile);
|
||||
assert_eq!(
|
||||
seen[0].arguments,
|
||||
ToolArgs::ReadFile {
|
||||
path: "/h/notes/deep/a.md".to_string()
|
||||
}
|
||||
);
|
||||
// The longest path that holds the argument, and only that one.
|
||||
assert_eq!(seen[0].mounts, [mount("/h/notes/deep", false)]);
|
||||
assert_eq!(seen[0].egress, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_file_mounts_the_matched_path_writable() {
|
||||
// A grant path equal to the argument does not count, so `/s/out` is written through `/s`.
|
||||
let d = allowed(
|
||||
vec![grant("s", "write_file", Mode::Auto).paths(&["/s", "/s/out"])],
|
||||
request("write_file", r#"{"path":"/s/out","content":"x"}"#),
|
||||
);
|
||||
let rt = Recording::answering("");
|
||||
run(d, rt.as_ref());
|
||||
let seen = rt.seen();
|
||||
assert_eq!(seen[0].tool, ToolName::WriteFile);
|
||||
assert_eq!(seen[0].mounts, [mount("/s", true)]);
|
||||
assert_eq!(seen[0].egress, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_mounts_every_path_of_the_grant_writable() {
|
||||
let d = allowed(
|
||||
vec![grant("sh", "shell", Mode::Auto).paths(&["/a", "/b/c"])],
|
||||
request("shell", r#"{"command":"ls","cwd":"/b/c/d"}"#),
|
||||
);
|
||||
let rt = Recording::answering("");
|
||||
run(d, rt.as_ref());
|
||||
let seen = rt.seen();
|
||||
assert_eq!(seen[0].tool, ToolName::Shell);
|
||||
assert_eq!(seen[0].mounts, [mount("/a", true), mount("/b/c", true)]);
|
||||
assert_eq!(seen[0].egress, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_without_paths_mounts_nothing() {
|
||||
let d = allowed(
|
||||
vec![grant("sh", "shell", Mode::Auto)],
|
||||
request("shell", r#"{"command":"date"}"#),
|
||||
);
|
||||
let rt = Recording::answering("");
|
||||
run(d, rt.as_ref());
|
||||
let seen = rt.seen();
|
||||
assert_eq!(seen[0].mounts, []);
|
||||
assert_eq!(seen[0].egress, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn http_fetch_mounts_nothing_and_may_reach_the_grants_hosts_only() {
|
||||
let d = allowed(
|
||||
vec![grant("web", "http_fetch", Mode::Auto).hosts(&["example.com", "*.example.org"])],
|
||||
request("http_fetch", r#"{"url":"https://www.example.org/x"}"#),
|
||||
);
|
||||
let rt = Recording::answering("");
|
||||
run(d, rt.as_ref());
|
||||
let seen = rt.seen();
|
||||
assert_eq!(seen[0].tool, ToolName::HttpFetch);
|
||||
assert_eq!(seen[0].mounts, []);
|
||||
assert_eq!(
|
||||
seen[0].egress,
|
||||
Some(vec!["example.com".to_string(), "*.example.org".to_string()])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_result_carries_the_label_combined_over_every_matching_grant() {
|
||||
// `b-keys` has the longer path and wins the mount; the label is the highest class of both
|
||||
// grants, and untrusted because `a-home` says so.
|
||||
let d = allowed(
|
||||
vec![
|
||||
grant("a-home", "read_file", Mode::Auto)
|
||||
.paths(&["/home/kyle"])
|
||||
.class(DataClass::Private),
|
||||
grant("b-keys", "read_file", Mode::Auto)
|
||||
.paths(&["/home/kyle/keys"])
|
||||
.class(DataClass::Secret)
|
||||
.trusted(),
|
||||
],
|
||||
read("/home/kyle/keys/id"),
|
||||
);
|
||||
let rt = Recording::with(Ok(RunOutput {
|
||||
content: "key".to_string(),
|
||||
truncated: true,
|
||||
}));
|
||||
let answer = run(d, rt.as_ref());
|
||||
assert_eq!(
|
||||
answer,
|
||||
ToolResponse::Result {
|
||||
content: "key".to_string(),
|
||||
class: DataClass::Secret,
|
||||
untrusted: true,
|
||||
truncated: true,
|
||||
}
|
||||
);
|
||||
assert_eq!(rt.seen()[0].mounts, [mount("/home/kyle/keys", false)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_run_error_is_a_failure_with_the_runtimes_sentence() {
|
||||
for error in [
|
||||
RunError::Failed("the tool timed out".to_string()),
|
||||
RunError::Unavailable("the container could not start".to_string()),
|
||||
] {
|
||||
let d = allowed(
|
||||
vec![grant("n", "read_file", Mode::Auto).paths(&["/n"])],
|
||||
read("/n/a"),
|
||||
);
|
||||
let text = match &error {
|
||||
RunError::Failed(t) | RunError::Unavailable(t) => t.clone(),
|
||||
};
|
||||
let rt = Recording::with(Err(error));
|
||||
assert_eq!(run(d, rt.as_ref()), ToolResponse::Failed { message: text });
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_production_runtime_refuses_every_call() {
|
||||
assert_eq!(REFUSING, "the runner arrives in M3b");
|
||||
let d = allowed(
|
||||
vec![grant("n", "read_file", Mode::Auto).paths(&["/n"])],
|
||||
read("/n/a"),
|
||||
);
|
||||
assert_eq!(
|
||||
run(d, &Refusing),
|
||||
ToolResponse::Failed {
|
||||
message: REFUSING.to_string()
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user