# 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; pub fn load(path: &Path) -> Result; 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.