Findings that mattered
The six critical red-team findings with their fixes and tests, the high findings by theme, and what is still open.
The repository was reviewed in repeated adversarial passes: red-team rounds 1 to 13, plus separate audit passes. Every finding is a GitHub issue with a file, a line, a scenario and a suggested fix. This page shows the six findings labelled critical, the 96 labelled high grouped by theme, and the numbers behind both. It is a snapshot taken on 7 Nov 2026. Titles are quoted from the issues; nothing here is secret, because the issues are public.
The numbers
Counted with gh on 7 Nov 2026 (queries at the end of the page).
- 606 issues carry the
red-teamlabel. 589 are closed and 17 are open. - Of those 606, 37 are batch or epic issues that track the fix work (their titles start with
FEATURE:orEPIC). The other 569 are individual findings: 560 closed, 9 open. - Severity labels on the 569 findings: 6 critical (6 closed), 96 high (93 closed), 210 medium (208 closed), 97 low (95 closed). 160 findings have no severity label (158 closed). Later rounds mostly did not set one.
- Three further label sets are separate from
red-teamand are not in the 606:audit-r1has 27 issues (23 closed),audit-r2has 33 (all closed) andredteam-r4has 68 (67 closed).
"Closed" means the issue is closed after a fix PR merged. The count does not separate issues closed by a code change from any closed as a duplicate or as not real, so it is not a count of code changes.
Found and fixed per round
A round is the tag in the issue title: RT for the first pass, then R2 to R13. The three labelled sets are shown at the bottom.
Severity split
The six critical findings
There are six critical issues but five distinct defects, because #698 and #700 report the same break. All six issues were found by reading the code in a red-team round, and each cites file and line. All six are closed. None was found by a failing test; in the web cases the existing tests passed because they only used the web package's own copy of the contract.
1. The web app could not talk to the server
- What was wrong: the web package kept its own copy of the wire protocol. Every frame it sent lacked the
vfield and used different names, so the server closed the socket. Every frame the server sent failed the web guard and was dropped without an error. A live room could never render and no bid could be placed. - Found: red-team round 1, on 4 Nov 2026.
- Issue: #700, and the same break seen from the integration side in #698.
- Fix: PR #869 deletes the local copy and builds on
@gavel/shared. #698 was closed by PR #1477. - Regression test:
delivers frames encoded by the shared codecanddoes not send a message the shared schema rejectsinpackages/web/test/unit/connection.test.tsx. - Evidence: issue #700, issue #698, PR #869.
2. Registration called hold routes that did not exist
- What was wrong: the web adapter posted to
/api/rooms/{roomId}/ordersand/holds, but the payments contract definesPOST /api/holdsandPOST /api/holds/{holdId}/approve. Every registration would have hit a 404 and shownorder_failed. The adapter also sent the bidder id and the ceiling from the browser, so the browser chose the hold amount. - Found: red-team round 1, on 4 Nov 2026.
- Issue: #699.
- Fix: PR #1477, merged 5 Nov 2026. The adapters call the contract routes with the shared schemas and send no bidder id.
- Regression test:
sends bodies the shared hold contract accepts and names no bidder (WEB-RT-03)inpackages/web/test/unit/paypalAdapter.test.ts, plus the schema tests inpackages/shared/test/hold-contract.test.ts. - Evidence: issue #699, PR #1477.
3. Three frames shared one sequence number and all but one were dropped
- What was wrong: the server sends
bid_accepted,priceandaudit_eventwith the sameseq. The web reducer discarded any frame withseq <= lastSeq, so after the first one the others were lost. A bidder's own screen kept the old price and showed the wrong status. Other clients never received a live audit row. An existing test,replay of seq [1,2,2,3] equals [1,2,3], encoded the wrong rule. - Found: red-team round 2, on 4 Nov 2026.
- Issue: #890.
- Fix: PR #1017. The reducer dedupes on the pair (seq, type), not on seq alone.
- Regression test:
applies bid_accepted, price and audit_event that share one seqinpackages/web/test/unit/roundTwoBlocking.test.ts. A second test in the same file,still drops a repeat of the same type at one seq, keeps the original protection. - Evidence: issue #890, PR #1017.
4. A fallback sale could be charged but never booked
- What was wrong: when the winner's capture was declined, the saga captured the runner-up. The engine still had the first bidder as winner, so
SettlementBridge.finishthrewwinner_mismatch. The runner-up had been charged, the lot stayed in SETTLING forever,CloseAuctionwas never sent and every resume repeated the same error. - Found: red-team round 3, on 4 Nov 2026.
- Issue: #1060.
- Fix: PR #1134, merged 4 Nov 2026.
finish()promotes the engine through the journal's failed bidders until the engine winner equals the charged bidder. - Regression test:
promotes the engine to the charged runner-up, completes the lot and closesandresumes after a crash that happened between the promote and CompleteLotinpackages/payments/test/bridge/settlement-bridge.test.ts. - Evidence: issue #1060, PR #1134.
5. One bad registration could make an auction impossible to load
- What was wrong:
Room.registerBidderappended the registration to the log first and asked the engine afterwards. A ceiling the engine refuses (above its maximum) was therefore stored, the call threw, and every later replay hit the same throw. The room could never load again, and neither could its settlement or any other bidder's hold. - Found: red-team round 3, on 4 Nov 2026.
- Issue: #1061.
- Fix: PR #1139, merged 4 Nov 2026. The engine registration runs before the append, and replay skips a stored registration the engine refuses so an already poisoned log still loads.
- Regression test:
does not store a registration the engine refuses, so replay still loads (SERVICE-R3-01)andskips an already stored registration the engine refuses instead of failing the load (SERVICE-R3-01)inpackages/server/test/room.test.ts. - Evidence: issue #1061, PR #1139.
High findings by theme
The 96 high findings are grouped by the main thing each one was about. Each issue is counted once, so the groups add up to 96. Two readers could file a few of them differently, so treat the split as a guide. The "open" column counts issues still open today.
| Theme | High | Open | Two examples | Filter |
|---|---|---|---|---|
| Money (holds, captures, prices, voids, remainders) | 19 | 0 | #718: Fake invariant allows capturing above the hold the buyer authorized #1082: Check and release the winner's authorization remainder after capture | filtered list |
| Idempotency, retry and crash safety | 12 | 0 | #892: Stop PromoteRunnerUp retries from failing with command_conflict when the promotion ends unsold #1081: Recover the ledger file from a failed partial write and fail on mid-file corruption | filtered list |
| Auth, tokens and privacy of ceilings | 6 | 0 | #727: Check jti replay and denylist, which verifyToken never consults #724: Redact ceilings for every non-owner, not only for screens | filtered list |
| Webhooks and settlement events | 3 | 0 | #1064: Make webhook application crash-safe: hold status is saved before the engine revoke, so a crash drops the revoke permanently #717: The settlement runner, hold service, real PayPal client, reconciler, webhook processor and fake HTTP server do not exist | filtered list |
| Voice | 12 | 0 | #731: Gate ASR repair so a trailing 'for'/'to' after a number is not turned into a digit #1327: The room marks every bid as confirmed before it reaches the engine, so a voice bid with any confidence skips the confirm gate | filtered list |
| Realtime and the web client | 17 | 0 | #722: Ship the room host, HTTP routes, store and ports that the spec requires #911: Private frames consume global seqs, so every rejected bid or registration forces all other clients to resync | filtered list |
| CI and tests | 7 | 0 | #719: Required CI jobs pass vacuously: secrets, scenarios and e2e are echo stubs #901: Make the sandbox secrets unreachable: they are repository secrets and the protecting environment does not exist | filtered list |
| Wire contract and integration | 18 | 3 | #708: Engine cannot record or settle: five commands throw bad_command #896: Move the hold route schemas into shared: web cannot import the payments contract the docs say it uses | filtered list |
| Engine and ledger | 2 | 0 | #1063: Wire AbortLot to a caller: nothing sends the kill-switch cancel, so the pre-hammer void path does not exist at runtime #1188: The ledger export, verifyJsonl and the verify-ledger command in the spec do not exist | filtered list |
The filter links open the GitHub issue list for high red-team issues with a keyword match. A keyword match is looser than the grouping above, so its count will not equal the table. The exact issue numbers per theme:
- Money (holds, captures, prices, voids, remainders): #701, #704, #706, #707, #715, #718, #736, #737, #900, #914, #1062, #1066, #1067, #1068, #1071, #1076, #1082, #1217, #1378
- Idempotency, retry and crash safety: #891, #892, #898, #905, #1065, #1070, #1075, #1077, #1079, #1080, #1081, #1209
- Auth, tokens and privacy of ceilings: #724, #725, #726, #727, #728, #1273
- Webhooks and settlement events: #717, #894, #1064
- Voice: #702, #731, #732, #733, #734, #735, #908, #909, #910, #1327, #1445, #1447
- Realtime and the web client: #722, #723, #729, #730, #738, #739, #740, #904, #906, #907, #911, #912, #913, #915, #916, #1069, #1078
- CI and tests: #719, #720, #721, #901, #902, #903, #1271
- Wire contract and integration: #703, #705, #708, #709, #710, #711, #712, #713, #714, #716, #893, #895, #896, #897, #899, #1072, #1073, #1074
- Engine and ledger: #1063, #1188
Money, realtime state and the wire contract between packages hold most of the high findings. Webhooks are a small group (3). Seven were about CI that passed without testing anything, which is the kind of problem a green badge hides.
What is still open
Open today: 17 red-team issues, of which 8 are batch trackers and 9 are findings. The 9 findings are 3 high (#710, #712, #1074), 2 medium (#760, #959), 2 low (#824, #986) and 2 with no severity label (#1598, #1602).
Outside the red-team label, 4 audit-r1 issues and 1 redteam-r4 issue are open. One of them is rated high: #1708, the auction HTTP routes are not mounted in the running server. These are gaps between modules that exist and what the running server wires up, not hidden defects.
Closed does not mean proven correct. A closed finding has a merged fix and, where the issue asked for one, a regression test. It has not been checked by an independent reviewer, and the real PayPal sandbox gates are tracked separately in the evidence index.
How these numbers were computed
All counts come from the GitHub issue tracker with the gh CLI, run on 7 Nov 2026:
R=harshbhx/gavel-auction
# totals and open counts per label
gh issue list -R $R -l red-team -s all -L 2000 --json number --jq length # 606
gh issue list -R $R -l red-team -s open -L 2000 --json number --jq length # 17
for l in audit-r1 audit-r2 redteam-r4; do
gh issue list -R $R -l $l -s all -L 2000 --json number --jq length
gh issue list -R $R -l $l -s open -L 2000 --json number --jq length
done
# severity, within red-team
for s in critical high medium low; do
gh issue list -R $R -l red-team -l severity:$s -s all -L 2000 --json number --jq length
gh issue list -R $R -l red-team -l severity:$s -s open -L 2000 --json number --jq length
done
# every red-team issue with its title, state and dates, used for the rounds and trackers
gh issue list -R $R -l red-team -s all -L 2000 --json number,title,state,labels,createdAt,closedAt
# fix PRs: the cross-referenced events on each critical issue
gh api repos/$R/issues/<n>/timeline --paginate
gh pr view <pr> -R $R --json title,state,mergedAt,bodyRules applied to that data: batch issues are those whose title starts with FEATURE: or EPIC; the round is the first tag in the title (RT, R2 to R13, and R7P counted in round 7); an issue counts as fixed when it is closed. The theme of each high finding was assigned by reading its title and body. Test names were read from the added lines of each fix PR's diff and checked against main.
Last updated on