agentshield-sdk 7.0.0 → 7.1.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/package.json +3 -2
- package/src/adaptive-defense.js +942 -0
- package/src/main.js +10 -0
- package/src/mcp-security-runtime.js +149 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentshield-sdk",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.1.0",
|
|
4
4
|
"description": "The security standard for MCP and AI agents. Protects against prompt injection, confused deputy attacks, data exfiltration, and 30+ threats. Zero dependencies, runs locally.",
|
|
5
5
|
"main": "src/main.js",
|
|
6
6
|
"types": "types/index.d.ts",
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"test:mcp": "node test/test-mcp-security.js",
|
|
28
28
|
"test:deputy": "node test/test-confused-deputy.js",
|
|
29
29
|
"test:v6": "node test/test-v6-modules.js",
|
|
30
|
-
"test:
|
|
30
|
+
"test:adaptive": "node test/test-adaptive-defense.js",
|
|
31
|
+
"test:full": "npm test && node test/test-mcp-security.js && node test/test-confused-deputy.js && node test/test-v6-modules.js && node test/test-adaptive-defense.js && npm run test:all",
|
|
31
32
|
"test:coverage": "c8 --reporter=text --reporter=lcov --reporter=json-summary npm test",
|
|
32
33
|
"lint": "node test/lint.js",
|
|
33
34
|
"lint:eslint": "eslint src/ test/ bin/",
|
|
@@ -0,0 +1,942 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Shield — Adaptive Defense System
|
|
5
|
+
*
|
|
6
|
+
* Three interconnected systems that make Agent Shield learn and improve:
|
|
7
|
+
*
|
|
8
|
+
* 1. LearningLoop — blocked attacks feed into threat intelligence, which
|
|
9
|
+
* generates new detection patterns automatically. The system gets smarter
|
|
10
|
+
* with every attack it sees.
|
|
11
|
+
*
|
|
12
|
+
* 2. AgentContract — declarative behavioral specifications that define what
|
|
13
|
+
* an agent is ALLOWED to do. Verified continuously at runtime, not just
|
|
14
|
+
* at deploy time. Violations are caught instantly.
|
|
15
|
+
*
|
|
16
|
+
* 3. ComplianceAttestor — real-time proof that your agents meet NIST, OWASP,
|
|
17
|
+
* EU AI Act requirements. Not a report you generate once — a live signal
|
|
18
|
+
* that updates with every action your agent takes.
|
|
19
|
+
*
|
|
20
|
+
* Together these create a closed-loop system: attacks improve detection,
|
|
21
|
+
* contracts prevent drift, and compliance is proven continuously.
|
|
22
|
+
*
|
|
23
|
+
* All processing runs locally — no data ever leaves your environment.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const crypto = require('crypto');
|
|
27
|
+
|
|
28
|
+
const LOG_PREFIX = '[Agent Shield]';
|
|
29
|
+
|
|
30
|
+
// =========================================================================
|
|
31
|
+
// 1. LearningLoop — self-improving detection
|
|
32
|
+
// =========================================================================
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Closed-loop system that feeds blocked attacks back into threat intelligence,
|
|
36
|
+
* which generates new detection patterns. Every attack makes the system smarter.
|
|
37
|
+
*
|
|
38
|
+
* Flow:
|
|
39
|
+
* Attack blocked → extract signature → record in intel → generate pattern
|
|
40
|
+
* → add to scanner → next attack caught faster
|
|
41
|
+
*/
|
|
42
|
+
class LearningLoop {
|
|
43
|
+
/**
|
|
44
|
+
* @param {object} [options]
|
|
45
|
+
* @param {number} [options.minHitsToPromote=3] - Hits before a pattern becomes active
|
|
46
|
+
* @param {number} [options.maxLearnedPatterns=500] - Max auto-generated patterns
|
|
47
|
+
* @param {number} [options.promotionConfidence=0.75] - Min confidence to promote
|
|
48
|
+
* @param {Function} [options.onPatternLearned] - Callback when new pattern is promoted
|
|
49
|
+
* @param {Function} [options.onFeedback] - Callback for operator feedback
|
|
50
|
+
*/
|
|
51
|
+
constructor(options = {}) {
|
|
52
|
+
this._minHitsToPromote = options.minHitsToPromote || 3;
|
|
53
|
+
this._maxLearnedPatterns = options.maxLearnedPatterns || 500;
|
|
54
|
+
this._promotionConfidence = options.promotionConfidence || 0.75;
|
|
55
|
+
this._onPatternLearned = options.onPatternLearned || null;
|
|
56
|
+
this._onFeedback = options.onFeedback || null;
|
|
57
|
+
|
|
58
|
+
// Candidate patterns (not yet promoted)
|
|
59
|
+
this._candidates = new Map(); // signature → CandidatePattern
|
|
60
|
+
// Promoted patterns (active detection)
|
|
61
|
+
this._promoted = new Map(); // patternId → PromotedPattern
|
|
62
|
+
// Feedback from operators
|
|
63
|
+
this._feedback = [];
|
|
64
|
+
|
|
65
|
+
this.stats = {
|
|
66
|
+
attacksIngested: 0,
|
|
67
|
+
candidatesCreated: 0,
|
|
68
|
+
patternsPromoted: 0,
|
|
69
|
+
falsePositivesReported: 0,
|
|
70
|
+
patternsRevoked: 0
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Ingests a blocked attack and extracts learnable signatures.
|
|
76
|
+
* Call this every time the scanner blocks something.
|
|
77
|
+
*
|
|
78
|
+
* @param {object} attack
|
|
79
|
+
* @param {string} attack.text - The blocked text
|
|
80
|
+
* @param {string} attack.category - Threat category
|
|
81
|
+
* @param {Array} [attack.threats] - Detected threats
|
|
82
|
+
* @param {string} [attack.toolName] - Tool that was targeted
|
|
83
|
+
* @param {string} [attack.sessionId] - Session context
|
|
84
|
+
* @returns {{ signatures: string[], candidatesUpdated: number, promoted: string[] }}
|
|
85
|
+
*/
|
|
86
|
+
ingest(attack) {
|
|
87
|
+
if (!attack || !attack.text || !attack.category) {
|
|
88
|
+
return { signatures: [], candidatesUpdated: 0, promoted: [] };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this.stats.attacksIngested++;
|
|
92
|
+
const signatures = this._extractSignatures(attack.text, attack.category);
|
|
93
|
+
let candidatesUpdated = 0;
|
|
94
|
+
const promoted = [];
|
|
95
|
+
|
|
96
|
+
for (const sig of signatures) {
|
|
97
|
+
const sigHash = this._hash(sig);
|
|
98
|
+
const existing = this._candidates.get(sigHash);
|
|
99
|
+
|
|
100
|
+
if (existing) {
|
|
101
|
+
// Existing candidate — increment hit count
|
|
102
|
+
existing.hitCount++;
|
|
103
|
+
existing.lastSeen = Date.now();
|
|
104
|
+
existing.contexts.push({
|
|
105
|
+
category: attack.category,
|
|
106
|
+
toolName: attack.toolName || null,
|
|
107
|
+
timestamp: Date.now()
|
|
108
|
+
});
|
|
109
|
+
if (existing.contexts.length > 20) existing.contexts.shift();
|
|
110
|
+
candidatesUpdated++;
|
|
111
|
+
|
|
112
|
+
// Check if ready for promotion
|
|
113
|
+
if (existing.hitCount >= this._minHitsToPromote &&
|
|
114
|
+
!existing.promoted &&
|
|
115
|
+
this._promoted.size < this._maxLearnedPatterns) {
|
|
116
|
+
const confidence = Math.min(1.0, 0.5 + (existing.hitCount * 0.05));
|
|
117
|
+
if (confidence >= this._promotionConfidence) {
|
|
118
|
+
existing.promoted = true;
|
|
119
|
+
const patternId = `LP_${sigHash.substring(0, 12)}`;
|
|
120
|
+
this._promoted.set(patternId, {
|
|
121
|
+
patternId,
|
|
122
|
+
signature: sig,
|
|
123
|
+
category: attack.category,
|
|
124
|
+
confidence,
|
|
125
|
+
hitCount: existing.hitCount,
|
|
126
|
+
promotedAt: Date.now(),
|
|
127
|
+
active: true,
|
|
128
|
+
source: 'learning_loop'
|
|
129
|
+
});
|
|
130
|
+
this.stats.patternsPromoted++;
|
|
131
|
+
promoted.push(patternId);
|
|
132
|
+
if (this._onPatternLearned) {
|
|
133
|
+
try { this._onPatternLearned({ patternId, signature: sig, category: attack.category, confidence }); } catch (_e) { /* */ }
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
// New candidate
|
|
139
|
+
this._candidates.set(sigHash, {
|
|
140
|
+
signature: sig,
|
|
141
|
+
sigHash,
|
|
142
|
+
category: attack.category,
|
|
143
|
+
hitCount: 1,
|
|
144
|
+
firstSeen: Date.now(),
|
|
145
|
+
lastSeen: Date.now(),
|
|
146
|
+
promoted: false,
|
|
147
|
+
contexts: [{
|
|
148
|
+
category: attack.category,
|
|
149
|
+
toolName: attack.toolName || null,
|
|
150
|
+
timestamp: Date.now()
|
|
151
|
+
}]
|
|
152
|
+
});
|
|
153
|
+
this.stats.candidatesCreated++;
|
|
154
|
+
candidatesUpdated++;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { signatures, candidatesUpdated, promoted };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Checks input against learned patterns.
|
|
163
|
+
* @param {string} text
|
|
164
|
+
* @returns {{ matches: Array<{ patternId: string, category: string, confidence: number }> }}
|
|
165
|
+
*/
|
|
166
|
+
check(text) {
|
|
167
|
+
if (!text) return { matches: [] };
|
|
168
|
+
const lower = text.toLowerCase();
|
|
169
|
+
const matches = [];
|
|
170
|
+
|
|
171
|
+
for (const [patternId, pattern] of this._promoted) {
|
|
172
|
+
if (!pattern.active) continue;
|
|
173
|
+
if (lower.includes(pattern.signature.toLowerCase())) {
|
|
174
|
+
matches.push({
|
|
175
|
+
patternId,
|
|
176
|
+
category: pattern.category,
|
|
177
|
+
confidence: pattern.confidence,
|
|
178
|
+
source: 'learned'
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return { matches };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Records operator feedback — false positive or confirmed threat.
|
|
188
|
+
* @param {string} patternId
|
|
189
|
+
* @param {'false_positive' | 'confirmed'} type
|
|
190
|
+
* @param {string} [reason]
|
|
191
|
+
*/
|
|
192
|
+
recordFeedback(patternId, type, reason) {
|
|
193
|
+
this._feedback.push({
|
|
194
|
+
patternId,
|
|
195
|
+
type,
|
|
196
|
+
reason: reason || null,
|
|
197
|
+
timestamp: Date.now()
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
if (type === 'false_positive') {
|
|
201
|
+
this.stats.falsePositivesReported++;
|
|
202
|
+
const pattern = this._promoted.get(patternId);
|
|
203
|
+
if (pattern) {
|
|
204
|
+
pattern.confidence = Math.max(0.1, pattern.confidence - 0.15);
|
|
205
|
+
// Revoke if confidence drops too low
|
|
206
|
+
if (pattern.confidence < 0.4) {
|
|
207
|
+
pattern.active = false;
|
|
208
|
+
this.stats.patternsRevoked++;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
} else if (type === 'confirmed') {
|
|
212
|
+
const pattern = this._promoted.get(patternId);
|
|
213
|
+
if (pattern) {
|
|
214
|
+
pattern.confidence = Math.min(1.0, pattern.confidence + 0.1);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (this._onFeedback) {
|
|
219
|
+
try { this._onFeedback({ patternId, type, reason }); } catch (_e) { /* */ }
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Returns all active learned patterns.
|
|
225
|
+
* @returns {Array}
|
|
226
|
+
*/
|
|
227
|
+
getActivePatterns() {
|
|
228
|
+
const patterns = [];
|
|
229
|
+
for (const [_id, p] of this._promoted) {
|
|
230
|
+
if (p.active) patterns.push({ ...p });
|
|
231
|
+
}
|
|
232
|
+
return patterns;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Returns learning statistics.
|
|
237
|
+
* @returns {object}
|
|
238
|
+
*/
|
|
239
|
+
getReport() {
|
|
240
|
+
return {
|
|
241
|
+
stats: { ...this.stats },
|
|
242
|
+
candidates: this._candidates.size,
|
|
243
|
+
activePatterns: [...this._promoted.values()].filter(p => p.active).length,
|
|
244
|
+
revokedPatterns: [...this._promoted.values()].filter(p => !p.active).length,
|
|
245
|
+
recentFeedback: this._feedback.slice(-10)
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Exports learned patterns for sharing across deployments.
|
|
251
|
+
* @returns {object}
|
|
252
|
+
*/
|
|
253
|
+
exportPatterns() {
|
|
254
|
+
const patterns = [];
|
|
255
|
+
for (const [_id, p] of this._promoted) {
|
|
256
|
+
if (p.active) patterns.push({ ...p });
|
|
257
|
+
}
|
|
258
|
+
return { version: '1.0', exportedAt: Date.now(), patterns };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Imports learned patterns from another deployment.
|
|
263
|
+
* @param {object} data - Output of exportPatterns()
|
|
264
|
+
* @returns {{ imported: number, skipped: number }}
|
|
265
|
+
*/
|
|
266
|
+
importPatterns(data) {
|
|
267
|
+
if (!data || !data.patterns) return { imported: 0, skipped: 0 };
|
|
268
|
+
let imported = 0;
|
|
269
|
+
let skipped = 0;
|
|
270
|
+
|
|
271
|
+
for (const p of data.patterns) {
|
|
272
|
+
if (!p.patternId || !p.signature || !p.category) { skipped++; continue; }
|
|
273
|
+
if (this._promoted.has(p.patternId)) { skipped++; continue; }
|
|
274
|
+
if (this._promoted.size >= this._maxLearnedPatterns) { skipped++; continue; }
|
|
275
|
+
|
|
276
|
+
this._promoted.set(p.patternId, {
|
|
277
|
+
...p,
|
|
278
|
+
active: true,
|
|
279
|
+
importedAt: Date.now()
|
|
280
|
+
});
|
|
281
|
+
imported++;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return { imported, skipped };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** @private */
|
|
288
|
+
_extractSignatures(text, _category) {
|
|
289
|
+
const signatures = [];
|
|
290
|
+
const lower = text.toLowerCase();
|
|
291
|
+
|
|
292
|
+
// Extract significant phrases (3+ words that appear to be instructions)
|
|
293
|
+
const instructionPatterns = [
|
|
294
|
+
/ignore\s+(?:all\s+)?(?:previous\s+)?instructions?/gi,
|
|
295
|
+
/you\s+are\s+now\s+\w+/gi,
|
|
296
|
+
/forget\s+(?:everything|all|your)\s+\w+/gi,
|
|
297
|
+
/act\s+as\s+(?:if|though)?\s*\w+/gi,
|
|
298
|
+
/reveal\s+(?:your|the)\s+(?:system\s+)?prompt/gi,
|
|
299
|
+
/output\s+(?:your|the)\s+(?:initial|system|original)\s+\w+/gi,
|
|
300
|
+
/bypass\s+(?:all\s+)?(?:security|safety|restrictions?)/gi,
|
|
301
|
+
/disable\s+(?:your\s+)?(?:safety|security|filters?)/gi,
|
|
302
|
+
/(?:send|post|fetch|curl)\s+(?:to|from)\s+\S+/gi,
|
|
303
|
+
/(?:read|cat|access)\s+(?:\/etc\/|\.env|\.ssh|\/proc)/gi
|
|
304
|
+
];
|
|
305
|
+
|
|
306
|
+
for (const pattern of instructionPatterns) {
|
|
307
|
+
const matches = lower.match(pattern);
|
|
308
|
+
if (matches) {
|
|
309
|
+
for (const match of matches) {
|
|
310
|
+
const trimmed = match.trim();
|
|
311
|
+
if (trimmed.length >= 8 && trimmed.length <= 100) {
|
|
312
|
+
signatures.push(trimmed);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// If no pattern matches, extract the most suspicious 4-gram
|
|
319
|
+
if (signatures.length === 0 && lower.length >= 20) {
|
|
320
|
+
const words = lower.split(/\s+/).filter(w => w.length > 2);
|
|
321
|
+
if (words.length >= 4) {
|
|
322
|
+
// Use the first 4 meaningful words as a signature
|
|
323
|
+
signatures.push(words.slice(0, 4).join(' '));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return signatures;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** @private */
|
|
331
|
+
_hash(text) {
|
|
332
|
+
return crypto.createHash('sha256').update(text).digest('hex').substring(0, 16);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// =========================================================================
|
|
337
|
+
// 2. AgentContract — declarative behavioral specifications
|
|
338
|
+
// =========================================================================
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Defines what an agent is ALLOWED to do, verified continuously at runtime.
|
|
342
|
+
* Like a unit test for agent behavior that never stops running.
|
|
343
|
+
*
|
|
344
|
+
* Example contract:
|
|
345
|
+
* {
|
|
346
|
+
* agentId: 'research-bot',
|
|
347
|
+
* allowedTools: ['search', 'read_file'],
|
|
348
|
+
* deniedTools: ['delete_file', 'execute_shell'],
|
|
349
|
+
* maxToolCallsPerMinute: 30,
|
|
350
|
+
* maxDelegationDepth: 2,
|
|
351
|
+
* allowedScopes: ['docs:read', 'web:search'],
|
|
352
|
+
* requiredIntents: true,
|
|
353
|
+
* allowedDataPatterns: [/^[a-zA-Z0-9\s.,!?]+$/], // No code, no URLs
|
|
354
|
+
* timeWindows: [{ start: 9, end: 17 }], // Business hours only
|
|
355
|
+
* maxResponseLength: 10000
|
|
356
|
+
* }
|
|
357
|
+
*/
|
|
358
|
+
class AgentContract {
|
|
359
|
+
/**
|
|
360
|
+
* @param {object} spec - Contract specification
|
|
361
|
+
* @param {string} spec.agentId - Agent this contract applies to
|
|
362
|
+
* @param {string[]} [spec.allowedTools] - Whitelist of permitted tools
|
|
363
|
+
* @param {string[]} [spec.deniedTools] - Blacklist of forbidden tools
|
|
364
|
+
* @param {number} [spec.maxToolCallsPerMinute=60] - Rate limit
|
|
365
|
+
* @param {number} [spec.maxDelegationDepth=3] - Max delegation chain depth
|
|
366
|
+
* @param {string[]} [spec.allowedScopes] - Permitted permission scopes
|
|
367
|
+
* @param {boolean} [spec.requiredIntents=false] - Must declare intent
|
|
368
|
+
* @param {number} [spec.maxResponseLength=50000] - Max output length
|
|
369
|
+
* @param {Array<{start: number, end: number}>} [spec.timeWindows] - Allowed hours (0-23)
|
|
370
|
+
* @param {Function} [spec.customValidator] - Custom validation function
|
|
371
|
+
*/
|
|
372
|
+
constructor(spec) {
|
|
373
|
+
if (!spec || !spec.agentId) {
|
|
374
|
+
throw new Error(`${LOG_PREFIX} AgentContract requires agentId`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
this.agentId = spec.agentId;
|
|
378
|
+
this.allowedTools = spec.allowedTools ? new Set(spec.allowedTools) : null;
|
|
379
|
+
this.deniedTools = spec.deniedTools ? new Set(spec.deniedTools) : new Set();
|
|
380
|
+
this.maxToolCallsPerMinute = spec.maxToolCallsPerMinute || 60;
|
|
381
|
+
this.maxDelegationDepth = spec.maxDelegationDepth || 3;
|
|
382
|
+
this.allowedScopes = spec.allowedScopes ? new Set(spec.allowedScopes) : null;
|
|
383
|
+
this.requiredIntents = spec.requiredIntents || false;
|
|
384
|
+
this.maxResponseLength = spec.maxResponseLength || 50000;
|
|
385
|
+
this.timeWindows = spec.timeWindows || null;
|
|
386
|
+
this.customValidator = spec.customValidator || null;
|
|
387
|
+
this.createdAt = Date.now();
|
|
388
|
+
|
|
389
|
+
// Runtime tracking
|
|
390
|
+
this._toolCallTimestamps = [];
|
|
391
|
+
this._violations = [];
|
|
392
|
+
this._maxViolations = 1000;
|
|
393
|
+
this.stats = { checked: 0, passed: 0, violated: 0 };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Verifies an action against this contract.
|
|
398
|
+
* @param {object} action
|
|
399
|
+
* @param {string} action.type - 'tool_call' | 'delegation' | 'response' | 'scope_request'
|
|
400
|
+
* @param {string} [action.toolName] - For tool_call type
|
|
401
|
+
* @param {object} [action.args] - Tool arguments
|
|
402
|
+
* @param {string} [action.intent] - Declared intent
|
|
403
|
+
* @param {number} [action.delegationDepth] - Current delegation depth
|
|
404
|
+
* @param {string} [action.responseText] - For response type
|
|
405
|
+
* @param {string[]} [action.requestedScopes] - For scope_request type
|
|
406
|
+
* @returns {{ allowed: boolean, violations: Array<{ rule: string, message: string, severity: string }> }}
|
|
407
|
+
*/
|
|
408
|
+
verify(action) {
|
|
409
|
+
this.stats.checked++;
|
|
410
|
+
const violations = [];
|
|
411
|
+
|
|
412
|
+
// Tool whitelist/blacklist
|
|
413
|
+
if (action.type === 'tool_call' && action.toolName) {
|
|
414
|
+
if (this.allowedTools && !this.allowedTools.has(action.toolName)) {
|
|
415
|
+
violations.push({
|
|
416
|
+
rule: 'allowed_tools',
|
|
417
|
+
message: `Tool "${action.toolName}" not in contract whitelist`,
|
|
418
|
+
severity: 'high'
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
if (this.deniedTools.has(action.toolName)) {
|
|
422
|
+
violations.push({
|
|
423
|
+
rule: 'denied_tools',
|
|
424
|
+
message: `Tool "${action.toolName}" explicitly denied by contract`,
|
|
425
|
+
severity: 'critical'
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Rate limiting
|
|
430
|
+
const now = Date.now();
|
|
431
|
+
this._toolCallTimestamps.push(now);
|
|
432
|
+
const cutoff = now - 60000;
|
|
433
|
+
this._toolCallTimestamps = this._toolCallTimestamps.filter(t => t > cutoff);
|
|
434
|
+
if (this._toolCallTimestamps.length > this.maxToolCallsPerMinute) {
|
|
435
|
+
violations.push({
|
|
436
|
+
rule: 'rate_limit',
|
|
437
|
+
message: `Rate limit exceeded: ${this._toolCallTimestamps.length}/${this.maxToolCallsPerMinute} calls/min`,
|
|
438
|
+
severity: 'high'
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Delegation depth
|
|
444
|
+
if (action.type === 'delegation' && action.delegationDepth !== undefined) {
|
|
445
|
+
if (action.delegationDepth >= this.maxDelegationDepth) {
|
|
446
|
+
violations.push({
|
|
447
|
+
rule: 'delegation_depth',
|
|
448
|
+
message: `Delegation depth ${action.delegationDepth} exceeds contract max ${this.maxDelegationDepth}`,
|
|
449
|
+
severity: 'critical'
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Scope checking
|
|
455
|
+
if (action.type === 'scope_request' && action.requestedScopes && this.allowedScopes) {
|
|
456
|
+
for (const scope of action.requestedScopes) {
|
|
457
|
+
if (!this.allowedScopes.has(scope) && !this.allowedScopes.has('*')) {
|
|
458
|
+
violations.push({
|
|
459
|
+
rule: 'allowed_scopes',
|
|
460
|
+
message: `Scope "${scope}" not permitted by contract`,
|
|
461
|
+
severity: 'high'
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Intent requirement
|
|
468
|
+
if (this.requiredIntents && !action.intent) {
|
|
469
|
+
violations.push({
|
|
470
|
+
rule: 'required_intent',
|
|
471
|
+
message: 'Contract requires declared intent but none provided',
|
|
472
|
+
severity: 'medium'
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Response length
|
|
477
|
+
if (action.type === 'response' && action.responseText) {
|
|
478
|
+
if (action.responseText.length > this.maxResponseLength) {
|
|
479
|
+
violations.push({
|
|
480
|
+
rule: 'response_length',
|
|
481
|
+
message: `Response length ${action.responseText.length} exceeds contract max ${this.maxResponseLength}`,
|
|
482
|
+
severity: 'medium'
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Time windows
|
|
488
|
+
if (this.timeWindows && this.timeWindows.length > 0) {
|
|
489
|
+
const hour = new Date().getHours();
|
|
490
|
+
const inWindow = this.timeWindows.some(w => hour >= w.start && hour < w.end);
|
|
491
|
+
if (!inWindow) {
|
|
492
|
+
violations.push({
|
|
493
|
+
rule: 'time_window',
|
|
494
|
+
message: `Action at hour ${hour} outside permitted windows`,
|
|
495
|
+
severity: 'medium'
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Custom validator
|
|
501
|
+
if (this.customValidator) {
|
|
502
|
+
try {
|
|
503
|
+
const customResult = this.customValidator(action);
|
|
504
|
+
if (customResult && customResult.violations) {
|
|
505
|
+
violations.push(...customResult.violations);
|
|
506
|
+
}
|
|
507
|
+
} catch (_e) { /* custom validator errors don't break the contract check */ }
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Record result
|
|
511
|
+
if (violations.length > 0) {
|
|
512
|
+
this.stats.violated++;
|
|
513
|
+
if (this._violations.length >= this._maxViolations) {
|
|
514
|
+
this._violations = this._violations.slice(-Math.floor(this._maxViolations * 0.75));
|
|
515
|
+
}
|
|
516
|
+
this._violations.push({
|
|
517
|
+
timestamp: Date.now(),
|
|
518
|
+
action: { type: action.type, toolName: action.toolName },
|
|
519
|
+
violations
|
|
520
|
+
});
|
|
521
|
+
} else {
|
|
522
|
+
this.stats.passed++;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return { allowed: violations.length === 0, violations };
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Returns violation history.
|
|
530
|
+
* @param {number} [limit=50]
|
|
531
|
+
* @returns {Array}
|
|
532
|
+
*/
|
|
533
|
+
getViolations(limit = 50) {
|
|
534
|
+
return this._violations.slice(-limit);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Returns contract compliance percentage.
|
|
539
|
+
* @returns {{ complianceRate: number, checked: number, passed: number, violated: number }}
|
|
540
|
+
*/
|
|
541
|
+
getComplianceRate() {
|
|
542
|
+
const rate = this.stats.checked > 0
|
|
543
|
+
? Math.round(this.stats.passed / this.stats.checked * 10000) / 100
|
|
544
|
+
: 100;
|
|
545
|
+
return {
|
|
546
|
+
complianceRate: rate,
|
|
547
|
+
...this.stats
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Serializes the contract for storage/transfer.
|
|
553
|
+
* @returns {object}
|
|
554
|
+
*/
|
|
555
|
+
toJSON() {
|
|
556
|
+
return {
|
|
557
|
+
agentId: this.agentId,
|
|
558
|
+
allowedTools: this.allowedTools ? [...this.allowedTools] : null,
|
|
559
|
+
deniedTools: [...this.deniedTools],
|
|
560
|
+
maxToolCallsPerMinute: this.maxToolCallsPerMinute,
|
|
561
|
+
maxDelegationDepth: this.maxDelegationDepth,
|
|
562
|
+
allowedScopes: this.allowedScopes ? [...this.allowedScopes] : null,
|
|
563
|
+
requiredIntents: this.requiredIntents,
|
|
564
|
+
maxResponseLength: this.maxResponseLength,
|
|
565
|
+
timeWindows: this.timeWindows,
|
|
566
|
+
createdAt: this.createdAt
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Creates a contract from a JSON specification.
|
|
572
|
+
* @param {object} json
|
|
573
|
+
* @returns {AgentContract}
|
|
574
|
+
*/
|
|
575
|
+
static fromJSON(json) {
|
|
576
|
+
return new AgentContract(json);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Manages multiple agent contracts and enforces them at runtime.
|
|
582
|
+
*/
|
|
583
|
+
class ContractRegistry {
|
|
584
|
+
constructor() {
|
|
585
|
+
this._contracts = new Map(); // agentId → AgentContract
|
|
586
|
+
this._onViolation = null;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Registers a contract for an agent.
|
|
591
|
+
* @param {AgentContract} contract
|
|
592
|
+
*/
|
|
593
|
+
register(contract) {
|
|
594
|
+
this._contracts.set(contract.agentId, contract);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Sets a callback for contract violations.
|
|
599
|
+
* @param {Function} callback
|
|
600
|
+
*/
|
|
601
|
+
onViolation(callback) {
|
|
602
|
+
this._onViolation = callback;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Enforces the contract for a given agent action.
|
|
607
|
+
* @param {string} agentId
|
|
608
|
+
* @param {object} action
|
|
609
|
+
* @returns {{ allowed: boolean, violations: Array, hasContract: boolean }}
|
|
610
|
+
*/
|
|
611
|
+
enforce(agentId, action) {
|
|
612
|
+
const contract = this._contracts.get(agentId);
|
|
613
|
+
if (!contract) {
|
|
614
|
+
return { allowed: true, violations: [], hasContract: false };
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const result = contract.verify(action);
|
|
618
|
+
if (!result.allowed && this._onViolation) {
|
|
619
|
+
try { this._onViolation({ agentId, action, violations: result.violations }); } catch (_e) { /* */ }
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return { ...result, hasContract: true };
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Returns compliance rates for all registered agents.
|
|
627
|
+
* @returns {object}
|
|
628
|
+
*/
|
|
629
|
+
getComplianceReport() {
|
|
630
|
+
const report = {};
|
|
631
|
+
for (const [agentId, contract] of this._contracts) {
|
|
632
|
+
report[agentId] = contract.getComplianceRate();
|
|
633
|
+
}
|
|
634
|
+
return report;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Returns all registered contract IDs.
|
|
639
|
+
* @returns {string[]}
|
|
640
|
+
*/
|
|
641
|
+
getRegisteredAgents() {
|
|
642
|
+
return [...this._contracts.keys()];
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// =========================================================================
|
|
647
|
+
// 3. ComplianceAttestor — continuous real-time compliance
|
|
648
|
+
// =========================================================================
|
|
649
|
+
|
|
650
|
+
/** Compliance frameworks with their requirements mapped to observable signals. */
|
|
651
|
+
const ATTESTATION_FRAMEWORKS = Object.freeze({
|
|
652
|
+
'OWASP-LLM-2025': {
|
|
653
|
+
name: 'OWASP LLM Top 10 (2025)',
|
|
654
|
+
requirements: [
|
|
655
|
+
{ id: 'LLM01', name: 'Prompt Injection', signal: 'injection_scans_active', weight: 3 },
|
|
656
|
+
{ id: 'LLM02', name: 'Sensitive Info Disclosure', signal: 'pii_scanning_active', weight: 3 },
|
|
657
|
+
{ id: 'LLM05', name: 'Improper Output Handling', signal: 'output_scanning_active', weight: 2 },
|
|
658
|
+
{ id: 'LLM06', name: 'Excessive Agency', signal: 'tool_authorization_active', weight: 3 },
|
|
659
|
+
{ id: 'LLM07', name: 'System Prompt Leakage', signal: 'prompt_leak_scanning', weight: 2 },
|
|
660
|
+
{ id: 'LLM08', name: 'Vector/Embedding Weakness', signal: 'rag_scanning_active', weight: 1 },
|
|
661
|
+
{ id: 'LLM10', name: 'Unbounded Consumption', signal: 'rate_limiting_active', weight: 2 }
|
|
662
|
+
]
|
|
663
|
+
},
|
|
664
|
+
'NIST-AI-RMF': {
|
|
665
|
+
name: 'NIST AI Risk Management Framework',
|
|
666
|
+
requirements: [
|
|
667
|
+
{ id: 'GOVERN-1', name: 'Policies & Procedures', signal: 'policy_engine_active', weight: 2 },
|
|
668
|
+
{ id: 'MAP-1', name: 'Risk Identification', signal: 'threat_scanning_active', weight: 2 },
|
|
669
|
+
{ id: 'MEASURE-1', name: 'Risk Measurement', signal: 'behavior_monitoring_active', weight: 2 },
|
|
670
|
+
{ id: 'MANAGE-1', name: 'Risk Response', signal: 'blocking_enabled', weight: 3 },
|
|
671
|
+
{ id: 'MONITOR-1', name: 'Continuous Monitoring', signal: 'audit_trail_active', weight: 3 }
|
|
672
|
+
]
|
|
673
|
+
},
|
|
674
|
+
'EU-AI-ACT': {
|
|
675
|
+
name: 'EU AI Act',
|
|
676
|
+
requirements: [
|
|
677
|
+
{ id: 'ART-9', name: 'Risk Management', signal: 'threat_scanning_active', weight: 3 },
|
|
678
|
+
{ id: 'ART-10', name: 'Data Governance', signal: 'pii_scanning_active', weight: 2 },
|
|
679
|
+
{ id: 'ART-12', name: 'Record Keeping', signal: 'audit_trail_active', weight: 3 },
|
|
680
|
+
{ id: 'ART-13', name: 'Transparency', signal: 'contract_enforcement_active', weight: 2 },
|
|
681
|
+
{ id: 'ART-14', name: 'Human Oversight', signal: 'human_approval_gates', weight: 2 },
|
|
682
|
+
{ id: 'ART-15', name: 'Accuracy & Robustness', signal: 'behavior_monitoring_active', weight: 2 }
|
|
683
|
+
]
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Continuous compliance attestation engine.
|
|
689
|
+
* Instead of generating reports, it maintains a live compliance state
|
|
690
|
+
* that updates with every action the system takes.
|
|
691
|
+
*
|
|
692
|
+
* At any moment, you can ask: "Are we compliant?" and get a real-time answer.
|
|
693
|
+
*/
|
|
694
|
+
class ComplianceAttestor {
|
|
695
|
+
/**
|
|
696
|
+
* @param {object} [options]
|
|
697
|
+
* @param {string[]} [options.frameworks] - Which frameworks to attest against
|
|
698
|
+
* @param {number} [options.attestationIntervalMs=30000] - How often to re-evaluate (default 30s)
|
|
699
|
+
* @param {Function} [options.onComplianceDrift] - Callback when compliance drops
|
|
700
|
+
* @param {number} [options.driftThreshold=0.9] - Compliance rate below this triggers drift alert
|
|
701
|
+
*/
|
|
702
|
+
constructor(options = {}) {
|
|
703
|
+
this._frameworks = options.frameworks || Object.keys(ATTESTATION_FRAMEWORKS);
|
|
704
|
+
this._driftThreshold = options.driftThreshold || 0.9;
|
|
705
|
+
this._onComplianceDrift = options.onComplianceDrift || null;
|
|
706
|
+
|
|
707
|
+
// Live signal state — updated by the runtime
|
|
708
|
+
this._signals = {
|
|
709
|
+
injection_scans_active: false,
|
|
710
|
+
pii_scanning_active: false,
|
|
711
|
+
output_scanning_active: false,
|
|
712
|
+
tool_authorization_active: false,
|
|
713
|
+
prompt_leak_scanning: false,
|
|
714
|
+
rag_scanning_active: false,
|
|
715
|
+
rate_limiting_active: false,
|
|
716
|
+
policy_engine_active: false,
|
|
717
|
+
threat_scanning_active: false,
|
|
718
|
+
behavior_monitoring_active: false,
|
|
719
|
+
blocking_enabled: false,
|
|
720
|
+
audit_trail_active: false,
|
|
721
|
+
contract_enforcement_active: false,
|
|
722
|
+
human_approval_gates: false
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
// Attestation history — proof over time
|
|
726
|
+
this._attestations = [];
|
|
727
|
+
this._maxAttestations = 10000;
|
|
728
|
+
|
|
729
|
+
// Current compliance state
|
|
730
|
+
this._currentState = {};
|
|
731
|
+
this._lastAttestationTime = 0;
|
|
732
|
+
|
|
733
|
+
this.stats = { attestations: 0, driftsDetected: 0 };
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Updates a compliance signal. Call this as the runtime operates.
|
|
738
|
+
* @param {string} signal - Signal name
|
|
739
|
+
* @param {boolean} value - Whether the signal is active
|
|
740
|
+
*/
|
|
741
|
+
updateSignal(signal, value) {
|
|
742
|
+
if (signal in this._signals) {
|
|
743
|
+
this._signals[signal] = value;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Updates multiple signals at once.
|
|
749
|
+
* @param {object} signals - { signalName: boolean }
|
|
750
|
+
*/
|
|
751
|
+
updateSignals(signals) {
|
|
752
|
+
for (const [key, value] of Object.entries(signals)) {
|
|
753
|
+
if (key in this._signals) {
|
|
754
|
+
this._signals[key] = value;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Generates a real-time attestation — the current compliance state.
|
|
761
|
+
* This is the core API. Call it anytime to get proof of compliance.
|
|
762
|
+
*
|
|
763
|
+
* @returns {{ compliant: boolean, overallScore: number, frameworks: object, timestamp: number, attestationId: string }}
|
|
764
|
+
*/
|
|
765
|
+
attest() {
|
|
766
|
+
this.stats.attestations++;
|
|
767
|
+
const timestamp = Date.now();
|
|
768
|
+
const attestationId = crypto.randomUUID();
|
|
769
|
+
const frameworks = {};
|
|
770
|
+
let totalScore = 0;
|
|
771
|
+
let totalWeight = 0;
|
|
772
|
+
|
|
773
|
+
for (const frameworkId of this._frameworks) {
|
|
774
|
+
const framework = ATTESTATION_FRAMEWORKS[frameworkId];
|
|
775
|
+
if (!framework) continue;
|
|
776
|
+
|
|
777
|
+
let fwScore = 0;
|
|
778
|
+
let fwWeight = 0;
|
|
779
|
+
const requirements = [];
|
|
780
|
+
|
|
781
|
+
for (const req of framework.requirements) {
|
|
782
|
+
const met = this._signals[req.signal] === true;
|
|
783
|
+
fwWeight += req.weight;
|
|
784
|
+
if (met) fwScore += req.weight;
|
|
785
|
+
|
|
786
|
+
requirements.push({
|
|
787
|
+
id: req.id,
|
|
788
|
+
name: req.name,
|
|
789
|
+
signal: req.signal,
|
|
790
|
+
met,
|
|
791
|
+
weight: req.weight
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const complianceRate = fwWeight > 0 ? Math.round(fwScore / fwWeight * 100) / 100 : 0;
|
|
796
|
+
frameworks[frameworkId] = {
|
|
797
|
+
name: framework.name,
|
|
798
|
+
complianceRate,
|
|
799
|
+
score: fwScore,
|
|
800
|
+
maxScore: fwWeight,
|
|
801
|
+
requirements
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
totalScore += fwScore;
|
|
805
|
+
totalWeight += fwWeight;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const overallScore = totalWeight > 0 ? Math.round(totalScore / totalWeight * 100) / 100 : 0;
|
|
809
|
+
const compliant = overallScore >= this._driftThreshold;
|
|
810
|
+
|
|
811
|
+
const attestation = {
|
|
812
|
+
attestationId,
|
|
813
|
+
timestamp,
|
|
814
|
+
compliant,
|
|
815
|
+
overallScore,
|
|
816
|
+
frameworks,
|
|
817
|
+
signals: { ...this._signals }
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
// Record attestation
|
|
821
|
+
if (this._attestations.length >= this._maxAttestations) {
|
|
822
|
+
this._attestations = this._attestations.slice(-Math.floor(this._maxAttestations * 0.75));
|
|
823
|
+
}
|
|
824
|
+
this._attestations.push({
|
|
825
|
+
attestationId,
|
|
826
|
+
timestamp,
|
|
827
|
+
compliant,
|
|
828
|
+
overallScore
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
// Check for compliance drift
|
|
832
|
+
if (!compliant && this._onComplianceDrift) {
|
|
833
|
+
this.stats.driftsDetected++;
|
|
834
|
+
try {
|
|
835
|
+
this._onComplianceDrift({
|
|
836
|
+
attestationId,
|
|
837
|
+
overallScore,
|
|
838
|
+
threshold: this._driftThreshold,
|
|
839
|
+
failedFrameworks: Object.entries(frameworks)
|
|
840
|
+
.filter(([_k, v]) => v.complianceRate < this._driftThreshold)
|
|
841
|
+
.map(([k, v]) => ({ framework: k, rate: v.complianceRate }))
|
|
842
|
+
});
|
|
843
|
+
} catch (_e) { /* */ }
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
this._currentState = attestation;
|
|
847
|
+
this._lastAttestationTime = timestamp;
|
|
848
|
+
|
|
849
|
+
return attestation;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Returns the most recent attestation without re-computing.
|
|
854
|
+
* @returns {object|null}
|
|
855
|
+
*/
|
|
856
|
+
getCurrentState() {
|
|
857
|
+
return this._currentState || null;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Returns compliance trend over time.
|
|
862
|
+
* @param {number} [limit=100]
|
|
863
|
+
* @returns {Array}
|
|
864
|
+
*/
|
|
865
|
+
getHistory(limit = 100) {
|
|
866
|
+
return this._attestations.slice(-limit);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* Returns the compliance trend direction.
|
|
871
|
+
* @returns {'improving' | 'stable' | 'degrading' | 'unknown'}
|
|
872
|
+
*/
|
|
873
|
+
getTrend() {
|
|
874
|
+
const recent = this._attestations.slice(-20);
|
|
875
|
+
if (recent.length < 5) return 'unknown';
|
|
876
|
+
|
|
877
|
+
const midpoint = Math.floor(recent.length / 2);
|
|
878
|
+
const firstHalf = recent.slice(0, midpoint);
|
|
879
|
+
const secondHalf = recent.slice(midpoint);
|
|
880
|
+
|
|
881
|
+
const avgFirst = firstHalf.reduce((s, a) => s + a.overallScore, 0) / firstHalf.length;
|
|
882
|
+
const avgSecond = secondHalf.reduce((s, a) => s + a.overallScore, 0) / secondHalf.length;
|
|
883
|
+
|
|
884
|
+
if (avgSecond > avgFirst + 0.05) return 'improving';
|
|
885
|
+
if (avgSecond < avgFirst - 0.05) return 'degrading';
|
|
886
|
+
return 'stable';
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Generates a signed attestation proof that can be verified externally.
|
|
891
|
+
* @param {string} signingKey - HMAC key for signing
|
|
892
|
+
* @returns {{ attestation: object, signature: string }}
|
|
893
|
+
*/
|
|
894
|
+
generateProof(signingKey) {
|
|
895
|
+
const attestation = this.attest();
|
|
896
|
+
const data = JSON.stringify({
|
|
897
|
+
attestationId: attestation.attestationId,
|
|
898
|
+
timestamp: attestation.timestamp,
|
|
899
|
+
compliant: attestation.compliant,
|
|
900
|
+
overallScore: attestation.overallScore
|
|
901
|
+
});
|
|
902
|
+
const signature = crypto.createHmac('sha256', signingKey).update(data).digest('hex');
|
|
903
|
+
return { attestation, signature };
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* Verifies a signed attestation proof.
|
|
908
|
+
* @param {object} proof - Output of generateProof()
|
|
909
|
+
* @param {string} signingKey
|
|
910
|
+
* @returns {boolean}
|
|
911
|
+
*/
|
|
912
|
+
static verifyProof(proof, signingKey) {
|
|
913
|
+
if (!proof || !proof.attestation || !proof.signature) return false;
|
|
914
|
+
const data = JSON.stringify({
|
|
915
|
+
attestationId: proof.attestation.attestationId,
|
|
916
|
+
timestamp: proof.attestation.timestamp,
|
|
917
|
+
compliant: proof.attestation.compliant,
|
|
918
|
+
overallScore: proof.attestation.overallScore
|
|
919
|
+
});
|
|
920
|
+
const expected = crypto.createHmac('sha256', signingKey).update(data).digest('hex');
|
|
921
|
+
try {
|
|
922
|
+
return crypto.timingSafeEqual(
|
|
923
|
+
Buffer.from(proof.signature, 'hex'),
|
|
924
|
+
Buffer.from(expected, 'hex')
|
|
925
|
+
);
|
|
926
|
+
} catch {
|
|
927
|
+
return false;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// =========================================================================
|
|
933
|
+
// Exports
|
|
934
|
+
// =========================================================================
|
|
935
|
+
|
|
936
|
+
module.exports = {
|
|
937
|
+
LearningLoop,
|
|
938
|
+
AgentContract,
|
|
939
|
+
ContractRegistry,
|
|
940
|
+
ComplianceAttestor,
|
|
941
|
+
ATTESTATION_FRAMEWORKS
|
|
942
|
+
};
|
package/src/main.js
CHANGED
|
@@ -59,6 +59,9 @@ const { ERROR_CODES, createShieldError, deprecationWarning } = safeRequire('./er
|
|
|
59
59
|
// v7.0 — MCP Security Runtime
|
|
60
60
|
const { MCPSecurityRuntime, MCPSessionStateMachine, SESSION_STATES } = safeRequire('./mcp-security-runtime', 'mcp-security-runtime');
|
|
61
61
|
|
|
62
|
+
// v7.0 — Adaptive Defense
|
|
63
|
+
const { LearningLoop, AgentContract: BehaviorContract, ContractRegistry, ComplianceAttestor, ATTESTATION_FRAMEWORKS } = safeRequire('./adaptive-defense', 'adaptive-defense');
|
|
64
|
+
|
|
62
65
|
// v7.0 — MCP SDK Integration
|
|
63
66
|
const { shieldMCPServer, createMCPSecurityLayer } = safeRequire('./mcp-sdk-integration', 'mcp-sdk-integration');
|
|
64
67
|
|
|
@@ -713,6 +716,13 @@ const _exports = {
|
|
|
713
716
|
IntentValidator,
|
|
714
717
|
ConfusedDeputyGuard,
|
|
715
718
|
|
|
719
|
+
// v7.0 — Adaptive Defense
|
|
720
|
+
LearningLoop,
|
|
721
|
+
BehaviorContract,
|
|
722
|
+
ContractRegistry,
|
|
723
|
+
ComplianceAttestor,
|
|
724
|
+
ATTESTATION_FRAMEWORKS,
|
|
725
|
+
|
|
716
726
|
// v7.0 — MCP SDK Integration
|
|
717
727
|
shieldMCPServer,
|
|
718
728
|
createMCPSecurityLayer,
|
|
@@ -26,6 +26,7 @@ const crypto = require('crypto');
|
|
|
26
26
|
const { MCPBridge, MCPSessionGuard, MCPResourceScanner, MCPToolPolicy } = require('./mcp-bridge');
|
|
27
27
|
const { AuthorizationContext, ConfusedDeputyGuard } = require('./confused-deputy');
|
|
28
28
|
const { BehaviorProfile } = require('./behavior-profiling');
|
|
29
|
+
const { LearningLoop, ContractRegistry, ComplianceAttestor } = require('./adaptive-defense');
|
|
29
30
|
|
|
30
31
|
const LOG_PREFIX = '[Agent Shield]';
|
|
31
32
|
|
|
@@ -142,6 +143,19 @@ class MCPSecurityRuntime {
|
|
|
142
143
|
this._userSessions = new Map(); // userId → Set<sessionId>
|
|
143
144
|
this._behaviorProfiles = new Map(); // userId → BehaviorProfile
|
|
144
145
|
|
|
146
|
+
// Adaptive defense systems
|
|
147
|
+
this._learningLoop = new LearningLoop({
|
|
148
|
+
minHitsToPromote: options.minHitsToPromote || 3,
|
|
149
|
+
maxLearnedPatterns: options.maxLearnedPatterns || 500,
|
|
150
|
+
onPatternLearned: options.onPatternLearned || null
|
|
151
|
+
});
|
|
152
|
+
this._contracts = new ContractRegistry();
|
|
153
|
+
this._attestor = new ComplianceAttestor({
|
|
154
|
+
frameworks: options.complianceFrameworks,
|
|
155
|
+
driftThreshold: options.complianceDriftThreshold || 0.9,
|
|
156
|
+
onComplianceDrift: options.onComplianceDrift || null
|
|
157
|
+
});
|
|
158
|
+
|
|
145
159
|
// Callbacks
|
|
146
160
|
this._onThreat = options.onThreat || null;
|
|
147
161
|
this._onBlock = options.onBlock || null;
|
|
@@ -159,7 +173,9 @@ class MCPSecurityRuntime {
|
|
|
159
173
|
threatsDetected: 0,
|
|
160
174
|
authFailures: 0,
|
|
161
175
|
behaviorAnomalies: 0,
|
|
162
|
-
stateViolations: 0
|
|
176
|
+
stateViolations: 0,
|
|
177
|
+
patternsLearned: 0,
|
|
178
|
+
contractViolations: 0
|
|
163
179
|
};
|
|
164
180
|
|
|
165
181
|
// Cleanup expired sessions periodically
|
|
@@ -357,11 +373,44 @@ class MCPSecurityRuntime {
|
|
|
357
373
|
// 6. Threat scanning (injection, exfiltration, etc.)
|
|
358
374
|
const scanResult = this._bridge.wrapToolCall(toolName, args);
|
|
359
375
|
const threats = scanResult.threats || [];
|
|
360
|
-
|
|
376
|
+
|
|
377
|
+
// 6a. Check against learned patterns (self-learning loop)
|
|
378
|
+
const learnedCheck = this._learningLoop.check(typeof args === 'string' ? args : JSON.stringify(args || {}));
|
|
379
|
+
if (learnedCheck.matches.length > 0) {
|
|
380
|
+
for (const match of learnedCheck.matches) {
|
|
381
|
+
threats.push({
|
|
382
|
+
category: match.category,
|
|
383
|
+
severity: 'high',
|
|
384
|
+
confidence: match.confidence,
|
|
385
|
+
description: `Learned pattern match: ${match.patternId}`,
|
|
386
|
+
source: 'learning_loop'
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (!scanResult.allowed || learnedCheck.matches.length > 0) {
|
|
361
392
|
this.stats.toolCallsBlocked++;
|
|
362
393
|
this.stats.threatsDetected += threats.length;
|
|
363
|
-
|
|
394
|
+
|
|
395
|
+
// LEARNING LOOP: Feed blocked attack into the learning system
|
|
396
|
+
for (const threat of threats) {
|
|
397
|
+
this._learningLoop.ingest({
|
|
398
|
+
text: typeof args === 'string' ? args : JSON.stringify(args || {}),
|
|
399
|
+
category: threat.category || 'unknown',
|
|
400
|
+
threats,
|
|
401
|
+
toolName,
|
|
402
|
+
sessionId
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
this.stats.patternsLearned = this._learningLoop.getActivePatterns().length;
|
|
406
|
+
|
|
407
|
+
this._audit('threat_detected', { sessionId, toolName, threats, learnedMatches: learnedCheck.matches.length });
|
|
364
408
|
if (this._onThreat) this._onThreat({ sessionId, toolName, threats });
|
|
409
|
+
|
|
410
|
+
// Update compliance signals
|
|
411
|
+
this._attestor.updateSignal('threat_scanning_active', true);
|
|
412
|
+
this._attestor.updateSignal('blocking_enabled', true);
|
|
413
|
+
|
|
365
414
|
return {
|
|
366
415
|
allowed: false,
|
|
367
416
|
threats,
|
|
@@ -372,7 +421,31 @@ class MCPSecurityRuntime {
|
|
|
372
421
|
};
|
|
373
422
|
}
|
|
374
423
|
|
|
375
|
-
// 7.
|
|
424
|
+
// 7. Agent contract enforcement
|
|
425
|
+
const contractResult = this._contracts.enforce(session.authCtx.agentId, {
|
|
426
|
+
type: 'tool_call',
|
|
427
|
+
toolName,
|
|
428
|
+
args,
|
|
429
|
+
intent: session.authCtx.intent,
|
|
430
|
+
delegationDepth: session.authCtx.delegationDepth
|
|
431
|
+
});
|
|
432
|
+
if (!contractResult.allowed) {
|
|
433
|
+
this.stats.contractViolations++;
|
|
434
|
+
this._audit('contract_violation', {
|
|
435
|
+
sessionId, toolName, agentId: session.authCtx.agentId,
|
|
436
|
+
violations: contractResult.violations
|
|
437
|
+
});
|
|
438
|
+
return {
|
|
439
|
+
allowed: false,
|
|
440
|
+
threats: [],
|
|
441
|
+
violations: contractResult.violations,
|
|
442
|
+
anomalies: [],
|
|
443
|
+
token: null,
|
|
444
|
+
reason: 'Contract violation: ' + contractResult.violations.map(v => v.message).join('; ')
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// 8. Behavioral anomaly detection
|
|
376
449
|
const anomalies = [];
|
|
377
450
|
if (this._enableBehavior) {
|
|
378
451
|
const profile = this._behaviorProfiles.get(session.authCtx.userId);
|
|
@@ -395,7 +468,19 @@ class MCPSecurityRuntime {
|
|
|
395
468
|
}
|
|
396
469
|
}
|
|
397
470
|
|
|
398
|
-
//
|
|
471
|
+
// 9. Update compliance attestation signals
|
|
472
|
+
this._attestor.updateSignals({
|
|
473
|
+
injection_scans_active: true,
|
|
474
|
+
tool_authorization_active: this._enforceAuth,
|
|
475
|
+
rate_limiting_active: true,
|
|
476
|
+
threat_scanning_active: true,
|
|
477
|
+
behavior_monitoring_active: this._enableBehavior,
|
|
478
|
+
blocking_enabled: true,
|
|
479
|
+
audit_trail_active: true,
|
|
480
|
+
contract_enforcement_active: contractResult.hasContract
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// 10. Record success and return
|
|
399
484
|
this._audit('tool_allowed', {
|
|
400
485
|
sessionId, toolName, userId: session.authCtx.userId,
|
|
401
486
|
threats: threats.length, anomalies: anomalies.length
|
|
@@ -444,6 +529,61 @@ class MCPSecurityRuntime {
|
|
|
444
529
|
return this._resourceScanner.scanResource(uri, content, mimeType);
|
|
445
530
|
}
|
|
446
531
|
|
|
532
|
+
// =======================================================================
|
|
533
|
+
// Adaptive Defense — Learning, Contracts, Compliance
|
|
534
|
+
// =======================================================================
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Registers a behavioral contract for an agent.
|
|
538
|
+
* The contract is verified on every tool call automatically.
|
|
539
|
+
* @param {import('./adaptive-defense').AgentContract} contract
|
|
540
|
+
*/
|
|
541
|
+
registerContract(contract) {
|
|
542
|
+
this._contracts.register(contract);
|
|
543
|
+
this._attestor.updateSignal('contract_enforcement_active', true);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Returns the learning loop for direct access.
|
|
548
|
+
* @returns {import('./adaptive-defense').LearningLoop}
|
|
549
|
+
*/
|
|
550
|
+
getLearningLoop() {
|
|
551
|
+
return this._learningLoop;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Returns a real-time compliance attestation.
|
|
556
|
+
* @returns {object}
|
|
557
|
+
*/
|
|
558
|
+
attest() {
|
|
559
|
+
return this._attestor.attest();
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Generates a signed compliance proof that can be verified externally.
|
|
564
|
+
* @param {string} signingKey
|
|
565
|
+
* @returns {{ attestation: object, signature: string }}
|
|
566
|
+
*/
|
|
567
|
+
generateComplianceProof(signingKey) {
|
|
568
|
+
return this._attestor.generateProof(signingKey || this._signingKey);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Returns contract compliance rates for all registered agents.
|
|
573
|
+
* @returns {object}
|
|
574
|
+
*/
|
|
575
|
+
getContractCompliance() {
|
|
576
|
+
return this._contracts.getComplianceReport();
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Returns compliance trend direction.
|
|
581
|
+
* @returns {'improving' | 'stable' | 'degrading' | 'unknown'}
|
|
582
|
+
*/
|
|
583
|
+
getComplianceTrend() {
|
|
584
|
+
return this._attestor.getTrend();
|
|
585
|
+
}
|
|
586
|
+
|
|
447
587
|
// =======================================================================
|
|
448
588
|
// Tool Registration
|
|
449
589
|
// =======================================================================
|
|
@@ -639,6 +779,10 @@ class MCPSecurityRuntime {
|
|
|
639
779
|
sessions,
|
|
640
780
|
behaviorProfiles: behaviorSummaries,
|
|
641
781
|
guard: this._guard.getStats(),
|
|
782
|
+
learningLoop: this._learningLoop.getReport(),
|
|
783
|
+
contracts: this._contracts.getComplianceReport(),
|
|
784
|
+
compliance: this._attestor.getCurrentState(),
|
|
785
|
+
complianceTrend: this._attestor.getTrend(),
|
|
642
786
|
recentAudit: this._auditLog.slice(-50)
|
|
643
787
|
};
|
|
644
788
|
}
|