← back to Model Arena
Harden ARENA_PAUSED to fail-closed (Cody/DTD): live sentinel + spend tripwire
a1b5af20868a2ba3a6397b73b7a59669f9d5bfb0 · 2026-09-23 09:03:38 -0700 · Steve Abrams
The env var alone lives only in the launcher — a git pull, hand-run node server.js,
or the daily-challenge job reloading would bypass it, and launchd KeepAlive would then
resurrect a spending arena. isPaused() now = env OR a live ~/.claude/model-arena.PAUSED
sentinel (outside the repo, re-checked at every spend decision; fs error = paused).
generateMetered/generateMeteredTools refuse + log to spend-tripwire.jsonl if reached
while paused. Verified: sentinel-only boot pauses; both-absent boot does not (negative
test can go red).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
Diff
commit a1b5af20868a2ba3a6397b73b7a59669f9d5bfb0
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Sep 23 09:03:38 2026 -0700
Harden ARENA_PAUSED to fail-closed (Cody/DTD): live sentinel + spend tripwire
The env var alone lives only in the launcher — a git pull, hand-run node server.js,
or the daily-challenge job reloading would bypass it, and launchd KeepAlive would then
resurrect a spending arena. isPaused() now = env OR a live ~/.claude/model-arena.PAUSED
sentinel (outside the repo, re-checked at every spend decision; fs error = paused).
generateMetered/generateMeteredTools refuse + log to spend-tripwire.jsonl if reached
while paused. Verified: sentinel-only boot pauses; both-absent boot does not (negative
test can go red).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
server.js | 29 +++++++++++++++++++++++++----
1 file changed, 25 insertions(+), 4 deletions(-)
diff --git a/server.js b/server.js
index 1d4b81a..648d238 100644
--- a/server.js
+++ b/server.js
@@ -43,6 +43,25 @@ const DIR = __dirname;
// DATA_DIR keeps verification and canary servers from touching the live arena ledger.
const DATA = process.env.DATA_DIR ? path.resolve(process.env.DATA_DIR) : path.join(DIR, 'data');
const ART = path.join(DATA, 'artifacts');
+// Fail-closed pause guard (Cody/DTD verdict 2026-09-23): the ARENA_PAUSED env is read once at
+// boot and lives only in the launcher — a `git pull`, a hand-run `node server.js`, or the
+// daily-challenge job reloading would all silently bypass it, and launchd KeepAlive would then
+// resurrect a *spending* arena through every crash. So ALSO honor a live sentinel file, OUTSIDE
+// the repo (git can't remove it), re-checked at EVERY spend decision. Either signal = paused; any
+// fs error = paused (never fail open). Unpause = unset the env AND remove the sentinel file.
+const PAUSE_SENTINEL = path.join(os.homedir(), '.claude', 'model-arena.PAUSED');
+function isPaused() {
+ if (ARENA_PAUSED) return true;
+ try { return fs.existsSync(PAUSE_SENTINEL); } catch { return true; }
+}
+// Spend tripwire: a metered call reaching a spend point while paused should be impossible
+// (runModel gates first). If it ever does, REFUSE the spend and record it loudly so a bypass is
+// DETECTED, not silent.
+function tripwirePausedSpend(where, model) {
+ const rec = { ts: new Date().toISOString(), event: 'BLOCKED_PAID_CALL_WHILE_PAUSED', where, model: model || null };
+ try { fs.appendFileSync(path.join(DATA, 'spend-tripwire.jsonl'), JSON.stringify(rec) + '\n'); } catch {}
+ console.error('[model-arena] TRIPWIRE — refused a paid ' + (model || '?') + ' call at ' + where + ' while PAUSED');
+}
const CH_FILE = path.join(DATA, 'challenges.json');
const VOTES_FILE = path.join(DATA, 'votes.jsonl');
const COST_FILE = path.join(DATA, 'costlog.jsonl');
@@ -144,7 +163,7 @@ const MAX_RESUMES = 4;
const resumeOnBoot = [];
for (const c of challenges) {
if (c.judging) c.judging = false; // a restart mid-judge would otherwise block judging forever
- if (ARENA_PAUSED) continue; // paused: don't burn the resume budget — leave interrupted runs for a later unpaused boot
+ if (isPaused()) continue; // paused: don't burn the resume budget — leave interrupted runs for a later unpaused boot
for (const r of c.runs) {
const corrupt = r.started_at && r.finished_at && r.finished_at < r.started_at;
const interrupted = r.status === 'running' || r.status === 'queued' ||
@@ -407,6 +426,7 @@ function meteredCost(provider, inTok, outTok) {
}
async function generateMetered(m, prompt) {
+ if (isPaused()) { tripwirePausedSpend('generateMetered', m && m.id); throw new Error('arena paused — metered call refused'); }
const key = process.env[m.envKey];
if (!key) throw new Error(m.envKey + ' not set — metered model unavailable');
let text, inTok = 0, outTok = 0;
@@ -456,6 +476,7 @@ async function runDesignTool(name, args, calls) {
// OpenAI-compatible function-calling loop (openai / xai / moonshot share the wire format)
async function generateMeteredTools(m, prompt) {
+ if (isPaused()) { tripwirePausedSpend('generateMeteredTools', m && m.id); throw new Error('arena paused — metered call refused'); }
const key = process.env[m.envKey];
if (!key) throw new Error(m.envKey + ' not set — metered model unavailable');
const base = { moonshot: 'https://api.moonshot.ai/v1', openai: 'https://api.openai.com/v1', xai: 'https://api.x.ai/v1', openrouter: 'https://openrouter.ai/api/v1' }[m.provider];
@@ -584,7 +605,7 @@ function maybeAutoJudge(challenge) {
}
function runModel(challenge, modelId) {
- if (ARENA_PAUSED) return; // paused: never dispatch a model run (no metered spend)
+ if (isPaused()) return; // paused (env OR live sentinel): never dispatch a model run (no metered spend)
const m = MODELS.find(x => x.id === modelId);
const run = challenge.runs.find(r => r.model === modelId);
if (!m || !run) return;
@@ -1077,8 +1098,8 @@ iframe{width:100%;height:460px;border:0;background:#000}.c.win iframe{height:560
});
server.listen(PORT, function () {
- console.log('[model-arena] http://localhost:' + this.address().port + (BASIC_AUTH ? ' (basic auth on)' : '') + (ARENA_PAUSED ? ' [CHALLENGES PAUSED — no model runs, no spend]' : ''));
- if (!ARENA_PAUSED && resumeOnBoot.length) {
+ console.log('[model-arena] http://localhost:' + this.address().port + (BASIC_AUTH ? ' (basic auth on)' : '') + (isPaused() ? ' [CHALLENGES PAUSED — no model runs, no spend]' : ''));
+ if (!isPaused() && resumeOnBoot.length) {
console.log('[model-arena] resuming ' + resumeOnBoot.length + ' run(s) interrupted by restart');
for (const j of resumeOnBoot) runModel(j.c, j.model);
}
← 969f6b3 auto-data-snapshot: 2026-09-23T08:59:35 (1 data files) — dat
·
back to Model Arena
·
Add durable keepalive wrapper + launchd plist draft (paused, 0183631 →