Files
boxmaker/docs/plans/M3b/09-brokerd-runner-config.md
T
kyleandClaude Opus 5.5 b426ca1958 Specify and plan M3b: the runner and the tools
A draft spec for the owner's review and 13 offline tasks with their given
tests: shared tool arguments and host rules in proto, the sealed fetch
target (M3a finding 14), the toolkit tools and SOCKS5 egress proxy, and
brokerd's [runner], podman argument lists, runtime and proxy lifecycle. Each
task's tests were run against a reference at that task's end state (560 to
638 tests, clippy clean); the reference is not in the repository. Adds the
runner-unavailable runbook entry and tip T23 (ETXTBSY in script tests).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-22 22:29:27 -07:00

101 lines
4.6 KiB
Markdown

# M3b task 09: `[runner]` in `brokerd.toml`
**Branch:** `m3b` (run `git switch m3b`; `git status --short` must be empty, otherwise stop)
**Commit subject:** `brokerd: the [runner] section`
## Goal
`[runner]` says how tools run: which `podman`, which image, and the limits. Without the section,
`brokerd` keeps M3a's runtime, which refuses every call (task 13 does that choice). The image must
be named **by digest**, so what runs is exactly what was built. Spec section 6, "Configuration".
## Files
- Copy: `crates/brokerd/tests/config_runner.rs`, the five files
`crates/brokerd/tests/fixtures/config/runner_*.toml`, and `crates/brokerd/tests/support/rig.rs`
(replaces the old one: it builds a `Config` with a struct literal, which now needs
`runner: None`)
- Modify: `crates/brokerd/src/config.rs`, `docs/implementer-log.md`
## Interfaces
In `config.rs`:
```rust
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Runner {
#[serde(default = "default_podman")] pub podman: PathBuf, // "podman"
pub image: String, // required, no default
#[serde(default = "default_egress_network")] pub egress_network: String, // "pasta"
#[serde(default = "default_output_cap")] pub output_cap: u64, // 262_144
#[serde(default = "default_memory")] pub memory: String, // "512m"
#[serde(default = "default_pids")] pub pids: u32, // 128
#[serde(default = "default_read_file_ms")] pub read_file_ms: u64, // 30_000
#[serde(default = "default_write_file_ms")] pub write_file_ms: u64, // 30_000
#[serde(default = "default_shell_ms")] pub shell_ms: u64, // 100_000
#[serde(default = "default_http_fetch_ms")] pub http_fetch_ms: u64, // 60_000
}
impl Runner {
/// The time limit for one call of `tool`: the matching `_ms` field.
pub fn time_limit(&self, tool: crate::args::ToolName) -> std::time::Duration;
}
pub struct Config {
// paths, sockets, approvals as now, then:
#[serde(default)]
pub runner: Option<Runner>,
}
impl Config {
// … as now, plus:
/// `<home>/run/egress`: where each `http_fetch` call gets a directory for the proxy's socket.
pub fn egress_dir(&self) -> PathBuf;
}
```
Write one small private `fn default_…() -> …` per defaulted field (serde needs a function path).
Put the struct attributes on separate lines as usual; the table above only saves space.
## Checks in `Config::load`
After the `ttl_ms` check, if `runner` is `Some`, check it and return the first problem as
`ConfigError::Invalid(path, text)`. In this order:
1. `image` is not `<name>@sha256:<hex>` where `<name>` is not empty and `<hex>` is exactly 64 of
`0-9a-f` (lowercase). Split with `rsplit_once("@sha256:")`. Text:
`[runner] image is {image:?}; it must be named by digest: <name>@sha256:<64 hex digits>`.
2. `memory` is not digits followed by one of `b`, `k`, `m`, `g` (at least one digit). Text:
`[runner] memory is {memory:?}; it must be a number and one of b, k, m, g`.
3. `egress_network` or `podman` is empty. Text:
`[runner] podman and egress_network must not be empty`.
4. Any of `output_cap`, `pids`, `read_file_ms`, `write_file_ms`, `shell_ms`, `http_fetch_ms` is 0,
checked in that order. Text: `[runner] <name> must be at least 1`, with the field's name.
A missing `image` or an unknown key is already a `ConfigError::Parse` from serde; do not add
checks for those. `Config::parse` (the string version the tests use for plain values) does not run
these checks; only `load` does, as for `ttl_ms` today.
## Steps
- [ ] **1. Copy.** `git switch m3b`, then
`cp docs/plans/M3b/files/crates/brokerd/tests/config_runner.rs crates/brokerd/tests/`,
`cp docs/plans/M3b/files/crates/brokerd/tests/fixtures/config/runner_*.toml crates/brokerd/tests/fixtures/config/`,
`cp docs/plans/M3b/files/crates/brokerd/tests/support/rig.rs crates/brokerd/tests/support/`
- [ ] **2. See it fail.** `cargo test -p brokerd --test config_runner`. Expected: it does not
compile.
- [ ] **3. Write the code.** Run `cargo fmt --all`.
- [ ] **4. See it pass.** `cargo test -p brokerd --test config_runner --test config`. Expected: 7
and 7 passed. Then `cargo test -p brokerd`: everything passes.
- [ ] **5. Run the gate.** `make gate`. Expected last line: `gate: ok`.
- [ ] **6. Log and commit.** `git add crates/brokerd docs/implementer-log.md && git commit`
## Done when
- `cargo test -p brokerd` passes; `make gate` prints `gate: ok`.
## Stop and report if
- A test wants an image named by tag (`:latest`) to be accepted.