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>
4.7 KiB
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(addpub mod admin;),docs/implementer-log.md
Interfaces
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
let grants = broker.grants();(it reads the grant files now: an approval decides again with the grants as they are).- Destructure
Entry { info, ask, reply }, thenlet Answered { verdict, outcome } = broker.ledger().answer(ask, info.approval, answer, &grants, now); reply.send(verdict). If it isErr(the waiting thread has gone),broker.logthe linebrokerd: approval {id} was answered after its requester had gone. The record is written either way; there is nothing else to do.- 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
read_requestisNone→ return.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.
- 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, thencp 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, addpub mod admin;. Runcargo fmt --all. - 4. See the tests pass.
cargo test -p brokerd --test admin, five times. Expected:12 passedevery 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
adminreports 12 passed five runs in a row, andmake gateprintsgate: ok.
Stop and report if
- A test expects
approveto 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.