From 9ad31a7495792433fb452695457924573e584e78 Mon Sep 17 00:00:00 2001 From: archipelago Date: Fri, 31 Jul 2026 09:37:40 -0400 Subject: [PATCH] docs(09): record demo-day deviations + Cashu fixed-stake feature scoping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../deferred-items.md | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/.planning/phases/09-botfights-platform-upgrade/deferred-items.md b/.planning/phases/09-botfights-platform-upgrade/deferred-items.md index 74997e4c..e99a8695 100644 --- a/.planning/phases/09-botfights-platform-upgrade/deferred-items.md +++ b/.planning/phases/09-botfights-platform-upgrade/deferred-items.md @@ -41,3 +41,151 @@ touches). 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. + +## 4. `DocsPage.vue`'s `promptUrl` display link was proxy-unaware + +- 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.