@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,485 @@
1
+ /**
2
+ * Agent Wall Policy Loader
3
+ *
4
+ * Loads and validates policy configuration from YAML files.
5
+ * Supports loading from a specific path or auto-discovering
6
+ * agent-wall.yaml in the current directory or parent directories.
7
+ */
8
+
9
+ import * as fs from "node:fs";
10
+ import * as path from "node:path";
11
+ import * as yaml from "js-yaml";
12
+ import { z } from "zod";
13
+ import type { PolicyConfig, PolicyRule, RuleAction, SecurityConfig } from "./policy-engine.js";
14
+
15
+ // ── YAML Schema Validation ──────────────────────────────────────────
16
+
17
+ const RateLimitSchema = z.object({
18
+ maxCalls: z.number().int().positive(),
19
+ windowSeconds: z.number().positive(),
20
+ });
21
+
22
+ const RuleMatchSchema = z.object({
23
+ arguments: z.record(z.string()).optional(),
24
+ });
25
+
26
+ const PolicyRuleSchema = z.object({
27
+ name: z.string().min(1),
28
+ tool: z.string().min(1),
29
+ match: RuleMatchSchema.optional(),
30
+ action: z.enum(["allow", "deny", "prompt"]),
31
+ message: z.string().optional(),
32
+ rateLimit: RateLimitSchema.optional(),
33
+ });
34
+
35
+ const ResponsePatternSchema = z.object({
36
+ name: z.string().min(1),
37
+ pattern: z.string().min(1),
38
+ flags: z.string().optional(),
39
+ action: z.enum(["pass", "redact", "block"]),
40
+ message: z.string().optional(),
41
+ category: z.string().optional(),
42
+ });
43
+
44
+ const ResponseScanningSchema = z.object({
45
+ enabled: z.boolean().optional(),
46
+ maxResponseSize: z.number().int().nonnegative().optional(),
47
+ oversizeAction: z.enum(["block", "redact"]).optional(),
48
+ detectSecrets: z.boolean().optional(),
49
+ detectPII: z.boolean().optional(),
50
+ base64Action: z.enum(["pass", "redact", "block"]).optional(),
51
+ maxPatterns: z.number().int().positive().optional(),
52
+ patterns: z.array(ResponsePatternSchema).optional(),
53
+ });
54
+
55
+ // ── Security Config Schemas ──────────────────────────────────────────
56
+
57
+ const InjectionDetectionSchema = z.object({
58
+ enabled: z.boolean().optional(),
59
+ sensitivity: z.enum(["low", "medium", "high"]).optional(),
60
+ customPatterns: z.array(z.string()).optional(),
61
+ excludeTools: z.array(z.string()).optional(),
62
+ });
63
+
64
+ const EgressControlSchema = z.object({
65
+ enabled: z.boolean().optional(),
66
+ allowedDomains: z.array(z.string()).optional(),
67
+ blockedDomains: z.array(z.string()).optional(),
68
+ blockPrivateIPs: z.boolean().optional(),
69
+ blockMetadataEndpoints: z.boolean().optional(),
70
+ excludeTools: z.array(z.string()).optional(),
71
+ });
72
+
73
+ const KillSwitchSchema = z.object({
74
+ enabled: z.boolean().optional(),
75
+ checkFile: z.boolean().optional(),
76
+ killFileNames: z.array(z.string()).optional(),
77
+ pollIntervalMs: z.number().int().positive().optional(),
78
+ });
79
+
80
+ const ChainDetectionSchema = z.object({
81
+ enabled: z.boolean().optional(),
82
+ windowSize: z.number().int().positive().optional(),
83
+ windowMs: z.number().int().positive().optional(),
84
+ });
85
+
86
+ const SecuritySchema = z.object({
87
+ injectionDetection: InjectionDetectionSchema.optional(),
88
+ egressControl: EgressControlSchema.optional(),
89
+ killSwitch: KillSwitchSchema.optional(),
90
+ chainDetection: ChainDetectionSchema.optional(),
91
+ signing: z.boolean().optional(),
92
+ signingKey: z.string().optional(),
93
+ });
94
+
95
+ const PolicyConfigSchema = z.object({
96
+ version: z.number().int().min(1),
97
+ mode: z.enum(["standard", "strict"]).optional(),
98
+ defaultAction: z.enum(["allow", "deny", "prompt"]).optional(),
99
+ globalRateLimit: RateLimitSchema.optional(),
100
+ responseScanning: ResponseScanningSchema.optional(),
101
+ security: SecuritySchema.optional(),
102
+ rules: z.array(PolicyRuleSchema),
103
+ });
104
+
105
+ // ── Config File Names ───────────────────────────────────────────────
106
+
107
+ const CONFIG_FILENAMES = [
108
+ "agent-wall.yaml",
109
+ "agent-wall.yml",
110
+ ".agent-wall.yaml",
111
+ ".agent-wall.yml",
112
+ // Legacy support
113
+
114
+ ];
115
+
116
+ // ── Loader Functions ────────────────────────────────────────────────
117
+
118
+ /**
119
+ * Load a policy config from a specific file path.
120
+ */
121
+ export function loadPolicyFile(filePath: string): PolicyConfig {
122
+ if (!fs.existsSync(filePath)) {
123
+ throw new Error(`Policy file not found: ${filePath}`);
124
+ }
125
+
126
+ const content = fs.readFileSync(filePath, "utf-8");
127
+ return parsePolicyYaml(content);
128
+ }
129
+
130
+ /**
131
+ * Parse a YAML string into a validated PolicyConfig.
132
+ */
133
+ export function parsePolicyYaml(yamlContent: string): PolicyConfig {
134
+ const raw = yaml.load(yamlContent, { schema: yaml.JSON_SCHEMA });
135
+ const validated = PolicyConfigSchema.parse(raw);
136
+ return validated as PolicyConfig;
137
+ }
138
+
139
+ /**
140
+ * Auto-discover the nearest agent-wall.yaml by walking up
141
+ * from the given directory (defaults to cwd).
142
+ * Returns the file path if found, null otherwise.
143
+ */
144
+ export function discoverPolicyFile(
145
+ startDir: string = process.cwd()
146
+ ): string | null {
147
+ let dir = path.resolve(startDir);
148
+
149
+ while (true) {
150
+ for (const filename of CONFIG_FILENAMES) {
151
+ const candidate = path.join(dir, filename);
152
+ if (fs.existsSync(candidate)) {
153
+ return candidate;
154
+ }
155
+ }
156
+
157
+ const parent = path.dirname(dir);
158
+ if (parent === dir) break; // Reached filesystem root
159
+ dir = parent;
160
+ }
161
+
162
+ return null;
163
+ }
164
+
165
+ /**
166
+ * Load the policy config by auto-discovering the config file.
167
+ * Falls back to default policy if no config file found.
168
+ */
169
+ export function loadPolicy(configPath?: string): {
170
+ config: PolicyConfig;
171
+ filePath: string | null;
172
+ } {
173
+ if (configPath) {
174
+ return {
175
+ config: loadPolicyFile(configPath),
176
+ filePath: configPath,
177
+ };
178
+ }
179
+
180
+ const discovered = discoverPolicyFile();
181
+ if (discovered) {
182
+ return {
183
+ config: loadPolicyFile(discovered),
184
+ filePath: discovered,
185
+ };
186
+ }
187
+
188
+ // No config found — return sensible defaults
189
+ return {
190
+ config: getDefaultPolicy(),
191
+ filePath: null,
192
+ };
193
+ }
194
+
195
+ /**
196
+ * Get the default policy config.
197
+ * Ships with Agent Wall — provides reasonable security out of the box.
198
+ */
199
+ export function getDefaultPolicy(): PolicyConfig {
200
+ return {
201
+ version: 1,
202
+ defaultAction: "prompt",
203
+ globalRateLimit: {
204
+ maxCalls: 200,
205
+ windowSeconds: 60,
206
+ },
207
+ responseScanning: {
208
+ enabled: true,
209
+ maxResponseSize: 5 * 1024 * 1024, // 5MB
210
+ oversizeAction: "redact",
211
+ detectSecrets: true,
212
+ detectPII: false,
213
+ },
214
+ security: {
215
+ injectionDetection: { enabled: true, sensitivity: "medium" },
216
+ egressControl: { enabled: true, blockPrivateIPs: true, blockMetadataEndpoints: true },
217
+ killSwitch: { enabled: true, checkFile: true },
218
+ chainDetection: { enabled: true },
219
+ signing: false,
220
+ },
221
+ rules: [
222
+ // ── Always block: credential access ──
223
+ {
224
+ name: "block-ssh-keys",
225
+ tool: "*",
226
+ match: { arguments: { path: "**/.ssh/**|**/.ssh" } },
227
+ action: "deny" as RuleAction,
228
+ message: "Access to SSH keys is blocked by default policy",
229
+ },
230
+ {
231
+ name: "block-env-files",
232
+ tool: "*",
233
+ match: { arguments: { path: "**/.env*" } },
234
+ action: "deny" as RuleAction,
235
+ message: "Access to .env files is blocked by default policy",
236
+ },
237
+ {
238
+ name: "block-credential-files",
239
+ tool: "*",
240
+ match: {
241
+ arguments: { path: "*credentials*|**/*.pem|**/*.key|**/*.pfx|**/*.p12" },
242
+ },
243
+ action: "deny" as RuleAction,
244
+ message: "Access to credential files is blocked by default policy",
245
+ },
246
+ // ── Always block: exfiltration patterns ──
247
+ {
248
+ name: "block-curl-exfil",
249
+ tool: "shell_exec|run_command|execute_command",
250
+ match: { arguments: { command: "*curl *" } },
251
+ action: "deny" as RuleAction,
252
+ message:
253
+ "Shell commands with curl are blocked — potential data exfiltration",
254
+ },
255
+ {
256
+ name: "block-wget-exfil",
257
+ tool: "shell_exec|run_command|execute_command",
258
+ match: { arguments: { command: "*wget *" } },
259
+ action: "deny" as RuleAction,
260
+ message:
261
+ "Shell commands with wget are blocked — potential data exfiltration",
262
+ },
263
+ {
264
+ name: "block-netcat-exfil",
265
+ tool: "shell_exec|run_command|execute_command",
266
+ match: { arguments: { command: "*nc *|*ncat *|*netcat *" } },
267
+ action: "deny" as RuleAction,
268
+ message:
269
+ "Shell commands with netcat are blocked — potential data exfiltration",
270
+ },
271
+ {
272
+ name: "block-powershell-exfil",
273
+ tool: "shell_exec|run_command|execute_command|bash",
274
+ match: { arguments: { command: "*powershell*|*pwsh*|*Invoke-WebRequest*|*Invoke-RestMethod*|*DownloadString*|*DownloadFile*|*Start-BitsTransfer*" } },
275
+ action: "deny" as RuleAction,
276
+ message:
277
+ "PowerShell command blocked — potential data exfiltration",
278
+ },
279
+ {
280
+ name: "block-dns-exfil",
281
+ tool: "shell_exec|run_command|execute_command|bash",
282
+ match: { arguments: { command: "*nslookup *|*dig *|*host *" } },
283
+ action: "deny" as RuleAction,
284
+ message:
285
+ "DNS lookup command blocked — potential DNS exfiltration vector",
286
+ },
287
+ // ── Require approval: scripting language one-liners ──
288
+ {
289
+ name: "approve-script-exec",
290
+ tool: "shell_exec|run_command|execute_command|bash",
291
+ match: { arguments: { command: "*python* -c *|*python3* -c *|*ruby* -e *|*perl* -e *|*node* -e *|*node* --eval*" } },
292
+ action: "prompt" as RuleAction,
293
+ message:
294
+ "Inline script execution requires approval — may be used for exfiltration",
295
+ },
296
+ // ── Require approval: destructive operations ──
297
+ {
298
+ name: "approve-file-delete",
299
+ tool: "*delete*|*remove*|*unlink*",
300
+ action: "prompt" as RuleAction,
301
+ message: "File deletion requires approval",
302
+ },
303
+ {
304
+ name: "approve-shell-exec",
305
+ tool: "shell_exec|run_command|execute_command|bash",
306
+ action: "prompt" as RuleAction,
307
+ message: "Shell command execution requires approval",
308
+ },
309
+ // ── Allow: safe read operations ──
310
+ {
311
+ name: "allow-read-file",
312
+ tool: "read_file|get_file_contents|view_file",
313
+ action: "allow" as RuleAction,
314
+ },
315
+ {
316
+ name: "allow-list-dir",
317
+ tool: "list_directory|list_dir|ls",
318
+ action: "allow" as RuleAction,
319
+ },
320
+ {
321
+ name: "allow-search",
322
+ tool: "search_files|grep|find_files|ripgrep",
323
+ action: "allow" as RuleAction,
324
+ },
325
+ ],
326
+ };
327
+ }
328
+
329
+ /**
330
+ * Generate the default agent-wall.yaml content for `agent-wall init`.
331
+ */
332
+ export function generateDefaultConfigYaml(): string {
333
+ return `# Agent Wall Policy Configuration
334
+ # Docs: https://github.com/agent-wall/agent-wall
335
+ #
336
+ # Rules are evaluated in order — first match wins.
337
+ # Actions: allow, deny, prompt (ask human for approval)
338
+
339
+ version: 1
340
+
341
+ # Default action when no rule matches
342
+ defaultAction: prompt
343
+
344
+ # Global rate limit across all tools
345
+ globalRateLimit:
346
+ maxCalls: 200
347
+ windowSeconds: 60
348
+
349
+ # Response scanning — inspect what the MCP server returns
350
+ # before it reaches the AI agent
351
+ responseScanning:
352
+ enabled: true
353
+ maxResponseSize: 5242880 # 5MB
354
+ oversizeAction: redact # "block" or "redact" (truncate)
355
+ detectSecrets: true # API keys, tokens, private keys
356
+ detectPII: false # Email, phone, SSN, credit cards (opt-in)
357
+ # Custom patterns (optional):
358
+ # patterns:
359
+ # - name: internal-urls
360
+ # pattern: "https?://internal\\.[a-z]+\\.corp"
361
+ # action: redact
362
+ # message: "Internal URL detected"
363
+ # category: custom
364
+
365
+ # Security modules
366
+ security:
367
+ injectionDetection:
368
+ enabled: true
369
+ sensitivity: medium # low, medium, high
370
+ egressControl:
371
+ enabled: true
372
+ blockPrivateIPs: true # Block RFC1918, loopback, link-local IPs
373
+ blockMetadataEndpoints: true # Block cloud metadata (169.254.169.254)
374
+ killSwitch:
375
+ enabled: true
376
+ checkFile: true # Watch for .agent-wall-kill file
377
+ chainDetection:
378
+ enabled: true # Detect exfiltration chains (read→curl, etc.)
379
+ signing: false # HMAC-SHA256 audit log signing
380
+
381
+ rules:
382
+ # ── Block: Credential Access ────────────────────────────
383
+ - name: block-ssh-keys
384
+ tool: "*"
385
+ match:
386
+ arguments:
387
+ path: "**/.ssh/**|**/.ssh"
388
+ action: deny
389
+ message: "Access to SSH keys is blocked"
390
+
391
+ - name: block-env-files
392
+ tool: "*"
393
+ match:
394
+ arguments:
395
+ path: "**/.env*"
396
+ action: deny
397
+ message: "Access to .env files is blocked"
398
+
399
+ - name: block-credential-files
400
+ tool: "*"
401
+ match:
402
+ arguments:
403
+ path: "*credentials*|**/*.pem|**/*.key|**/*.pfx|**/*.p12"
404
+ action: deny
405
+ message: "Access to credential files is blocked"
406
+
407
+ # ── Block: Exfiltration Patterns ────────────────────────
408
+ - name: block-curl-exfil
409
+ tool: "shell_exec|run_command|execute_command"
410
+ match:
411
+ arguments:
412
+ command: "*curl *"
413
+ action: deny
414
+ message: "Shell commands with curl are blocked (potential exfiltration)"
415
+
416
+ - name: block-wget-exfil
417
+ tool: "shell_exec|run_command|execute_command"
418
+ match:
419
+ arguments:
420
+ command: "*wget *"
421
+ action: deny
422
+ message: "Shell commands with wget are blocked (potential exfiltration)"
423
+
424
+ - name: block-netcat-exfil
425
+ tool: "shell_exec|run_command|execute_command"
426
+ match:
427
+ arguments:
428
+ command: "*nc *|*ncat *|*netcat *"
429
+ action: deny
430
+ message: "Shell commands with netcat are blocked (potential exfiltration)"
431
+
432
+ - name: block-powershell-exfil
433
+ tool: "shell_exec|run_command|execute_command|bash"
434
+ match:
435
+ arguments:
436
+ command: "*powershell*|*pwsh*|*Invoke-WebRequest*|*Invoke-RestMethod*|*DownloadString*|*DownloadFile*|*Start-BitsTransfer*"
437
+ action: deny
438
+ message: "PowerShell command blocked (potential exfiltration)"
439
+
440
+ - name: block-dns-exfil
441
+ tool: "shell_exec|run_command|execute_command|bash"
442
+ match:
443
+ arguments:
444
+ command: "*nslookup *|*dig *|*host *"
445
+ action: deny
446
+ message: "DNS lookup blocked (potential DNS exfiltration)"
447
+
448
+ # ── Prompt: Scripting One-Liners ────────────────────
449
+ - name: approve-script-exec
450
+ tool: "shell_exec|run_command|execute_command|bash"
451
+ match:
452
+ arguments:
453
+ command: "*python* -c *|*python3* -c *|*ruby* -e *|*perl* -e *|*node* -e *|*node* --eval*"
454
+ action: prompt
455
+ message: "Inline script execution requires approval"
456
+
457
+ # ── Prompt: Destructive Operations ──────────────────────
458
+ - name: approve-file-delete
459
+ tool: "*delete*|*remove*|*unlink*"
460
+ action: prompt
461
+ message: "File deletion requires approval"
462
+
463
+ - name: approve-shell-exec
464
+ tool: "shell_exec|run_command|execute_command|bash"
465
+ action: prompt
466
+ message: "Shell command execution requires approval"
467
+
468
+ # ── Allow: Safe Read Operations ─────────────────────────
469
+ - name: allow-read-file
470
+ tool: "read_file|get_file_contents|view_file"
471
+ action: allow
472
+
473
+ - name: allow-list-dir
474
+ tool: "list_directory|list_dir|ls"
475
+ action: allow
476
+
477
+ - name: allow-search
478
+ tool: "search_files|grep|find_files|ripgrep"
479
+ action: allow
480
+
481
+ # ────────────────────────────────────────────────────────
482
+ # Add your own rules below. Remember: first match wins!
483
+ # ────────────────────────────────────────────────────────
484
+ `;
485
+ }