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>
This commit is contained in:
Mikael Hugo 2026-05-15 19:59:03 +02:00
parent 15ae3d02b7
commit 5b831e587b

View file

@ -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;