# M1 task 07: `Decision` in `brokerd` **Branch:** `m1` (run `git switch m1`; `git status --short` must be empty, otherwise stop) **Commit subject:** `Add Decision, decide and the runner stub to brokerd` ## Goal Put the project's central safety rule into types: a tool can only run if policy produced a `Decision`, and nothing outside `brokerd`'s policy module can make one. This task adds the type, a policy function that denies everything, and a runner stub. Grants are matched in M3, not here. ## Context From the design brief: "Authority is encoded in types: a tool cannot execute without a `Decision` value that only `brokerd`'s policy module can construct. `Decision` is defined in `brokerd`, has a private field and does not implement `Deserialize`, so no other crate or wire message can produce one." Also: "No matching grant means deny." The proof is two `compile_fail` doctests: examples that must **fail** to compile. A third doctest uses the same setup through the allowed path and must compile and pass. That third one shows the first two fail because of `Decision`'s privacy and not because of a mistake in the example. ## Files - Create: `crates/brokerd/src/policy.rs`, `crates/brokerd/src/runner.rs` - Modify: `crates/brokerd/src/lib.rs`, `docs/implementer-log.md` ## Interfaces Consumes from `proto`: `ToolRequest`, `ToolResponse`, `DenyReason`, `SessionId`, `CallId`. Produces: ```rust // crates/brokerd/src/policy.rs #[derive(Debug)] // and nothing else: not Clone, not Serialize, not Deserialize pub struct Decision { request: ToolRequest, grant: String } // both fields private impl Decision { fn new(request: ToolRequest, grant: String) -> Self; // private: no `pub` pub fn request(&self) -> &ToolRequest; pub fn grant(&self) -> &str; } /// Until M3 there are no grants, so every request is denied with DenyReason::NoGrant. pub fn decide(request: ToolRequest) -> Result; // crates/brokerd/src/runner.rs /// Takes the Decision by value, so one decision cannot run a tool twice. pub fn run(decision: Decision) -> ToolResponse; // returns Failed { message: "no tool runner until M3" } ``` `Decision::new` is not called by non-test code until M3, so the compiler reports it as dead code. This is the one place in M1 where a lint attribute is allowed. Put exactly this on `new`: ```rust #[cfg_attr(not(test), expect(dead_code, reason = "grant matching arrives in M3"))] ``` ## Steps - [ ] **1. Write the module documentation first.** `crates/brokerd/src/policy.rs` starts with exactly this doc comment. It contains the three doctests. ````rust //! 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); //! ``` ```` - [ ] **2. Write the unit tests** at the end of `policy.rs`, exactly: ```rust #[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"); } } ``` - [ ] **3. Register the modules and see the tests fail.** Add to `crates/brokerd/src/lib.rs`, below the doc comment: ```rust pub mod policy; pub mod runner; ``` Create `runner.rs` as an empty file for now. Run `cargo test -p brokerd`. Expected: it does not compile, because `Decision`, `decide`, `ToolRequest` and `DenyReason` are not defined in `policy.rs`. - [ ] **4. Write `Decision`, `decide` and `run`** as described under Interfaces. - [ ] **5. See the tests pass.** `cargo test -p brokerd`. Expected: 2 unit tests pass, and under `Doc-tests brokerd` 3 pass, two of them marked `compile fail`. - [ ] **6. Check that the doctests guard what they claim.** Temporarily change `fn new` to `pub fn new` and run `cargo test -p brokerd --doc`. Expected: one failure saying `Test compiled successfully, but it's marked compile_fail`. Change it back and run the command again. Expected: 3 pass. Write in your log row that you did this check and what you saw. - [ ] **7. Run the gate.** `make gate`. Expected last line: `gate: ok`. - [ ] **8. Log and commit.** ```sh git add crates/brokerd docs/implementer-log.md git commit ``` ## Done when - `cargo test -p brokerd` passes 2 unit tests and 3 doctests. - `Decision` derives only `Debug`, and `new` has no `pub`. - `make gate` prints `gate: ok`. - `git log --oneline master..m1` shows one commit per task. ## Stop and report if - The `expect(dead_code)` attribute itself causes a warning or an error. - A `compile_fail` doctest passes in step 6 even with `pub fn new`.