Files
boxmaker/docs/plans/M3b/02-brokerd-fetch-url.md
T
kyleandClaude Opus 5.5 5e55fe4c66 M3b plan: every task's git add includes Cargo.lock
Task 04 added dependencies to toolkit and its git add line left out the lock
file, so the driver stopped on an unclean tree. The lock change is folded into
task 04's commit.

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

105 lines
4.3 KiB
Markdown

# M3b task 02: seal the fetch target
**Branch:** `m3b` (run `git switch m3b`; `git status --short` must be empty, otherwise stop)
**Commit subject:** `Seal the fetch target: one value holds the URL and its host`
## Goal
Today `ToolArgs::HttpFetch { url, host }` has two public fields. Policy matches `host` against the
grant; from M3b on, the runtime fetches `url`. Any code could build one where the two disagree
(`url` of `evil.example`, `host` of `example.com`), and policy would allow a fetch of the wrong
host. Make that impossible: one sealed value, built only by `args::parse` after the URL checks.
This is M3a review finding 14.
## Files
- Copy: `crates/brokerd/tests/args.rs` (replaces the old one; one test now reads the parsed value
through getters, because it can no longer build one)
- Modify: `crates/brokerd/src/args.rs`, `crates/brokerd/src/policy.rs`,
`crates/brokerd/src/runner.rs`, `docs/implementer-log.md`
## Interfaces
In `args.rs`, the variant becomes a tuple variant holding a new struct:
```rust
pub enum ToolArgs {
ReadFile { path: String },
WriteFile { path: String, content: String },
Shell { command: String, cwd: Option<String> },
HttpFetch(FetchUrl),
}
/// A URL that passed the checks, and its host. Only `parse` makes one, so the host policy matched
/// is always the host of the URL the tool fetches.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FetchUrl {
url: String, // private
host: String, // private
}
impl FetchUrl {
pub fn url(&self) -> &str;
pub fn host(&self) -> &str;
}
```
No `pub fn new`, no `Default`, no `Deserialize`, no public field: `parse` (in the same module) is
the only code that writes `FetchUrl { url, host }`.
Put these two doctests in `FetchUrl`'s doc comment, word for word. The first proves the struct
cannot be built outside `args`; the second proves the first fails for that reason and not a typo:
````rust
/// ```compile_fail
/// let _ = brokerd::args::FetchUrl {
/// url: "https://evil.example/".to_string(),
/// host: "example.com".to_string(),
/// };
/// ```
///
/// ```
/// let args = brokerd::args::parse(brokerd::args::ToolName::HttpFetch, r#"{"url":"https://example.com/a"}"#);
/// let Ok(brokerd::args::ToolArgs::HttpFetch(target)) = args else { panic!("{args:?}") };
/// assert_eq!((target.url(), target.host()), ("https://example.com/a", "example.com"));
/// ```
````
Every place that matched the old variant changes. There are four:
1. `ToolArgs::tool`: `ToolArgs::HttpFetch(_) => ToolName::HttpFetch`.
2. `ToolArgs::canonical_json`: `ToolArgs::HttpFetch(target) =>` serialise
`HttpFetchArgs { url: target.url.clone() }` (still only the URL: the host is not an argument).
3. `parse`'s `ToolName::HttpFetch` arm: `Ok(ToolArgs::HttpFetch(FetchUrl { url: value.url, host }))`.
4. `policy.rs`, in `covers`: `ToolArgs::HttpFetch(target) =>` and match the grant's patterns with
`host_matches(pattern, target.host())`.
In `runner.rs`: `ToolArgs::HttpFetch { .. }` becomes `ToolArgs::HttpFetch(_)`.
Search for any other: `grep -rn "HttpFetch {" crates/` must show nothing when you are done.
## Steps
- [ ] **1. Copy.** `git switch m3b`, then
`cp docs/plans/M3b/files/crates/brokerd/tests/args.rs crates/brokerd/tests/`
- [ ] **2. See it fail.** `cargo test -p brokerd --test args`. Expected: it does not compile
(`ToolArgs::HttpFetch(target)` is not a tuple variant yet).
- [ ] **3. Make the change.** Run `cargo fmt --all`.
- [ ] **4. See it pass.** `cargo test -p brokerd --test args --test policy --test runner`.
Expected: `args` 13 passed; the others pass unchanged. `cargo test -p brokerd --doc`: every
doctest passes, including your two new ones (`compile_fail` counts as passing when it fails to
compile).
- [ ] **5. Check the seal.** `grep -rn "FetchUrl {" crates/brokerd/src/` shows the struct
definition, the `parse` line, and the doctest only.
- [ ] **6. Run the gate.** `make gate`. Expected last line: `gate: ok`.
- [ ] **7. Log and commit.** `git add crates/brokerd docs/implementer-log.md Cargo.lock && git commit`
## Done when
- `cargo test -p brokerd` and `cargo test -p brokerd --doc` pass; `make gate` prints `gate: ok`.
## Stop and report if
- Some code outside `args.rs` needs to build a `FetchUrl`: that would break the seal.
- A test other than `args.rs` needs a change.