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/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 > 1_000_000) {
117
- console.warn('[Agent Shield] Input exceeds 1MB consider scanning in chunks');
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 > 100) {
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 (!toolName || typeof toolName !== 'string') {
236
- return { status: 'safe', toolName: toolName || '', threats: [], warnings: ['Invalid tool name'], blocked: false, isDangerousTool: false, timestamp: Date.now() };
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 = 10) {
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 = 10) {
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
- const sessionIds = [...this._sessions.keys()];
822
- for (const sessionId of sessionIds) {
823
- this.terminateSession(sessionId);
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
- process.on('SIGINT', () => {
731
- server.stop();
732
- process.exit(0);
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 };