Files
felhom-agent/REPORT.md
T

80 lines
3.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.