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:
@@ -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`.
|
||||
Reference in New Issue
Block a user