From 5b831e587ba25874688c6400d9fe198aa5fd0797 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Fri, 15 May 2026 19:59:03 +0200 Subject: [PATCH] feat(headless): gate v1 string-matching fallback behind env var Require SF_HEADLESS_ALLOW_V1_FALLBACK=1 to use legacy v1 fallback. Default behavior now exits with error when v2 init fails, preventing silent degradation to less reliable protocol matching. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/headless.ts | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/headless.ts b/src/headless.ts index 5f981255c..2a069e0e6 100644 --- a/src/headless.ts +++ b/src/headless.ts @@ -105,6 +105,7 @@ import { } from "./resources/extensions/sf/trace-collector.js"; const HEADLESS_HEARTBEAT_INTERVAL_MS = 60_000; +const HEADLESS_LEGACY_FALLBACK_ENV = "SF_HEADLESS_ALLOW_V1_FALLBACK"; interface HeadlessTimeoutSolverEvalRecord { runId: string; @@ -1982,17 +1983,27 @@ async function runHeadlessOnce( process.exit(1); } - // v2 protocol negotiation — attempt init for structured completion events - let _v2Enabled = false; - try { - await client.init({ clientId: "sf-headless" }); - _v2Enabled = true; - } catch (initErr) { - const reason = initErr instanceof Error ? initErr.message : String(initErr); - process.stderr.write( - `[headless] Warning: v2 init failed (${reason}), falling back to v1 string-matching\n`, - ); - } + // v2 protocol negotiation — attempt init for structured completion events + let _v2Enabled = false; + try { + await client.init({ clientId: "sf-headless" }); + _v2Enabled = true; + } catch (initErr) { + const reason = initErr instanceof Error ? initErr.message : String(initErr); + if (process.env[HEADLESS_LEGACY_FALLBACK_ENV] !== "1") { + process.stderr.write( + `[headless] v2 init failed (${reason}); refusing legacy v1 string-matching fallback. Set ${HEADLESS_LEGACY_FALLBACK_ENV}=1 only for explicit recovery.\n`, + ); + await client.stop().catch(() => {}); + if (timeoutTimer) clearTimeout(timeoutTimer); + if (idleTimer) clearTimeout(idleTimer); + if (heartbeatTimer) clearInterval(heartbeatTimer); + process.exit(EXIT_ERROR); + } + process.stderr.write( + `[headless] Warning: v2 init failed (${reason}), falling back to v1 string-matching because ${HEADLESS_LEGACY_FALLBACK_ENV}=1\n`, + ); + } clientStarted = true;