agentshield-sdk 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +191 -0
- package/LICENSE +21 -0
- package/README.md +975 -0
- package/bin/agent-shield.js +680 -0
- package/package.json +118 -0
- package/src/adaptive.js +330 -0
- package/src/agent-protocol.js +998 -0
- package/src/alert-tuning.js +480 -0
- package/src/allowlist.js +603 -0
- package/src/audit-immutable.js +914 -0
- package/src/audit-streaming.js +469 -0
- package/src/badges.js +196 -0
- package/src/behavior-profiling.js +289 -0
- package/src/benchmark-harness.js +804 -0
- package/src/canary.js +271 -0
- package/src/certification.js +563 -0
- package/src/circuit-breaker.js +321 -0
- package/src/compliance.js +617 -0
- package/src/confidence-tuning.js +324 -0
- package/src/confused-deputy.js +624 -0
- package/src/context-scoring.js +360 -0
- package/src/conversation.js +494 -0
- package/src/cost-optimizer.js +1024 -0
- package/src/ctf.js +462 -0
- package/src/detector-core.js +1999 -0
- package/src/distributed.js +359 -0
- package/src/document-scanner.js +795 -0
- package/src/embedding.js +307 -0
- package/src/encoding.js +429 -0
- package/src/enterprise.js +405 -0
- package/src/errors.js +100 -0
- package/src/eu-ai-act.js +523 -0
- package/src/fuzzer.js +764 -0
- package/src/honeypot.js +328 -0
- package/src/i18n-patterns.js +523 -0
- package/src/index.js +430 -0
- package/src/integrations.js +528 -0
- package/src/llm-redteam.js +670 -0
- package/src/main.js +741 -0
- package/src/main.mjs +38 -0
- package/src/mcp-bridge.js +542 -0
- package/src/mcp-certification.js +846 -0
- package/src/mcp-sdk-integration.js +355 -0
- package/src/mcp-security-runtime.js +741 -0
- package/src/mcp-server.js +740 -0
- package/src/middleware.js +208 -0
- package/src/model-finetuning.js +884 -0
- package/src/model-fingerprint.js +1042 -0
- package/src/multi-agent-trust.js +453 -0
- package/src/multi-agent.js +404 -0
- package/src/multimodal.js +296 -0
- package/src/nist-mapping.js +505 -0
- package/src/observability.js +330 -0
- package/src/openclaw.js +450 -0
- package/src/otel.js +544 -0
- package/src/owasp-2025.js +483 -0
- package/src/pii.js +390 -0
- package/src/plugin-marketplace.js +628 -0
- package/src/plugin-system.js +349 -0
- package/src/policy-dsl.js +775 -0
- package/src/policy-extended.js +635 -0
- package/src/policy.js +443 -0
- package/src/presets.js +409 -0
- package/src/production.js +557 -0
- package/src/prompt-leakage.js +321 -0
- package/src/rag-vulnerability.js +579 -0
- package/src/redteam.js +475 -0
- package/src/response-handler.js +429 -0
- package/src/scanners.js +357 -0
- package/src/self-healing.js +363 -0
- package/src/semantic.js +339 -0
- package/src/shield-score.js +250 -0
- package/src/sso-saml.js +897 -0
- package/src/stream-scanner.js +806 -0
- package/src/testing.js +505 -0
- package/src/threat-encyclopedia.js +629 -0
- package/src/threat-intel-network.js +1017 -0
- package/src/token-analysis.js +467 -0
- package/src/tool-guard.js +412 -0
- package/src/tool-output-validator.js +354 -0
- package/src/utils.js +83 -0
- package/src/watermark.js +235 -0
- package/src/worker-scanner.js +601 -0
- package/types/index.d.ts +2088 -0
|
@@ -0,0 +1,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
|
+
};
|