Files
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

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 (add pub 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

  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.

  1. 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.