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>
5.5 KiB
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.rsis 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
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
- Take
let label = decision.label();before building the spec. runtime.run(&spec)isOk(output)→ToolResponse::Result { content: output.content, class: label.class, untrusted: label.untrusted, truncated: output.truncated }.Err(RunError::Failed(m))orErr(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:
//! ```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, thencp docs/plans/M3a/files/crates/brokerd/tests/runner.rs crates/brokerd/tests/andcp 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. Runcargo 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 twotest resultlines). - 5. Prove the doctest has teeth. Make the four fields of
RunSpecpub, runcargo 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
runnerreports 8 passed, the doctests pass, step 5 was done and logged, andmake gateprintsgate: ok.
Stop and report if
- A test needs
RunSpecto have a public constructor or to beClone. - A test expects a runtime's error text to be changed, prefixed or labelled other than
failed.