Give loopd's tool port approvals, its own clock and plain denials
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
@@ -57,6 +57,7 @@ reviewer adds findings under "Reviews" once per milestone.
|
||||
| M3a/11-brokerd-approvals | 2026-09-19 | done | 1 | pass | none | Wrote crates/brokerd/src/approvals.rs: Verdict { Run(Box<Decision>), Denied(DenyReason) } (boxed for clippy's large_enum_variant), Entry { info, ask, reply: Sender<Verdict> }, and Table { entries: Mutex<BTreeMap<u64, Entry>> } with a single private `lock()` helper that takes the mutex and recovers a poisoned guard with `unwrap_or_else(|p| p.into_inner())`. `insert` makes a channel and stores the Entry under `info.approval` returning the receiver; `take` removes under the lock and returns the Entry (so the non-Clone `Ask` is not cloned); `take_expired` holds one lock, collects the ids where `now >= expires` (BTreeMap `values()` already yields id order, so no per-id lock to race), removes each, returns them in id order; `list` clones every `info` in id order. No method sends on `reply`. 7 approvals tests pass five runs in a row; `make gate` prints `gate: ok`. | ? | Wrote `crates/brokerd/src/runner.rs`: `Mount` (path/writable), `RunSpec` (four private fields in the given order, no constructor, no Clone, four getters, `egress` as `as_deref`), `RunOutput`, `RunError` (Failed/Unavailable), `Runtime` (Send+Sync), `Refusing` (every call `Err(Unavailable(REFUSING))`), and `run(decision, &dyn Runtime)`. `run` takes `decision.label()` first, builds the spec (`tool = args().tool()`, `arguments = args().clone()`), then the mounts/egress table is one `match` on the `ToolArgs` variant with no `_` arm: ReadFile mounts the matched path read-only, WriteFile writable, Shell mounts every grant path in order writable, HttpFetch mounts nothing and carries `Some(hosts())`; only HttpFetch has network. `Ok(output)` -> `ToolResponse::Result` with the label's class/untrusted; `Err(Failed|Unavailable(m))` -> `Failed { message: m }` unchanged. Added the two module doctests (compile_fail proving the fields private, compiling through proving `tool()` public). 8 runner tests pass; 9 doctests pass; `make gate` prints `gate: ok`. Proved the compile_fail doctest has teeth: making the four RunSpec fields `pub` made it fail ("test compiled"), reverting to private made it pass again. | ? |
|
||||
| M3a/14-brokerd-admin | 2026-09-20 | done | 1 | pass | none | Wrote crates/brokerd/src/admin.rs (129 lines): BY ("bxctl") and REFUSAL_INTERNAL ("the refusal could not be recorded; the call is denied; see docs/runbook.md#audit-unavailable"); answer (grants as they are now, ledger.answer(ask, info.approval, answer, &grants, now) -> Answered { verdict, outcome }, reply.send(verdict) logging "brokerd: approval {id} was answered after its requester had gone" on Err, return outcome); expire_due (take_expired(now), answer(.., Answer::Expired, now) each, return count); handle (read_request None -> return; by kind: Approvals -> ApprovalList{table().list()}, Approve/Refuse -> table().take(approval) None -> NoSuchApproval "approval {id} is not pending" else answer and ApproveResult{outcome}/Ok(Empty {}), a Refuse whose outcome is not Denied{ApprovalRefused} -> Internal with REFUSAL_INTERNAL, CheckGrants -> grants::load(cfg.paths.grants) Err's list or empty -> GrantsReport{problems}, anything else -> forbid on admin.sock); copied crates/brokerd/tests/admin.rs byte-identical (support/ already identical), added pub mod admin; to lib.rs. All twelve admin tests pass five runs in a row; make gate prints gate: ok. Two compiler-necessary deviations from the interface shorthand: handle takes `mut stream` (the task wrote `stream`, but read_request/forbid/send take &mut, and `mut` is not part of the fn(UnixStream,&Broker) type the test passes); Answer::Approved/Refused use `Some(BY.to_string())` because ledger::Answer::by is Option<String>. | ? |
|
||||
| M3a/15-brokerd-serve | 2026-09-20 | done | 2 | fail | none | Copied crates/brokerd/tests/serve.rs byte-identical. Wrote crates/brokerd/src/serve.rs: ServeError { Audit(AuditError), Dir(PathBuf, io::Error), Socket(PathBuf, io::Error) } with Display (Audit is the audit error's text; Dir/Socket are "cannot prepare"/"cannot listen on" {path}.display() + ": {e}") and source() returning the inner error for all three; Started { pub broker, pub recovered, pub accepted, private tools/admin }; start opens the audit lock first so nothing is made on failure, binds both sockets in order, and builds the ledger and broker each through its own box over the one log Arc; listen makes the socket's dir at 0700, sets 0700 on it always (even when pre-existing), removes a stale socket (NotFound ignored), binds, sets 0600 on the socket; run spawns the once-a-second expiry thread and one accept thread per socket (a handler per stream, the first listener error sent on an mpsc channel, run returns it) — serve is the only module that starts threads; main.rs parses `serve --config <path> [--accept-break]` (exit 2 on a bad list), reads config before touching disk (exit 1), routes NothingToAccept to exit 2 and every other start error to exit 1, prints the recovered/accepted/serving notices to stderr. Added `pub mod serve;` to lib.rs. All 9 serve tests plus the rest of brokerd pass; `make gate` prints `gate: ok`. First `make gate` failed at `cargo fmt --all --check` (edits landed after fmt); passed on the second run after re-running `cargo fmt`. | ? |
|
||||
| M3a/16-loopd-tools | 2026-09-20 | done | 1 | pass | none | Added `Pending { approval, expires }` and the `ToolPort::call` two-argument form (`on_pending`) in tools.rs; `Registry::m3a()` with clock (core) then read_file/write_file/shell/http_fetch (all core: false), `m2b()` retitled the test registry; the four broker schemas (`read_file_schema`/`write_file_schema`/`shell_schema`/`http_fetch_schema`) with the exact descriptions and required lists; `CLOCK` const and the clock arm in `dispatch` returning `Timestamp::now()` locally; `denial_text` covering all ten DenyReason variants with no `_` arm; `FakeTools::call` took `_on_pending` and lost its clock arm so clock falls through to Denied{NoGrant}. turn.rs: `run_call` gained an `on_event` param, the Dispatch::Port arm calls `port.call` with the on_pending closure that emits `ApprovalPending` (named by `request.tool`) and the four response arms (Result/Failed/Denied emits `ToolDenied` by `request.tool` then `denial_text`, PendingApproval the fixed "no final answer" text); main.rs `Registry::m3a()`. Copied tests/tools.rs, turn.rs, turn_broker.rs, tests/support/mod.rs byte-identical. tools 10, turn 6, turn_broker 5, limits 9, channel 6; `make gate` prints `gate: ok`. | ? |
|
||||
|
||||
|
||||
## Reviews
|
||||
|
||||
Reference in New Issue
Block a user