Files
boxmaker/docs/plans/M4b/07-gatewayd-config-approvals.md
T
kyleandClaude Opus 5.5 2d415fc1be Plan M4b: approvals over Mattermost in 15 tasks
Skeletons for new files only; existing files change by exact edits, so no task overwrites an
earlier one's work. Each task's tests were run against a reference built in order from master,
with the gate at each step and mutations checked; each skeleton compiles and fails its tests, and
every todo!() comment was read beside its signature. The reference is kept off this machine.

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

4.2 KiB

M4b task 07: [approvals] in gatewayd.toml

Branch: m4b (run git switch m4b; git status --short must be empty, otherwise stop) Commit subject: gatewayd: config, [approvals] users and socket

Goal

Who may answer approvals, and where brokerd brings them (M4b spec, section 3):

[approvals]
users = []                    # Mattermost user ids who may answer; empty: everyone in [allow] users
socket = ""                   # empty: <home>/run/gateway/gateway.sock

An approver must also be in [allow] users: one who is not heard could never answer, so that is a configuration error. The first approver (approvals.users[0], or allow.users[0] when the list is empty) gets, in direct messages, the approvals of sessions that are not in Mattermost.

Files

  • Copy: crates/gatewayd/tests/config_approvals.rs
  • Modify: crates/gatewayd/src/config.rs, docs/runbook.md, docs/implementer-log.md

The changes, exactly

In crates/gatewayd/src/config.rs:

  1. In struct Config, after pub limits: Limits,:
    #[serde(default)]
    pub approvals: ApprovalsConfig,

and after struct Config:

/// `[approvals]`: who may answer approvals, and where `brokerd` brings them.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Default)]
#[serde(deny_unknown_fields, default)]
pub struct ApprovalsConfig {
    /// Mattermost user ids; empty means everyone in `[allow] users`.
    pub users: Vec<String>,
    /// Empty: `<home>/run/gateway/gateway.sock`.
    pub socket: PathBuf,
}
  1. In problem, right before the [limits] checks (if self.limits.queue == 0):
        for id in &self.approvals.users {
            if !self.allow.users.contains(id) {
                return Some(format!(
                    "[approvals] {id:?} is not in [allow] users: an approver who is not heard could never answer"
                ));
            }
        }
  1. In impl Config, before state_path, three methods. Write them from these descriptions:
    • pub fn approvers(&self) -> &[String]: &self.approvals.users, or &self.allow.users when the first is empty.
    • pub fn first_approver(&self) -> Option<&str>: the first of approvers() (.first().map(String::as_str)).
    • pub fn gateway_socket(&self) -> PathBuf: self.approvals.socket, or self.paths.home.join("run/gateway/gateway.sock") when it is empty (as_os_str().is_empty()).

In docs/runbook.md, entry gatewayd-start-failed, replace the Why and Confirm paragraphs with:

**Why.** `gatewayd` could not read or parse `gatewayd.toml`, or a value in it is not allowed: a
`url` that is not `http://` or `https://` with a host and an optional port, an id that is not 26
characters of `a-z0-9`, an empty `[allow] users`, an id in `[approvals] users` that is not also in
`[allow] users`, or a secret with not exactly one of `credential`, `env` and `file`. Or it could
not create `<home>/gateway/`, or could not listen on `gateway.sock` (its directory is made 0700
and the socket 0600, as `brokerd`'s are; a path of 108 bytes or more cannot be a socket). It will
not guess at a configuration that decides who it answers and who approves.

**Confirm.** The line says which: `<config path>: …` for the file (unknown keys are errors; check
it against `docs/specs/2026-09-23-m4a-gateway.md`, section 3, and
`docs/specs/2026-09-24-m4b-approvals.md`, section 3), `cannot prepare <dir>: …` for a directory
(`ls -ld <dir> "$(dirname <dir>)"`), or `cannot listen on <socket>: …` for `gateway.sock`
(`Address already in use` means another `gatewayd` runs: `pgrep -x gatewayd`).

Steps

  • 1. Copy. git switch m4b, then cp docs/plans/M4b/files/crates/gatewayd/tests/config_approvals.rs crates/gatewayd/tests/
  • 2. See it fail. cargo test -p gatewayd --test config_approvals. Expected: it does not compile.
  • 3. Make the changes. Run cargo fmt --all.
  • 4. See it pass. cargo test -p gatewayd --test config_approvals --test config. Expected: 4 and 7 passed.
  • 5. Run the gate. make gate. Expected last line: gate: ok.
  • 6. Log and commit. git add crates/gatewayd docs/runbook.md docs/implementer-log.md Cargo.lock && git commit

Done when

  • Both suites pass; make gate prints gate: ok.