Archipelago — open-source initial import
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
---
|
||||
phase: 09-botfights-platform-upgrade
|
||||
plan: 05
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: [09-01, 09-02, 09-03, 09-04]
|
||||
files_modified:
|
||||
- /home/archipelago/Projects/botfight/docker-compose.arena.yml
|
||||
- /home/archipelago/Projects/botfight/docs/arena-deployment.md
|
||||
autonomous: true
|
||||
requirements: [BOT-03, BOT-04]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "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"
|
||||
artifacts:
|
||||
- path: /home/archipelago/Projects/botfight/docs/arena-deployment.md
|
||||
provides: "Runbook updated with the 1.2.0 rollout, the image build/push recipe and the verified public URLs"
|
||||
contains: "1.2.0"
|
||||
key_links:
|
||||
- from: the temporary proxy-mode instance on archi-dev-box
|
||||
to: the canonical arena on VPS2
|
||||
via: "ARENA_UPSTREAM_URL pointing at the public arena URL, proving the whole BOT-03 path on real hosts"
|
||||
pattern: "ARENA_UPSTREAM_URL"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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`.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
||||
@$HOME/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<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
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Confirm the public arena is reachable, then build and push botfights:1.2.0</name>
|
||||
<precondition>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.</precondition>
|
||||
<files>/home/archipelago/Projects/botfight/docs/arena-deployment.md</files>
|
||||
<read_first>
|
||||
- `.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.
|
||||
</read_first>
|
||||
<action>
|
||||
First verify the public entrance, because everything downstream depends on it:
|
||||
`curl -fsSI https://<arena-host>/api/health` (or the plain-HTTP contingency URL) must return 200,
|
||||
and for the TLS case `curl -fsS https://<arena-host>/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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>skopeo inspect --tls-verify=false docker://146.59.87.168:3000/lfg2025/botfights:1.2.0 >/dev/null && echo pushed</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `curl -fsS <public arena base>/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).
|
||||
</acceptance_criteria>
|
||||
<done>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.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Roll the canonical arena to 1.2.0 and verify the public contract end-to-end</name>
|
||||
<reversibility rating="reversible">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.</reversibility>
|
||||
<files>/home/archipelago/Projects/botfight/docker-compose.arena.yml, /home/archipelago/Projects/botfight/docs/arena-deployment.md</files>
|
||||
<read_first>
|
||||
- `/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.
|
||||
</read_first>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>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}}'</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `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 <arena>/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).
|
||||
</acceptance_criteria>
|
||||
<done>The public arena runs 1.2.0 and demonstrably serves the prompt, registration, bot auth and live streaming over its public URL.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Prove cross-instance fighter visibility with a real second instance</name>
|
||||
<reversibility rating="reversible">A throwaway podman container on a spare port that touches
|
||||
nothing the installed BotFights app owns; removing it is `podman rm -f`.</reversibility>
|
||||
<files>/home/archipelago/Projects/botfight/docs/arena-deployment.md</files>
|
||||
<read_first>
|
||||
- `/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.
|
||||
</read_first>
|
||||
<action>
|
||||
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=<public arena base>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>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</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 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.
|
||||
</acceptance_criteria>
|
||||
<done>The BOT-03 architecture is proven on real hosts across the public internet, before anything is published to the fleet.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<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>
|
||||
|
||||
<verification>
|
||||
- `curl -fsS <arena>/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.
|
||||
</verification>
|
||||
|
||||
<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>
|
||||
|
||||
<output>
|
||||
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`.
|
||||
</output>
|
||||
Reference in New Issue
Block a user