fix(setup-guide): stop leaking proxy-mode local addresses into the AI setup prompt
CI / check (push) Failing after 6m17s

JoinBoutPage.vue and BotProfilePage.vue's "copy AI setup guide" flows both
fetched the static /docs/BOTFIGHTS.md file and substituted {{ARENA_URL}}
client-side with window.location.origin. On an instance running in proxy
mode (ARENA_UPSTREAM_URL set), that substitutes whatever address the
browser happens to be on — e.g. this node's own LAN/Tailscale IP — into a
guide meant to be handed to an external AI agent, which then can't reach
that address at all (private/overlay network, no route from outside).

Root cause: the static file bypasses arena-proxy entirely (it only mounts
on /api/*), so the substitution had no way to know about proxy mode.

Fix: both flows now fetch the server-rendered /api/docs/prompt instead.
That route is under /api/*, so arena-proxy transparently forwards it to
the real upstream arena in proxy mode, which resolves {{ARENA_URL}} to its
own correct, externally-reachable origin (server/src/routes/docs.ts,
unchanged, already correct) — same fix class as the JoinBoutPage
mode-picker guide-banner change (603e09b), same root cause discovered via
a live demo incident (a bot got a Tailscale address in its setup guide and
correctly refused to act on it).

Known follow-up, not fixed here: DocsPage.vue's `promptUrl` display link
(`${window.location.origin}/api/docs/prompt`) has the same class of issue
for the *link itself* (not its content) — lower risk, deferred.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-07-31 06:37:55 -04:00
co-authored by Claude Fable 5
parent 603e09b6d8
commit ffd4dfd25f
2 changed files with 27 additions and 5 deletions
+9 -2
View File
@@ -298,11 +298,18 @@ async function loadGuide() {
guideContent.value = ''
guideCopied.value = false
try {
const res = await fetch('/docs/BOTFIGHTS.md')
// Fetch the server-rendered /api/docs/prompt, not the static
// /docs/BOTFIGHTS.md file — the static file's {{ARENA_URL}} has no
// choice but to be substituted client-side with window.location.origin,
// which on a proxy-mode instance (ARENA_UPSTREAM_URL set) is this
// node's own local/LAN/Tailscale address, not the real externally-
// reachable arena. /api/docs/prompt is under /api/*, so arena-proxy
// forwards it to the real upstream arena in proxy mode, which resolves
// {{ARENA_URL}} to its own correct origin (see server/src/routes/docs.ts).
const res = await fetch('/api/docs/prompt')
let content = await res.text()
content = content.replace(/YOUR_BOT_ID/g, regeneratedBotId.value)
content = content.replace(/YOUR_BOT_SECRET/g, regeneratedSecret.value)
content = content.replace(/\{\{ARENA_URL\}\}/g, window.location.origin)
guideContent.value = content
} catch {
guideContent.value = '# Failed to load setup guide'
+18 -3
View File
@@ -522,8 +522,21 @@ const setupContentCopied = ref(false)
// What DOES need to visibly react to the picker is `modeHint` below, so a
// click still produces an immediate, obvious change instead of looking inert.
// IMPORTANT: fetch the server-rendered /api/docs/prompt, NOT the static
// /docs/BOTFIGHTS.md file. The static file is never proxy-aware — on an
// instance running in proxy mode (ARENA_UPSTREAM_URL set), the raw file's
// {{ARENA_URL}} would have to be substituted client-side with
// window.location.origin, which is whatever address the browser happens to
// be on (e.g. this node's own LAN/Tailscale IP) — reachable on that network,
// but not the real, externally-reachable arena, and useless to an external
// bot with no route to that address. /api/docs/prompt is mounted under
// /api/*, so arena-proxy transparently forwards it to the real upstream
// arena in proxy mode, which resolves {{ARENA_URL}} to ITS OWN correct,
// externally-reachable origin — the same substitution already proven
// correct (see server/src/routes/docs.ts). Standalone instances (no
// ARENA_UPSTREAM_URL) get their own correct origin either way.
function setupDocPath() {
return '/docs/BOTFIGHTS.md'
return '/api/docs/prompt'
}
function setupDocName() {
@@ -546,9 +559,10 @@ async function toggleSetupContent() {
try {
const res = await fetch(setupDocPath())
let content = await res.text()
// {{ARENA_URL}} is already resolved server-side (proxy-aware — see
// setupDocPath() above); only the bot-specific placeholders remain.
content = content.replace(/YOUR_BOT_ID/g, botId.value)
content = content.replace(/YOUR_BOT_SECRET/g, botSecret.value)
content = content.replace(/\{\{ARENA_URL\}\}/g, window.location.origin)
setupContent.value = content
} catch {
setupContent.value = '# Failed to load setup guide'
@@ -568,9 +582,10 @@ async function copyFullPrompt() {
try {
const res = await fetch(setupDocPath())
let content = await res.text()
// {{ARENA_URL}} is already resolved server-side (proxy-aware — see
// setupDocPath() above); only the bot-specific placeholders remain.
content = content.replace(/YOUR_BOT_ID/g, botId.value)
content = content.replace(/YOUR_BOT_SECRET/g, botSecret.value)
content = content.replace(/\{\{ARENA_URL\}\}/g, window.location.origin)
setupContent.value = content
} catch { /* fall through with empty content */ }
}