diff --git a/CHANGELOG.md b/CHANGELOG.md index 7929db7a..f458ff5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ Notable changes between releases. Detailed migration notes for storage transitio ### Fixed +- **CLI help no longer prints callback signing secrets.** `awa serve --help` and `awa callbacks serve --help` identify `AWA_CALLBACK_HMAC_SECRET` without rendering its current value. Root help already keeps the manually resolved `DATABASE_URL` out of Clap output; regression coverage now protects all three credential-bearing help surfaces. + - **Nightly flake gates now carry runner-contention margin ([#399](https://github.com/hardbyte/awa/issues/399), [#434](https://github.com/hardbyte/awa/issues/434)).** Four assertion shapes in the chaos and benchmark suites were tight enough that shared-runner CPU contention failed them while every invariant they exist for was intact, eroding the 14-consecutive-green-nightlies release gate. `awa/tests/ci_timing.rs` now holds the scaling for all of them, and it only ever loosens a bound, and only when `CI` is set: - The mixed-fleet chaos test set `heartbeat_staleness` to 250ms against a 50ms heartbeat interval. Under contention a *live* worker's heartbeat missed that window, so the runtime correctly rescued a healthy attempt and the test saw a genuine duplicate completion — a margin problem that read as a correctness bug. Chaos clients scale the staleness window via `scaled_staleness` while leaving the heartbeat and rescue *intervals* at chaos cadence, so the rescue path is still exercised. - That test's assert-after-drain step watched both completion streams for a fixed 250ms quiet window, the same race [#335](https://github.com/hardbyte/awa/issues/335) fixed in `test_weight_proportionality`: a duplicate arriving at 251ms was missed, and on a slow runner the window expired while work was still moving. It now waits for the queue itself to reach a terminal state — the authoritative drain signal — then drains both streams with `try_recv`, so duplicate detection no longer depends on wall-clock timing. diff --git a/awa-cli/src/main.rs b/awa-cli/src/main.rs index 7b179128..cdfae683 100644 --- a/awa-cli/src/main.rs +++ b/awa-cli/src/main.rs @@ -130,7 +130,7 @@ enum Commands { #[arg(long, default_value = "5", env = "AWA_CACHE_TTL")] cache_ttl: u64, /// Hex-encoded 32-byte key used to verify callback signatures. - #[arg(long, env = "AWA_CALLBACK_HMAC_SECRET")] + #[arg(long, env = "AWA_CALLBACK_HMAC_SECRET", hide_env_values = true)] callback_hmac_secret: Option, /// Force the server into read-only mode regardless of DB privilege. /// @@ -213,7 +213,7 @@ enum CallbackCommands { pool_acquire_timeout: u64, /// Hex-encoded 32-byte key used to verify callback signatures. /// Required unless `--allow-unsigned` is set. - #[arg(long, env = "AWA_CALLBACK_HMAC_SECRET")] + #[arg(long, env = "AWA_CALLBACK_HMAC_SECRET", hide_env_values = true)] callback_hmac_secret: Option, /// Path prefix the callback routes are mounted under. Defaults to /// `/api/callbacks`, matching the built-in `awa serve` layout. diff --git a/awa-cli/tests/help_cli_test.rs b/awa-cli/tests/help_cli_test.rs new file mode 100644 index 00000000..1b15397e --- /dev/null +++ b/awa-cli/tests/help_cli_test.rs @@ -0,0 +1,46 @@ +use assert_cmd::Command; + +fn rendered_help(args: &[&str], env_name: &str, secret: &str) -> String { + let output = Command::cargo_bin("awa") + .expect("awa binary") + .env(env_name, secret) + .args(args) + .output() + .expect("run awa help command"); + + assert!(output.status.success()); + format!( + "{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ) +} + +#[test] +fn help_does_not_disclose_environment_database_url() { + let secret_url = "postgres://secret-user:secret-password@db.example/awa"; + let rendered = rendered_help(&["--help"], "DATABASE_URL", secret_url); + assert!(rendered.contains("DATABASE_URL")); + assert!(!rendered.contains(secret_url)); + assert!(!rendered.contains("secret-password")); +} + +#[test] +fn serve_help_does_not_disclose_callback_secret() { + let secret = "serve-callback-secret-sentinel"; + let rendered = rendered_help(&["serve", "--help"], "AWA_CALLBACK_HMAC_SECRET", secret); + assert!(rendered.contains("AWA_CALLBACK_HMAC_SECRET")); + assert!(!rendered.contains(secret)); +} + +#[test] +fn callback_receiver_help_does_not_disclose_callback_secret() { + let secret = "receiver-callback-secret-sentinel"; + let rendered = rendered_help( + &["callbacks", "serve", "--help"], + "AWA_CALLBACK_HMAC_SECRET", + secret, + ); + assert!(rendered.contains("AWA_CALLBACK_HMAC_SECRET")); + assert!(!rendered.contains(secret)); +}