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>
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user