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.
- package/CHANGELOG.md +191 -0
- package/LICENSE +21 -0
- package/README.md +975 -0
- package/bin/agent-shield.js +680 -0
- package/package.json +118 -0
- package/src/adaptive.js +330 -0
- package/src/agent-protocol.js +998 -0
- package/src/alert-tuning.js +480 -0
- package/src/allowlist.js +603 -0
- package/src/audit-immutable.js +914 -0
- package/src/audit-streaming.js +469 -0
- package/src/badges.js +196 -0
- package/src/behavior-profiling.js +289 -0
- package/src/benchmark-harness.js +804 -0
- package/src/canary.js +271 -0
- package/src/certification.js +563 -0
- package/src/circuit-breaker.js +321 -0
- package/src/compliance.js +617 -0
- package/src/confidence-tuning.js +324 -0
- package/src/confused-deputy.js +624 -0
- package/src/context-scoring.js +360 -0
- package/src/conversation.js +494 -0
- package/src/cost-optimizer.js +1024 -0
- package/src/ctf.js +462 -0
- package/src/detector-core.js +1999 -0
- package/src/distributed.js +359 -0
- package/src/document-scanner.js +795 -0
- package/src/embedding.js +307 -0
- package/src/encoding.js +429 -0
- package/src/enterprise.js +405 -0
- package/src/errors.js +100 -0
- package/src/eu-ai-act.js +523 -0
- package/src/fuzzer.js +764 -0
- package/src/honeypot.js +328 -0
- package/src/i18n-patterns.js +523 -0
- package/src/index.js +430 -0
- package/src/integrations.js +528 -0
- package/src/llm-redteam.js +670 -0
- package/src/main.js +741 -0
- package/src/main.mjs +38 -0
- package/src/mcp-bridge.js +542 -0
- package/src/mcp-certification.js +846 -0
- package/src/mcp-sdk-integration.js +355 -0
- package/src/mcp-security-runtime.js +741 -0
- package/src/mcp-server.js +740 -0
- package/src/middleware.js +208 -0
- package/src/model-finetuning.js +884 -0
- package/src/model-fingerprint.js +1042 -0
- package/src/multi-agent-trust.js +453 -0
- package/src/multi-agent.js +404 -0
- package/src/multimodal.js +296 -0
- package/src/nist-mapping.js +505 -0
- package/src/observability.js +330 -0
- package/src/openclaw.js +450 -0
- package/src/otel.js +544 -0
- package/src/owasp-2025.js +483 -0
- package/src/pii.js +390 -0
- package/src/plugin-marketplace.js +628 -0
- package/src/plugin-system.js +349 -0
- package/src/policy-dsl.js +775 -0
- package/src/policy-extended.js +635 -0
- package/src/policy.js +443 -0
- package/src/presets.js +409 -0
- package/src/production.js +557 -0
- package/src/prompt-leakage.js +321 -0
- package/src/rag-vulnerability.js +579 -0
- package/src/redteam.js +475 -0
- package/src/response-handler.js +429 -0
- package/src/scanners.js +357 -0
- package/src/self-healing.js +363 -0
- package/src/semantic.js +339 -0
- package/src/shield-score.js +250 -0
- package/src/sso-saml.js +897 -0
- package/src/stream-scanner.js +806 -0
- package/src/testing.js +505 -0
- package/src/threat-encyclopedia.js +629 -0
- package/src/threat-intel-network.js +1017 -0
- package/src/token-analysis.js +467 -0
- package/src/tool-guard.js +412 -0
- package/src/tool-output-validator.js +354 -0
- package/src/utils.js +83 -0
- package/src/watermark.js +235 -0
- package/src/worker-scanner.js +601 -0
- package/types/index.d.ts +2088 -0
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Shield — Worker Scanner
|
|
5
|
+
*
|
|
6
|
+
* Async scanning for non-blocking operation. Uses setImmediate/setTimeout to
|
|
7
|
+
* yield to the event loop between scans, preventing long-running scans from
|
|
8
|
+
* blocking the main thread.
|
|
9
|
+
*
|
|
10
|
+
* NOTE: This implementation uses async wrappers around the synchronous scanner
|
|
11
|
+
* with event loop yielding. In production environments requiring true parallel
|
|
12
|
+
* CPU-bound scanning, you can swap in Node.js worker_threads by replacing the
|
|
13
|
+
* _runInWorker method with actual Worker thread dispatch.
|
|
14
|
+
*
|
|
15
|
+
* All detection runs locally — no data ever leaves your environment.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const { scanText } = require('./detector-core');
|
|
19
|
+
|
|
20
|
+
// =========================================================================
|
|
21
|
+
// HELPERS
|
|
22
|
+
// =========================================================================
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Yield to the event loop. Uses setImmediate when available, falls back to setTimeout.
|
|
26
|
+
* @returns {Promise<void>}
|
|
27
|
+
*/
|
|
28
|
+
const yieldToEventLoop = () => new Promise(resolve => {
|
|
29
|
+
if (typeof setImmediate === 'function') {
|
|
30
|
+
setImmediate(resolve);
|
|
31
|
+
} else {
|
|
32
|
+
setTimeout(resolve, 0);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create a deferred promise with external resolve/reject.
|
|
38
|
+
* @returns {{ promise: Promise, resolve: Function, reject: Function }}
|
|
39
|
+
*/
|
|
40
|
+
function createDeferred() {
|
|
41
|
+
let resolve, reject;
|
|
42
|
+
const promise = new Promise((res, rej) => {
|
|
43
|
+
resolve = res;
|
|
44
|
+
reject = rej;
|
|
45
|
+
});
|
|
46
|
+
return { promise, resolve, reject };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// =========================================================================
|
|
50
|
+
// WORKER SCANNER
|
|
51
|
+
// =========================================================================
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Async scanner that runs scans without blocking the event loop.
|
|
55
|
+
* Manages a virtual "pool" with concurrency control and timeout support.
|
|
56
|
+
*/
|
|
57
|
+
class WorkerScanner {
|
|
58
|
+
/**
|
|
59
|
+
* @param {object} [options]
|
|
60
|
+
* @param {number} [options.poolSize=2] - Maximum concurrent scans.
|
|
61
|
+
* @param {number} [options.timeout=5000] - Per-scan timeout in milliseconds.
|
|
62
|
+
*/
|
|
63
|
+
constructor(options = {}) {
|
|
64
|
+
this.poolSize = options.poolSize || 2;
|
|
65
|
+
this.timeout = options.timeout || 5000;
|
|
66
|
+
|
|
67
|
+
this._activeWorkers = 0;
|
|
68
|
+
this._completedJobs = 0;
|
|
69
|
+
this._errorCount = 0;
|
|
70
|
+
this._queue = [];
|
|
71
|
+
this._terminated = false;
|
|
72
|
+
|
|
73
|
+
console.log('[Agent Shield] WorkerScanner initialized (poolSize: %d, timeout: %dms)', this.poolSize, this.timeout);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Scan text asynchronously without blocking the event loop.
|
|
78
|
+
* @param {string} text - The text to scan.
|
|
79
|
+
* @param {object} [options] - Scan options passed to scanText.
|
|
80
|
+
* @returns {Promise<object>} Scan result from detector-core.
|
|
81
|
+
*/
|
|
82
|
+
async scan(text, options = {}) {
|
|
83
|
+
if (this._terminated) {
|
|
84
|
+
throw new Error('WorkerScanner has been terminated.');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Wait for an available slot
|
|
88
|
+
while (this._activeWorkers >= this.poolSize) {
|
|
89
|
+
await yieldToEventLoop();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return this._runScan(text, options);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Scan multiple texts in parallel using the worker pool.
|
|
97
|
+
* @param {string[]} texts - Array of texts to scan.
|
|
98
|
+
* @param {object} [options] - Scan options passed to scanText.
|
|
99
|
+
* @returns {Promise<object[]>} Array of scan results.
|
|
100
|
+
*/
|
|
101
|
+
async scanBatch(texts, options = {}) {
|
|
102
|
+
if (this._terminated) {
|
|
103
|
+
throw new Error('WorkerScanner has been terminated.');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!Array.isArray(texts) || texts.length === 0) {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Launch all scans, concurrency is managed inside _runScan
|
|
111
|
+
const promises = texts.map(text => this.scan(text, options));
|
|
112
|
+
return Promise.all(promises);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get pool statistics.
|
|
117
|
+
* @returns {object} Stats: { activeWorkers, queuedJobs, completed, errors, poolSize, terminated }.
|
|
118
|
+
*/
|
|
119
|
+
getStats() {
|
|
120
|
+
return {
|
|
121
|
+
activeWorkers: this._activeWorkers,
|
|
122
|
+
queuedJobs: this._queue.length,
|
|
123
|
+
completed: this._completedJobs,
|
|
124
|
+
errors: this._errorCount,
|
|
125
|
+
poolSize: this.poolSize,
|
|
126
|
+
terminated: this._terminated
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Shut down the worker pool. Pending scans will be rejected.
|
|
132
|
+
*/
|
|
133
|
+
terminate() {
|
|
134
|
+
this._terminated = true;
|
|
135
|
+
|
|
136
|
+
// Reject any queued jobs
|
|
137
|
+
for (const job of this._queue) {
|
|
138
|
+
job.reject(new Error('WorkerScanner terminated.'));
|
|
139
|
+
}
|
|
140
|
+
this._queue = [];
|
|
141
|
+
|
|
142
|
+
console.log('[Agent Shield] WorkerScanner terminated (completed: %d, errors: %d)', this._completedJobs, this._errorCount);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Run a single scan with timeout and event loop yielding.
|
|
147
|
+
* @param {string} text
|
|
148
|
+
* @param {object} options
|
|
149
|
+
* @returns {Promise<object>}
|
|
150
|
+
* @private
|
|
151
|
+
*/
|
|
152
|
+
async _runScan(text, options) {
|
|
153
|
+
this._activeWorkers++;
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
// Yield to the event loop before starting CPU work
|
|
157
|
+
await yieldToEventLoop();
|
|
158
|
+
|
|
159
|
+
const result = await this._withTimeout(() => {
|
|
160
|
+
return scanText(text, options);
|
|
161
|
+
}, this.timeout);
|
|
162
|
+
|
|
163
|
+
this._completedJobs++;
|
|
164
|
+
|
|
165
|
+
// Yield after completing CPU work
|
|
166
|
+
await yieldToEventLoop();
|
|
167
|
+
|
|
168
|
+
return result;
|
|
169
|
+
} catch (err) {
|
|
170
|
+
this._errorCount++;
|
|
171
|
+
throw err;
|
|
172
|
+
} finally {
|
|
173
|
+
this._activeWorkers--;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Run a function with a timeout.
|
|
179
|
+
* @param {Function} fn - Synchronous function to run.
|
|
180
|
+
* @param {number} timeoutMs - Timeout in milliseconds.
|
|
181
|
+
* @returns {Promise<*>} Result of the function.
|
|
182
|
+
* @private
|
|
183
|
+
*/
|
|
184
|
+
_withTimeout(fn, timeoutMs) {
|
|
185
|
+
return new Promise((resolve, reject) => {
|
|
186
|
+
const timer = setTimeout(() => {
|
|
187
|
+
reject(new Error(`Scan timed out after ${timeoutMs}ms`));
|
|
188
|
+
}, timeoutMs);
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const result = fn();
|
|
192
|
+
clearTimeout(timer);
|
|
193
|
+
resolve(result);
|
|
194
|
+
} catch (err) {
|
|
195
|
+
clearTimeout(timer);
|
|
196
|
+
reject(err);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// =========================================================================
|
|
203
|
+
// SCAN QUEUE
|
|
204
|
+
// =========================================================================
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Priority queue for managing scan jobs with concurrency control,
|
|
208
|
+
* pause/resume, and drain support.
|
|
209
|
+
*/
|
|
210
|
+
class ScanQueue {
|
|
211
|
+
/**
|
|
212
|
+
* @param {object} [options]
|
|
213
|
+
* @param {number} [options.concurrency=4] - Maximum concurrent scans.
|
|
214
|
+
* @param {number} [options.maxQueue=10000] - Maximum queued items.
|
|
215
|
+
*/
|
|
216
|
+
constructor(options = {}) {
|
|
217
|
+
this.concurrency = options.concurrency || 4;
|
|
218
|
+
this.maxQueue = options.maxQueue || 10000;
|
|
219
|
+
|
|
220
|
+
this._queue = [];
|
|
221
|
+
this._activeCount = 0;
|
|
222
|
+
this._paused = false;
|
|
223
|
+
this._totalEnqueued = 0;
|
|
224
|
+
this._totalProcessed = 0;
|
|
225
|
+
this._totalErrors = 0;
|
|
226
|
+
this._latencySum = 0;
|
|
227
|
+
this._drainCallbacks = [];
|
|
228
|
+
|
|
229
|
+
console.log('[Agent Shield] ScanQueue initialized (concurrency: %d, maxQueue: %d)', this.concurrency, this.maxQueue);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Add a scan job to the queue.
|
|
234
|
+
* @param {string} text - The text to scan.
|
|
235
|
+
* @param {object} [options] - Scan options passed to scanText.
|
|
236
|
+
* @param {number} [priority=0] - Priority (higher = processed first).
|
|
237
|
+
* @returns {Promise<object>} Promise that resolves with the scan result.
|
|
238
|
+
*/
|
|
239
|
+
async enqueue(text, options = {}, priority = 0) {
|
|
240
|
+
if (this._queue.length >= this.maxQueue) {
|
|
241
|
+
throw new Error(`ScanQueue is full (${this.maxQueue} items). Rejecting new scan.`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const deferred = createDeferred();
|
|
245
|
+
const job = {
|
|
246
|
+
text,
|
|
247
|
+
options,
|
|
248
|
+
priority,
|
|
249
|
+
enqueuedAt: Date.now(),
|
|
250
|
+
deferred
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
this._queue.push(job);
|
|
254
|
+
this._totalEnqueued++;
|
|
255
|
+
|
|
256
|
+
// Sort by priority (descending) — highest priority first
|
|
257
|
+
this._queue.sort((a, b) => b.priority - a.priority);
|
|
258
|
+
|
|
259
|
+
// Try to process
|
|
260
|
+
this._processNext();
|
|
261
|
+
|
|
262
|
+
return deferred.promise;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Pause queue processing. In-flight scans will complete, but no new scans start.
|
|
267
|
+
*/
|
|
268
|
+
pause() {
|
|
269
|
+
this._paused = true;
|
|
270
|
+
console.log('[Agent Shield] ScanQueue paused');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Resume queue processing.
|
|
275
|
+
*/
|
|
276
|
+
resume() {
|
|
277
|
+
this._paused = false;
|
|
278
|
+
console.log('[Agent Shield] ScanQueue resumed');
|
|
279
|
+
this._processNext();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Wait for all pending and in-flight jobs to complete.
|
|
284
|
+
* @returns {Promise<void>}
|
|
285
|
+
*/
|
|
286
|
+
drain() {
|
|
287
|
+
if (this._queue.length === 0 && this._activeCount === 0) {
|
|
288
|
+
return Promise.resolve();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return new Promise(resolve => {
|
|
292
|
+
this._drainCallbacks.push(resolve);
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Get queue statistics.
|
|
298
|
+
* @returns {object} Stats: { depth, active, processed, errors, avgLatencyMs, paused }.
|
|
299
|
+
*/
|
|
300
|
+
getStats() {
|
|
301
|
+
const avgLatencyMs = this._totalProcessed > 0
|
|
302
|
+
? Math.round(this._latencySum / this._totalProcessed)
|
|
303
|
+
: 0;
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
depth: this._queue.length,
|
|
307
|
+
active: this._activeCount,
|
|
308
|
+
processed: this._totalProcessed,
|
|
309
|
+
errors: this._totalErrors,
|
|
310
|
+
avgLatencyMs,
|
|
311
|
+
paused: this._paused,
|
|
312
|
+
totalEnqueued: this._totalEnqueued,
|
|
313
|
+
maxQueue: this.maxQueue,
|
|
314
|
+
concurrency: this.concurrency
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Process the next job in the queue if concurrency allows.
|
|
320
|
+
* @private
|
|
321
|
+
*/
|
|
322
|
+
_processNext() {
|
|
323
|
+
if (this._paused) return;
|
|
324
|
+
if (this._activeCount >= this.concurrency) return;
|
|
325
|
+
if (this._queue.length === 0) {
|
|
326
|
+
this._checkDrain();
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const job = this._queue.shift();
|
|
331
|
+
this._activeCount++;
|
|
332
|
+
|
|
333
|
+
// Use setImmediate/setTimeout to avoid blocking the event loop
|
|
334
|
+
const run = async () => {
|
|
335
|
+
const startTime = Date.now();
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
// Yield before CPU work
|
|
339
|
+
await yieldToEventLoop();
|
|
340
|
+
|
|
341
|
+
const result = scanText(job.text, job.options);
|
|
342
|
+
const latency = Date.now() - job.enqueuedAt;
|
|
343
|
+
|
|
344
|
+
this._latencySum += latency;
|
|
345
|
+
this._totalProcessed++;
|
|
346
|
+
|
|
347
|
+
job.deferred.resolve(result);
|
|
348
|
+
} catch (err) {
|
|
349
|
+
this._totalErrors++;
|
|
350
|
+
job.deferred.reject(err);
|
|
351
|
+
} finally {
|
|
352
|
+
this._activeCount--;
|
|
353
|
+
|
|
354
|
+
// Yield after CPU work, then try next
|
|
355
|
+
await yieldToEventLoop();
|
|
356
|
+
this._processNext();
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
run();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Check if the queue has drained and notify any waiting callbacks.
|
|
365
|
+
* @private
|
|
366
|
+
*/
|
|
367
|
+
_checkDrain() {
|
|
368
|
+
if (this._queue.length === 0 && this._activeCount === 0) {
|
|
369
|
+
const callbacks = this._drainCallbacks;
|
|
370
|
+
this._drainCallbacks = [];
|
|
371
|
+
for (const cb of callbacks) {
|
|
372
|
+
cb();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// =========================================================================
|
|
379
|
+
// THREADED WORKER SCANNER (opt-in, uses worker_threads)
|
|
380
|
+
// =========================================================================
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Worker scanner using real Node.js worker_threads for true parallel scanning.
|
|
384
|
+
* Falls back to WorkerScanner (async yield) if worker_threads is unavailable.
|
|
385
|
+
*
|
|
386
|
+
* Usage:
|
|
387
|
+
* const scanner = new ThreadedWorkerScanner({ poolSize: 4 });
|
|
388
|
+
* const result = await scanner.scan('some text');
|
|
389
|
+
* scanner.terminate();
|
|
390
|
+
*
|
|
391
|
+
* NOTE: This uses worker_threads which requires Node.js >= 12. The zero-dep
|
|
392
|
+
* constraint is maintained since worker_threads is a Node.js built-in.
|
|
393
|
+
*/
|
|
394
|
+
class ThreadedWorkerScanner {
|
|
395
|
+
/**
|
|
396
|
+
* @param {object} [options]
|
|
397
|
+
* @param {number} [options.poolSize=2] - Number of worker threads.
|
|
398
|
+
* @param {number} [options.timeout=5000] - Per-scan timeout in ms.
|
|
399
|
+
*/
|
|
400
|
+
constructor(options = {}) {
|
|
401
|
+
this.poolSize = options.poolSize || 2;
|
|
402
|
+
this.timeout = options.timeout || 5000;
|
|
403
|
+
this._workers = [];
|
|
404
|
+
this._queue = [];
|
|
405
|
+
this._completedJobs = 0;
|
|
406
|
+
this._errorCount = 0;
|
|
407
|
+
this._terminated = false;
|
|
408
|
+
this._workerThreadsAvailable = false;
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
this._workerThreadsModule = require('worker_threads');
|
|
412
|
+
if (this._workerThreadsModule.isMainThread) {
|
|
413
|
+
this._workerThreadsAvailable = true;
|
|
414
|
+
this._initWorkers();
|
|
415
|
+
}
|
|
416
|
+
} catch (e) {
|
|
417
|
+
console.log('[Agent Shield] worker_threads not available, falling back to async mode.');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (!this._workerThreadsAvailable) {
|
|
421
|
+
this._fallback = new WorkerScanner({
|
|
422
|
+
poolSize: this.poolSize,
|
|
423
|
+
timeout: this.timeout
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
console.log('[Agent Shield] ThreadedWorkerScanner initialized (poolSize: %d, threaded: %s)', this.poolSize, this._workerThreadsAvailable);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Initialize the worker thread pool.
|
|
432
|
+
* @private
|
|
433
|
+
*/
|
|
434
|
+
_initWorkers() {
|
|
435
|
+
const { Worker } = this._workerThreadsModule;
|
|
436
|
+
const workerScript = `
|
|
437
|
+
const { parentPort } = require('worker_threads');
|
|
438
|
+
const { scanText } = require('${require('path').resolve(__dirname, 'detector-core.js').replace(/\\/g, '\\\\')}');
|
|
439
|
+
|
|
440
|
+
parentPort.on('message', (msg) => {
|
|
441
|
+
try {
|
|
442
|
+
const result = scanText(msg.text, msg.options || {});
|
|
443
|
+
parentPort.postMessage({ id: msg.id, result, error: null });
|
|
444
|
+
} catch (err) {
|
|
445
|
+
parentPort.postMessage({ id: msg.id, result: null, error: err.message });
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
`;
|
|
449
|
+
|
|
450
|
+
for (let i = 0; i < this.poolSize; i++) {
|
|
451
|
+
const worker = new Worker(workerScript, { eval: true });
|
|
452
|
+
worker._busy = false;
|
|
453
|
+
worker._currentJob = null;
|
|
454
|
+
|
|
455
|
+
worker.on('message', (msg) => {
|
|
456
|
+
const job = worker._currentJob;
|
|
457
|
+
worker._busy = false;
|
|
458
|
+
worker._currentJob = null;
|
|
459
|
+
|
|
460
|
+
if (job) {
|
|
461
|
+
clearTimeout(job.timer);
|
|
462
|
+
if (msg.error) {
|
|
463
|
+
this._errorCount++;
|
|
464
|
+
job.reject(new Error(msg.error));
|
|
465
|
+
} else {
|
|
466
|
+
this._completedJobs++;
|
|
467
|
+
job.resolve(msg.result);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
this._processQueue();
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
worker.on('error', (err) => {
|
|
475
|
+
const job = worker._currentJob;
|
|
476
|
+
worker._busy = false;
|
|
477
|
+
worker._currentJob = null;
|
|
478
|
+
this._errorCount++;
|
|
479
|
+
|
|
480
|
+
if (job) {
|
|
481
|
+
clearTimeout(job.timer);
|
|
482
|
+
job.reject(err);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
this._processQueue();
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
this._workers.push(worker);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Scan text using a worker thread (or fallback).
|
|
494
|
+
* @param {string} text - The text to scan.
|
|
495
|
+
* @param {object} [options] - Scan options passed to scanText.
|
|
496
|
+
* @returns {Promise<object>} Scan result.
|
|
497
|
+
*/
|
|
498
|
+
async scan(text, options = {}) {
|
|
499
|
+
if (this._terminated) {
|
|
500
|
+
throw new Error('ThreadedWorkerScanner has been terminated.');
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (!this._workerThreadsAvailable) {
|
|
504
|
+
return this._fallback.scan(text, options);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return new Promise((resolve, reject) => {
|
|
508
|
+
const id = ++this._completedJobs + this._errorCount + this._queue.length;
|
|
509
|
+
const job = { id, text, options, resolve, reject, timer: null };
|
|
510
|
+
|
|
511
|
+
job.timer = setTimeout(() => {
|
|
512
|
+
reject(new Error(`Scan timed out after ${this.timeout}ms`));
|
|
513
|
+
}, this.timeout);
|
|
514
|
+
|
|
515
|
+
this._queue.push(job);
|
|
516
|
+
this._processQueue();
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Scan multiple texts in parallel.
|
|
522
|
+
* @param {string[]} texts - Array of texts to scan.
|
|
523
|
+
* @param {object} [options] - Scan options.
|
|
524
|
+
* @returns {Promise<object[]>} Array of scan results.
|
|
525
|
+
*/
|
|
526
|
+
async scanBatch(texts, options = {}) {
|
|
527
|
+
if (!Array.isArray(texts) || texts.length === 0) return [];
|
|
528
|
+
return Promise.all(texts.map(text => this.scan(text, options)));
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Process queued jobs by dispatching to idle workers.
|
|
533
|
+
* @private
|
|
534
|
+
*/
|
|
535
|
+
_processQueue() {
|
|
536
|
+
if (this._queue.length === 0) return;
|
|
537
|
+
|
|
538
|
+
for (const worker of this._workers) {
|
|
539
|
+
if (!worker._busy && this._queue.length > 0) {
|
|
540
|
+
const job = this._queue.shift();
|
|
541
|
+
worker._busy = true;
|
|
542
|
+
worker._currentJob = job;
|
|
543
|
+
worker.postMessage({ id: job.id, text: job.text, options: job.options });
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Get pool statistics.
|
|
550
|
+
* @returns {object}
|
|
551
|
+
*/
|
|
552
|
+
getStats() {
|
|
553
|
+
if (!this._workerThreadsAvailable && this._fallback) {
|
|
554
|
+
return { ...this._fallback.getStats(), threaded: false };
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return {
|
|
558
|
+
activeWorkers: this._workers.filter(w => w._busy).length,
|
|
559
|
+
queuedJobs: this._queue.length,
|
|
560
|
+
completed: this._completedJobs,
|
|
561
|
+
errors: this._errorCount,
|
|
562
|
+
poolSize: this.poolSize,
|
|
563
|
+
terminated: this._terminated,
|
|
564
|
+
threaded: true
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Shut down all worker threads.
|
|
570
|
+
*/
|
|
571
|
+
terminate() {
|
|
572
|
+
this._terminated = true;
|
|
573
|
+
|
|
574
|
+
if (this._workerThreadsAvailable) {
|
|
575
|
+
for (const worker of this._workers) {
|
|
576
|
+
if (worker._currentJob) {
|
|
577
|
+
clearTimeout(worker._currentJob.timer);
|
|
578
|
+
worker._currentJob.reject(new Error('ThreadedWorkerScanner terminated.'));
|
|
579
|
+
}
|
|
580
|
+
worker.terminate();
|
|
581
|
+
}
|
|
582
|
+
this._workers = [];
|
|
583
|
+
} else if (this._fallback) {
|
|
584
|
+
this._fallback.terminate();
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
for (const job of this._queue) {
|
|
588
|
+
clearTimeout(job.timer);
|
|
589
|
+
job.reject(new Error('ThreadedWorkerScanner terminated.'));
|
|
590
|
+
}
|
|
591
|
+
this._queue = [];
|
|
592
|
+
|
|
593
|
+
console.log('[Agent Shield] ThreadedWorkerScanner terminated (completed: %d, errors: %d)', this._completedJobs, this._errorCount);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// =========================================================================
|
|
598
|
+
// EXPORTS
|
|
599
|
+
// =========================================================================
|
|
600
|
+
|
|
601
|
+
module.exports = { WorkerScanner, ScanQueue, ThreadedWorkerScanner };
|