Archipelago — open-source initial import

This commit is contained in:
Archipelago
2026-08-12 10:55:50 +00:00
commit 520b93d58e
1937 changed files with 441306 additions and 0 deletions
@@ -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>