Hand over the M3a plan: 22 tasks, their files, and the check record

Task files, the files they copy in (byte-identical to the reference on
m3a-ref), each area's check record, and a README with the per-task
table of what each check exposed. The handoff note is done with.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-18 23:45:43 -07:00
co-authored by Claude Opus 5
parent 69f0a0a218
commit e3f37da232
180 changed files with 17219 additions and 292 deletions
+121
View File
@@ -0,0 +1,121 @@
# M3a task 04: `brokerd`'s configuration
**Branch:** `m3a` (run `git switch m3a`; `git status --short` must be empty, otherwise stop)
**Commit subject:** `Add brokerd's configuration`
## Goal
`brokerd.toml` into a typed `Config`. This is a format we define: unknown keys are errors, in
every table. It works like `crates/loopd/src/config.rs`; read that file first and follow its style.
```toml
# brokerd.toml — every key is optional
[paths]
home = "/var/lib/boxmaker" # default: $BOXMAKER_HOME, then /var/lib/boxmaker
grants = "/etc/boxmaker/grants"
[sockets]
broker = "/var/lib/boxmaker/run/loop-broker/broker.sock" # default: under home
admin = "/var/lib/boxmaker/run/owner-broker/admin.sock" # default: under home
[approvals]
ttl_ms = 900000 # 15 min
```
## Files
- Copy: `crates/brokerd/tests/config.rs`, and the six files in
`crates/brokerd/tests/fixtures/config/`
- Create: `crates/brokerd/src/config.rs`
- Modify: `crates/brokerd/src/lib.rs` (add `pub mod config;`), `crates/brokerd/Cargo.toml`,
`docs/dependencies.md`, `docs/implementer-log.md`
`brokerd` gains three dependencies, all already in `[workspace.dependencies]`. Add to
`crates/brokerd/Cargo.toml` under `[dependencies]`:
```toml
serde.workspace = true
serde_json.workspace = true
toml.workspace = true
```
In `docs/dependencies.md`, add `brokerd` to the "Used by" cell of the `serde` row and of the
`serde_json` row (for example `` `proto`, `brokerd` ``). The `toml` row already names `brokerd`.
Change nothing else in that file.
## Interfaces
```rust
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct Paths { pub home: PathBuf, pub grants: PathBuf }
/// An empty path means "the default under `home`".
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Default)]
#[serde(deny_unknown_fields, default)]
pub struct Sockets { pub broker: PathBuf, pub admin: PathBuf }
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct Approvals { pub ttl_ms: u64 }
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct Config {
#[serde(default)] pub paths: Paths,
#[serde(default)] pub sockets: Sockets,
#[serde(default)] pub approvals: Approvals,
}
#[derive(Debug)]
pub enum ConfigError { Read(PathBuf, std::io::Error), Parse(PathBuf, toml::de::Error) }
impl Config {
pub fn parse(text: &str) -> Result<Config, toml::de::Error>;
pub fn load(path: &Path) -> Result<Config, ConfigError>;
pub fn broker_socket(&self) -> PathBuf;
pub fn admin_socket(&self) -> PathBuf;
pub fn audit_dir(&self) -> PathBuf;
pub fn state_dir(&self) -> PathBuf;
}
```
## Rules
1. `impl Default for Paths`: `home` is `$BOXMAKER_HOME` if the variable is set, otherwise
`/var/lib/boxmaker`; `grants` is `/etc/boxmaker/grants`. Copy how `loopd`'s `Paths` reads the
variable (`std::env::var_os`). `impl Default for Approvals`: `ttl_ms` is `900_000`.
2. `deny_unknown_fields` goes on **all four structs**: `Paths`, `Sockets`, `Approvals` and
`Config`. The test checks an unknown key in each table and at the top level.
3. `broker_socket()`: if `sockets.broker` is empty, `home` joined with
`run/loop-broker/broker.sock`; otherwise `sockets.broker`. `admin_socket()`: the same with
`run/owner-broker/admin.sock`. Test for empty with `as_os_str().is_empty()`, as `loopd`'s
`channel_socket` does.
4. `audit_dir()` is `home` joined with `audit`. `state_dir()` is `home` joined with
`broker/sessions`. Neither can be set on its own.
5. `load`: a file that cannot be read is `ConfigError::Read(path, err)`, including a missing file;
a file that does not parse is `ConfigError::Parse(path, err)`. `ConfigError` implements
`Display` (the path, `: `, the inner error) and `std::error::Error`, by hand.
6. Do not set or change environment variables anywhere, tests included.
## Steps
- [ ] **1. Copy.** `git switch m3a`, then
`mkdir -p crates/brokerd/tests/fixtures && cp docs/plans/M3a/files/crates/brokerd/tests/config.rs crates/brokerd/tests/ && cp -r docs/plans/M3a/files/crates/brokerd/tests/fixtures/config crates/brokerd/tests/fixtures/`
- [ ] **2. See the test fail.** `cargo test -p brokerd --test config`. Expected: it does not
compile.
- [ ] **3. Add the dependencies, write `config.rs`, add `pub mod config;`.** Run `cargo build`
once so `Cargo.lock` is updated, then `cargo fmt --all`.
- [ ] **4. See the tests pass.** `cargo test -p brokerd --test config`. Expected: `7 passed`.
- [ ] **5. Run the gate.** `make gate`. Expected last line: `gate: ok`.
- [ ] **6. Log and commit.**
`git add crates/brokerd Cargo.lock docs/dependencies.md docs/implementer-log.md && git commit`
## Done when
- `cargo test -p brokerd --test config` reports 7 passed; `make gate` prints `gate: ok`.
## Stop and report if
- `[workspace.dependencies]` in the root `Cargo.toml` lacks `serde`, `serde_json` or `toml`.
- A test seems to need an environment variable to be set.