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

104 lines
4.2 KiB
Markdown

# 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):
```toml
[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,`:
```rust
#[serde(default)]
pub approvals: ApprovalsConfig,
```
and after `struct Config`:
```rust
/// `[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,
}
```
2. In `problem`, right before the `[limits]` checks (`if self.limits.queue == 0`):
```rust
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"
));
}
}
```
3. 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:
```markdown
**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`.