docs(09-04): resolve arena decisions — plain-HTTP :9100 endpoint (no DNS/TLS), full DB copy seed

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago 2026-07-30 21:09:15 -04:00
parent 6a112797a5
commit de022b7b4e
2 changed files with 51 additions and 71 deletions

View File

@ -0,0 +1 @@

View File

@ -7,26 +7,20 @@ depends_on: []
files_modified:
- /home/archipelago/Projects/botfight/docker-compose.arena.yml
- /home/archipelago/Projects/botfight/docs/arena-deployment.md
autonomous: false
autonomous: true
requirements: [BOT-03]
user_setup:
- service: godaddy-dns
why: "The canonical arena needs a public hostname; archipelago-foundation.org is on GoDaddy nameservers (ns29/ns30.domaincontrol.com) with no wildcard record, so a new A record must be created by the domain owner"
dashboard_config:
- task: "Create an A record for the arena subdomain pointing at 146.59.87.168"
location: "GoDaddy DNS management for archipelago-foundation.org"
- service: nginx-proxy-manager
why: "TLS termination and public routing on VPS2 live in NPM's own database, not in any git repo"
dashboard_config:
- task: "Add a Proxy Host: the arena subdomain -> http 146.59.87.168:9100, websockets enabled, SSL forced, request a new Let's Encrypt certificate"
location: "http://146.59.87.168:81 (admin lfg2025@proton.me)"
# user_setup RESOLVED 2026-07-30: user chose NO DNS/TLS — canonical arena URL is
# http://146.59.87.168:9100 (plain HTTP on the raw port). The node→arena hop is
# server-side so there is no mixed-content issue; a TLS subdomain can be added later
# by changing only ARENA_UPSTREAM_URL (env), no code change. Data seed: FULL COPY
# of archi-dev-box's 351 MB botfights.db (user decision, option-c data).
must_haves:
truths:
- "One canonical BotFights arena runs on VPS2 (146.59.87.168) in standalone mode — it owns the only real match/fighter database (D-03/BOT-03)"
- "The arena answers /api/health on the VPS2 host before any DNS or TLS work begins, so a later public failure is unambiguously a routing problem and not an app problem"
- "The arena's JWT signing secret is generated on the VPS2 host, stored 0600 outside git, and never appears in a tracked file or in a log"
- "The arena trusts the forwarding headers nginx-proxy-manager sets, so per-IP rate limiting sees real client IPs and not one bucket for the whole internet"
- "The arena rate-limits on the direct socket peer IP (no reverse proxy in front — TRUSTED_PROXY deliberately unset), so per-IP limits see real client IPs"
- "Lightning/cashu payment features are left unconfigured on the public arena — they are explicitly out of this phase's scope"
- "How to redeploy this arena from scratch is written down in the repo, because NPM's routing and the host .env are not git-tracked artifacts"
artifacts:
@ -37,15 +31,16 @@ must_haves:
provides: "The runbook: host paths, port, NPM proxy-host values, DNS record, secret handling, rollback"
contains: "146.59.87.168"
key_links:
- from: nginx-proxy-manager on VPS2 (:80/:443)
to: the canonical arena container on 146.59.87.168:9100
via: "an NPM proxy host forwarding http to 146.59.87.168:9100 with websockets on and SSL forced — the same shape as the existing demo/source/fips/companion hosts"
- from: public internet (node instances + cloud bots)
to: the canonical arena container on VPS2
via: "direct http://146.59.87.168:9100 (user-chosen: no DNS/NPM/TLS; port 9100 published by docker and open through the host firewall)"
pattern: "9100"
---
<objective>
Stand up the one canonical BotFights arena on VPS2 and give it a public, TLS-terminated hostname —
the shared endpoint every node's instance will proxy to (D-03/BOT-03).
Stand up the one canonical BotFights arena on VPS2 at `http://146.59.87.168:9100`
the shared endpoint every node's instance will proxy to (D-03/BOT-03). (User decision 2026-07-30:
no DNS/TLS; plain HTTP on the raw port, TLS is a later env-only upgrade.)
Decision IDs map to `09-CONTEXT.md` **Locked Decisions**: D-01 = BOT-01 signer login,
D-02 = BOT-02 unified prompt, D-03 = BOT-03 shared public match endpoint on VPS2,
@ -94,8 +89,8 @@ the rootless-podman invariant applies to Archipelago nodes, not to this host.
<tasks>
<task type="checkpoint:decision" gate="blocking">
<name>Task 1: Decide the arena hostname and what data the public arena starts with</name>
<task type="checkpoint:decision" gate="blocking" resolved="2026-07-30">
<name>Task 1: Decide the arena hostname and what data the public arena starts with — RESOLVED: no hostname (http://146.59.87.168:9100 direct), full database copy</name>
<decision>What hostname does the canonical arena use, and what data does it start with?</decision>
<context>
Two choices must be made before the arena is deployed, because both are baked into the compose
@ -146,7 +141,7 @@ the rootless-podman invariant applies to Archipelago nodes, not to this host.
<cons>Must be decided now — it is written into the signed catalog in plan 09-06</cons>
</option>
</options>
<resume-signal>Reply with the chosen option id (and the hostname if option-d), e.g. "option-b"</resume-signal>
<resume-signal>RESOLVED by user 2026-07-30 via AskUserQuestion: (1) hostname — "can we do it a different way without needing to do that?" → no DNS record; the canonical arena URL is `http://146.59.87.168:9100` directly (server-side proxy hop, no mixed content; TLS subdomain is a later env-only upgrade). (2) data — "Full copy" → copy the entire 351 MB botfights.db from archi-dev-box (read-only export; source file untouched). Do NOT re-ask.</resume-signal>
</task>
<task type="auto">
@ -180,8 +175,10 @@ the rootless-podman invariant applies to Archipelago nodes, not to this host.
built and pushed) and keep the tag in one place so the roll is a one-line edit;
- bind `9100:9100`, `restart: unless-stopped`, container name `botfights-arena`, named volume
`botfights-arena-data:/app/server/data`;
- set `NODE_ENV=production`, `PORT=9100`, `TRUSTED_PROXY=1`, `FIGHT_LOOP_ENABLED=true`,
`PUBLIC_ARENA_URL=https://<chosen hostname>`, `JWT_SECRET=${JWT_SECRET}` and
- set `NODE_ENV=production`, `PORT=9100`, `FIGHT_LOOP_ENABLED=true`,
`PUBLIC_ARENA_URL=http://146.59.87.168:9100`, `JWT_SECRET=${JWT_SECRET}` and
OMIT `TRUSTED_PROXY` (no NPM in front — clients hit :9100 directly, so the arena must
use the socket peer IP for rate limiting, not forwarded headers), and
`BOTFIGHTS_CREATOR_PUBKEYS=${BOTFIGHTS_CREATOR_PUBKEYS:-da5e0c1b646bdb13c2300f805b0ca3e5afe5b052c594ce78bac8978d21c3fa39}`;
- deliberately omit `ARENA_UPSTREAM_URL` (this instance IS the upstream) and omit every
`BOTFIGHTS_WALLET_ENCRYPTION_KEY` / `BOTFIGHTS_NWC_URL` / `BOTFIGHTS_CASHU_MINT_URL` /
@ -219,7 +216,7 @@ the rootless-podman invariant applies to Archipelago nodes, not to this host.
- `ssh debian@146.59.87.168 'curl -fsS http://127.0.0.1:9100/api/health'` returns the ok payload.
- `ssh debian@146.59.87.168 'sudo docker ps --filter name=botfights-arena --format "{{.Status}}"'` shows `Up` and healthy.
- `ssh debian@146.59.87.168 'stat -c %a /opt/botfights-arena/.env'` prints `600`.
- `ssh debian@146.59.87.168 'sudo docker inspect botfights-arena --format "{{json .Config.Env}}"' | grep -q 'TRUSTED_PROXY=1'` succeeds and the same output contains no `ARENA_UPSTREAM_URL` entry.
- `ssh debian@146.59.87.168 'sudo docker inspect botfights-arena --format "{{json .Config.Env}}"'` contains no `ARENA_UPSTREAM_URL` entry and no `TRUSTED_PROXY` entry (direct exposure — rate limit on socket peer IP).
- `grep -c 'BOTFIGHTS_WALLET_ENCRYPTION_KEY\|BOTFIGHTS_NWC_URL\|BOTFIGHTS_CASHU_MINT_URL' /home/archipelago/Projects/botfight/docker-compose.arena.yml` counts only commented lines, and no such variable is present in the container's env output.
- `git -C /home/archipelago/Projects/botfight grep -c 'JWT_SECRET=' -- docker-compose.arena.yml` shows only the `${JWT_SECRET}` indirection, never a literal value.
- `test -f /home/archipelago/Projects/botfight/docs/arena-deployment.md` and it contains the chosen hostname, `9100`, and the NPM field values.
@ -228,50 +225,32 @@ the rootless-podman invariant applies to Archipelago nodes, not to this host.
<done>A canonical arena container is running and healthy on VPS2 with a host-generated secret, correct proxy-trust settings, no payment configuration, and the agreed starting data.</done>
</task>
<task type="checkpoint:human-action" gate="blocking">
<name>Task 3: Create the arena DNS record and the nginx-proxy-manager host with a Let's Encrypt certificate</name>
<what-built>
The canonical BotFights arena is live on VPS2 at `http://146.59.87.168:9100` and answering
`/api/health`. Two things remain that have no CLI or API path available to Claude: the DNS
record at the registrar, and the nginx-proxy-manager routing + certificate.
</what-built>
<task type="auto">
<name>Task 3: Prove the arena is internet-reachable at http://146.59.87.168:9100 (no DNS/TLS — user decision 2026-07-30)</name>
<precondition>Task 2 shows the arena healthy on-host.</precondition>
<reversibility rating="reversible">Firewall rule (if one is needed) is a single ufw/iptables/OVH
entry that can be removed; nothing else changes.</reversibility>
<action>
Human-only: create the DNS A record for the arena subdomain at the registrar, and create the
nginx-proxy-manager proxy host + Let's Encrypt certificate that fronts `146.59.87.168:9100`.
Neither has a CLI or credentialed API path available to Claude — the domain is on GoDaddy
nameservers and the NPM admin password is not held by the agent.
The user chose to skip DNS + NPM + Let's Encrypt entirely: the canonical arena URL IS
`http://146.59.87.168:9100`. This works because the node→arena hop is a server-side proxy
(no browser mixed-content) and cloud bots speak server-to-server. TLS can be added later by
fronting the same port with an NPM host and changing only `ARENA_UPSTREAM_URL` — no code change.
Verify reachability from OFF the VPS: from archi-dev-box, `curl -fsS --max-time 10
http://146.59.87.168:9100/api/health`. If it fails, check and fix host-level firewalling on
VPS2 (`sudo ufw status`, iptables, and note OVH network-level firewall may need the user).
Record the final reachable URL + any firewall change in `docs/arena-deployment.md` and in
09-04-SUMMARY.md — plans 09-05/09-06/09-07 read the arena URL from there.
</action>
<instructions>
Please do these two things, then reply.
**1. DNS (GoDaddy — `archipelago-foundation.org` uses ns29/ns30.domaincontrol.com).**
Add an `A` record: host = the chosen arena subdomain label (e.g. `arena`), value =
`146.59.87.168`, TTL 600. There is no wildcard record on this domain, so this step cannot be
skipped.
**2. nginx-proxy-manager (http://146.59.87.168:81, admin `lfg2025@proton.me`).**
Hosts → Proxy Hosts → Add Proxy Host, matching the existing entries exactly:
- Domain Names: the full arena hostname
- Scheme: `http`; Forward Hostname/IP: `146.59.87.168`; Forward Port: `9100`
- Enable **Websockets Support** (the live fight stream needs it) and Block Common Exploits
- SSL tab: request a new Let's Encrypt certificate, enable **Force SSL** and HTTP/2, agree to the ToS
If you can instead give Claude the NPM admin password, it can create the proxy host through the
NPM API and only the DNS record needs doing by hand.
**If the DNS record is not possible before the demo**, say so — the fallback is to run the demo
against `http://146.59.87.168:9100` directly. A cloud bot works fine over plain HTTP; the cost
is that credentials cross the internet unencrypted, so it is a demo-only contingency and the
manifest in plan 09-06 would carry the plain-HTTP URL until DNS lands.
</instructions>
<verification>
Once you reply, Claude confirms it automatically:
`dig +short <arena-host> A` returns `146.59.87.168`, and
`curl -fsSI https://<arena-host>/api/health` returns 200 over a valid certificate chain (no
`--insecure`). Both are re-asserted as the precondition of plan 09-05 Task 1, which will not
proceed until they pass.
</verification>
<resume-signal>Reply "dns+proxy done" (or "fallback: plain http", or paste the NPM admin password for Claude to automate the proxy host)</resume-signal>
<verify>
<automated>curl -fsS --max-time 10 http://146.59.87.168:9100/api/health | grep -q '"status":"ok"'</automated>
</verify>
<acceptance_criteria>
- `curl -fsS http://146.59.87.168:9100/api/health` succeeds from archi-dev-box (off-host).
- `curl -fsS http://146.59.87.168:9100/api/bots` returns the seeded fighters (full-copy decision: expect 115 bots).
- `docs/arena-deployment.md` records the plain-HTTP decision and the later-TLS upgrade path.
</acceptance_criteria>
<done>The canonical arena is publicly reachable at http://146.59.87.168:9100 with the full data copy live.</done>
</task>
</tasks>
@ -290,7 +269,7 @@ the rootless-podman invariant applies to Archipelago nodes, not to this host.
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-09-16 | Information disclosure | credentials over plain HTTP on the raw port | high | mitigate | NPM terminates TLS with Force SSL on; the raw port exists for host-level verification and as an explicit, user-acknowledged demo fallback recorded in the runbook |
| T-09-16 | Information disclosure | credentials over plain HTTP on the raw port | high | accept (user decision 2026-07-30) | User explicitly chose plain HTTP on :9100 over DNS/TLS setup; recorded in runbook with the env-only TLS upgrade path for later |
| T-09-17 | Information disclosure | `JWT_SECRET` leaking into git, logs or `docker inspect` transcripts | high | mitigate | Generated on the host into a 0600 `.env` outside any repo; the compose file only carries `${JWT_SECRET}`; the value is never echoed |
| T-09-18 | Spoofing | forged `X-Forwarded-For` from a direct caller to :9100 bypassing per-IP limits | medium | accept | `TRUSTED_PROXY` is required for the arena to honour those headers and NPM overwrites `X-Real-IP` for traffic through 443; a direct caller can already choose its own source IP cheaply, so this adds no meaningful capability |
| T-09-19 | Denial of service | anonymous bot-registration flooding the public arena | medium | mitigate | Existing `rateLimit(3600_000, 5)` on `POST /api/bots`, now meaningful per real client IP thanks to `TRUSTED_PROXY` |
@ -303,8 +282,8 @@ the rootless-podman invariant applies to Archipelago nodes, not to this host.
- `ssh debian@146.59.87.168 'curl -fsS http://127.0.0.1:9100/api/health'` — ok payload.
- `curl -fsS http://146.59.87.168:9100/api/health` from archi-dev-box — ok payload (or a recorded
firewall finding).
- After the checkpoint: `curl -fsSI https://<arena-host>/api/health` returns 200 with a valid
certificate chain — this is asserted in plan 09-05 Task 1, which gates the rest of the demo path.
- `curl -fsS http://146.59.87.168:9100/api/health` returns 200 from off-host — this is asserted
again as plan 09-05 Task 1's precondition, which gates the rest of the demo path.
- Supports `09-VALIDATION.md`'s manual-only item "Cross-node fighter visibility" by providing the
shared endpoint that item depends on.
</verification>
@ -312,8 +291,8 @@ the rootless-podman invariant applies to Archipelago nodes, not to this host.
<success_criteria>
- One canonical arena container runs on VPS2 with a host-generated secret and no payment config.
- Its compose definition and full runbook are committed to the botfight repo.
- The hostname and starting-data decisions are recorded in the SUMMARY.
- DNS + NPM proxy host + certificate are done, or the plain-HTTP contingency is explicitly accepted.
- The full-copy data seed is live (115 bots visible via /api/bots) and the source DB on archi-dev-box is untouched.
- The arena is internet-reachable at http://146.59.87.168:9100 (user-accepted plain HTTP; TLS is a later env-only upgrade).
</success_criteria>
<output>