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,624 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Agent Shield — Confused Deputy Prevention (Meta Incident Response)
5
+ *
6
+ * Addresses the four IAM gaps exposed by Meta's rogue AI agent incident (March 2026):
7
+ * Gap 2: Post-authentication blindness — validates intent after auth succeeds
8
+ * Gap 3: Static credentials — ephemeral, scoped, auto-rotating tokens
9
+ * Gap 4: Confused deputy via MCP — per-user authorization context propagation
10
+ *
11
+ * Gap 1 (inter-agent identity) is already addressed by agent-protocol.js.
12
+ *
13
+ * References:
14
+ * - VentureBeat: "Meta's rogue AI agent passed every identity check"
15
+ * - OWASP Feb 2026: Practical Guide for Secure MCP Server Development
16
+ * - CVE-2026-27826, CVE-2026-27825 (mcp-atlassian)
17
+ *
18
+ * All processing runs locally — no data ever leaves your environment.
19
+ */
20
+
21
+ const crypto = require('crypto');
22
+
23
+ /** Default HMAC key — callers should override via params.signingKey for production use. */
24
+ const DEFAULT_SIGNING_KEY = 'agent-shield-default-signing-key';
25
+
26
+ // =========================================================================
27
+ // Authorization Context — binds user identity to agent actions
28
+ // =========================================================================
29
+
30
+ /**
31
+ * Immutable authorization context that flows through delegation chains.
32
+ * Ensures every tool call traces back to the originating user + permissions.
33
+ * Uses HMAC-SHA256 signing to prevent context forgery.
34
+ */
35
+ class AuthorizationContext {
36
+ /**
37
+ * @param {object} params
38
+ * @param {string} params.userId - Originating user identity
39
+ * @param {string} params.agentId - Agent performing the action
40
+ * @param {string[]} [params.roles] - User's roles
41
+ * @param {string[]} [params.scopes] - Granted permission scopes
42
+ * @param {string} [params.intent] - Declared intent for this session
43
+ * @param {number} [params.ttlMs=300000] - Context TTL (default 5 min)
44
+ * @param {string} [params.parentContextId] - Parent context for delegation
45
+ * @param {string} [params.signingKey] - HMAC key for tamper-proof signatures
46
+ */
47
+ constructor(params) {
48
+ if (!params.userId) throw new Error('AuthorizationContext requires userId');
49
+ if (!params.agentId) throw new Error('AuthorizationContext requires agentId');
50
+
51
+ this.contextId = crypto.randomUUID();
52
+ this.userId = params.userId;
53
+ this.agentId = params.agentId;
54
+ this.roles = Object.freeze([...(params.roles || [])]);
55
+ this.scopes = Object.freeze([...(params.scopes || [])]);
56
+ this.intent = params.intent || null;
57
+ this.createdAt = Date.now();
58
+ this.expiresAt = this.createdAt + (params.ttlMs !== null && params.ttlMs !== undefined ? params.ttlMs : 300000);
59
+ this.parentContextId = params.parentContextId || null;
60
+ this.delegationDepth = 0;
61
+ this._signingKey = params.signingKey || DEFAULT_SIGNING_KEY;
62
+
63
+ // Sign the context with HMAC to prevent forgery
64
+ this._signature = this._sign();
65
+ }
66
+
67
+ /** @returns {boolean} */
68
+ isExpired() {
69
+ return Date.now() >= this.expiresAt;
70
+ }
71
+
72
+ /** @returns {boolean} */
73
+ hasScope(scope) {
74
+ return this.scopes.includes(scope) || this.scopes.includes('*');
75
+ }
76
+
77
+ /** @returns {boolean} */
78
+ hasRole(role) {
79
+ return this.roles.includes(role) || this.roles.includes('admin');
80
+ }
81
+
82
+ /**
83
+ * Creates a child context for delegation — scopes can only narrow, never widen.
84
+ * @param {string} delegateAgentId
85
+ * @param {string[]} [delegateScopes] - Must be subset of current scopes
86
+ * @returns {AuthorizationContext}
87
+ */
88
+ delegate(delegateAgentId, delegateScopes) {
89
+ if (this.isExpired()) throw new Error('Cannot delegate expired context');
90
+ if (!this.verify()) throw new Error('Context integrity check failed');
91
+
92
+ const narrowedScopes = delegateScopes
93
+ ? delegateScopes.filter(s => this.hasScope(s))
94
+ : [...this.scopes];
95
+
96
+ const child = new AuthorizationContext({
97
+ userId: this.userId,
98
+ agentId: delegateAgentId,
99
+ roles: [...this.roles],
100
+ scopes: narrowedScopes,
101
+ intent: this.intent,
102
+ ttlMs: Math.max(0, this.expiresAt - Date.now()),
103
+ parentContextId: this.contextId,
104
+ signingKey: this._signingKey
105
+ });
106
+ child.delegationDepth = this.delegationDepth + 1;
107
+ child._signature = child._sign();
108
+ return child;
109
+ }
110
+
111
+ /**
112
+ * Verifies context has not been tampered with using timing-safe comparison.
113
+ * @returns {boolean}
114
+ */
115
+ verify() {
116
+ const expected = this._sign();
117
+ try {
118
+ return crypto.timingSafeEqual(
119
+ Buffer.from(this._signature, 'hex'),
120
+ Buffer.from(expected, 'hex')
121
+ );
122
+ } catch {
123
+ return false;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * HMAC-SHA256 signature over all critical context fields.
129
+ * @returns {string} Hex-encoded HMAC
130
+ * @private
131
+ */
132
+ _sign() {
133
+ const data = `${this.contextId}:${this.userId}:${this.agentId}:${this.roles.join(',')}:${this.scopes.join(',')}:${this.expiresAt}:${this.parentContextId || ''}`;
134
+ return crypto.createHmac('sha256', this._signingKey).update(data).digest('hex');
135
+ }
136
+ }
137
+
138
+ // =========================================================================
139
+ // Ephemeral Token Manager — scoped, auto-rotating credentials
140
+ // =========================================================================
141
+
142
+ /**
143
+ * Issues short-lived, scoped tokens that replace static API keys.
144
+ * Tokens are bound to a specific user, agent, and set of actions.
145
+ */
146
+ class EphemeralTokenManager {
147
+ /**
148
+ * @param {object} [options]
149
+ * @param {number} [options.tokenTtlMs=900000] - Token lifetime (default 15 min)
150
+ * @param {number} [options.maxTokensPerUser=10] - Max active tokens per user
151
+ * @param {number} [options.rotationWindowMs=60000] - Grace period after rotation
152
+ */
153
+ constructor(options = {}) {
154
+ this.tokenTtlMs = options.tokenTtlMs || 900000;
155
+ this.maxTokensPerUser = options.maxTokensPerUser || 10;
156
+ this.rotationWindowMs = options.rotationWindowMs || 60000;
157
+ this._signingKey = options.signingKey || DEFAULT_SIGNING_KEY;
158
+ this.tokens = new Map();
159
+ this.userTokens = new Map();
160
+ this.revokedTokens = new Set();
161
+ this.stats = { issued: 0, rotated: 0, revoked: 0, expired: 0, validated: 0 };
162
+ this._cleanupInterval = null;
163
+ }
164
+
165
+ /**
166
+ * Starts automatic cleanup of expired tokens.
167
+ * @param {number} [intervalMs=60000] - Cleanup interval (default 60s)
168
+ */
169
+ startCleanup(intervalMs = 60000) {
170
+ this.stopCleanup();
171
+ this._cleanupInterval = setInterval(() => this._purgeExpired(), intervalMs);
172
+ if (this._cleanupInterval.unref) this._cleanupInterval.unref();
173
+ }
174
+
175
+ /** Stops automatic cleanup. */
176
+ stopCleanup() {
177
+ if (this._cleanupInterval) {
178
+ clearInterval(this._cleanupInterval);
179
+ this._cleanupInterval = null;
180
+ }
181
+ }
182
+
183
+ /** @private */
184
+ _purgeExpired() {
185
+ let purged = 0;
186
+ const expiredIds = [];
187
+
188
+ for (const [tokenId, tokenData] of this.tokens) {
189
+ if (this._isTokenExpired(tokenData)) {
190
+ expiredIds.push(tokenId);
191
+ }
192
+ }
193
+
194
+ for (const tokenId of expiredIds) {
195
+ this.tokens.delete(tokenId);
196
+ this.revokedTokens.delete(tokenId);
197
+ purged++;
198
+ this.stats.expired++;
199
+ }
200
+
201
+ // Clean stale entries from userTokens
202
+ if (purged > 0) {
203
+ for (const [userId, tokenIds] of this.userTokens) {
204
+ const active = tokenIds.filter(id => this.tokens.has(id));
205
+ if (active.length === 0) {
206
+ this.userTokens.delete(userId);
207
+ } else {
208
+ this.userTokens.set(userId, active);
209
+ }
210
+ }
211
+ }
212
+
213
+ return purged;
214
+ }
215
+
216
+ /**
217
+ * Issues an ephemeral token scoped to specific actions.
218
+ * @param {AuthorizationContext} authCtx
219
+ * @param {string[]} scopes - Scopes this token grants (must be subset of authCtx scopes)
220
+ * @returns {{ tokenId: string, token: string, expiresAt: number, scopes: string[] }}
221
+ */
222
+ issueToken(authCtx, scopes = []) {
223
+ if (!authCtx.verify()) throw new Error('Context integrity check failed — possible tampering');
224
+ if (authCtx.isExpired()) throw new Error('Cannot issue token for expired context');
225
+
226
+ // Scopes can only narrow, never widen
227
+ const grantedScopes = scopes.length > 0
228
+ ? scopes.filter(s => authCtx.hasScope(s))
229
+ : [...authCtx.scopes];
230
+
231
+ // Enforce per-user token limit
232
+ const userTokenList = this.userTokens.get(authCtx.userId) || [];
233
+ const activeTokens = userTokenList.filter(id => {
234
+ const t = this.tokens.get(id);
235
+ return t && !this._isTokenExpired(t);
236
+ });
237
+ if (activeTokens.length >= this.maxTokensPerUser) {
238
+ // Revoke oldest
239
+ const oldest = activeTokens[0];
240
+ this.revokeToken(oldest);
241
+ }
242
+
243
+ const tokenId = crypto.randomUUID();
244
+ const tokenData = {
245
+ tokenId,
246
+ userId: authCtx.userId,
247
+ agentId: authCtx.agentId,
248
+ contextId: authCtx.contextId,
249
+ scopes: grantedScopes,
250
+ issuedAt: Date.now(),
251
+ expiresAt: Date.now() + this.tokenTtlMs,
252
+ rotatedFrom: null,
253
+ usageCount: 0
254
+ };
255
+
256
+ this.tokens.set(tokenId, tokenData);
257
+ const updated = [...activeTokens, tokenId];
258
+ this.userTokens.set(authCtx.userId, updated);
259
+ this.stats.issued++;
260
+
261
+ return {
262
+ tokenId,
263
+ token: this._encodeToken(tokenData),
264
+ expiresAt: tokenData.expiresAt,
265
+ scopes: grantedScopes
266
+ };
267
+ }
268
+
269
+ /**
270
+ * Validates a token and returns its context.
271
+ * @param {string} tokenId
272
+ * @returns {{ valid: boolean, reason: string|null, userId: string|null, scopes: string[] }}
273
+ */
274
+ validateToken(tokenId) {
275
+ this.stats.validated++;
276
+ const tokenData = this.tokens.get(tokenId);
277
+
278
+ if (!tokenData) {
279
+ return { valid: false, reason: 'Token not found', userId: null, scopes: [] };
280
+ }
281
+ if (this.revokedTokens.has(tokenId)) {
282
+ return { valid: false, reason: 'Token has been revoked', userId: tokenData.userId, scopes: [] };
283
+ }
284
+ if (this._isTokenExpired(tokenData)) {
285
+ this.stats.expired++;
286
+ return { valid: false, reason: 'Token has expired', userId: tokenData.userId, scopes: [] };
287
+ }
288
+
289
+ tokenData.usageCount++;
290
+ return { valid: true, reason: null, userId: tokenData.userId, scopes: tokenData.scopes };
291
+ }
292
+
293
+ /**
294
+ * Rotates a token — issues new token, old remains valid during grace period.
295
+ * @param {string} oldTokenId
296
+ * @param {AuthorizationContext} authCtx
297
+ * @returns {{ tokenId: string, token: string, expiresAt: number, scopes: string[] }|null}
298
+ */
299
+ rotateToken(oldTokenId, authCtx) {
300
+ const oldToken = this.tokens.get(oldTokenId);
301
+ if (!oldToken || this.revokedTokens.has(oldTokenId)) return null;
302
+
303
+ // Issue new token with same scopes
304
+ const newTokenResult = this.issueToken(authCtx, oldToken.scopes);
305
+
306
+ // Mark old token for grace-period expiry
307
+ oldToken.expiresAt = Math.min(oldToken.expiresAt, Date.now() + this.rotationWindowMs);
308
+ const newTokenData = this.tokens.get(newTokenResult.tokenId);
309
+ if (newTokenData) newTokenData.rotatedFrom = oldTokenId;
310
+
311
+ this.stats.rotated++;
312
+ return newTokenResult;
313
+ }
314
+
315
+ /**
316
+ * Revokes a token immediately.
317
+ * @param {string} tokenId
318
+ */
319
+ revokeToken(tokenId) {
320
+ this.revokedTokens.add(tokenId);
321
+ this.stats.revoked++;
322
+ }
323
+
324
+ /**
325
+ * Revokes all tokens for a user.
326
+ * @param {string} userId
327
+ * @returns {number} Number of tokens revoked
328
+ */
329
+ revokeAllForUser(userId) {
330
+ const userTokenList = this.userTokens.get(userId) || [];
331
+ let count = 0;
332
+ for (const id of userTokenList) {
333
+ if (!this.revokedTokens.has(id)) {
334
+ this.revokeToken(id);
335
+ count++;
336
+ }
337
+ }
338
+ return count;
339
+ }
340
+
341
+ /** @returns {object} */
342
+ getStats() {
343
+ return { ...this.stats, activeTokens: this.tokens.size - this.revokedTokens.size };
344
+ }
345
+
346
+ /** @private */
347
+ _isTokenExpired(tokenData) {
348
+ return Date.now() > tokenData.expiresAt;
349
+ }
350
+
351
+ /** @private */
352
+ _encodeToken(tokenData) {
353
+ const payload = `${tokenData.tokenId}:${tokenData.userId}:${tokenData.scopes.join(',')}:${tokenData.expiresAt}`;
354
+ return crypto.createHmac('sha256', this._signingKey).update(payload).digest('hex');
355
+ }
356
+ }
357
+
358
+ // =========================================================================
359
+ // Intent Validator — post-auth action verification
360
+ // =========================================================================
361
+
362
+ /**
363
+ * Validates that tool calls match the agent's declared intent and user permissions.
364
+ * Closes the "post-authentication blindness" gap.
365
+ */
366
+ class IntentValidator {
367
+ /**
368
+ * @param {object} [options]
369
+ * @param {boolean} [options.requireIntent=false] - Require declared intent for all actions
370
+ * @param {number} [options.maxDelegationDepth=5] - Maximum delegation chain depth
371
+ * @param {Function} [options.onViolation] - Callback on policy violation
372
+ */
373
+ constructor(options = {}) {
374
+ this.requireIntent = options.requireIntent || false;
375
+ this.maxDelegationDepth = options.maxDelegationDepth || 5;
376
+ this.onViolation = options.onViolation || null;
377
+ this.policies = [];
378
+ this.auditLog = [];
379
+ }
380
+
381
+ /**
382
+ * Registers a tool-level authorization policy.
383
+ * @param {object} policy
384
+ * @param {string|RegExp} policy.tool - Tool name or pattern
385
+ * @param {string[]} [policy.requiredScopes] - Scopes needed to use this tool
386
+ * @param {string[]} [policy.requiredRoles] - Roles needed to use this tool
387
+ * @param {string[]} [policy.allowedIntents] - Intents that may use this tool
388
+ * @param {boolean} [policy.requiresHumanApproval=false] - Needs human-in-the-loop
389
+ */
390
+ addPolicy(policy) {
391
+ this.policies.push({
392
+ tool: policy.tool,
393
+ requiredScopes: policy.requiredScopes || [],
394
+ requiredRoles: policy.requiredRoles || [],
395
+ allowedIntents: policy.allowedIntents || [],
396
+ requiresHumanApproval: policy.requiresHumanApproval || false
397
+ });
398
+ }
399
+
400
+ /**
401
+ * Validates a tool call against the authorization context and policies.
402
+ * @param {string} toolName
403
+ * @param {object} args
404
+ * @param {AuthorizationContext} authCtx
405
+ * @returns {{ allowed: boolean, violations: Array, requiresApproval: boolean }}
406
+ */
407
+ validateAction(toolName, args, authCtx) {
408
+ const violations = [];
409
+ let requiresApproval = false;
410
+
411
+ // Check context validity
412
+ if (!authCtx || !authCtx.verify()) {
413
+ violations.push({ type: 'integrity', message: 'Authorization context is missing or tampered' });
414
+ } else if (authCtx.isExpired()) {
415
+ violations.push({ type: 'expired', message: 'Authorization context has expired' });
416
+ }
417
+
418
+ if (violations.length > 0) {
419
+ this._logAudit(toolName, authCtx, violations, false);
420
+ return { allowed: false, violations, requiresApproval: false };
421
+ }
422
+
423
+ // Check delegation depth
424
+ if (authCtx.delegationDepth > this.maxDelegationDepth) {
425
+ violations.push({ type: 'delegation_depth', message: `Delegation depth ${authCtx.delegationDepth} exceeds max ${this.maxDelegationDepth}` });
426
+ }
427
+
428
+ // Check intent requirement
429
+ if (this.requireIntent && !authCtx.intent) {
430
+ violations.push({ type: 'missing_intent', message: 'No intent declared for this action' });
431
+ }
432
+
433
+ // Check policies
434
+ for (const policy of this.policies) {
435
+ const toolMatch = policy.tool instanceof RegExp
436
+ ? policy.tool.test(toolName)
437
+ : policy.tool === toolName || policy.tool === '*';
438
+
439
+ if (!toolMatch) continue;
440
+
441
+ // Scope check
442
+ for (const scope of policy.requiredScopes) {
443
+ if (!authCtx.hasScope(scope)) {
444
+ violations.push({ type: 'scope', message: `Missing required scope "${scope}" for tool "${toolName}"`, tool: toolName });
445
+ }
446
+ }
447
+
448
+ // Role check
449
+ if (policy.requiredRoles.length > 0) {
450
+ const hasRequired = policy.requiredRoles.some(r => authCtx.hasRole(r));
451
+ if (!hasRequired) {
452
+ violations.push({ type: 'role', message: `Requires role ${policy.requiredRoles.join('|')} for tool "${toolName}"`, tool: toolName });
453
+ }
454
+ }
455
+
456
+ // Intent check — uses word-boundary matching to prevent substring spoofing
457
+ if (policy.allowedIntents.length > 0 && authCtx.intent) {
458
+ const intentWords = authCtx.intent.toLowerCase().split(/[\s_-]+/);
459
+ const intentAllowed = policy.allowedIntents.some(i => {
460
+ const allowedWords = i.toLowerCase().split(/[\s_-]+/);
461
+ return allowedWords.every(w => intentWords.includes(w));
462
+ });
463
+ if (!intentAllowed) {
464
+ violations.push({ type: 'intent', message: `Intent "${authCtx.intent}" not allowed for tool "${toolName}"`, tool: toolName });
465
+ }
466
+ }
467
+
468
+ // Human approval check
469
+ if (policy.requiresHumanApproval) {
470
+ requiresApproval = true;
471
+ }
472
+ }
473
+
474
+ const allowed = violations.length === 0;
475
+
476
+ if (!allowed && this.onViolation) {
477
+ this.onViolation({ toolName, args, authCtx, violations });
478
+ }
479
+
480
+ this._logAudit(toolName, authCtx, violations, allowed);
481
+ return { allowed, violations, requiresApproval };
482
+ }
483
+
484
+ /**
485
+ * Returns the audit log.
486
+ * @param {number} [limit=100]
487
+ * @returns {Array}
488
+ */
489
+ getAuditLog(limit = 100) {
490
+ return this.auditLog.slice(-limit);
491
+ }
492
+
493
+ /** @private */
494
+ _logAudit(toolName, authCtx, violations, allowed) {
495
+ this.auditLog.push({
496
+ timestamp: Date.now(),
497
+ toolName,
498
+ userId: authCtx ? authCtx.userId : null,
499
+ agentId: authCtx ? authCtx.agentId : null,
500
+ contextId: authCtx ? authCtx.contextId : null,
501
+ delegationDepth: authCtx ? authCtx.delegationDepth : null,
502
+ allowed,
503
+ violations: violations.length > 0 ? violations : undefined
504
+ });
505
+ }
506
+ }
507
+
508
+ // =========================================================================
509
+ // ConfusedDeputyGuard — MCP-aware per-user authorization
510
+ // =========================================================================
511
+
512
+ /**
513
+ * Prevents confused deputy attacks by enforcing per-user authorization
514
+ * on every tool call, not just per-agent or per-session.
515
+ */
516
+ class ConfusedDeputyGuard {
517
+ /**
518
+ * @param {object} [options]
519
+ * @param {boolean} [options.enforceContext=true] - Require AuthorizationContext on all calls
520
+ * @param {boolean} [options.logOnly=false] - Log violations without blocking
521
+ */
522
+ constructor(options = {}) {
523
+ this.enforceContext = options.enforceContext !== false;
524
+ this.logOnly = options.logOnly || false;
525
+ this.tokenManager = new EphemeralTokenManager(options);
526
+ this.intentValidator = new IntentValidator(options);
527
+ this.toolPermissions = new Map();
528
+ this.stats = { checked: 0, allowed: 0, denied: 0, escalations: 0 };
529
+ }
530
+
531
+ /**
532
+ * Registers tool-level permission requirements.
533
+ * @param {string} toolName
534
+ * @param {object} requirements
535
+ * @param {string[]} [requirements.scopes] - Required scopes
536
+ * @param {string[]} [requirements.roles] - Required roles
537
+ * @param {boolean} [requirements.requiresHumanApproval] - Needs HITL
538
+ */
539
+ registerTool(toolName, requirements = {}) {
540
+ this.toolPermissions.set(toolName, requirements);
541
+ this.intentValidator.addPolicy({
542
+ tool: toolName,
543
+ requiredScopes: requirements.scopes || [],
544
+ requiredRoles: requirements.roles || [],
545
+ requiresHumanApproval: requirements.requiresHumanApproval || false
546
+ });
547
+ }
548
+
549
+ /**
550
+ * Wraps a tool call with confused deputy prevention.
551
+ * @param {string} toolName
552
+ * @param {object} args
553
+ * @param {AuthorizationContext} [authCtx]
554
+ * @returns {{ allowed: boolean, violations: Array, requiresApproval: boolean, token: object|null }}
555
+ */
556
+ wrapToolCall(toolName, args, authCtx) {
557
+ this.stats.checked++;
558
+
559
+ // Enforce context requirement
560
+ if (this.enforceContext && !authCtx) {
561
+ this.stats.denied++;
562
+ const violation = [{ type: 'missing_context', message: 'AuthorizationContext required but not provided — potential confused deputy' }];
563
+ if (!this.logOnly) {
564
+ return { allowed: false, violations: violation, requiresApproval: false, token: null };
565
+ }
566
+ }
567
+
568
+ if (!authCtx) {
569
+ this.stats.allowed++;
570
+ return { allowed: true, violations: [], requiresApproval: false, token: null };
571
+ }
572
+
573
+ // Validate via intent validator (checks scopes, roles, intent, delegation depth)
574
+ const validation = this.intentValidator.validateAction(toolName, args, authCtx);
575
+
576
+ if (validation.allowed) {
577
+ this.stats.allowed++;
578
+ // Issue ephemeral token for this action
579
+ const token = this.tokenManager.issueToken(authCtx, [toolName]);
580
+ return { allowed: true, violations: [], requiresApproval: validation.requiresApproval, token };
581
+ }
582
+
583
+ this.stats.denied++;
584
+ this.stats.escalations += validation.violations.filter(v => v.type === 'scope' || v.type === 'role').length;
585
+
586
+ if (this.logOnly) {
587
+ return { allowed: true, violations: validation.violations, requiresApproval: false, token: null };
588
+ }
589
+
590
+ return { allowed: false, violations: validation.violations, requiresApproval: validation.requiresApproval, token: null };
591
+ }
592
+
593
+ /**
594
+ * Returns combined stats from all sub-components.
595
+ * @returns {object}
596
+ */
597
+ getStats() {
598
+ return {
599
+ ...this.stats,
600
+ tokens: this.tokenManager.getStats(),
601
+ auditEntries: this.intentValidator.getAuditLog().length
602
+ };
603
+ }
604
+
605
+ /**
606
+ * Returns the audit trail for forensic analysis.
607
+ * @param {number} [limit=100]
608
+ * @returns {Array}
609
+ */
610
+ getAuditLog(limit) {
611
+ return this.intentValidator.getAuditLog(limit);
612
+ }
613
+ }
614
+
615
+ // =========================================================================
616
+ // Exports
617
+ // =========================================================================
618
+
619
+ module.exports = {
620
+ AuthorizationContext,
621
+ EphemeralTokenManager,
622
+ IntentValidator,
623
+ ConfusedDeputyGuard
624
+ };