diff --git a/crates/bxctl/src/cli.rs b/crates/bxctl/src/cli.rs index 9f1948c..fc552ba 100644 --- a/crates/bxctl/src/cli.rs +++ b/crates/bxctl/src/cli.rs @@ -21,6 +21,9 @@ usage: bxctl [options] bxctl refuse [--reason TEXT] [--admin-socket PATH] Refuse approval , recording a reason. + bxctl deny [--reason TEXT] [--admin-socket PATH] + Another name for refuse. + bxctl grants check [--admin-socket PATH] Check that the grants load. @@ -98,7 +101,7 @@ pub fn parse(args: &[String], home: &Path) -> Result { "chat" => parse_chat(rest, home), "approvals" => parse_approvals(rest, home), "approve" => parse_approve(rest, home), - "refuse" => parse_refuse(rest, home), + "refuse" | "deny" => parse_refuse(rest, home), "grants" => parse_grants(rest, home), "audit" => parse_audit(rest, home), // A flag before the command, or any unknown command, is not valid. diff --git a/crates/bxctl/tests/cli_deny.rs b/crates/bxctl/tests/cli_deny.rs new file mode 100644 index 0000000..3d0ccd0 --- /dev/null +++ b/crates/bxctl/tests/cli_deny.rs @@ -0,0 +1,42 @@ +//! `bxctl deny` is another name for `bxctl refuse`: every argument list means the same under both +//! (M4b spec, section 2). Do not edit. + +use bxctl::cli::{USAGE, parse}; +use std::path::Path; + +fn under(command: &str, rest: &[&str]) -> Vec { + std::iter::once(command) + .chain(rest.iter().copied()) + .map(str::to_string) + .collect() +} + +#[test] +fn deny_parses_as_refuse() { + let cases: &[&[&str]] = &[ + &["41"], + &["41", "--reason", "not on a Friday"], + &["--reason", "no", "41"], + &["41", "--admin-socket", "/tmp/a.sock"], + &["41", "--reason", "x", "--admin-socket", "/tmp/a.sock"], + &[], + &["x41"], + &["41", "--reason"], + &["41", "42"], + &["41", "--bogus"], + ]; + for rest in cases { + let home = Path::new("/srv/bx"); + assert_eq!( + parse(&under("deny", rest), home), + parse(&under("refuse", rest), home), + "{rest:?}" + ); + } +} + +#[test] +fn usage_names_deny() { + assert!(USAGE.contains("bxctl deny "), "{USAGE}"); + assert!(USAGE.contains("bxctl refuse "), "{USAGE}"); +} diff --git a/docs/implementer-log.md b/docs/implementer-log.md index 6f199fa..b57c0ba 100644 --- a/docs/implementer-log.md +++ b/docs/implementer-log.md @@ -6,6 +6,7 @@ reviewer adds findings under "Reviews" once per milestone. | Task | Date | Status | Gate runs | First gate | Deviations | Notes | Model | |---|---|---|---|---|---|---|---| +| M4b/03-bxctl-deny | 2026-09-24 | done | 1 | pass | none | Copied `docs/plans/M4b/files/crates/bxctl/tests/cli_deny.rs` into `crates/bxctl/tests/`. In `parse`, `"refuse" => parse_refuse(rest, home)` became `"refuse" | "deny" => parse_refuse(rest, home)`, so `deny` produces the same `Command::Refuse` as `refuse` for every argument list including the error cases. Added a blank line and the two-line `bxctl deny [--reason TEXT] [--admin-socket PATH]` / `Another name for refuse.` block to `USAGE` after the `refuse` block, keeping the two-space and six-space indents. The copied test's `deny_parses_as_refuse` and `usage_names_deny` both failed before the changes and pass after; `cargo test -p bxctl` is green and `make gate` prints `gate: ok` on the first run. | ? | | M4b/02-proto-gateway-wire | 2026-09-24 | done | 2 | fail | none | Copied `gateway.rs`, `gateway_wire.rs` and the eight `gateway_*.json` fixtures into place; added `pub mod gateway;` after `pub mod frame;` and the re-export to `lib.rs`, the four `Message` variants plus the `use` to `wire.rs`, and the four `kind` arms (changing "fourteen" to "eighteen") in `broker.rs`. Filled `is_mattermost_id` (`id.len() == 26 && every byte a-z0-9`) and `ApprovalReply::check` in the given order: `by`, `post`, then a reason over `MAX_REASON` chars by `chars().count()`. All 3 `gateway_wire` tests pass; the first `make gate` failed on clippy `collapsible_if`, fixed by collapsing the nested `if let` into a let-chain guard, and the second prints `gate: ok`. | ? | | M4b/01-proto-escape | 2026-09-24 | done | 1 | pass | none | Copied `docs/plans/M4b/files/crates/proto/src/escape.rs`, `bxctl/src/escape.rs` and `proto/tests/escape.rs` into place, and added `pub mod escape;` to `proto/src/lib.rs` after `pub mod class;`. The bxctl file is now the one-line re-export `pub use proto::escape::{escape_json_text, escape_model_text};`, so no bxctl source or test changed and behaviour is identical. `cargo test -p proto --test escape` reports 8 passed; every bxctl suite passes unchanged (the moved escape test now runs against `proto::escape`); `cargo fmt --all` then `make gate` prints `gate: ok` on the first run. | ? | | M4a/16-gatewayd-secret-file-race | 2026-09-24 | done | 2 | pass | Reworded the template comment in `read_checked` to drop the literal `check_file` token, so step 5's grep prints only the credential branch's `std::fs::read(&path)` | Replaced `check_file` with `pub fn read_checked(path, between) -> Result>, String>`: check the path (absolute; `symlink_metadata` as `named`; not a symlink; a regular file), call `between()`, open the path, take `opened` metadata, refuse if `(opened.dev(), opened.ino()) != (named.dev(), named.ino())`, then the owner-uid and mode checks on `opened` (not `named`), then read the whole file into a `Zeroizing>` from that same handle. Added `use std::io::Read;`. In `load`, the File branch now calls `read_checked(path, &|| {})` and `value(std::mem::take(&mut *bytes))` so nothing unwiped remains. The three suites pass (5, 8, 5); the only remaining `std::fs::read(` is the credential branch and `check_file` is gone. `make gate` prints `gate: ok`. | ? |