Files
archy/.planning/phases/09-botfights-platform-upgrade/09-05-PLAN.md
T

19 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
09-botfights-platform-upgrade 05 execute 2
09-01
09-02
09-03
09-04
/home/archipelago/Projects/botfight/docker-compose.arena.yml
/home/archipelago/Projects/botfight/docs/arena-deployment.md
true
BOT-03
BOT-04
truths artifacts key_links
A botfights:1.2.0 image built from main — carrying the signer-login fix, the unified prompt and the arena proxy — exists in the registry at 146.59.87.168:3000/lfg2025 and is the image the canonical arena runs (D-04/BOT-04)
The public arena serves the unified prompt over its public URL with the arena's own hostname already substituted into it, so a cloud agent needs nothing but that one URL (D-02/BOT-02)
A bot can be registered against the public arena from outside the LAN with a single anonymous POST, and it then appears in the public fighter list
A second botfights instance pointed at the public arena shows that same bot — cross-instance fighter visibility is demonstrated on real hosts before anything is published to the fleet (D-03/BOT-03)
A live fight's SSE event stream arrives incrementally through the public HTTPS path, not buffered by nginx until the fight ends
path provides contains
/home/archipelago/Projects/botfight/docs/arena-deployment.md Runbook updated with the 1.2.0 rollout, the image build/push recipe and the verified public URLs 1.2.0
from to via pattern
the temporary proxy-mode instance on archi-dev-box the canonical arena on VPS2 ARENA_UPSTREAM_URL pointing at the public arena URL, proving the whole BOT-03 path on real hosts ARENA_UPSTREAM_URL
Build and publish `botfights:1.2.0`, roll the canonical arena onto it, and then prove the entire BOT-03 claim on real hosts — a bot registered against the public arena is visible through a *different* instance that is only proxying to it.

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 arena, D-04 = BOT-04 registry/manifest + signed catalog (its "build + push new image to the vps2 registry" clause is executed here).

Purpose: everything after this plan — the signed catalog, the node update, the demo — assumes a real 1.2.0 image exists and that proxy mode works across the public internet. This plan is where that assumption becomes a fact, using a throwaway container rather than the node's installed app, so a failure costs nothing. Output: the 1.2.0 image in the registry, the arena running it, and a recorded live cross-instance proof.

Repo: /home/archipelago/Projects/botfight for the build; the arena runs on VPS2 (debian@146.59.87.168). Commit target: git push origin main.

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/09-botfights-platform-upgrade/09-CONTEXT.md @.planning/phases/09-botfights-platform-upgrade/09-RESEARCH.md @.planning/phases/09-botfights-platform-upgrade/09-PATTERNS.md @.planning/phases/09-botfights-platform-upgrade/09-VALIDATION.md @.planning/phases/09-botfights-platform-upgrade/09-04-SUMMARY.md Task 1: Confirm the public arena is reachable, then build and push botfights:1.2.0 Plan 09-04's human-action checkpoint is complete: either the arena hostname resolves to 146.59.87.168 with a valid certificate, or the plain-HTTP contingency was explicitly accepted and recorded in 09-04-SUMMARY.md. /home/archipelago/Projects/botfight/docs/arena-deployment.md - `.planning/phases/09-botfights-platform-upgrade/09-04-SUMMARY.md` — the chosen arena hostname and whether TLS or the plain-HTTP contingency is in effect. Every URL in this plan comes from there; do not assume a hostname. - `/home/archipelago/Projects/botfight/Dockerfile` — the four-stage build, the `CACHE_BUST` arg that forces frontend/server rebuild stages, and the final `node:22-slim` image. - `/home/archipelago/Projects/archy/scripts/build-bitcoin-image.sh` — the in-repo precedent for building and pushing to this registry (`podman push --tls-verify=false` against `146.59.87.168:3000/lfg2025`), including how it handles credentials. - `~/.claude/projects/-home-archipelago-Projects-archy/memory/reference_ovh_168_mirror.md` — the Gitea admin account and API token for `146.59.87.168:3000`. Use it for `podman login`; never copy any token into a tracked file, a plan, a SUMMARY or a commit message. First verify the public entrance, because everything downstream depends on it: `curl -fsSI https:///api/health` (or the plain-HTTP contingency URL) must return 200, and for the TLS case `curl -fsS https:///api/health` must succeed without `--insecure`. If it fails, stop and report — do not build on top of a broken public path.
Then build the image from the current `main` of the botfight repo, which now contains the arena
proxy (09-01), the signer-login fix (09-02) and the unified prompt (09-03):
- `cd /home/archipelago/Projects/botfight && git pull --ff-only origin main` first, so the image
  contains all three wave-1 plans and not a stale tree; abort if any of the three are missing
  (check `server/src/middleware/arena-proxy.ts`, the `/me` route and the `/prompt` route exist).
- `podman build --build-arg CACHE_BUST=$(date +%s) -t 146.59.87.168:3000/lfg2025/botfights:1.2.0 .`
- `podman login 146.59.87.168:3000` with the Gitea credentials from the memory note, then
  `podman push --tls-verify=false 146.59.87.168:3000/lfg2025/botfights:1.2.0`.
- Verify the pushed image is real and complete by inspecting it from the registry side:
  `skopeo inspect --tls-verify=false docker://146.59.87.168:3000/lfg2025/botfights:1.2.0`
  returns a manifest, and record its digest in the SUMMARY.

A build gotcha to expect: the frontend build stage can silently reuse cache and ship a stale
bundle. Confirm the new bundle really contains this phase's work before pushing — run the
container locally on a spare port with no upstream configured and assert that
`/api/docs/prompt` returns 200 and `/api/auth/me` without a token returns 401. Only push after
both hold.

Update `docs/arena-deployment.md` with the exact build+push commands and the recorded digest.
skopeo inspect --tls-verify=false docker://146.59.87.168:3000/lfg2025/botfights:1.2.0 >/dev/null && echo pushed - `curl -fsS /api/health` (URL taken from 09-04-SUMMARY.md) returns the ok payload; for the TLS case it succeeds without `--insecure`. - `skopeo inspect --tls-verify=false docker://146.59.87.168:3000/lfg2025/botfights:1.2.0` exits 0 and its digest is recorded in the SUMMARY. - A local throwaway run of the pushed tag answers 200 on `/api/docs/prompt` and 401 on `/api/auth/me` with no Authorization header. - `git -C /home/archipelago/Projects/botfight log --oneline -1` shows the tree the image was built from, and that commit is on `origin/main`. - No registry token, password or JWT secret appears anywhere in `docs/arena-deployment.md` (`grep -Eq '[0-9a-f]{40}' docs/arena-deployment.md` finds nothing). botfights:1.2.0 exists in the registry, is verifiably built from the tree carrying all three wave-1 plans, and the public arena entrance is confirmed working. Task 2: Roll the canonical arena to 1.2.0 and verify the public contract end-to-end A compose image-tag change on one host; rolling back is re-pointing the tag at 1.1.0 and recreating the container — the data volume is untouched. /home/archipelago/Projects/botfight/docker-compose.arena.yml, /home/archipelago/Projects/botfight/docs/arena-deployment.md - `/home/archipelago/Projects/botfight/docker-compose.arena.yml` as created by plan 09-04 — the single place the image tag is written. - `/home/archipelago/Projects/botfight/server/src/routes/bots.ts` lines 34-110 — the exact registration request/response so the curl checks assert real fields. - `/home/archipelago/Projects/botfight/server/src/routes/fights.ts` around lines 410-480 — the SSE stream route and the `X-Accel-Buffering` header added in 09-01, which is what makes the stream survive nginx. Bump the image tag in `docker-compose.arena.yml` to `1.2.0`, copy the file to `/opt/botfights-arena/docker-compose.yml` on VPS2, then `docker compose pull && docker compose up -d` there. The named volume is unchanged, so whatever starting data was chosen in 09-04 survives the roll.
Then verify the public contract, all through the public URL (never through 127.0.0.1), because
the demo's cloud bot will only ever see this path:
1. `GET /api/health` → ok.
2. `GET /api/docs/prompt` → 200, `text/markdown`, and the body contains the arena's own public
   base URL with no unsubstituted template token left in it. This is the single URL a cloud
   agent will be handed.
3. `POST /api/bots` with a unique test name → 200 with an id and secret. Record them only in
   the working transcript, not in any committed file.
4. `GET /api/bots` → the test bot is present.
5. `GET /api/fights/poll` with that bot's credentials in the `Authorization: Bot id:secret`
   form → 200 with `pending:false` (proves bot auth works through the public path).
6. `POST /api/queue/join/<botId>` then `curl -N <base>/api/fights/<fightId>/stream` on the
   resulting fight → events must appear progressively while the fight runs. If the whole
   payload only lands when the connection closes, nginx is buffering: confirm the response
   carries `X-Accel-Buffering: no`, and if it still buffers add `proxy_buffering off;` to the
   proxy host's Custom Nginx Configuration in NPM and record that in the runbook.
7. Confirm the JWT secret survived the roll — a container restart with a different secret would
   invalidate every existing session: `docker inspect` shows the env var is still sourced from
   the same host `.env` (do not print the value).

Update `docs/arena-deployment.md` with the verified public URLs and any NPM custom-config change.
ARENA=$(grep -Eo 'https?://[a-z0-9.:-]+' /home/archipelago/Projects/botfight/docs/arena-deployment.md | grep -v '146.59.87.168:3000' | head -1); curl -fsS "$ARENA/api/health" | grep -q '"status":"ok"' && curl -fsS "$ARENA/api/docs/prompt" | grep -q '/api/bots' && curl -fsS "$ARENA/api/docs/prompt" | grep -qv '{{ARENA_URL}}' - `ssh debian@146.59.87.168 'sudo docker inspect botfights-arena --format "{{.Config.Image}}"'` ends in `:1.2.0` and the container is healthy. - `curl -fsS /api/docs/prompt` returns markdown containing the arena's own public base URL and no unsubstituted `{{ARENA_URL}}` token. - A test bot registered through the public URL appears in `GET /api/bots` and authenticates against `GET /api/fights/poll`. - An SSE stream on a real fight delivers at least two events more than one second apart (evidence of incremental delivery) — the timing observation is recorded in the SUMMARY. - `GET /api/auth/me` with no token returns 401 through the public URL. - The arena's data from 09-04 is still present after the roll (fighter count unchanged or grown). The public arena runs 1.2.0 and demonstrably serves the prompt, registration, bot auth and live streaming over its public URL. Task 3: Prove cross-instance fighter visibility with a real second instance A throwaway podman container on a spare port that touches nothing the installed BotFights app owns; removing it is `podman rm -f`. /home/archipelago/Projects/botfight/docs/arena-deployment.md - `/home/archipelago/Projects/botfight/server/src/middleware/arena-proxy.ts` as built in 09-01 — in particular which paths bypass the proxy and how the upstream base URL is read. - `.planning/phases/09-botfights-platform-upgrade/09-01-SUMMARY.md` — any deviation from the planned proxy behaviour that this live test should account for. - The output of `podman ps --filter name=botfights` on archi-dev-box — the installed app runs on host port 9100; the throwaway instance must use a different port and must not be given the installed app's volume. On archi-dev-box, run a temporary second instance in proxy mode: `podman run --rm -d --name botfights-proxytest -p 9101:9100 -e NODE_ENV=production -e JWT_SECRET=$(openssl rand -hex 32) -e ARENA_UPSTREAM_URL= 146.59.87.168:3000/lfg2025/botfights:1.2.0` with no volume mount at all — it must have no local database worth reading, which is exactly the point: everything it shows has to come from the arena.
Then assert the BOT-03 claim:
1. `curl -fsS http://127.0.0.1:9101/api/health` → ok, and confirm from the arena's logs that this
   request did NOT reach it (health is answered locally by design).
2. `curl -fsS http://127.0.0.1:9101/api/bots` → contains the test bot registered against the
   public arena in Task 2. A fighter registered on one host is visible through another instance
   that never stored it.
3. Register a second bot through the proxy instance (`POST http://127.0.0.1:9101/api/bots`) and
   assert it appears in `GET <public arena>/api/bots` — the reverse direction.
4. Open an SSE stream through the proxy instance on a live fight and confirm incremental events.
5. Stop the arena container briefly and confirm the proxy instance answers 502 with a JSON error
   on `/api/bots` while `/api/health` still returns ok; restart the arena and confirm recovery.
   Keep the outage to seconds and do it before any human is testing against the arena.
6. `podman rm -f botfights-proxytest` when finished, and confirm the installed BotFights app on
   port 9100 was untouched throughout (still running its original container id).

Record the observed evidence for each step in `docs/arena-deployment.md` under a
"verified cross-instance behaviour" heading, and in the SUMMARY.
podman run --rm -d --name botfights-proxytest -p 9101:9100 -e NODE_ENV=production -e JWT_SECRET=$(openssl rand -hex 32) -e ARENA_UPSTREAM_URL="$(grep -Eo 'https?://[a-z0-9.:-]+' /home/archipelago/Projects/botfight/docs/arena-deployment.md | grep -v '146.59.87.168:3000' | head -1)" 146.59.87.168:3000/lfg2025/botfights:1.2.0 >/dev/null && sleep 12 && curl -fsS http://127.0.0.1:9101/api/health | grep -q '"status":"ok"' && curl -fsS http://127.0.0.1:9101/api/bots | grep -q '"name"' && podman rm -f botfights-proxytest >/dev/null && echo federation-proof-ok - The throwaway proxy-mode instance, started with no volume, lists the fighters that live in the arena's database. - A bot registered through the proxy instance is visible in the arena's own `GET /api/bots`. - `/api/health` on the proxy instance returns ok while the arena is stopped, and `/api/bots` returns 502 with a JSON `error` key during that window. - An SSE stream through the proxy instance delivers events incrementally. - `podman ps --filter name=botfights --format "{{.Names}} {{.Status}}"` shows the installed `botfights` app still up with its original container id, and no `botfights-proxytest` container remains. - `docs/arena-deployment.md` contains a "verified cross-instance behaviour" section with the observed evidence. The BOT-03 architecture is proven on real hosts across the public internet, before anything is published to the fleet.

<threat_model>

Trust Boundaries

Boundary Description
build host → container registry A pushed image becomes what every node will run
public internet → the canonical arena Anonymous registration, bot auth and streaming
throwaway proxy instance → arena The same node→arena hop nodes will use, exercised deliberately

STRIDE Threat Register

Threat ID Category Component Severity Disposition Mitigation Plan
T-09-23 Tampering a stale or partial image published as 1.2.0 high mitigate The image is smoke-tested locally for the new routes before push, its digest is recorded, and the build commit must be on origin/main (Task 1)
T-09-24 Information disclosure registry credentials leaking into the repo or a SUMMARY high mitigate Credentials are read from the agent's infra memory note and used only for podman login; an acceptance criterion greps the runbook for token-shaped strings
T-09-25 Denial of service the deliberate arena outage in Task 3 hitting real users low mitigate Seconds-long, performed before human testing begins, and the recovery is asserted
T-09-26 Tampering the throwaway test container disturbing the installed app's data high mitigate It runs on a different port with no volume mount; an acceptance criterion asserts the installed container is untouched
T-09-27 Information disclosure test bot secrets committed to the repo medium mitigate Test credentials stay in the transcript; nothing generated in this plan is written to a tracked file
</threat_model>
- `curl -fsS /api/health`, `/api/docs/prompt`, `/api/bots` over the public URL — all pass. - A registered test bot round-trips through both the arena directly and a proxy-mode instance. - SSE delivers incrementally through the public HTTPS path and through the proxy. - Directly satisfies the `09-VALIDATION.md` manual-only item "Cross-node fighter visibility: bot registered via VPS2 public arena appears on another instance" ahead of the human demo rehearsal.

<success_criteria>

  • 146.59.87.168:3000/lfg2025/botfights:1.2.0 exists, is verified, and its digest is recorded.
  • The canonical arena runs 1.2.0 and serves the full public contract.
  • Cross-instance fighter visibility is demonstrated on real hosts, in both directions, with a documented degradation path when the arena is unreachable. </success_criteria>
Create `.planning/phases/09-botfights-platform-upgrade/09-05-SUMMARY.md` when done, recording the image digest, the verified public URLs and the cross-instance evidence — plan 09-06 writes that image tag and arena URL into the app manifest. Commit the botfight changes with `git add` by explicit path and `git push origin main`. Commit the SUMMARY in archy and `git push gitea-ai main`.