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

351 lines
25 KiB
Markdown
Raw Normal View History

2026-08-12 10:55:50 +00:00
---
phase: 09-botfights-platform-upgrade
plan: 03
type: execute
wave: 1
depends_on: []
files_modified:
- /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS.md
- /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS-EASY.md
- /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS-POLLING.md
- /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS-WEBHOOK.md
- /home/archipelago/Projects/botfight/BOTFIGHTS.md
- /home/archipelago/Projects/botfight/BOT_SETUP.md
- /home/archipelago/Projects/botfight/server/src/routes/docs.ts
- /home/archipelago/Projects/botfight/server/src/routes/docs.test.ts
- /home/archipelago/Projects/botfight/frontend/src/pages/JoinBoutPage.vue
- /home/archipelago/Projects/botfight/frontend/src/pages/BotProfilePage.vue
- /home/archipelago/Projects/botfight/frontend/src/pages/DocsPage.vue
- /home/archipelago/Projects/botfight/e2e/signup-bot.spec.ts
autonomous: true
requirements: [BOT-02]
must_haves:
truths:
- "One copy-paste prompt contains everything an AI agent needs to build a working bot — registration, credential handling, BOTH webhook and polling protocols, every endpoint it calls, and every response format — with no instruction to go read another document (D-02/BOT-02)"
- "A cloud-hosted AI agent with no browser can fetch the whole prompt with a single unauthenticated GET and the arena URL inside it is already correct for the arena it fetched from"
- "The prompt documents the registration call the old docs never mentioned: an anonymous POST /api/bots that returns the bot id and secret"
- "The in-app 'show my setup guide' flow still hands the user their real credentials substituted into the text, from the single consolidated doc"
- "No user-facing surface links to BOTFIGHTS-EASY / BOTFIGHTS-POLLING / BOTFIGHTS-WEBHOOK / BOT_SETUP any more"
artifacts:
- path: /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS.md
provides: "The single canonical AI bot-setup prompt (ships into the container at server/public/docs/BOTFIGHTS.md via the frontend build)"
contains: "POST /api/bots"
- path: /home/archipelago/Projects/botfight/server/src/routes/docs.ts
provides: "GET /api/docs/prompt serving the prompt as text/markdown with the arena URL resolved"
contains: "docsRouter.get('/prompt'"
key_links:
- from: /home/archipelago/Projects/botfight/frontend/src/pages/JoinBoutPage.vue
to: /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS.md
via: "setupDocPath() resolves to the single consolidated doc and the YOUR_BOT_ID / YOUR_BOT_SECRET substitution still runs on it"
pattern: "BOTFIGHTS.md"
---
<objective>
Deliver D-02/BOT-02: replace the five-document maze (`BOTFIGHTS.md`, `BOTFIGHTS-EASY.md`,
`BOTFIGHTS-POLLING.md`, `BOTFIGHTS-WEBHOOK.md`, `BOT_SETUP.md`, plus DocsPage's own duplicated copy)
with ONE self-contained prompt that an AI agent can be handed verbatim, and serve it at a stable URL
an agent can `curl`.
Decision IDs map to `09-CONTEXT.md` **Locked Decisions**: D-01 = BOT-01 signer login,
D-02 = BOT-02 unified AI bot-setup prompt, D-03 = BOT-03 shared arena, D-04 = BOT-04 catalog.
Purpose: tomorrow's demo has a cloud-hosted "openclaw" bot register and fight using ONLY this prompt.
That is the acceptance bar — not "the docs are tidier". Today's docs fail it twice: they never
document the registration call (`POST /api/bots`), and the polling example defaults to a stale host
(`BOTFIGHTS_HOST || 'botfights.io'`) that is not the arena.
Output: one consolidated prompt file, `GET /api/docs/prompt`, updated in-app call sites with a copy
button, and the superseded files removed.
**Repo: `/home/archipelago/Projects/botfight`.** 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
</context>
## Artifacts this plan produces
| Symbol | Kind | File |
|---|---|---|
| the unified prompt | canonical markdown (single source of truth) | `frontend/public/docs/BOTFIGHTS.md` |
| `docsRouter.get('/prompt')` | Hono route returning `text/markdown` | `server/src/routes/docs.ts` |
| `docs.test.ts` additions | Vitest cases for the prompt route | `server/src/routes/docs.test.ts` |
| copy-the-prompt affordance | UI | `frontend/src/pages/DocsPage.vue` |
Why `frontend/public/docs/BOTFIGHTS.md` is the canonical location: Vite copies `frontend/public/**`
into `frontend/dist`, and the Dockerfile copies `frontend/dist` to `server/public` — so this file is
the only copy that exists inside the shipped container. The repo-root `BOTFIGHTS.md` is NOT in the
image and must not be the source the server reads.
<tasks>
<task type="auto">
<name>Task 1: Write the one prompt that a cloud AI agent can build a working bot from</name>
<files>/home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS.md, /home/archipelago/Projects/botfight/BOTFIGHTS.md, /home/archipelago/Projects/botfight/BOT_SETUP.md, /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS-EASY.md, /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS-POLLING.md, /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS-WEBHOOK.md</files>
<read_first>
- `/home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS.md` — all 452 lines. It is
~90% of the target already: Credentials, Choose a Mode, Option A webhook bot (full JS), Option
B polling bot (full JS), How Fights Work, Challenge Payload, All Challenge Types, Security
Notes, Tips, After Setup. Extend it in place; do not rewrite from scratch.
- `/home/archipelago/Projects/botfight/BOT_SETUP.md` — mine it for the unique content the public
doc lacks (customization API, archetype list) before deleting it.
- `/home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS-EASY.md`,
`BOTFIGHTS-POLLING.md`, `BOTFIGHTS-WEBHOOK.md` — confirm every fact in each already appears in
the consolidated doc before removing them.
- `/home/archipelago/Projects/botfight/server/src/routes/bots.ts` lines 34-110 — the real
registration contract: `POST /api/bots` with `{name}` (2-12 chars, alphanumeric/hyphen/
underscore, lowercased, unique), optional `webhook_url` (omit or empty string = poll mode),
rate limit 5 per hour per IP, 409 on duplicate name, and webhook mode verifying the URL by
calling it before accepting.
- `/home/archipelago/Projects/botfight/server/src/middleware/bot-auth.ts` lines 16-45 — the two
accepted credential forms: `Authorization: Bot <bot_id>:<secret>` or
`?bot_id=...&secret=...`.
- `/home/archipelago/Projects/botfight/server/src/routes/fights.ts` lines 363-410 —
`GET /api/fights/poll` response fields (`pending`, `fight_id`, `round`, `type`, `challenge`,
`constraints`, `opponent`, `arena`, `arena_modifier`, `remaining_ms`, `scoring`) and
`POST /api/fights/poll/respond` (`{answer, trashTalk}``{accepted: true}`, 404 when nothing
is pending), plus `POST /api/queue/join/:botId` in `routes/queue.ts`.
- `/home/archipelago/Projects/botfight/server/src/engine/orchestrator.ts` lines 180-195 — the
webhook `X-Botfights-Signature: sha256=...` HMAC scheme so the prompt can tell a webhook bot
how to verify inbound challenges.
- `/home/archipelago/Projects/botfight/server/src/routes/docs.ts` lines 6-163 — the challenge
types, scoring rules, failure modes and tips already curated for machine consumption; the
prompt must agree with them.
</read_first>
<action>
Consolidate everything into `frontend/public/docs/BOTFIGHTS.md` as the single canonical prompt.
Required content, in this order:
1. A one-paragraph framing line stating this file is a complete, self-contained instruction set
for an AI agent to build and run a BOTFIGHTS bot, and that nothing else needs to be read.
2. **Register** (new section, currently missing everywhere): the anonymous
`POST {{ARENA_URL}}/api/bots` call with a `curl` example and the JSON response, stating that
omitting `webhook_url` selects poll mode, that the returned secret is shown once, the 2-12
character name rule, the 409-on-duplicate behaviour and the 5-per-hour-per-IP limit.
3. **Credentials**: keep the existing `BOT_ID=YOUR_BOT_ID` / `BOT_SECRET=YOUR_BOT_SECRET` block
verbatim — those two placeholder tokens are substituted in-app and MUST survive unchanged —
and add both accepted auth forms (`Authorization: Bot <bot_id>:<secret>` and the query-param
form), plus an instruction to keep the secret in an env var and never commit it.
4. **Choose a mode** and the two full working bot implementations (webhook and polling), kept
inline as today. In the polling example, replace the stale hard-coded fallback host that
`BOTFIGHTS_HOST` currently defaults to with `{{ARENA_URL}}`, and make both examples use one base-URL constant so an agent
edits a single line. Keep them dependency-free Node scripts. The polling example currently
falls back to a stale public host when `BOTFIGHTS_HOST` is unset — that fallback literal must
be gone from the file when you are done.
5. **Webhook verification**: the `X-Botfights-Signature: sha256=<hmac>` header, the exact
derivation (HMAC-SHA256 over the request body with a key derived from the bot's secret hash),
and that a webhook must answer 200 with JSON within `constraints.timeout_ms`.
6. **Enter a fight**: `POST {{ARENA_URL}}/api/queue/join/{BOT_ID}` and what happens when no
opponent shows up.
7. **Endpoint reference**: a compact table of every endpoint a bot uses — `POST /api/bots`,
`GET /api/fights/poll`, `POST /api/fights/poll/respond`, `POST /api/queue/join/:botId`,
`GET /api/bots/:name`, `POST /api/bots/:name/test-challenge`, `GET /api/fights/:id` — with
method, auth requirement, request shape and response shape for each.
8. **Challenge payload, challenge types, scoring, failure modes, tips** — keep the existing
sections, reconciled with `routes/docs.ts` so the two never disagree.
9. **Troubleshooting**: 401 (bad credentials), 404 from poll/respond (no pending challenge), 429
(rate limited — poll no faster than once per second), and the five-consecutive-errors
auto-deactivation rule.
Use the literal token `{{ARENA_URL}}` everywhere a base URL appears. The server route in Task 2
substitutes it with the real arena origin; the in-app viewer substitutes it client-side.
Then remove the superseded files: delete `frontend/public/docs/BOTFIGHTS-EASY.md`,
`frontend/public/docs/BOTFIGHTS-POLLING.md`, `frontend/public/docs/BOTFIGHTS-WEBHOOK.md` and
`BOT_SETUP.md` (these four filenames appear here only as delete targets — the Task 3 gate greps
`frontend/src` for references to them).
<!-- planner-discipline-allow: BOTFIGHTS-POLLING, BOTFIGHTS-WEBHOOK, BOTFIGHTS-EASY, BOT_SETUP -->
Replace the repo-root `BOTFIGHTS.md` with a three-line stub pointing readers at
`frontend/public/docs/BOTFIGHTS.md` and at `GET /api/docs/prompt`, so the two near-identical
root/public copies can never drift again. Do not delete any file until Task 3's call-site sweep
has a plan for every reference to it (grep first — references exist in `JoinBoutPage.vue`,
`BotProfilePage.vue` and `DocsPage.vue`).
</action>
<verify>
<automated>cd /home/archipelago/Projects/botfight && test ! -e frontend/public/docs/BOTFIGHTS-POLLING.md && test ! -e frontend/public/docs/BOTFIGHTS-WEBHOOK.md && test ! -e frontend/public/docs/BOTFIGHTS-EASY.md && test ! -e BOT_SETUP.md && grep -q '/api/bots' frontend/public/docs/BOTFIGHTS.md && grep -q 'YOUR_BOT_ID' frontend/public/docs/BOTFIGHTS.md && grep -q '{{ARENA_URL}}' frontend/public/docs/BOTFIGHTS.md</automated>
</verify>
<acceptance_criteria>
- `grep -c '{{ARENA_URL}}' /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS.md` is at least 5.
- `grep -q 'YOUR_BOT_ID' /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS.md` and `grep -q 'YOUR_BOT_SECRET' ...` both succeed (in-app substitution tokens preserved).
- `grep -q 'api/fights/poll' /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS.md`, `grep -q 'api/fights/poll/respond' ...`, `grep -q 'api/queue/join' ...`, `grep -q 'X-Botfights-Signature' ...` all succeed.
- `grep -q 'Authorization: Bot ' /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS.md` succeeds.
- `grep -c 'botfights.io' /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS.md` is 0.
- The four superseded files no longer exist and the root `BOTFIGHTS.md` is under 10 lines.
- `wc -l < /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS.md` is at least 452 (content was added, not lost).
</acceptance_criteria>
<done>One document contains registration, credentials, both protocols, every endpoint and every response format; the four superseded documents are gone.</done>
</task>
<task type="auto">
<name>Task 2: Serve the prompt at GET /api/docs/prompt with the real arena URL baked in</name>
<files>/home/archipelago/Projects/botfight/server/src/routes/docs.ts, /home/archipelago/Projects/botfight/server/src/routes/docs.test.ts</files>
<read_first>
- `/home/archipelago/Projects/botfight/server/src/routes/docs.ts` lines 1-10 and the shape of the
existing `GET /webhook` handler — the new handler sits alongside it on the same router.
- `/home/archipelago/Projects/botfight/server/src/app.ts` lines 90-93 (the `/api/docs/*` GET
cache header, already `public, max-age=3600` — no per-route cache handling needed) and lines
110-142 (`publicDir` resolution and `serveFile`, which is how `/docs/*` static files are
served in production and confirms `server/public/docs/BOTFIGHTS.md` is the in-container path).
- `/home/archipelago/Projects/botfight/server/src/routes/docs.test.ts` — existing harness style
for this router.
- `/home/archipelago/Projects/botfight/Dockerfile` lines 20-30 — `COPY --from=build-fe /app/frontend/dist server/public`,
the reason the in-container path is `server/public/docs/BOTFIGHTS.md`.
</read_first>
<action>
Add `docsRouter.get('/prompt', ...)` to `server/src/routes/docs.ts`:
- Resolve the prompt file at request time by trying, in order:
`<serverRoot>/public/docs/BOTFIGHTS.md` (the shipped container layout) then
`<repoRoot>/frontend/public/docs/BOTFIGHTS.md` (a dev checkout where the frontend has not been
built). Derive both from `dirname(fileURLToPath(import.meta.url))` the same way `app.ts`
derives `publicDir`. Cache the resolved contents in a module-level variable keyed by path plus
mtime, or simply read on each request — this endpoint is cached for an hour upstream, so a
plain read is acceptable; do not add a file-watcher.
- If neither path exists, return `c.json({ error: 'Prompt not available.' }, 404)` following the
codebase's direct-return error convention.
- Substitute the `{{ARENA_URL}}` token with, in precedence order: `process.env.PUBLIC_ARENA_URL`
when set, otherwise the origin of the incoming request (`new URL(c.req.url).origin`). On the
canonical arena behind nginx-proxy-manager the inbound request carries the public host, so an
agent that curls the public URL gets a prompt whose examples already point back at that same
public arena — which is precisely what makes the prompt self-contained for a cloud bot.
- Respond with `c.header('Content-Type', 'text/markdown; charset=utf-8')` and the substituted
body. Plain text, not JSON — an agent should be able to pipe the response straight into its
context.
Add cases to `server/src/routes/docs.test.ts`:
- 200 with a `text/markdown` content type and a body containing the registration endpoint path.
- no `{{ARENA_URL}}` token survives in the response body.
- with `PUBLIC_ARENA_URL` set, the body contains that value; restore the env var afterwards.
- without it, the body contains the origin the request was made to.
- the `YOUR_BOT_ID` placeholder is still present (the in-app substitution contract).
</action>
<verify>
<automated>cd /home/archipelago/Projects/botfight && pnpm vitest run server/src/routes/docs.test.ts</automated>
</verify>
<acceptance_criteria>
- `cd /home/archipelago/Projects/botfight && pnpm vitest run server/src/routes/docs.test.ts` exits 0 with at least 5 new cases passing.
- `cd /home/archipelago/Projects/botfight && pnpm exec tsc --noEmit -p server/tsconfig.json` exits 0.
- `grep -Eq "docsRouter\.get\(['\"]/prompt['\"]" /home/archipelago/Projects/botfight/server/src/routes/docs.ts` succeeds.
- `grep -q 'text/markdown' /home/archipelago/Projects/botfight/server/src/routes/docs.ts` succeeds.
- `grep -q 'PUBLIC_ARENA_URL' /home/archipelago/Projects/botfight/server/src/routes/docs.ts` succeeds.
- `grep -q 'public/docs/BOTFIGHTS.md' /home/archipelago/Projects/botfight/server/src/routes/docs.ts` succeeds (the container-layout path, not the repo-root file).
</acceptance_criteria>
<done>`curl <arena>/api/docs/prompt` returns the complete prompt as markdown with working URLs, from both a built container and a dev checkout.</done>
</task>
<task type="auto">
<name>Task 3: Point every in-app surface at the one prompt (and give it a copy button)</name>
<files>/home/archipelago/Projects/botfight/frontend/src/pages/JoinBoutPage.vue, /home/archipelago/Projects/botfight/frontend/src/pages/BotProfilePage.vue, /home/archipelago/Projects/botfight/frontend/src/pages/DocsPage.vue, /home/archipelago/Projects/botfight/e2e/signup-bot.spec.ts</files>
<read_first>
- `/home/archipelago/Projects/botfight/frontend/src/pages/JoinBoutPage.vue` around lines 515-560 —
`setupDocPath()`, `setupDocName()` and `toggleSetupContent()`, including the
`.replace(/YOUR_BOT_ID/g, ...)` / `.replace(/YOUR_BOT_SECRET/g, ...)` substitution that is the
load-bearing behaviour to preserve.
- `/home/archipelago/Projects/botfight/frontend/src/pages/BotProfilePage.vue` around lines 295-310
and 880-890 — the same mode-conditional doc path plus the filename shown in the UI.
- `/home/archipelago/Projects/botfight/frontend/src/pages/DocsPage.vue` lines 1-60 and 500-530 —
it fetches `/api/docs/webhook` (a JSON API reference) and hosts the interactive webhook tester.
Both stay; what changes is that the page leads with the prompt.
- `/home/archipelago/Projects/botfight/e2e/signup-bot.spec.ts` (43 lines) — the Playwright
conventions to follow: `test.describe` grouping, `page.getByText(/regex/i)` selectors, explicit
`{ timeout: N_000 }`.
- `/home/archipelago/Projects/botfight/CLAUDE.md` "Vue 3 Conventions" — `<script setup lang="ts">`,
script section ordering, naming rules.
</read_first>
<action>
`JoinBoutPage.vue`: collapse `setupDocPath()` and `setupDocName()` to the single consolidated doc
(`/docs/BOTFIGHTS.md`, displayed as `BOTFIGHTS.md`) regardless of the selected connection mode.
Keep `toggleSetupContent()` and the two placeholder substitutions exactly as they are, and add a
third substitution replacing the `{{ARENA_URL}}` token with `window.location.origin` so the
user's copied text has a working base URL. Keep the mode selector itself — it still drives what
gets registered — it just no longer selects a different document.
`BotProfilePage.vue`: same change at its own call site, including the filename shown in the UI.
`DocsPage.vue`: add a prominent panel at the top of the page — "Give this to your AI" — with a
copy-to-clipboard button that fetches `/api/docs/prompt`, copies the response text, and shows a
transient confirmation; plus the literal URL `/api/docs/prompt` displayed so a user can hand the
URL itself to an agent. Keep the existing API reference and webhook tester below it, and remove
any link, tab or inline copy that sends the reader to one of the deleted documents. Follow the
repo's Vue conventions (`<script setup lang="ts">`, `ref` for primitives, kebab-case emits).
Sweep `frontend/src` for every reference to the four documents deleted in Task 1 and confirm
zero remaining hits when finished.
<!-- planner-discipline-allow: BOTFIGHTS-POLLING, BOTFIGHTS-WEBHOOK, BOTFIGHTS-EASY, BOT_SETUP -->
`e2e/signup-bot.spec.ts`: add one test that navigates to the docs page and asserts the copy
affordance is visible, and one that fetches `/api/docs/prompt` through the page's `request`
fixture and asserts a 200 with a body containing the registration endpoint path — that is the
automated stand-in for "an agent could actually consume this".
</action>
<verify>
<automated>cd /home/archipelago/Projects/botfight && pnpm exec vue-tsc --noEmit -p frontend/tsconfig.json && test -z "$(grep -rl 'BOTFIGHTS-POLLING\|BOTFIGHTS-WEBHOOK\|BOTFIGHTS-EASY\|BOT_SETUP' frontend/src || true)"</automated>
</verify>
<acceptance_criteria>
- `cd /home/archipelago/Projects/botfight && pnpm exec vue-tsc --noEmit -p frontend/tsconfig.json` exits 0.
- `grep -rl 'BOTFIGHTS-POLLING\|BOTFIGHTS-WEBHOOK\|BOTFIGHTS-EASY\|BOT_SETUP' /home/archipelago/Projects/botfight/frontend/src` returns nothing.
- `grep -q 'YOUR_BOT_SECRET' /home/archipelago/Projects/botfight/frontend/src/pages/JoinBoutPage.vue` succeeds (credential substitution retained).
- `grep -q 'api/docs/prompt' /home/archipelago/Projects/botfight/frontend/src/pages/DocsPage.vue` succeeds.
- `grep -q 'api/docs/prompt' /home/archipelago/Projects/botfight/e2e/signup-bot.spec.ts` succeeds.
- `cd /home/archipelago/Projects/botfight && pnpm --filter frontend build` exits 0 and `test -f frontend/dist/docs/BOTFIGHTS.md` succeeds (the prompt ships in the bundle that becomes `server/public`).
- `cd /home/archipelago/Projects/botfight && pnpm lint` reports no new errors in the three touched Vue files.
</acceptance_criteria>
<done>Every in-app path to setup instructions leads to the one prompt, credentials still substitute, and the prompt is one click or one curl away.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| anonymous internet client → `GET /api/docs/prompt` | Unauthenticated read of a public document |
| server filesystem → HTTP response | A file path is resolved and its contents returned |
| prompt content → a third-party AI agent's execution context | Whatever this file says, an agent will do |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-09-12 | Information disclosure | path traversal via the prompt route | medium | mitigate | The route resolves two hard-coded constant paths derived from `import.meta.url`; no request input reaches the filesystem call (Task 2) |
| T-09-13 | Tampering | the prompt instructing agents to leak the bot secret | high | mitigate | The credentials section tells agents to hold the secret in an env var, never to commit it and never to send it anywhere but the arena host; the endpoint table marks exactly which calls take credentials (Task 1) |
| T-09-14 | Spoofing | a prompt served from an attacker-controlled origin pointing bots at a fake arena | medium | mitigate | `{{ARENA_URL}}` resolves to `PUBLIC_ARENA_URL` or the origin the prompt was fetched from, so it can never silently name a third host; the arena is HTTPS-only (plan 09-04) |
| T-09-15 | Denial of service | unauthenticated repeated reads of the prompt | low | accept | Covered by the global `/api/*` rate limit plus the existing one-hour cache header on `/api/docs/*` |
</threat_model>
<verification>
- `cd /home/archipelago/Projects/botfight && pnpm vitest run --project server` — green.
- `cd /home/archipelago/Projects/botfight && pnpm --filter frontend build` — exits 0 and emits `frontend/dist/docs/BOTFIGHTS.md`.
- `cd /home/archipelago/Projects/botfight && pnpm test:e2e -- e2e/signup-bot.spec.ts` — green (runs against a local dev server per `e2e/playwright.config.ts`).
- Maps to `09-VALIDATION.md` row "BOT-02 | prompt-documented flow works exactly as written | e2e |
`pnpm test:e2e -- e2e/signup-bot.spec.ts` (extended)". The end-to-end proof that a real cloud agent
can build a bot from this prompt alone is the human checkpoint in plan 09-07.
</verification>
<success_criteria>
- Exactly one setup document exists and it contains registration, credentials, both protocols, every
endpoint and every response format.
- `GET /api/docs/prompt` returns it as markdown with a working arena base URL.
- No UI surface or repo file points at the deleted documents.
- The in-app personalised-credentials flow still works.
</success_criteria>
<output>
Create `.planning/phases/09-botfights-platform-upgrade/09-03-SUMMARY.md` when done.
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>