Files
felhom-agent/REPORT.md
T

3.9 KiB
Raw Blame History

REPORT — escrow recovery-code wordlist fix (TASK-E Part 1), 2026-07-21

Version: v0.93.0. Baseline 935904f (v0.92.1), clean tree. NOT built or deployed — no operator action needed today; this rides the next agent publish train.

What was wrong

The EFF large wordlist contains exactly four entries that themselves contain the hyphen the words are joined with: drop-down (L2009), felt-tip (L2528), t-shirt (L6640), yo-yo (L7748). Drawing one produced a recovery code that reads as 11 words instead of 10 — ambiguous to transcribe in the one situation R exists for, a customer reading a code back during a disaster.

The "known flake" was this defect, not a flaky test. Per code the hit rate is 1 (7772/7776)^10 ≈ 0.51%; over the test's 50 draws that is ≈ 23%, which is exactly the "fails ~1/5" that had been documented in the felhom-testing skill and re-run past for weeks. felhom.eu/REPORT.md §6 had it at 3/8 in one session.

The fix

joinSafe filters those four out of the effective wordlist at init. Generation-only.

before after
draw space 7776 7772
10-word code entropy 129.248 bits 129.241 bits
margin over the 128-bit floor 1.248 1.241

The cost is 0.007 bits. Both numbers are asserted in TestEntropyFloorSurvivesFiltering, so a future wordlist swap cannot quietly move the floor.

Every recovery code already issued remains valid, and this was verified rather than assumed: R is consumed as a whole passphrase by the PBS scrypt KDF (Wrap/Unwrap), and grep confirms nothing in the consume path ever splits it. The joiner, the word count and the KDF path are untouched.

Tests

TestGenerateRecoveryCode_EntropyAndFormat now counts words by generation count, not by splitting the joined string — conflating those two is what made it flake. It asserts the segmentation property separately, because that is the property joinSafe actually buys.

New in wordlist_test.go: the four filtered words are pinned by name (a wordlist swap that changes the set fails loudly); the entropy numbers above; and a production-wiring test that drives the exported GenerateRecoveryCode 500×, so the fix is proven at the entry point and not only in the helper.

Red-proof — RUN, both deterministic and probabilistic halves

Pre-fix shape restored (var wordlist = parseWordlist(wordlistRaw)), suite re-run:

--- FAIL: TestWordlistLoaded
    effective wordlist should be 7772 words (7776 EFF - 4 hyphenated), got 7776
--- FAIL: TestGenerateRecoveryCode_EntropyAndFormat
    joined code must segment into 10 words, got 11 (a drawn word contained "-")
--- FAIL: TestEntropyFloorSurvivesFiltering
    filtered entropy moved: 129.248, expected 129.241
--- FAIL: TestGenerateRecoveryCode_NeverContainsAmbiguousWord
    code 193 segmented into 11 parts, want 10

Fix restored → green; git diff clean.

TestGeneratedCodeSegments_FilteredVsUnfiltered is the deterministic companion: it drives the generator against a fixture list where every word is hyphenated, so the pre-fix defect reproduces with probability 1 rather than ~1/5, and shows the same list through joinSafe refuses to generate at all.

Gates

go build ./... && go vet ./... && go test ./...green. The ex-flaky package was additionally run 10× consecutively: 10/10 pass (it previously failed ~1 in 5).

Observations

  • The felhom-testing skill's "Known flake … re-run before diagnosing; it is not a regression" line is now removed. It had been actively harmful: it told every future session to dismiss a true positive. Replaced with the generalised lesson — "known flake, just re-run it" is a diagnosis and needs evidence like any other.
  • WordlistSize() now reports the effective (filtered) draw space, 7772. WordlistFilteredOut() is new, for audit. Any external consumer expecting a literal 7776 would need updating; there is none outside the tests.