The third attempt filled five functions, then ran out of room planning all of run in one turn. run and run_container are now given; spawn, Io::start, Io::finish and answer are small todo!()s. Checked fillable: 11 of 11 passed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
92 lines
6.3 KiB
Markdown
92 lines
6.3 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.
|
|
|
|
## 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.
|