351 lines
25 KiB
Markdown
351 lines
25 KiB
Markdown
---
|
|||
|
|
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>
|