← back to Kickbacks Ai Upstream
src/activation/bootCanary.ts
122 lines
import * as vscode from "vscode";
import { homedir } from "node:os";
import { join } from "node:path";
import { existsSync, writeFileSync, mkdirSync, unlinkSync, statSync,
readFileSync } from "node:fs";
import type { DebugController } from "../debug";
import type { TargetAdapter } from "../adapters/types";
import { canPatch, suspendServing } from "../servingGate";
import { dlog } from "../log";
import { errMsg } from "../util/errMsg";
const CANARY_PATH = join(homedir(), ".vibe-ads", "boot.canary");
const SETTLE_MS = 5_000;
const CANARY_STALE_MS = 90 * 1000;
const FIRST_RUN_KEY = "kickbacks.firstRun.completed";
/** `firstRun` is true exactly once — the activation that flips
* FIRST_RUN_KEY (i.e. the install). The crash-recovery path reports
* false (no patch was applied, so a reload nudge would be a lie) and
* leaves the key unset, so the next clean activation still counts.
*
* `anyTargetCompatible` (optional) widens the clean-boot auto-enable gate
* to EITHER webview target: a Codex-only machine (no/incompatible Claude
* Code) must still persist K_ON on first run, or it never serves —
* DebugController.apply() already treats a Codex-only patch as success.
* Omitted ⇒ falls back to the Claude-only preflight (legacy call shape). */
export async function setupBootCanary(
adapter: TargetAdapter,
debugCtl: DebugController,
ctx: vscode.ExtensionContext,
anyTargetCompatible?: boolean,
): Promise<{ firstRun: boolean }> {
let firstRun = false;
let canaryFromCrash = false;
// The settle timer below may only unlink the canary whose content it owns
// (crash path: the file it observed; clean path: the token it wrote). The
// canary lives in the shared ~/.vibe-ads, so a second VS Code window (or a
// parallel test worker) may re-write it at any time — an unguarded unlink
// would strip THAT activation's crash protection.
let settleToken = "";
try {
if (existsSync(CANARY_PATH)) {
settleToken = readFileSync(CANARY_PATH, "utf8");
const age = Date.now() - statSync(CANARY_PATH).mtimeMs;
if (age < CANARY_STALE_MS) canaryFromCrash = true;
}
} catch { /* ignore */ }
if (canaryFromCrash) {
// Suspend the WHOLE session's automatic patch writers (wave 2, audit
// #14) — pre-fix only the calls below were skipped and the production
// activation path re-patched seconds after the toast. The suspension
// lifts only on an explicit user re-enable (DebugController.setOn(true)),
// matching the toast's "manually re-enable" wording.
suspendServing();
dlog("ext", "boot.canary.skip", {
reason: "prior activation didn't settle (likely VS Code crash mid-patch)" });
try {
await vscode.window.showWarningMessage?.(
"Kickbacks: prior activation didn't complete cleanly — skipping " +
"automatic patch this run. Click the status bar to manually " +
"re-enable once you're sure VS Code is stable.");
} catch { /* no-op (test mock may lack showWarningMessage) */ }
} else {
try {
mkdirSync(join(homedir(), ".vibe-ads"), { recursive: true });
settleToken = String(Date.now());
writeFileSync(CANARY_PATH, settleToken);
} catch { /* canary is best-effort */ }
try {
const targetOk = anyTargetCompatible ?? adapter.preflight().compatible;
// Auto-enable on a clean boot only when the consent gate allows it
// (never-toggled first run, or injection was ON before the last
// sign-out). PRESERVE a deliberate "Disable Kickbacks" — the old
// condition re-enabled on every boot, stomping an explicit opt-out
// (audit EXT-01 / 2A-02). canPatch() additionally blocks the
// auto-enable's setOn(true) apply on a persisted-kill boot (wave 2,
// audit #19) — setOn itself is the MANUAL path and stays ungated.
if (targetOk && canPatch() && !debugCtl.on()
&& debugCtl.shouldAutoEnableOnSignIn()) {
await debugCtl.setOn(true);
dlog("ext", "boot.autoenable", { applied: true });
}
if (ctx.globalState.get<boolean>(FIRST_RUN_KEY) !== true) {
firstRun = true;
await ctx.globalState.update(FIRST_RUN_KEY, true);
}
} catch (e) {
dlog("ext", "boot.autoenable.error",
{ msg: errMsg(e) });
}
await debugCtl.reapplyIfOn();
try {
const cycleKill = join(homedir(), ".vibe-ads", "no-boot-cycle.enabled");
if (existsSync(cycleKill)) {
dlog("ext", "boot.cycle.skip", { reason: "sentinel" });
} else {
dlog("ext", "boot.cycle.start", {});
const r = debugCtl.cyclePatch();
dlog("ext", "boot.cycle.done", { ok: r.ok, reason: r.reason });
}
} catch (e) {
dlog("ext", "boot.cycle.error",
{ msg: errMsg(e) });
}
}
// Clear the canary once VS Code has been alive for SETTLE_MS — but only
// if it is still OUR canary (see settleToken above).
setTimeout(() => {
try {
if (readFileSync(CANARY_PATH, "utf8") === settleToken) {
unlinkSync(CANARY_PATH);
}
} catch { /* ignore */ }
}, SETTLE_MS).unref?.();
return { firstRun };
}