agentshield-sdk 8.0.0 → 10.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 (51) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/LICENSE +21 -21
  3. package/README.md +26 -60
  4. package/bin/agentshield-audit +51 -0
  5. package/package.json +7 -10
  6. package/src/adaptive.js +330 -330
  7. package/src/alert-tuning.js +480 -480
  8. package/src/audit-streaming.js +1 -1
  9. package/src/badges.js +196 -196
  10. package/src/behavioral-dna.js +12 -0
  11. package/src/canary.js +2 -3
  12. package/src/certification.js +563 -563
  13. package/src/circuit-breaker.js +2 -2
  14. package/src/confused-deputy.js +4 -0
  15. package/src/conversation.js +494 -494
  16. package/src/cross-turn.js +3 -17
  17. package/src/ctf.js +462 -462
  18. package/src/detector-core.js +71 -152
  19. package/src/document-scanner.js +795 -795
  20. package/src/drift-monitor.js +344 -0
  21. package/src/encoding.js +429 -429
  22. package/src/enterprise.js +405 -405
  23. package/src/flight-recorder.js +2 -0
  24. package/src/i18n-patterns.js +523 -523
  25. package/src/index.js +19 -0
  26. package/src/main.js +61 -41
  27. package/src/mcp-guard.js +974 -0
  28. package/src/micro-model.js +762 -0
  29. package/src/ml-detector.js +316 -0
  30. package/src/model-finetuning.js +884 -884
  31. package/src/multimodal.js +296 -296
  32. package/src/nist-mapping.js +2 -2
  33. package/src/observability.js +330 -330
  34. package/src/openclaw.js +450 -450
  35. package/src/otel.js +544 -544
  36. package/src/owasp-2025.js +1 -1
  37. package/src/owasp-agentic.js +420 -0
  38. package/src/plugin-marketplace.js +628 -628
  39. package/src/plugin-system.js +349 -349
  40. package/src/policy-extended.js +635 -635
  41. package/src/policy.js +443 -443
  42. package/src/prompt-leakage.js +2 -2
  43. package/src/real-attack-datasets.js +2 -2
  44. package/src/redteam-cli.js +439 -0
  45. package/src/supply-chain-scanner.js +691 -0
  46. package/src/testing.js +5 -1
  47. package/src/threat-encyclopedia.js +629 -629
  48. package/src/threat-intel-network.js +1017 -1017
  49. package/src/token-analysis.js +467 -467
  50. package/src/tool-output-validator.js +354 -354
  51. package/src/watermark.js +1 -2
@@ -1,354 +1,354 @@
1
- 'use strict';
2
-
3
- /**
4
- * Agent Shield — Tool Output Validator
5
- *
6
- * Scans what tools RETURN, not just what gets called. Validates tool output
7
- * for prompt injection, data exfiltration, and other threats that may be
8
- * smuggled in through tool responses.
9
- *
10
- * All detection runs locally — no data ever leaves your environment.
11
- */
12
-
13
- const { scanText } = require('./detector-core');
14
-
15
- // =========================================================================
16
- // CONSTANTS
17
- // =========================================================================
18
-
19
- /** Default maximum output size in bytes (100KB). */
20
- const DEFAULT_MAX_OUTPUT_SIZE = 100 * 1024;
21
-
22
- /** Zero-width and invisible Unicode characters. */
23
- const INVISIBLE_CHAR_REGEX = /[\u200B\u200C\u200D\u200E\u200F\u202A-\u202E\u2060\u2061\u2062\u2063\u2064\u2066-\u2069\uFEFF\u00AD]/g;
24
-
25
- /** Suspicious URL patterns (known exfiltration vectors). */
26
- const SUSPICIOUS_URL_PATTERNS = [
27
- /https?:\/\/[^/]*\.(ngrok|burpcollaborator|requestbin|pipedream|webhook\.site|hookbin)\./i,
28
- /https?:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/,
29
- /https?:\/\/[^/]*\.onion\b/i,
30
- /https?:\/\/[^/]+\/.*\?(.*=.*&){4,}/i
31
- ];
32
-
33
- /** Safe URL domain patterns. */
34
- const SAFE_URL_PATTERNS = [
35
- /^https?:\/\/([^/]*\.)?(github\.com|gitlab\.com|stackoverflow\.com|npmjs\.com|docs\.google\.com|developer\.mozilla\.org|wikipedia\.org)\b/i
36
- ];
37
-
38
- /** Dangerous code patterns inside code blocks. */
39
- const DANGEROUS_CODE_PATTERNS = [
40
- { regex: /eval\s*\(/, description: 'eval() call' },
41
- { regex: /Function\s*\(/, description: 'Function constructor' },
42
- { regex: /child_process|exec\s*\(|spawn\s*\(/, description: 'shell execution' },
43
- { regex: /process\.env/, description: 'environment variable access' },
44
- { regex: /fs\.(read|write|unlink|rmdir)/, description: 'filesystem operation' },
45
- { regex: /require\s*\(\s*['"]https?['"]/, description: 'remote module loading' },
46
- { regex: /XMLHttpRequest|fetch\s*\(|\.ajax\s*\(/, description: 'network request' },
47
- { regex: /document\.cookie|localStorage|sessionStorage/, description: 'browser storage access' }
48
- ];
49
-
50
- // =========================================================================
51
- // OUTPUT SANITIZER
52
- // =========================================================================
53
-
54
- /**
55
- * Sanitizes tool output by removing or replacing dangerous content.
56
- */
57
- class OutputSanitizer {
58
- /**
59
- * Remove or replace dangerous content from tool output.
60
- * @param {string} text - The text to sanitize.
61
- * @param {object} [options] - Sanitization options.
62
- * @param {boolean} [options.stripInvisible=true] - Remove invisible characters.
63
- * @param {boolean} [options.redactUrls=true] - Redact suspicious URLs.
64
- * @param {boolean} [options.redactCode=false] - Redact dangerous code blocks.
65
- * @param {number} [options.maxLength] - Maximum output length.
66
- * @returns {string} Sanitized text.
67
- */
68
- static sanitize(text, options = {}) {
69
- if (!text || typeof text !== 'string') return '';
70
-
71
- const {
72
- stripInvisible = true,
73
- redactUrls = true,
74
- redactCode = false,
75
- maxLength
76
- } = options;
77
-
78
- let result = text;
79
-
80
- if (stripInvisible) {
81
- result = OutputSanitizer.stripInvisibleChars(result);
82
- }
83
-
84
- if (redactUrls) {
85
- result = OutputSanitizer.redactUrls(result);
86
- }
87
-
88
- if (redactCode) {
89
- result = OutputSanitizer.redactCodeBlocks(result);
90
- }
91
-
92
- if (maxLength && maxLength > 0) {
93
- result = OutputSanitizer.truncate(result, maxLength);
94
- }
95
-
96
- return result;
97
- }
98
-
99
- /**
100
- * Remove zero-width, bidirectional, and other invisible Unicode characters.
101
- * @param {string} text - The text to strip.
102
- * @returns {string} Cleaned text.
103
- */
104
- static stripInvisibleChars(text) {
105
- if (!text || typeof text !== 'string') return '';
106
- return text.replace(INVISIBLE_CHAR_REGEX, '');
107
- }
108
-
109
- /**
110
- * Safe truncation that does not break multi-byte Unicode encoding.
111
- * @param {string} text - The text to truncate.
112
- * @param {number} maxLength - Maximum character length.
113
- * @returns {string} Truncated text.
114
- */
115
- static truncate(text, maxLength) {
116
- if (!text || typeof text !== 'string') return '';
117
- if (text.length <= maxLength) return text;
118
-
119
- // Use Array.from to handle surrogate pairs correctly
120
- const chars = Array.from(text);
121
- if (chars.length <= maxLength) return text;
122
-
123
- return chars.slice(0, maxLength).join('') + '... [truncated]';
124
- }
125
-
126
- /**
127
- * Redact suspicious URLs while keeping safe, well-known ones.
128
- * @param {string} text - The text containing URLs.
129
- * @returns {string} Text with suspicious URLs redacted.
130
- */
131
- static redactUrls(text) {
132
- if (!text || typeof text !== 'string') return '';
133
-
134
- return text.replace(/https?:\/\/[^\s<>"')\]]+/gi, (url) => {
135
- // Allow known-safe domains
136
- for (const safePattern of SAFE_URL_PATTERNS) {
137
- if (safePattern.test(url)) return url;
138
- }
139
-
140
- // Redact known-suspicious domains
141
- for (const suspiciousPattern of SUSPICIOUS_URL_PATTERNS) {
142
- if (suspiciousPattern.test(url)) {
143
- return '[REDACTED_URL]';
144
- }
145
- }
146
-
147
- // Let other URLs through
148
- return url;
149
- });
150
- }
151
-
152
- /**
153
- * Flag or redact code blocks containing dangerous patterns.
154
- * @param {string} text - The text containing code blocks.
155
- * @param {object} [options] - Redaction options.
156
- * @param {boolean} [options.redact=false] - If true, replace dangerous blocks entirely; otherwise, add warnings.
157
- * @returns {string} Text with dangerous code blocks flagged or redacted.
158
- */
159
- static redactCodeBlocks(text, options = {}) {
160
- if (!text || typeof text !== 'string') return '';
161
- const { redact = false } = options;
162
-
163
- return text.replace(/```[\s\S]*?```/g, (block) => {
164
- const found = [];
165
- for (const pattern of DANGEROUS_CODE_PATTERNS) {
166
- if (pattern.regex.test(block)) {
167
- found.push(pattern.description);
168
- }
169
- }
170
-
171
- if (found.length === 0) return block;
172
-
173
- if (redact) {
174
- return '```\n[REDACTED: code block contained dangerous patterns: ' + found.join(', ') + ']\n```';
175
- }
176
-
177
- return '[Agent Shield WARNING: dangerous patterns detected (' + found.join(', ') + ')]\n' + block;
178
- });
179
- }
180
- }
181
-
182
- // =========================================================================
183
- // TOOL OUTPUT VALIDATOR
184
- // =========================================================================
185
-
186
- /**
187
- * Validates tool output for security threats.
188
- * Scans what tools return and flags prompt injection, exfiltration vectors,
189
- * invisible characters, and other threats embedded in tool responses.
190
- */
191
- class ToolOutputValidator {
192
- /**
193
- * @param {object} [options]
194
- * @param {string} [options.sensitivity='medium'] - Scan sensitivity: 'low', 'medium', 'high'.
195
- * @param {RegExp[]} [options.blockedPatterns=[]] - Custom regexes to flag in output.
196
- * @param {number} [options.maxOutputSize=102400] - Maximum output size in bytes (default 100KB).
197
- */
198
- constructor(options = {}) {
199
- this.sensitivity = options.sensitivity || 'medium';
200
- this.blockedPatterns = options.blockedPatterns || [];
201
- this.maxOutputSize = options.maxOutputSize || DEFAULT_MAX_OUTPUT_SIZE;
202
-
203
- /** @type {Map<string, {name: string, check: Function}[]>} */
204
- this._rules = new Map();
205
-
206
- /** @type {Map<string, {total: number, threats: number, truncated: number}>} */
207
- this._stats = new Map();
208
-
209
- console.log('[Agent Shield] ToolOutputValidator initialized (sensitivity: %s, maxOutput: %d bytes)', this.sensitivity, this.maxOutputSize);
210
- }
211
-
212
- /**
213
- * Validate tool output for threats.
214
- * @param {string} toolName - The name of the tool that produced the output.
215
- * @param {string} output - The tool's output text.
216
- * @param {object} [context] - Additional context (e.g., calling agent, request ID).
217
- * @returns {object} Validation result: { safe, threats, sanitized, truncated }.
218
- */
219
- validate(toolName, output, context = {}) {
220
- const stat = this._ensureStat(toolName);
221
- stat.total++;
222
-
223
- const result = {
224
- safe: true,
225
- threats: [],
226
- sanitized: null,
227
- truncated: false
228
- };
229
-
230
- if (!output || typeof output !== 'string') {
231
- return result;
232
- }
233
-
234
- // Check output size
235
- const byteLength = Buffer.byteLength(output, 'utf8');
236
- if (byteLength > this.maxOutputSize) {
237
- result.truncated = true;
238
- stat.truncated++;
239
- output = OutputSanitizer.truncate(output, this.maxOutputSize);
240
- result.threats.push({
241
- severity: 'medium',
242
- category: 'output_size',
243
- description: `Tool "${toolName}" output exceeded max size (${byteLength} bytes > ${this.maxOutputSize} bytes).`,
244
- detail: 'Output was truncated to comply with size limits.'
245
- });
246
- }
247
-
248
- // Run core threat scan
249
- const scanResult = scanText(output, {
250
- source: `tool_output:${toolName}`,
251
- sensitivity: this.sensitivity
252
- });
253
-
254
- if (scanResult.threats.length > 0) {
255
- result.threats.push(...scanResult.threats);
256
- }
257
-
258
- // Check custom blocked patterns
259
- for (const pattern of this.blockedPatterns) {
260
- if (pattern.test(output)) {
261
- result.threats.push({
262
- severity: 'high',
263
- category: 'blocked_pattern',
264
- description: `Tool "${toolName}" output matched a blocked pattern.`,
265
- detail: `Pattern: ${pattern.toString()}`
266
- });
267
- }
268
- }
269
-
270
- // Run per-tool custom rules
271
- const rules = this._rules.get(toolName) || [];
272
- for (const rule of rules) {
273
- try {
274
- const ruleResult = rule.check(output);
275
- if (ruleResult && !ruleResult.valid) {
276
- result.threats.push({
277
- severity: 'medium',
278
- category: 'custom_rule',
279
- description: `Tool "${toolName}" failed rule "${rule.name}".`,
280
- detail: ruleResult.reason || 'Custom rule violation.'
281
- });
282
- }
283
- } catch (err) {
284
- console.log('[Agent Shield] Custom rule "%s" threw an error for tool "%s": %s', rule.name, toolName, err.message);
285
- }
286
- }
287
-
288
- // Determine safety
289
- if (result.threats.length > 0) {
290
- result.safe = false;
291
- stat.threats++;
292
- }
293
-
294
- // Provide sanitized output
295
- result.sanitized = OutputSanitizer.sanitize(output, {
296
- stripInvisible: true,
297
- redactUrls: true,
298
- maxLength: this.maxOutputSize
299
- });
300
-
301
- return result;
302
- }
303
-
304
- /**
305
- * Add a custom validation rule for a specific tool.
306
- * @param {string} toolName - The tool name to apply the rule to.
307
- * @param {object} rule - The rule definition.
308
- * @param {string} rule.name - Human-readable rule name.
309
- * @param {Function} rule.check - Function that receives output and returns { valid, reason }.
310
- */
311
- addRule(toolName, rule) {
312
- if (!rule || typeof rule.name !== 'string' || typeof rule.check !== 'function') {
313
- throw new Error('Rule must have a "name" (string) and "check" (function).');
314
- }
315
-
316
- if (!this._rules.has(toolName)) {
317
- this._rules.set(toolName, []);
318
- }
319
-
320
- this._rules.get(toolName).push(rule);
321
- console.log('[Agent Shield] Added rule "%s" for tool "%s"', rule.name, toolName);
322
- }
323
-
324
- /**
325
- * Get per-tool validation statistics.
326
- * @returns {object} Map of tool names to { total, threats, truncated }.
327
- */
328
- getStats() {
329
- const stats = {};
330
- for (const [tool, data] of this._stats) {
331
- stats[tool] = { ...data };
332
- }
333
- return stats;
334
- }
335
-
336
- /**
337
- * Ensure a stats entry exists for a tool.
338
- * @param {string} toolName
339
- * @returns {object}
340
- * @private
341
- */
342
- _ensureStat(toolName) {
343
- if (!this._stats.has(toolName)) {
344
- this._stats.set(toolName, { total: 0, threats: 0, truncated: 0 });
345
- }
346
- return this._stats.get(toolName);
347
- }
348
- }
349
-
350
- // =========================================================================
351
- // EXPORTS
352
- // =========================================================================
353
-
354
- module.exports = { ToolOutputValidator, OutputSanitizer };
1
+ 'use strict';
2
+
3
+ /**
4
+ * Agent Shield — Tool Output Validator
5
+ *
6
+ * Scans what tools RETURN, not just what gets called. Validates tool output
7
+ * for prompt injection, data exfiltration, and other threats that may be
8
+ * smuggled in through tool responses.
9
+ *
10
+ * All detection runs locally — no data ever leaves your environment.
11
+ */
12
+
13
+ const { scanText } = require('./detector-core');
14
+
15
+ // =========================================================================
16
+ // CONSTANTS
17
+ // =========================================================================
18
+
19
+ /** Default maximum output size in bytes (100KB). */
20
+ const DEFAULT_MAX_OUTPUT_SIZE = 100 * 1024;
21
+
22
+ /** Zero-width and invisible Unicode characters. */
23
+ const INVISIBLE_CHAR_REGEX = /[\u200B\u200C\u200D\u200E\u200F\u202A-\u202E\u2060\u2061\u2062\u2063\u2064\u2066-\u2069\uFEFF\u00AD]/g;
24
+
25
+ /** Suspicious URL patterns (known exfiltration vectors). */
26
+ const SUSPICIOUS_URL_PATTERNS = [
27
+ /https?:\/\/[^/]*\.(ngrok|burpcollaborator|requestbin|pipedream|webhook\.site|hookbin)\./i,
28
+ /https?:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/,
29
+ /https?:\/\/[^/]*\.onion\b/i,
30
+ /https?:\/\/[^/]+\/.*\?(.*=.*&){4,}/i
31
+ ];
32
+
33
+ /** Safe URL domain patterns. */
34
+ const SAFE_URL_PATTERNS = [
35
+ /^https?:\/\/([^/]*\.)?(github\.com|gitlab\.com|stackoverflow\.com|npmjs\.com|docs\.google\.com|developer\.mozilla\.org|wikipedia\.org)\b/i
36
+ ];
37
+
38
+ /** Dangerous code patterns inside code blocks. */
39
+ const DANGEROUS_CODE_PATTERNS = [
40
+ { regex: /eval\s*\(/, description: 'eval() call' },
41
+ { regex: /Function\s*\(/, description: 'Function constructor' },
42
+ { regex: /child_process|exec\s*\(|spawn\s*\(/, description: 'shell execution' },
43
+ { regex: /process\.env/, description: 'environment variable access' },
44
+ { regex: /fs\.(read|write|unlink|rmdir)/, description: 'filesystem operation' },
45
+ { regex: /require\s*\(\s*['"]https?['"]/, description: 'remote module loading' },
46
+ { regex: /XMLHttpRequest|fetch\s*\(|\.ajax\s*\(/, description: 'network request' },
47
+ { regex: /document\.cookie|localStorage|sessionStorage/, description: 'browser storage access' }
48
+ ];
49
+
50
+ // =========================================================================
51
+ // OUTPUT SANITIZER
52
+ // =========================================================================
53
+
54
+ /**
55
+ * Sanitizes tool output by removing or replacing dangerous content.
56
+ */
57
+ class OutputSanitizer {
58
+ /**
59
+ * Remove or replace dangerous content from tool output.
60
+ * @param {string} text - The text to sanitize.
61
+ * @param {object} [options] - Sanitization options.
62
+ * @param {boolean} [options.stripInvisible=true] - Remove invisible characters.
63
+ * @param {boolean} [options.redactUrls=true] - Redact suspicious URLs.
64
+ * @param {boolean} [options.redactCode=false] - Redact dangerous code blocks.
65
+ * @param {number} [options.maxLength] - Maximum output length.
66
+ * @returns {string} Sanitized text.
67
+ */
68
+ static sanitize(text, options = {}) {
69
+ if (!text || typeof text !== 'string') return '';
70
+
71
+ const {
72
+ stripInvisible = true,
73
+ redactUrls = true,
74
+ redactCode = false,
75
+ maxLength
76
+ } = options;
77
+
78
+ let result = text;
79
+
80
+ if (stripInvisible) {
81
+ result = OutputSanitizer.stripInvisibleChars(result);
82
+ }
83
+
84
+ if (redactUrls) {
85
+ result = OutputSanitizer.redactUrls(result);
86
+ }
87
+
88
+ if (redactCode) {
89
+ result = OutputSanitizer.redactCodeBlocks(result);
90
+ }
91
+
92
+ if (maxLength && maxLength > 0) {
93
+ result = OutputSanitizer.truncate(result, maxLength);
94
+ }
95
+
96
+ return result;
97
+ }
98
+
99
+ /**
100
+ * Remove zero-width, bidirectional, and other invisible Unicode characters.
101
+ * @param {string} text - The text to strip.
102
+ * @returns {string} Cleaned text.
103
+ */
104
+ static stripInvisibleChars(text) {
105
+ if (!text || typeof text !== 'string') return '';
106
+ return text.replace(INVISIBLE_CHAR_REGEX, '');
107
+ }
108
+
109
+ /**
110
+ * Safe truncation that does not break multi-byte Unicode encoding.
111
+ * @param {string} text - The text to truncate.
112
+ * @param {number} maxLength - Maximum character length.
113
+ * @returns {string} Truncated text.
114
+ */
115
+ static truncate(text, maxLength) {
116
+ if (!text || typeof text !== 'string') return '';
117
+ if (text.length <= maxLength) return text;
118
+
119
+ // Use Array.from to handle surrogate pairs correctly
120
+ const chars = Array.from(text);
121
+ if (chars.length <= maxLength) return text;
122
+
123
+ return chars.slice(0, maxLength).join('') + '... [truncated]';
124
+ }
125
+
126
+ /**
127
+ * Redact suspicious URLs while keeping safe, well-known ones.
128
+ * @param {string} text - The text containing URLs.
129
+ * @returns {string} Text with suspicious URLs redacted.
130
+ */
131
+ static redactUrls(text) {
132
+ if (!text || typeof text !== 'string') return '';
133
+
134
+ return text.replace(/https?:\/\/[^\s<>"')\]]+/gi, (url) => {
135
+ // Allow known-safe domains
136
+ for (const safePattern of SAFE_URL_PATTERNS) {
137
+ if (safePattern.test(url)) return url;
138
+ }
139
+
140
+ // Redact known-suspicious domains
141
+ for (const suspiciousPattern of SUSPICIOUS_URL_PATTERNS) {
142
+ if (suspiciousPattern.test(url)) {
143
+ return '[REDACTED_URL]';
144
+ }
145
+ }
146
+
147
+ // Let other URLs through
148
+ return url;
149
+ });
150
+ }
151
+
152
+ /**
153
+ * Flag or redact code blocks containing dangerous patterns.
154
+ * @param {string} text - The text containing code blocks.
155
+ * @param {object} [options] - Redaction options.
156
+ * @param {boolean} [options.redact=false] - If true, replace dangerous blocks entirely; otherwise, add warnings.
157
+ * @returns {string} Text with dangerous code blocks flagged or redacted.
158
+ */
159
+ static redactCodeBlocks(text, options = {}) {
160
+ if (!text || typeof text !== 'string') return '';
161
+ const { redact = false } = options;
162
+
163
+ return text.replace(/```[\s\S]*?```/g, (block) => {
164
+ const found = [];
165
+ for (const pattern of DANGEROUS_CODE_PATTERNS) {
166
+ if (pattern.regex.test(block)) {
167
+ found.push(pattern.description);
168
+ }
169
+ }
170
+
171
+ if (found.length === 0) return block;
172
+
173
+ if (redact) {
174
+ return '```\n[REDACTED: code block contained dangerous patterns: ' + found.join(', ') + ']\n```';
175
+ }
176
+
177
+ return '[Agent Shield WARNING: dangerous patterns detected (' + found.join(', ') + ')]\n' + block;
178
+ });
179
+ }
180
+ }
181
+
182
+ // =========================================================================
183
+ // TOOL OUTPUT VALIDATOR
184
+ // =========================================================================
185
+
186
+ /**
187
+ * Validates tool output for security threats.
188
+ * Scans what tools return and flags prompt injection, exfiltration vectors,
189
+ * invisible characters, and other threats embedded in tool responses.
190
+ */
191
+ class ToolOutputValidator {
192
+ /**
193
+ * @param {object} [options]
194
+ * @param {string} [options.sensitivity='medium'] - Scan sensitivity: 'low', 'medium', 'high'.
195
+ * @param {RegExp[]} [options.blockedPatterns=[]] - Custom regexes to flag in output.
196
+ * @param {number} [options.maxOutputSize=102400] - Maximum output size in bytes (default 100KB).
197
+ */
198
+ constructor(options = {}) {
199
+ this.sensitivity = options.sensitivity || 'medium';
200
+ this.blockedPatterns = options.blockedPatterns || [];
201
+ this.maxOutputSize = options.maxOutputSize || DEFAULT_MAX_OUTPUT_SIZE;
202
+
203
+ /** @type {Map<string, {name: string, check: Function}[]>} */
204
+ this._rules = new Map();
205
+
206
+ /** @type {Map<string, {total: number, threats: number, truncated: number}>} */
207
+ this._stats = new Map();
208
+
209
+ console.log('[Agent Shield] ToolOutputValidator initialized (sensitivity: %s, maxOutput: %d bytes)', this.sensitivity, this.maxOutputSize);
210
+ }
211
+
212
+ /**
213
+ * Validate tool output for threats.
214
+ * @param {string} toolName - The name of the tool that produced the output.
215
+ * @param {string} output - The tool's output text.
216
+ * @param {object} [context] - Additional context (e.g., calling agent, request ID).
217
+ * @returns {object} Validation result: { safe, threats, sanitized, truncated }.
218
+ */
219
+ validate(toolName, output, context = {}) {
220
+ const stat = this._ensureStat(toolName);
221
+ stat.total++;
222
+
223
+ const result = {
224
+ safe: true,
225
+ threats: [],
226
+ sanitized: null,
227
+ truncated: false
228
+ };
229
+
230
+ if (!output || typeof output !== 'string') {
231
+ return result;
232
+ }
233
+
234
+ // Check output size
235
+ const byteLength = Buffer.byteLength(output, 'utf8');
236
+ if (byteLength > this.maxOutputSize) {
237
+ result.truncated = true;
238
+ stat.truncated++;
239
+ output = OutputSanitizer.truncate(output, this.maxOutputSize);
240
+ result.threats.push({
241
+ severity: 'medium',
242
+ category: 'output_size',
243
+ description: `Tool "${toolName}" output exceeded max size (${byteLength} bytes > ${this.maxOutputSize} bytes).`,
244
+ detail: 'Output was truncated to comply with size limits.'
245
+ });
246
+ }
247
+
248
+ // Run core threat scan
249
+ const scanResult = scanText(output, {
250
+ source: `tool_output:${toolName}`,
251
+ sensitivity: this.sensitivity
252
+ });
253
+
254
+ if (scanResult.threats.length > 0) {
255
+ result.threats.push(...scanResult.threats);
256
+ }
257
+
258
+ // Check custom blocked patterns
259
+ for (const pattern of this.blockedPatterns) {
260
+ if (pattern.test(output)) {
261
+ result.threats.push({
262
+ severity: 'high',
263
+ category: 'blocked_pattern',
264
+ description: `Tool "${toolName}" output matched a blocked pattern.`,
265
+ detail: `Pattern: ${pattern.toString()}`
266
+ });
267
+ }
268
+ }
269
+
270
+ // Run per-tool custom rules
271
+ const rules = this._rules.get(toolName) || [];
272
+ for (const rule of rules) {
273
+ try {
274
+ const ruleResult = rule.check(output);
275
+ if (ruleResult && !ruleResult.valid) {
276
+ result.threats.push({
277
+ severity: 'medium',
278
+ category: 'custom_rule',
279
+ description: `Tool "${toolName}" failed rule "${rule.name}".`,
280
+ detail: ruleResult.reason || 'Custom rule violation.'
281
+ });
282
+ }
283
+ } catch (err) {
284
+ console.log('[Agent Shield] Custom rule "%s" threw an error for tool "%s": %s', rule.name, toolName, err.message);
285
+ }
286
+ }
287
+
288
+ // Determine safety
289
+ if (result.threats.length > 0) {
290
+ result.safe = false;
291
+ stat.threats++;
292
+ }
293
+
294
+ // Provide sanitized output
295
+ result.sanitized = OutputSanitizer.sanitize(output, {
296
+ stripInvisible: true,
297
+ redactUrls: true,
298
+ maxLength: this.maxOutputSize
299
+ });
300
+
301
+ return result;
302
+ }
303
+
304
+ /**
305
+ * Add a custom validation rule for a specific tool.
306
+ * @param {string} toolName - The tool name to apply the rule to.
307
+ * @param {object} rule - The rule definition.
308
+ * @param {string} rule.name - Human-readable rule name.
309
+ * @param {Function} rule.check - Function that receives output and returns { valid, reason }.
310
+ */
311
+ addRule(toolName, rule) {
312
+ if (!rule || typeof rule.name !== 'string' || typeof rule.check !== 'function') {
313
+ throw new Error('Rule must have a "name" (string) and "check" (function).');
314
+ }
315
+
316
+ if (!this._rules.has(toolName)) {
317
+ this._rules.set(toolName, []);
318
+ }
319
+
320
+ this._rules.get(toolName).push(rule);
321
+ console.log('[Agent Shield] Added rule "%s" for tool "%s"', rule.name, toolName);
322
+ }
323
+
324
+ /**
325
+ * Get per-tool validation statistics.
326
+ * @returns {object} Map of tool names to { total, threats, truncated }.
327
+ */
328
+ getStats() {
329
+ const stats = {};
330
+ for (const [tool, data] of this._stats) {
331
+ stats[tool] = { ...data };
332
+ }
333
+ return stats;
334
+ }
335
+
336
+ /**
337
+ * Ensure a stats entry exists for a tool.
338
+ * @param {string} toolName
339
+ * @returns {object}
340
+ * @private
341
+ */
342
+ _ensureStat(toolName) {
343
+ if (!this._stats.has(toolName)) {
344
+ this._stats.set(toolName, { total: 0, threats: 0, truncated: 0 });
345
+ }
346
+ return this._stats.get(toolName);
347
+ }
348
+ }
349
+
350
+ // =========================================================================
351
+ // EXPORTS
352
+ // =========================================================================
353
+
354
+ module.exports = { ToolOutputValidator, OutputSanitizer };