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>
This commit is contained in:
2026-09-18 23:45:43 -07:00
co-authored by Claude Opus 5
parent 69f0a0a218
commit e3f37da232
180 changed files with 17219 additions and 292 deletions
+92
View File
@@ -0,0 +1,92 @@
# M3a task 11: the pending-approval table
**Branch:** `m3a` (run `git switch m3a`; `git status --short` must be empty, otherwise stop)
**Commit subject:** `Add the table of pending approvals`
## Goal
A call an `ask` grant matched waits in `brokerd::approvals::Table` until someone answers it:
`bxctl approve`, `bxctl refuse`, the expiry thread, or the waiting thread itself when `loopd` has
gone. They can race. One rule settles every race: **whoever takes the entry out of the table
answers it**, and everyone else finds it gone. So there is no "look, then remove": `take` is one
step under the lock. The table is in memory only.
## Files
- Copy: `crates/brokerd/tests/approvals.rs`
- Create: `crates/brokerd/src/approvals.rs`
- Modify: `crates/brokerd/src/lib.rs` (add `pub mod approvals;`), `docs/implementer-log.md`
## Interfaces
```rust
use std::collections::BTreeMap;
use std::sync::mpsc::{Receiver, Sender, channel};
use std::sync::{Mutex, MutexGuard};
use proto::{DenyReason, PendingApproval, Timestamp};
use crate::policy::{Ask, Decision};
/// What the waiting thread is told. Boxed: clippy's `large_enum_variant` rejects it unboxed.
#[derive(Debug)]
pub enum Verdict { Run(Box<Decision>), Denied(DenyReason) }
#[derive(Debug)]
pub struct Entry { pub info: PendingApproval, pub ask: Ask, pub reply: Sender<Verdict> }
#[derive(Debug, Default)]
pub struct Table { entries: Mutex<BTreeMap<u64, Entry>> }
impl Table {
pub fn new() -> Table;
pub fn insert(&self, info: PendingApproval, ask: Ask) -> Receiver<Verdict>;
pub fn take(&self, id: u64) -> Option<Entry>;
pub fn take_expired(&self, now: Timestamp) -> Vec<Entry>;
pub fn list(&self) -> Vec<PendingApproval>;
}
```
`PendingApproval` is `proto`'s (task 02); its `approval` field is the id. `Ask` is not `Clone`,
which is why `take` returns the entry itself.
Verified in the std docs: `std::sync::mpsc::channel::<T>() -> (Sender<T>, Receiver<T>)`;
`Sender::send(&self, t) -> Result<(), SendError<T>>`, an error only when the receiver is gone;
`Mutex::lock() -> LockResult<MutexGuard<T>>`, and a poisoned lock's guard is
`poisoned.into_inner()`; `BTreeMap::remove(&mut self, &k) -> Option<V>`; `values()` iterates in
key order.
## Rules
1. **One private helper takes the lock**, and every method uses it:
`self.entries.lock().unwrap_or_else(|poisoned| poisoned.into_inner())`. Nothing in this module
can leave the map half-changed, so a lock poisoned by a panic elsewhere is taken over as it is
(the ledger lock of task 12 is different; do not copy this there).
2. `insert`: make a channel, insert `Entry { info, ask, reply: sender }` under `info.approval`,
return the receiver. Ids come from the audit log's `seq` and never repeat.
3. `take`: `remove` under the lock and return it. `None` when the id is not there.
4. `take_expired`: under **one** lock, collect the ids of the entries with `now >= expires`
(exactly at `expires` is expired), remove each, and return the entries in id order. Taking
the lock once per id would let an `approve` slip in between; the tests race the two.
5. `list`: the `info` of every entry, cloned, in id order.
No method sends on `reply`: whoever took the entry does that (tasks 13 and 14).
## Steps
- [ ] **1. Copy.** `git switch m3a`, then
`cp docs/plans/M3a/files/crates/brokerd/tests/approvals.rs crates/brokerd/tests/`
- [ ] **2. See the test fail.** `cargo test -p brokerd --test approvals`. Expected: it does not
compile.
- [ ] **3. Write `approvals.rs`** and add `pub mod approvals;` to `lib.rs`. `cargo fmt --all`.
- [ ] **4. See the tests pass.** `cargo test -p brokerd --test approvals`, five times. Expected:
`7 passed` every time. Two tests race threads a hundred times each; a failure now and then
means a method looks and removes under two separate locks.
- [ ] **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
- `approvals` reports 7 passed five runs in a row, and `make gate` prints `gate: ok`.
## Stop and report if
- A test needs the table to write anything to disk, or to answer an entry itself.