agentshield-sdk 7.2.0 → 7.2.1
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/README.md +5 -4
- package/package.json +4 -3
- package/src/circuit-breaker.js +321 -321
- package/src/detector-core.js +3 -3
- package/src/distributed.js +402 -359
- package/src/fuzzer.js +764 -764
- package/src/index.js +23 -7
- package/src/main.js +6 -2
- package/src/mcp-security-runtime.js +30 -5
- package/src/mcp-server.js +12 -8
- package/src/middleware.js +303 -208
- package/src/multi-agent.js +421 -404
- package/src/pii.js +401 -390
- package/src/stream-scanner.js +34 -4
- package/src/testing.js +505 -505
- package/src/utils.js +199 -83
- package/types/index.d.ts +374 -0
package/src/index.js
CHANGED
|
@@ -53,6 +53,15 @@ const DEFAULT_CONFIG = {
|
|
|
53
53
|
/** Custom callback when a threat is detected. */
|
|
54
54
|
onThreat: null,
|
|
55
55
|
|
|
56
|
+
/** Maximum input size in bytes before truncation warning. */
|
|
57
|
+
maxInputSize: 1_000_000,
|
|
58
|
+
|
|
59
|
+
/** Maximum number of scan history entries to retain. */
|
|
60
|
+
maxScanHistory: 100,
|
|
61
|
+
|
|
62
|
+
/** Maximum recursion depth when flattening tool arguments. */
|
|
63
|
+
maxArgDepth: 10,
|
|
64
|
+
|
|
56
65
|
/** Dangerous tool names that should be scrutinized more carefully. */
|
|
57
66
|
dangerousTools: [
|
|
58
67
|
'bash', 'shell', 'terminal', 'exec', 'execute',
|
|
@@ -113,8 +122,8 @@ class AgentShield {
|
|
|
113
122
|
if (typeof text !== 'string') {
|
|
114
123
|
throw new TypeError(`[Agent Shield] scan() expects a string, got ${typeof text}`);
|
|
115
124
|
}
|
|
116
|
-
if (text.length >
|
|
117
|
-
console.warn('[Agent Shield] Input exceeds
|
|
125
|
+
if (text.length > this.config.maxInputSize) {
|
|
126
|
+
console.warn('[Agent Shield] Input exceeds configured maxInputSize - consider scanning in chunks');
|
|
118
127
|
}
|
|
119
128
|
const result = scanText(text, {
|
|
120
129
|
source: options.source || 'unknown',
|
|
@@ -133,7 +142,7 @@ class AgentShield {
|
|
|
133
142
|
threatCount: result.threats.length,
|
|
134
143
|
source: options.source || 'unknown'
|
|
135
144
|
});
|
|
136
|
-
if (this.stats.scanHistory.length >
|
|
145
|
+
if (this.stats.scanHistory.length > this.config.maxScanHistory) {
|
|
137
146
|
this.stats.scanHistory.shift();
|
|
138
147
|
}
|
|
139
148
|
|
|
@@ -197,6 +206,7 @@ class AgentShield {
|
|
|
197
206
|
* @param {object} [options] - Options.
|
|
198
207
|
* @param {string} [options.source='user_input'] - Where the input came from.
|
|
199
208
|
* @returns {object} Scan result with additional `blocked` field.
|
|
209
|
+
* @throws {TypeError} If text is not a string.
|
|
200
210
|
*/
|
|
201
211
|
scanInput(text, options = {}) {
|
|
202
212
|
if (typeof text !== 'string') {
|
|
@@ -214,6 +224,7 @@ class AgentShield {
|
|
|
214
224
|
* @param {object} [options] - Options.
|
|
215
225
|
* @param {string} [options.source='agent_output'] - Source label.
|
|
216
226
|
* @returns {object} Scan result with additional `blocked` field.
|
|
227
|
+
* @throws {TypeError} If text is not a string.
|
|
217
228
|
*/
|
|
218
229
|
scanOutput(text, options = {}) {
|
|
219
230
|
if (typeof text !== 'string') {
|
|
@@ -232,8 +243,11 @@ class AgentShield {
|
|
|
232
243
|
* @returns {object} Scan result with `blocked` and `warnings` fields.
|
|
233
244
|
*/
|
|
234
245
|
scanToolCall(toolName, args = {}, options = {}) {
|
|
235
|
-
if (
|
|
236
|
-
|
|
246
|
+
if (typeof toolName !== 'string') {
|
|
247
|
+
throw new TypeError(`[Agent Shield] scanToolCall() expects toolName to be a string, got ${typeof toolName}`);
|
|
248
|
+
}
|
|
249
|
+
if (!toolName) {
|
|
250
|
+
return { status: 'safe', toolName: '', threats: [], warnings: ['Empty tool name'], blocked: false, isDangerousTool: false, timestamp: Date.now() };
|
|
237
251
|
}
|
|
238
252
|
const warnings = [];
|
|
239
253
|
const allThreats = [];
|
|
@@ -371,7 +385,8 @@ class AgentShield {
|
|
|
371
385
|
* @param {object} args
|
|
372
386
|
* @returns {string}
|
|
373
387
|
*/
|
|
374
|
-
_flattenArgs(args, maxDepth
|
|
388
|
+
_flattenArgs(args, maxDepth) {
|
|
389
|
+
if (maxDepth == null) maxDepth = this.config.maxArgDepth;
|
|
375
390
|
const parts = [];
|
|
376
391
|
const flatten = (obj, depth) => {
|
|
377
392
|
if (depth > maxDepth) return;
|
|
@@ -393,7 +408,8 @@ class AgentShield {
|
|
|
393
408
|
* @param {object} args
|
|
394
409
|
* @returns {Array<string>}
|
|
395
410
|
*/
|
|
396
|
-
_extractFilePaths(args, maxDepth
|
|
411
|
+
_extractFilePaths(args, maxDepth) {
|
|
412
|
+
if (maxDepth == null) maxDepth = this.config.maxArgDepth;
|
|
397
413
|
const paths = [];
|
|
398
414
|
const fileKeys = [
|
|
399
415
|
'file', 'path', 'file_path', 'filepath', 'filename', 'target',
|
package/src/main.js
CHANGED
|
@@ -27,7 +27,7 @@ function safeRequire(path, label) {
|
|
|
27
27
|
// Core (these are critical — if they fail, we still export what we can)
|
|
28
28
|
const { AgentShield } = safeRequire('./index', 'core');
|
|
29
29
|
const { scanText, getPatterns, SEVERITY_ORDER } = safeRequire('./detector-core', 'detector-core');
|
|
30
|
-
const { expressMiddleware, wrapAgent, shieldTools, extractTextFromBody } = safeRequire('./middleware', 'middleware');
|
|
30
|
+
const { expressMiddleware, wrapAgent, shieldTools, extractTextFromBody, rateLimitMiddleware, shieldMiddleware } = safeRequire('./middleware', 'middleware');
|
|
31
31
|
|
|
32
32
|
// Protection
|
|
33
33
|
const { CircuitBreaker, shadowMode, RateLimiter, STATE } = safeRequire('./circuit-breaker', 'circuit-breaker');
|
|
@@ -51,7 +51,7 @@ const { SteganographyDetector, EncodingBruteforceDetector, StructuredDataScanner
|
|
|
51
51
|
const { OutputWatermark, DifferentialPrivacy } = safeRequire('./watermark', 'watermark');
|
|
52
52
|
|
|
53
53
|
// Utilities
|
|
54
|
-
const { getGrade, getGradeLabel, makeBar, truncate, formatHeader, generateId } = safeRequire('./utils', 'utils');
|
|
54
|
+
const { getGrade, getGradeLabel, makeBar, truncate, formatHeader, generateId, createGracefulShutdown, loadEnvFile } = safeRequire('./utils', 'utils');
|
|
55
55
|
|
|
56
56
|
// Error codes & deprecation
|
|
57
57
|
const { ERROR_CODES, createShieldError, deprecationWarning } = safeRequire('./errors', 'errors');
|
|
@@ -288,6 +288,8 @@ const _exports = {
|
|
|
288
288
|
wrapAgent,
|
|
289
289
|
shieldTools,
|
|
290
290
|
extractTextFromBody,
|
|
291
|
+
rateLimitMiddleware,
|
|
292
|
+
shieldMiddleware,
|
|
291
293
|
|
|
292
294
|
// Protection
|
|
293
295
|
CircuitBreaker,
|
|
@@ -343,6 +345,8 @@ const _exports = {
|
|
|
343
345
|
truncate,
|
|
344
346
|
formatHeader,
|
|
345
347
|
generateId,
|
|
348
|
+
createGracefulShutdown,
|
|
349
|
+
loadEnvFile,
|
|
346
350
|
|
|
347
351
|
// Integrations
|
|
348
352
|
ShieldCallbackHandler,
|
|
@@ -812,16 +812,41 @@ class MCPSecurityRuntime {
|
|
|
812
812
|
|
|
813
813
|
/**
|
|
814
814
|
* Shuts down the runtime and cleans up resources.
|
|
815
|
+
* @param {object} [options]
|
|
816
|
+
* @param {number} [options.timeoutMs=10000] - Max time to wait for drain before forced cleanup.
|
|
817
|
+
* @returns {Promise<void>}
|
|
815
818
|
*/
|
|
816
|
-
shutdown() {
|
|
819
|
+
async shutdown(options = {}) {
|
|
820
|
+
const timeoutMs = options.timeoutMs || 10000;
|
|
821
|
+
|
|
817
822
|
if (this._cleanupInterval) {
|
|
818
823
|
clearInterval(this._cleanupInterval);
|
|
819
824
|
this._cleanupInterval = null;
|
|
820
825
|
}
|
|
821
|
-
|
|
822
|
-
for
|
|
823
|
-
|
|
824
|
-
|
|
826
|
+
|
|
827
|
+
// Drain: wait for in-flight operations with timeout
|
|
828
|
+
const drainPromise = new Promise((resolve) => {
|
|
829
|
+
const sessionIds = [...this._sessions.keys()];
|
|
830
|
+
for (const sessionId of sessionIds) {
|
|
831
|
+
try {
|
|
832
|
+
this.terminateSession(sessionId);
|
|
833
|
+
} catch (err) {
|
|
834
|
+
console.error(`${LOG_PREFIX} Error terminating session ${sessionId}: ${err.message}`);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
resolve();
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
841
|
+
const timer = setTimeout(() => {
|
|
842
|
+
console.error(`${LOG_PREFIX} Shutdown drain timeout (${timeoutMs}ms), forcing cleanup`);
|
|
843
|
+
resolve();
|
|
844
|
+
}, timeoutMs);
|
|
845
|
+
if (timer.unref) timer.unref();
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
await Promise.race([drainPromise, timeoutPromise]);
|
|
849
|
+
|
|
825
850
|
this._audit('runtime_shutdown', { totalProcessed: this.stats.toolCallsProcessed });
|
|
826
851
|
}
|
|
827
852
|
|
package/src/mcp-server.js
CHANGED
|
@@ -707,6 +707,11 @@ class MCPServer {
|
|
|
707
707
|
// =========================================================================
|
|
708
708
|
|
|
709
709
|
if (require.main === module) {
|
|
710
|
+
const { createGracefulShutdown, loadEnvFile } = require('./utils');
|
|
711
|
+
|
|
712
|
+
// Load .env file if present
|
|
713
|
+
loadEnvFile();
|
|
714
|
+
|
|
710
715
|
let config = {};
|
|
711
716
|
|
|
712
717
|
// Parse --config flag
|
|
@@ -726,15 +731,14 @@ if (require.main === module) {
|
|
|
726
731
|
const server = new MCPServer(config);
|
|
727
732
|
server.start();
|
|
728
733
|
|
|
729
|
-
// Graceful shutdown
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
});
|
|
734
|
-
process.on('SIGTERM', () => {
|
|
735
|
-
server.stop();
|
|
736
|
-
process.exit(0);
|
|
734
|
+
// Graceful shutdown with timeout enforcement
|
|
735
|
+
const { shutdown } = createGracefulShutdown({
|
|
736
|
+
timeoutMs: parseInt(process.env.SHIELD_SHUTDOWN_TIMEOUT_MS, 10) || 10000,
|
|
737
|
+
cleanupFns: [() => server.stop()]
|
|
737
738
|
});
|
|
739
|
+
|
|
740
|
+
process.on('SIGINT', () => shutdown('SIGINT').then(() => process.exit(0)));
|
|
741
|
+
process.on('SIGTERM', () => shutdown('SIGTERM').then(() => process.exit(0)));
|
|
738
742
|
}
|
|
739
743
|
|
|
740
744
|
module.exports = { MCPServer, MCPToolHandler };
|