Files
boxmaker/docs/plans/M3a/04-brokerd-config.md
kyleandClaude Opus 5 e3f37da232 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>
2026-09-18 23:45:43 -07:00

5.0 KiB

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.

# 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]:

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

#[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.