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>
122 lines
5.0 KiB
Markdown
122 lines
5.0 KiB
Markdown
# 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.
|