# M3b implementation plan: the runner and the tools > **For the implementing model:** do not work from this file. The owner gives you one task file at > a time (`01-…` to `13-…`). This file is the index for the owner and the reviewer. **Goal:** an allowed tool call runs in a fresh rootless Podman container, with no network unless it is `http_fetch`, which reaches the grant's hosts only through an egress proxy of our own. The four tools and the proxy are programs in `toolkit`; `brokerd` builds the `podman` argument lists and runs them. **Architecture:** shared definitions move to `proto` first (01), the fetch target is sealed (02), and grant paths that cannot be mounted are refused (03). `toolkit` gets its four tools (04 to 06), the public-address check (07) and the proxy (08). `brokerd` gets `[runner]` (09), the argument lists (10), the runtime (11), the proxy's lifecycle (12) and the wiring in `serve` (13). **Spec:** `docs/specs/2026-09-22-m3b-runner.md`. Brief: `docs/design.md`. Every fail-closed message ends with a pointer into `docs/runbook.md`; the `runner-unavailable` entry is already there. **No task needs Podman, Nix or a network.** Everything runs against fakes: fake `curl` and `podman` scripts, a fake resolver, `UnixStream::pair()`. The image and the checks on straylight are done by the design model afterwards. ## Global constraints - Everything in `AGENTS.md`, including "Lessons from earlier reviews". - No new dependency. `toolkit` gains `serde` and `serde_json`, both already vetted (task 04). - Branch `m3b`. One task, one fresh OpenCode session, one commit. Run `cargo fmt --all` before the gate. Review happens once, after task 13. - Tests that write a script and run it take a lock (`serial()`): otherwise another test's fork can make running it fail with "text file busy". Keep that pattern in anything you add. ## Tasks The last column is how the given tests were checked before hand-over (decision of 2026-09-18, tip T17). For M3b every task had a reference implementation: the tests of each task were run against it at that task's end state, then the reference was deleted so it cannot be read (tip T18). | # | File | Delivers | Tests | Check | |---|---|---|---|---| | 01 | `01-proto-tools-hosts.md` | `proto::tools`, `proto::hosts`; `brokerd::args` uses them | `proto/tests/tools.rs`, `hosts.rs` | reference; the given test pinned an accepted serde weakness, removed | | 02 | `02-brokerd-fetch-url.md` | `ToolArgs::HttpFetch(FetchUrl)`, sealed (M3a finding 14) | `brokerd/tests/args.rs` (changed), a `compile_fail` doctest | reference | | 03 | `03-brokerd-grant-mount-rule.md` | grant paths with `:` or `,` are invalid | `brokerd/tests/grants_mount.rs` | reference | | 04 | `04-toolkit-files.md` | the `toolkit` program, `read_file`, `write_file` | `toolkit/tests/files.rs`, `support/mod.rs` | reference | | 05 | `05-toolkit-shell.md` | `shell` | `toolkit/tests/shell.rs` | reference | | 06 | `06-toolkit-fetch.md` | `http_fetch` through `curl` | `toolkit/tests/fetch.rs` | reference; found the ETXTBSY race (1 in 7 runs), fixed with `serial()` | | 07 | `07-toolkit-addr.md` | `is_public` | `toolkit/tests/addr.rs` | reference; found `::/96` missing from the spec, added | | 08 | `08-toolkit-egress-proxy.md` | the SOCKS5 egress proxy | `toolkit/tests/egress.rs` | reference; 20 runs clean | | 09 | `09-brokerd-runner-config.md` | `[runner]` | `config_runner.rs`, 5 fixtures, `support/rig.rs` (changed) | reference | | 10 | `10-brokerd-podman-args.md` | the `podman` argument lists; `RunSpec` gains session and call | `podman_args.rs`, 6 golden files | reference | | 11 | `11-brokerd-container.md` | the Podman runtime | `container.rs`, `support/fake_podman.rs` | reference; 20 runs clean | | 12 | `12-brokerd-egress.md` | the proxy's lifecycle for `http_fetch` | `container_egress.rs` | reference; 20 runs clean | | 13 | `13-brokerd-serve-runner.md` | `brokerd serve` uses the runtime | `serve_runner.rs` | reference | At the end: `make gate` prints `gate: ok` with about 638 tests. ## Changes during the run - 2026-09-23, after task 04: task 04 adds dependencies to `toolkit`, which changes `Cargo.lock`, but its `git add` line left the lock out, so the driver stopped on an unclean tree. The design model folded the lock into task 04's commit and added `Cargo.lock` to every task's `git add` (a no-op when it has not changed). The owner resumes from task 05. - 2026-09-23, task 08: the first attempt passed 13 of 15 tests and ended without a commit, after OpenCode refused a command in `/tmp`. Its two defects: `main` did not skip the program name, so `egress-proxy` never matched; and `serve` joined each handler thread, serving one connection at a time. The attempt is saved in `.state/runs/M3b/08-first-attempt.diff`; the tree was reset, and the task now says both things explicitly and to debug inside the repository. Resume from task 08. - 2026-09-23, task 11: the session ended in deliberation, with nothing written. The task stored `egress_dir` for task 12 but gave nothing that read it, so clippy's `dead_code` would fail the gate and `#[allow]` is forbidden: a task that could not be written as described (tip T16). The reference had a public `egress_dir()` getter; the task now gives it. Resume from task 11. - 2026-09-23, task 11 again: the second session also ended with nothing written, cut off while deliberating over API details the compiler would have settled. The task now hands over a compiling skeleton of `container.rs` (signatures, constants, the steps as comments, `todo!()` bodies), checked against the given tests (11 red), and says to fill one function at a time with `cargo check` between. Resume from task 11. ## Running it ```sh BOXMAKER_MODEL=straylight/ornith-1.5-35b-a3b tools/run-plan.sh docs/plans/M3b ``` Keep the OpenCode TUI closed while it runs.