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,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 };
package/src/utils.js ADDED
@@ -0,0 +1,83 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Agent Shield — Shared Utilities
5
+ *
6
+ * Common helpers used across multiple modules to avoid duplication.
7
+ */
8
+
9
+ /**
10
+ * Calculate a letter grade from a numeric score (0-100).
11
+ */
12
+ function getGrade(score) {
13
+ if (score >= 95) return 'A+';
14
+ if (score >= 90) return 'A';
15
+ if (score >= 85) return 'A-';
16
+ if (score >= 80) return 'B+';
17
+ if (score >= 75) return 'B';
18
+ if (score >= 70) return 'B-';
19
+ if (score >= 65) return 'C+';
20
+ if (score >= 60) return 'C';
21
+ if (score >= 55) return 'C-';
22
+ if (score >= 50) return 'D';
23
+ return 'F';
24
+ }
25
+
26
+ /**
27
+ * Get a human-readable grade label.
28
+ */
29
+ function getGradeLabel(score) {
30
+ if (score >= 95) return 'A+ — Excellent';
31
+ if (score >= 90) return 'A — Strong';
32
+ if (score >= 80) return 'B — Good';
33
+ if (score >= 70) return 'C — Moderate';
34
+ if (score >= 60) return 'D — Weak';
35
+ return 'F — Critical gaps';
36
+ }
37
+
38
+ /**
39
+ * Render a progress bar using block characters.
40
+ */
41
+ function makeBar(filled, total, width) {
42
+ const ratio = total > 0 ? filled / total : 0;
43
+ const filledCount = Math.round(ratio * width);
44
+ return '█'.repeat(filledCount) + '░'.repeat(width - filledCount);
45
+ }
46
+
47
+ /**
48
+ * Truncate text to a maximum length with an optional suffix.
49
+ */
50
+ function truncate(text, maxLength = 200, suffix = '') {
51
+ if (!text || text.length <= maxLength) return text || '';
52
+ return text.substring(0, maxLength) + suffix;
53
+ }
54
+
55
+ /**
56
+ * Format a boxed console header.
57
+ */
58
+ function formatHeader(title, width = 54) {
59
+ const padded = title.length < width - 4
60
+ ? ' '.repeat(Math.floor((width - 2 - title.length) / 2)) + title + ' '.repeat(Math.ceil((width - 2 - title.length) / 2))
61
+ : title;
62
+ return [
63
+ '╔' + '═'.repeat(width) + '╗',
64
+ '║' + padded + '║',
65
+ '╚' + '═'.repeat(width) + '╝'
66
+ ].join('\n');
67
+ }
68
+
69
+ /**
70
+ * Generate a unique event ID.
71
+ */
72
+ function generateId(prefix = 'evt') {
73
+ return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8).padEnd(6, '0')}`;
74
+ }
75
+
76
+ module.exports = {
77
+ getGrade,
78
+ getGradeLabel,
79
+ makeBar,
80
+ truncate,
81
+ formatHeader,
82
+ generateId
83
+ };
@@ -0,0 +1,235 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Output Watermarking (#44) and Differential Privacy (#46)
5
+ *
6
+ * - Watermarking: Embed invisible watermarks in agent outputs for tracing.
7
+ * - Differential Privacy: Add noise to stored conversation data.
8
+ */
9
+
10
+ const crypto = require('crypto');
11
+
12
+ // =========================================================================
13
+ // OUTPUT WATERMARKING
14
+ // =========================================================================
15
+
16
+ /**
17
+ * Zero-width characters used for binary watermark encoding.
18
+ */
19
+ const WM_ZERO = '\u200B'; // zero-width space = 0
20
+ const WM_ONE = '\u200C'; // zero-width non-joiner = 1
21
+ const WM_START = '\u200D'; // zero-width joiner = start marker
22
+ const WM_END = '\uFEFF'; // byte order mark = end marker
23
+
24
+ class OutputWatermark {
25
+ /**
26
+ * @param {object} [options]
27
+ * @param {string} [options.secret] - Secret key for HMAC signing.
28
+ * @param {boolean} [options.includeTimestamp=true] - Include timestamp in watermark.
29
+ */
30
+ constructor(options = {}) {
31
+ this.secret = options.secret || crypto.randomBytes(16).toString('hex');
32
+ this.includeTimestamp = options.includeTimestamp !== undefined ? options.includeTimestamp : true;
33
+ }
34
+
35
+ /**
36
+ * Embeds an invisible watermark in text.
37
+ *
38
+ * @param {string} text - The text to watermark.
39
+ * @param {object} metadata - Data to encode in the watermark.
40
+ * @param {string} [metadata.agentId] - Agent identifier.
41
+ * @param {string} [metadata.sessionId] - Session identifier.
42
+ * @returns {string} Watermarked text.
43
+ */
44
+ embed(text, metadata = {}) {
45
+ if (!text) return text;
46
+
47
+ const payload = {
48
+ ...metadata,
49
+ ts: this.includeTimestamp ? Date.now() : undefined
50
+ };
51
+
52
+ // Create signed payload
53
+ const payloadStr = JSON.stringify(payload);
54
+ const signature = crypto.createHmac('sha256', this.secret)
55
+ .update(payloadStr)
56
+ .digest('hex')
57
+ .substring(0, 8);
58
+
59
+ const data = `${payloadStr}|${signature}`;
60
+
61
+ // Encode to binary using zero-width characters
62
+ const binary = this._textToBinary(data);
63
+ const watermark = WM_START + binary + WM_END;
64
+
65
+ // Insert watermark in the middle of the text to be less detectable
66
+ const midpoint = Math.floor(text.length / 2);
67
+ // Find a space near the midpoint
68
+ let insertAt = text.indexOf(' ', midpoint);
69
+ if (insertAt === -1) insertAt = midpoint;
70
+
71
+ return text.slice(0, insertAt) + watermark + text.slice(insertAt);
72
+ }
73
+
74
+ /**
75
+ * Extracts a watermark from text.
76
+ *
77
+ * @param {string} text - Text that may contain a watermark.
78
+ * @returns {object} { found: boolean, metadata?: object, verified: boolean }
79
+ */
80
+ extract(text) {
81
+ if (!text) return { found: false };
82
+
83
+ const startIdx = text.indexOf(WM_START);
84
+ const endIdx = text.indexOf(WM_END, startIdx);
85
+
86
+ if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) {
87
+ return { found: false };
88
+ }
89
+
90
+ const binary = text.slice(startIdx + 1, endIdx);
91
+ const data = this._binaryToText(binary);
92
+
93
+ if (!data) return { found: false };
94
+
95
+ const pipeIdx = data.lastIndexOf('|');
96
+ if (pipeIdx === -1) return { found: false };
97
+
98
+ const payloadStr = data.substring(0, pipeIdx);
99
+ const signature = data.substring(pipeIdx + 1);
100
+
101
+ // Verify signature
102
+ const expectedSig = crypto.createHmac('sha256', this.secret)
103
+ .update(payloadStr)
104
+ .digest('hex')
105
+ .substring(0, 8);
106
+
107
+ const verified = signature === expectedSig;
108
+
109
+ let metadata = null;
110
+ try {
111
+ metadata = JSON.parse(payloadStr);
112
+ } catch (e) {
113
+ return { found: true, verified: false, raw: payloadStr };
114
+ }
115
+
116
+ return { found: true, metadata, verified };
117
+ }
118
+
119
+ /**
120
+ * Removes watermark from text.
121
+ *
122
+ * @param {string} text
123
+ * @returns {string} Clean text.
124
+ */
125
+ strip(text) {
126
+ if (!text) return text;
127
+ return text.replace(/[\u200B\u200C\u200D\uFEFF]/g, '');
128
+ }
129
+
130
+ /** @private */
131
+ _textToBinary(text) {
132
+ let binary = '';
133
+ for (let i = 0; i < text.length; i++) {
134
+ const charCode = text.charCodeAt(i);
135
+ const bits = charCode.toString(2).padStart(8, '0');
136
+ for (const bit of bits) {
137
+ binary += bit === '0' ? WM_ZERO : WM_ONE;
138
+ }
139
+ }
140
+ return binary;
141
+ }
142
+
143
+ /** @private */
144
+ _binaryToText(binary) {
145
+ try {
146
+ let text = '';
147
+ let bits = '';
148
+ for (const char of binary) {
149
+ if (char === WM_ZERO) bits += '0';
150
+ else if (char === WM_ONE) bits += '1';
151
+ else continue;
152
+
153
+ if (bits.length === 8) {
154
+ text += String.fromCharCode(parseInt(bits, 2));
155
+ bits = '';
156
+ }
157
+ }
158
+ return text;
159
+ } catch (e) {
160
+ return null;
161
+ }
162
+ }
163
+ }
164
+
165
+ // =========================================================================
166
+ // DIFFERENTIAL PRIVACY FOR AGENT MEMORY
167
+ // =========================================================================
168
+
169
+ class DifferentialPrivacy {
170
+ /**
171
+ * Adds noise to stored conversation data so individual user data
172
+ * can't be extracted even if the memory store is compromised.
173
+ *
174
+ * @param {object} [options]
175
+ * @param {number} [options.epsilon=1.0] - Privacy budget. Lower = more private, noisier.
176
+ * @param {number} [options.redactProbability=0.1] - Probability of redacting a token.
177
+ */
178
+ constructor(options = {}) {
179
+ this.epsilon = options.epsilon || 1.0;
180
+ this.redactProbability = options.redactProbability || 0.1;
181
+ }
182
+
183
+ /**
184
+ * Sanitizes text for storage by adding noise.
185
+ *
186
+ * @param {string} text - Text to sanitize.
187
+ * @returns {object} { sanitized: string, tokensRedacted: number }
188
+ */
189
+ sanitize(text) {
190
+ if (!text) return { sanitized: text, tokensRedacted: 0 };
191
+
192
+ const words = text.split(/\s+/);
193
+ let tokensRedacted = 0;
194
+
195
+ const sanitized = words.map(word => {
196
+ // Higher epsilon = less noise (more utility)
197
+ const threshold = this.redactProbability / this.epsilon;
198
+
199
+ if (Math.random() < threshold) {
200
+ tokensRedacted++;
201
+ return '[REDACTED]';
202
+ }
203
+
204
+ // For numbers, add Laplacian noise
205
+ if (/^\d+\.?\d*$/.test(word)) {
206
+ const num = parseFloat(word);
207
+ const noise = this._laplacianNoise(1 / this.epsilon);
208
+ const noisy = Math.round((num + noise) * 100) / 100;
209
+ return String(noisy);
210
+ }
211
+
212
+ return word;
213
+ });
214
+
215
+ return {
216
+ sanitized: sanitized.join(' '),
217
+ tokensRedacted
218
+ };
219
+ }
220
+
221
+ /**
222
+ * Generates Laplacian noise for numeric privacy.
223
+ * @private
224
+ * @param {number} scale - Scale parameter (b = sensitivity/epsilon).
225
+ * @returns {number}
226
+ */
227
+ _laplacianNoise(scale) {
228
+ // Use crypto for proper randomness instead of Math.random()
229
+ const bytes = crypto.randomBytes(4);
230
+ const u = (bytes.readUInt32BE(0) / 0xFFFFFFFF) - 0.5;
231
+ return -scale * Math.sign(u) * Math.log(1 - 2 * Math.abs(u));
232
+ }
233
+ }
234
+
235
+ module.exports = { OutputWatermark, DifferentialPrivacy };