[object Object]

← 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 →