# M3a task 14: one owner request on `admin.sock`, and expiry **Branch:** `m3a` (run `git switch m3a`; `git status --short` must be empty, otherwise stop) **Commit subject:** `Handle approvals, refusals and grant checks on admin.sock` ## Goal `brokerd::admin::handle` serves one `admin.sock` connection from `bxctl`: `approvals`, `approve`, `refuse` or `check_grants`, one final answer. `expire_due` answers approvals that have run out. Both use one function, `answer`, because they do the same thing: **whoever takes an entry out of the table answers it**: the ledger writes the `Approval` record, then the verdict goes to the waiting thread (task 13), and only then is `bxctl` answered. ## Files - Copy: `crates/brokerd/tests/admin.rs` - Create: `crates/brokerd/src/admin.rs` - Modify: `crates/brokerd/src/lib.rs` (add `pub mod admin;`), `docs/implementer-log.md` ## Interfaces ```rust use crate::approvals::Entry; use crate::broker::{Broker, forbid, read_request, send}; use crate::ledger::{Answer, Answered}; pub const BY: &str = "bxctl"; // the Approval record's `by` for answers through admin.sock pub fn answer(broker: &Broker, entry: Entry, answer: Answer, now: Timestamp) -> DecisionRecord; pub fn expire_due(broker: &Broker, now: Timestamp) -> usize; pub fn handle(stream: UnixStream, broker: &Broker); ``` ## `answer` 1. `let grants = broker.grants();` (it reads the grant files now: an approval decides again with the grants as they are). 2. Destructure `Entry { info, ask, reply }`, then `let Answered { verdict, outcome } = broker.ledger().answer(ask, info.approval, answer, &grants, now);` 3. `reply.send(verdict)`. If it is `Err` (the waiting thread has gone), `broker.log` the line `brokerd: approval {id} was answered after its requester had gone`. The record is written either way; there is nothing else to do. 4. Return `outcome`. `expire_due`: `broker.table().take_expired(now)`, call `answer(.., Answer::Expired, now)` on each entry, return how many there were. ## `handle`: every exit 1. `read_request` is `None` → return. 2. `now = Timestamp::now()`. Then, by message kind: | Message | Does | Answer (`final: true`, the request's `id`) | |---|---|---| | `Approvals(Empty {})` | `table().list()` | `ApprovalList { items }` | | `Approve { approval }` | `table().take(approval)`: `None` | error `NoSuchApproval`, detail `approval {id} is not pending` | | | `Some(e)` → `answer(.., Answer::Approved { by: Some(BY) }, now)` | `ApproveResult { outcome }` | | `Refuse { approval, reason }` | `take`: `None` | error `NoSuchApproval` as above | | | `Some(e)` → `answer(.., Answer::Refused { by: Some(BY), reason }, now)`, outcome is `Denied { reason: ApprovalRefused }` | `Ok(Empty {})` | | | the same, any other outcome (the record could not be written) | error `Internal`, detail below | | `CheckGrants(Empty {})` | `grants::load(&broker.cfg().paths.grants)` | `GrantsReport { problems }`: the `Err`'s list, or empty | | anything else | `forbid(broker, &mut stream, id, &msg, "admin.sock")` | (sent by `forbid`) | The `Internal` detail is one literal: `"the refusal could not be recorded; the call is denied; see docs/runbook.md#audit-unavailable"`. The waiting call is denied whatever happens: the ledger sends `Denied(AuditUnavailable)` when it cannot record. `CheckGrants` does not go through `broker.grants()`: `bxctl grants check` shows the problems itself and must not use up the "print once" of task 13. 3. Send the answer. A failed send is ignored. Notice what is **not** here: nothing looks at an entry and then removes it in a second step; `take` is the only way in. The test `approve_and_refuse_at_once_give_exactly_one_answer` races the two a hundred times and checks for exactly one `Approval` record each time. ## Steps - [ ] **1. Copy.** `git switch m3a`, then `cp docs/plans/M3a/files/crates/brokerd/tests/admin.rs crates/brokerd/tests/` - [ ] **2. See the test fail.** `cargo test -p brokerd --test admin`. Expected: no compile. - [ ] **3. Write `admin.rs`**, add `pub mod admin;`. Run `cargo fmt --all`. - [ ] **4. See the tests pass.** `cargo test -p brokerd --test admin`, five times. Expected: `12 passed` every time. - [ ] **5. Run the gate.** `make gate`. Expected last line: `gate: ok`. - [ ] **6. Log and commit.** `git add crates/brokerd docs/implementer-log.md && git commit` ## Done when - `admin` reports 12 passed five runs in a row, and `make gate` prints `gate: ok`. ## Stop and report if - A test expects `approve` to run the call on the admin thread. It never does: the waiting thread of task 13 runs it. - `bxctl`'s tests (`cargo test -p bxctl`) stop passing. They talk to a fake, but the kinds and answers above are the ones they expect; report the difference.