Files
boxmaker/crates/brokerd/tests/support/build.rs
T
kyle e1e6c7a338 Decide tool calls against grants, taint and time
Implemented decide and redecide in crates/brokerd/src/policy.rs:
SessionState, Label, Denial, private Matched, and Decision/Ask (private
fields, Debug only, nine getters each) with the Outcome enum. decide
rejects an unknown tool (args not parsed) and malformed arguments before
matching, then runs the M1-M5 matching pass in id order and returns
Allowed/Ask/Denied by the winner's mode; redecide re-runs matching now
and rebuilds the Decision from the Ask. Seven doctests (six compile_fail,
one compiling) guard the two facts. policy 7, policy_matching 10,
policy_redecide 7, policy_property 4, doc 7 all pass; make gate ok.

Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
2026-09-19 03:05:01 -07:00

157 lines
4.3 KiB
Rust

//! Builders for grants and requests, for the policy tests. Do not edit.
//!
//! Included with `#[path = "support/build.rs"] mod build;`.
#![allow(dead_code)] // each test file uses a different part of this module
use brokerd::grants::{GrantSet, LoadedGrant};
use brokerd::policy::{Ask, Decision, Denial, Outcome, SessionState};
use proto::{
CallId, Constraints, DataClass, DenyReason, Grant, Mode, SessionId, Timestamp, ToolRequest,
};
/// The moment every policy test decides at.
pub const NOW: &str = "2026-09-18T12:00:00.000Z";
pub fn ts(text: &str) -> Timestamp {
Timestamp::parse(text).unwrap()
}
pub fn now() -> Timestamp {
ts(NOW)
}
pub struct Build(LoadedGrant);
/// A grant with the widest settings: it applies at every taint, never expires, and labels its
/// results `private` and untrusted. Each test narrows what it is about.
pub fn grant(id: &str, tool: &str, mode: Mode) -> Build {
Build(LoadedGrant {
id: id.to_string(),
grant: Grant {
tool: tool.to_string(),
mode,
max_taint: DataClass::Secret,
result_class: DataClass::Private,
untrusted: true,
expires: None,
secret: None,
constraints: Constraints::default(),
},
// Stands in for the file's hash, and differs from grant to grant.
sha256: proto::sha256(id.as_bytes()).unwrap(),
})
}
impl Build {
pub fn paths(mut self, paths: &[&str]) -> Build {
self.0.grant.constraints.paths = paths.iter().map(|p| p.to_string()).collect();
self
}
pub fn hosts(mut self, hosts: &[&str]) -> Build {
self.0.grant.constraints.hosts = hosts.iter().map(|h| h.to_string()).collect();
self
}
pub fn max_taint(mut self, class: DataClass) -> Build {
self.0.grant.max_taint = class;
self
}
pub fn class(mut self, class: DataClass) -> Build {
self.0.grant.result_class = class;
self
}
pub fn trusted(mut self) -> Build {
self.0.grant.untrusted = false;
self
}
pub fn expires(mut self, at: &str) -> Build {
self.0.grant.expires = Some(ts(at));
self
}
pub fn done(self) -> LoadedGrant {
self.0
}
}
pub fn set(grants: Vec<Build>) -> GrantSet {
GrantSet::from_grants(grants.into_iter().map(Build::done).collect())
.unwrap_or_else(|problems| panic!("the test's grants are not valid: {problems:?}"))
}
pub fn request(tool: &str, arguments: &str) -> ToolRequest {
ToolRequest {
session: SessionId::new("s1").unwrap(),
call: CallId(1),
tool: tool.to_string(),
arguments: arguments.to_string(),
}
}
pub fn read(path: &str) -> ToolRequest {
request("read_file", &format!(r#"{{"path":"{path}"}}"#))
}
pub fn write(path: &str) -> ToolRequest {
request(
"write_file",
&format!(r#"{{"path":"{path}","content":"x"}}"#),
)
}
pub fn shell(cwd: Option<&str>) -> ToolRequest {
match cwd {
Some(cwd) => request("shell", &format!(r#"{{"command":"ls","cwd":"{cwd}"}}"#)),
None => request("shell", r#"{"command":"ls"}"#),
}
}
pub fn fetch(url: &str) -> ToolRequest {
request("http_fetch", &format!(r#"{{"url":"{url}"}}"#))
}
pub fn at(taint: DataClass) -> SessionState {
SessionState {
taint,
untrusted: false,
}
}
pub fn private() -> SessionState {
at(DataClass::Private)
}
pub fn secret() -> SessionState {
at(DataClass::Secret)
}
pub fn allowed(outcome: Outcome) -> Decision {
match outcome {
Outcome::Allowed(decision) => decision,
other => panic!("expected allowed, got {other:?}"),
}
}
pub fn asked(outcome: Outcome) -> Ask {
match outcome {
Outcome::Ask(ask) => ask,
other => panic!("expected ask, got {other:?}"),
}
}
pub fn denied(outcome: Outcome) -> Denial {
match outcome {
Outcome::Denied(denial) => denial,
other => panic!("expected denied, got {other:?}"),
}
}
/// The reason of a denial. Only `denied_by_grant` may name a grant.
pub fn reason(outcome: Outcome) -> DenyReason {
let denial = denied(outcome);
if denial.reason != DenyReason::DeniedByGrant {
assert_eq!(denial.grant, None, "only denied_by_grant names a grant");
assert_eq!(denial.grant_sha256, None);
}
denial.reason
}