Add Decision, decide and the runner stub to brokerd
Implemented-By: Laguna S 2.1 (OpenCode)
This commit is contained in:
@@ -1 +1,4 @@
|
|||||||
//! The broker: the only role that holds authority.
|
//! The broker: the only role that holds authority.
|
||||||
|
|
||||||
|
pub mod policy;
|
||||||
|
pub mod runner;
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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/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/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/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
|
## Reviews
|
||||||
|
|||||||
Reference in New Issue
Block a user