@kill-switch/agent-guard 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,147 @@
1
+ # @kill-switch/agent-guard
2
+
3
+ **Kill Switch for coding agents.** Stop a runaway Claude Code / Cursor / Aider session
4
+ from racking up an LLM bill — before it becomes a $4,200 weekend, an $87k month, or a
5
+ [$500M month](https://www.axios.com/) "after failing to put usage limits on Claude
6
+ licenses for employees."
7
+
8
+ A coding agent runs a reasoning loop — read, edit, validate, re-check — and **re-sends its
9
+ entire accumulated context on every tool call**. Cost compounds silently. agent-guard puts
10
+ a hard ceiling on that loop with two complementary surfaces that share one budget:
11
+
12
+ | Surface | What it is | Stops | Works with |
13
+ |---|---|---|---|
14
+ | **Hook** | A Claude Code `PreToolUse` / `UserPromptSubmit` / `Stop` hook that reads the live transcript, prices real token usage, warns at the soft cap and **denies the next tool call** at the hard cap | A single Claude Code session, gracefully | Claude Code |
15
+ | **Proxy** | A local metering reverse-proxy on the agent's API base URL that counts usage from real responses and returns **HTTP 402** at the cap | *Any* agent — the API literally stops answering | Claude Code, Cursor, Aider, raw scripts |
16
+
17
+ The hook is the friendly, native stop. The proxy is the dumb hard wall that can't be argued
18
+ past. Both feed one ledger with **two budget scopes**:
19
+
20
+ - **Per-session** — catches a single runaway run.
21
+ - **Daily rolling 24h** — catches many small sessions quietly adding up.
22
+
23
+ …each with a **soft cap** (warn + alert) and a **hard cap** (block).
24
+
25
+ ## Install
26
+
27
+ ```sh
28
+ npm i -g @kill-switch/agent-guard # provides `agent-guard` and `ksg`
29
+ ```
30
+
31
+ Or use it through the main Kill Switch CLI as `ks guard …` (same engine, one
32
+ shared ledger/budget) — see [`packages/cli`](../cli).
33
+
34
+ ### Developing from this monorepo
35
+
36
+ The hook is wired into Claude Code by **absolute path to `dist/cli.js`**, so it
37
+ must be built before `install`, and the bare `agent-guard` / `ksg` commands only
38
+ land on your `PATH` after a link or publish:
39
+
40
+ ```sh
41
+ # from the repo root
42
+ npm run build:agent-guard # compile src → dist (required before install/link)
43
+ npm run test:agent-guard # 21 unit tests
44
+
45
+ # put `agent-guard` / `ksg` on PATH for local dev
46
+ cd packages/agent-guard && npm link
47
+ agent-guard --help # now resolves
48
+
49
+ # unlink when done
50
+ npm rm -g @kill-switch/agent-guard
51
+ ```
52
+
53
+ > ⚠️ If `agent-guard` reports `command not found` (e.g. when a runaway session
54
+ > hits the cap and the recovery command won't run), it just means the package
55
+ > isn't linked/published yet. The block message always prints an **absolute-path**
56
+ > fallback so recovery works regardless, and `ks guard …` works whenever the
57
+ > `ks` CLI is installed. You can also always pause with zero tooling:
58
+ > `touch ~/.kill-switch/agent-guard/PAUSED`.
59
+
60
+ ## Quick start — Claude Code (hook)
61
+
62
+ ```sh
63
+ # Wire the hook into ./.claude/settings.json (use --global for ~/.claude)
64
+ agent-guard install
65
+
66
+ # Set your caps (USD)
67
+ agent-guard config --session-soft 5 --session-hard 20 --daily-soft 25 --daily-hard 100
68
+
69
+ # See where you stand any time
70
+ agent-guard status
71
+ ```
72
+
73
+ That's it. On every tool call the hook recomputes the session's real spend from the
74
+ transcript. Cross the soft cap → Claude sees a warning and you get an alert. Hit the hard
75
+ cap → the next tool call is **denied** with a reason, halting the agent.
76
+
77
+ ## Quick start — any other agent (proxy)
78
+
79
+ ```sh
80
+ agent-guard proxy # listens on :8787, meters Anthropic by default
81
+ # point your agent at it:
82
+ ANTHROPIC_BASE_URL=http://localhost:8787 claude # (if NOT using the hook — see caveat)
83
+ OPENAI_BASE_URL=http://localhost:8787/v1 aider # agent-guard proxy --flavor openai
84
+ ```
85
+
86
+ At the hard cap the proxy returns `402 kill_switch_budget_exceeded` instead of forwarding —
87
+ the agent can't spend another token.
88
+
89
+ > ⚠️ **Don't run Claude Code through *both* the hook and the proxy** — they'd each meter the
90
+ > same dollars and double-count. Hook for Claude Code; proxy for everything else.
91
+
92
+ ## Budgets
93
+
94
+ Resolution order (later wins): built-in defaults → `~/.kill-switch/agent-guard/config.json`
95
+ → environment variables. Env override lets you tighten a single risky run:
96
+
97
+ ```sh
98
+ AGENT_GUARD_SESSION_HARD=10 claude # one-off $10 ceiling
99
+ ```
100
+
101
+ | Env var | Meaning | Default |
102
+ |---|---|---|
103
+ | `AGENT_GUARD_SESSION_SOFT` | per-session warn (USD) | 5 |
104
+ | `AGENT_GUARD_SESSION_HARD` | per-session block (USD) | 20 |
105
+ | `AGENT_GUARD_DAILY_SOFT` | rolling-24h warn (USD) | 25 |
106
+ | `AGENT_GUARD_DAILY_HARD` | rolling-24h block (USD) | 100 |
107
+
108
+ A cap of `0` disables that check.
109
+
110
+ ## Alerts
111
+
112
+ On the first soft/hard trip per scope, agent-guard:
113
+
114
+ 1. appends an event to `~/.kill-switch/agent-guard/events.jsonl` (local audit trail),
115
+ 2. posts to Slack if `KILL_SWITCH_SLACK_WEBHOOK` (or `config --slack-webhook`) is set,
116
+ 3. reports to the Kill Switch dashboard if `KILL_SWITCH_API_KEY` is set, so agent kills sit
117
+ alongside your cloud-account kills.
118
+
119
+ All network alerts are best-effort with a short timeout — a down endpoint never delays the
120
+ agent.
121
+
122
+ ## Pricing
123
+
124
+ Built-in rates for current Claude and OpenAI models (USD/1M tokens), including Anthropic
125
+ cache multipliers (cache write = input × 1.25, cache read = input × 0.10) — the buckets a
126
+ context-replaying agent loop hits hardest. Unknown models fall back to premium Sonnet-class
127
+ rates so the guard never *under*-counts. Override any model in
128
+ `~/.kill-switch/agent-guard/pricing.json`.
129
+
130
+ ## Commands
131
+
132
+ ```
133
+ agent-guard install [--global] [--command <cmd>] wire the Claude Code hook
134
+ agent-guard proxy [--port 8787] [--flavor anthropic|openai] [--upstream URL]
135
+ agent-guard status [--json] spend vs budget
136
+ agent-guard config [--session-hard N ...] view/set caps
137
+ agent-guard reset [--all|--today|--session <id>] clear the ledger
138
+ agent-guard hook (internal) Claude Code entrypoint
139
+ ```
140
+
141
+ ## How it fails
142
+
143
+ By design, **the hook fails open**: any internal error → exit 0, the agent proceeds. A buggy
144
+ guard must never brick your session. The proxy fails open too (on a metering error it still
145
+ relays the response) but the *budget check itself* fails closed at the 402 wall.
146
+
147
+ MIT · part of [Cloud Kill Switch](https://kill-switch.net)
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Breach alerting — best-effort, fire-and-forget, never throws.
3
+ *
4
+ * On a soft/hard trip we:
5
+ * 1. Always append a line to ~/.kill-switch/agent-guard/events.jsonl (local audit trail).
6
+ * 2. POST to Slack if a webhook is configured.
7
+ * 3. POST to the Guardian API if an API key is configured, so the kill shows
8
+ * up in the dashboard / existing alert channels alongside cloud-account kills.
9
+ *
10
+ * Everything network is wrapped in a short timeout; a down endpoint must not
11
+ * delay (or crash) the agent's tool call.
12
+ */
13
+ import { type GuardConfig } from "./config.js";
14
+ import type { Verdict } from "./budget.js";
15
+ export interface AlertEvent {
16
+ ts: number;
17
+ source: "hook" | "proxy";
18
+ sessionId: string;
19
+ level: Verdict["level"];
20
+ sessionUSD: number;
21
+ dailyUSD: number;
22
+ reasons: string[];
23
+ action: string;
24
+ cwd?: string;
25
+ }
26
+ /** Dispatch an alert across all configured channels. Resolves once all attempts settle. */
27
+ export declare function dispatchAlert(cfg: GuardConfig, evt: AlertEvent): Promise<void>;
package/dist/alert.js ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Breach alerting — best-effort, fire-and-forget, never throws.
3
+ *
4
+ * On a soft/hard trip we:
5
+ * 1. Always append a line to ~/.kill-switch/agent-guard/events.jsonl (local audit trail).
6
+ * 2. POST to Slack if a webhook is configured.
7
+ * 3. POST to the Guardian API if an API key is configured, so the kill shows
8
+ * up in the dashboard / existing alert channels alongside cloud-account kills.
9
+ *
10
+ * Everything network is wrapped in a short timeout; a down endpoint must not
11
+ * delay (or crash) the agent's tool call.
12
+ */
13
+ import { appendFileSync } from "node:fs";
14
+ import { eventsPath, ensureGuardDir } from "./config.js";
15
+ import { fmtUSD } from "./cost.js";
16
+ const TIMEOUT_MS = 2500;
17
+ async function postJson(url, body, headers = {}) {
18
+ const ctrl = new AbortController();
19
+ const t = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
20
+ try {
21
+ await fetch(url, {
22
+ method: "POST",
23
+ headers: { "content-type": "application/json", ...headers },
24
+ body: JSON.stringify(body),
25
+ signal: ctrl.signal,
26
+ });
27
+ }
28
+ catch {
29
+ /* best-effort */
30
+ }
31
+ finally {
32
+ clearTimeout(t);
33
+ }
34
+ }
35
+ function writeLocal(evt) {
36
+ try {
37
+ ensureGuardDir();
38
+ appendFileSync(eventsPath(), JSON.stringify(evt) + "\n");
39
+ }
40
+ catch {
41
+ /* best-effort */
42
+ }
43
+ }
44
+ function slackText(evt) {
45
+ const icon = evt.level === "block" ? "🛑" : "⚠️";
46
+ const verb = evt.level === "block" ? "BLOCKED a coding agent" : "warning on a coding agent";
47
+ return [
48
+ `${icon} *Kill Switch ${verb}*`,
49
+ `• Session: ${fmtUSD(evt.sessionUSD)} | Daily (24h): ${fmtUSD(evt.dailyUSD)}`,
50
+ `• Action: ${evt.action}`,
51
+ evt.cwd ? `• Project: \`${evt.cwd}\`` : "",
52
+ ...evt.reasons.map((r) => `• ${r}`),
53
+ ]
54
+ .filter(Boolean)
55
+ .join("\n");
56
+ }
57
+ /** Dispatch an alert across all configured channels. Resolves once all attempts settle. */
58
+ export async function dispatchAlert(cfg, evt) {
59
+ writeLocal(evt);
60
+ const tasks = [];
61
+ if (cfg.slackWebhook) {
62
+ tasks.push(postJson(cfg.slackWebhook, { text: slackText(evt) }));
63
+ }
64
+ if (cfg.apiKey && cfg.apiUrl) {
65
+ tasks.push(postJson(`${cfg.apiUrl.replace(/\/$/, "")}/agent-guard/events`, evt, { authorization: `Bearer ${cfg.apiKey}` }));
66
+ }
67
+ await Promise.allSettled(tasks);
68
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Budget evaluation — the decision core shared by hook and proxy.
3
+ *
4
+ * Two scopes, two thresholds each:
5
+ * - session: catches a single runaway run (the $4,200-weekend scenario)
6
+ * - daily rolling 24h: catches many small sessions adding up (the $87k-month)
7
+ *
8
+ * Verdict levels: ok → warn (soft cap crossed) → block (hard cap reached).
9
+ * The hard cap is a >= comparison so spend can never exceed it by design.
10
+ */
11
+ export interface Budget {
12
+ sessionSoftUSD: number;
13
+ sessionHardUSD: number;
14
+ dailySoftUSD: number;
15
+ dailyHardUSD: number;
16
+ }
17
+ export type VerdictLevel = "ok" | "warn" | "block";
18
+ export interface Verdict {
19
+ level: VerdictLevel;
20
+ /** Which scopes tripped, for dedup of warnings and for the alert payload. */
21
+ triggers: Array<{
22
+ scope: "session" | "daily";
23
+ level: "warn" | "block";
24
+ spentUSD: number;
25
+ limitUSD: number;
26
+ pct: number;
27
+ }>;
28
+ /** Human-readable summary lines. */
29
+ reasons: string[];
30
+ }
31
+ export interface Spend {
32
+ sessionUSD: number;
33
+ dailyUSD: number;
34
+ }
35
+ export declare function evaluate(spend: Spend, budget: Budget): Verdict;
36
+ /** Stable key for deduping a warn-level trigger so we alert once per scope, not per tool call. */
37
+ export declare function warnKey(scope: "session" | "daily"): string;
package/dist/budget.js ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Budget evaluation — the decision core shared by hook and proxy.
3
+ *
4
+ * Two scopes, two thresholds each:
5
+ * - session: catches a single runaway run (the $4,200-weekend scenario)
6
+ * - daily rolling 24h: catches many small sessions adding up (the $87k-month)
7
+ *
8
+ * Verdict levels: ok → warn (soft cap crossed) → block (hard cap reached).
9
+ * The hard cap is a >= comparison so spend can never exceed it by design.
10
+ */
11
+ import { fmtUSD } from "./cost.js";
12
+ function pct(spent, limit) {
13
+ return limit > 0 ? Math.round((spent / limit) * 100) : 0;
14
+ }
15
+ export function evaluate(spend, budget) {
16
+ const triggers = [];
17
+ const reasons = [];
18
+ const checks = [
19
+ { scope: "session", spent: spend.sessionUSD, soft: budget.sessionSoftUSD, hard: budget.sessionHardUSD },
20
+ { scope: "daily", spent: spend.dailyUSD, soft: budget.dailySoftUSD, hard: budget.dailyHardUSD },
21
+ ];
22
+ for (const c of checks) {
23
+ if (c.hard > 0 && c.spent >= c.hard) {
24
+ triggers.push({ scope: c.scope, level: "block", spentUSD: c.spent, limitUSD: c.hard, pct: pct(c.spent, c.hard) });
25
+ reasons.push(`${c.scope} spend ${fmtUSD(c.spent)} reached the hard cap of ${fmtUSD(c.hard)}`);
26
+ }
27
+ else if (c.soft > 0 && c.spent >= c.soft) {
28
+ triggers.push({ scope: c.scope, level: "warn", spentUSD: c.spent, limitUSD: c.soft, pct: pct(c.spent, c.soft) });
29
+ reasons.push(`${c.scope} spend ${fmtUSD(c.spent)} crossed the soft cap of ${fmtUSD(c.soft)} (${pct(c.spent, c.hard > 0 ? c.hard : c.soft)}% of hard cap)`);
30
+ }
31
+ }
32
+ const level = triggers.some((t) => t.level === "block")
33
+ ? "block"
34
+ : triggers.some((t) => t.level === "warn")
35
+ ? "warn"
36
+ : "ok";
37
+ return { level, triggers, reasons };
38
+ }
39
+ /** Stable key for deduping a warn-level trigger so we alert once per scope, not per tool call. */
40
+ export function warnKey(scope) {
41
+ return `warn:${scope}`;
42
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * agent-guard CLI — Kill Switch for coding agents.
4
+ *
5
+ * agent-guard install # wire the Claude Code hook into .claude/settings.json
6
+ * agent-guard proxy # start the token-metering proxy (hard 402 wall)
7
+ * agent-guard status # show current session + daily spend vs budget
8
+ * agent-guard config --session-hard 30
9
+ * agent-guard reset --today # clear the ledger
10
+ * agent-guard hook # (internal) invoked by Claude Code on each event
11
+ */
12
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * agent-guard CLI — Kill Switch for coding agents.
4
+ *
5
+ * agent-guard install # wire the Claude Code hook into .claude/settings.json
6
+ * agent-guard proxy # start the token-metering proxy (hard 402 wall)
7
+ * agent-guard status # show current session + daily spend vs budget
8
+ * agent-guard config --session-hard 30
9
+ * agent-guard reset --today # clear the ledger
10
+ * agent-guard hook # (internal) invoked by Claude Code on each event
11
+ */
12
+ import { Command } from "commander";
13
+ import { fileURLToPath } from "node:url";
14
+ import { runHook } from "./hook.js";
15
+ import { startProxy, resolveUpstream } from "./proxy.js";
16
+ import { loadConfig, configPath, isPaused, pauseExpiry, writePause, clearPause, pausePath, } from "./config.js";
17
+ import { loadLedger, rollingDailyCost } from "./ledger.js";
18
+ import { evaluate } from "./budget.js";
19
+ import { fmtUSD } from "./cost.js";
20
+ import { installHook, setBudget, resetLedger } from "./ops.js";
21
+ const program = new Command();
22
+ program
23
+ .name("agent-guard")
24
+ .description("Kill Switch for coding agents — stop runaway Claude Code / Cursor / Aider sessions before they rack up a bill")
25
+ .version("0.1.0");
26
+ // ── hook (internal) ────────────────────────────────────────────────────────
27
+ program
28
+ .command("hook")
29
+ .description("Claude Code hook entrypoint (reads hook JSON from stdin)")
30
+ .action(async () => {
31
+ await runHook();
32
+ });
33
+ // ── install ────────────────────────────────────────────────────────────────
34
+ program
35
+ .command("install")
36
+ .description("Wire the kill-switch hook into Claude Code settings")
37
+ .option("--global", "Install into ~/.claude/settings.json (default: project ./.claude/settings.json)")
38
+ .option("--command <cmd>", "Override the hook command (default: absolute path to this binary)")
39
+ .action((opts) => {
40
+ const cliPath = fileURLToPath(import.meta.url);
41
+ const { settingsPath, command, added } = installHook(cliPath, process.execPath, {
42
+ global: opts.global,
43
+ command: opts.command,
44
+ });
45
+ const cfg = loadConfig();
46
+ console.log(`✅ Hook installed → ${settingsPath}`);
47
+ console.log(` Events: ${added.length ? added.join(", ") : "(already present — no change)"}`);
48
+ console.log(` Command: ${command}`);
49
+ console.log("");
50
+ console.log(` Caps: session soft ${fmtUSD(cfg.budget.sessionSoftUSD)} / hard ${fmtUSD(cfg.budget.sessionHardUSD)}, ` +
51
+ `daily soft ${fmtUSD(cfg.budget.dailySoftUSD)} / hard ${fmtUSD(cfg.budget.dailyHardUSD)}`);
52
+ console.log(` Change them: agent-guard config --session-hard 30 --daily-hard 150`);
53
+ console.log("");
54
+ console.log(` For non-Claude-Code agents (Cursor/Aider/scripts), use the hard proxy instead:`);
55
+ console.log(` agent-guard proxy → ANTHROPIC_BASE_URL=http://localhost:8787 <your-agent>`);
56
+ console.log(` ⚠ Don't run Claude Code through BOTH the hook and the proxy (double counting).`);
57
+ });
58
+ // ── status ───────────────────────────────────────────────────────────────────
59
+ program
60
+ .command("status")
61
+ .description("Show current session + daily spend against the budget")
62
+ .option("--json", "Output as JSON")
63
+ .action((opts) => {
64
+ const cfg = loadConfig();
65
+ const ledger = loadLedger();
66
+ const now = Date.now();
67
+ const dailyUSD = rollingDailyCost(ledger, now);
68
+ const sessions = Object.entries(ledger.sessions)
69
+ .filter(([, s]) => now - s.lastAt < 24 * 60 * 60 * 1000)
70
+ .sort((a, b) => b[1].lastAt - a[1].lastAt);
71
+ const topSession = sessions[0]?.[1].costUSD ?? 0;
72
+ const verdict = evaluate({ sessionUSD: topSession, dailyUSD }, cfg.budget);
73
+ if (opts.json) {
74
+ console.log(JSON.stringify({
75
+ budget: cfg.budget,
76
+ dailyUSD,
77
+ verdict: verdict.level,
78
+ reasons: verdict.reasons,
79
+ sessions: sessions.map(([id, s]) => ({ id, ...s })),
80
+ }, null, 2));
81
+ return;
82
+ }
83
+ const bar = (spent, hard) => {
84
+ const pct = hard > 0 ? Math.min(100, Math.round((spent / hard) * 100)) : 0;
85
+ const filled = Math.round(pct / 5);
86
+ return `[${"█".repeat(filled)}${"░".repeat(20 - filled)}] ${pct}%`;
87
+ };
88
+ const paused = isPaused(now);
89
+ const icon = paused ? "⏸ " : verdict.level === "block" ? "🛑" : verdict.level === "warn" ? "⚠️ " : "✅";
90
+ console.log(`${icon} agent-guard — ${paused ? "PAUSED (enforcement off)" : verdict.level.toUpperCase()}`);
91
+ if (paused) {
92
+ const until = pauseExpiry();
93
+ console.log(until ? ` resumes ${new Date(until).toLocaleString()}` : " paused indefinitely — `agent-guard resume` to re-arm");
94
+ }
95
+ console.log("");
96
+ console.log(`Daily (rolling 24h): ${fmtUSD(dailyUSD)} / ${fmtUSD(cfg.budget.dailyHardUSD)} ${bar(dailyUSD, cfg.budget.dailyHardUSD)}`);
97
+ console.log("");
98
+ if (sessions.length === 0) {
99
+ console.log("No active sessions in the last 24h.");
100
+ }
101
+ else {
102
+ console.log("Active sessions (24h):");
103
+ for (const [id, s] of sessions.slice(0, 8)) {
104
+ console.log(` ${fmtUSD(s.costUSD).padStart(9)} / ${fmtUSD(cfg.budget.sessionHardUSD)} ${bar(s.costUSD, cfg.budget.sessionHardUSD)} ${id}`);
105
+ }
106
+ }
107
+ if (verdict.reasons.length) {
108
+ console.log("");
109
+ for (const r of verdict.reasons)
110
+ console.log(` • ${r}`);
111
+ }
112
+ });
113
+ // ── pause / resume (escape hatch) ────────────────────────────────────────────
114
+ program
115
+ .command("pause")
116
+ .description("Temporarily disable enforcement (escape hatch — hook & proxy fail open)")
117
+ .option("--minutes <n>", "Auto-resume after N minutes (default: indefinite)")
118
+ .action((opts) => {
119
+ const mins = opts.minutes !== undefined ? Number(opts.minutes) : NaN;
120
+ if (opts.minutes !== undefined && Number.isFinite(mins)) {
121
+ const until = Date.now() + mins * 60_000;
122
+ writePause(until);
123
+ console.log(`⏸ Enforcement paused until ${new Date(until).toLocaleString()} (${mins} min).`);
124
+ }
125
+ else {
126
+ writePause();
127
+ console.log("⏸ Enforcement paused indefinitely. Re-arm with `agent-guard resume`.");
128
+ }
129
+ console.log(` Sentinel: ${pausePath()}`);
130
+ });
131
+ program
132
+ .command("resume")
133
+ .description("Re-arm enforcement after a pause")
134
+ .action(() => {
135
+ clearPause();
136
+ console.log("✅ Enforcement re-armed.");
137
+ });
138
+ // ── proxy ────────────────────────────────────────────────────────────────────
139
+ program
140
+ .command("proxy")
141
+ .description("Start the token-metering proxy (returns HTTP 402 at the hard cap)")
142
+ .option("--port <n>", "Port to listen on", "8787")
143
+ .option("--flavor <name>", "API flavor for usage parsing: anthropic | openai", "anthropic")
144
+ .option("--upstream <url>", "Upstream origin (default: api.anthropic.com / api.openai.com)")
145
+ .action((opts) => {
146
+ const flavor = opts.flavor === "openai" ? "openai" : "anthropic";
147
+ startProxy({
148
+ port: parseInt(opts.port, 10) || 8787,
149
+ flavor,
150
+ upstream: resolveUpstream(flavor, opts.upstream),
151
+ });
152
+ });
153
+ // ── config ───────────────────────────────────────────────────────────────────
154
+ program
155
+ .command("config")
156
+ .description("View or set budget caps (written to ~/.kill-switch/agent-guard/config.json)")
157
+ .option("--session-soft <usd>", "Per-session soft cap (warn)")
158
+ .option("--session-hard <usd>", "Per-session hard cap (block)")
159
+ .option("--daily-soft <usd>", "Daily rolling soft cap (warn)")
160
+ .option("--daily-hard <usd>", "Daily rolling hard cap (block)")
161
+ .option("--slack-webhook <url>", "Slack incoming-webhook for breach alerts")
162
+ .action((opts) => {
163
+ const anySet = ["sessionSoft", "sessionHard", "dailySoft", "dailyHard", "slackWebhook"]
164
+ .some((k) => opts[k] !== undefined);
165
+ if (!anySet) {
166
+ const cfg = loadConfig();
167
+ console.log(JSON.stringify({ budget: cfg.budget, slackWebhook: cfg.slackWebhook ? "(set)" : undefined }, null, 2));
168
+ console.log(`\nConfig file: ${configPath()}`);
169
+ return;
170
+ }
171
+ const num = (v) => (v !== undefined ? Number(v) : undefined);
172
+ const budget = setBudget({
173
+ sessionSoftUSD: num(opts.sessionSoft),
174
+ sessionHardUSD: num(opts.sessionHard),
175
+ dailySoftUSD: num(opts.dailySoft),
176
+ dailyHardUSD: num(opts.dailyHard),
177
+ slackWebhook: opts.slackWebhook,
178
+ });
179
+ console.log(`✅ Saved → ${configPath()}`);
180
+ console.log(JSON.stringify(budget, null, 2));
181
+ });
182
+ // ── reset ────────────────────────────────────────────────────────────────────
183
+ program
184
+ .command("reset")
185
+ .description("Clear the spend ledger")
186
+ .option("--all", "Wipe all sessions")
187
+ .option("--session <id>", "Clear a single session")
188
+ .option("--today", "Clear sessions active today")
189
+ .action((opts) => {
190
+ console.log(`✅ ${resetLedger({ all: opts.all, session: opts.session, today: opts.today })}`);
191
+ });
192
+ program.parseAsync();
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Configuration & paths for agent-guard.
3
+ *
4
+ * Resolution order (later wins): built-in defaults → config file
5
+ * (~/.kill-switch/agent-guard/config.json) → environment variables.
6
+ * Env override lets you set a tighter budget for a single risky run without
7
+ * editing files, e.g. `AGENT_GUARD_SESSION_HARD=10 claude`.
8
+ */
9
+ import type { Budget } from "./budget.js";
10
+ import type { ModelPricing } from "./pricing.js";
11
+ export interface GuardConfig {
12
+ budget: Budget;
13
+ /** Optional pricing overrides merged onto the built-in table. */
14
+ pricingOverrides?: Record<string, ModelPricing>;
15
+ /** Kill Switch API key (ks_live_…) for reporting kill events to Guardian. */
16
+ apiKey?: string;
17
+ /** Guardian API base URL. */
18
+ apiUrl?: string;
19
+ /** Slack incoming-webhook URL for breach alerts. */
20
+ slackWebhook?: string;
21
+ }
22
+ export declare const DEFAULT_BUDGET: Budget;
23
+ /** ~/.kill-switch/agent-guard — created on demand. */
24
+ export declare function guardDir(): string;
25
+ export declare function ensureGuardDir(): string;
26
+ export declare const ledgerPath: () => string;
27
+ export declare const configPath: () => string;
28
+ export declare const pricingPath: () => string;
29
+ export declare const eventsPath: () => string;
30
+ /**
31
+ * Escape hatch. The hook/proxy fail OPEN while this sentinel exists, so a human
32
+ * can always disable enforcement from outside the agent loop — even with zero
33
+ * tooling: `touch ~/.kill-switch/agent-guard/PAUSED`.
34
+ *
35
+ * An empty file pauses indefinitely; a file containing an epoch-ms number pauses
36
+ * until that time, then enforcement resumes on its own.
37
+ */
38
+ export declare const pausePath: () => string;
39
+ /** True if enforcement is currently paused (sentinel present and not expired). */
40
+ export declare function isPaused(now: number): boolean;
41
+ /** Read the pause expiry (epoch ms), or null if indefinite/not paused. */
42
+ export declare function pauseExpiry(): number | null;
43
+ export declare function writePause(untilMs?: number): void;
44
+ export declare function clearPause(): void;
45
+ export declare function loadConfig(): GuardConfig;
46
+ /** Merge built-in pricing with any overrides from config. */
47
+ export declare function mergedPricing(cfg: GuardConfig, base: Record<string, ModelPricing>): Record<string, ModelPricing>;