agentshield-sdk 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/LICENSE +21 -0
  3. package/README.md +975 -0
  4. package/bin/agent-shield.js +680 -0
  5. package/package.json +118 -0
  6. package/src/adaptive.js +330 -0
  7. package/src/agent-protocol.js +998 -0
  8. package/src/alert-tuning.js +480 -0
  9. package/src/allowlist.js +603 -0
  10. package/src/audit-immutable.js +914 -0
  11. package/src/audit-streaming.js +469 -0
  12. package/src/badges.js +196 -0
  13. package/src/behavior-profiling.js +289 -0
  14. package/src/benchmark-harness.js +804 -0
  15. package/src/canary.js +271 -0
  16. package/src/certification.js +563 -0
  17. package/src/circuit-breaker.js +321 -0
  18. package/src/compliance.js +617 -0
  19. package/src/confidence-tuning.js +324 -0
  20. package/src/confused-deputy.js +624 -0
  21. package/src/context-scoring.js +360 -0
  22. package/src/conversation.js +494 -0
  23. package/src/cost-optimizer.js +1024 -0
  24. package/src/ctf.js +462 -0
  25. package/src/detector-core.js +1999 -0
  26. package/src/distributed.js +359 -0
  27. package/src/document-scanner.js +795 -0
  28. package/src/embedding.js +307 -0
  29. package/src/encoding.js +429 -0
  30. package/src/enterprise.js +405 -0
  31. package/src/errors.js +100 -0
  32. package/src/eu-ai-act.js +523 -0
  33. package/src/fuzzer.js +764 -0
  34. package/src/honeypot.js +328 -0
  35. package/src/i18n-patterns.js +523 -0
  36. package/src/index.js +430 -0
  37. package/src/integrations.js +528 -0
  38. package/src/llm-redteam.js +670 -0
  39. package/src/main.js +741 -0
  40. package/src/main.mjs +38 -0
  41. package/src/mcp-bridge.js +542 -0
  42. package/src/mcp-certification.js +846 -0
  43. package/src/mcp-sdk-integration.js +355 -0
  44. package/src/mcp-security-runtime.js +741 -0
  45. package/src/mcp-server.js +740 -0
  46. package/src/middleware.js +208 -0
  47. package/src/model-finetuning.js +884 -0
  48. package/src/model-fingerprint.js +1042 -0
  49. package/src/multi-agent-trust.js +453 -0
  50. package/src/multi-agent.js +404 -0
  51. package/src/multimodal.js +296 -0
  52. package/src/nist-mapping.js +505 -0
  53. package/src/observability.js +330 -0
  54. package/src/openclaw.js +450 -0
  55. package/src/otel.js +544 -0
  56. package/src/owasp-2025.js +483 -0
  57. package/src/pii.js +390 -0
  58. package/src/plugin-marketplace.js +628 -0
  59. package/src/plugin-system.js +349 -0
  60. package/src/policy-dsl.js +775 -0
  61. package/src/policy-extended.js +635 -0
  62. package/src/policy.js +443 -0
  63. package/src/presets.js +409 -0
  64. package/src/production.js +557 -0
  65. package/src/prompt-leakage.js +321 -0
  66. package/src/rag-vulnerability.js +579 -0
  67. package/src/redteam.js +475 -0
  68. package/src/response-handler.js +429 -0
  69. package/src/scanners.js +357 -0
  70. package/src/self-healing.js +363 -0
  71. package/src/semantic.js +339 -0
  72. package/src/shield-score.js +250 -0
  73. package/src/sso-saml.js +897 -0
  74. package/src/stream-scanner.js +806 -0
  75. package/src/testing.js +505 -0
  76. package/src/threat-encyclopedia.js +629 -0
  77. package/src/threat-intel-network.js +1017 -0
  78. package/src/token-analysis.js +467 -0
  79. package/src/tool-guard.js +412 -0
  80. package/src/tool-output-validator.js +354 -0
  81. package/src/utils.js +83 -0
  82. package/src/watermark.js +235 -0
  83. package/src/worker-scanner.js +601 -0
  84. package/types/index.d.ts +2088 -0
@@ -0,0 +1,741 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Agent Shield — MCP Security Runtime
5
+ *
6
+ * The unified security layer for Model Context Protocol (MCP) servers.
7
+ * Connects authorization, threat scanning, behavioral monitoring, and
8
+ * audit logging into a single runtime that can be added with one line.
9
+ *
10
+ * Addresses all four IAM gaps from the Meta rogue AI agent incident:
11
+ * 1. Inter-agent identity verification (via agent-protocol.js)
12
+ * 2. Post-authentication intent validation (via confused-deputy.js)
13
+ * 3. Ephemeral, scoped credentials (via confused-deputy.js)
14
+ * 4. Per-user MCP tool authorization (this module)
15
+ *
16
+ * Usage:
17
+ * const { MCPSecurityRuntime } = require('agent-shield');
18
+ * const runtime = new MCPSecurityRuntime({ signingKey: process.env.SHIELD_KEY });
19
+ * // One-line integration:
20
+ * const secured = runtime.createMiddleware();
21
+ *
22
+ * All processing runs locally — no data ever leaves your environment.
23
+ */
24
+
25
+ const crypto = require('crypto');
26
+ const { MCPBridge, MCPSessionGuard, MCPResourceScanner, MCPToolPolicy } = require('./mcp-bridge');
27
+ const { AuthorizationContext, ConfusedDeputyGuard } = require('./confused-deputy');
28
+ const { BehaviorProfile } = require('./behavior-profiling');
29
+
30
+ const LOG_PREFIX = '[Agent Shield]';
31
+
32
+ // =========================================================================
33
+ // MCP Session State Machine — prevents tool ordering attacks
34
+ // =========================================================================
35
+
36
+ /** Valid session states and their allowed transitions. */
37
+ const SESSION_STATES = Object.freeze({
38
+ initialized: ['authenticated', 'terminated'],
39
+ authenticated: ['active', 'terminated'],
40
+ active: ['active', 'suspended', 'terminated'],
41
+ suspended: ['active', 'terminated'],
42
+ terminated: []
43
+ });
44
+
45
+ /**
46
+ * Tracks MCP session state to prevent tool call ordering attacks.
47
+ * Detects sequences like: skip_auth → execute → destroy_logs.
48
+ */
49
+ class MCPSessionStateMachine {
50
+ /**
51
+ * @param {string} sessionId
52
+ */
53
+ constructor(sessionId) {
54
+ this.sessionId = sessionId;
55
+ this.state = 'initialized';
56
+ this.transitions = [];
57
+ this.createdAt = Date.now();
58
+ }
59
+
60
+ /**
61
+ * Transitions to a new state if allowed.
62
+ * @param {string} newState
63
+ * @returns {{ allowed: boolean, from: string, to: string, reason?: string }}
64
+ */
65
+ transition(newState) {
66
+ const allowed = SESSION_STATES[this.state];
67
+ if (!allowed || !allowed.includes(newState)) {
68
+ return {
69
+ allowed: false,
70
+ from: this.state,
71
+ to: newState,
72
+ reason: `Invalid transition: ${this.state} → ${newState}`
73
+ };
74
+ }
75
+ const from = this.state;
76
+ this.state = newState;
77
+ this.transitions.push({ from, to: newState, timestamp: Date.now() });
78
+ return { allowed: true, from, to: newState };
79
+ }
80
+
81
+ /** @returns {boolean} */
82
+ isTerminated() {
83
+ return this.state === 'terminated';
84
+ }
85
+ }
86
+
87
+ // =========================================================================
88
+ // MCP Security Runtime — the unified security layer
89
+ // =========================================================================
90
+
91
+ /**
92
+ * Production-ready security runtime for MCP servers.
93
+ * Integrates scanning, authorization, behavior monitoring, and audit
94
+ * into a single coherent layer.
95
+ */
96
+ class MCPSecurityRuntime {
97
+ /**
98
+ * @param {object} [options]
99
+ * @param {string} [options.signingKey] - HMAC key for auth context signing
100
+ * @param {boolean} [options.enforceAuth=true] - Require auth context on all calls
101
+ * @param {boolean} [options.enableBehaviorMonitoring=true] - Track behavioral anomalies
102
+ * @param {boolean} [options.enableStateMachine=true] - Enforce session state transitions
103
+ * @param {number} [options.maxSessionsPerUser=10] - Max concurrent sessions per user
104
+ * @param {number} [options.sessionTtlMs=3600000] - Session timeout (default 1 hour)
105
+ * @param {number} [options.maxToolCallsPerSession=100] - Per-session tool call limit
106
+ * @param {number} [options.maxTokenBudget=100000] - Per-session token budget
107
+ * @param {string[]} [options.allowedTools] - Tool whitelist
108
+ * @param {string[]} [options.blockedTools] - Tool blacklist
109
+ * @param {Array} [options.policies] - MCPToolPolicy rules
110
+ * @param {Function} [options.onThreat] - Callback when threat detected
111
+ * @param {Function} [options.onBlock] - Callback when action blocked
112
+ * @param {Function} [options.onAudit] - Callback for all audit events
113
+ */
114
+ constructor(options = {}) {
115
+ this._signingKey = options.signingKey || 'agent-shield-mcp-runtime-key';
116
+ this._enforceAuth = options.enforceAuth !== false;
117
+ this._enableBehavior = options.enableBehaviorMonitoring !== false;
118
+ this._enableStateMachine = options.enableStateMachine !== false;
119
+ this._maxSessionsPerUser = options.maxSessionsPerUser || 10;
120
+ this._maxDelegationDepth = options.maxDelegationDepth || 5;
121
+ this._sessionTtlMs = options.sessionTtlMs || 3600000;
122
+
123
+ // Core components
124
+ this._bridge = new MCPBridge({
125
+ allowedTools: options.allowedTools,
126
+ blockedTools: options.blockedTools,
127
+ scanInputs: true,
128
+ scanOutputs: true,
129
+ maxToolCallsPerMinute: options.maxToolCallsPerMinute || 60
130
+ });
131
+
132
+ this._guard = new ConfusedDeputyGuard({
133
+ enforceContext: this._enforceAuth,
134
+ signingKey: this._signingKey
135
+ });
136
+
137
+ this._policy = new MCPToolPolicy(options.policies || []);
138
+ this._resourceScanner = new MCPResourceScanner();
139
+
140
+ // Per-session state
141
+ this._sessions = new Map(); // sessionId → SessionState
142
+ this._userSessions = new Map(); // userId → Set<sessionId>
143
+ this._behaviorProfiles = new Map(); // userId → BehaviorProfile
144
+
145
+ // Callbacks
146
+ this._onThreat = options.onThreat || null;
147
+ this._onBlock = options.onBlock || null;
148
+ this._onAudit = options.onAudit || null;
149
+
150
+ // Audit log
151
+ this._auditLog = [];
152
+ this._maxAuditEntries = options.maxAuditEntries || 10000;
153
+
154
+ // Runtime stats
155
+ this.stats = {
156
+ sessionsCreated: 0,
157
+ toolCallsProcessed: 0,
158
+ toolCallsBlocked: 0,
159
+ threatsDetected: 0,
160
+ authFailures: 0,
161
+ behaviorAnomalies: 0,
162
+ stateViolations: 0
163
+ };
164
+
165
+ // Cleanup expired sessions periodically
166
+ this._cleanupInterval = setInterval(() => this._purgeExpiredSessions(), 60000);
167
+ if (this._cleanupInterval.unref) this._cleanupInterval.unref();
168
+ }
169
+
170
+ // =======================================================================
171
+ // Session Management
172
+ // =======================================================================
173
+
174
+ /**
175
+ * Creates a new authenticated MCP session.
176
+ * @param {object} params
177
+ * @param {string} params.userId - Authenticated user identity
178
+ * @param {string} params.agentId - Agent identity
179
+ * @param {string[]} [params.roles] - User roles
180
+ * @param {string[]} [params.scopes] - Granted scopes
181
+ * @param {string} [params.intent] - Declared session intent
182
+ * @returns {{ sessionId: string, authCtx: AuthorizationContext }}
183
+ */
184
+ createSession(params) {
185
+ if (!params.userId || !params.agentId) {
186
+ throw new Error(`${LOG_PREFIX} createSession requires userId and agentId`);
187
+ }
188
+
189
+ // Enforce per-user session limit
190
+ const userSessions = this._userSessions.get(params.userId) || new Set();
191
+ if (userSessions.size >= this._maxSessionsPerUser) {
192
+ this._audit('session_denied', { userId: params.userId, reason: 'max_sessions_exceeded' });
193
+ throw new Error(`${LOG_PREFIX} Max sessions (${this._maxSessionsPerUser}) exceeded for user`);
194
+ }
195
+
196
+ const sessionId = crypto.randomUUID();
197
+ const authCtx = new AuthorizationContext({
198
+ userId: params.userId,
199
+ agentId: params.agentId,
200
+ roles: params.roles,
201
+ scopes: params.scopes,
202
+ intent: params.intent,
203
+ ttlMs: this._sessionTtlMs,
204
+ signingKey: this._signingKey
205
+ });
206
+
207
+ const session = {
208
+ sessionId,
209
+ authCtx,
210
+ guard: new MCPSessionGuard(sessionId, {
211
+ maxToolCalls: params.maxToolCalls || 100,
212
+ maxTokenBudget: params.maxTokenBudget || 100000,
213
+ allowedTools: params.allowedTools
214
+ }),
215
+ stateMachine: this._enableStateMachine ? new MCPSessionStateMachine(sessionId) : null,
216
+ createdAt: Date.now(),
217
+ lastActivity: Date.now(),
218
+ toolCallCount: 0
219
+ };
220
+
221
+ // Transition to authenticated state
222
+ if (session.stateMachine) {
223
+ session.stateMachine.transition('authenticated');
224
+ session.stateMachine.transition('active');
225
+ }
226
+
227
+ this._sessions.set(sessionId, session);
228
+ userSessions.add(sessionId);
229
+ this._userSessions.set(params.userId, userSessions);
230
+ this.stats.sessionsCreated++;
231
+
232
+ // Initialize behavior profile for user if needed
233
+ if (this._enableBehavior && !this._behaviorProfiles.has(params.userId)) {
234
+ this._behaviorProfiles.set(params.userId, new BehaviorProfile({
235
+ windowSize: 200,
236
+ learningPeriod: 10,
237
+ anomalyThreshold: 2.5
238
+ }));
239
+ }
240
+
241
+ this._audit('session_created', { sessionId, userId: params.userId, agentId: params.agentId });
242
+ return { sessionId, authCtx };
243
+ }
244
+
245
+ /**
246
+ * Terminates a session and cleans up resources.
247
+ * @param {string} sessionId
248
+ * @returns {boolean} True if session was found and terminated
249
+ */
250
+ terminateSession(sessionId) {
251
+ const session = this._sessions.get(sessionId);
252
+ if (!session || !session.authCtx) return false;
253
+
254
+ if (session.stateMachine) {
255
+ session.stateMachine.transition('terminated');
256
+ }
257
+
258
+ // Cascade: terminate child sessions first
259
+ const childIds = [];
260
+ for (const [id, s] of this._sessions) {
261
+ if (s.parentSessionId === sessionId) childIds.push(id);
262
+ }
263
+ for (const childId of childIds) {
264
+ this.terminateSession(childId);
265
+ }
266
+
267
+ // Remove from user sessions
268
+ const userId = session.authCtx.userId;
269
+ const userSessions = this._userSessions.get(userId);
270
+ if (userSessions) {
271
+ userSessions.delete(sessionId);
272
+ if (userSessions.size === 0) {
273
+ this._userSessions.delete(userId);
274
+ this._behaviorProfiles.delete(userId);
275
+ }
276
+ }
277
+
278
+ this._sessions.delete(sessionId);
279
+ this._audit('session_terminated', { sessionId, userId, toolCalls: session.toolCallCount });
280
+ return true;
281
+ }
282
+
283
+ // =======================================================================
284
+ // Tool Call Security — the core product
285
+ // =======================================================================
286
+
287
+ /**
288
+ * Secures an MCP tool call with full authorization, scanning, and monitoring.
289
+ * This is the primary API — every tool call flows through here.
290
+ *
291
+ * @param {string} sessionId - Active session ID
292
+ * @param {string} toolName - MCP tool being invoked
293
+ * @param {object} [args={}] - Tool arguments
294
+ * @returns {{ allowed: boolean, threats: Array, violations: Array, anomalies: Array, token: object|null, reason?: string }}
295
+ */
296
+ secureToolCall(sessionId, toolName, args = {}) {
297
+ const startTime = Date.now();
298
+ this.stats.toolCallsProcessed++;
299
+
300
+ // 1. Validate session
301
+ const session = this._sessions.get(sessionId);
302
+ if (!session) {
303
+ this.stats.authFailures++;
304
+ this._audit('tool_blocked', { sessionId, toolName, reason: 'invalid_session' });
305
+ return this._blocked('Invalid or expired session', toolName);
306
+ }
307
+
308
+ // Update activity timestamp
309
+ session.lastActivity = Date.now();
310
+
311
+ // 2. Check session state machine
312
+ if (session.stateMachine && session.stateMachine.isTerminated()) {
313
+ this.stats.stateViolations++;
314
+ this._audit('tool_blocked', { sessionId, toolName, reason: 'session_terminated' });
315
+ return this._blocked('Session has been terminated', toolName);
316
+ }
317
+
318
+ // 3. Check session budget (rate limiting)
319
+ const budgetCheck = session.guard.trackToolCall(toolName, args);
320
+ if (!budgetCheck.allowed) {
321
+ this._audit('tool_blocked', { sessionId, toolName, reason: budgetCheck.reason });
322
+ if (this._onBlock) this._onBlock({ sessionId, toolName, reason: budgetCheck.reason });
323
+ return this._blocked(budgetCheck.reason, toolName);
324
+ }
325
+
326
+ // 4. Authorization check (confused deputy prevention)
327
+ const authResult = this._guard.wrapToolCall(toolName, args, session.authCtx);
328
+ if (!authResult.allowed) {
329
+ this.stats.toolCallsBlocked++;
330
+ this.stats.authFailures++;
331
+ this._audit('auth_denied', {
332
+ sessionId, toolName, userId: session.authCtx.userId,
333
+ violations: authResult.violations
334
+ });
335
+ if (this._onBlock) this._onBlock({ sessionId, toolName, violations: authResult.violations });
336
+ return {
337
+ allowed: false,
338
+ threats: [],
339
+ violations: authResult.violations,
340
+ anomalies: [],
341
+ token: null,
342
+ reason: 'Authorization denied: ' + authResult.violations.map(v => v.message).join('; ')
343
+ };
344
+ }
345
+
346
+ // 5. Policy evaluation
347
+ const policyResult = this._policy.evaluate(toolName, args, {
348
+ userId: session.authCtx.userId,
349
+ roles: [...session.authCtx.roles],
350
+ scopes: [...session.authCtx.scopes]
351
+ });
352
+ if (policyResult.action === 'deny') {
353
+ this._audit('policy_denied', { sessionId, toolName, rule: policyResult.matchedRule });
354
+ return this._blocked(`Policy denied: ${policyResult.reason}`, toolName);
355
+ }
356
+
357
+ // 6. Threat scanning (injection, exfiltration, etc.)
358
+ const scanResult = this._bridge.wrapToolCall(toolName, args);
359
+ const threats = scanResult.threats || [];
360
+ if (!scanResult.allowed) {
361
+ this.stats.toolCallsBlocked++;
362
+ this.stats.threatsDetected += threats.length;
363
+ this._audit('threat_detected', { sessionId, toolName, threats });
364
+ if (this._onThreat) this._onThreat({ sessionId, toolName, threats });
365
+ return {
366
+ allowed: false,
367
+ threats,
368
+ violations: [],
369
+ anomalies: [],
370
+ token: null,
371
+ reason: 'Threat detected: ' + threats.map(t => t.category || t.type).join(', ')
372
+ };
373
+ }
374
+
375
+ // 7. Behavioral anomaly detection
376
+ const anomalies = [];
377
+ if (this._enableBehavior) {
378
+ const profile = this._behaviorProfiles.get(session.authCtx.userId);
379
+ if (profile) {
380
+ const elapsed = Date.now() - startTime;
381
+ session.toolCallCount++;
382
+ const observation = profile.record({
383
+ responseTimeMs: elapsed,
384
+ toolsCalled: [toolName],
385
+ threatScore: threats.length > 0 ? threats.reduce((s, t) => s + (t.severity === 'critical' ? 1 : t.severity === 'high' ? 0.7 : 0.3), 0) : 0
386
+ });
387
+ if (observation.anomalies && observation.anomalies.length > 0) {
388
+ anomalies.push(...observation.anomalies);
389
+ this.stats.behaviorAnomalies += observation.anomalies.length;
390
+ this._audit('behavior_anomaly', {
391
+ sessionId, toolName, userId: session.authCtx.userId,
392
+ anomalies: observation.anomalies
393
+ });
394
+ }
395
+ }
396
+ }
397
+
398
+ // 8. Record success and return
399
+ this._audit('tool_allowed', {
400
+ sessionId, toolName, userId: session.authCtx.userId,
401
+ threats: threats.length, anomalies: anomalies.length
402
+ });
403
+
404
+ return {
405
+ allowed: true,
406
+ threats,
407
+ violations: [],
408
+ anomalies,
409
+ token: authResult.token,
410
+ sanitizedArgs: scanResult.sanitizedArgs
411
+ };
412
+ }
413
+
414
+ /**
415
+ * Scans tool output/result before returning to the user.
416
+ * @param {string} sessionId
417
+ * @param {string} toolName
418
+ * @param {*} result
419
+ * @returns {{ safe: boolean, threats: Array, sanitizedResult: * }}
420
+ */
421
+ secureToolResult(sessionId, toolName, result) {
422
+ const session = this._sessions.get(sessionId);
423
+ if (!session) {
424
+ return { safe: false, threats: [{ type: 'invalid_session', message: 'Unknown session' }] };
425
+ }
426
+
427
+ const scanResult = this._bridge.wrapToolResult(toolName, result);
428
+ if (!scanResult.safe) {
429
+ this.stats.threatsDetected += (scanResult.threats || []).length;
430
+ this._audit('output_threat', { sessionId, toolName, threats: scanResult.threats });
431
+ if (this._onThreat) this._onThreat({ sessionId, toolName, threats: scanResult.threats, direction: 'output' });
432
+ }
433
+ return scanResult;
434
+ }
435
+
436
+ /**
437
+ * Scans an MCP resource before making it available.
438
+ * @param {string} uri
439
+ * @param {string} content
440
+ * @param {string} [mimeType='text/plain']
441
+ * @returns {{ safe: boolean, threats: Array }}
442
+ */
443
+ secureResource(uri, content, mimeType) {
444
+ return this._resourceScanner.scanResource(uri, content, mimeType);
445
+ }
446
+
447
+ // =======================================================================
448
+ // Tool Registration
449
+ // =======================================================================
450
+
451
+ /**
452
+ * Registers a tool with its security requirements.
453
+ * @param {string} toolName
454
+ * @param {object} requirements
455
+ * @param {string[]} [requirements.scopes] - Required scopes
456
+ * @param {string[]} [requirements.roles] - Required roles
457
+ * @param {boolean} [requirements.requiresHumanApproval] - HITL gate
458
+ * @param {string[]} [requirements.allowedIntents] - Allowed intents
459
+ */
460
+ registerTool(toolName, requirements = {}) {
461
+ this._guard.registerTool(toolName, requirements);
462
+ }
463
+
464
+ /**
465
+ * Adds a policy rule.
466
+ * @param {object} rule - MCPToolPolicy rule
467
+ */
468
+ addPolicy(rule) {
469
+ this._policy.addRule(rule);
470
+ }
471
+
472
+ // =======================================================================
473
+ // One-Line Middleware
474
+ // =======================================================================
475
+
476
+ /**
477
+ * Creates middleware handlers for MCP server integration.
478
+ * Drop-in for any MCP server implementation.
479
+ *
480
+ * @returns {object} Middleware with onToolCall, onToolResult, onResourceAccess, createSession, terminateSession
481
+ */
482
+ createMiddleware() {
483
+ const runtime = this;
484
+ return {
485
+ /**
486
+ * Creates an authenticated session. Call once per connection.
487
+ * @param {object} params - { userId, agentId, roles, scopes, intent }
488
+ * @returns {{ sessionId: string, authCtx: AuthorizationContext }}
489
+ */
490
+ createSession(params) {
491
+ return runtime.createSession(params);
492
+ },
493
+
494
+ /**
495
+ * Secures a tool call. Call for every tools/call request.
496
+ * @param {string} sessionId
497
+ * @param {string} toolName
498
+ * @param {object} args
499
+ * @returns {{ allowed: boolean, threats: Array, violations: Array, anomalies: Array }}
500
+ */
501
+ onToolCall(sessionId, toolName, args) {
502
+ return runtime.secureToolCall(sessionId, toolName, args);
503
+ },
504
+
505
+ /**
506
+ * Scans tool output before returning. Call for every tool response.
507
+ * @param {string} sessionId
508
+ * @param {string} toolName
509
+ * @param {*} result
510
+ * @returns {{ safe: boolean, threats: Array }}
511
+ */
512
+ onToolResult(sessionId, toolName, result) {
513
+ return runtime.secureToolResult(sessionId, toolName, result);
514
+ },
515
+
516
+ /**
517
+ * Scans MCP resources. Call for resources/read requests.
518
+ * @param {string} uri
519
+ * @param {string} content
520
+ * @param {string} mimeType
521
+ * @returns {{ safe: boolean, threats: Array }}
522
+ */
523
+ onResourceAccess(uri, content, mimeType) {
524
+ return runtime.secureResource(uri, content, mimeType);
525
+ },
526
+
527
+ /**
528
+ * Terminates a session. Call on connection close.
529
+ * @param {string} sessionId
530
+ */
531
+ terminateSession(sessionId) {
532
+ return runtime.terminateSession(sessionId);
533
+ },
534
+
535
+ /** Get runtime stats */
536
+ getStats() {
537
+ return runtime.getReport();
538
+ }
539
+ };
540
+ }
541
+
542
+ // =======================================================================
543
+ // Delegation — secure agent-to-agent handoff
544
+ // =======================================================================
545
+
546
+ /**
547
+ * Delegates a session's authorization to a sub-agent with narrowed scopes.
548
+ * @param {string} sessionId - Parent session
549
+ * @param {string} delegateAgentId - Sub-agent receiving delegation
550
+ * @param {string[]} [delegateScopes] - Subset of parent scopes
551
+ * @returns {{ sessionId: string, authCtx: AuthorizationContext }}
552
+ */
553
+ delegateSession(sessionId, delegateAgentId, delegateScopes) {
554
+ const parentSession = this._sessions.get(sessionId);
555
+ if (!parentSession) {
556
+ throw new Error(`${LOG_PREFIX} Cannot delegate: invalid session`);
557
+ }
558
+
559
+ // Enforce delegation depth limit
560
+ if ((parentSession.authCtx.delegationDepth || 0) >= this._maxDelegationDepth) {
561
+ throw new Error(`${LOG_PREFIX} Cannot delegate: max delegation depth (${this._maxDelegationDepth}) exceeded`);
562
+ }
563
+
564
+ // Enforce per-user session limit for delegated sessions too
565
+ const userSessions = this._userSessions.get(parentSession.authCtx.userId) || new Set();
566
+ if (userSessions.size >= this._maxSessionsPerUser) {
567
+ throw new Error(`${LOG_PREFIX} Cannot delegate: max sessions (${this._maxSessionsPerUser}) exceeded for user`);
568
+ }
569
+
570
+ const childCtx = parentSession.authCtx.delegate(delegateAgentId, delegateScopes);
571
+ const childSessionId = crypto.randomUUID();
572
+
573
+ const childSession = {
574
+ sessionId: childSessionId,
575
+ authCtx: childCtx,
576
+ guard: new MCPSessionGuard(childSessionId, {
577
+ maxToolCalls: 50, // Delegates get tighter budgets
578
+ maxTokenBudget: 50000
579
+ }),
580
+ stateMachine: this._enableStateMachine ? new MCPSessionStateMachine(childSessionId) : null,
581
+ createdAt: Date.now(),
582
+ lastActivity: Date.now(),
583
+ toolCallCount: 0,
584
+ parentSessionId: sessionId
585
+ };
586
+
587
+ if (childSession.stateMachine) {
588
+ childSession.stateMachine.transition('authenticated');
589
+ childSession.stateMachine.transition('active');
590
+ }
591
+
592
+ this._sessions.set(childSessionId, childSession);
593
+
594
+ // Track under same user (reuse userSessions from limit check above)
595
+ userSessions.add(childSessionId);
596
+ this._userSessions.set(parentSession.authCtx.userId, userSessions);
597
+
598
+ this._audit('session_delegated', {
599
+ parentSessionId: sessionId,
600
+ childSessionId,
601
+ delegateAgentId,
602
+ delegateScopes: childCtx.scopes,
603
+ delegationDepth: childCtx.delegationDepth
604
+ });
605
+
606
+ return { sessionId: childSessionId, authCtx: childCtx };
607
+ }
608
+
609
+ // =======================================================================
610
+ // Reporting & Observability
611
+ // =======================================================================
612
+
613
+ /**
614
+ * Returns comprehensive runtime report.
615
+ * @returns {object}
616
+ */
617
+ getReport() {
618
+ const sessions = [];
619
+ for (const [id, session] of this._sessions) {
620
+ sessions.push({
621
+ sessionId: id,
622
+ userId: session.authCtx.userId,
623
+ agentId: session.authCtx.agentId,
624
+ state: session.stateMachine ? session.stateMachine.state : 'active',
625
+ toolCalls: session.toolCallCount,
626
+ age: Date.now() - session.createdAt,
627
+ budget: session.guard.checkBudget()
628
+ });
629
+ }
630
+
631
+ const behaviorSummaries = {};
632
+ for (const [userId, profile] of this._behaviorProfiles) {
633
+ behaviorSummaries[userId] = profile.getReport();
634
+ }
635
+
636
+ return {
637
+ stats: { ...this.stats },
638
+ activeSessions: this._sessions.size,
639
+ sessions,
640
+ behaviorProfiles: behaviorSummaries,
641
+ guard: this._guard.getStats(),
642
+ recentAudit: this._auditLog.slice(-50)
643
+ };
644
+ }
645
+
646
+ /**
647
+ * Returns the full audit log.
648
+ * @param {number} [limit=100]
649
+ * @returns {Array}
650
+ */
651
+ getAuditLog(limit = 100) {
652
+ return this._auditLog.slice(-limit);
653
+ }
654
+
655
+ /**
656
+ * Returns behavior profile for a specific user.
657
+ * @param {string} userId
658
+ * @returns {object|null}
659
+ */
660
+ getBehaviorProfile(userId) {
661
+ const profile = this._behaviorProfiles.get(userId);
662
+ return profile ? profile.getReport() : null;
663
+ }
664
+
665
+ // =======================================================================
666
+ // Cleanup
667
+ // =======================================================================
668
+
669
+ /**
670
+ * Shuts down the runtime and cleans up resources.
671
+ */
672
+ shutdown() {
673
+ if (this._cleanupInterval) {
674
+ clearInterval(this._cleanupInterval);
675
+ this._cleanupInterval = null;
676
+ }
677
+ const sessionIds = [...this._sessions.keys()];
678
+ for (const sessionId of sessionIds) {
679
+ this.terminateSession(sessionId);
680
+ }
681
+ this._audit('runtime_shutdown', { totalProcessed: this.stats.toolCallsProcessed });
682
+ }
683
+
684
+ // =======================================================================
685
+ // Internal
686
+ // =======================================================================
687
+
688
+ /** @private */
689
+ _blocked(reason, toolName) {
690
+ this.stats.toolCallsBlocked++;
691
+ return {
692
+ allowed: false,
693
+ threats: [],
694
+ violations: [{ type: 'blocked', message: reason, tool: toolName }],
695
+ anomalies: [],
696
+ token: null,
697
+ reason
698
+ };
699
+ }
700
+
701
+ /** @private */
702
+ _audit(type, data) {
703
+ const entry = {
704
+ type,
705
+ timestamp: Date.now(),
706
+ eventId: crypto.randomUUID(),
707
+ ...data
708
+ };
709
+ if (this._auditLog.length >= this._maxAuditEntries) {
710
+ this._auditLog = this._auditLog.slice(-Math.floor(this._maxAuditEntries * 0.75));
711
+ }
712
+ this._auditLog.push(entry);
713
+ if (this._onAudit) {
714
+ try { this._onAudit(entry); } catch (_e) { /* callback errors should not break the runtime */ }
715
+ }
716
+ }
717
+
718
+ /** @private */
719
+ _purgeExpiredSessions() {
720
+ const now = Date.now();
721
+ const expiredIds = [];
722
+ for (const [sessionId, session] of this._sessions) {
723
+ const expired = session.authCtx.isExpired() ||
724
+ (now - session.lastActivity > this._sessionTtlMs);
725
+ if (expired) expiredIds.push(sessionId);
726
+ }
727
+ for (const sessionId of expiredIds) {
728
+ this.terminateSession(sessionId);
729
+ }
730
+ }
731
+ }
732
+
733
+ // =========================================================================
734
+ // Exports
735
+ // =========================================================================
736
+
737
+ module.exports = {
738
+ MCPSecurityRuntime,
739
+ MCPSessionStateMachine,
740
+ SESSION_STATES
741
+ };