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)
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
//! 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
|
||||
}
|
||||
Reference in New Issue
Block a user