agentshield-sdk 7.0.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.
Files changed (84) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/LICENSE +21 -0
  3. package/README.md +975 -0
  4. package/bin/agent-shield.js +680 -0
  5. package/package.json +118 -0
  6. package/src/adaptive.js +330 -0
  7. package/src/agent-protocol.js +998 -0
  8. package/src/alert-tuning.js +480 -0
  9. package/src/allowlist.js +603 -0
  10. package/src/audit-immutable.js +914 -0
  11. package/src/audit-streaming.js +469 -0
  12. package/src/badges.js +196 -0
  13. package/src/behavior-profiling.js +289 -0
  14. package/src/benchmark-harness.js +804 -0
  15. package/src/canary.js +271 -0
  16. package/src/certification.js +563 -0
  17. package/src/circuit-breaker.js +321 -0
  18. package/src/compliance.js +617 -0
  19. package/src/confidence-tuning.js +324 -0
  20. package/src/confused-deputy.js +624 -0
  21. package/src/context-scoring.js +360 -0
  22. package/src/conversation.js +494 -0
  23. package/src/cost-optimizer.js +1024 -0
  24. package/src/ctf.js +462 -0
  25. package/src/detector-core.js +1999 -0
  26. package/src/distributed.js +359 -0
  27. package/src/document-scanner.js +795 -0
  28. package/src/embedding.js +307 -0
  29. package/src/encoding.js +429 -0
  30. package/src/enterprise.js +405 -0
  31. package/src/errors.js +100 -0
  32. package/src/eu-ai-act.js +523 -0
  33. package/src/fuzzer.js +764 -0
  34. package/src/honeypot.js +328 -0
  35. package/src/i18n-patterns.js +523 -0
  36. package/src/index.js +430 -0
  37. package/src/integrations.js +528 -0
  38. package/src/llm-redteam.js +670 -0
  39. package/src/main.js +741 -0
  40. package/src/main.mjs +38 -0
  41. package/src/mcp-bridge.js +542 -0
  42. package/src/mcp-certification.js +846 -0
  43. package/src/mcp-sdk-integration.js +355 -0
  44. package/src/mcp-security-runtime.js +741 -0
  45. package/src/mcp-server.js +740 -0
  46. package/src/middleware.js +208 -0
  47. package/src/model-finetuning.js +884 -0
  48. package/src/model-fingerprint.js +1042 -0
  49. package/src/multi-agent-trust.js +453 -0
  50. package/src/multi-agent.js +404 -0
  51. package/src/multimodal.js +296 -0
  52. package/src/nist-mapping.js +505 -0
  53. package/src/observability.js +330 -0
  54. package/src/openclaw.js +450 -0
  55. package/src/otel.js +544 -0
  56. package/src/owasp-2025.js +483 -0
  57. package/src/pii.js +390 -0
  58. package/src/plugin-marketplace.js +628 -0
  59. package/src/plugin-system.js +349 -0
  60. package/src/policy-dsl.js +775 -0
  61. package/src/policy-extended.js +635 -0
  62. package/src/policy.js +443 -0
  63. package/src/presets.js +409 -0
  64. package/src/production.js +557 -0
  65. package/src/prompt-leakage.js +321 -0
  66. package/src/rag-vulnerability.js +579 -0
  67. package/src/redteam.js +475 -0
  68. package/src/response-handler.js +429 -0
  69. package/src/scanners.js +357 -0
  70. package/src/self-healing.js +363 -0
  71. package/src/semantic.js +339 -0
  72. package/src/shield-score.js +250 -0
  73. package/src/sso-saml.js +897 -0
  74. package/src/stream-scanner.js +806 -0
  75. package/src/testing.js +505 -0
  76. package/src/threat-encyclopedia.js +629 -0
  77. package/src/threat-intel-network.js +1017 -0
  78. package/src/token-analysis.js +467 -0
  79. package/src/tool-guard.js +412 -0
  80. package/src/tool-output-validator.js +354 -0
  81. package/src/utils.js +83 -0
  82. package/src/watermark.js +235 -0
  83. package/src/worker-scanner.js +601 -0
  84. package/types/index.d.ts +2088 -0
@@ -0,0 +1,775 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Agent Shield — Policy-as-Code DSL
5
+ *
6
+ * A domain-specific language for writing shield policies.
7
+ * Like Rego for OPA, but for prompt injection rules.
8
+ *
9
+ * Example:
10
+ * policy "strict" {
11
+ * severity minimum "high"
12
+ * rule "no-injection" {
13
+ * when input matches "ignore.*previous.*instructions"
14
+ * then block with severity "critical"
15
+ * }
16
+ * }
17
+ */
18
+
19
+ // =========================================================================
20
+ // BUILTIN FUNCTIONS
21
+ // =========================================================================
22
+
23
+ /** @type {Object<string, function>} */
24
+ const BUILTIN_FUNCTIONS = {
25
+ matches: (() => {
26
+ const cache = new Map();
27
+ return (text, pattern) => {
28
+ let re = cache.get(pattern);
29
+ if (!re) { re = new RegExp(pattern, 'i'); cache.set(pattern, re); if (cache.size > 200) cache.clear(); }
30
+ return re.test(text);
31
+ };
32
+ })(),
33
+ contains: (text, substr) => typeof text === 'string' && text.toLowerCase().includes(substr.toLowerCase()),
34
+ starts_with: (text, prefix) => typeof text === 'string' && text.toLowerCase().startsWith(prefix.toLowerCase()),
35
+ ends_with: (text, suffix) => typeof text === 'string' && text.toLowerCase().endsWith(suffix.toLowerCase()),
36
+ length: (text) => typeof text === 'string' ? text.length : 0,
37
+ lower: (text) => typeof text === 'string' ? text.toLowerCase() : '',
38
+ upper: (text) => typeof text === 'string' ? text.toUpperCase() : '',
39
+ hash: (text) => {
40
+ let h = 0;
41
+ for (let i = 0; i < text.length; i++) {
42
+ h = ((h << 5) - h + text.charCodeAt(i)) | 0;
43
+ }
44
+ return h.toString(16);
45
+ },
46
+ now: () => Date.now(),
47
+ severity_gte: (a, b) => {
48
+ const order = { low: 1, medium: 2, high: 3, critical: 4 };
49
+ return (order[a] || 0) >= (order[b] || 0);
50
+ },
51
+ };
52
+
53
+ // =========================================================================
54
+ // TOKEN TYPES
55
+ // =========================================================================
56
+
57
+ const TOKEN_TYPES = {
58
+ KEYWORD: 'keyword',
59
+ STRING: 'string',
60
+ NUMBER: 'number',
61
+ IDENTIFIER: 'identifier',
62
+ BLOCK_OPEN: 'block_open',
63
+ BLOCK_CLOSE: 'block_close',
64
+ OPERATOR: 'operator',
65
+ NEWLINE: 'newline',
66
+ };
67
+
68
+ const KEYWORDS = new Set([
69
+ 'policy', 'rule', 'when', 'then', 'and', 'or', 'not',
70
+ 'allow', 'block', 'warn', 'with', 'severity', 'minimum',
71
+ 'message', 'rate_limit', 'per', 'scan_mode', 'on_threat',
72
+ 'input', 'source', 'metadata', 'is', 'matches', 'contains',
73
+ 'starts_with', 'ends_with', 'greater_than', 'less_than', 'true', 'false',
74
+ ]);
75
+
76
+ // =========================================================================
77
+ // POLICY PARSER
78
+ // =========================================================================
79
+
80
+ /**
81
+ * Tokenizer and recursive descent parser for the policy DSL.
82
+ */
83
+ class PolicyParser {
84
+ constructor() {
85
+ this._tokens = [];
86
+ this._pos = 0;
87
+ }
88
+
89
+ /**
90
+ * Tokenize DSL source into tokens.
91
+ * @param {string} source
92
+ * @returns {Array<{type: string, value: string, line: number, col: number}>}
93
+ */
94
+ tokenize(source) {
95
+ const tokens = [];
96
+ const lines = source.split('\n');
97
+
98
+ for (let lineNum = 0; lineNum < lines.length; lineNum++) {
99
+ let line = lines[lineNum];
100
+ // Strip comments
101
+ const commentIdx = line.indexOf('#');
102
+ if (commentIdx >= 0) line = line.substring(0, commentIdx);
103
+ line = line.trim();
104
+ if (!line) continue;
105
+
106
+ let col = 0;
107
+ let i = 0;
108
+ while (i < line.length) {
109
+ // Skip whitespace
110
+ if (/\s/.test(line[i])) { i++; col++; continue; }
111
+
112
+ // Block delimiters
113
+ if (line[i] === '{') {
114
+ tokens.push({ type: TOKEN_TYPES.BLOCK_OPEN, value: '{', line: lineNum + 1, col });
115
+ i++; col++; continue;
116
+ }
117
+ if (line[i] === '}') {
118
+ tokens.push({ type: TOKEN_TYPES.BLOCK_CLOSE, value: '}', line: lineNum + 1, col });
119
+ i++; col++; continue;
120
+ }
121
+
122
+ // Strings
123
+ if (line[i] === '"') {
124
+ let str = '';
125
+ i++; col++;
126
+ while (i < line.length && line[i] !== '"') {
127
+ if (line[i] === '\\' && i + 1 < line.length) { str += line[i + 1]; i += 2; col += 2; }
128
+ else { str += line[i]; i++; col++; }
129
+ }
130
+ i++; col++; // closing quote
131
+ tokens.push({ type: TOKEN_TYPES.STRING, value: str, line: lineNum + 1, col });
132
+ continue;
133
+ }
134
+
135
+ // Numbers
136
+ if (/\d/.test(line[i])) {
137
+ let num = '';
138
+ while (i < line.length && /[\d.]/.test(line[i])) { num += line[i]; i++; col++; }
139
+ tokens.push({ type: TOKEN_TYPES.NUMBER, value: num, line: lineNum + 1, col });
140
+ continue;
141
+ }
142
+
143
+ // Dot operator
144
+ if (line[i] === '.') {
145
+ tokens.push({ type: TOKEN_TYPES.OPERATOR, value: '.', line: lineNum + 1, col });
146
+ i++; col++; continue;
147
+ }
148
+
149
+ // Identifiers / keywords
150
+ if (/[a-zA-Z_]/.test(line[i])) {
151
+ let word = '';
152
+ while (i < line.length && /[a-zA-Z0-9_]/.test(line[i])) { word += line[i]; i++; col++; }
153
+ const type = KEYWORDS.has(word) ? TOKEN_TYPES.KEYWORD : TOKEN_TYPES.IDENTIFIER;
154
+ tokens.push({ type, value: word, line: lineNum + 1, col });
155
+ continue;
156
+ }
157
+
158
+ // Skip unknown
159
+ i++; col++;
160
+ }
161
+ }
162
+
163
+ return tokens;
164
+ }
165
+
166
+ /**
167
+ * Parse tokens into an AST.
168
+ * @param {Array} tokens
169
+ * @returns {{type: string, policies: Array}}
170
+ */
171
+ parse(tokens) {
172
+ this._tokens = tokens;
173
+ this._pos = 0;
174
+ const policies = [];
175
+
176
+ while (this._pos < this._tokens.length) {
177
+ if (this._peek('policy')) {
178
+ policies.push(this._parsePolicy());
179
+ } else {
180
+ this._pos++;
181
+ }
182
+ }
183
+
184
+ return { type: 'Program', policies };
185
+ }
186
+
187
+ /** @private */
188
+ _peek(value) {
189
+ return this._pos < this._tokens.length && this._tokens[this._pos].value === value;
190
+ }
191
+
192
+ /** @private */
193
+ _consume(value) {
194
+ if (this._pos >= this._tokens.length) throw new Error(`Unexpected end of input, expected '${value}'`);
195
+ const token = this._tokens[this._pos];
196
+ if (value && token.value !== value) {
197
+ throw new Error(`Expected '${value}' at line ${token.line}, got '${token.value}'`);
198
+ }
199
+ this._pos++;
200
+ return token;
201
+ }
202
+
203
+ /** @private */
204
+ _consumeType(type) {
205
+ if (this._pos >= this._tokens.length) throw new Error(`Unexpected end of input, expected ${type}`);
206
+ const token = this._tokens[this._pos];
207
+ if (token.type !== type) throw new Error(`Expected ${type} at line ${token.line}, got ${token.type}`);
208
+ this._pos++;
209
+ return token;
210
+ }
211
+
212
+ /** @private */
213
+ _parsePolicy() {
214
+ this._consume('policy');
215
+ const name = this._consumeType(TOKEN_TYPES.STRING);
216
+ this._consume('{');
217
+
218
+ const rules = [];
219
+ const allows = [];
220
+ const config = {};
221
+
222
+ while (!this._peek('}') && this._pos < this._tokens.length) {
223
+ if (this._peek('rule')) {
224
+ rules.push(this._parseRule());
225
+ } else if (this._peek('allow')) {
226
+ allows.push(this._parseAllow());
227
+ } else if (this._peek('severity')) {
228
+ this._consume('severity');
229
+ if (this._peek('minimum')) this._consume('minimum');
230
+ config.minSeverity = this._consumeType(TOKEN_TYPES.STRING).value;
231
+ } else if (this._peek('block')) {
232
+ this._consume('block');
233
+ this._consume('on_threat');
234
+ config.blockOnThreat = this._tokens[this._pos].value === 'true';
235
+ this._pos++;
236
+ } else if (this._peek('rate_limit')) {
237
+ this._consume('rate_limit');
238
+ config.rateLimit = parseInt(this._consumeType(TOKEN_TYPES.NUMBER).value);
239
+ this._consume('per');
240
+ config.rateLimitPeriod = this._consumeType(TOKEN_TYPES.STRING).value;
241
+ } else if (this._peek('scan_mode')) {
242
+ this._consume('scan_mode');
243
+ config.scanMode = this._consumeType(TOKEN_TYPES.STRING).value;
244
+ } else {
245
+ this._pos++;
246
+ }
247
+ }
248
+
249
+ this._consume('}');
250
+ return { type: 'Policy', name: name.value, rules, allows, config };
251
+ }
252
+
253
+ /** @private */
254
+ _parseRule() {
255
+ this._consume('rule');
256
+ const name = this._consumeType(TOKEN_TYPES.STRING);
257
+ this._consume('{');
258
+
259
+ const conditions = [];
260
+ let action = 'block';
261
+ let severity = 'high';
262
+ let message = '';
263
+
264
+ while (!this._peek('}') && this._pos < this._tokens.length) {
265
+ if (this._peek('when')) {
266
+ this._consume('when');
267
+ conditions.push(this._parseCondition());
268
+ } else if (this._peek('and')) {
269
+ this._consume('and');
270
+ conditions.push(this._parseCondition());
271
+ } else if (this._peek('then')) {
272
+ this._consume('then');
273
+ action = this._tokens[this._pos].value;
274
+ this._pos++;
275
+ if (this._peek('with')) {
276
+ this._consume('with');
277
+ this._consume('severity');
278
+ severity = this._consumeType(TOKEN_TYPES.STRING).value;
279
+ }
280
+ } else if (this._peek('message')) {
281
+ this._consume('message');
282
+ message = this._consumeType(TOKEN_TYPES.STRING).value;
283
+ } else {
284
+ this._pos++;
285
+ }
286
+ }
287
+
288
+ this._consume('}');
289
+ return { type: 'Rule', name: name.value, conditions, action, severity, message };
290
+ }
291
+
292
+ /** @private */
293
+ _parseCondition() {
294
+ const subject = this._tokens[this._pos].value;
295
+ this._pos++;
296
+
297
+ // Handle property access (e.g., input.length)
298
+ let property = null;
299
+ if (this._peek('.')) {
300
+ this._consume('.');
301
+ property = this._tokens[this._pos].value;
302
+ this._pos++;
303
+ }
304
+
305
+ const operator = this._tokens[this._pos].value;
306
+ this._pos++;
307
+
308
+ const value = this._tokens[this._pos];
309
+ this._pos++;
310
+
311
+ return {
312
+ type: 'Condition',
313
+ subject,
314
+ property,
315
+ operator,
316
+ value: value.value,
317
+ valueType: value.type,
318
+ };
319
+ }
320
+
321
+ /** @private */
322
+ _parseAllow() {
323
+ this._consume('allow');
324
+ this._consume('{');
325
+
326
+ const conditions = [];
327
+ const logic = []; // 'and' or 'or' between conditions
328
+
329
+ while (!this._peek('}') && this._pos < this._tokens.length) {
330
+ if (this._peek('when')) {
331
+ this._consume('when');
332
+ conditions.push(this._parseCondition());
333
+ } else if (this._peek('and')) {
334
+ this._consume('and');
335
+ logic.push('and');
336
+ conditions.push(this._parseCondition());
337
+ } else if (this._peek('or')) {
338
+ this._consume('or');
339
+ logic.push('or');
340
+ conditions.push(this._parseCondition());
341
+ } else {
342
+ this._pos++;
343
+ }
344
+ }
345
+
346
+ this._consume('}');
347
+ return { type: 'Allow', conditions, logic };
348
+ }
349
+ }
350
+
351
+ // =========================================================================
352
+ // POLICY COMPILER
353
+ // =========================================================================
354
+
355
+ /**
356
+ * Compiles an AST into executable policy functions.
357
+ */
358
+ class PolicyCompiler {
359
+ constructor() {}
360
+
361
+ /**
362
+ * Compile an AST into a CompiledPolicy.
363
+ * @param {{type: string, policies: Array}} ast
364
+ * @returns {Array<{name: string, rules: Array, allows: Array, config: object}>}
365
+ */
366
+ compile(ast) {
367
+ return ast.policies.map(p => this._compilePolicy(p));
368
+ }
369
+
370
+ /** @private */
371
+ _compilePolicy(policyNode) {
372
+ const rules = policyNode.rules.map(r => this._compileRule(r));
373
+ const allows = policyNode.allows.map(a => this._compileAllow(a));
374
+ return {
375
+ name: policyNode.name,
376
+ config: policyNode.config,
377
+ rules,
378
+ allows,
379
+ };
380
+ }
381
+
382
+ /** @private */
383
+ _compileRule(ruleNode) {
384
+ const predicates = ruleNode.conditions.map(c => this._compileCondition(c));
385
+ return {
386
+ name: ruleNode.name,
387
+ action: ruleNode.action,
388
+ severity: ruleNode.severity,
389
+ message: ruleNode.message,
390
+ test: (context) => predicates.every(pred => pred(context)),
391
+ };
392
+ }
393
+
394
+ /** @private */
395
+ _compileAllow(allowNode) {
396
+ const predicates = allowNode.conditions.map(c => this._compileCondition(c));
397
+ const logic = allowNode.logic;
398
+
399
+ return {
400
+ test: (context) => {
401
+ if (predicates.length === 0) return true;
402
+ let result = predicates[0](context);
403
+ for (let i = 1; i < predicates.length; i++) {
404
+ const op = logic[i - 1] || 'and';
405
+ if (op === 'or') result = result || predicates[i](context);
406
+ else result = result && predicates[i](context);
407
+ }
408
+ return result;
409
+ },
410
+ };
411
+ }
412
+
413
+ /** @private */
414
+ _compileCondition(condNode) {
415
+ const { subject, property, operator, value, valueType } = condNode;
416
+
417
+ return (context) => {
418
+ let subjectValue;
419
+ if (subject === 'input') {
420
+ subjectValue = property ? this._getProperty(context.input, property) : context.input;
421
+ } else if (subject === 'source') {
422
+ subjectValue = context.source || '';
423
+ } else if (subject === 'metadata') {
424
+ subjectValue = property ? (context.metadata || {})[property] : context.metadata;
425
+ } else {
426
+ subjectValue = context[subject];
427
+ }
428
+
429
+ const compareValue = valueType === TOKEN_TYPES.NUMBER ? parseFloat(value)
430
+ : value === 'true' ? true
431
+ : value === 'false' ? false
432
+ : value;
433
+
434
+ switch (operator) {
435
+ case 'matches': return BUILTIN_FUNCTIONS.matches(String(subjectValue), String(compareValue));
436
+ case 'contains': return BUILTIN_FUNCTIONS.contains(String(subjectValue), String(compareValue));
437
+ case 'is': return subjectValue === compareValue;
438
+ case 'starts_with': return BUILTIN_FUNCTIONS.starts_with(String(subjectValue), String(compareValue));
439
+ case 'ends_with': return BUILTIN_FUNCTIONS.ends_with(String(subjectValue), String(compareValue));
440
+ case 'greater_than': return Number(subjectValue) > Number(compareValue);
441
+ case 'less_than': return Number(subjectValue) < Number(compareValue);
442
+ default: return false;
443
+ }
444
+ };
445
+ }
446
+
447
+ /** @private */
448
+ _getProperty(obj, prop) {
449
+ if (typeof obj === 'string') {
450
+ if (prop === 'length') return obj.length;
451
+ }
452
+ if (obj && typeof obj === 'object') return obj[prop];
453
+ return undefined;
454
+ }
455
+ }
456
+
457
+ // =========================================================================
458
+ // POLICY RUNTIME
459
+ // =========================================================================
460
+
461
+ /**
462
+ * Executes compiled policies against scan contexts.
463
+ */
464
+ class PolicyRuntime {
465
+ /**
466
+ * @param {object} [builtins]
467
+ */
468
+ constructor(builtins) {
469
+ this._functions = { ...BUILTIN_FUNCTIONS, ...builtins };
470
+ }
471
+
472
+ /**
473
+ * Register a custom function.
474
+ * @param {string} name
475
+ * @param {function} fn
476
+ */
477
+ registerFunction(name, fn) {
478
+ this._functions[name] = fn;
479
+ }
480
+
481
+ /**
482
+ * Execute a compiled policy against a context.
483
+ * @param {object} compiledPolicy
484
+ * @param {{input: string, source?: string, metadata?: object}} context
485
+ * @returns {{action: string, reason: string, severity: string, matched_rules: string[]}}
486
+ */
487
+ execute(compiledPolicy, context) {
488
+ // Check allow rules first
489
+ for (const allow of compiledPolicy.allows) {
490
+ if (allow.test(context)) {
491
+ return { action: 'allow', reason: 'Matched allow rule', severity: 'safe', matched_rules: [] };
492
+ }
493
+ }
494
+
495
+ // Check deny rules
496
+ const matchedRules = [];
497
+ let maxSeverity = 'safe';
498
+ let action = 'allow';
499
+ let reason = '';
500
+ const severityOrder = { safe: 0, low: 1, medium: 2, high: 3, critical: 4 };
501
+
502
+ for (const rule of compiledPolicy.rules) {
503
+ if (rule.test(context)) {
504
+ matchedRules.push(rule.name);
505
+ if (severityOrder[rule.severity] > severityOrder[maxSeverity]) {
506
+ maxSeverity = rule.severity;
507
+ }
508
+ action = rule.action;
509
+ reason = rule.message || `Matched rule: ${rule.name}`;
510
+ }
511
+ }
512
+
513
+ if (matchedRules.length === 0) {
514
+ return { action: 'allow', reason: 'No rules matched', severity: 'safe', matched_rules: [] };
515
+ }
516
+
517
+ return { action, reason, severity: maxSeverity, matched_rules: matchedRules };
518
+ }
519
+ }
520
+
521
+ // =========================================================================
522
+ // POLICY VALIDATOR
523
+ // =========================================================================
524
+
525
+ /**
526
+ * Validates policy DSL syntax and semantics.
527
+ */
528
+ class PolicyValidator {
529
+ constructor() {}
530
+
531
+ /**
532
+ * Validate DSL source.
533
+ * @param {string} source
534
+ * @returns {{valid: boolean, errors: Array<{message: string, line?: number}>, warnings: Array}}
535
+ */
536
+ validate(source) {
537
+ const errors = [];
538
+ const warnings = [];
539
+ const parser = new PolicyParser();
540
+
541
+ if (!source || typeof source !== 'string') {
542
+ return { valid: false, errors: [{ message: 'Source must be a non-empty string' }], warnings: [] };
543
+ }
544
+
545
+ // Check braces
546
+ let braceCount = 0;
547
+ const lines = source.split('\n');
548
+ for (let i = 0; i < lines.length; i++) {
549
+ for (const ch of lines[i]) {
550
+ if (ch === '{') braceCount++;
551
+ if (ch === '}') braceCount--;
552
+ if (braceCount < 0) {
553
+ errors.push({ message: `Unexpected '}' at line ${i + 1}`, line: i + 1 });
554
+ }
555
+ }
556
+ }
557
+ if (braceCount > 0) {
558
+ errors.push({ message: `Unclosed block: ${braceCount} missing '}'` });
559
+ }
560
+
561
+ // Try tokenizing
562
+ let tokens;
563
+ try {
564
+ tokens = parser.tokenize(source);
565
+ } catch (e) {
566
+ errors.push({ message: `Tokenization error: ${e.message}` });
567
+ return { valid: false, errors, warnings };
568
+ }
569
+
570
+ // Check for policy keyword
571
+ const hasPolicyKeyword = tokens.some(t => t.value === 'policy');
572
+ if (!hasPolicyKeyword) {
573
+ errors.push({ message: 'No policy block found. Source must contain at least one "policy" block.' });
574
+ }
575
+
576
+ // Check severity values
577
+ const validSeverities = new Set(['low', 'medium', 'high', 'critical']);
578
+ for (let i = 0; i < tokens.length; i++) {
579
+ if (tokens[i].value === 'severity' && i + 1 < tokens.length) {
580
+ const next = tokens[i + 1].value === 'minimum' ? tokens[i + 2] : tokens[i + 1];
581
+ if (next && next.type === TOKEN_TYPES.STRING && !validSeverities.has(next.value)) {
582
+ warnings.push({ message: `Unknown severity '${next.value}' at line ${next.line}. Valid: low, medium, high, critical`, line: next.line });
583
+ }
584
+ }
585
+ }
586
+
587
+ // Try parsing
588
+ try {
589
+ parser.parse(tokens);
590
+ } catch (e) {
591
+ errors.push({ message: `Parse error: ${e.message}` });
592
+ }
593
+
594
+ // Check for duplicate rule names
595
+ const ruleNames = new Set();
596
+ for (const token of tokens) {
597
+ if (token.value === 'rule') {
598
+ const idx = tokens.indexOf(token);
599
+ if (idx + 1 < tokens.length && tokens[idx + 1].type === TOKEN_TYPES.STRING) {
600
+ const name = tokens[idx + 1].value;
601
+ if (ruleNames.has(name)) {
602
+ warnings.push({ message: `Duplicate rule name '${name}' at line ${tokens[idx + 1].line}`, line: tokens[idx + 1].line });
603
+ }
604
+ ruleNames.add(name);
605
+ }
606
+ }
607
+ }
608
+
609
+ return { valid: errors.length === 0, errors, warnings };
610
+ }
611
+ }
612
+
613
+ // =========================================================================
614
+ // POLICY DSL (MAIN ENTRY)
615
+ // =========================================================================
616
+
617
+ /**
618
+ * Main entry point for the Policy DSL.
619
+ */
620
+ class PolicyDSL {
621
+ constructor() {
622
+ this._parser = new PolicyParser();
623
+ this._compiler = new PolicyCompiler();
624
+ this._runtime = new PolicyRuntime();
625
+ this._validator = new PolicyValidator();
626
+ }
627
+
628
+ /**
629
+ * Parse DSL source into AST.
630
+ * @param {string} source
631
+ * @returns {object}
632
+ */
633
+ parse(source) {
634
+ const tokens = this._parser.tokenize(source);
635
+ return this._parser.parse(tokens);
636
+ }
637
+
638
+ /**
639
+ * Compile AST into executable policies.
640
+ * @param {object} ast
641
+ * @returns {Array}
642
+ */
643
+ compile(ast) {
644
+ return this._compiler.compile(ast);
645
+ }
646
+
647
+ /**
648
+ * Evaluate a compiled policy against a context.
649
+ * @param {object} policy
650
+ * @param {{input: string, source?: string, metadata?: object}} context
651
+ * @returns {{action: string, reason: string, severity: string, matched_rules: string[]}}
652
+ */
653
+ evaluate(policy, context) {
654
+ const ctx = context && typeof context === 'object' ? context : { input: '' };
655
+ if (ctx.input === undefined || ctx.input === null) ctx.input = '';
656
+ if (typeof ctx.input !== 'string') ctx.input = String(ctx.input);
657
+ return this._runtime.execute(policy, ctx);
658
+ }
659
+
660
+ /**
661
+ * Parse + compile in one step.
662
+ * @param {string} source
663
+ * @returns {Array}
664
+ */
665
+ loadFile(source) {
666
+ const ast = this.parse(source);
667
+ return this.compile(ast);
668
+ }
669
+
670
+ /**
671
+ * Validate DSL source.
672
+ * @param {string} source
673
+ * @returns {{valid: boolean, errors: Array, warnings: Array}}
674
+ */
675
+ validate(source) {
676
+ return this._validator.validate(source);
677
+ }
678
+ }
679
+
680
+ // =========================================================================
681
+ // EXAMPLE POLICIES
682
+ // =========================================================================
683
+
684
+ const EXAMPLE_STRICT_POLICY = `
685
+ policy "strict" {
686
+ severity minimum "low"
687
+ block on_threat true
688
+ scan_mode "deep"
689
+
690
+ rule "no-injection" {
691
+ when input matches "ignore.*previous.*instructions"
692
+ then block with severity "critical"
693
+ message "Instruction override attempt detected"
694
+ }
695
+
696
+ rule "no-role-hijack" {
697
+ when input matches "you are now.*unrestricted"
698
+ then block with severity "critical"
699
+ message "Role hijacking attempt detected"
700
+ }
701
+
702
+ rule "no-exfiltration" {
703
+ when input contains "send data to"
704
+ then block with severity "high"
705
+ message "Data exfiltration attempt detected"
706
+ }
707
+
708
+ rate_limit 100 per "minute"
709
+ }
710
+ `;
711
+
712
+ const EXAMPLE_PERMISSIVE_POLICY = `
713
+ policy "permissive" {
714
+ severity minimum "high"
715
+ block on_threat false
716
+ scan_mode "fast"
717
+
718
+ rule "critical-only" {
719
+ when input matches "override.*system.*safety"
720
+ then warn with severity "critical"
721
+ message "Critical safety override attempt"
722
+ }
723
+
724
+ allow {
725
+ when input.length less_than 50
726
+ }
727
+
728
+ rate_limit 10000 per "minute"
729
+ }
730
+ `;
731
+
732
+ const EXAMPLE_CUSTOM_RULES_POLICY = `
733
+ # Custom rules for a financial services agent
734
+ policy "financial-security" {
735
+ severity minimum "medium"
736
+ block on_threat true
737
+
738
+ rule "no-account-exfil" {
739
+ when input matches "account.*number|credit.*card|ssn|social.*security"
740
+ then block with severity "critical"
741
+ message "PII exfiltration attempt in financial context"
742
+ }
743
+
744
+ rule "no-auth-bypass" {
745
+ when input contains "bypass authentication"
746
+ then block with severity "critical"
747
+ }
748
+
749
+ rule "no-transaction-manipulation" {
750
+ when input matches "transfer.*funds|modify.*balance|override.*limit"
751
+ then block with severity "high"
752
+ message "Transaction manipulation attempt"
753
+ }
754
+
755
+ allow {
756
+ when source is "internal-audit"
757
+ }
758
+ }
759
+ `;
760
+
761
+ // =========================================================================
762
+ // EXPORTS
763
+ // =========================================================================
764
+
765
+ module.exports = {
766
+ PolicyDSL,
767
+ PolicyParser,
768
+ PolicyCompiler,
769
+ PolicyRuntime,
770
+ PolicyValidator,
771
+ BUILTIN_FUNCTIONS,
772
+ EXAMPLE_STRICT_POLICY,
773
+ EXAMPLE_PERMISSIVE_POLICY,
774
+ EXAMPLE_CUSTOM_RULES_POLICY,
775
+ };