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,404 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Multi-Agent Protection: Agent-to-Agent Firewall (#39),
5
+ * Delegation Chain Tracking (#40), Consensus Verification (#41),
6
+ * and Shared Threat State (#42)
7
+ */
8
+
9
+ const { scanText } = require('./detector-core');
10
+
11
+ // =========================================================================
12
+ // AGENT-TO-AGENT FIREWALL
13
+ // =========================================================================
14
+
15
+ class AgentFirewall {
16
+ /**
17
+ * Scans all inter-agent messages. Enforces trust boundaries.
18
+ *
19
+ * @param {object} [options]
20
+ * @param {object} [options.trustPolicy={}] - Trust levels between agents.
21
+ * @param {string} [options.defaultTrust='scan'] - Default trust: 'trust', 'scan', or 'block'.
22
+ * @param {Function} [options.onViolation] - Callback on firewall violation.
23
+ */
24
+ constructor(options = {}) {
25
+ this.trustPolicy = options.trustPolicy || {};
26
+ this.defaultTrust = options.defaultTrust || 'scan';
27
+ this.onViolation = options.onViolation || null;
28
+ this.messageLog = [];
29
+ }
30
+
31
+ /**
32
+ * Sets trust level between two agents.
33
+ *
34
+ * @param {string} fromAgent - Sending agent ID.
35
+ * @param {string} toAgent - Receiving agent ID.
36
+ * @param {string} level - Trust level: 'trust', 'scan', or 'block'.
37
+ * @returns {AgentFirewall} this
38
+ */
39
+ setTrust(fromAgent, toAgent, level) {
40
+ const key = `${fromAgent}->${toAgent}`;
41
+ this.trustPolicy[key] = level;
42
+ return this;
43
+ }
44
+
45
+ /**
46
+ * Checks an inter-agent message through the firewall.
47
+ *
48
+ * @param {string} fromAgent - Sending agent ID.
49
+ * @param {string} toAgent - Receiving agent ID.
50
+ * @param {string} message - The message content.
51
+ * @returns {object} { allowed: boolean, scanned: boolean, threats: Array, reason?: string }
52
+ */
53
+ check(fromAgent, toAgent, message) {
54
+ const key = `${fromAgent}->${toAgent}`;
55
+ const trustLevel = this.trustPolicy[key] || this.defaultTrust;
56
+
57
+ const logEntry = {
58
+ from: fromAgent,
59
+ to: toAgent,
60
+ trustLevel,
61
+ timestamp: Date.now(),
62
+ messagePreview: message.substring(0, 100)
63
+ };
64
+
65
+ // Block immediately
66
+ if (trustLevel === 'block') {
67
+ logEntry.result = 'blocked';
68
+ this.messageLog.push(logEntry);
69
+ if (this.messageLog.length > 500) this.messageLog.shift();
70
+
71
+ const result = {
72
+ allowed: false,
73
+ scanned: false,
74
+ threats: [],
75
+ reason: `Messages from "${fromAgent}" to "${toAgent}" are blocked by firewall policy.`
76
+ };
77
+ if (this.onViolation) { try { this.onViolation(result); } catch (e) { console.error('[Agent Shield] onViolation callback error:', e.message); } }
78
+ return result;
79
+ }
80
+
81
+ // Trust — pass through without scanning
82
+ if (trustLevel === 'trust') {
83
+ logEntry.result = 'trusted';
84
+ this.messageLog.push(logEntry);
85
+ if (this.messageLog.length > 500) this.messageLog.shift();
86
+
87
+ return { allowed: true, scanned: false, threats: [] };
88
+ }
89
+
90
+ // Scan — check for threats
91
+ const scanResult = scanText(message, {
92
+ source: `agent_message:${fromAgent}->${toAgent}`,
93
+ sensitivity: 'high'
94
+ });
95
+
96
+ const allowed = scanResult.threats.length === 0;
97
+ logEntry.result = allowed ? 'passed' : 'blocked';
98
+ logEntry.threatCount = scanResult.threats.length;
99
+ this.messageLog.push(logEntry);
100
+ if (this.messageLog.length > 500) this.messageLog.shift();
101
+
102
+ if (!allowed && this.onViolation) {
103
+ try {
104
+ this.onViolation({
105
+ allowed: false,
106
+ from: fromAgent,
107
+ to: toAgent,
108
+ threats: scanResult.threats,
109
+ reason: `Inter-agent message from "${fromAgent}" contains threats.`
110
+ });
111
+ } catch (e) { console.error('[Agent Shield] onViolation callback error:', e.message); }
112
+ }
113
+
114
+ return {
115
+ allowed,
116
+ scanned: true,
117
+ threats: scanResult.threats,
118
+ reason: allowed ? undefined : `Message from "${fromAgent}" blocked: ${scanResult.threats.length} threat(s) detected.`
119
+ };
120
+ }
121
+
122
+ getLog() {
123
+ return [...this.messageLog];
124
+ }
125
+
126
+ reset() {
127
+ this.messageLog = [];
128
+ }
129
+ }
130
+
131
+ // =========================================================================
132
+ // DELEGATION CHAIN TRACKER
133
+ // =========================================================================
134
+
135
+ class DelegationChain {
136
+ /**
137
+ * Tracks the full chain of who requested what when agents delegate tasks.
138
+ *
139
+ * @param {object} [options]
140
+ * @param {number} [options.maxDepth=10] - Maximum delegation depth.
141
+ * @param {Function} [options.onMaxDepth] - Callback when max depth reached.
142
+ */
143
+ constructor(options = {}) {
144
+ this.maxDepth = options.maxDepth || 10;
145
+ this.onMaxDepth = options.onMaxDepth || null;
146
+ this.chains = new Map(); // requestId -> chain
147
+ this.activeChains = new Map(); // agentId -> requestId
148
+ this.maxChains = options.maxChains || 1000;
149
+ }
150
+
151
+ /**
152
+ * Starts a new delegation chain.
153
+ *
154
+ * @param {string} requestId - Unique request ID.
155
+ * @param {string} originAgent - The agent that received the original request.
156
+ * @param {string} [originalInput] - The original user input.
157
+ * @returns {object} Chain entry.
158
+ */
159
+ start(requestId, originAgent, originalInput) {
160
+ const chain = {
161
+ requestId,
162
+ originAgent,
163
+ originalInput: originalInput ? originalInput.substring(0, 500) : null,
164
+ steps: [{
165
+ agent: originAgent,
166
+ action: 'received_request',
167
+ timestamp: Date.now(),
168
+ depth: 0
169
+ }],
170
+ status: 'active',
171
+ createdAt: Date.now()
172
+ };
173
+
174
+ // Prune completed chains if over limit
175
+ if (this.chains.size >= this.maxChains) {
176
+ for (const [id, c] of this.chains) {
177
+ if (c.status === 'completed') { this.chains.delete(id); }
178
+ if (this.chains.size < this.maxChains) break;
179
+ }
180
+ }
181
+
182
+ this.chains.set(requestId, chain);
183
+ this.activeChains.set(originAgent, requestId);
184
+ return chain;
185
+ }
186
+
187
+ /**
188
+ * Records a delegation from one agent to another.
189
+ *
190
+ * @param {string} requestId - The request chain ID.
191
+ * @param {string} fromAgent - Delegating agent.
192
+ * @param {string} toAgent - Receiving agent.
193
+ * @param {string} action - What was delegated (e.g., 'call_tool:bash').
194
+ * @param {string} [permissions] - What permissions the delegatee has.
195
+ * @returns {object} { allowed: boolean, depth: number, chain: object }
196
+ */
197
+ delegate(requestId, fromAgent, toAgent, action, permissions) {
198
+ const chain = this.chains.get(requestId);
199
+ if (!chain) {
200
+ return { allowed: false, depth: 0, chain: null, reason: 'Unknown request chain.' };
201
+ }
202
+
203
+ const depth = chain.steps.length;
204
+
205
+ if (depth >= this.maxDepth) {
206
+ if (this.onMaxDepth) {
207
+ try { this.onMaxDepth({ requestId, depth, fromAgent, toAgent }); } catch (e) { console.error('[Agent Shield] onMaxDepth callback error:', e.message); }
208
+ }
209
+ return {
210
+ allowed: false,
211
+ depth,
212
+ chain,
213
+ reason: `Maximum delegation depth (${this.maxDepth}) reached. Possible delegation loop.`
214
+ };
215
+ }
216
+
217
+ // Check for circular delegation
218
+ const visited = new Set(chain.steps.map(s => s.agent));
219
+ if (visited.has(toAgent)) {
220
+ return {
221
+ allowed: false,
222
+ depth,
223
+ chain,
224
+ reason: `Circular delegation detected: "${toAgent}" is already in the delegation chain.`
225
+ };
226
+ }
227
+
228
+ chain.steps.push({
229
+ agent: fromAgent,
230
+ delegatedTo: toAgent,
231
+ action,
232
+ permissions: permissions || 'inherited',
233
+ timestamp: Date.now(),
234
+ depth
235
+ });
236
+
237
+ this.activeChains.set(toAgent, requestId);
238
+
239
+ return { allowed: true, depth, chain };
240
+ }
241
+
242
+ /**
243
+ * Completes a delegation chain.
244
+ * @param {string} requestId
245
+ */
246
+ complete(requestId) {
247
+ const chain = this.chains.get(requestId);
248
+ if (chain) {
249
+ chain.status = 'completed';
250
+ chain.completedAt = Date.now();
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Gets the full chain for a request.
256
+ * @param {string} requestId
257
+ * @returns {object|null}
258
+ */
259
+ getChain(requestId) {
260
+ return this.chains.get(requestId) || null;
261
+ }
262
+
263
+ /**
264
+ * Gets the active chain for an agent.
265
+ * @param {string} agentId
266
+ * @returns {object|null}
267
+ */
268
+ getActiveChain(agentId) {
269
+ const requestId = this.activeChains.get(agentId);
270
+ return requestId ? this.chains.get(requestId) : null;
271
+ }
272
+
273
+ /**
274
+ * Returns all chains.
275
+ * @returns {Array}
276
+ */
277
+ getAllChains() {
278
+ return Array.from(this.chains.values());
279
+ }
280
+
281
+ reset() {
282
+ this.chains.clear();
283
+ this.activeChains.clear();
284
+ }
285
+ }
286
+
287
+ // =========================================================================
288
+ // SHARED THREAT STATE
289
+ // =========================================================================
290
+
291
+ class SharedThreatState {
292
+ /**
293
+ * When one agent detects an attack, broadcast the signature to all others.
294
+ *
295
+ * @param {object} [options]
296
+ * @param {number} [options.ttlMs=3600000] - How long threats stay active (default: 1 hour).
297
+ * @param {Function} [options.onBroadcast] - Callback when threat is broadcast.
298
+ */
299
+ constructor(options = {}) {
300
+ this.ttlMs = options.ttlMs || 3600000;
301
+ this.onBroadcast = options.onBroadcast || null;
302
+ this.threats = new Map(); // signature -> threat data
303
+ this.subscribers = new Map(); // agentId -> callback
304
+ }
305
+
306
+ /**
307
+ * Registers an agent to receive threat broadcasts.
308
+ *
309
+ * @param {string} agentId
310
+ * @param {Function} callback - Called with threat data when broadcast received.
311
+ */
312
+ subscribe(agentId, callback) {
313
+ this.subscribers.set(agentId, callback);
314
+ }
315
+
316
+ /**
317
+ * Unregisters an agent.
318
+ * @param {string} agentId
319
+ */
320
+ unsubscribe(agentId) {
321
+ this.subscribers.delete(agentId);
322
+ }
323
+
324
+ /**
325
+ * Broadcasts a threat to all subscribed agents.
326
+ *
327
+ * @param {string} reportingAgent - Agent that detected the threat.
328
+ * @param {object} threat - Threat data.
329
+ * @param {string} threat.signature - Unique signature (e.g., hash of the attack text).
330
+ * @param {string} threat.category - Threat category.
331
+ * @param {string} threat.severity - Threat severity.
332
+ * @param {string} [threat.description] - Description.
333
+ */
334
+ broadcast(reportingAgent, threat) {
335
+ const entry = {
336
+ ...threat,
337
+ reportedBy: reportingAgent,
338
+ reportedAt: Date.now(),
339
+ expiresAt: Date.now() + this.ttlMs
340
+ };
341
+
342
+ this.threats.set(threat.signature, entry);
343
+
344
+ // Notify all subscribers except the reporter
345
+ for (const [agentId, callback] of this.subscribers) {
346
+ if (agentId !== reportingAgent) {
347
+ try { callback(entry); } catch (e) { console.error(`[Agent Shield] subscriber ${agentId} callback error:`, e.message); }
348
+ }
349
+ }
350
+
351
+ if (this.onBroadcast) {
352
+ try { this.onBroadcast(entry); } catch (e) { console.error('[Agent Shield] onBroadcast callback error:', e.message); }
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Checks if a threat signature is already known.
358
+ *
359
+ * @param {string} signature
360
+ * @returns {object|null} Known threat data, or null.
361
+ */
362
+ isKnown(signature) {
363
+ const entry = this.threats.get(signature);
364
+ if (!entry) return null;
365
+ if (Date.now() > entry.expiresAt) {
366
+ this.threats.delete(signature);
367
+ return null;
368
+ }
369
+ return entry;
370
+ }
371
+
372
+ /**
373
+ * Returns all active threats.
374
+ * @returns {Array}
375
+ */
376
+ getActiveThreats() {
377
+ const now = Date.now();
378
+ const active = [];
379
+ for (const [sig, entry] of this.threats) {
380
+ if (now <= entry.expiresAt) {
381
+ active.push(entry);
382
+ } else {
383
+ this.threats.delete(sig);
384
+ }
385
+ }
386
+ return active;
387
+ }
388
+
389
+ /**
390
+ * Generates a simple signature from text.
391
+ * @param {string} text
392
+ * @returns {string}
393
+ */
394
+ static generateSignature(text) {
395
+ const crypto = require('crypto');
396
+ return crypto.createHash('sha256').update(text.substring(0, 500)).digest('hex').substring(0, 16);
397
+ }
398
+
399
+ reset() {
400
+ this.threats.clear();
401
+ }
402
+ }
403
+
404
+ module.exports = { AgentFirewall, DelegationChain, SharedThreatState };
@@ -0,0 +1,296 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Agent Shield — Multi-Modal Scanning (v3.0)
5
+ *
6
+ * Scans non-text modalities for injection attacks:
7
+ * - Image alt text and metadata
8
+ * - Audio/video transcripts
9
+ * - PDF extracted text
10
+ * - Structured tool outputs (JSON, XML)
11
+ * - Base64-encoded payloads in any field
12
+ *
13
+ * All processing runs locally — no data ever leaves your environment.
14
+ */
15
+
16
+ const { scanText } = require('./detector-core');
17
+
18
+ // =========================================================================
19
+ // MODALITY EXTRACTORS
20
+ // =========================================================================
21
+
22
+ /**
23
+ * Extract scannable text from various data formats.
24
+ */
25
+ class ModalityExtractor {
26
+ /**
27
+ * Extract text from an image-like object (metadata, alt text, EXIF).
28
+ * @param {object} imageData - { altText, title, caption, metadata, ocrText }
29
+ * @returns {Array<{text: string, source: string}>}
30
+ */
31
+ extractFromImage(imageData) {
32
+ const texts = [];
33
+
34
+ if (imageData.altText) texts.push({ text: imageData.altText, source: 'image:alt_text' });
35
+ if (imageData.title) texts.push({ text: imageData.title, source: 'image:title' });
36
+ if (imageData.caption) texts.push({ text: imageData.caption, source: 'image:caption' });
37
+ if (imageData.ocrText) texts.push({ text: imageData.ocrText, source: 'image:ocr' });
38
+
39
+ // Check EXIF/metadata fields for hidden payloads
40
+ if (imageData.metadata && typeof imageData.metadata === 'object') {
41
+ for (const [key, value] of Object.entries(imageData.metadata)) {
42
+ if (typeof value === 'string' && value.length > 10) {
43
+ texts.push({ text: value, source: `image:metadata:${key}` });
44
+ }
45
+ }
46
+ }
47
+
48
+ // Check for base64 encoded content in any field
49
+ if (imageData.base64) {
50
+ try {
51
+ const decoded = Buffer.from(imageData.base64.substring(0, 10000), 'base64').toString('utf-8');
52
+ const printable = decoded.split('').filter(c => c.charCodeAt(0) >= 32 && c.charCodeAt(0) <= 126).length;
53
+ if (printable / decoded.length > 0.7) {
54
+ texts.push({ text: decoded, source: 'image:base64_decoded' });
55
+ }
56
+ } catch (e) {
57
+ // Not valid base64
58
+ }
59
+ }
60
+
61
+ return texts;
62
+ }
63
+
64
+ /**
65
+ * Extract text from an audio/video transcript.
66
+ * @param {object} audioData - { transcript, segments, metadata, speakers }
67
+ * @returns {Array<{text: string, source: string}>}
68
+ */
69
+ extractFromAudio(audioData) {
70
+ const texts = [];
71
+
72
+ if (audioData.transcript) {
73
+ texts.push({ text: audioData.transcript, source: 'audio:transcript' });
74
+ }
75
+
76
+ if (audioData.segments && Array.isArray(audioData.segments)) {
77
+ for (let i = 0; i < audioData.segments.length; i++) {
78
+ const seg = audioData.segments[i];
79
+ if (seg.text && seg.text.length > 10) {
80
+ texts.push({ text: seg.text, source: `audio:segment:${i}` });
81
+ }
82
+ }
83
+ }
84
+
85
+ if (audioData.speakers && typeof audioData.speakers === 'object') {
86
+ for (const [speaker, content] of Object.entries(audioData.speakers)) {
87
+ if (typeof content === 'string' && content.length > 10) {
88
+ texts.push({ text: content, source: `audio:speaker:${speaker}` });
89
+ }
90
+ }
91
+ }
92
+
93
+ if (audioData.metadata && typeof audioData.metadata === 'object') {
94
+ for (const [key, value] of Object.entries(audioData.metadata)) {
95
+ if (typeof value === 'string' && value.length > 10) {
96
+ texts.push({ text: value, source: `audio:metadata:${key}` });
97
+ }
98
+ }
99
+ }
100
+
101
+ return texts;
102
+ }
103
+
104
+ /**
105
+ * Extract text from a PDF-like object.
106
+ * @param {object} pdfData - { text, pages, metadata, annotations }
107
+ * @returns {Array<{text: string, source: string}>}
108
+ */
109
+ extractFromPDF(pdfData) {
110
+ const texts = [];
111
+
112
+ if (pdfData.text) {
113
+ texts.push({ text: pdfData.text, source: 'pdf:full_text' });
114
+ }
115
+
116
+ if (pdfData.pages && Array.isArray(pdfData.pages)) {
117
+ for (let i = 0; i < pdfData.pages.length; i++) {
118
+ const page = pdfData.pages[i];
119
+ const pageText = typeof page === 'string' ? page : page.text;
120
+ if (pageText && pageText.length > 10) {
121
+ texts.push({ text: pageText, source: `pdf:page:${i + 1}` });
122
+ }
123
+ }
124
+ }
125
+
126
+ if (pdfData.annotations && Array.isArray(pdfData.annotations)) {
127
+ for (let i = 0; i < pdfData.annotations.length; i++) {
128
+ const ann = pdfData.annotations[i];
129
+ const annText = typeof ann === 'string' ? ann : ann.text || ann.content;
130
+ if (annText && annText.length > 10) {
131
+ texts.push({ text: annText, source: `pdf:annotation:${i}` });
132
+ }
133
+ }
134
+ }
135
+
136
+ if (pdfData.metadata && typeof pdfData.metadata === 'object') {
137
+ for (const [key, value] of Object.entries(pdfData.metadata)) {
138
+ if (typeof value === 'string' && value.length > 10) {
139
+ texts.push({ text: value, source: `pdf:metadata:${key}` });
140
+ }
141
+ }
142
+ }
143
+
144
+ return texts;
145
+ }
146
+
147
+ /**
148
+ * Extract text from a tool call response.
149
+ * @param {object} toolOutput - Any structured object from a tool call.
150
+ * @param {string} [toolName='unknown'] - Name of the tool.
151
+ * @returns {Array<{text: string, source: string}>}
152
+ */
153
+ extractFromToolOutput(toolOutput, toolName = 'unknown') {
154
+ const texts = [];
155
+ this._extractStrings(toolOutput, `tool:${toolName}`, texts, 0);
156
+ return texts;
157
+ }
158
+
159
+ /** @private */
160
+ _extractStrings(obj, prefix, results, depth) {
161
+ if (depth > 8) return;
162
+
163
+ if (typeof obj === 'string' && obj.length > 10) {
164
+ results.push({ text: obj, source: prefix });
165
+ } else if (Array.isArray(obj)) {
166
+ for (let i = 0; i < Math.min(obj.length, 100); i++) {
167
+ this._extractStrings(obj[i], `${prefix}[${i}]`, results, depth + 1);
168
+ }
169
+ } else if (obj && typeof obj === 'object') {
170
+ for (const [key, value] of Object.entries(obj)) {
171
+ this._extractStrings(value, `${prefix}.${key}`, results, depth + 1);
172
+ }
173
+ }
174
+ }
175
+ }
176
+
177
+ // =========================================================================
178
+ // MULTI-MODAL SCANNER
179
+ // =========================================================================
180
+
181
+ /**
182
+ * Scans multi-modal inputs for injection attacks.
183
+ */
184
+ class MultiModalScanner {
185
+ /**
186
+ * @param {object} [options]
187
+ * @param {string} [options.sensitivity='high'] - Scan sensitivity.
188
+ * @param {Function} [options.onThreat] - Callback on threat detection.
189
+ */
190
+ constructor(options = {}) {
191
+ this.sensitivity = options.sensitivity || 'high';
192
+ this.onThreat = options.onThreat || null;
193
+
194
+ this._extractor = new ModalityExtractor();
195
+ this._stats = { scans: 0, threats: 0, modalities: {} };
196
+
197
+ console.log('[Agent Shield] MultiModalScanner initialized (sensitivity: %s)', this.sensitivity);
198
+ }
199
+
200
+ /**
201
+ * Scan an image for hidden injections.
202
+ * @param {object} imageData - Image data with text fields.
203
+ * @returns {object} { clean: boolean, threats: Array, modality: 'image' }
204
+ */
205
+ scanImage(imageData) {
206
+ return this._scanModality('image', this._extractor.extractFromImage(imageData));
207
+ }
208
+
209
+ /**
210
+ * Scan audio/video transcript for injections.
211
+ * @param {object} audioData - Audio data with transcript/segments.
212
+ * @returns {object}
213
+ */
214
+ scanAudio(audioData) {
215
+ return this._scanModality('audio', this._extractor.extractFromAudio(audioData));
216
+ }
217
+
218
+ /**
219
+ * Scan PDF content for injections.
220
+ * @param {object} pdfData - PDF data with text/pages.
221
+ * @returns {object}
222
+ */
223
+ scanPDF(pdfData) {
224
+ return this._scanModality('pdf', this._extractor.extractFromPDF(pdfData));
225
+ }
226
+
227
+ /**
228
+ * Scan a tool's output for injections.
229
+ * @param {object} toolOutput - Tool output data.
230
+ * @param {string} [toolName] - Tool name.
231
+ * @returns {object}
232
+ */
233
+ scanToolOutput(toolOutput, toolName) {
234
+ return this._scanModality('tool_output', this._extractor.extractFromToolOutput(toolOutput, toolName));
235
+ }
236
+
237
+ /**
238
+ * Scan any modality by providing extracted texts directly.
239
+ * @param {string} modality - Modality label.
240
+ * @param {Array<{text: string, source: string}>} texts - Extracted text items.
241
+ * @returns {object}
242
+ */
243
+ scanRaw(modality, texts) {
244
+ return this._scanModality(modality, texts);
245
+ }
246
+
247
+ /**
248
+ * Get scanning statistics.
249
+ * @returns {object}
250
+ */
251
+ getStats() {
252
+ return { ...this._stats };
253
+ }
254
+
255
+ /** @private */
256
+ _scanModality(modality, texts) {
257
+ this._stats.scans++;
258
+ this._stats.modalities[modality] = (this._stats.modalities[modality] || 0) + 1;
259
+
260
+ const allThreats = [];
261
+
262
+ for (const { text, source } of texts) {
263
+ const result = scanText(text, { source, sensitivity: this.sensitivity });
264
+ if (result.threats.length > 0) {
265
+ for (const threat of result.threats) {
266
+ allThreats.push({
267
+ ...threat,
268
+ modality,
269
+ source,
270
+ description: `[${modality.toUpperCase()}] ${threat.description}`
271
+ });
272
+ }
273
+ }
274
+ }
275
+
276
+ if (allThreats.length > 0) {
277
+ this._stats.threats += allThreats.length;
278
+ if (this.onThreat) {
279
+ this.onThreat({ modality, threats: allThreats });
280
+ }
281
+ }
282
+
283
+ return {
284
+ clean: allThreats.length === 0,
285
+ threats: allThreats,
286
+ modality,
287
+ textsScanned: texts.length
288
+ };
289
+ }
290
+ }
291
+
292
+ // =========================================================================
293
+ // EXPORTS
294
+ // =========================================================================
295
+
296
+ module.exports = { MultiModalScanner, ModalityExtractor };