Hand over the M3a plan: 22 tasks, their files, and the check record

Task files, the files they copy in (byte-identical to the reference on
m3a-ref), each area's check record, and a README with the per-task
table of what each check exposed. The handoff note is done with.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-18 23:45:43 -07:00
co-authored by Claude Opus 5
parent 69f0a0a218
commit e3f37da232
180 changed files with 17219 additions and 292 deletions
+137
View File
@@ -0,0 +1,137 @@
# 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<Mount>,
egress: Option<Vec<String>>, // 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<RunOutput, RunError>;
}
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`.