Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/db/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ import { DEFAULT_COMMAND_AUTHORIZATION_POLICY, normalizeCommandAuthorizationPoli
import { normalizeContributorBlacklist } from "../settings/contributor-blacklist";
import { normalizeAutonomyPolicy, normalizeAutoMaintainPolicy, DEFAULT_AUTO_MAINTAIN_POLICY } from "../settings/autonomy";
import { decryptSecret, encryptSecret, sha256Hex } from "../utils/crypto";
import { jsonString, nowIso, parseJson, repoParts } from "../utils/json";
import { errorMessage, jsonString, nowIso, parseJson, repoParts } from "../utils/json";
import { PUBLIC_LOCAL_PATH_SCRUB_PATTERN } from "../signals/redaction";

const MAX_STORED_BODY_CHARS = 4000;
Expand Down Expand Up @@ -2063,11 +2063,19 @@ export async function getProductUsageRollupStatus(
// Global agent kill-switch (#audit-§5.2). A DB-backed emergency brake an operator flips with one row (no
// redeploy), complementing the env-var AGENT_ACTIONS_PAUSED hard backstop. Fail-OPEN on a read error (return
// false): a transient D1 hiccup must not by itself halt the whole fleet, and the env var is the hard backstop.
// The fail-open VALUE is an intentional, tested tradeoff — but it must never be SILENT: an operator who flips
// this during an incident concurrent with a D1 hiccup (or on a self-host instance that never ran migration
// 0059, or whose singleton row was later lost to a backup restore / manual cleanup) needs a visible signal that
// the kill-switch may not have actually engaged, not silent normal-looking operation. (#2125)
export async function isGlobalAgentFrozen(env: Env): Promise<boolean> {
try {
const row = await env.DB.prepare("SELECT frozen FROM global_agent_controls WHERE id = 'singleton'").first<{ frozen: number }>();
if (!row) {
console.warn(JSON.stringify({ ev: "global_kill_switch_row_missing", message: "global_agent_controls has no singleton row — treating as unfrozen; re-run migrations or re-seed the row" }));
}
return row?.frozen === 1;
} catch {
} catch (error) {
console.warn(JSON.stringify({ ev: "global_kill_switch_read_error", message: errorMessage(error).slice(0, 200) }));
return false;
}
}
Expand Down
10 changes: 8 additions & 2 deletions src/review/ops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,12 +156,18 @@ export interface OpsHealthDeps {
export const defaultOpsHealthDeps: OpsHealthDeps = {
validateAgentConfig: () => [],
// The DB-backed global kill-switch (#audit-§5.2): /status now reports the REAL freeze state instead of a
// hardcoded false. Raw SQL keeps this module self-contained; fail-open on a read error.
// hardcoded false. Raw SQL keeps this module self-contained; fail-open on a read error — but this is the
// operator-facing health surface used to CONFIRM a freeze took effect, so a swallowed read failure must be
// visible, not silently reported as an ordinary "unfrozen" (#2125).
isFrozen: async (env) => {
try {
const row = await env.DB.prepare("SELECT frozen FROM global_agent_controls WHERE id = 'singleton'").first<{ frozen: number }>();
if (!row) {
console.warn(JSON.stringify({ ev: "global_kill_switch_row_missing", message: "global_agent_controls has no singleton row — /status will report unfrozen" }));
}
return row?.frozen === 1;
} catch {
} catch (error) {
console.warn(JSON.stringify({ ev: "global_kill_switch_read_error", message: error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200) }));
return false;
}
},
Expand Down
17 changes: 17 additions & 0 deletions test/unit/agent-action-executor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,23 @@ describe("executeAgentMaintenanceActions (#778 gate stack)", () => {
expect(await isGlobalAgentFrozen(broken)).toBe(false);
});

it("isGlobalAgentFrozen's fail-open is never SILENT — a read error is observable, not indistinguishable from a genuine unfrozen state (#2125)", async () => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
const broken = { ...createTestEnv({}), DB: null } as unknown as Env;
expect(await isGlobalAgentFrozen(broken)).toBe(false);
expect(warn).toHaveBeenCalledWith(expect.stringContaining("global_kill_switch_read_error"));
warn.mockRestore();
});

it("isGlobalAgentFrozen also warns (but still fails open) when the table exists but the singleton row is absent (#2125)", async () => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
const env = createTestEnv({});
await env.DB.prepare("DELETE FROM global_agent_controls WHERE id = 'singleton'").run();
expect(await isGlobalAgentFrozen(env)).toBe(false);
expect(warn).toHaveBeenCalledWith(expect.stringContaining("global_kill_switch_row_missing"));
warn.mockRestore();
});

it("auto_with_approval: stages the action (queued) instead of executing", async () => {
const env = createTestEnv({});
const outcomes = await executeAgentMaintenanceActions(env, ctx(), [{ ...merge, requiresApproval: true }]);
Expand Down
30 changes: 29 additions & 1 deletion test/unit/ops.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import {
computeAgentHealth,
computeCalibration,
Expand All @@ -20,6 +20,34 @@ describe("defaultOpsHealthDeps.isFrozen — DB-backed global freeze (#audit-§5.
const broken = { ...env, DB: null } as unknown as Env;
expect(await defaultOpsHealthDeps.isFrozen(broken, "owner/repo")).toBe(false); // fail-open on a read error
});

it("warns (but still fails open) on a read error and on an absent singleton row (#2125)", async () => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
const env = createTestEnv();
const broken = { ...env, DB: null } as unknown as Env;
expect(await defaultOpsHealthDeps.isFrozen(broken, "owner/repo")).toBe(false);
expect(warn).toHaveBeenCalledWith(expect.stringContaining("global_kill_switch_read_error"));
warn.mockClear();

await env.DB.prepare("DELETE FROM global_agent_controls WHERE id = 'singleton'").run();
expect(await defaultOpsHealthDeps.isFrozen(env, "owner/repo")).toBe(false);
expect(warn).toHaveBeenCalledWith(expect.stringContaining("global_kill_switch_row_missing"));
warn.mockRestore();
});

it("formats a non-Error throw (e.g. a driver rejecting with a plain string) without crashing", async () => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
const thrown: Env = {
DB: {
prepare: () => {
throw "driver exploded"; // eslint-disable-line no-throw-literal -- exercising the non-Error catch arm
},
} as unknown as Env["DB"],
} as unknown as Env;
expect(await defaultOpsHealthDeps.isFrozen(thrown, "owner/repo")).toBe(false);
expect(warn).toHaveBeenCalledWith(expect.stringContaining("driver exploded"));
warn.mockRestore();
});
});

// ── computeCalibration (ported from reviewbot test/calibration.test.ts) ──────────────────────────
Expand Down
Loading