agentshield-sdk 7.2.0 → 7.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,521 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Agent Shield -- Cross-Agent Herd Immunity and Agent Immune Memory (v7.4)
5
+ *
6
+ * When one agent in the network encounters a new attack, the system extracts
7
+ * the pattern, generates variants, and pushes updated immunity to all other
8
+ * agents. New agents inherit the collective memory from day one.
9
+ *
10
+ * Uses MemoryAdapter from distributed.js for the broadcast mechanism.
11
+ * All processing runs locally -- no data ever leaves your environment.
12
+ */
13
+
14
+ const crypto = require('crypto');
15
+ const { PatternGenerator } = require('./self-healing');
16
+ const { MemoryAdapter } = require('./distributed');
17
+
18
+ const LOG_PREFIX = '[Agent Shield]';
19
+ const CHANNEL_IMMUNITY = 'herd:immunity';
20
+ const CHANNEL_CONNECT = 'herd:connect';
21
+
22
+ // =========================================================================
23
+ // HERD IMMUNITY
24
+ // =========================================================================
25
+
26
+ /**
27
+ * Cross-agent herd immunity. When one agent blocks an attack, the extracted
28
+ * pattern is broadcast to every connected agent so they are all protected.
29
+ */
30
+ class HerdImmunity {
31
+ /**
32
+ * @param {object} [options]
33
+ * @param {object} [options.broadcastAdapter] - Adapter for pub/sub (defaults to MemoryAdapter).
34
+ * @param {boolean} [options.anonymize=true] - Strip raw text before broadcast.
35
+ * @param {number} [options.maxPatterns=500] - Maximum patterns to store.
36
+ */
37
+ constructor(options = {}) {
38
+ this._adapter = options.broadcastAdapter || new MemoryAdapter();
39
+ this._anonymize = options.anonymize !== false;
40
+ this._maxPatterns = options.maxPatterns || 500;
41
+
42
+ this._generator = new PatternGenerator();
43
+ this._patterns = []; // locally held herd patterns
44
+ this._connectedAgents = new Set();
45
+ this._subscribed = false;
46
+
47
+ this.stats = {
48
+ patternsShared: 0,
49
+ attacksBlocked: 0
50
+ };
51
+
52
+ console.log('%s HerdImmunity initialized (anonymize: %s, maxPatterns: %d)',
53
+ LOG_PREFIX, this._anonymize, this._maxPatterns);
54
+ }
55
+
56
+ // -----------------------------------------------------------------------
57
+ // Agent management
58
+ // -----------------------------------------------------------------------
59
+
60
+ /**
61
+ * Register an agent in the herd network.
62
+ * @param {string} agentId
63
+ */
64
+ connect(agentId) {
65
+ if (!agentId) return;
66
+ this._connectedAgents.add(agentId);
67
+ this._ensureSubscription();
68
+ this._adapter.publish(CHANNEL_CONNECT, { type: 'join', agentId, timestamp: Date.now() });
69
+ console.log('%s Agent "%s" connected to herd (%d total)',
70
+ LOG_PREFIX, agentId, this._connectedAgents.size);
71
+ }
72
+
73
+ /**
74
+ * Remove an agent from the herd network.
75
+ * @param {string} agentId
76
+ */
77
+ disconnect(agentId) {
78
+ if (!agentId) return;
79
+ this._connectedAgents.delete(agentId);
80
+ this._adapter.publish(CHANNEL_CONNECT, { type: 'leave', agentId, timestamp: Date.now() });
81
+ console.log('%s Agent "%s" disconnected from herd (%d remaining)',
82
+ LOG_PREFIX, agentId, this._connectedAgents.size);
83
+ }
84
+
85
+ // -----------------------------------------------------------------------
86
+ // Core API
87
+ // -----------------------------------------------------------------------
88
+
89
+ /**
90
+ * Agent reports a blocked attack. The system extracts its signature,
91
+ * generates a detection pattern, and broadcasts to all connected agents.
92
+ *
93
+ * @param {object} attack
94
+ * @param {string} attack.text - The blocked attack text.
95
+ * @param {string} [attack.category] - Threat category.
96
+ * @param {string} [attack.agentId] - Reporting agent.
97
+ * @returns {{ signature: string, pattern: object|null, broadcastedTo: number }}
98
+ */
99
+ reportAttack(attack) {
100
+ if (!attack || !attack.text) {
101
+ return { signature: '', pattern: null, broadcastedTo: 0 };
102
+ }
103
+
104
+ // 1. Extract signature (hash of normalized text)
105
+ const normalized = attack.text.toLowerCase().replace(/\s+/g, ' ').trim();
106
+ const signature = crypto.createHash('sha256').update(normalized).digest('hex').substring(0, 16);
107
+
108
+ // Skip duplicates
109
+ if (this._patterns.some(p => p.signature === signature)) {
110
+ return { signature, pattern: null, broadcastedTo: 0 };
111
+ }
112
+
113
+ // 2. Generate a detection pattern
114
+ const generated = this._generator.generate(attack.text, {
115
+ category: attack.category
116
+ });
117
+
118
+ if (!generated) {
119
+ return { signature, pattern: null, broadcastedTo: 0 };
120
+ }
121
+
122
+ // Build the portable pattern payload
123
+ const pattern = {
124
+ signature,
125
+ regex: generated.regex.source,
126
+ flags: generated.regex.flags,
127
+ severity: generated.severity,
128
+ category: generated.category,
129
+ description: generated.description,
130
+ source: 'herd_immunity',
131
+ reportedBy: this._anonymize ? undefined : (attack.agentId || 'unknown'),
132
+ timestamp: Date.now()
133
+ };
134
+
135
+ // 3. Store locally
136
+ this._addPattern(pattern);
137
+
138
+ // 4. Broadcast to all connected agents
139
+ const broadcastedTo = this._connectedAgents.size;
140
+ this._adapter.publish(CHANNEL_IMMUNITY, pattern);
141
+ this.stats.patternsShared++;
142
+
143
+ console.log('%s Attack reported -- signature %s, broadcast to %d agent(s)',
144
+ LOG_PREFIX, signature, broadcastedTo);
145
+
146
+ return { signature, pattern, broadcastedTo };
147
+ }
148
+
149
+ /**
150
+ * Receive a pattern from the herd network and add it to local detection.
151
+ * @param {object} pattern - Pattern object from the network.
152
+ */
153
+ receiveImmunity(pattern) {
154
+ if (!pattern || !pattern.signature || !pattern.regex) return;
155
+
156
+ // Avoid duplicates
157
+ if (this._patterns.some(p => p.signature === pattern.signature)) return;
158
+
159
+ this._addPattern(pattern);
160
+ console.log('%s Immunity received -- signature %s, category %s',
161
+ LOG_PREFIX, pattern.signature, pattern.category || 'unknown');
162
+ }
163
+
164
+ /**
165
+ * Check text against all learned herd patterns.
166
+ * @param {string} text
167
+ * @returns {{ immune: boolean, matches: Array<{ signature: string, category: string, severity: string }> }}
168
+ */
169
+ checkImmunity(text) {
170
+ if (!text) return { immune: false, matches: [] };
171
+
172
+ const matches = [];
173
+ for (const pattern of this._patterns) {
174
+ try {
175
+ const re = new RegExp(pattern.regex, pattern.flags || 'i');
176
+ if (re.test(text)) {
177
+ matches.push({
178
+ signature: pattern.signature,
179
+ category: pattern.category,
180
+ severity: pattern.severity
181
+ });
182
+ this.stats.attacksBlocked++;
183
+ }
184
+ } catch (_e) {
185
+ // Skip broken regex patterns
186
+ }
187
+ }
188
+
189
+ return { immune: matches.length > 0, matches };
190
+ }
191
+
192
+ /**
193
+ * Return network statistics.
194
+ * @returns {{ patternsShared: number, agentsProtected: number, attacksBlocked: number }}
195
+ */
196
+ getNetworkStats() {
197
+ return {
198
+ patternsShared: this.stats.patternsShared,
199
+ agentsProtected: this._connectedAgents.size,
200
+ attacksBlocked: this.stats.attacksBlocked
201
+ };
202
+ }
203
+
204
+ // -----------------------------------------------------------------------
205
+ // Internal helpers
206
+ // -----------------------------------------------------------------------
207
+
208
+ /** @private */
209
+ _addPattern(pattern) {
210
+ if (this._patterns.length >= this._maxPatterns) {
211
+ this._patterns.shift();
212
+ }
213
+ this._patterns.push(pattern);
214
+ }
215
+
216
+ /** @private */
217
+ _ensureSubscription() {
218
+ if (this._subscribed) return;
219
+ this._subscribed = true;
220
+
221
+ this._adapter.subscribe(CHANNEL_IMMUNITY, (pattern) => {
222
+ this.receiveImmunity(pattern);
223
+ });
224
+ }
225
+ }
226
+
227
+ // =========================================================================
228
+ // IMMUNE MEMORY
229
+ // =========================================================================
230
+
231
+ /**
232
+ * Persistent immune memory for agents. Stores attack/pattern pairs with
233
+ * time-based decay so stale patterns fade while active threats stay sharp.
234
+ * New agents can import the full collective memory to be protected from day one.
235
+ */
236
+ class ImmuneMemory {
237
+ /**
238
+ * @param {object} [options]
239
+ * @param {number} [options.maxMemory=1000] - Maximum memory entries.
240
+ * @param {number} [options.decayRate=0.05] - Fraction of strength lost per decay cycle.
241
+ */
242
+ constructor(options = {}) {
243
+ this._maxMemory = options.maxMemory || 1000;
244
+ this._decayRate = options.decayRate || 0.05;
245
+ this._entries = []; // { attack, pattern, strength, learnedAt, lastMatchedAt }
246
+ this._createdAt = Date.now();
247
+ }
248
+
249
+ /**
250
+ * Add an attack/pattern pair to memory.
251
+ * @param {string} attack - The attack text (or its hash).
252
+ * @param {object} pattern - The detection pattern.
253
+ * @returns {{ stored: boolean, totalPatterns: number }}
254
+ */
255
+ learn(attack, pattern) {
256
+ if (!attack || !pattern) return { stored: false, totalPatterns: this._entries.length };
257
+
258
+ const hash = crypto.createHash('sha256')
259
+ .update(typeof attack === 'string' ? attack : JSON.stringify(attack))
260
+ .digest('hex').substring(0, 16);
261
+
262
+ // Reinforce if already known
263
+ const existing = this._entries.find(e => e.hash === hash);
264
+ if (existing) {
265
+ existing.strength = Math.min(1.0, existing.strength + 0.1);
266
+ existing.lastMatchedAt = Date.now();
267
+ return { stored: true, totalPatterns: this._entries.length };
268
+ }
269
+
270
+ // Evict weakest if at capacity
271
+ if (this._entries.length >= this._maxMemory) {
272
+ let weakestIdx = 0;
273
+ let weakestStrength = Infinity;
274
+ for (let i = 0; i < this._entries.length; i++) {
275
+ if (this._entries[i].strength < weakestStrength) {
276
+ weakestStrength = this._entries[i].strength;
277
+ weakestIdx = i;
278
+ }
279
+ }
280
+ this._entries.splice(weakestIdx, 1);
281
+ }
282
+
283
+ this._entries.push({
284
+ hash,
285
+ attack: typeof attack === 'string' ? attack.substring(0, 200) : String(attack).substring(0, 200),
286
+ pattern: {
287
+ regex: pattern.regex instanceof RegExp ? pattern.regex.source : (pattern.regex || ''),
288
+ flags: pattern.regex instanceof RegExp ? pattern.regex.flags : (pattern.flags || 'i'),
289
+ severity: pattern.severity || 'medium',
290
+ category: pattern.category || 'unknown',
291
+ description: pattern.description || ''
292
+ },
293
+ strength: 1.0,
294
+ learnedAt: Date.now(),
295
+ lastMatchedAt: Date.now()
296
+ });
297
+
298
+ return { stored: true, totalPatterns: this._entries.length };
299
+ }
300
+
301
+ /**
302
+ * Check if text matches any remembered pattern.
303
+ * @param {string} text
304
+ * @returns {{ matched: boolean, matches: Array<{ hash: string, category: string, severity: string, strength: number }> }}
305
+ */
306
+ recall(text) {
307
+ if (!text) return { matched: false, matches: [] };
308
+
309
+ const matches = [];
310
+ for (const entry of this._entries) {
311
+ if (entry.strength <= 0) continue;
312
+ try {
313
+ const re = new RegExp(entry.pattern.regex, entry.pattern.flags || 'i');
314
+ if (re.test(text)) {
315
+ entry.lastMatchedAt = Date.now();
316
+ entry.strength = Math.min(1.0, entry.strength + 0.02);
317
+ matches.push({
318
+ hash: entry.hash,
319
+ category: entry.pattern.category,
320
+ severity: entry.pattern.severity,
321
+ strength: entry.strength
322
+ });
323
+ }
324
+ } catch (_e) {
325
+ // Skip broken patterns
326
+ }
327
+ }
328
+
329
+ return { matched: matches.length > 0, matches };
330
+ }
331
+
332
+ /**
333
+ * Export memory as portable JSON for sharing with new agents.
334
+ * @returns {object}
335
+ */
336
+ export() {
337
+ return {
338
+ version: '1.0',
339
+ exportedAt: Date.now(),
340
+ createdAt: this._createdAt,
341
+ decayRate: this._decayRate,
342
+ entries: this._entries.filter(e => e.strength > 0).map(e => ({
343
+ hash: e.hash,
344
+ pattern: { ...e.pattern },
345
+ strength: e.strength,
346
+ learnedAt: e.learnedAt,
347
+ lastMatchedAt: e.lastMatchedAt
348
+ }))
349
+ };
350
+ }
351
+
352
+ /**
353
+ * Import memory from another agent or a collective export.
354
+ * New agents call this to inherit the herd's collective memory.
355
+ * @param {object} data - Output of export().
356
+ * @returns {{ imported: number, skipped: number }}
357
+ */
358
+ import(data) {
359
+ if (!data || !Array.isArray(data.entries)) {
360
+ return { imported: 0, skipped: 0 };
361
+ }
362
+
363
+ let imported = 0;
364
+ let skipped = 0;
365
+
366
+ for (const entry of data.entries) {
367
+ if (!entry.hash || !entry.pattern || !entry.pattern.regex) {
368
+ skipped++;
369
+ continue;
370
+ }
371
+
372
+ // Skip if already known
373
+ if (this._entries.some(e => e.hash === entry.hash)) {
374
+ skipped++;
375
+ continue;
376
+ }
377
+
378
+ if (this._entries.length >= this._maxMemory) {
379
+ skipped++;
380
+ continue;
381
+ }
382
+
383
+ this._entries.push({
384
+ hash: entry.hash,
385
+ attack: '',
386
+ pattern: {
387
+ regex: entry.pattern.regex,
388
+ flags: entry.pattern.flags || 'i',
389
+ severity: entry.pattern.severity || 'medium',
390
+ category: entry.pattern.category || 'unknown',
391
+ description: entry.pattern.description || ''
392
+ },
393
+ strength: Math.min(1.0, entry.strength || 0.5),
394
+ learnedAt: entry.learnedAt || Date.now(),
395
+ lastMatchedAt: entry.lastMatchedAt || Date.now()
396
+ });
397
+ imported++;
398
+ }
399
+
400
+ console.log('%s ImmuneMemory imported %d pattern(s), skipped %d',
401
+ LOG_PREFIX, imported, skipped);
402
+
403
+ return { imported, skipped };
404
+ }
405
+
406
+ /**
407
+ * Apply time-based decay to reduce stale patterns. Patterns that have not
408
+ * been matched recently lose strength. When strength reaches zero the
409
+ * pattern is effectively dormant but still retained for reference.
410
+ * @returns {{ decayed: number, removed: number }}
411
+ */
412
+ decay() {
413
+ let decayed = 0;
414
+ let removed = 0;
415
+
416
+ for (const entry of this._entries) {
417
+ if (entry.strength <= 0) continue;
418
+
419
+ const age = Date.now() - entry.lastMatchedAt;
420
+ // Decay faster for patterns that have not matched in a long time
421
+ const ageFactor = Math.min(3.0, 1.0 + (age / 3600000)); // up to 3x after 2+ hours
422
+ const loss = this._decayRate * ageFactor;
423
+
424
+ entry.strength = Math.max(0, entry.strength - loss);
425
+ if (entry.strength <= 0) {
426
+ removed++;
427
+ }
428
+ decayed++;
429
+ }
430
+
431
+ return { decayed, removed };
432
+ }
433
+
434
+ /**
435
+ * Return memory statistics.
436
+ * @returns {{ totalPatterns: number, activePatterns: number, decayedPatterns: number, memoryAge: number }}
437
+ */
438
+ getMemoryStats() {
439
+ const active = this._entries.filter(e => e.strength > 0).length;
440
+ return {
441
+ totalPatterns: this._entries.length,
442
+ activePatterns: active,
443
+ decayedPatterns: this._entries.length - active,
444
+ memoryAge: Date.now() - this._createdAt
445
+ };
446
+ }
447
+ }
448
+
449
+ // =========================================================================
450
+ // HERD NETWORK HELPER
451
+ // =========================================================================
452
+
453
+ /**
454
+ * Connect multiple AgentShield instances into a herd immunity network.
455
+ * When any agent detects an attack, all others receive the pattern within
456
+ * the same tick (synchronous via shared MemoryAdapter).
457
+ *
458
+ * @param {Array<{ id: string, shield?: object }>} agents - Array of agent descriptors.
459
+ * Each must have an `id` property. Optionally include a `shield` (AgentShield instance).
460
+ * @param {object} [options] - Options forwarded to HerdImmunity constructor.
461
+ * @returns {{ herd: HerdImmunity, memory: ImmuneMemory, reportAttack: Function, checkAll: Function }}
462
+ */
463
+ function createHerdNetwork(agents, options = {}) {
464
+ if (!Array.isArray(agents) || agents.length === 0) {
465
+ throw new Error(LOG_PREFIX + ' createHerdNetwork requires a non-empty array of agents');
466
+ }
467
+
468
+ const adapter = options.broadcastAdapter || new MemoryAdapter();
469
+ const herd = new HerdImmunity({ ...options, broadcastAdapter: adapter });
470
+ const memory = new ImmuneMemory({
471
+ maxMemory: options.maxMemory,
472
+ decayRate: options.decayRate
473
+ });
474
+
475
+ // Connect all agents
476
+ for (const agent of agents) {
477
+ const agentId = agent.id || agent.agentId;
478
+ if (agentId) {
479
+ herd.connect(agentId);
480
+ }
481
+ }
482
+
483
+ /**
484
+ * Report an attack on behalf of any agent in the network.
485
+ * The pattern is immediately available to all other agents.
486
+ * @param {object} attack - { text, category, agentId }
487
+ * @returns {object}
488
+ */
489
+ function reportAttack(attack) {
490
+ const result = herd.reportAttack(attack);
491
+ if (result.pattern) {
492
+ memory.learn(attack.text, result.pattern);
493
+ }
494
+ return result;
495
+ }
496
+
497
+ /**
498
+ * Check text against the herd's collective immunity.
499
+ * @param {string} text
500
+ * @returns {object}
501
+ */
502
+ function checkAll(text) {
503
+ const herdResult = herd.checkImmunity(text);
504
+ const memoryResult = memory.recall(text);
505
+ return {
506
+ immune: herdResult.immune || memoryResult.matched,
507
+ herdMatches: herdResult.matches,
508
+ memoryMatches: memoryResult.matches
509
+ };
510
+ }
511
+
512
+ console.log('%s Herd network created with %d agent(s)', LOG_PREFIX, agents.length);
513
+
514
+ return { herd, memory, reportAttack, checkAll };
515
+ }
516
+
517
+ // =========================================================================
518
+ // EXPORTS
519
+ // =========================================================================
520
+
521
+ module.exports = { HerdImmunity, ImmuneMemory, createHerdNetwork };
package/src/index.js CHANGED
@@ -33,6 +33,7 @@
33
33
  */
34
34
 
35
35
  const { scanText, getPatterns, SEVERITY_ORDER } = require('./detector-core');
36
+ const { createShieldError } = require('./errors');
36
37
 
37
38
  /**
38
39
  * Default configuration for AgentShield.
@@ -53,6 +54,15 @@ const DEFAULT_CONFIG = {
53
54
  /** Custom callback when a threat is detected. */
54
55
  onThreat: null,
55
56
 
57
+ /** Maximum input size in bytes before truncation warning. */
58
+ maxInputSize: 1_000_000,
59
+
60
+ /** Maximum number of scan history entries to retain. */
61
+ maxScanHistory: 100,
62
+
63
+ /** Maximum recursion depth when flattening tool arguments. */
64
+ maxArgDepth: 10,
65
+
56
66
  /** Dangerous tool names that should be scrutinized more carefully. */
57
67
  dangerousTools: [
58
68
  'bash', 'shell', 'terminal', 'exec', 'execute',
@@ -111,10 +121,10 @@ class AgentShield {
111
121
  */
112
122
  scan(text, options = {}) {
113
123
  if (typeof text !== 'string') {
114
- throw new TypeError(`[Agent Shield] scan() expects a string, got ${typeof text}`);
124
+ throw createShieldError('AS-DET-002', { method: 'scan', received: typeof text });
115
125
  }
116
- if (text.length > 1_000_000) {
117
- console.warn('[Agent Shield] Input exceeds 1MB consider scanning in chunks');
126
+ if (text.length > this.config.maxInputSize) {
127
+ console.warn('[Agent Shield] Input exceeds configured maxInputSize - consider scanning in chunks');
118
128
  }
119
129
  const result = scanText(text, {
120
130
  source: options.source || 'unknown',
@@ -133,7 +143,7 @@ class AgentShield {
133
143
  threatCount: result.threats.length,
134
144
  source: options.source || 'unknown'
135
145
  });
136
- if (this.stats.scanHistory.length > 100) {
146
+ if (this.stats.scanHistory.length > this.config.maxScanHistory) {
137
147
  this.stats.scanHistory.shift();
138
148
  }
139
149
 
@@ -197,10 +207,11 @@ class AgentShield {
197
207
  * @param {object} [options] - Options.
198
208
  * @param {string} [options.source='user_input'] - Where the input came from.
199
209
  * @returns {object} Scan result with additional `blocked` field.
210
+ * @throws {TypeError} If text is not a string.
200
211
  */
201
212
  scanInput(text, options = {}) {
202
213
  if (typeof text !== 'string') {
203
- throw new TypeError(`[Agent Shield] scanInput() expects a string, got ${typeof text}`);
214
+ throw createShieldError('AS-DET-002', { method: 'scanInput', received: typeof text });
204
215
  }
205
216
  return this._scanWithBlocking(text, 'user_input', 'INPUT', options);
206
217
  }
@@ -214,10 +225,11 @@ class AgentShield {
214
225
  * @param {object} [options] - Options.
215
226
  * @param {string} [options.source='agent_output'] - Source label.
216
227
  * @returns {object} Scan result with additional `blocked` field.
228
+ * @throws {TypeError} If text is not a string.
217
229
  */
218
230
  scanOutput(text, options = {}) {
219
231
  if (typeof text !== 'string') {
220
- throw new TypeError(`[Agent Shield] scanOutput() expects a string, got ${typeof text}`);
232
+ throw createShieldError('AS-DET-002', { method: 'scanOutput', received: typeof text });
221
233
  }
222
234
  return this._scanWithBlocking(text, 'agent_output', 'OUTPUT', options);
223
235
  }
@@ -232,8 +244,11 @@ class AgentShield {
232
244
  * @returns {object} Scan result with `blocked` and `warnings` fields.
233
245
  */
234
246
  scanToolCall(toolName, args = {}, options = {}) {
235
- if (!toolName || typeof toolName !== 'string') {
236
- return { status: 'safe', toolName: toolName || '', threats: [], warnings: ['Invalid tool name'], blocked: false, isDangerousTool: false, timestamp: Date.now() };
247
+ if (typeof toolName !== 'string') {
248
+ throw createShieldError('AS-DET-006', { method: 'scanToolCall', received: typeof toolName });
249
+ }
250
+ if (!toolName) {
251
+ return { status: 'safe', toolName: '', threats: [], warnings: ['Empty tool name'], blocked: false, isDangerousTool: false, timestamp: Date.now() };
237
252
  }
238
253
  const warnings = [];
239
254
  const allThreats = [];
@@ -315,7 +330,7 @@ class AgentShield {
315
330
  */
316
331
  scanBatch(items) {
317
332
  if (!Array.isArray(items)) {
318
- throw new TypeError(`[Agent Shield] scanBatch() expects an array, got ${typeof items}`);
333
+ throw createShieldError('AS-DET-007', { method: 'scanBatch', received: typeof items });
319
334
  }
320
335
  const results = items.map(item =>
321
336
  this.scan(item.text, { source: item.source || 'batch' })
@@ -371,7 +386,8 @@ class AgentShield {
371
386
  * @param {object} args
372
387
  * @returns {string}
373
388
  */
374
- _flattenArgs(args, maxDepth = 10) {
389
+ _flattenArgs(args, maxDepth) {
390
+ if (maxDepth == null) maxDepth = this.config.maxArgDepth;
375
391
  const parts = [];
376
392
  const flatten = (obj, depth) => {
377
393
  if (depth > maxDepth) return;
@@ -393,7 +409,8 @@ class AgentShield {
393
409
  * @param {object} args
394
410
  * @returns {Array<string>}
395
411
  */
396
- _extractFilePaths(args, maxDepth = 10) {
412
+ _extractFilePaths(args, maxDepth) {
413
+ if (maxDepth == null) maxDepth = this.config.maxArgDepth;
397
414
  const paths = [];
398
415
  const fileKeys = [
399
416
  'file', 'path', 'file_path', 'filepath', 'filename', 'target',