Files
archy/.planning/phases/09-botfights-platform-upgrade/deferred-items.md
T
archipelagoandClaude Fable 5 9ad31a7495 docs(09): record demo-day deviations + Cashu fixed-stake feature scoping
Records everything found/fixed live during 09-07's blocking human-verify
checkpoints today (botfights 1.2.2-1.2.8): iframe embed, native signer
bridge, mode-picker discoverability, proxy-URL leaks, round-jump backfill,
broken profile images (CSP), AI-answer discoverability, and the webhook_test
signature exception. Also records the full threat register worked through
for the user-directed Cashu fixed-stake entry-fee request (21 sats, winner
takes all) — mint configured and verified live (Minibits), actual
token-accepting implementation deliberately NOT done yet, scoped as a
follow-up.

Note: this section was written once already earlier in the session and
appears to have been lost to a shared-tree overwrite before it was
committed (CLAUDE.md "Concurrent agent in shared tree" hazard) — recreated
and committed immediately this time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 09:37:40 -04:00

11 KiB

Deferred Items — Phase 9 Plan 01 (arena-proxy tracer)

Out-of-scope discoveries found while executing 09-01-PLAN.md. Not fixed (per Scope Boundary rule — these are pre-existing, unrelated to the files this plan touches).

1. server/src/db/migrate.ts is stale vs server/src/db/schema.ts (blocks a clean local pnpm vitest run --project server)

  • Found during: Task 1/2 verification (pnpm vitest run --project server).
  • Symptom: SqliteError: no such column: "sats_won" (and similar) on a freshly migrated local dev DB (server/data/botfights.db, gitignored). Causes ~14-20 pre-existing test failures in auth.test.ts, auth-audit.test.ts, auth-edge.test.ts, tournaments.test.ts — all going through the real db/index.ts singleton against a table missing columns that schema.ts declares (e.g. satsWon / sats_won in the bots table).
  • Root cause: server/src/db/migrate.ts hand-writes CREATE TABLE IF NOT EXISTS DDL that has drifted out of sync with schema.ts (payments/wallet columns were added to the Drizzle schema without updating the raw migration script). drizzle-kit push also fails against the existing db for the same reason (introspection step queries a column drizzle-kit itself expects to exist).
  • Verified unrelated to this plan's changes: the failing requests never touch arena-proxy.tsARENA_UPSTREAM_URL is unset in the test env, so arenaProxy is a pure next() no-op; the SQL error occurs deep in db.select(...) inside routes/auth.ts/routes/tournaments.ts, completely independent of the proxy mount added in app.ts. Confirmed by reproducing the identical error on the fresh migrated DB before making any arena-proxy changes were involved in that code path.
  • Recommendation: A future plan should either update migrate.ts's DDL to match schema.ts column-for-column, or replace it with real drizzle-kit generated migrations (drizzle-kit generate + drizzle-kit migrate) so the two never diverge again.

2. Timing-sensitive perf tests are flaky under parallel CPU load

  • Found during: same full-suite run.
  • Symptom: answers.test.ts ("completes 1000 checks in under 50ms"), lifecycle.test.ts ("10,000 automated fight simulations", "throughput

    500 fights/second"), bot-auth.test.ts ("constant-time comparison... variance is minimal") intermittently fail when Vitest's parallel workers contend for CPU on this machine. Pre-existing, unrelated to arena-proxy.

  • Recommendation: Not this plan's concern; note if a future hardening pass wants to raise these budgets or mark them --sequential.

Deferred Items — 2026-07-31 demo-day session (09-06/09-07 hotfix cycle)

Found live while verifying 09-07's blocking human checkpoints (real signer login, cross-node visibility, cloud bot from prompt). Fixed in-line where demo-blocking (shipped as botfights 1.2.2-1.2.8, see 09-07-SUMMARY.md deviations once written); these are the remainder — real but not that session's blocker. (This section was lost once already to a shared-tree overwrite mid-session — recreated and committed immediately this time; see CLAUDE.md "Concurrent agent in shared tree".)

3. FightViewer/round display can open mid-fight instead of at round 1

  • Reported: live during demo verification — opening a fight ("botfights training") landed straight on round 5 instead of round 1.
  • Fixed same session (botfight commit ca5b634, part of 1.2.7): FightPage.vue's backfillCompletedRounds()loadFight() already fetched already-completed rounds via GET /api/fights/:id's rounds array, but nothing rendered them into the visible log; only live SSE round_end events ever pushed into liveLogItems. Backfill now renders a compact (non-animated) summary of already-completed rounds and sets HP/round-counter to current state on mount, before wireSSE() connects.
  • Same root cause as the JoinBoutPage/BotProfilePage {{ARENA_URL}} leak (item 5 below) but for a displayed URL string, not fetched content. Fixed same session (botfight commit 2512265, part of 1.2.6): promptUrl now resolves the real arena origin by parsing it out of the fetched prompt's own (correctly proxy-resolved) content, instead of window.location.origin.

5. Client-side "copy AI setup guide" flows leaked proxy-mode local addresses

  • Root cause, found live during a real demo incident: a bot got a Tailscale address (http://100.69.68.39:9100) in its AI setup guide and correctly refused to act on it, suspecting prompt injection — it wasn't injection, it was real output from real code. JoinBoutPage.vue and BotProfilePage.vue fetched the static /docs/BOTFIGHTS.md file and substituted {{ARENA_URL}} client-side with window.location.origin — on a proxy-mode instance (ARENA_UPSTREAM_URL set), that's whatever address the browser happens to be on, not the real externally-reachable arena. Fixed same session (botfight commits 2512265/ffd4dfd, part of 1.2.6): both flows now fetch the server-rendered /api/docs/prompt instead, which is under /api/* and therefore correctly forwarded by arena-proxy to the real upstream arena in proxy mode.

6. Decentralization roadmap: Tor/FIPS-aware standalone-arena addressing

  • User explicitly parked this for later. Today's fix (item 5) solves it for proxy-mode instances by construction — the hub always reports its own correct origin. The gap is standalone arenas (no ARENA_UPSTREAM_URL): server/src/routes/docs.ts falls back to new URL(c.req.url).origin when PUBLIC_ARENA_URL isn't set — same class of problem for an operator only reachable over Tailscale/Tor/FIPS. PUBLIC_ARENA_URL is already the correct escape hatch (explicit config, same pattern as our own ARENA_UPSTREAM_URL); wiring it to FIPS/Tor auto-discovery automatically is new scope, not a bug fix. Design deliberately later, not accidentally now.

7. Decentralization roadmap: arena-less (nostr-event-based) match state

  • Also explicitly deferred. Today's implementation is federated-by-choice (any node can be a hub via unset ARENA_UPSTREAM_URL), not peer-to-peer — one SQLite DB per arena is still the single source of truth for whoever points at it. A genuinely arena-less design (fight results/registrations as signed, independently-verifiable nostr events propagated across many relays, no server authoritative) is a real architecture change. Design later.

8. External bots that "fix" a webhook_test 401 by disabling signature verification entirely

  • Not a bug in this codebase — a warning for the unified prompt's audience. webhook_test (the registration-time verification call) is deliberately unsigned (no BOT_SECRET exists yet at registration). A bot that generalizes "this one request was unsigned" into "disable signature checking entirely" reopens its own webhook to spoofed challenges from anyone, not just the real arena — confirmed real fight deliveries ARE always signed (server/src/engine/orchestrator.ts, secretHash unconditionally stored at registration). Fixed in the doc itself (BOTFIGHTS.md section 4 + Option B example + troubleshooting table, botfight commit ffd4dfd, part of 1.2.6) to special-case type === 'webhook_test' only.

9. Broken profile images — CSP img-src missing https:

  • Reported live: "images in profile and such coming up as broken links." Root cause: secureHeaders()'s CSP img-src only allowed 'self', data:, blob:' — nostr profile pictures come from kind:0 metadata, a URL the user sets via their own client, hosted on whatever domain they picked. There's no central image host for a decentralized identity system. Fixed same session (botfight commit 877d1f6, part of 1.2.8): added https: (broad) to img-src — safe since images can't execute script even from an untrusted origin, unlike script-src, which stays locked to 'self'.

10. AI-answer feature discoverability

  • The new "let BotFights answer for me" feature (1.2.7) was reported as invisible after shipping — it was gated behind picking POLLING (not the default WEBHOOK) AND behind a collapsed toggle within that. Fixed same session (botfight commit 877d1f6, part of 1.2.8): POLLING is now the default connection mode (also BOTFIGHTS.md's own documented default), the AI section is expanded by default, and the POLLING button's own description mentions the option.

11. Cashu fixed-stake entry fee ("winner takes all, 21 sats each, only ever") — SCOPED, NOT IMPLEMENTED

  • User-directed feature request, explicitly asked to be scoped properly before any code, given real bearer-instrument money is involved.
  • What exists today: server/src/engine/payments.ts only mints outbound cashu tokens to pay a winner from the arena's own configured mint. There is no code path that accepts a user-submitted token as an entry stake — this is genuinely new work, not a wire-up.
  • Mint chosen and verified live (not guessed): Minibits, https://mint.minibits.cash/Bitcoin — confirmed NUT-4 (mint, bolt11/sat), NUT-5 (melt), NUT-7 (spend-check), NUT-11 (P2PK) all present via its /v1/info endpoint. BOTFIGHTS_CASHU_MINT_URL set on the canonical arena (botfight commit 7341ca0) — config-only, moves no funds by itself (the existing payout branch is only reached from ranked-mode fights, which still requires the still-unset BOTFIGHTS_NWC_URL).
  • Threat register worked through before any implementation:
    1. Bearer-token custody window — must swap a posted token into server-owned proofs immediately on receipt (NUT-7), never hold the raw wire-format token as state (crash-loses-money risk otherwise).
    2. Mint trust — inherent to any Cashu design; mitigated by choosing an established, verified-live mint whose own operators recommend small amounts (matches the 21-sat cap).
    3. Amount enforcement — must hard-reject anything that isn't exactly 21 sats, server-side, at submission — the whole point of the cap is bounding any bug's blast radius to a trivial amount.
    4. Double-posting / replay — must validate/swap with the mint (NUT-7) at submission time, not trust the token string at face value.
    5. Refund path — an unmatched/cancelled/timed-out stake must return to its poster; no refund path = silent fund loss on the (common, for anonymous poll-mode bots) timeout case.
    6. Payout destination — resolved: NUT-11 P2PK lets a payout be locked to the winner's own pubkey with no interactive receive step required.
    7. Logging hygiene — a raw token string is money; same rule as API keys, never logged, never in error messages.
  • Recommendation for the actual implementation (not done): a new escrow-and-swap flow (post token → validate exact amount + NUT-7 check → immediate mint-side swap to server custody → hold server-owned proof reference in DB, never the raw token → on fight resolution, mint a fresh 42-sat P2PK token to the winner's pubkey → on timeout/cancellation, mint a refund token back to the poster's pubkey if they provided one, or require pubkey-at-submission specifically so a refund destination always exists). This should be its own properly-planned unit of work, not folded into an already-massive hotfix batch.