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
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.mdLocked 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.
@.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`.