Add Decision, decide and the runner stub to brokerd

Implemented-By: Laguna S 2.1 (OpenCode)
This commit is contained in:
2026-09-17 09:05:48 -07:00
parent b9f3ab45c1
commit 8c1852ec47
4 changed files with 111 additions and 0 deletions
+3
View File
@@ -1 +1,4 @@
//! The broker: the only role that holds authority.
pub mod policy;
pub mod runner;
+95
View File
@@ -0,0 +1,95 @@
//! Policy decisions. `Decision` can only be constructed in this module.
//!
//! Code outside this module cannot build a `Decision` with a struct literal, because its fields
//! are private:
//!
//! ```compile_fail
//! let request = proto::ToolRequest {
//! session: proto::SessionId::new("s1").unwrap(),
//! call: proto::CallId(1),
//! tool: "read_file".to_string(),
//! arguments: "{}".to_string(),
//! };
//! let _ = brokerd::policy::Decision { request, grant: "g".to_string() };
//! ```
//!
//! Nor with the constructor, because it is private to this module:
//!
//! ```compile_fail
//! let request = proto::ToolRequest {
//! session: proto::SessionId::new("s1").unwrap(),
//! call: proto::CallId(1),
//! tool: "read_file".to_string(),
//! arguments: "{}".to_string(),
//! };
//! let _ = brokerd::policy::Decision::new(request, "g".to_string());
//! ```
//!
//! The same setup compiles when it goes through `decide`, which proves the two examples above
//! fail because of `Decision` and not because of a mistake in the setup:
//!
//! ```
//! let request = proto::ToolRequest {
//! session: proto::SessionId::new("s1").unwrap(),
//! call: proto::CallId(1),
//! tool: "read_file".to_string(),
//! arguments: "{}".to_string(),
//! };
//! assert_eq!(brokerd::policy::decide(request).unwrap_err(), proto::DenyReason::NoGrant);
//! ```
use proto::{DenyReason, ToolRequest};
#[derive(Debug)]
pub struct Decision {
request: ToolRequest,
grant: String,
}
impl Decision {
#[cfg_attr(not(test), expect(dead_code, reason = "grant matching arrives in M3"))]
fn new(request: ToolRequest, grant: String) -> Self {
Decision { request, grant }
}
pub fn request(&self) -> &ToolRequest {
&self.request
}
pub fn grant(&self) -> &str {
&self.grant
}
}
/// Until M3 there are no grants, so every request is denied with DenyReason::NoGrant.
pub fn decide(request: ToolRequest) -> Result<Decision, DenyReason> {
let _ = request;
Err(DenyReason::NoGrant)
}
#[cfg(test)]
mod tests {
use super::*;
use proto::{CallId, SessionId};
fn request() -> ToolRequest {
ToolRequest {
session: SessionId::new("s1").unwrap(),
call: CallId(1),
tool: "read_file".to_string(),
arguments: "{}".to_string(),
}
}
#[test]
fn no_grants_means_deny() {
assert_eq!(decide(request()).unwrap_err(), DenyReason::NoGrant);
}
#[test]
fn decision_exposes_request_and_grant() {
let d = Decision::new(request(), "g1".to_string());
assert_eq!(d.request().tool, "read_file");
assert_eq!(d.grant(), "g1");
}
}
+12
View File
@@ -0,0 +1,12 @@
//! Tool runner stub. Real tool execution arrives in M3.
use crate::policy::Decision;
use proto::ToolResponse;
/// Takes the Decision by value, so one decision cannot run a tool twice.
pub fn run(decision: Decision) -> ToolResponse {
let _ = decision;
ToolResponse::Failed {
message: "no tool runner until M3".to_string(),
}
}
+1
View File
@@ -11,5 +11,6 @@ reviewer adds findings under "Reviews" once per milestone.
| M1/04-proto-frame | 2026-09-17 | done | 2 | fail | none | Added crates/proto/src/frame.rs (MAX_FRAME, FrameError, write_frame, read_frame) re-exported from lib.rs; 13 fixture tests pass. Two compile fixes: mapped read_bytes io::Error to FrameError::Io and annotated serde_json::from_slice::<Envelope>; cargo-fmt reordered the lib.rs re-export lines; `make gate` prints `gate: ok`. |
| M1/05-proto-grant | 2026-09-17 | done | 2 | fail | none | Added crates/proto/src/grant.rs (Mode, Constraints with Default, Grant with serde defaults + deny_unknown_fields) re-exported from lib.rs and toml 1.1.6 as a proto dev-dependency (workspace dep + dependencies.md row); 4 fixture tests pass. cargo-fmt reordered the lib.rs re-exports before the gate. |
| M1/06-proto-records | 2026-09-17 | done | 2 | fail | none | Added crates/proto/src/audit.rs (DecisionRecord, AuditRecord) and crates/proto/src/log.rs (ToolCall, LogRecord) re-exported from lib.rs; 3 fixture tests pass, 40 total across the five proto test files. cargo-fmt reordered the lib.rs re-exports before the gate. |
| M1/07-brokerd-decision | 2026-09-17 | done | 1 | pass | none | Added Decision (Debug only, private fields), decide (Err(NoGrant) until M3) and the run stub (ToolResponse::Failed) in crates/brokerd; Decision::new carries expect(dead_code). 2 unit + 3 doctests (2 compile_fail) pass; verified the compile_fail guards by temporarily making new pub. `make gate` prints `gate: ok`. |
## Reviews