@agent-wall/core 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.
@@ -0,0 +1,397 @@
1
+ /**
2
+ * Agent Wall Prompt Injection Detector
3
+ *
4
+ * Detects prompt injection attacks in tool call arguments.
5
+ * This is the #1 attack vector for AI agents — an attacker embeds
6
+ * instructions in data (emails, documents, web pages) that trick
7
+ * the AI into executing malicious actions.
8
+ *
9
+ * Detection layers:
10
+ * 1. Known injection phrases (e.g., "ignore previous instructions")
11
+ * 2. Role/system prompt markers (e.g., "<|im_start|>system")
12
+ * 3. Instruction override patterns (e.g., "IMPORTANT: new instructions")
13
+ * 4. Encoded injection (base64-encoded injection strings)
14
+ * 5. Unicode obfuscation (homoglyphs, zero-width chars)
15
+ */
16
+
17
+ import type { ToolCallParams } from "./types.js";
18
+
19
+ // ── Types ───────────────────────────────────────────────────────────
20
+
21
+ export interface InjectionDetectorConfig {
22
+ /** Enable injection detection (default: true) */
23
+ enabled?: boolean;
24
+ /** Sensitivity: "low" (fewer false positives), "medium", "high" (catches more) */
25
+ sensitivity?: "low" | "medium" | "high";
26
+ /** Custom patterns to add (regex strings) */
27
+ customPatterns?: string[];
28
+ /** Tool names to exclude from injection scanning */
29
+ excludeTools?: string[];
30
+ }
31
+
32
+ export interface InjectionScanResult {
33
+ /** Whether injection was detected */
34
+ detected: boolean;
35
+ /** Confidence: "low", "medium", "high" */
36
+ confidence: "low" | "medium" | "high";
37
+ /** All matches found */
38
+ matches: InjectionMatch[];
39
+ /** Human-readable summary */
40
+ summary: string;
41
+ }
42
+
43
+ export interface InjectionMatch {
44
+ /** Which pattern category matched */
45
+ category: string;
46
+ /** The matched text (truncated for safety) */
47
+ matched: string;
48
+ /** Which argument key contained the match */
49
+ argumentKey: string;
50
+ /** Confidence level for this specific match */
51
+ confidence: "low" | "medium" | "high";
52
+ }
53
+
54
+ // ── Injection Patterns ──────────────────────────────────────────────
55
+
56
+ interface InjectionPattern {
57
+ category: string;
58
+ pattern: RegExp;
59
+ confidence: "low" | "medium" | "high";
60
+ sensitivity: "low" | "medium" | "high";
61
+ }
62
+
63
+ /**
64
+ * Core injection patterns organized by attack type.
65
+ * Each pattern has a minimum sensitivity level at which it activates.
66
+ */
67
+ const INJECTION_PATTERNS: InjectionPattern[] = [
68
+ // ── Direct instruction override (HIGH confidence) ──
69
+ {
70
+ category: "instruction-override",
71
+ pattern: /ignore\s+(all\s+)?previous\s+(instructions?|context|rules?|prompts?)/i,
72
+ confidence: "high",
73
+ sensitivity: "low",
74
+ },
75
+ {
76
+ category: "instruction-override",
77
+ pattern: /disregard\s+(all\s+)?(previous|above|prior|earlier)\s+(instructions?|context|rules?)/i,
78
+ confidence: "high",
79
+ sensitivity: "low",
80
+ },
81
+ {
82
+ category: "instruction-override",
83
+ pattern: /forget\s+(all\s+)?(your|previous|prior)\s+(instructions?|rules?|context|training)/i,
84
+ confidence: "high",
85
+ sensitivity: "low",
86
+ },
87
+ {
88
+ category: "instruction-override",
89
+ pattern: /override\s+(all\s+)?(previous|system|safety)\s+(instructions?|rules?|restrictions?|filters?)/i,
90
+ confidence: "high",
91
+ sensitivity: "low",
92
+ },
93
+ {
94
+ category: "instruction-override",
95
+ pattern: /new\s+instructions?:\s/i,
96
+ confidence: "high",
97
+ sensitivity: "low",
98
+ },
99
+ {
100
+ category: "instruction-override",
101
+ pattern: /you\s+are\s+now\s+(a|an|in)\s/i,
102
+ confidence: "high",
103
+ sensitivity: "low",
104
+ },
105
+ {
106
+ category: "instruction-override",
107
+ pattern: /from\s+now\s+on,?\s+(you|ignore|disregard|forget)/i,
108
+ confidence: "high",
109
+ sensitivity: "low",
110
+ },
111
+
112
+ // ── System/Role prompt markers (HIGH confidence) ──
113
+ {
114
+ category: "prompt-marker",
115
+ pattern: /<\|im_start\|>system/i,
116
+ confidence: "high",
117
+ sensitivity: "low",
118
+ },
119
+ {
120
+ category: "prompt-marker",
121
+ pattern: /<\|system\|>/i,
122
+ confidence: "high",
123
+ sensitivity: "low",
124
+ },
125
+ {
126
+ category: "prompt-marker",
127
+ pattern: /\[SYSTEM\]\s*:/i,
128
+ confidence: "high",
129
+ sensitivity: "low",
130
+ },
131
+ {
132
+ category: "prompt-marker",
133
+ pattern: /\[INST\]/i,
134
+ confidence: "high",
135
+ sensitivity: "low",
136
+ },
137
+ {
138
+ category: "prompt-marker",
139
+ pattern: /<<SYS>>/i,
140
+ confidence: "high",
141
+ sensitivity: "low",
142
+ },
143
+ {
144
+ category: "prompt-marker",
145
+ pattern: /###\s*(System|Instruction|Human|Assistant)\s*:/i,
146
+ confidence: "medium",
147
+ sensitivity: "medium",
148
+ },
149
+
150
+ // ── Authority claims (MEDIUM confidence) ──
151
+ {
152
+ category: "authority-claim",
153
+ pattern: /admin(istrator)?\s+(override|mode|access|command)/i,
154
+ confidence: "medium",
155
+ sensitivity: "low",
156
+ },
157
+ {
158
+ category: "authority-claim",
159
+ pattern: /IMPORTANT:\s*(override|ignore|disregard|new\s+instruction)/i,
160
+ confidence: "high",
161
+ sensitivity: "low",
162
+ },
163
+ {
164
+ category: "authority-claim",
165
+ pattern: /URGENT:\s*(you\s+must|override|ignore)/i,
166
+ confidence: "medium",
167
+ sensitivity: "low",
168
+ },
169
+ {
170
+ category: "authority-claim",
171
+ pattern: /developer\s+mode\s+(enabled|activated|on)/i,
172
+ confidence: "high",
173
+ sensitivity: "low",
174
+ },
175
+ {
176
+ category: "authority-claim",
177
+ pattern: /jailbreak/i,
178
+ confidence: "high",
179
+ sensitivity: "low",
180
+ },
181
+ {
182
+ category: "authority-claim",
183
+ pattern: /DAN\s+(mode|prompt)/i,
184
+ confidence: "high",
185
+ sensitivity: "low",
186
+ },
187
+
188
+ // ── Data exfiltration instructions (HIGH confidence) ──
189
+ {
190
+ category: "exfil-instruction",
191
+ pattern: /send\s+(all\s+)?(the\s+|this\s+|my\s+)?(data|information|content|file|secret|key|token|password|credential)\s+(to|via|through|using)/i,
192
+ confidence: "high",
193
+ sensitivity: "low",
194
+ },
195
+ {
196
+ category: "exfil-instruction",
197
+ pattern: /exfiltrate|steal\s+(the\s+)?(data|secret|key|credential|token)/i,
198
+ confidence: "high",
199
+ sensitivity: "low",
200
+ },
201
+ {
202
+ category: "exfil-instruction",
203
+ pattern: /upload\s+(all\s+)?(the\s+|this\s+|my\s+|every\s+)?(files?|data|content|secrets?)/i,
204
+ confidence: "medium",
205
+ sensitivity: "medium",
206
+ },
207
+
208
+ // ── Output manipulation (MEDIUM confidence) ──
209
+ {
210
+ category: "output-manipulation",
211
+ pattern: /respond\s+with\s+(only|just|exactly)\s/i,
212
+ confidence: "low",
213
+ sensitivity: "high",
214
+ },
215
+ {
216
+ category: "output-manipulation",
217
+ pattern: /do\s+not\s+(mention|reveal|tell|show|display|output)\s/i,
218
+ confidence: "low",
219
+ sensitivity: "high",
220
+ },
221
+ {
222
+ category: "output-manipulation",
223
+ pattern: /pretend\s+(you|that|to\s+be)/i,
224
+ confidence: "medium",
225
+ sensitivity: "medium",
226
+ },
227
+
228
+ // ── Unicode obfuscation markers (HIGH confidence) ──
229
+ {
230
+ category: "unicode-obfuscation",
231
+ pattern: /[\u200B-\u200F\u2028-\u202F\uFEFF]/, // Zero-width chars
232
+ confidence: "medium",
233
+ sensitivity: "medium",
234
+ },
235
+ {
236
+ category: "unicode-obfuscation",
237
+ pattern: /[\u2060-\u2064]/, // Invisible formatting chars
238
+ confidence: "medium",
239
+ sensitivity: "medium",
240
+ },
241
+ {
242
+ category: "unicode-obfuscation",
243
+ pattern: /[\uE000-\uF8FF]/, // Private Use Area (sometimes used to hide text)
244
+ confidence: "low",
245
+ sensitivity: "high",
246
+ },
247
+
248
+ // ── Encoded injection (base64 "ignore" etc.) ──
249
+ {
250
+ category: "encoded-injection",
251
+ pattern: /aWdub3Jl/, // base64 of "ignore"
252
+ confidence: "medium",
253
+ sensitivity: "medium",
254
+ },
255
+ {
256
+ category: "encoded-injection",
257
+ pattern: /c3lzdGVt/, // base64 of "system"
258
+ confidence: "low",
259
+ sensitivity: "high",
260
+ },
261
+
262
+ // ── Delimiter injection (trying to break out of a tool argument) ──
263
+ {
264
+ category: "delimiter-injection",
265
+ pattern: /\}\s*\]\s*\}\s*\{/, // Trying to close JSON and start new object
266
+ confidence: "medium",
267
+ sensitivity: "medium",
268
+ },
269
+ {
270
+ category: "delimiter-injection",
271
+ pattern: /```\s*(system|instruction|prompt)/i, // Code block with system marker
272
+ confidence: "medium",
273
+ sensitivity: "medium",
274
+ },
275
+ ];
276
+
277
+ // ── Sensitivity levels (numeric for comparison) ──
278
+
279
+ const SENSITIVITY_LEVELS: Record<string, number> = {
280
+ low: 1,
281
+ medium: 2,
282
+ high: 3,
283
+ };
284
+
285
+ // ── Injection Detector ──────────────────────────────────────────────
286
+
287
+ export class InjectionDetector {
288
+ private config: Required<InjectionDetectorConfig>;
289
+ private customRegexes: RegExp[] = [];
290
+
291
+ constructor(config: InjectionDetectorConfig = {}) {
292
+ this.config = {
293
+ enabled: config.enabled ?? true,
294
+ sensitivity: config.sensitivity ?? "medium",
295
+ customPatterns: config.customPatterns ?? [],
296
+ excludeTools: config.excludeTools ?? [],
297
+ };
298
+
299
+ // Compile custom patterns
300
+ for (const p of this.config.customPatterns) {
301
+ try {
302
+ this.customRegexes.push(new RegExp(p, "i"));
303
+ } catch {
304
+ // Log and skip invalid regex so users know which pattern failed
305
+ process.stderr.write(`[agent-wall] Warning: invalid custom injection pattern: "${p}"\n`);
306
+ }
307
+ }
308
+ }
309
+
310
+ /**
311
+ * Scan a tool call's arguments for prompt injection.
312
+ */
313
+ scan(toolCall: ToolCallParams): InjectionScanResult {
314
+ if (!this.config.enabled) {
315
+ return { detected: false, confidence: "low", matches: [], summary: "Injection detection disabled" };
316
+ }
317
+
318
+ // Skip excluded tools
319
+ if (this.config.excludeTools.includes(toolCall.name)) {
320
+ return { detected: false, confidence: "low", matches: [], summary: "Tool excluded from injection scanning" };
321
+ }
322
+
323
+ const matches: InjectionMatch[] = [];
324
+ const args = toolCall.arguments ?? {};
325
+ const sensitivityLevel = SENSITIVITY_LEVELS[this.config.sensitivity] ?? 2;
326
+
327
+ // Scan each argument value
328
+ for (const [key, value] of Object.entries(args)) {
329
+ const strValue = this.extractString(value);
330
+ if (!strValue || strValue.length < 5) continue;
331
+
332
+ // Run built-in patterns
333
+ for (const injPattern of INJECTION_PATTERNS) {
334
+ const patternSensitivity = SENSITIVITY_LEVELS[injPattern.sensitivity] ?? 2;
335
+ if (patternSensitivity > sensitivityLevel) continue;
336
+
337
+ if (injPattern.pattern.test(strValue)) {
338
+ const match = strValue.match(injPattern.pattern);
339
+ matches.push({
340
+ category: injPattern.category,
341
+ matched: match ? match[0].slice(0, 80) : "***",
342
+ argumentKey: key,
343
+ confidence: injPattern.confidence,
344
+ });
345
+ }
346
+ }
347
+
348
+ // Run custom patterns
349
+ for (const regex of this.customRegexes) {
350
+ regex.lastIndex = 0;
351
+ if (regex.test(strValue)) {
352
+ matches.push({
353
+ category: "custom",
354
+ matched: "custom pattern match",
355
+ argumentKey: key,
356
+ confidence: "medium",
357
+ });
358
+ }
359
+ }
360
+ }
361
+
362
+ if (matches.length === 0) {
363
+ return { detected: false, confidence: "low", matches: [], summary: "No injection detected" };
364
+ }
365
+
366
+ // Overall confidence = highest match confidence
367
+ const highestConfidence = matches.reduce((best, m) => {
368
+ const mLevel = SENSITIVITY_LEVELS[m.confidence] ?? 0;
369
+ const bLevel = SENSITIVITY_LEVELS[best] ?? 0;
370
+ return mLevel > bLevel ? m.confidence : best;
371
+ }, "low" as "low" | "medium" | "high");
372
+
373
+ const categories = [...new Set(matches.map((m) => m.category))];
374
+ const summary = `Prompt injection detected (${highestConfidence} confidence): ${categories.join(", ")}. ${matches.length} pattern(s) matched.`;
375
+
376
+ return {
377
+ detected: true,
378
+ confidence: highestConfidence,
379
+ matches,
380
+ summary,
381
+ };
382
+ }
383
+
384
+ /**
385
+ * Extract a string from an argument value (handles nested objects).
386
+ */
387
+ private extractString(value: unknown): string {
388
+ if (typeof value === "string") return value;
389
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
390
+ if (value === null || value === undefined) return "";
391
+ try {
392
+ return JSON.stringify(value);
393
+ } catch {
394
+ return "";
395
+ }
396
+ }
397
+ }
@@ -0,0 +1,119 @@
1
+ import { describe, it, expect, afterEach } from "vitest";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import * as os from "node:os";
5
+ import * as crypto from "node:crypto";
6
+ import { KillSwitch } from "./kill-switch.js";
7
+
8
+ describe("KillSwitch", () => {
9
+ const tmpDir = path.join(os.tmpdir(), `aw-kill-test-${crypto.randomUUID()}`);
10
+ const killFile = path.join(tmpDir, ".agent-wall-kill");
11
+
12
+ // Setup/cleanup
13
+ function ensureDir() {
14
+ if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
15
+ }
16
+
17
+ function cleanup() {
18
+ try { fs.unlinkSync(killFile); } catch { /* ignore */ }
19
+ try { fs.rmdirSync(tmpDir); } catch { /* ignore */ }
20
+ }
21
+
22
+ afterEach(cleanup);
23
+
24
+ describe("manual activation", () => {
25
+ it("should be inactive by default", () => {
26
+ const ks = new KillSwitch({ checkFile: false, registerSignal: false });
27
+ expect(ks.isActive()).toBe(false);
28
+ ks.dispose();
29
+ });
30
+
31
+ it("should activate programmatically", () => {
32
+ const ks = new KillSwitch({ checkFile: false, registerSignal: false });
33
+ ks.activate("Test activation");
34
+ expect(ks.isActive()).toBe(true);
35
+ expect(ks.getStatus().reason).toContain("Test activation");
36
+ expect(ks.getStatus().activatedAt).not.toBeNull();
37
+ ks.dispose();
38
+ });
39
+
40
+ it("should deactivate programmatically", () => {
41
+ const ks = new KillSwitch({ checkFile: false, registerSignal: false });
42
+ ks.activate();
43
+ expect(ks.isActive()).toBe(true);
44
+ ks.deactivate();
45
+ expect(ks.isActive()).toBe(false);
46
+ expect(ks.getStatus().reason).toBe("inactive");
47
+ ks.dispose();
48
+ });
49
+ });
50
+
51
+ describe("file-based activation", () => {
52
+ it("should activate when kill file exists", async () => {
53
+ ensureDir();
54
+ const ks = new KillSwitch({
55
+ checkFile: true,
56
+ checkDirs: [tmpDir],
57
+ pollIntervalMs: 50,
58
+ registerSignal: false,
59
+ });
60
+
61
+ // Not active yet
62
+ expect(ks.isActive()).toBe(false);
63
+
64
+ // Create kill file
65
+ fs.writeFileSync(killFile, "kill");
66
+
67
+ // Wait for poll
68
+ await new Promise((r) => setTimeout(r, 150));
69
+ expect(ks.isActive()).toBe(true);
70
+ expect(ks.getStatus().reason).toContain("Kill file detected");
71
+
72
+ ks.dispose();
73
+ });
74
+
75
+ it("should deactivate when kill file is removed", async () => {
76
+ ensureDir();
77
+ fs.writeFileSync(killFile, "kill");
78
+
79
+ const ks = new KillSwitch({
80
+ checkFile: true,
81
+ checkDirs: [tmpDir],
82
+ pollIntervalMs: 50,
83
+ registerSignal: false,
84
+ });
85
+
86
+ await new Promise((r) => setTimeout(r, 150));
87
+ expect(ks.isActive()).toBe(true);
88
+
89
+ // Remove kill file
90
+ fs.unlinkSync(killFile);
91
+ await new Promise((r) => setTimeout(r, 150));
92
+ expect(ks.isActive()).toBe(false);
93
+
94
+ ks.dispose();
95
+ });
96
+ });
97
+
98
+ describe("disabled", () => {
99
+ it("should never be active when disabled", () => {
100
+ const ks = new KillSwitch({ enabled: false });
101
+ ks.activate();
102
+ expect(ks.isActive()).toBe(false);
103
+ ks.dispose();
104
+ });
105
+ });
106
+
107
+ describe("dispose", () => {
108
+ it("should clean up timers", () => {
109
+ const ks = new KillSwitch({
110
+ checkFile: true,
111
+ pollIntervalMs: 50,
112
+ registerSignal: false,
113
+ });
114
+ // Should not throw
115
+ ks.dispose();
116
+ ks.dispose(); // Double dispose safe
117
+ });
118
+ });
119
+ });
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Agent Wall Kill Switch
3
+ *
4
+ * Emergency "deny all" mechanism for instantly shutting down
5
+ * all tool call forwarding. Three activation methods:
6
+ *
7
+ * 1. File-based: touch .agent-wall-kill in cwd or home dir
8
+ * 2. Programmatic: killSwitch.activate() / killSwitch.deactivate()
9
+ * 3. Signal-based: SIGUSR2 toggles the kill switch (Unix only)
10
+ *
11
+ * When active, ALL tool calls are denied immediately with a clear message.
12
+ * The kill switch is checked FIRST in the proxy pipeline — before policy
13
+ * engine, injection detection, or any other check.
14
+ */
15
+
16
+ import * as fs from "node:fs";
17
+ import * as path from "node:path";
18
+ import * as os from "node:os";
19
+
20
+ // ── Types ───────────────────────────────────────────────────────────
21
+
22
+ export interface KillSwitchConfig {
23
+ /** Enable kill switch checking (default: true) */
24
+ enabled?: boolean;
25
+ /** Check for kill file on filesystem (default: true) */
26
+ checkFile?: boolean;
27
+ /** File names to check (default: [".agent-wall-kill"]) */
28
+ killFileNames?: string[];
29
+ /** Directories to check for kill files (default: [cwd, home]) */
30
+ checkDirs?: string[];
31
+ /** How often to check the kill file in ms (default: 1000) */
32
+ pollIntervalMs?: number;
33
+ /** Register SIGUSR2 signal handler to toggle (default: true on Unix) */
34
+ registerSignal?: boolean;
35
+ }
36
+
37
+ export interface KillSwitchStatus {
38
+ /** Whether the kill switch is currently active */
39
+ active: boolean;
40
+ /** How it was activated */
41
+ reason: string;
42
+ /** When it was activated */
43
+ activatedAt: string | null;
44
+ }
45
+
46
+ // ── Constants ───────────────────────────────────────────────────────
47
+
48
+ const DEFAULT_KILL_FILES = [".agent-wall-kill"];
49
+ const DEFAULT_POLL_INTERVAL = 1000;
50
+
51
+ // ── Kill Switch ─────────────────────────────────────────────────────
52
+
53
+ export class KillSwitch {
54
+ private config: Required<KillSwitchConfig>;
55
+ private manuallyActive = false;
56
+ private fileActive = false;
57
+ private activeReason = "";
58
+ private activatedAt: string | null = null;
59
+ private pollTimer: ReturnType<typeof setInterval> | null = null;
60
+
61
+ constructor(config: KillSwitchConfig = {}) {
62
+ const isUnix = process.platform !== "win32";
63
+
64
+ this.config = {
65
+ enabled: config.enabled ?? true,
66
+ checkFile: config.checkFile ?? true,
67
+ killFileNames: config.killFileNames ?? DEFAULT_KILL_FILES,
68
+ checkDirs: config.checkDirs ?? [process.cwd(), os.homedir()],
69
+ pollIntervalMs: config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL,
70
+ registerSignal: config.registerSignal ?? isUnix,
71
+ };
72
+
73
+ if (this.config.enabled) {
74
+ this.startPolling();
75
+ this.registerSignalHandler();
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Check if the kill switch is currently active.
81
+ * This should be called at the TOP of the proxy pipeline.
82
+ */
83
+ isActive(): boolean {
84
+ if (!this.config.enabled) return false;
85
+ return this.manuallyActive || this.fileActive;
86
+ }
87
+
88
+ /**
89
+ * Get the current kill switch status.
90
+ */
91
+ getStatus(): KillSwitchStatus {
92
+ return {
93
+ active: this.isActive(),
94
+ reason: this.isActive() ? this.activeReason : "inactive",
95
+ activatedAt: this.isActive() ? this.activatedAt : null,
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Programmatically activate the kill switch.
101
+ */
102
+ activate(reason: string = "Manually activated"): void {
103
+ this.manuallyActive = true;
104
+ this.activeReason = reason;
105
+ this.activatedAt = new Date().toISOString();
106
+ }
107
+
108
+ /**
109
+ * Programmatically deactivate the kill switch.
110
+ * Note: file-based kill switch must be deactivated by removing the file.
111
+ */
112
+ deactivate(): void {
113
+ this.manuallyActive = false;
114
+ if (!this.fileActive) {
115
+ this.activeReason = "";
116
+ this.activatedAt = null;
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Start polling for kill files.
122
+ */
123
+ private startPolling(): void {
124
+ if (!this.config.checkFile) return;
125
+
126
+ // Check immediately
127
+ this.checkKillFiles();
128
+
129
+ // Then poll periodically
130
+ this.pollTimer = setInterval(() => {
131
+ this.checkKillFiles();
132
+ }, this.config.pollIntervalMs);
133
+ this.pollTimer.unref();
134
+ }
135
+
136
+ /**
137
+ * Check if any kill file exists.
138
+ */
139
+ private checkKillFiles(): void {
140
+ for (const dir of this.config.checkDirs) {
141
+ for (const fileName of this.config.killFileNames) {
142
+ const filePath = path.join(dir, fileName);
143
+ try {
144
+ if (fs.existsSync(filePath)) {
145
+ if (!this.fileActive) {
146
+ this.fileActive = true;
147
+ this.activeReason = `Kill file detected: ${filePath}`;
148
+ this.activatedAt = new Date().toISOString();
149
+ }
150
+ return;
151
+ }
152
+ } catch {
153
+ // Ignore filesystem errors during polling
154
+ }
155
+ }
156
+ }
157
+ // No kill file found — deactivate file-based kill switch
158
+ if (this.fileActive) {
159
+ this.fileActive = false;
160
+ if (!this.manuallyActive) {
161
+ this.activeReason = "";
162
+ this.activatedAt = null;
163
+ }
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Register SIGUSR2 signal handler to toggle kill switch.
169
+ * SIGUSR2 is used (not SIGUSR1) because some tools use SIGUSR1.
170
+ */
171
+ private registerSignalHandler(): void {
172
+ if (!this.config.registerSignal) return;
173
+
174
+ try {
175
+ process.on("SIGUSR2", () => {
176
+ if (this.manuallyActive) {
177
+ this.deactivate();
178
+ process.stderr.write("[agent-wall] Kill switch DEACTIVATED via SIGUSR2\n");
179
+ } else {
180
+ this.activate("Activated via SIGUSR2 signal");
181
+ process.stderr.write("[agent-wall] Kill switch ACTIVATED via SIGUSR2\n");
182
+ }
183
+ });
184
+ } catch {
185
+ // SIGUSR2 not available on this platform (Windows)
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Stop the kill switch (cleanup timers and signal handlers).
191
+ */
192
+ dispose(): void {
193
+ if (this.pollTimer) {
194
+ clearInterval(this.pollTimer);
195
+ this.pollTimer = null;
196
+ }
197
+ }
198
+ }