Files
boxmaker/docs/plans/M3b/README.md
T
kyleandClaude Opus 5.5 c3aaecdae2 M3b review fixes by the design model: curl --globoff, podman --pull=never
Review findings 1 and 2, both plan defects. curl gains --globoff and a
leading --disable; both podman runs gain --pull=never. The given fetch.rs and
the six golden files change with them. Checked on straylight with a rebuilt
image: a glob URL is one request, a missing image fails at once.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 15:25:51 -07:00

98 lines
6.7 KiB
Markdown

# 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.
- 2026-09-23, task 11, third attempt: it filled five of the six functions from the skeleton, with
`cargo check` between, then planned all of `run` in one turn and was cut off (two compile errors
left). Saved in `.state/runs/M3b/11-third-attempt.diff`. The skeleton now has `run` and
`run_container` written as glue, and the rest as small helpers (`spawn`, `Io::start`,
`Io::finish`, `answer`); the design model filled them in a scratch copy to check the split can
pass (11 of 11, five runs, clippy clean) and removed that. Task 12 now calls `run_container`.
Resume from task 11.
- 2026-09-23, after the review: two plan defects found on straylight were fixed by the design
model: `curl` gains `--globoff` (and a leading `--disable`), since `[1-3]` in a URL made three
requests; both `podman run`s gain `--pull=never`, since a missing image made Podman try to pull.
The given `fetch.rs` and the six golden files changed with them. Follow-up tasks 14 to 16 are for
the implementer.
## 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.