# M3a task 10: the runner seam **Branch:** `m3a` (run `git switch m3a`; `git status --short` must be empty, otherwise stop) **Commit subject:** `Replace the runner stub with the Runtime seam and RunSpec` ## Goal `brokerd::runner` is where an allowed call becomes something a runtime can run. `run` takes a `Decision` by value, builds a `RunSpec` from it (the tool, the typed arguments, the directories to mount, the hosts it may reach) and asks a `Runtime`. A `RunSpec` can only be built here, from a `Decision`, so a runtime never sees a call policy did not allow. M3a's production runtime, `Refusing`, runs nothing; M3b adds Podman. ## Files - Copy: `crates/brokerd/tests/runner.rs`, `crates/brokerd/tests/support/runtime.rs` (`support/build.rs` is already there from task 07) - Modify: `crates/brokerd/src/runner.rs` (it holds the M1 stub; replace all of it), `docs/implementer-log.md` `lib.rs` already has `pub mod runner;`. Nothing else calls the old `run(decision)`. ## Interfaces ```rust pub const REFUSING: &str = "the runner arrives in M3b"; #[derive(Debug, Clone, PartialEq, Eq)] pub struct Mount { pub path: String, pub writable: bool } #[derive(Debug)] pub struct RunSpec { // fields private, in this order; no constructor, no Clone tool: ToolName, // crate::args::ToolName arguments: ToolArgs, // crate::args::ToolArgs mounts: Vec, egress: Option>, // None: no network at all } impl RunSpec { pub fn tool(&self) -> ToolName; pub fn arguments(&self) -> &ToolArgs; pub fn mounts(&self) -> &[Mount]; pub fn egress(&self) -> Option<&[String]>; // self.egress.as_deref() } #[derive(Debug, Clone, PartialEq, Eq)] pub struct RunOutput { pub content: String, pub truncated: bool } #[derive(Debug, Clone, PartialEq, Eq)] pub enum RunError { Failed(String), Unavailable(String) } pub trait Runtime: Send + Sync { fn run(&self, spec: &RunSpec) -> Result; } pub struct Refusing; // Runtime: always Err(RunError::Unavailable(REFUSING.to_string())) pub fn run(decision: Decision, runtime: &dyn Runtime) -> proto::ToolResponse; ``` What `Decision` gives you (task 07): `args() -> &ToolArgs` (and `args().tool() -> ToolName`), `matched_path() -> Option<&str>`, `paths() -> &[String]`, `hosts() -> &[String]`, `label() -> Label` with `class: DataClass` and `untrusted: bool`. ## What `run` puts in the spec `tool` is `decision.args().tool()`; `arguments` is `decision.args().clone()`. | `ToolArgs` variant | `mounts` | `egress` | |---|---|---| | `ReadFile` | the matched path, `writable: false` (none if `matched_path()` is `None`) | `None` | | `WriteFile` | the matched path, `writable: true` (none if `None`) | `None` | | `Shell` | **every** path in `decision.paths()`, in order, each `writable: true` | `None` | | `HttpFetch` | none | `Some(decision.hosts().to_vec())` | Only `HttpFetch` has network. Write the table as one `match` on the `ToolArgs` variant, with no `_` arm. ## What `run` answers 1. Take `let label = decision.label();` before building the spec. 2. `runtime.run(&spec)` is `Ok(output)` → `ToolResponse::Result { content: output.content, class: label.class, untrusted: label.untrusted, truncated: output.truncated }`. 3. `Err(RunError::Failed(m))` or `Err(RunError::Unavailable(m))` → `ToolResponse::Failed { message: m }`, unchanged. There is no other exit. A `RunError`'s text reaches the model as a `failed` result labelled public and trusted, so a runtime may only put a fixed sentence in it; say so in its doc comment. ## The doctests Put these two in `runner.rs`'s module doc comment (`//!`), as `task 07` did for `Decision`. The first must fail to compile because the fields are private (`todo!()` fits any type, so it would compile if they were public); the second proves the getters are public: ````rust //! ```compile_fail //! let _ = brokerd::runner::RunSpec { //! tool: brokerd::args::ToolName::Shell, //! arguments: todo!(), //! mounts: Vec::new(), //! egress: None, //! }; //! ``` //! //! ``` //! fn tool_of(spec: &brokerd::runner::RunSpec) -> brokerd::args::ToolName { //! spec.tool() //! } //! ``` ```` ## Steps - [ ] **1. Copy.** `git switch m3a`, then `cp docs/plans/M3a/files/crates/brokerd/tests/runner.rs crates/brokerd/tests/` and `cp docs/plans/M3a/files/crates/brokerd/tests/support/runtime.rs crates/brokerd/tests/support/` - [ ] **2. See the test fail.** `cargo test -p brokerd --test runner`. Expected: it does not compile. - [ ] **3. Write `runner.rs`.** Run `cargo fmt --all`. - [ ] **4. See the tests pass.** `cargo test -p brokerd --test runner`. Expected: `8 passed`. `cargo test -p brokerd --doc`: 9 doctests in all pass (7 from task 07, 2 new; cargo may print them on two `test result` lines). - [ ] **5. Prove the doctest has teeth.** Make the four fields of `RunSpec` `pub`, run `cargo test -p brokerd --doc`, and see one doctest fail ("test compiled"). Make them private again and see it pass. Say in the log's Notes that you did this. - [ ] **6. Run the gate.** `make gate`. Expected last line: `gate: ok`. - [ ] **7. Log and commit.** `git add crates/brokerd docs/implementer-log.md && git commit` ## Done when - `runner` reports 8 passed, the doctests pass, step 5 was done and logged, and `make gate` prints `gate: ok`. ## Stop and report if - A test needs `RunSpec` to have a public constructor or to be `Clone`. - A test expects a runtime's error text to be changed, prefixed or labelled other than `failed`.