Say so at startup when a security-relevant alert is unconfigured, instead of skipping silently #107

Closed
opened 2026-08-22 11:59:44 +00:00 by mathias · 1 comment
Owner

Small hardening, from the #100 post-mortem.

What happened

buildNotifier() returns a nil interface when BRAIN_NTFY_URL is unset, and every consumer skips a nil notifier silently. That is correct for an unconfigured homelab — the service should run without a notification stack. It is wrong for an alert whose whole purpose is that someone finds out.

Two alerts were dark in production and nothing said so: the governance-rewrite alert (#97) and the audit-degradation alert (DegradingSink), the latter since it shipped. Both were found by firing a real event and watching nothing arrive.

The cost of the current design is that "configured and working" and "never configured" are byte-identical in behaviour: no error, no warning, no log line. The only difference is a message that never arrives, which nobody notices, because nobody is waiting for an alert they have never seen.

Proposal

At startup, log at WARN once per unconfigured alerting dependency:

governance alerting DISABLED: BRAIN_NTFY_URL unset — brain_edit_root and audit
degradation will not notify

Not an error, not a refusal to start: an unconfigured homelab must still boot. Just a line that makes the absence visible at the only moment anyone is reading the logs anyway.

  • caveman: machine say out loud when alarm not plugged in
  • WARN at startup when BRAIN_NTFY_URL is unset, naming what will not fire
  • Same treatment for any other optional dependency whose failure mode is silence — BRAIN_LOKI_URL is the obvious sibling (audit records fall back to a slog sink, which is fine, but should be stated)
  • Test: a config with no notifier logs the warning; a config with one does not

Why it is worth the ten lines

One log line would have turned a live-fire test into a five-second discovery. The general rule is recorded at brain wiki/hyperguild/failures/a-nil-notifier-is-indistinguishable-from-an-alert-that-never-fires: for anything whose value is "someone finds out", prefer loud-on-unconfigured over silent skip.

Risk: LOW — logging only.

Refs: #100, #97.

Small hardening, from the #100 post-mortem. ## What happened `buildNotifier()` returns a **nil interface** when `BRAIN_NTFY_URL` is unset, and every consumer skips a nil notifier silently. That is correct for an unconfigured homelab — the service should run without a notification stack. It is wrong for an alert whose whole purpose is that someone finds out. Two alerts were dark in production and nothing said so: the governance-rewrite alert (#97) and the audit-degradation alert (`DegradingSink`), the latter since it shipped. Both were found by firing a real event and watching nothing arrive. The cost of the current design is that **"configured and working" and "never configured" are byte-identical in behaviour**: no error, no warning, no log line. The only difference is a message that never arrives, which nobody notices, because nobody is waiting for an alert they have never seen. ## Proposal At startup, log at WARN once per unconfigured alerting dependency: ``` governance alerting DISABLED: BRAIN_NTFY_URL unset — brain_edit_root and audit degradation will not notify ``` Not an error, not a refusal to start: an unconfigured homelab must still boot. Just a line that makes the absence visible at the only moment anyone is reading the logs anyway. - [ ] `caveman: machine say out loud when alarm not plugged in` - [ ] WARN at startup when `BRAIN_NTFY_URL` is unset, naming what will not fire - [ ] Same treatment for any other optional dependency whose failure mode is silence — `BRAIN_LOKI_URL` is the obvious sibling (audit records fall back to a slog sink, which is fine, but should be stated) - [ ] Test: a config with no notifier logs the warning; a config with one does not ## Why it is worth the ten lines One log line would have turned a live-fire test into a five-second discovery. The general rule is recorded at brain `wiki/hyperguild/failures/a-nil-notifier-is-indistinguishable-from-an-alert-that-never-fires`: for anything whose value is "someone finds out", prefer loud-on-unconfigured over silent skip. **Risk:** LOW — logging only. Refs: #100, #97.
mathias added the risk/lowsize/xstype/feat labels 2026-08-27 05:25:42 +00:00
mathias added the agent/ready label 2026-08-27 05:28:18 +00:00
Author
Owner

Merged via PR #140 (squash, merge commit c6341f020ab781179d2a642b1796f2a49d45820c), a rebase of the now-closed PR #108 onto current main. Verification performed before merge:

  1. Byte-identical rebase — extracted alertingWarnings and TestAlertingWarnings from PR #108's original commit (1ff6af4a...) and diffed against #140's HEAD (67c30394...): zero difference in either the function body/comments or the test. The main() call site (for _, w := range alertingWarnings(os.Getenv) { logger.Warn(w) }) is also identical.
  2. No feature reverted — grepped current main.go on the branch: BRAIN_LLM_TERTIARY_URL/Tertiary router block (lines 274-277) and CLAUDE_INGEST_SKIP_PROJECTS (line 452) are both present, and neither appears in the PR's diff (confirmed the diff touches only the new alertingWarnings func + call site + test).
  3. BRAIN_LOKI_URL reasoning verified independently — read buildAuditSink (main.go:193-210): unset BRAIN_LOKI_URL already logs "capture audit: slog sink (BRAIN_LOKI_URL unset)" at Info and returns audit.NewSlogSink, which is a genuinely functional sink (Reserve always grants, Record writes every audit entry via slog) — not a stub. So unlike BRAIN_NTFY_URL unset (a true silent failure — no code path announces it), Loki-unset is already stated. Distinguishing the two is sound, not an oversight.
  4. Flip-tested TestAlertingWarnings — mutated alertingWarnings to always return the warning: the "configured notifier is silent" case failed as expected. Mutated it to never return a warning: the "notifier unconfigured" case failed on all three required substrings (BRAIN_NTFY_URL, brain_edit_root, audit). Both mutations caught; test reverted to baseline afterward (working tree left clean).
  5. Full gate: go build ./..., go vet ./..., go test ./... -race -count=1 — all 31 testable packages green, zero FAIL lines.
  6. CI: commit status success — CI / Lint / Test / Vet run 7947, 36s.

All "done when" clauses satisfied: WARN-at-startup naming BRAIN_NTFY_URL, brain_edit_root, audit degradation; BRAIN_LOKI_URL sibling addressed with a verified, sound justification rather than silently skipped; test covers both configured and unconfigured cases and was proven to actually discriminate them.

Merged via PR #140 (squash, merge commit `c6341f020ab781179d2a642b1796f2a49d45820c`), a rebase of the now-closed PR #108 onto current main. Verification performed before merge: 1. **Byte-identical rebase** — extracted `alertingWarnings` and `TestAlertingWarnings` from PR #108's original commit (`1ff6af4a...`) and diffed against #140's HEAD (`67c30394...`): zero difference in either the function body/comments or the test. The `main()` call site (`for _, w := range alertingWarnings(os.Getenv) { logger.Warn(w) }`) is also identical. 2. **No feature reverted** — grepped current `main.go` on the branch: `BRAIN_LLM_TERTIARY_URL`/`Tertiary` router block (lines 274-277) and `CLAUDE_INGEST_SKIP_PROJECTS` (line 452) are both present, and neither appears in the PR's diff (confirmed the diff touches only the new `alertingWarnings` func + call site + test). 3. **BRAIN_LOKI_URL reasoning verified independently** — read `buildAuditSink` (main.go:193-210): unset `BRAIN_LOKI_URL` already logs `"capture audit: slog sink (BRAIN_LOKI_URL unset)"` at Info and returns `audit.NewSlogSink`, which is a genuinely functional sink (`Reserve` always grants, `Record` writes every audit entry via slog) — not a stub. So unlike `BRAIN_NTFY_URL` unset (a true silent failure — no code path announces it), Loki-unset is already stated. Distinguishing the two is sound, not an oversight. 4. **Flip-tested `TestAlertingWarnings`** — mutated `alertingWarnings` to always return the warning: the "configured notifier is silent" case failed as expected. Mutated it to never return a warning: the "notifier unconfigured" case failed on all three required substrings (`BRAIN_NTFY_URL`, `brain_edit_root`, `audit`). Both mutations caught; test reverted to baseline afterward (working tree left clean). 5. **Full gate**: `go build ./...`, `go vet ./...`, `go test ./... -race -count=1` — all 31 testable packages green, zero FAIL lines. 6. **CI**: commit status `success` — `CI / Lint / Test / Vet` run 7947, 36s. All "done when" clauses satisfied: WARN-at-startup naming `BRAIN_NTFY_URL`, `brain_edit_root`, audit degradation; BRAIN_LOKI_URL sibling addressed with a verified, sound justification rather than silently skipped; test covers both configured and unconfigured cases and was proven to actually discriminate them.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mathias/hyperguild#107