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,467 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Shield — Token-Level Analysis Module
|
|
5
|
+
*
|
|
6
|
+
* Detects prompt injection through statistical analysis of text.
|
|
7
|
+
* Uses Shannon entropy, character n-gram perplexity estimation,
|
|
8
|
+
* and vocabulary burst detection to identify injected content.
|
|
9
|
+
*
|
|
10
|
+
* All computation is pure JavaScript — no external dependencies.
|
|
11
|
+
* No data ever leaves the user's environment.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// =========================================================================
|
|
15
|
+
// TEXT STATISTICS (utility class)
|
|
16
|
+
// =========================================================================
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Utility class for computing text statistics.
|
|
20
|
+
* All methods are static and operate on raw strings.
|
|
21
|
+
*/
|
|
22
|
+
class TextStatistics {
|
|
23
|
+
/**
|
|
24
|
+
* Compute Shannon entropy of the character distribution in text.
|
|
25
|
+
* H = -sum(p * log2(p)) for each character frequency p.
|
|
26
|
+
* @param {string} text - Input text
|
|
27
|
+
* @returns {number} Shannon entropy in bits
|
|
28
|
+
*/
|
|
29
|
+
static charEntropy(text) {
|
|
30
|
+
if (!text || text.length === 0) return 0;
|
|
31
|
+
|
|
32
|
+
const freq = {};
|
|
33
|
+
for (let i = 0; i < text.length; i++) {
|
|
34
|
+
const ch = text[i];
|
|
35
|
+
freq[ch] = (freq[ch] || 0) + 1;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const len = text.length;
|
|
39
|
+
let entropy = 0;
|
|
40
|
+
const keys = Object.keys(freq);
|
|
41
|
+
for (let i = 0; i < keys.length; i++) {
|
|
42
|
+
const p = freq[keys[i]] / len;
|
|
43
|
+
if (p > 0) {
|
|
44
|
+
entropy -= p * Math.log2(p);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return entropy;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Build a word frequency map from text.
|
|
53
|
+
* @param {string} text - Input text
|
|
54
|
+
* @returns {Object<string, number>} Map of word to count
|
|
55
|
+
*/
|
|
56
|
+
static wordFrequency(text) {
|
|
57
|
+
if (!text || text.length === 0) return {};
|
|
58
|
+
|
|
59
|
+
const words = text.toLowerCase().match(/[a-z'-]+/g);
|
|
60
|
+
if (!words) return {};
|
|
61
|
+
|
|
62
|
+
const freq = {};
|
|
63
|
+
for (let i = 0; i < words.length; i++) {
|
|
64
|
+
const w = words[i];
|
|
65
|
+
freq[w] = (freq[w] || 0) + 1;
|
|
66
|
+
}
|
|
67
|
+
return freq;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Compute vocabulary richness as the type-token ratio.
|
|
72
|
+
* TTR = unique words / total words.
|
|
73
|
+
* @param {string} text - Input text
|
|
74
|
+
* @returns {number} Type-token ratio between 0 and 1
|
|
75
|
+
*/
|
|
76
|
+
static vocabularyRichness(text) {
|
|
77
|
+
if (!text || text.length === 0) return 0;
|
|
78
|
+
|
|
79
|
+
const words = text.toLowerCase().match(/[a-z'-]+/g);
|
|
80
|
+
if (!words || words.length === 0) return 0;
|
|
81
|
+
|
|
82
|
+
const unique = new Set(words);
|
|
83
|
+
return unique.size / words.length;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Compute the average word length.
|
|
88
|
+
* @param {string} text - Input text
|
|
89
|
+
* @returns {number} Mean word length in characters
|
|
90
|
+
*/
|
|
91
|
+
static averageWordLength(text) {
|
|
92
|
+
if (!text || text.length === 0) return 0;
|
|
93
|
+
|
|
94
|
+
const words = text.match(/[a-zA-Z'-]+/g);
|
|
95
|
+
if (!words || words.length === 0) return 0;
|
|
96
|
+
|
|
97
|
+
let total = 0;
|
|
98
|
+
for (let i = 0; i < words.length; i++) {
|
|
99
|
+
total += words[i].length;
|
|
100
|
+
}
|
|
101
|
+
return total / words.length;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Compute sentence complexity as average words per sentence.
|
|
106
|
+
* @param {string} text - Input text
|
|
107
|
+
* @returns {number} Average number of words per sentence
|
|
108
|
+
*/
|
|
109
|
+
static sentenceComplexity(text) {
|
|
110
|
+
if (!text || text.length === 0) return 0;
|
|
111
|
+
|
|
112
|
+
const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 0);
|
|
113
|
+
if (sentences.length === 0) return 0;
|
|
114
|
+
|
|
115
|
+
let totalWords = 0;
|
|
116
|
+
for (let i = 0; i < sentences.length; i++) {
|
|
117
|
+
const words = sentences[i].trim().match(/\S+/g);
|
|
118
|
+
if (words) {
|
|
119
|
+
totalWords += words.length;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return totalWords / sentences.length;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// =========================================================================
|
|
127
|
+
// ENTROPY ANALYZER
|
|
128
|
+
// =========================================================================
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Detects prompt injection via Shannon entropy shifts across text segments.
|
|
132
|
+
* Injected instructions often have markedly different entropy than natural
|
|
133
|
+
* conversational text, creating detectable anomalies.
|
|
134
|
+
*/
|
|
135
|
+
class EntropyAnalyzer {
|
|
136
|
+
/**
|
|
137
|
+
* Create an EntropyAnalyzer.
|
|
138
|
+
* @param {Object} [options] - Configuration options
|
|
139
|
+
* @param {number} [options.threshold=0.3] - Entropy shift threshold for flagging anomalies
|
|
140
|
+
* @param {number} [options.windowSize=200] - Segment size in characters
|
|
141
|
+
*/
|
|
142
|
+
constructor(options = {}) {
|
|
143
|
+
this.threshold = options.threshold !== undefined ? options.threshold : 0.3;
|
|
144
|
+
this.windowSize = options.windowSize !== undefined ? options.windowSize : 200;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Analyze text for entropy-based anomalies.
|
|
149
|
+
* Splits text into segments, computes per-segment entropy, and flags
|
|
150
|
+
* segments that deviate significantly from the overall average.
|
|
151
|
+
* @param {string} text - Input text to analyze
|
|
152
|
+
* @returns {{entropy: number, segments: Array<{text: string, entropy: number, suspicious: boolean}>, anomalies: Array<{text: string, entropy: number, deviation: number, position: number}>}}
|
|
153
|
+
*/
|
|
154
|
+
analyze(text) {
|
|
155
|
+
if (!text || text.length === 0) {
|
|
156
|
+
return { entropy: 0, segments: [], anomalies: [] };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const overallEntropy = TextStatistics.charEntropy(text);
|
|
160
|
+
|
|
161
|
+
// Split text into segments of windowSize characters
|
|
162
|
+
const segments = [];
|
|
163
|
+
for (let i = 0; i < text.length; i += this.windowSize) {
|
|
164
|
+
const segText = text.slice(i, i + this.windowSize);
|
|
165
|
+
const segEntropy = TextStatistics.charEntropy(segText);
|
|
166
|
+
segments.push({
|
|
167
|
+
text: segText,
|
|
168
|
+
entropy: segEntropy,
|
|
169
|
+
suspicious: false
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Compute average segment entropy
|
|
174
|
+
let entropySum = 0;
|
|
175
|
+
for (let i = 0; i < segments.length; i++) {
|
|
176
|
+
entropySum += segments[i].entropy;
|
|
177
|
+
}
|
|
178
|
+
const avgEntropy = segments.length > 0 ? entropySum / segments.length : 0;
|
|
179
|
+
|
|
180
|
+
// Flag segments where entropy deviates beyond threshold
|
|
181
|
+
const anomalies = [];
|
|
182
|
+
for (let i = 0; i < segments.length; i++) {
|
|
183
|
+
const deviation = Math.abs(segments[i].entropy - avgEntropy);
|
|
184
|
+
if (deviation > this.threshold) {
|
|
185
|
+
segments[i].suspicious = true;
|
|
186
|
+
anomalies.push({
|
|
187
|
+
text: segments[i].text,
|
|
188
|
+
entropy: segments[i].entropy,
|
|
189
|
+
deviation: deviation,
|
|
190
|
+
position: i * this.windowSize
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (anomalies.length > 0) {
|
|
196
|
+
console.log('[Agent Shield] Entropy anomalies detected: ' + anomalies.length + ' segment(s) flagged');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
entropy: overallEntropy,
|
|
201
|
+
segments: segments,
|
|
202
|
+
anomalies: anomalies
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// =========================================================================
|
|
208
|
+
// PERPLEXITY ESTIMATOR
|
|
209
|
+
// =========================================================================
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Built-in English baseline corpus for bootstrapping the n-gram model.
|
|
213
|
+
* Common words and phrases that represent normal English text.
|
|
214
|
+
*/
|
|
215
|
+
const ENGLISH_BASELINE = [
|
|
216
|
+
'the quick brown fox jumps over the lazy dog',
|
|
217
|
+
'to be or not to be that is the question',
|
|
218
|
+
'I would like to help you with your request today',
|
|
219
|
+
'please let me know if you have any questions about this',
|
|
220
|
+
'thank you for your patience and understanding',
|
|
221
|
+
'the weather is nice today and I hope you are doing well',
|
|
222
|
+
'can you please provide more information about your issue',
|
|
223
|
+
'I am happy to assist you with anything you need',
|
|
224
|
+
'this is a common question and here is the answer',
|
|
225
|
+
'we appreciate your feedback and will work to improve',
|
|
226
|
+
'hello how are you doing today I hope everything is going well',
|
|
227
|
+
'the project is progressing smoothly and we are on schedule',
|
|
228
|
+
'please review the following document and provide your comments',
|
|
229
|
+
'I understand your concern and will look into this matter',
|
|
230
|
+
'the meeting has been scheduled for next week at the usual time'
|
|
231
|
+
];
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Estimates text perplexity using character n-gram frequency models.
|
|
235
|
+
* High perplexity relative to a trained baseline suggests the text
|
|
236
|
+
* deviates from normal patterns, indicating potential injection.
|
|
237
|
+
*/
|
|
238
|
+
class PerplexityEstimator {
|
|
239
|
+
/**
|
|
240
|
+
* Create a PerplexityEstimator.
|
|
241
|
+
* @param {Object} [options] - Configuration options
|
|
242
|
+
* @param {number} [options.ngramSize=3] - Character n-gram size
|
|
243
|
+
*/
|
|
244
|
+
constructor(options = {}) {
|
|
245
|
+
this.ngramSize = options.ngramSize !== undefined ? options.ngramSize : 3;
|
|
246
|
+
this.ngramCounts = {};
|
|
247
|
+
this.totalNgrams = 0;
|
|
248
|
+
this.trained = false;
|
|
249
|
+
this.baselinePerplexity = 0;
|
|
250
|
+
|
|
251
|
+
// Automatically train on the built-in English baseline
|
|
252
|
+
this.train(ENGLISH_BASELINE);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Build an n-gram frequency model from an array of normal texts.
|
|
257
|
+
* Subsequent calls to train() will add to the existing model.
|
|
258
|
+
* @param {string[]} corpusTexts - Array of normal/clean text samples
|
|
259
|
+
*/
|
|
260
|
+
train(corpusTexts) {
|
|
261
|
+
if (!corpusTexts || corpusTexts.length === 0) return;
|
|
262
|
+
|
|
263
|
+
for (let t = 0; t < corpusTexts.length; t++) {
|
|
264
|
+
const text = corpusTexts[t].toLowerCase();
|
|
265
|
+
const maxI = Math.max(-1, text.length - this.ngramSize);
|
|
266
|
+
for (let i = 0; i <= maxI; i++) {
|
|
267
|
+
const ngram = text.slice(i, i + this.ngramSize);
|
|
268
|
+
this.ngramCounts[ngram] = (this.ngramCounts[ngram] || 0) + 1;
|
|
269
|
+
this.totalNgrams++;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
this.trained = true;
|
|
274
|
+
|
|
275
|
+
// Compute baseline perplexity from the training corpus
|
|
276
|
+
let totalPerplexity = 0;
|
|
277
|
+
for (let t = 0; t < corpusTexts.length; t++) {
|
|
278
|
+
totalPerplexity += this._computePerplexity(corpusTexts[t]);
|
|
279
|
+
}
|
|
280
|
+
this.baselinePerplexity = totalPerplexity / corpusTexts.length;
|
|
281
|
+
|
|
282
|
+
console.log('[Agent Shield] Perplexity model trained: ' + this.totalNgrams + ' n-grams, baseline perplexity=' + this.baselinePerplexity.toFixed(2));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Compute raw perplexity for a given text using the trained model.
|
|
287
|
+
* @param {string} text - Input text
|
|
288
|
+
* @returns {number} Perplexity score
|
|
289
|
+
* @private
|
|
290
|
+
*/
|
|
291
|
+
_computePerplexity(text) {
|
|
292
|
+
if (!text || text.length < this.ngramSize) return 0;
|
|
293
|
+
|
|
294
|
+
const lowered = text.toLowerCase();
|
|
295
|
+
const ngramCount = lowered.length - this.ngramSize + 1;
|
|
296
|
+
if (ngramCount <= 0) return 0;
|
|
297
|
+
|
|
298
|
+
let logProbSum = 0;
|
|
299
|
+
const vocabSize = Object.keys(this.ngramCounts).length;
|
|
300
|
+
|
|
301
|
+
for (let i = 0; i <= lowered.length - this.ngramSize; i++) {
|
|
302
|
+
const ngram = lowered.slice(i, i + this.ngramSize);
|
|
303
|
+
// Laplace smoothing: add 1 to avoid zero probabilities
|
|
304
|
+
const count = (this.ngramCounts[ngram] || 0) + 1;
|
|
305
|
+
const prob = count / (this.totalNgrams + vocabSize);
|
|
306
|
+
logProbSum += Math.log2(prob);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const avgLogProb = logProbSum / ngramCount;
|
|
310
|
+
// Perplexity = 2^(-avg log2 probability)
|
|
311
|
+
return Math.pow(2, -avgLogProb);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Estimate whether text is suspicious based on its perplexity.
|
|
316
|
+
* @param {string} text - Input text to evaluate
|
|
317
|
+
* @returns {{perplexity: number, suspicious: boolean}}
|
|
318
|
+
*/
|
|
319
|
+
estimate(text) {
|
|
320
|
+
if (!this.trained) {
|
|
321
|
+
console.log('[Agent Shield] Perplexity estimator not trained, returning neutral result');
|
|
322
|
+
return { perplexity: 0, suspicious: false };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const perplexity = this._computePerplexity(text);
|
|
326
|
+
|
|
327
|
+
// Text is suspicious if its perplexity is significantly higher
|
|
328
|
+
// than the baseline (more than 2x the baseline)
|
|
329
|
+
const suspicious = perplexity > this.baselinePerplexity * 2;
|
|
330
|
+
|
|
331
|
+
if (suspicious) {
|
|
332
|
+
console.log('[Agent Shield] High perplexity detected: ' + perplexity.toFixed(2) + ' (baseline: ' + this.baselinePerplexity.toFixed(2) + ')');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
perplexity: perplexity,
|
|
337
|
+
suspicious: suspicious
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// =========================================================================
|
|
343
|
+
// BURST DETECTOR
|
|
344
|
+
// =========================================================================
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Detects sudden topic or style shifts in text using vocabulary overlap
|
|
348
|
+
* between sliding windows. Injection payloads often introduce a burst of
|
|
349
|
+
* new terminology that diverges sharply from surrounding content.
|
|
350
|
+
*/
|
|
351
|
+
class BurstDetector {
|
|
352
|
+
/**
|
|
353
|
+
* Create a BurstDetector.
|
|
354
|
+
* @param {Object} [options] - Configuration options
|
|
355
|
+
* @param {number} [options.sensitivity=0.5] - Detection sensitivity (0 to 1). Higher = more sensitive.
|
|
356
|
+
*/
|
|
357
|
+
constructor(options = {}) {
|
|
358
|
+
this.sensitivity = options.sensitivity !== undefined ? options.sensitivity : 0.5;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Extract words from text as a set for vocabulary comparison.
|
|
363
|
+
* @param {string} text - Input text
|
|
364
|
+
* @returns {Set<string>} Set of lowercase words
|
|
365
|
+
* @private
|
|
366
|
+
*/
|
|
367
|
+
_wordSet(text) {
|
|
368
|
+
const words = text.toLowerCase().match(/[a-z'-]+/g);
|
|
369
|
+
return new Set(words || []);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Compute Jaccard similarity between two sets.
|
|
374
|
+
* J(A, B) = |A ∩ B| / |A ∪ B|
|
|
375
|
+
* @param {Set<string>} setA
|
|
376
|
+
* @param {Set<string>} setB
|
|
377
|
+
* @returns {number} Similarity between 0 and 1
|
|
378
|
+
* @private
|
|
379
|
+
*/
|
|
380
|
+
_jaccardSimilarity(setA, setB) {
|
|
381
|
+
if (setA.size === 0 && setB.size === 0) return 1;
|
|
382
|
+
|
|
383
|
+
let intersection = 0;
|
|
384
|
+
for (const item of setA) {
|
|
385
|
+
if (setB.has(item)) intersection++;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const union = setA.size + setB.size - intersection;
|
|
389
|
+
return union === 0 ? 1 : intersection / union;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Analyze text for vocabulary bursts indicating style/topic shifts.
|
|
394
|
+
* Uses sliding windows with vocabulary overlap measurement.
|
|
395
|
+
* @param {string} text - Input text to analyze
|
|
396
|
+
* @returns {{bursts: Array<{position: number, before: string, after: string, score: number}>, suspicious: boolean}}
|
|
397
|
+
*/
|
|
398
|
+
analyze(text) {
|
|
399
|
+
if (!text || text.length === 0) {
|
|
400
|
+
return { bursts: [], suspicious: false };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Split into sentences for natural boundary detection
|
|
404
|
+
const sentences = text.split(/(?<=[.!?\n])\s+/).filter(s => s.trim().length > 0);
|
|
405
|
+
|
|
406
|
+
if (sentences.length < 2) {
|
|
407
|
+
return { bursts: [], suspicious: false };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Use a sliding window of sentences to detect vocabulary shifts
|
|
411
|
+
const windowSize = Math.max(1, Math.floor(sentences.length / 4));
|
|
412
|
+
const bursts = [];
|
|
413
|
+
// Threshold: lower sensitivity means we need a bigger vocabulary gap
|
|
414
|
+
const threshold = 1 - this.sensitivity;
|
|
415
|
+
|
|
416
|
+
for (let i = windowSize; i < sentences.length; i++) {
|
|
417
|
+
const beforeText = sentences.slice(Math.max(0, i - windowSize), i).join(' ');
|
|
418
|
+
const afterText = sentences.slice(i, Math.min(sentences.length, i + windowSize)).join(' ');
|
|
419
|
+
|
|
420
|
+
const beforeWords = this._wordSet(beforeText);
|
|
421
|
+
const afterWords = this._wordSet(afterText);
|
|
422
|
+
|
|
423
|
+
// Skip if either window has too few words
|
|
424
|
+
if (beforeWords.size < 3 || afterWords.size < 3) continue;
|
|
425
|
+
|
|
426
|
+
const similarity = this._jaccardSimilarity(beforeWords, afterWords);
|
|
427
|
+
const burstScore = 1 - similarity;
|
|
428
|
+
|
|
429
|
+
if (burstScore > threshold) {
|
|
430
|
+
// Compute character position
|
|
431
|
+
let position = 0;
|
|
432
|
+
for (let j = 0; j < i; j++) {
|
|
433
|
+
position += sentences[j].length + 1;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
bursts.push({
|
|
437
|
+
position: position,
|
|
438
|
+
before: beforeText.slice(-100),
|
|
439
|
+
after: afterText.slice(0, 100),
|
|
440
|
+
score: burstScore
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const suspicious = bursts.length > 0;
|
|
446
|
+
|
|
447
|
+
if (suspicious) {
|
|
448
|
+
console.log('[Agent Shield] Vocabulary burst detected: ' + bursts.length + ' transition(s) flagged');
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
bursts: bursts,
|
|
453
|
+
suspicious: suspicious
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// =========================================================================
|
|
459
|
+
// EXPORTS
|
|
460
|
+
// =========================================================================
|
|
461
|
+
|
|
462
|
+
module.exports = {
|
|
463
|
+
EntropyAnalyzer,
|
|
464
|
+
PerplexityEstimator,
|
|
465
|
+
BurstDetector,
|
|
466
|
+
TextStatistics
|
|
467
|
+
};
|