Add bxctl approvals, approve, refuse and grants check

Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
2026-09-20 23:25:05 -07:00
parent 469be2c0a1
commit 2d94067b52
12 changed files with 1780 additions and 79 deletions
+1
View File
@@ -59,6 +59,7 @@ reviewer adds findings under "Reviews" once per milestone.
| 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`. | ? |
| M3a/17-broker-port | 2026-09-20 | done | 2 | fail | none | Wrote crates/loopd/src/broker_port.rs: BrokerPort { socket: PathBuf, timeout: Duration, log: Box<dyn Fn(&str)+Send+Sync> } with new() and with_log(); call() makes one connection per call, reads through the spec's Deadline so a one-byte-at-a-time peer cannot hold the turn, and routes every one of the eleven exits through a single unavailable() helper that logs unavailable_line once, returns Failed{UNAVAILABLE} and drops the stream, never returning PendingApproval; NoBroker always returns Failed{NOT_CONFIGURED} and prints nothing. Wired main.rs (run_serve takes BrokerPort when cfg.broker.socket is Some and NoBroker printing not_configured_line() once when None, FakeTools no longer used; run_selftest_check appends the loopd-selftest-failed pointer for both commands), session.rs (Torn ends at #session-log-damaged; NotFound left without a pointer) and baseline.rs (new Core(PathBuf, io::Error), displayed with #core-memory-unreadable and returned for an unreadable core.md, Read left as-is for system.md). The four gate test files pass five runs in a row (15, 2, 7, 11); each of the eleven exits was checked against the code one by one. First gate failed on fmt import order, fixed with cargo fmt. NOTE: crates/loopd/src/config.rs (the Broker struct) and crates/loopd/src/lib.rs (pub mod broker_port) were already modified in the working tree when I began — they are not in HEAD 1ceaa36 and I made no edit to either; I verified they match the task spec and the gate passes, so left them as-is. | ? |
| M3a/18-bxctl-admin | 2026-09-20 | done | 1 | pass | the task says "reads $BOXMAKER_HOME (unset → 'bxctl: $BOXMAKER_HOME is not set', exit 1)"; the authoritative chat and admin tests run with no $BOXMAKER_HOME and pass --socket/--admin-socket, so a hard unset-home error fails them. Home is read with default /var/lib/boxmaker and used only for socket defaults; no exit-1-on-unset-home check. | Wrote crates/bxctl/src/{cli.rs,admin.rs,escape.rs,verify.rs} and edited lib.rs, main.rs and chat.rs. Command is struct-variant: Chat(ChatOptions), Approvals{admin_socket}, Approve{admin_socket,approval}, Refuse{admin_socket,approval,reason}, GrantsCheck{admin_socket}, AuditVerify{home}; UsageError is a unit struct; the socket flag is --admin-socket (hyphen). escape_json_text escapes control/DEL/C1/zero-width/line-separator code points as \uXXXX; escape_model_text copies \n and \t. cli::parse takes flags before or after positionals, a value-flag consumes the next word literally even if it looks like a flag, an id is ASCII digits fitting u64 (rejects +41 and a leading space). admin::list sends Approvals(Empty {}) and rejects any other kind with Protocol; write_block prints the grant (escaped) then the taint (wire name). main.rs parses before connecting so a usage error exits 2 even with no broker, only chat runs a turn, and audit verify stays unimplemented. All 53 bxctl tests pass (admin 21, chat 12, cli 12, escape 8); main.rs is 266 lines; make gate prints gate: ok. | ? |
## Reviews