agentshield-sdk 7.0.0 → 7.2.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 +28 -0
- package/README.md +43 -9
- package/package.json +4 -2
- package/src/adaptive-defense.js +942 -0
- package/src/ipia-detector.js +821 -0
- package/src/main.js +24 -0
- package/src/mcp-security-runtime.js +149 -5
- package/types/index.d.ts +69 -0
|
@@ -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
|
+
};
|