@privacyscrubber/mcp-server 2.3.9 → 2.4.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/.well-known/mcp/server-card.json +78 -0
- package/LICENSE +23 -0
- package/README.md +50 -11
- package/index.js +868 -66
- package/manifest.json +84 -8
- package/package.json +3 -2
- package/pii-masking-run.js +43 -16
- package/ps-license-manager.cjs +1 -1
- package/ps-license-manager.js +11 -257
- package/ps-pii-engine.cjs +8 -3254
- package/ps-pii-engine.js +8 -3254
- package/scrubber-core.cjs +8 -1795
- package/server.json +26 -3
package/index.js
CHANGED
|
@@ -26,6 +26,7 @@ import { fileURLToPath } from 'url';
|
|
|
26
26
|
import crypto from 'crypto';
|
|
27
27
|
import os from 'os';
|
|
28
28
|
import { execSync } from 'child_process';
|
|
29
|
+
import { Transform } from 'stream';
|
|
29
30
|
|
|
30
31
|
const require = createRequire(import.meta.url);
|
|
31
32
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -63,6 +64,54 @@ if (!LicenseManager || typeof LicenseManager.validate !== 'function') {
|
|
|
63
64
|
// Initialize core engine
|
|
64
65
|
PrivacyScrubberCore.init();
|
|
65
66
|
|
|
67
|
+
let piiEngine;
|
|
68
|
+
try {
|
|
69
|
+
piiEngine = require('./ps-pii-engine.cjs');
|
|
70
|
+
} catch (e) {
|
|
71
|
+
try {
|
|
72
|
+
piiEngine = require('./ps-pii-engine.js');
|
|
73
|
+
} catch (err) {}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const CANONICAL_INDUSTRY_PROFILES = [
|
|
77
|
+
'General', 'Dev', 'Medical', 'Pharma', 'Biotech', 'Telecom', 'Legal', 'Compliance', 'CCPA',
|
|
78
|
+
'Finance', 'Bizops', 'Sales', 'WealthMgmt', 'Insurance', 'Accounting', 'Underwriting',
|
|
79
|
+
'Automotive', 'Energy', 'Hospitality', 'HR', 'Security', 'Marketing', 'Support',
|
|
80
|
+
'RealEstate', 'Agents', 'Academic', 'Creative', 'Tech', 'Personal', 'Engineering'
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
export function resolveRequestedProfile(args, defaultProfile = 'General') {
|
|
84
|
+
if (!args || typeof args !== 'object') return defaultProfile;
|
|
85
|
+
const sec = typeof args.sector === 'string' ? args.sector.trim() : '';
|
|
86
|
+
const prof = typeof args.profile === 'string' ? args.profile.trim() : '';
|
|
87
|
+
|
|
88
|
+
let candidate = defaultProfile;
|
|
89
|
+
if (sec && (!prof || prof.toLowerCase() === 'general')) {
|
|
90
|
+
candidate = sec;
|
|
91
|
+
} else if (prof) {
|
|
92
|
+
candidate = prof;
|
|
93
|
+
} else if (sec) {
|
|
94
|
+
candidate = sec;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Canonical normalization via PROFILE_ALIAS_MAP if available
|
|
98
|
+
if (candidate && candidate.toLowerCase() !== 'general' && piiEngine && piiEngine.PROFILE_ALIAS_MAP) {
|
|
99
|
+
const mapped = piiEngine.PROFILE_ALIAS_MAP[candidate.toLowerCase()];
|
|
100
|
+
if (mapped) {
|
|
101
|
+
if (candidate.toLowerCase() === mapped.toLowerCase()) {
|
|
102
|
+
return candidate;
|
|
103
|
+
}
|
|
104
|
+
return mapped.charAt(0).toUpperCase() + mapped.slice(1);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return candidate;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const PROFILE_PARAM_DESCRIPTION = "The detection profile to use. Available: 'General' (Free, 15,000 chars), or 30 specialized industry profiles (5,000 chars Free trial quota; unlimited on PRO/TEAMS/SDK): 'Dev' (Engineering/Code), 'Medical', 'Pharma', 'Biotech', 'Telecom', 'Legal', 'Compliance', 'CCPA', 'Finance', 'Bizops', 'Sales', 'WealthMgmt', 'Insurance', 'Accounting', 'Underwriting', 'Automotive', 'Energy', 'Hospitality', 'HR', 'Security', 'Marketing', 'Support', 'RealEstate', 'Agents', 'Academic', 'Creative', 'Tech', 'Personal', 'Engineering'. Defaults to 'General'.";
|
|
112
|
+
|
|
113
|
+
const SECTOR_PARAM_DESCRIPTION = "Optional alias for 'profile'. The industry sector or compliance vertical to apply (e.g. 'medical', 'finance', 'legal', 'dev', 'hr', 'accounting', 'security', 'compliance', 'underwriting', 'telecom').";
|
|
114
|
+
|
|
66
115
|
// Volatile in-memory token map per session (multi-task agent isolation)
|
|
67
116
|
const agentSessions = new Map();
|
|
68
117
|
const MAX_CONCURRENT_SESSIONS = 200;
|
|
@@ -75,9 +124,13 @@ function getSession(sessionId = 'default') {
|
|
|
75
124
|
if (!session) {
|
|
76
125
|
if (agentSessions.size >= MAX_CONCURRENT_SESSIONS) {
|
|
77
126
|
const oldestKey = agentSessions.keys().next().value;
|
|
127
|
+
const oldestSession = agentSessions.get(oldestKey);
|
|
128
|
+
if (oldestSession && oldestSession.streamDetokenizer) {
|
|
129
|
+
oldestSession.streamDetokenizer.reset();
|
|
130
|
+
}
|
|
78
131
|
agentSessions.delete(oldestKey);
|
|
79
132
|
}
|
|
80
|
-
session = { map: {}, ignoreList: new Set(), createdAt: now, lastActive: now };
|
|
133
|
+
session = { map: {}, ignoreList: new Set(), streamDetokenizer: null, createdAt: now, lastActive: now };
|
|
81
134
|
agentSessions.set(sid, session);
|
|
82
135
|
}
|
|
83
136
|
session.lastActive = now;
|
|
@@ -96,6 +149,282 @@ function getSessionIgnoreList(sessionId = 'default') {
|
|
|
96
149
|
const sessionMap = getSessionMap('default');
|
|
97
150
|
const sessionIgnoreList = getSessionIgnoreList('default');
|
|
98
151
|
|
|
152
|
+
function escapeRegex(s) {
|
|
153
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Builds a fast streaming token replacement function for MCP.
|
|
158
|
+
* Preserves all whitespace, indentation, newlines, and code blocks.
|
|
159
|
+
* Handles descending key ordering, case-insensitivity, alternative delimiters (<...>, {{...}}, __...__),
|
|
160
|
+
* and possessive suffixes ('s).
|
|
161
|
+
*/
|
|
162
|
+
export function buildStreamReplacer(tokenMap = {}, options = {}) {
|
|
163
|
+
if (!tokenMap || typeof tokenMap !== 'object' || Object.keys(tokenMap).length === 0) {
|
|
164
|
+
return (text) => text || '';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const onTokenRestored = typeof options?.onTokenRestored === 'function' ? options.onTokenRestored : null;
|
|
168
|
+
const lookup = new Map();
|
|
169
|
+
const patterns = [];
|
|
170
|
+
const sortedKeys = Object.keys(tokenMap).sort((a, b) => b.length - a.length);
|
|
171
|
+
|
|
172
|
+
for (const k of sortedKeys) {
|
|
173
|
+
const val = tokenMap[k];
|
|
174
|
+
const inner = k.replace(/^(?:\[|<|\{\{?|__|【|[|〔|「|『)|(?:\]|>|\}\}?|__|】|]|〕|」|』)$/g, '');
|
|
175
|
+
const match = inner.match(/^([A-Za-z_0-9]+?)[-_ ]?(\d+)$/);
|
|
176
|
+
|
|
177
|
+
if (match) {
|
|
178
|
+
const label = escapeRegex(match[1]);
|
|
179
|
+
const idx = match[2];
|
|
180
|
+
const corePat = '[*`_~\\s]*' + label + '[*`_~]*[-_\\s]*0*' + idx + '[*`_~.,;:!?\\s]*';
|
|
181
|
+
const patternStr = '(?:\\[' + corePat + '\\]|<' + corePat + '>|\\{\\{' + corePat + '\\}\\}|\\{' + corePat + '\\}|__' + corePat + '__|[【[〔「『]' + corePat + '[】]〕」』]|%5B' + corePat + '%5D|&#(?:91|091|x5[bB]);' + corePat + '&#(?:93|093|x5[dD]);|&(?:lbrack|lsqb);' + corePat + '&(?:rbrack|rsqb);|\\\\u005[bB]' + corePat + '\\\\u005[dD]|\\b' + label + '[-_\\s]+0*' + idx + '\\b)';
|
|
182
|
+
patterns.push(patternStr);
|
|
183
|
+
const normKey = (match[1] + '_' + parseInt(idx, 10)).toUpperCase();
|
|
184
|
+
lookup.set(normKey, val);
|
|
185
|
+
lookup.set(k, val);
|
|
186
|
+
lookup.set(k.toUpperCase(), val);
|
|
187
|
+
} else {
|
|
188
|
+
const safe = escapeRegex(k);
|
|
189
|
+
patterns.push(safe);
|
|
190
|
+
lookup.set(k, val);
|
|
191
|
+
lookup.set(k.toUpperCase(), val);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const unifiedRegex = new RegExp('(?:' + patterns.join('|') + ')(\'s|’s|s)?', 'gi');
|
|
196
|
+
|
|
197
|
+
return function replaceTokens(text) {
|
|
198
|
+
if (!text) return '';
|
|
199
|
+
unifiedRegex.lastIndex = 0;
|
|
200
|
+
return text.replace(unifiedRegex, (match, suffix) => {
|
|
201
|
+
const isPossessive = suffix && /('s|’s)$/i.test(suffix);
|
|
202
|
+
const coreMatch = suffix ? match.slice(0, match.length - suffix.length) : match;
|
|
203
|
+
let restored = null;
|
|
204
|
+
|
|
205
|
+
if (lookup.has(coreMatch)) {
|
|
206
|
+
restored = lookup.get(coreMatch);
|
|
207
|
+
} else if (lookup.has(coreMatch.toUpperCase())) {
|
|
208
|
+
restored = lookup.get(coreMatch.toUpperCase());
|
|
209
|
+
} else {
|
|
210
|
+
const inner = coreMatch
|
|
211
|
+
.replace(/^(?:%5B|&#(?:91|091|x5[bB]);|&(?:lbrack|lsqb);|\\u005[bB]|\[|<|\{\{?|__|【|[|〔|「|『)/i, '')
|
|
212
|
+
.replace(/(?:%5D|&#(?:93|093|x5[dD]);|&(?:rbrack|rsqb);|\\u005[dD]|\]|>|\}\}?|__|】|]|〕|」|』)$/i, '')
|
|
213
|
+
.replace(/[*`_~.,;:!?\s]+/g, ' ')
|
|
214
|
+
.trim();
|
|
215
|
+
const m = inner.match(/^([A-Za-z_0-9]+?)[-_ ]?(\d+)$/);
|
|
216
|
+
if (m) {
|
|
217
|
+
const normKey = (m[1] + '_' + parseInt(m[2], 10)).toUpperCase();
|
|
218
|
+
if (lookup.has(normKey)) {
|
|
219
|
+
restored = lookup.get(normKey);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (restored !== null) {
|
|
225
|
+
if (onTokenRestored) {
|
|
226
|
+
try { onTokenRestored({ token: match, originalValue: restored }); } catch (_) {}
|
|
227
|
+
}
|
|
228
|
+
return restored + (isPossessive ? "'s" : '');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (lookup.has(match)) {
|
|
232
|
+
restored = lookup.get(match);
|
|
233
|
+
if (onTokenRestored) {
|
|
234
|
+
try { onTokenRestored({ token: match, originalValue: restored }); } catch (_) {}
|
|
235
|
+
}
|
|
236
|
+
return restored;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return match;
|
|
240
|
+
});
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Streaming Token Detokenizer with sliding window buffer for LLM chunk boundary splits.
|
|
246
|
+
* Buffers partial token candidates at chunk boundaries across stdio and Server-Sent Events,
|
|
247
|
+
* preventing character loss and token corruption.
|
|
248
|
+
*/
|
|
249
|
+
export class StreamingDetokenizer {
|
|
250
|
+
constructor(tokenMap = {}, options = {}) {
|
|
251
|
+
this.tokenMap = tokenMap && typeof tokenMap === 'object' ? tokenMap : {};
|
|
252
|
+
this.options = options || {};
|
|
253
|
+
this.replacer = buildStreamReplacer(this.tokenMap, this.options);
|
|
254
|
+
this.pendingBuffer = '';
|
|
255
|
+
this.hasTokens = Object.keys(this.tokenMap).length > 0;
|
|
256
|
+
this._lastTokenCount = Object.keys(this.tokenMap).length;
|
|
257
|
+
this.decoder = new TextDecoder('utf-8', { fatal: false });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
get buffer() {
|
|
261
|
+
return this.pendingBuffer;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
set buffer(val) {
|
|
265
|
+
this.pendingBuffer = typeof val === 'string' ? val : '';
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
updateTokenMap(newMap) {
|
|
269
|
+
if (newMap && typeof newMap === 'object') {
|
|
270
|
+
this.tokenMap = newMap;
|
|
271
|
+
this.replacer = buildStreamReplacer(this.tokenMap, this.options);
|
|
272
|
+
this.hasTokens = Object.keys(this.tokenMap).length > 0;
|
|
273
|
+
this._lastTokenCount = Object.keys(this.tokenMap).length;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
reset() {
|
|
278
|
+
this.pendingBuffer = '';
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
feed(chunk) {
|
|
282
|
+
if (chunk === undefined || chunk === null) return '';
|
|
283
|
+
let text = '';
|
|
284
|
+
if (typeof chunk === 'string') {
|
|
285
|
+
text = chunk;
|
|
286
|
+
} else if (chunk instanceof Uint8Array || (typeof Buffer !== 'undefined' && Buffer.isBuffer(chunk))) {
|
|
287
|
+
text = this.decoder.decode(chunk, { stream: true });
|
|
288
|
+
} else {
|
|
289
|
+
text = String(chunk);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!text) return '';
|
|
293
|
+
|
|
294
|
+
const currentTokenCount = Object.keys(this.tokenMap).length;
|
|
295
|
+
if (currentTokenCount !== this._lastTokenCount) {
|
|
296
|
+
this.replacer = buildStreamReplacer(this.tokenMap, this.options);
|
|
297
|
+
this.hasTokens = currentTokenCount > 0;
|
|
298
|
+
this._lastTokenCount = currentTokenCount;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!this.hasTokens) return text;
|
|
302
|
+
|
|
303
|
+
this.pendingBuffer += text;
|
|
304
|
+
|
|
305
|
+
const tailLen = 160;
|
|
306
|
+
const tail = this.pendingBuffer.length > tailLen ? this.pendingBuffer.slice(-tailLen) : this.pendingBuffer;
|
|
307
|
+
const offset = this.pendingBuffer.length > tailLen ? this.pendingBuffer.length - tailLen : 0;
|
|
308
|
+
const m = tail.match(/(?:(?:\[|【|[|〔|「|『|<|\{\{?|__?|%5[Bb]|\\u005[Bb]|&(?:amp;)?(?:#0*91;|#[xX]0*5[Bb];|lbrack;))[A-Za-z0-9_\-А-Яа-яЁё *`~.\\]{0,128}(?:%5?|&(?:a(?:m(?:p(?:;)?)?)?)?(?:#(?:0*9?3?|[xX]0*5?[Dd]?)?|r(?:b(?:r(?:a(?:c(?:k)?)?)?)?)?)?)?|%[0-9A-Fa-f]?|\\(?:u(?:0(?:0(?:5)?)?)?)?|&(?:a(?:m(?:p(?:;)?)?)?)?(?:#(?:0*9?1?|[xX]0*5?)?|l(?:b(?:r(?:a(?:c(?:k)?)?)?)?)?)?)$/);
|
|
309
|
+
|
|
310
|
+
if (m) {
|
|
311
|
+
const candidate = m[0];
|
|
312
|
+
const isClosedUnderscoreOrUnicode = (candidate.length >= 4 && candidate.startsWith('__') && candidate.endsWith('__')) || /\\u005[Dd]/.test(candidate);
|
|
313
|
+
if (isClosedUnderscoreOrUnicode && this.replacer(candidate) !== candidate) {
|
|
314
|
+
const toEmit = this.pendingBuffer;
|
|
315
|
+
this.pendingBuffer = '';
|
|
316
|
+
return this.replacer(toEmit);
|
|
317
|
+
}
|
|
318
|
+
const splitIdx = offset + m.index;
|
|
319
|
+
const safeToEmit = this.pendingBuffer.slice(0, splitIdx);
|
|
320
|
+
this.pendingBuffer = this.pendingBuffer.slice(splitIdx);
|
|
321
|
+
return safeToEmit ? this.replacer(safeToEmit) : '';
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const toEmit = this.pendingBuffer;
|
|
325
|
+
this.pendingBuffer = '';
|
|
326
|
+
return this.replacer(toEmit);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
process(chunk) {
|
|
330
|
+
return this.feed(chunk);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
flush() {
|
|
334
|
+
let text = '';
|
|
335
|
+
try {
|
|
336
|
+
text = this.decoder.decode(new Uint8Array(), { stream: false });
|
|
337
|
+
} catch (_) {}
|
|
338
|
+
|
|
339
|
+
if (text) {
|
|
340
|
+
this.pendingBuffer += text;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (!this.pendingBuffer) return '';
|
|
344
|
+
const remaining = this.pendingBuffer;
|
|
345
|
+
this.pendingBuffer = '';
|
|
346
|
+
return this.replacer(remaining);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
toNodeTransform() {
|
|
350
|
+
const self = this;
|
|
351
|
+
const transformStream = new Transform({
|
|
352
|
+
transform(chunk, encoding, callback) {
|
|
353
|
+
try {
|
|
354
|
+
const out = self.feed(chunk);
|
|
355
|
+
if (out) this.push(out);
|
|
356
|
+
callback();
|
|
357
|
+
} catch (err) {
|
|
358
|
+
callback(err);
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
flush(callback) {
|
|
362
|
+
try {
|
|
363
|
+
const out = self.flush();
|
|
364
|
+
if (out) this.push(out);
|
|
365
|
+
callback();
|
|
366
|
+
} catch (err) {
|
|
367
|
+
callback(err);
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
destroy(err, callback) {
|
|
371
|
+
self.reset();
|
|
372
|
+
callback(err);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
transformStream.detokenizer = self;
|
|
376
|
+
return transformStream;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async* wrapStream(asyncIterable) {
|
|
380
|
+
for await (const chunk of asyncIterable) {
|
|
381
|
+
if (chunk === null || chunk === undefined) continue;
|
|
382
|
+
if (chunk.choices && Array.isArray(chunk.choices) && chunk.choices[0]?.delta?.content !== undefined) {
|
|
383
|
+
const delta = chunk.choices[0].delta.content;
|
|
384
|
+
if (typeof delta === 'string') {
|
|
385
|
+
const restored = this.feed(delta);
|
|
386
|
+
if (restored) {
|
|
387
|
+
chunk.choices[0].delta.content = restored;
|
|
388
|
+
yield chunk;
|
|
389
|
+
}
|
|
390
|
+
} else {
|
|
391
|
+
yield chunk;
|
|
392
|
+
}
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
let text = '';
|
|
397
|
+
if (typeof chunk === 'string') {
|
|
398
|
+
text = chunk;
|
|
399
|
+
} else if (typeof chunk?.text === 'string') {
|
|
400
|
+
text = chunk.text;
|
|
401
|
+
} else if (typeof chunk?.content === 'string') {
|
|
402
|
+
text = chunk.content;
|
|
403
|
+
} else if (chunk instanceof Uint8Array || (typeof Buffer !== 'undefined' && Buffer.isBuffer(chunk))) {
|
|
404
|
+
text = chunk;
|
|
405
|
+
} else {
|
|
406
|
+
text = String(chunk);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const emitted = this.feed(text);
|
|
410
|
+
if (emitted) yield emitted;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const finalChunk = this.flush();
|
|
414
|
+
if (finalChunk) yield finalChunk;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export function getStreamDetokenizer(sessionId = 'default') {
|
|
419
|
+
const session = getSession(sessionId);
|
|
420
|
+
if (!session.streamDetokenizer) {
|
|
421
|
+
session.streamDetokenizer = new StreamingDetokenizer(session.map);
|
|
422
|
+
} else {
|
|
423
|
+
session.streamDetokenizer.updateTokenMap(session.map);
|
|
424
|
+
}
|
|
425
|
+
return session.streamDetokenizer;
|
|
426
|
+
}
|
|
427
|
+
|
|
99
428
|
function reverseTokens(text, map) {
|
|
100
429
|
if (!text || typeof text !== 'string' || !map || Object.keys(map).length === 0) return text;
|
|
101
430
|
if (PrivacyScrubberCore && typeof PrivacyScrubberCore.unscrubText === 'function') {
|
|
@@ -130,16 +459,33 @@ function mcpLog(msg) {
|
|
|
130
459
|
|
|
131
460
|
// Secrets detection patterns — defined once here, shared by detectSecrets() and performSanitization()
|
|
132
461
|
const DEVOPS_SECRETS_DETECTOR = [
|
|
133
|
-
{ name: 'AWS Credentials', regex: /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA)[A-Z0-9]{
|
|
462
|
+
{ name: 'AWS Credentials', regex: /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA)[A-Z0-9]{12,28}\b/g },
|
|
463
|
+
{ name: 'Google Cloud / Gemini API Key', regex: /\bAIza[0-9A-Za-z_-]{35}\b/g },
|
|
464
|
+
{ name: 'Anthropic API Key', regex: /\bsk-ant-(?:api\d{2}|admin\d{2}|sid\d{2})-[a-zA-Z0-9_-]{16,128}\b/g },
|
|
465
|
+
{ name: 'HuggingFace / Perplexity API Key', regex: /\b(?:hf_[a-zA-Z0-9]{20,64}|pplx-[a-zA-Z0-9]{24,64})\b/g },
|
|
466
|
+
{ name: 'SendGrid API Key', regex: /\bSG\.[a-zA-Z0-9_-]{16,32}\.[a-zA-Z0-9_-]{32,64}\b/g },
|
|
467
|
+
{ name: 'Postman API Key', regex: /\bPMAK-[a-fA-F0-9]{24}-[a-fA-F0-9]{34}\b/g },
|
|
468
|
+
{ name: 'GitHub Fine-Grained PAT', regex: /\bgithub_pat_[a-zA-Z0-9]{20,30}_[a-zA-Z0-9]{40,80}\b/g },
|
|
469
|
+
{ name: 'Slack Enterprise / Refresh Token', regex: /\bxoxe\.xox[baprs]-\d-[a-zA-Z0-9-]{10,}\b|\bxox[eapbrs]-[a-zA-Z0-9-]{10,}\b/g },
|
|
470
|
+
{ name: 'Telegram Bot API Token', regex: /\b\d{8,10}:AA[A-Za-z0-9_-]{33}\b/g },
|
|
471
|
+
{ name: 'Age Encryption Secret Key', regex: /\bAGE-SECRET-KEY-1[A-Z0-9]{58}\b/g },
|
|
472
|
+
{ name: 'Modern Cloud & AI Platform Key', regex: /\b(?:sbp_[a-fA-F0-9]{40}|dop_v1_[a-fA-F0-9]{64}|doo_v1_[a-fA-F0-9]{64}|dp\.(?:st|pt|ct|sa)\.[a-zA-Z0-9_-]{20,}|lin_api_[a-zA-Z0-9]{32,64}|ntn_[a-zA-Z0-9]{32,64}|secret_[a-zA-Z0-9]{32,64}|gsk_[a-zA-Z0-9]{32,64}|r8_[a-zA-Z0-9]{24,64}|tvly-[a-zA-Z0-9_-]{20,64}|fc-[a-fA-F0-9]{32}|dapi[a-fA-F0-9]{32}(?:-\d)?|re_[a-zA-Z0-9_]{20,64}|napi_[a-zA-Z0-9]{32,64}|[a-zA-Z0-9]{14}\.atlasv1\.[a-zA-Z0-9]{50,80}|pypi-[A-Za-z0-9_-]{16,128}|whsec_[A-Za-z0-9+/=_-]{24,128}|shpat_[a-fA-F0-9]{32}|shpss_[a-fA-F0-9]{32}|shpca_[a-fA-F0-9]{32}|shppa_[a-fA-F0-9]{32}|hvs\.[A-Za-z0-9_-]{24,128}|hvb\.[A-Za-z0-9_-]{24,128}|glsa_[A-Za-z0-9]{32}_[A-Fa-f0-9]{8}|glc_[A-Za-z0-9+/=_-]{20,128}|pscale_tkn_[A-Za-z0-9_.-]{20,64}|pscale_pw_[A-Za-z0-9_.-]{20,64}|pat[A-Za-z0-9]{14}\.[a-fA-F0-9]{64}|pat-(?:na1|eu1)-[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}|sq0atp-[A-Za-z0-9_-]{22,64}|sq0csp-[A-Za-z0-9_-]{32,64}|NRAK-[A-Z0-9]{27}|v1\.0-[a-fA-F0-9]{24}-[a-fA-F0-9]{16,160}|ATATT3xFfGF0[A-Za-z0-9+/=_-]{24,128}|tskey-(?:auth|api|client)-[A-Za-z0-9_-]{20,96}|sntry[su]_[A-Za-z0-9+/=_-]{32,160}|(?:pk|sk)\.eyJ1[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{16,}|key-[a-f0-9]{32}|[a-f0-9]{32}-us\d{1,2})\b/g },
|
|
473
|
+
{ name: 'Twilio Account SID / API Key', regex: /\b(?:AC|SK)[a-fA-F0-9]{32}\b/g },
|
|
474
|
+
{ name: 'Bcrypt / Argon2 Password Hash', regex: /(?<![A-Za-z0-9])(?:\$2[aby]\$\d{2}\$[./A-Za-z0-9=]{20,60}|\$argon2(?:id|i|d)\$[^\s"']{15,120})/g },
|
|
134
475
|
{ name: 'JSON Web Token (JWT)', regex: /\beyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b/g },
|
|
135
|
-
{ name: 'API Token/Key (GitHub/Slack/NPM)', regex: /\b(?:ghp|gho|ghu|ghs|ghr|glpat|npm|xox[baprs])[-_][A-Za-z0-9_]{10,}\b/g },
|
|
136
|
-
{ name: 'Stripe API Key', regex: /\b(?:[rs]k)_(?:test|live)_[a-zA-Z0-9]{
|
|
476
|
+
{ name: 'API Token/Key (GitHub/Slack/NPM)', regex: /\b(?:ghp|gho|ghu|ghs|ghr|glpat|npm|xox[baprs])[-_][A-Za-z0-9_-]{10,}\b/g },
|
|
477
|
+
{ name: 'Stripe API Key', regex: /\b(?:[rs]k)_(?:test|live)_[a-zA-Z0-9]{10,}\b/g },
|
|
137
478
|
{ name: 'OpenAI Project API Key', regex: /\b(?:sk|pk)-(?:proj-)?[a-zA-Z0-9_-]{16,}\b/gi },
|
|
479
|
+
{ name: 'Slack / Discord Incoming Webhook URL', regex: /\bhttps:\/\/(?:hooks\.slack\.com\/services\/T[A-Z0-9_]+\/B[A-Z0-9_]+\/[A-Za-z0-9_-]+|(?:ptb\.|canary\.)?discord(?:app)?\.com\/api\/webhooks\/\d{17,20}\/[A-Za-z0-9_-]{60,70})\b/gi },
|
|
480
|
+
{ name: 'Azure Connection String / SAS Token', regex: /(?:DefaultEndpointsProtocol=https?;AccountName=[^;\s"']+;AccountKey=[A-Za-z0-9+/=]{20,}(?:;EndpointSuffix=[^;\s"']+)?|Endpoint=sb:\/\/[^;\s"']+;SharedAccessKeyName=[^;\s"']+;SharedAccessKey=[A-Za-z0-9+/=]{16,}|\bSharedAccessSignature=sr=[^\s"']+&sig=[A-Za-z0-9%/+=]{16,}|\bAccountKey=[A-Za-z0-9+/=]{40,88}==)/gi },
|
|
138
481
|
{ name: 'Database Connection URI', regex: /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp|mssql):\/\/[^\s"']+/gi },
|
|
139
|
-
{ name: 'Embedded Credentials URI', regex: /\b(?:https?|s?ftp):\/\/[a-zA-Z0-9_.%+-]+:[^\s/@:"']+@[a-zA-Z0-9_.-]+(?::\d+)?(?:\/[^\s"']*)?/gi },
|
|
482
|
+
{ name: 'Embedded Credentials URI', regex: /\b(?:https?|hxxps?|s?ftp|fxp|wss?):\/\/[a-zA-Z0-9_.%+-]+:[^\s/@:"']+@[a-zA-Z0-9_.-]+(?:\[\.\][a-zA-Z0-9_.-]+|\(\.\)[a-zA-Z0-9_.-]+)*(?::\d+)?(?:\/[^\s"']*)?/gi },
|
|
140
483
|
{ name: 'URL Query Parameter Secret', regex: /(?<=[?&](?:api_key|apikey|access_token|client_secret|auth_token|token|secret|password|private_key)=)[^\s"'\x26#)>]{8,}/gi },
|
|
484
|
+
{ name: 'CLI Credential Flag / Curl User Auth', regex: /(?:\bcurl\b[^\r\n]*?\s(?:-u|--user)\s+["']?[A-Za-z0-9_.+-]+:[^\s"']+|\b(?:--password|--api-key|--apikey|--access-token|--auth-token|--client-secret|--secret-key|--private-key)(?:=|\s+)["']?[^\s"']{6,}["']?)/gi },
|
|
485
|
+
{ name: 'HTTP API Key Header', regex: /\b(?:X-API-Key|X-Amz-Security-Token|X-Functions-Key|X-Shopify-Access-Token|X-Auth-Token|Api-Key)\s*:\s*["']?[A-Za-z0-9_.\-~+/=]{12,}["']?/gi },
|
|
486
|
+
{ name: 'Docker Registry Auth / K8s Secret Base64', regex: /(?<=["']?(?:auth|identitytoken|registrytoken|\.dockerconfigjson)["']?\s*:\s*["'])[A-Za-z0-9+/=]{16,}(?=["'])/gi },
|
|
141
487
|
{ name: 'Bearer / Auth Header Token', regex: /(?<=\b(?:Bearer|Token)\s+)[a-zA-Z0-9_\-\.~+/=]{16,}\b/gi },
|
|
142
|
-
{ name: 'Database/API Secret', regex: /\b(DB|POSTGRES|REDIS|MYSQL|AWS|SECRET|PASSWORD|TOKEN|API|KEY)[A-Z0-9_]*\s*[:=]\s*[^ \t\r\n"']{8,}
|
|
488
|
+
{ name: 'Database/API Secret', regex: /\b(DB|POSTGRES|REDIS|MYSQL|AWS|SECRET|PASSWORD|TOKEN|API|KEY)[A-Z0-9_]*\s*[:=]\s*[^ \t\r\n"']{8,}/gi },
|
|
143
489
|
{ name: 'Private Cryptographic Key (PEM Block)', regex: /-----BEGIN (?:RSA |EC |PGP |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----[\s\S]{10,8192}?-----END (?:RSA |EC |PGP |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----/g },
|
|
144
490
|
{ name: 'Private Cryptographic Key (Header)', regex: /-----BEGIN (?:RSA |EC |PGP |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----/g }
|
|
145
491
|
];
|
|
@@ -151,7 +497,9 @@ function checkLicenseStatus() {
|
|
|
151
497
|
const result = LicenseManager.validate(key);
|
|
152
498
|
|
|
153
499
|
if (!result.valid) {
|
|
154
|
-
|
|
500
|
+
if (process.env.DEBUG_PRIVACYSCRUBBER) {
|
|
501
|
+
mcpLog(`[License] Key validation notice: ${result.reason || "Unknown"}`);
|
|
502
|
+
}
|
|
155
503
|
return { isPro: false, type: null, error: result.reason || "Invalid license format or signature." };
|
|
156
504
|
}
|
|
157
505
|
|
|
@@ -264,8 +612,8 @@ function buildCisoAuditTelemetry(currentTokenMap = {}) {
|
|
|
264
612
|
frameworksSet.add('CCPA/CPRA');
|
|
265
613
|
}
|
|
266
614
|
if (types.includes('ID') || types.includes('SSN') || types.includes('PASSPORT') || types.includes('NATIONAL_ID')) {
|
|
267
|
-
frameworksSet.add('SOC 2
|
|
268
|
-
frameworksSet.add('ISO 27001
|
|
615
|
+
frameworksSet.add('SOC 2 controls');
|
|
616
|
+
frameworksSet.add('ISO 27001 A.8.11 controls');
|
|
269
617
|
}
|
|
270
618
|
if (types.some(t => ['CREDIT_CARD', 'BANK', 'IBAN', 'FINANCIAL', 'CARD', 'STRIPE_KEY', 'TAX_ID', 'EIN', 'W2', '1099', 'SALARY', 'WAGE'].includes(t))) {
|
|
271
619
|
frameworksSet.add('PCI DSS v4.0');
|
|
@@ -934,7 +1282,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
934
1282
|
},
|
|
935
1283
|
profile: {
|
|
936
1284
|
type: "string",
|
|
937
|
-
description:
|
|
1285
|
+
description: PROFILE_PARAM_DESCRIPTION
|
|
1286
|
+
},
|
|
1287
|
+
sector: {
|
|
1288
|
+
type: "string",
|
|
1289
|
+
description: SECTOR_PARAM_DESCRIPTION
|
|
938
1290
|
},
|
|
939
1291
|
format: {
|
|
940
1292
|
type: "string",
|
|
@@ -947,7 +1299,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
947
1299
|
},
|
|
948
1300
|
{
|
|
949
1301
|
name: "sanitize_text",
|
|
950
|
-
description: "STEP 1: Call this first. You MUST NOT process raw user data before calling this. Locally scrubs PII, secrets, and credentials (like API keys, passwords, emails, phones, names) from code, logs, or text. Replaces them with safe placeholders (e.g., [EMAIL_1], [API_KEY_1]). Keep your data secure before passing it to any LLM. (For in-code backend services or RAG vector pipelines outside of MCP, use '@privacyscrubber/sdk': npm i @privacyscrubber/sdk)",
|
|
1302
|
+
description: "STEP 1: Call this first. You MUST NOT process raw user data before calling this. Locally scrubs PII, secrets, and credentials (like API keys, passwords, emails, phones, names) from code, logs, or text. Replaces them with safe placeholders (e.g., [EMAIL_1], [API_KEY_1]). Keep your data secure before passing it to any LLM. Supports General and all 30 specialized industry profiles in RAM session map. (For in-code backend services or RAG vector pipelines outside of MCP, use '@privacyscrubber/sdk': npm i @privacyscrubber/sdk)",
|
|
951
1303
|
inputSchema: {
|
|
952
1304
|
type: "object",
|
|
953
1305
|
properties: {
|
|
@@ -957,7 +1309,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
957
1309
|
},
|
|
958
1310
|
profile: {
|
|
959
1311
|
type: "string",
|
|
960
|
-
description:
|
|
1312
|
+
description: PROFILE_PARAM_DESCRIPTION
|
|
1313
|
+
},
|
|
1314
|
+
sector: {
|
|
1315
|
+
type: "string",
|
|
1316
|
+
description: SECTOR_PARAM_DESCRIPTION
|
|
961
1317
|
},
|
|
962
1318
|
compact: {
|
|
963
1319
|
type: "boolean",
|
|
@@ -978,7 +1334,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
978
1334
|
},
|
|
979
1335
|
{
|
|
980
1336
|
name: "scrub_text",
|
|
981
|
-
description: "Alias for 'sanitize_text'. Locally scrubs PII and
|
|
1337
|
+
description: "Alias for 'sanitize_text'. Locally scrubs PII, secrets, and credentials from prompts or text before LLM ingestion. Supports General and all 30 specialized industry profiles in RAM session map.",
|
|
982
1338
|
inputSchema: {
|
|
983
1339
|
type: "object",
|
|
984
1340
|
properties: {
|
|
@@ -988,7 +1344,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
988
1344
|
},
|
|
989
1345
|
profile: {
|
|
990
1346
|
type: "string",
|
|
991
|
-
description:
|
|
1347
|
+
description: PROFILE_PARAM_DESCRIPTION
|
|
1348
|
+
},
|
|
1349
|
+
sector: {
|
|
1350
|
+
type: "string",
|
|
1351
|
+
description: SECTOR_PARAM_DESCRIPTION
|
|
992
1352
|
},
|
|
993
1353
|
compact: {
|
|
994
1354
|
type: "boolean",
|
|
@@ -1009,13 +1369,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1009
1369
|
},
|
|
1010
1370
|
{
|
|
1011
1371
|
name: "reveal_text",
|
|
1012
|
-
description: "STEP 3: Call this last. You MUST pass your final generated response through this tool to restore tokens (e.g., [EMAIL_1]) back with the original private data from the local volatile RAM-only session map before showing it to the user.",
|
|
1372
|
+
description: "STEP 3: Call this last. You MUST pass your final generated response through this tool to restore tokens (e.g., [EMAIL_1]) back with the original private data from the local volatile RAM-only session map before showing it to the user. Supports both standard batch mode and real-time streaming mode (stream: true).",
|
|
1013
1373
|
inputSchema: {
|
|
1014
1374
|
type: "object",
|
|
1015
1375
|
properties: {
|
|
1016
1376
|
text: {
|
|
1017
1377
|
type: "string",
|
|
1018
|
-
description: "The AI generated response containing placeholders to restore."
|
|
1378
|
+
description: "The AI generated response (or streaming chunk) containing placeholders to restore."
|
|
1379
|
+
},
|
|
1380
|
+
stream: {
|
|
1381
|
+
type: "boolean",
|
|
1382
|
+
description: "Optional. When true, uses the sliding-window StreamingDetokenizer to prevent token splitting across chunk boundaries."
|
|
1383
|
+
},
|
|
1384
|
+
flush: {
|
|
1385
|
+
type: "boolean",
|
|
1386
|
+
description: "Optional. When true in streaming mode, flushes any remaining buffered text at EOF."
|
|
1019
1387
|
},
|
|
1020
1388
|
session_id: {
|
|
1021
1389
|
type: "string",
|
|
@@ -1025,6 +1393,32 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1025
1393
|
required: ["text"]
|
|
1026
1394
|
}
|
|
1027
1395
|
},
|
|
1396
|
+
{
|
|
1397
|
+
name: "stream_reveal_text",
|
|
1398
|
+
description: "STEP 3 (Streaming): Real-time streaming token detokenizer for LLM Server-Sent Events (SSE) and stdio chunk pipelines. Buffers split token candidates across chunk boundaries (e.g. '[EM' + 'AIL_1]') so no characters or tokens are corrupted. Pass 'flush: true' on the final chunk.",
|
|
1399
|
+
inputSchema: {
|
|
1400
|
+
type: "object",
|
|
1401
|
+
properties: {
|
|
1402
|
+
chunk: {
|
|
1403
|
+
type: "string",
|
|
1404
|
+
description: "The incoming text chunk from the LLM stream or stdout."
|
|
1405
|
+
},
|
|
1406
|
+
flush: {
|
|
1407
|
+
type: "boolean",
|
|
1408
|
+
description: "Optional. Set to true on the final chunk or at EOF to flush remaining buffer. Defaults to false."
|
|
1409
|
+
},
|
|
1410
|
+
reset: {
|
|
1411
|
+
type: "boolean",
|
|
1412
|
+
description: "Optional. Set to true to reset internal streaming buffer for this session. Defaults to false."
|
|
1413
|
+
},
|
|
1414
|
+
session_id: {
|
|
1415
|
+
type: "string",
|
|
1416
|
+
description: "Optional session ID matching the sanitize_text call for multi-agent isolation. Defaults to 'default'."
|
|
1417
|
+
}
|
|
1418
|
+
},
|
|
1419
|
+
required: ["chunk"]
|
|
1420
|
+
}
|
|
1421
|
+
},
|
|
1028
1422
|
{
|
|
1029
1423
|
name: "sanitize_file",
|
|
1030
1424
|
description: "Reads a local file, sanitizes its contents using the selected profile, and outputs the safe version for AI analysis. Securely keeps original identifiers in memory.",
|
|
@@ -1037,7 +1431,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1037
1431
|
},
|
|
1038
1432
|
profile: {
|
|
1039
1433
|
type: "string",
|
|
1040
|
-
description:
|
|
1434
|
+
description: PROFILE_PARAM_DESCRIPTION
|
|
1435
|
+
},
|
|
1436
|
+
sector: {
|
|
1437
|
+
type: "string",
|
|
1438
|
+
description: SECTOR_PARAM_DESCRIPTION
|
|
1041
1439
|
},
|
|
1042
1440
|
compact: {
|
|
1043
1441
|
type: "boolean",
|
|
@@ -1063,7 +1461,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1063
1461
|
},
|
|
1064
1462
|
profile: {
|
|
1065
1463
|
type: "string",
|
|
1066
|
-
description:
|
|
1464
|
+
description: PROFILE_PARAM_DESCRIPTION
|
|
1465
|
+
},
|
|
1466
|
+
sector: {
|
|
1467
|
+
type: "string",
|
|
1468
|
+
description: SECTOR_PARAM_DESCRIPTION
|
|
1067
1469
|
},
|
|
1068
1470
|
compact: {
|
|
1069
1471
|
type: "boolean",
|
|
@@ -1089,7 +1491,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1089
1491
|
},
|
|
1090
1492
|
profile: {
|
|
1091
1493
|
type: "string",
|
|
1092
|
-
description:
|
|
1494
|
+
description: PROFILE_PARAM_DESCRIPTION
|
|
1495
|
+
},
|
|
1496
|
+
sector: {
|
|
1497
|
+
type: "string",
|
|
1498
|
+
description: SECTOR_PARAM_DESCRIPTION
|
|
1093
1499
|
},
|
|
1094
1500
|
extensions: {
|
|
1095
1501
|
type: "array",
|
|
@@ -1116,7 +1522,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1116
1522
|
},
|
|
1117
1523
|
profile: {
|
|
1118
1524
|
type: "string",
|
|
1119
|
-
description:
|
|
1525
|
+
description: PROFILE_PARAM_DESCRIPTION
|
|
1526
|
+
},
|
|
1527
|
+
sector: {
|
|
1528
|
+
type: "string",
|
|
1529
|
+
description: SECTOR_PARAM_DESCRIPTION
|
|
1120
1530
|
},
|
|
1121
1531
|
dry_run: {
|
|
1122
1532
|
type: "boolean",
|
|
@@ -1157,6 +1567,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1157
1567
|
department: {
|
|
1158
1568
|
type: "string",
|
|
1159
1569
|
description: "Optional department or auditor ID (e.g. 'SecOps / Engineering')."
|
|
1570
|
+
},
|
|
1571
|
+
session_id: {
|
|
1572
|
+
type: "string",
|
|
1573
|
+
description: "Optional session ID for multi-agent isolation. Defaults to 'default'."
|
|
1160
1574
|
}
|
|
1161
1575
|
},
|
|
1162
1576
|
required: []
|
|
@@ -1205,7 +1619,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1205
1619
|
},
|
|
1206
1620
|
profile: {
|
|
1207
1621
|
type: "string",
|
|
1208
|
-
description: "Detection profile. Available: '
|
|
1622
|
+
description: "Detection profile. Available: 'Dev' (default for code/secrets), 'General', or any of the 30 specialized industry profiles (e.g. 'Security', 'Finance', 'Medical'). Defaults to 'Dev'."
|
|
1623
|
+
},
|
|
1624
|
+
sector: {
|
|
1625
|
+
type: "string",
|
|
1626
|
+
description: SECTOR_PARAM_DESCRIPTION
|
|
1209
1627
|
},
|
|
1210
1628
|
timeout_ms: {
|
|
1211
1629
|
type: "number",
|
|
@@ -1227,7 +1645,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1227
1645
|
},
|
|
1228
1646
|
profile: {
|
|
1229
1647
|
type: "string",
|
|
1230
|
-
description: "Detection profile. Defaults to 'Dev'."
|
|
1648
|
+
description: "Detection profile. Available: 'Dev' (default), 'General', or any of the 30 specialized industry profiles. Defaults to 'Dev'."
|
|
1649
|
+
},
|
|
1650
|
+
sector: {
|
|
1651
|
+
type: "string",
|
|
1652
|
+
description: SECTOR_PARAM_DESCRIPTION
|
|
1231
1653
|
},
|
|
1232
1654
|
max_lines: {
|
|
1233
1655
|
type: "number",
|
|
@@ -1253,7 +1675,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1253
1675
|
},
|
|
1254
1676
|
profile: {
|
|
1255
1677
|
type: "string",
|
|
1256
|
-
description: "Detection profile. Defaults to 'Dev'."
|
|
1678
|
+
description: "Detection profile. Available: 'Dev' (default), 'General', or any of the 30 specialized industry profiles. Defaults to 'Dev'."
|
|
1679
|
+
},
|
|
1680
|
+
sector: {
|
|
1681
|
+
type: "string",
|
|
1682
|
+
description: SECTOR_PARAM_DESCRIPTION
|
|
1257
1683
|
},
|
|
1258
1684
|
session_id: {
|
|
1259
1685
|
type: "string",
|
|
@@ -1382,7 +1808,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1382
1808
|
},
|
|
1383
1809
|
profile: {
|
|
1384
1810
|
type: "string",
|
|
1385
|
-
description:
|
|
1811
|
+
description: PROFILE_PARAM_DESCRIPTION
|
|
1812
|
+
},
|
|
1813
|
+
sector: {
|
|
1814
|
+
type: "string",
|
|
1815
|
+
description: SECTOR_PARAM_DESCRIPTION
|
|
1386
1816
|
},
|
|
1387
1817
|
session_id: {
|
|
1388
1818
|
type: "string",
|
|
@@ -1619,14 +2049,14 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
|
1619
2049
|
"biotech": "Biotech & Genomics profile. Scrubs accessions, patient phenotypes, HGVS variants, DNA/RNA samples, and flowcell IDs.",
|
|
1620
2050
|
"telecom": "Call Centers & Telephony profile. Scrubs CTI interaction IDs, Amazon Connect/Genesys sessions, agent extensions, ANI/DNIS, IVR DTMF, and audio transcript diarization headers."
|
|
1621
2051
|
};
|
|
1622
|
-
const desc = descriptions[profileName.toLowerCase()] || `The '${profileName}' profile is a
|
|
2052
|
+
const desc = descriptions[profileName.toLowerCase()] || `The '${profileName}' profile is a specialized detection ruleset tuned for specific industry compliance. It detects and sanitizes domain-specific identifiers.`;
|
|
1623
2053
|
|
|
1624
2054
|
return {
|
|
1625
2055
|
contents: [
|
|
1626
2056
|
{
|
|
1627
2057
|
uri,
|
|
1628
2058
|
mimeType: "text/markdown",
|
|
1629
|
-
text: `# Profile: ${profileName}\n\n**Description:** ${desc}\n\n*Note: To use this profile, pass \`"profile": "${profileName}"\` to
|
|
2059
|
+
text: `# Profile: ${profileName}\n\n**Description:** ${desc}\n\n*Note: To use this profile, pass \`"profile": "${profileName}"\` or \`"sector": "${profileName}"\` to tools like \`scrub_text\`, \`sanitize_text\`, or \`sanitize_file\`. Advanced profiles include a 5,000 character Free trial quota; PRO/TEAMS tiers grant unlimited characters.*`
|
|
1630
2060
|
}
|
|
1631
2061
|
]
|
|
1632
2062
|
};
|
|
@@ -1641,7 +2071,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1641
2071
|
|
|
1642
2072
|
try {
|
|
1643
2073
|
if (name === "audit_context") {
|
|
1644
|
-
const { text,
|
|
2074
|
+
const { text, format = "detailed" } = args || {};
|
|
1645
2075
|
|
|
1646
2076
|
if (text === undefined || text === null) {
|
|
1647
2077
|
return {
|
|
@@ -1656,7 +2086,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1656
2086
|
};
|
|
1657
2087
|
}
|
|
1658
2088
|
|
|
1659
|
-
const targetProfile = (
|
|
2089
|
+
const targetProfile = resolveRequestedProfile(args, "General");
|
|
1660
2090
|
const customRules = loadCustomRules();
|
|
1661
2091
|
const normalizedProfile = targetProfile.toLowerCase();
|
|
1662
2092
|
const license = checkLicenseStatus();
|
|
@@ -1707,7 +2137,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1707
2137
|
}
|
|
1708
2138
|
|
|
1709
2139
|
if (name === "sanitize_text" || name === "scrub_text") {
|
|
1710
|
-
const { text,
|
|
2140
|
+
const { text, ignore_list, compact = false, session_id = "default" } = args || {};
|
|
1711
2141
|
const currentSession = getSessionMap(session_id);
|
|
1712
2142
|
const currentIgnoreList = getSessionIgnoreList(session_id);
|
|
1713
2143
|
|
|
@@ -1727,7 +2157,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1727
2157
|
content: [{ type: "text", text: `Error: Parameter 'text' must be a string, got '${typeof text}'. Stringify objects before passing.` }]
|
|
1728
2158
|
};
|
|
1729
2159
|
}
|
|
1730
|
-
const targetProfile = (
|
|
2160
|
+
const targetProfile = resolveRequestedProfile(args, "General");
|
|
1731
2161
|
|
|
1732
2162
|
// Check tier gating for advanced profiles
|
|
1733
2163
|
const isAdvanced = targetProfile.toLowerCase() !== "general";
|
|
@@ -1772,7 +2202,38 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1772
2202
|
}
|
|
1773
2203
|
|
|
1774
2204
|
if (name === "reveal_text") {
|
|
1775
|
-
const { text, session_id = "default" } = args || {};
|
|
2205
|
+
const { text, stream = false, flush = false, session_id = "default" } = args || {};
|
|
2206
|
+
|
|
2207
|
+
if (stream) {
|
|
2208
|
+
if (text === undefined || text === null) {
|
|
2209
|
+
if (flush) {
|
|
2210
|
+
const detok = getStreamDetokenizer(session_id);
|
|
2211
|
+
const flushedText = detok.flush();
|
|
2212
|
+
return {
|
|
2213
|
+
content: [{ type: "text", text: flushedText }]
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
return {
|
|
2217
|
+
isError: true,
|
|
2218
|
+
content: [{ type: "text", text: "Error: Missing required parameter 'text'. Provide the stream chunk to restore." }]
|
|
2219
|
+
};
|
|
2220
|
+
}
|
|
2221
|
+
if (typeof text !== "string") {
|
|
2222
|
+
return {
|
|
2223
|
+
isError: true,
|
|
2224
|
+
content: [{ type: "text", text: `Error: Parameter 'text' must be a string, got '${typeof text}'.` }]
|
|
2225
|
+
};
|
|
2226
|
+
}
|
|
2227
|
+
const detok = getStreamDetokenizer(session_id);
|
|
2228
|
+
let emitted = detok.feed(text);
|
|
2229
|
+
if (flush) {
|
|
2230
|
+
emitted += detok.flush();
|
|
2231
|
+
}
|
|
2232
|
+
return {
|
|
2233
|
+
content: [{ type: "text", text: emitted }]
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
2236
|
+
|
|
1776
2237
|
if (text === undefined || text === null) {
|
|
1777
2238
|
return {
|
|
1778
2239
|
isError: true,
|
|
@@ -1812,6 +2273,43 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1812
2273
|
};
|
|
1813
2274
|
}
|
|
1814
2275
|
|
|
2276
|
+
if (name === "stream_reveal_text") {
|
|
2277
|
+
const { chunk, flush = false, reset = false, session_id = "default" } = args || {};
|
|
2278
|
+
if (chunk === undefined || chunk === null) {
|
|
2279
|
+
if (flush) {
|
|
2280
|
+
const detok = getStreamDetokenizer(session_id);
|
|
2281
|
+
const flushedText = detok.flush();
|
|
2282
|
+
return {
|
|
2283
|
+
content: [{ type: "text", text: flushedText }]
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
return {
|
|
2287
|
+
isError: true,
|
|
2288
|
+
content: [{ type: "text", text: "Error: Missing required parameter 'chunk'. Provide the text chunk to process." }]
|
|
2289
|
+
};
|
|
2290
|
+
}
|
|
2291
|
+
if (typeof chunk !== "string") {
|
|
2292
|
+
return {
|
|
2293
|
+
isError: true,
|
|
2294
|
+
content: [{ type: "text", text: `Error: Parameter 'chunk' must be a string, got '${typeof chunk}'.` }]
|
|
2295
|
+
};
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
const detok = getStreamDetokenizer(session_id);
|
|
2299
|
+
if (reset) {
|
|
2300
|
+
detok.reset();
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
let emitted = detok.feed(chunk);
|
|
2304
|
+
if (flush) {
|
|
2305
|
+
emitted += detok.flush();
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
return {
|
|
2309
|
+
content: [{ type: "text", text: emitted }]
|
|
2310
|
+
};
|
|
2311
|
+
}
|
|
2312
|
+
|
|
1815
2313
|
if (name === "mark_false_positive") {
|
|
1816
2314
|
const { token, session_id = "default" } = args || {};
|
|
1817
2315
|
if (!token || typeof token !== 'string') {
|
|
@@ -1860,7 +2358,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1860
2358
|
]
|
|
1861
2359
|
};
|
|
1862
2360
|
}
|
|
1863
|
-
const
|
|
2361
|
+
const targetProfile = resolveRequestedProfile(args, "General");
|
|
2362
|
+
const { compact = false, session_id = "default" } = args || {};
|
|
1864
2363
|
const targetSession = getSessionMap(session_id);
|
|
1865
2364
|
const targetIgnoreList = getSessionIgnoreList(session_id);
|
|
1866
2365
|
const filePath = rawPath;
|
|
@@ -1908,9 +2407,111 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1908
2407
|
if (resolvedPath.toLowerCase().endsWith(".docx")) {
|
|
1909
2408
|
try {
|
|
1910
2409
|
const mammoth = require('mammoth');
|
|
1911
|
-
const
|
|
1912
|
-
const
|
|
1913
|
-
const
|
|
2410
|
+
const zlib = require('zlib');
|
|
2411
|
+
const docxBuf = fs.readFileSync(resolvedPath);
|
|
2412
|
+
const result = await mammoth.extractRawText({ buffer: docxBuf });
|
|
2413
|
+
let extraXmlText = "";
|
|
2414
|
+
try {
|
|
2415
|
+
const zipEntries = {};
|
|
2416
|
+
let eocdOffset = -1;
|
|
2417
|
+
for (let i = docxBuf.length - 22; i >= Math.max(0, docxBuf.length - 65557); i--) {
|
|
2418
|
+
if (docxBuf.readUInt32LE(i) === 0x06054b50) {
|
|
2419
|
+
eocdOffset = i;
|
|
2420
|
+
break;
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
if (eocdOffset !== -1) {
|
|
2424
|
+
const cdEntries = docxBuf.readUInt16LE(eocdOffset + 10);
|
|
2425
|
+
let pos = docxBuf.readUInt32LE(eocdOffset + 16);
|
|
2426
|
+
for (let e = 0; e < cdEntries && pos + 46 <= docxBuf.length; e++) {
|
|
2427
|
+
if (docxBuf.readUInt32LE(pos) !== 0x02014b50) break;
|
|
2428
|
+
const method = docxBuf.readUInt16LE(pos + 10);
|
|
2429
|
+
const compSize = docxBuf.readUInt32LE(pos + 20);
|
|
2430
|
+
const nameLen = docxBuf.readUInt16LE(pos + 28);
|
|
2431
|
+
const extraLen = docxBuf.readUInt16LE(pos + 30);
|
|
2432
|
+
const commentLen = docxBuf.readUInt16LE(pos + 32);
|
|
2433
|
+
const localOffset = docxBuf.readUInt32LE(pos + 42);
|
|
2434
|
+
const entryName = docxBuf.toString('utf8', pos + 46, pos + 46 + nameLen);
|
|
2435
|
+
if (localOffset + 30 <= docxBuf.length && docxBuf.readUInt32LE(localOffset) === 0x04034b50) {
|
|
2436
|
+
const lNameLen = docxBuf.readUInt16LE(localOffset + 26);
|
|
2437
|
+
const lExtraLen = docxBuf.readUInt16LE(localOffset + 28);
|
|
2438
|
+
const dataStart = localOffset + 30 + lNameLen + lExtraLen;
|
|
2439
|
+
const compData = docxBuf.subarray(dataStart, dataStart + compSize);
|
|
2440
|
+
if (method === 0) {
|
|
2441
|
+
zipEntries[entryName] = compData.toString('utf8');
|
|
2442
|
+
} else if (method === 8) {
|
|
2443
|
+
zipEntries[entryName] = zlib.inflateRawSync(compData).toString('utf8');
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
pos += 46 + nameLen + extraLen + commentLen;
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
const xmlTargets = Object.keys(zipEntries).filter((n) =>
|
|
2450
|
+
/^(?:word\/(?:header\d*|footer\d*|footnotes|endnotes|comments|commentsExtended|people)\.xml|docProps\/(?:core|app|custom)\.xml)$/i.test(n)
|
|
2451
|
+
);
|
|
2452
|
+
const docAuthors = new Set();
|
|
2453
|
+
for (const xmlPath of xmlTargets) {
|
|
2454
|
+
const rawXml = zipEntries[xmlPath];
|
|
2455
|
+
const authorRx = /\b(?:w:author|w15:author|w15:userId)="([^"]+)"/gi;
|
|
2456
|
+
let am;
|
|
2457
|
+
while ((am = authorRx.exec(rawXml)) !== null) {
|
|
2458
|
+
if (am[1] && am[1].trim()) docAuthors.add(am[1].trim());
|
|
2459
|
+
}
|
|
2460
|
+
const stripped = rawXml
|
|
2461
|
+
.replace(/<\/w:p>/gi, "\n")
|
|
2462
|
+
.replace(/<w:tab\b[^>]*\/?>/gi, "\t")
|
|
2463
|
+
.replace(/<[^>]+>/g, " ")
|
|
2464
|
+
.replace(/&/g, "&")
|
|
2465
|
+
.replace(/</g, "<")
|
|
2466
|
+
.replace(/>/g, ">")
|
|
2467
|
+
.replace(/"/g, '"')
|
|
2468
|
+
.replace(/'/g, "'")
|
|
2469
|
+
.replace(/[ \t]+/g, " ")
|
|
2470
|
+
.trim();
|
|
2471
|
+
if (stripped) extraXmlText += `\n${stripped}\n`;
|
|
2472
|
+
}
|
|
2473
|
+
if (zipEntries["word/document.xml"]) {
|
|
2474
|
+
const docXml = zipEntries["word/document.xml"];
|
|
2475
|
+
const authorRx = /\bw:author="([^"]+)"/gi;
|
|
2476
|
+
let am;
|
|
2477
|
+
while ((am = authorRx.exec(docXml)) !== null) {
|
|
2478
|
+
if (am[1] && am[1].trim()) docAuthors.add(am[1].trim());
|
|
2479
|
+
}
|
|
2480
|
+
const delAndInstrRx = /<w:(?:delText|instrText)\b[^>]*>([\s\S]*?)<\/w:(?:delText|instrText)>/gi;
|
|
2481
|
+
let dm;
|
|
2482
|
+
const hiddenChunks = [];
|
|
2483
|
+
while ((dm = delAndInstrRx.exec(docXml)) !== null) {
|
|
2484
|
+
const val = dm[1]
|
|
2485
|
+
.replace(/<[^>]+>/g, " ")
|
|
2486
|
+
.replace(/&/g, "&")
|
|
2487
|
+
.replace(/</g, "<")
|
|
2488
|
+
.replace(/>/g, ">")
|
|
2489
|
+
.replace(/"/g, '"')
|
|
2490
|
+
.replace(/'/g, "'")
|
|
2491
|
+
.trim();
|
|
2492
|
+
if (val) hiddenChunks.push(val);
|
|
2493
|
+
}
|
|
2494
|
+
if (hiddenChunks.length > 0) {
|
|
2495
|
+
extraXmlText += `\n[Tracked Changes / Field Text]: ${hiddenChunks.join(" | ")}\n`;
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
if (docAuthors.size > 0) {
|
|
2499
|
+
extraXmlText += `\n[Document Reviewers]: ${Array.from(docAuthors).join(", ")}\n`;
|
|
2500
|
+
}
|
|
2501
|
+
const relsTargets = Object.keys(zipEntries).filter((n) => /^word\/_rels\/.*\.rels$/i.test(n));
|
|
2502
|
+
for (const relPath of relsTargets) {
|
|
2503
|
+
const relXml = zipEntries[relPath];
|
|
2504
|
+
const targetRegex = /Target="([^"]+)"/gi;
|
|
2505
|
+
let tm;
|
|
2506
|
+
while ((tm = targetRegex.exec(relXml)) !== null) {
|
|
2507
|
+
const val = tm[1];
|
|
2508
|
+
if (/^(?:mailto:|tel:|https?:\/\/)/i.test(val) && !/schemas\.openxmlformats\.org|schemas\.microsoft\.com|w3\.org/i.test(val)) {
|
|
2509
|
+
extraXmlText += `\n[Document Link]: ${val}\n`;
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
} catch (_) {}
|
|
2514
|
+
const content = (result.value || '') + (extraXmlText ? '\n' + extraXmlText : '');
|
|
1914
2515
|
const isAdvanced = targetProfile.toLowerCase() !== "general";
|
|
1915
2516
|
const license = checkLicenseStatus();
|
|
1916
2517
|
let finalProfile = targetProfile;
|
|
@@ -1973,7 +2574,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1973
2574
|
const pdf = require('pdf-parse');
|
|
1974
2575
|
const data = await pdf(buffer);
|
|
1975
2576
|
const content = data.text;
|
|
1976
|
-
const targetProfile = profile.trim();
|
|
1977
2577
|
let finalProfile = targetProfile;
|
|
1978
2578
|
|
|
1979
2579
|
const sanitized = performSanitization(content, finalProfile, targetIgnoreList, targetSession);
|
|
@@ -2000,13 +2600,46 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2000
2600
|
const XLSX = require('xlsx');
|
|
2001
2601
|
const workbook = XLSX.read(buffer, { type: 'buffer' });
|
|
2002
2602
|
let content = '';
|
|
2603
|
+
if (workbook.Props && typeof workbook.Props === 'object') {
|
|
2604
|
+
const propFields = ['Author', 'LastAuthor', 'Company', 'Manager', 'Title', 'Subject', 'Comments'];
|
|
2605
|
+
for (const pf of propFields) {
|
|
2606
|
+
if (workbook.Props[pf] && String(workbook.Props[pf]).trim()) {
|
|
2607
|
+
content += `[Workbook ${pf}]: ${String(workbook.Props[pf]).trim()}\n`;
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
if (workbook.CustProps && typeof workbook.CustProps === 'object') {
|
|
2612
|
+
for (const [ck, cv] of Object.entries(workbook.CustProps)) {
|
|
2613
|
+
if (cv !== undefined && cv !== null && String(cv).trim()) {
|
|
2614
|
+
content += `[Custom Property ${ck}]: ${String(cv).trim()}\n`;
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2003
2618
|
workbook.SheetNames.forEach(sheetName => {
|
|
2004
2619
|
const worksheet = workbook.Sheets[sheetName];
|
|
2005
2620
|
content += `--- Sheet: ${sheetName} ---\n`;
|
|
2006
2621
|
content += XLSX.utils.sheet_to_csv(worksheet) + '\n';
|
|
2622
|
+
if (worksheet && typeof worksheet === 'object') {
|
|
2623
|
+
for (const cellKey of Object.keys(worksheet)) {
|
|
2624
|
+
if (cellKey.startsWith('!')) continue;
|
|
2625
|
+
const cell = worksheet[cellKey];
|
|
2626
|
+
if (!cell || typeof cell !== 'object') continue;
|
|
2627
|
+
if (cell.l && cell.l.Target) {
|
|
2628
|
+
content += `[Hyperlink ${cellKey}]: ${cell.l.Target}\n`;
|
|
2629
|
+
}
|
|
2630
|
+
if (Array.isArray(cell.c)) {
|
|
2631
|
+
for (const comm of cell.c) {
|
|
2632
|
+
const author = comm.a ? `${comm.a}: ` : '';
|
|
2633
|
+
const cText = (comm.t || '').trim();
|
|
2634
|
+
if (cText) {
|
|
2635
|
+
content += `[Comment ${cellKey}] ${author}${cText}\n`;
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2007
2641
|
});
|
|
2008
2642
|
|
|
2009
|
-
const targetProfile = profile.trim();
|
|
2010
2643
|
let finalProfile = targetProfile;
|
|
2011
2644
|
|
|
2012
2645
|
const sanitized = performSanitization(content, finalProfile, targetIgnoreList, targetSession);
|
|
@@ -2051,14 +2684,63 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2051
2684
|
};
|
|
2052
2685
|
}
|
|
2053
2686
|
|
|
2054
|
-
|
|
2055
|
-
const
|
|
2687
|
+
let content = buffer.toString("utf8");
|
|
2688
|
+
const ext = path.extname(resolvedPath).toLowerCase();
|
|
2689
|
+
if (ext === ".eml") {
|
|
2690
|
+
const decodeQpUtf8 = (str) =>
|
|
2691
|
+
str
|
|
2692
|
+
.replace(/=\r?\n/g, "")
|
|
2693
|
+
.replace(/(?:=[0-9A-Fa-f]{2})+/g, (seq) => {
|
|
2694
|
+
try {
|
|
2695
|
+
const bytes = Buffer.alloc(seq.length / 3);
|
|
2696
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
2697
|
+
bytes[i] = parseInt(seq.slice(i * 3 + 1, i * 3 + 3), 16);
|
|
2698
|
+
}
|
|
2699
|
+
return bytes.toString("utf8");
|
|
2700
|
+
} catch (_) {
|
|
2701
|
+
return seq;
|
|
2702
|
+
}
|
|
2703
|
+
});
|
|
2704
|
+
content = content.replace(/=\?([^?]+)\?([BQbq])\?([^?]+)\?=/g, (full, charset, enc, payload) => {
|
|
2705
|
+
try {
|
|
2706
|
+
if (enc.toUpperCase() === "B") {
|
|
2707
|
+
return Buffer.from(payload.replace(/\s+/g, ""), "base64").toString("utf8");
|
|
2708
|
+
}
|
|
2709
|
+
return decodeQpUtf8(payload.replace(/_/g, " "));
|
|
2710
|
+
} catch (_) {
|
|
2711
|
+
return full;
|
|
2712
|
+
}
|
|
2713
|
+
});
|
|
2714
|
+
content = content.replace(
|
|
2715
|
+
/(Content-Transfer-Encoding\s*:\s*base64\b[\s\S]*?\r?\n\r?\n)([A-Za-z0-9+/=\r\n]+)(?=\r?\n--|\s*$)/gi,
|
|
2716
|
+
(full, hdr, b64Block) => {
|
|
2717
|
+
try {
|
|
2718
|
+
const cleanB64 = b64Block.replace(/[\r\n\s]+/g, "");
|
|
2719
|
+
if (cleanB64.length < 8) return full;
|
|
2720
|
+
return hdr + Buffer.from(cleanB64, "base64").toString("utf8");
|
|
2721
|
+
} catch (_) {
|
|
2722
|
+
return full;
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
);
|
|
2726
|
+
content = decodeQpUtf8(content);
|
|
2727
|
+
} else if (ext === ".rtf") {
|
|
2728
|
+
content = content
|
|
2729
|
+
.replace(/\\par\b|\\line\b/gi, "\n")
|
|
2730
|
+
.replace(/\\'([0-9a-fA-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
|
|
2731
|
+
.replace(/\\u(-?\d+)\??/g, (_, code) => {
|
|
2732
|
+
const n = parseInt(code, 10);
|
|
2733
|
+
return String.fromCharCode(n < 0 ? n + 65536 : n);
|
|
2734
|
+
})
|
|
2735
|
+
.replace(/\\[a-zA-Z]+(?:-?\d+)?[ ]?/g, " ")
|
|
2736
|
+
.replace(/[{}]/g, " ")
|
|
2737
|
+
.replace(/[ \t]+/g, " ");
|
|
2738
|
+
}
|
|
2056
2739
|
const isAdvanced = targetProfile.toLowerCase() !== "general";
|
|
2057
2740
|
const license = checkLicenseStatus();
|
|
2058
2741
|
let finalProfile = targetProfile;
|
|
2059
2742
|
let isAutoElevated = false;
|
|
2060
2743
|
|
|
2061
|
-
const ext = path.extname(resolvedPath).toLowerCase();
|
|
2062
2744
|
const CODE_EXTENSIONS = new Set(['.js', '.ts', '.jsx', '.tsx', '.py', '.go', '.rs', '.java', '.rb', '.php', '.cs', '.sh', '.bash', '.zsh', '.env', '.yaml', '.yml', '.toml', '.sql']);
|
|
2063
2745
|
const DEV_HEURISTIC_REGEX = /(?:AKIA[0-9A-Z]{16}|ghp_[a-zA-Z0-9]{36}|sk-[a-zA-Z0-9]{20,}|-----BEGIN (?:RSA )?PRIVATE KEY-----|Bearer\s+eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+|(?:postgres|postgresql|mysql|mongodb|redis):\/\/[^\s"'>]+|export\s+(?:AWS_|DATABASE_|API_KEY|SECRET)|const\s+\w+\s*=\s*require\(|import\s+.*\s+from\s+['"])/i;
|
|
2064
2746
|
|
|
@@ -2095,7 +2777,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2095
2777
|
}
|
|
2096
2778
|
|
|
2097
2779
|
if (name === "redact_file") {
|
|
2098
|
-
const { file_path,
|
|
2780
|
+
const { file_path, dry_run, no_backup } = args;
|
|
2099
2781
|
if (!file_path || !fs.existsSync(file_path)) {
|
|
2100
2782
|
return { isError: true, content: [{ type: "text", text: `Error: File not found at path: ${file_path}` }] };
|
|
2101
2783
|
}
|
|
@@ -2110,10 +2792,55 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2110
2792
|
if (buffer.indexOf(0) !== -1) {
|
|
2111
2793
|
return { isError: true, content: [{ type: "text", text: `Error: Cannot redact binary file.` }] };
|
|
2112
2794
|
}
|
|
2113
|
-
|
|
2795
|
+
let content = buffer.toString('utf8');
|
|
2796
|
+
const ext = path.extname(file_path).toLowerCase();
|
|
2797
|
+
if (ext === ".eml") {
|
|
2798
|
+
const decodeQpUtf8 = (str) =>
|
|
2799
|
+
str.replace(/=\r?\n/g, "").replace(/(?:=[0-9A-Fa-f]{2})+/g, (seq) => {
|
|
2800
|
+
try {
|
|
2801
|
+
const bytes = Buffer.alloc(seq.length / 3);
|
|
2802
|
+
for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(seq.slice(i * 3 + 1, i * 3 + 3), 16);
|
|
2803
|
+
return bytes.toString("utf8");
|
|
2804
|
+
} catch (_) { return seq; }
|
|
2805
|
+
});
|
|
2806
|
+
content = content.replace(/=\?([^?]+)\?([BQbq])\?([^?]+)\?=/g, (full, charset, enc, payload) => {
|
|
2807
|
+
try {
|
|
2808
|
+
if (enc.toUpperCase() === "B") return Buffer.from(payload.replace(/\s+/g, ""), "base64").toString("utf8");
|
|
2809
|
+
return decodeQpUtf8(payload.replace(/_/g, " "));
|
|
2810
|
+
} catch (_) { return full; }
|
|
2811
|
+
});
|
|
2812
|
+
content = content.replace(/(Content-Transfer-Encoding\s*:\s*base64\b[\s\S]*?\r?\n\r?\n)([A-Za-z0-9+/=\r\n]+)(?=\r?\n--|\s*$)/gi, (full, hdr, b64Block) => {
|
|
2813
|
+
try {
|
|
2814
|
+
const cleanB64 = b64Block.replace(/[\r\n\s]+/g, "");
|
|
2815
|
+
if (cleanB64.length < 8) return full;
|
|
2816
|
+
return hdr + Buffer.from(cleanB64, "base64").toString("utf8");
|
|
2817
|
+
} catch (_) { return full; }
|
|
2818
|
+
});
|
|
2819
|
+
content = decodeQpUtf8(content);
|
|
2820
|
+
} else if (ext === ".rtf") {
|
|
2821
|
+
content = content
|
|
2822
|
+
.replace(/\\par\b|\\line\b/gi, "\n")
|
|
2823
|
+
.replace(/\\'([0-9a-fA-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
|
|
2824
|
+
.replace(/\\u(-?\d+)\??/g, (_, code) => {
|
|
2825
|
+
const n = parseInt(code, 10);
|
|
2826
|
+
return String.fromCharCode(n < 0 ? n + 65536 : n);
|
|
2827
|
+
})
|
|
2828
|
+
.replace(/\\[a-zA-Z]+(?:-?\d+)?[ ]?/g, " ")
|
|
2829
|
+
.replace(/[{}]/g, " ")
|
|
2830
|
+
.replace(/[ \t]+/g, " ");
|
|
2831
|
+
}
|
|
2114
2832
|
|
|
2115
2833
|
const customRules = loadCustomRules();
|
|
2116
|
-
const
|
|
2834
|
+
const activeRules = [...customRules];
|
|
2835
|
+
DEVOPS_SECRETS_DETECTOR.forEach(rule => {
|
|
2836
|
+
activeRules.push({
|
|
2837
|
+
name: rule.name,
|
|
2838
|
+
pattern: rule.regex.source,
|
|
2839
|
+
regex: new RegExp(rule.regex.source, rule.regex.flags),
|
|
2840
|
+
label: rule.label || 'SECRET'
|
|
2841
|
+
});
|
|
2842
|
+
});
|
|
2843
|
+
const normalizedProfile = resolveRequestedProfile(args, "general").toLowerCase();
|
|
2117
2844
|
const license = checkLicenseStatus();
|
|
2118
2845
|
|
|
2119
2846
|
const limitStatus = checkFreeTierLimit(license.isPro);
|
|
@@ -2125,7 +2852,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2125
2852
|
}
|
|
2126
2853
|
const localSessionMap = {};
|
|
2127
2854
|
|
|
2128
|
-
const result = PrivacyScrubberCore.scrubText(content,
|
|
2855
|
+
const result = PrivacyScrubberCore.scrubText(content, activeRules, {}, normalizedProfile, localSessionMap, license.isPro);
|
|
2129
2856
|
|
|
2130
2857
|
let isSecretLeak = false;
|
|
2131
2858
|
if (!license.isPro && result.tokenMap) {
|
|
@@ -2240,8 +2967,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2240
2967
|
const tierIcon = license.isPro ? '✅' : '🔓';
|
|
2241
2968
|
const profileList = license.isPro
|
|
2242
2969
|
? 'All 30 specialized profiles active (General, Dev, Medical, Legal, Finance, HR…)'
|
|
2243
|
-
: '
|
|
2244
|
-
const sizeLimit = license.isPro ? 'Unlimited' : '15,000
|
|
2970
|
+
: 'All 30 profiles active (5k quota) · PRO unlocks unlimited';
|
|
2971
|
+
const sizeLimit = license.isPro ? 'Unlimited' : '15,000 char (General) / 5,000 char (Industry)';
|
|
2245
2972
|
|
|
2246
2973
|
const configPath = resolveConfigPath();
|
|
2247
2974
|
let rulesStatus = '';
|
|
@@ -2323,7 +3050,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2323
3050
|
}
|
|
2324
3051
|
|
|
2325
3052
|
if (name === "audit_directory_for_pii") {
|
|
2326
|
-
const { directory_path,
|
|
3053
|
+
const { directory_path, extensions, ignore_node_modules } = args;
|
|
2327
3054
|
if (!directory_path || !fs.existsSync(directory_path)) {
|
|
2328
3055
|
return { isError: true, content: [{ type: "text", text: `Error: Directory not found at path: ${directory_path}` }] };
|
|
2329
3056
|
}
|
|
@@ -2352,7 +3079,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2352
3079
|
let reportLines = [`## Security Audit Report: ${directory_path}`];
|
|
2353
3080
|
let filesWithIssues = 0;
|
|
2354
3081
|
const customRules = loadCustomRules();
|
|
2355
|
-
const
|
|
3082
|
+
const activeRules = [...customRules];
|
|
3083
|
+
DEVOPS_SECRETS_DETECTOR.forEach(rule => {
|
|
3084
|
+
activeRules.push({
|
|
3085
|
+
name: rule.name,
|
|
3086
|
+
pattern: rule.regex.source,
|
|
3087
|
+
regex: new RegExp(rule.regex.source, rule.regex.flags),
|
|
3088
|
+
label: rule.label || 'SECRET'
|
|
3089
|
+
});
|
|
3090
|
+
});
|
|
3091
|
+
const normalizedProfile = resolveRequestedProfile(args, "general").toLowerCase();
|
|
2356
3092
|
const license = checkLicenseStatus();
|
|
2357
3093
|
|
|
2358
3094
|
const limitStatus = checkFreeTierLimit(license.isPro);
|
|
@@ -2374,7 +3110,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2374
3110
|
const content = buffer.toString('utf8');
|
|
2375
3111
|
|
|
2376
3112
|
const localSessionMap = {};
|
|
2377
|
-
const result = PrivacyScrubberCore.scrubText(content,
|
|
3113
|
+
const result = PrivacyScrubberCore.scrubText(content, activeRules, {}, normalizedProfile, localSessionMap, license.isPro);
|
|
2378
3114
|
|
|
2379
3115
|
Object.assign(totalTokensForDirectory, localSessionMap);
|
|
2380
3116
|
|
|
@@ -2422,17 +3158,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2422
3158
|
}
|
|
2423
3159
|
|
|
2424
3160
|
if (name === "generate_compliance_report") {
|
|
2425
|
-
const { format = "markdown", company_name = "PrivacyScrubber Client", department = "SecOps / Compliance" } = args || {};
|
|
2426
|
-
const
|
|
3161
|
+
const { format = "markdown", company_name = "PrivacyScrubber Client", department = "SecOps / Compliance", session_id = "default" } = args || {};
|
|
3162
|
+
const targetSession = getSessionMap(session_id);
|
|
3163
|
+
const telemetry = buildCisoAuditTelemetry(targetSession);
|
|
2427
3164
|
|
|
2428
|
-
const receipt = buildPsr2Receipt(
|
|
3165
|
+
const receipt = buildPsr2Receipt(targetSession);
|
|
2429
3166
|
return {
|
|
2430
3167
|
content: [{ type: "text", text: formatPsr2Report({ format, receipt, telemetry, company_name, department, utmSource: 'mcp_compliance_report', utmMedium: format === 'json' ? 'json' : 'markdown' }) }]
|
|
2431
3168
|
};
|
|
2432
3169
|
}
|
|
2433
3170
|
|
|
2434
3171
|
if (name === "guard_exec") {
|
|
2435
|
-
const { command, cwd,
|
|
3172
|
+
const { command, cwd, timeout_ms = 15000, session_id = "default" } = args || {};
|
|
2436
3173
|
if (!command || typeof command !== "string") {
|
|
2437
3174
|
return {
|
|
2438
3175
|
isError: true,
|
|
@@ -2472,7 +3209,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2472
3209
|
exitCode = typeof err.status === 'number' ? err.status : 1;
|
|
2473
3210
|
}
|
|
2474
3211
|
|
|
2475
|
-
const targetProfile = (
|
|
3212
|
+
const targetProfile = resolveRequestedProfile(args, "Dev");
|
|
2476
3213
|
const isAdvanced = targetProfile.toLowerCase() !== "general";
|
|
2477
3214
|
const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
|
|
2478
3215
|
|
|
@@ -2502,7 +3239,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2502
3239
|
}
|
|
2503
3240
|
|
|
2504
3241
|
if (name === "guard_read_file") {
|
|
2505
|
-
const { file_path,
|
|
3242
|
+
const { file_path, max_lines = 500, session_id = "default" } = args || {};
|
|
2506
3243
|
if (!file_path || typeof file_path !== "string") {
|
|
2507
3244
|
return {
|
|
2508
3245
|
isError: true,
|
|
@@ -2523,6 +3260,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2523
3260
|
content: [{ type: "text", text: `Error: Path is a directory. Use audit_directory_for_pii to scan directories.` }]
|
|
2524
3261
|
};
|
|
2525
3262
|
}
|
|
3263
|
+
if (stat.size > 10 * 1024 * 1024) {
|
|
3264
|
+
return {
|
|
3265
|
+
isError: true,
|
|
3266
|
+
content: [{ type: "text", text: `Error: File is too large (${(stat.size / 1024 / 1024).toFixed(2)}MB). Maximum allowed size is 10MB.` }]
|
|
3267
|
+
};
|
|
3268
|
+
}
|
|
3269
|
+
const rawBuffer = fs.readFileSync(resolvedPath);
|
|
3270
|
+
const checkLen = Math.min(rawBuffer.length, 8000);
|
|
3271
|
+
for (let i = 0; i < checkLen; i++) {
|
|
3272
|
+
if (rawBuffer[i] === 0) {
|
|
3273
|
+
return {
|
|
3274
|
+
isError: true,
|
|
3275
|
+
content: [{ type: "text", text: `Error: Binary file format detected. Use sanitize_file for DOCX/PDF/XLSX documents.` }]
|
|
3276
|
+
};
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
2526
3279
|
const license = checkLicenseStatus();
|
|
2527
3280
|
const limitStatus = checkFreeTierLimit(license.isPro);
|
|
2528
3281
|
if (limitStatus.blocked) {
|
|
@@ -2532,12 +3285,48 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2532
3285
|
};
|
|
2533
3286
|
}
|
|
2534
3287
|
|
|
2535
|
-
let rawContent =
|
|
3288
|
+
let rawContent = rawBuffer.toString('utf8');
|
|
3289
|
+
const ext = path.extname(resolvedPath).toLowerCase();
|
|
3290
|
+
if (ext === '.eml') {
|
|
3291
|
+
const decodeQpUtf8 = (str) =>
|
|
3292
|
+
str.replace(/=\r?\n/g, '').replace(/(?:=[0-9A-Fa-f]{2})+/g, (seq) => {
|
|
3293
|
+
try {
|
|
3294
|
+
const bytes = Buffer.alloc(seq.length / 3);
|
|
3295
|
+
for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(seq.slice(i * 3 + 1, i * 3 + 3), 16);
|
|
3296
|
+
return bytes.toString('utf8');
|
|
3297
|
+
} catch (_) { return seq; }
|
|
3298
|
+
});
|
|
3299
|
+
rawContent = rawContent.replace(/=\?([^?]+)\?([BQbq])\?([^?]+)\?=/g, (full, charset, enc, payload) => {
|
|
3300
|
+
try {
|
|
3301
|
+
if (enc.toUpperCase() === 'B') return Buffer.from(payload.replace(/\s+/g, ''), 'base64').toString('utf8');
|
|
3302
|
+
return decodeQpUtf8(payload.replace(/_/g, ' '));
|
|
3303
|
+
} catch (_) { return full; }
|
|
3304
|
+
});
|
|
3305
|
+
rawContent = rawContent.replace(/(Content-Transfer-Encoding\s*:\s*base64\b[\s\S]*?\r?\n\r?\n)([A-Za-z0-9+/=\r\n]+)(?=\r?\n--|\s*$)/gi, (full, hdr, b64Block) => {
|
|
3306
|
+
try {
|
|
3307
|
+
const cleanB64 = b64Block.replace(/[\r\n\s]+/g, '');
|
|
3308
|
+
if (cleanB64.length < 8) return full;
|
|
3309
|
+
return hdr + Buffer.from(cleanB64, 'base64').toString('utf8');
|
|
3310
|
+
} catch (_) { return full; }
|
|
3311
|
+
});
|
|
3312
|
+
rawContent = decodeQpUtf8(rawContent);
|
|
3313
|
+
} else if (ext === '.rtf') {
|
|
3314
|
+
rawContent = rawContent
|
|
3315
|
+
.replace(/\\par\b|\\line\b/gi, '\n')
|
|
3316
|
+
.replace(/\\'([0-9a-fA-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
|
|
3317
|
+
.replace(/\\u(-?\d+)\??/g, (_, code) => {
|
|
3318
|
+
const n = parseInt(code, 10);
|
|
3319
|
+
return String.fromCharCode(n < 0 ? n + 65536 : n);
|
|
3320
|
+
})
|
|
3321
|
+
.replace(/\\[a-zA-Z]+(?:-?\d+)?[ ]?/g, ' ')
|
|
3322
|
+
.replace(/[{}]/g, ' ')
|
|
3323
|
+
.replace(/[ \t]+/g, ' ');
|
|
3324
|
+
}
|
|
2536
3325
|
const lines = rawContent.split(/\r?\n/);
|
|
2537
3326
|
const capped = lines.slice(0, max_lines).join('\n');
|
|
2538
3327
|
const wasLineTruncated = lines.length > max_lines;
|
|
2539
3328
|
|
|
2540
|
-
const targetProfile = (
|
|
3329
|
+
const targetProfile = resolveRequestedProfile(args, "Dev");
|
|
2541
3330
|
const isAdvanced = targetProfile.toLowerCase() !== "general";
|
|
2542
3331
|
const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
|
|
2543
3332
|
|
|
@@ -2563,7 +3352,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2563
3352
|
}
|
|
2564
3353
|
|
|
2565
3354
|
if (name === "guard_git_diff") {
|
|
2566
|
-
const { staged = false, cwd,
|
|
3355
|
+
const { staged = false, cwd, session_id = "default" } = args || {};
|
|
2567
3356
|
const targetSession = getSessionMap(session_id);
|
|
2568
3357
|
const targetIgnoreList = getSessionIgnoreList(session_id);
|
|
2569
3358
|
const repoCwd = cwd ? path.resolve(cwd) : process.cwd();
|
|
@@ -2597,7 +3386,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2597
3386
|
};
|
|
2598
3387
|
}
|
|
2599
3388
|
|
|
2600
|
-
const targetProfile = (
|
|
3389
|
+
const targetProfile = resolveRequestedProfile(args, "Dev");
|
|
2601
3390
|
const isAdvanced = targetProfile.toLowerCase() !== "general";
|
|
2602
3391
|
const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
|
|
2603
3392
|
|
|
@@ -2806,6 +3595,7 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
2806
3595
|
}
|
|
2807
3596
|
const tokenEntries = Object.entries(session.map);
|
|
2808
3597
|
const telemetry = buildCisoAuditTelemetry(session.map);
|
|
3598
|
+
const streamBufferLen = session.streamDetokenizer ? session.streamDetokenizer.buffer.length : 0;
|
|
2809
3599
|
return {
|
|
2810
3600
|
content: [{
|
|
2811
3601
|
type: "text",
|
|
@@ -2813,6 +3603,7 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
2813
3603
|
`Created: ${new Date(session.createdAt).toISOString()}\n` +
|
|
2814
3604
|
`Last Active: ${new Date(session.lastActive).toISOString()}\n` +
|
|
2815
3605
|
`Total Tokens Stored in RAM: ${tokenEntries.length}\n` +
|
|
3606
|
+
`Stream Buffer: ${streamBufferLen} bytes pending\n` +
|
|
2816
3607
|
`Entity Types: ${Object.keys(telemetry.entities).join(', ') || 'None'}\n` +
|
|
2817
3608
|
`Risk Level: ${telemetry.riskLevel}\n` +
|
|
2818
3609
|
`Status: Volatile in-memory mapping active (0 bytes disk/network).`
|
|
@@ -2825,7 +3616,8 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
2825
3616
|
for (const [sid, sess] of agentSessions.entries()) {
|
|
2826
3617
|
const count = Object.keys(sess.map).length;
|
|
2827
3618
|
totalTokens += count;
|
|
2828
|
-
|
|
3619
|
+
const streamBufferLen = sess.streamDetokenizer ? sess.streamDetokenizer.buffer.length : 0;
|
|
3620
|
+
sessionList.push({ id: sid, tokens: count, streamBufferBytes: streamBufferLen, lastActive: new Date(sess.lastActive).toISOString() });
|
|
2829
3621
|
}
|
|
2830
3622
|
|
|
2831
3623
|
return {
|
|
@@ -2836,7 +3628,7 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
2836
3628
|
`Total Tokens in Memory: ${totalTokens}\n` +
|
|
2837
3629
|
`Process Heap Used: ${(memUsage.heapUsed / (1024 * 1024)).toFixed(2)} MB\n` +
|
|
2838
3630
|
`Uptime: ${Math.floor(process.uptime())} seconds\n` +
|
|
2839
|
-
`Active Sessions: ${sessionList.map(s => `${s.id} (${s.tokens} tokens)`).join(', ') || 'None'}`
|
|
3631
|
+
`Active Sessions: ${sessionList.map(s => `${s.id} (${s.tokens} tokens, ${s.streamBufferBytes} buf)`).join(', ') || 'None'}`
|
|
2840
3632
|
}]
|
|
2841
3633
|
};
|
|
2842
3634
|
}
|
|
@@ -2845,6 +3637,11 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
2845
3637
|
const { session_id, all = false, reset_all = false } = args || {};
|
|
2846
3638
|
if (all || reset_all) {
|
|
2847
3639
|
const count = agentSessions.size;
|
|
3640
|
+
for (const sess of agentSessions.values()) {
|
|
3641
|
+
if (sess.streamDetokenizer) {
|
|
3642
|
+
sess.streamDetokenizer.reset();
|
|
3643
|
+
}
|
|
3644
|
+
}
|
|
2848
3645
|
agentSessions.clear();
|
|
2849
3646
|
getSessionMap('default');
|
|
2850
3647
|
sessionIgnoreList.clear();
|
|
@@ -2857,6 +3654,10 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
2857
3654
|
}
|
|
2858
3655
|
|
|
2859
3656
|
const sid = (session_id || 'default').toString().trim();
|
|
3657
|
+
const sess = agentSessions.get(sid);
|
|
3658
|
+
if (sess && sess.streamDetokenizer) {
|
|
3659
|
+
sess.streamDetokenizer.reset();
|
|
3660
|
+
}
|
|
2860
3661
|
const existed = agentSessions.delete(sid);
|
|
2861
3662
|
if (sid === 'default') {
|
|
2862
3663
|
getSessionMap('default');
|
|
@@ -2872,7 +3673,7 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
2872
3673
|
}
|
|
2873
3674
|
|
|
2874
3675
|
if (name === "guard_rag_chunk") {
|
|
2875
|
-
const { chunks,
|
|
3676
|
+
const { chunks, session_id = "default" } = args || {};
|
|
2876
3677
|
if (!Array.isArray(chunks)) {
|
|
2877
3678
|
return {
|
|
2878
3679
|
isError: true,
|
|
@@ -2880,6 +3681,7 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
2880
3681
|
};
|
|
2881
3682
|
}
|
|
2882
3683
|
|
|
3684
|
+
const targetProfile = resolveRequestedProfile(args, "General");
|
|
2883
3685
|
const targetSession = getSessionMap(session_id);
|
|
2884
3686
|
const targetIgnoreList = getSessionIgnoreList(session_id);
|
|
2885
3687
|
let totalTokensRedacted = 0;
|
|
@@ -2888,13 +3690,13 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
2888
3690
|
for (let i = 0; i < chunks.length; i++) {
|
|
2889
3691
|
const item = chunks[i];
|
|
2890
3692
|
if (typeof item === 'string') {
|
|
2891
|
-
const res = performSanitization(item,
|
|
3693
|
+
const res = performSanitization(item, targetProfile, targetIgnoreList, targetSession);
|
|
2892
3694
|
totalTokensRedacted += Object.keys(res.newTokens || {}).length;
|
|
2893
3695
|
sanitizedChunks.push(res.scrubbedText);
|
|
2894
3696
|
} else if (item && typeof item === 'object') {
|
|
2895
3697
|
const copy = { ...item };
|
|
2896
3698
|
const rawText = copy.text || copy.pageContent || copy.document || '';
|
|
2897
|
-
const res = performSanitization(rawText,
|
|
3699
|
+
const res = performSanitization(rawText, targetProfile, targetIgnoreList, targetSession);
|
|
2898
3700
|
totalTokensRedacted += Object.keys(res.newTokens || {}).length;
|
|
2899
3701
|
|
|
2900
3702
|
if (copy.text !== undefined) copy.text = res.scrubbedText;
|
|
@@ -3183,7 +3985,7 @@ function detectSecrets(text) {
|
|
|
3183
3985
|
return detected;
|
|
3184
3986
|
}
|
|
3185
3987
|
|
|
3186
|
-
function performSanitization(text, profile, ignoreList = null, customSessionMap = null) {
|
|
3988
|
+
export function performSanitization(text, profile, ignoreList = null, customSessionMap = null) {
|
|
3187
3989
|
const customRules = loadCustomRules();
|
|
3188
3990
|
const normalizedProfile = (profile || "general").trim().toLowerCase();
|
|
3189
3991
|
const license = checkLicenseStatus();
|
|
@@ -3211,7 +4013,7 @@ function performSanitization(text, profile, ignoreList = null, customSessionMap
|
|
|
3211
4013
|
return { scrubbedText: result.scrubbedText, newTokens };
|
|
3212
4014
|
}
|
|
3213
4015
|
|
|
3214
|
-
function truncateIfFree(text, isPro, charLimit = 15000) {
|
|
4016
|
+
export function truncateIfFree(text, isPro, charLimit = 15000) {
|
|
3215
4017
|
if (!isPro && text.length > charLimit) {
|
|
3216
4018
|
mcpLog(`${colors.yellowBold}[PrivacyScrubber] Input truncated to ${charLimit.toLocaleString()} characters (Free Tier Limit).${colors.reset}\n${colors.cyan}[Upgrade] Set PRIVACYSCRUBBER_KEY to your PRO/TEAMS/SDK license key for unlimited throughput: https://privacyscrubber.com/pricing?tier=teams&utm_source=mcp&utm_medium=truncation&utm_campaign=teams_upgrade${colors.reset}\n`);
|
|
3217
4019
|
const upsellNotice = `\n\n[PrivacyScrubber Free Tier: Payload truncated to ${charLimit.toLocaleString()} chars. Upgrade to PRO ($15/mo or $110 Lifetime) or Developer SDK ($299/mo) for unlimited payload processing: https://privacyscrubber.com/pricing?tier=teams&utm_source=mcp&utm_medium=truncation&utm_campaign=teams_upgrade]`;
|