fix(setup-guide): stop leaking proxy-mode local addresses into the AI setup prompt
CI / check (push) Failing after 6m17s
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:
@@ -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'
|
||||
|
||||
@@ -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 */ }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user