@weave_protocol/domere 1.0.13 → 1.0.14

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.
@@ -0,0 +1,504 @@
1
+ /**
2
+ * Dōmere - Execution Replay & Audit Trail
3
+ *
4
+ * Cryptographically verifiable audit trail for AI agent actions.
5
+ * Enables complete replay and forensic analysis of agent behavior.
6
+ */
7
+
8
+ import * as crypto from 'crypto';
9
+
10
+ // =============================================================================
11
+ // Types
12
+ // =============================================================================
13
+
14
+ export interface ActionRecord {
15
+ id: string;
16
+ thread_id: string;
17
+ sequence: number;
18
+ timestamp: Date;
19
+
20
+ // Actor
21
+ agent_id: string;
22
+ agent_type: 'llm' | 'tool' | 'human' | 'system';
23
+
24
+ // Action details
25
+ action_type: 'inference' | 'tool_call' | 'api_request' | 'file_access' | 'data_read' | 'data_write' | 'delegation' | 'decision';
26
+ action_name: string;
27
+
28
+ // I/O hashes (not raw data for privacy)
29
+ input_hash: string;
30
+ output_hash: string;
31
+ input_size_bytes?: number;
32
+ output_size_bytes?: number;
33
+
34
+ // Optional raw data (encrypted)
35
+ input_encrypted?: string;
36
+ output_encrypted?: string;
37
+
38
+ // Metadata
39
+ latency_ms: number;
40
+ cost_usd?: number;
41
+ tokens_in?: number;
42
+ tokens_out?: number;
43
+ model?: string;
44
+ provider?: string;
45
+
46
+ // Verification
47
+ previous_hash: string;
48
+ action_hash: string;
49
+ signature?: string;
50
+ }
51
+
52
+ export interface ExecutionTrail {
53
+ thread_id: string;
54
+ created_at: Date;
55
+ updated_at: Date;
56
+ action_count: number;
57
+ total_cost_usd: number;
58
+ total_latency_ms: number;
59
+ agents_involved: string[];
60
+ merkle_root: string;
61
+ actions: ActionRecord[];
62
+ integrity_valid: boolean;
63
+ }
64
+
65
+ export interface ReplayOptions {
66
+ from_sequence?: number;
67
+ to_sequence?: number;
68
+ agent_filter?: string[];
69
+ action_type_filter?: ActionRecord['action_type'][];
70
+ include_encrypted?: boolean;
71
+ }
72
+
73
+ export interface AuditQuery {
74
+ thread_id?: string;
75
+ agent_id?: string;
76
+ action_type?: ActionRecord['action_type'];
77
+ start_time?: Date;
78
+ end_time?: Date;
79
+ min_cost_usd?: number;
80
+ min_latency_ms?: number;
81
+ limit?: number;
82
+ }
83
+
84
+ export interface AuditReport {
85
+ query: AuditQuery;
86
+ generated_at: Date;
87
+ total_actions: number;
88
+ total_cost_usd: number;
89
+ total_latency_ms: number;
90
+ actions_by_type: Record<string, number>;
91
+ actions_by_agent: Record<string, number>;
92
+ cost_by_agent: Record<string, number>;
93
+ anomalies: AuditAnomaly[];
94
+ }
95
+
96
+ export interface AuditAnomaly {
97
+ type: 'high_latency' | 'high_cost' | 'repeated_failure' | 'unusual_pattern' | 'integrity_violation';
98
+ severity: 'low' | 'medium' | 'high' | 'critical';
99
+ description: string;
100
+ action_ids: string[];
101
+ detected_at: Date;
102
+ }
103
+
104
+ // =============================================================================
105
+ // Execution Replay Manager
106
+ // =============================================================================
107
+
108
+ export class ExecutionReplayManager {
109
+ private trails: Map<string, ActionRecord[]> = new Map();
110
+ private encryptionKey?: Buffer;
111
+
112
+ constructor(encryptionKey?: string) {
113
+ if (encryptionKey) {
114
+ this.encryptionKey = crypto.scryptSync(encryptionKey, 'domere-audit', 32);
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Record an action in the audit trail
120
+ */
121
+ async recordAction(params: {
122
+ thread_id: string;
123
+ agent_id: string;
124
+ agent_type: ActionRecord['agent_type'];
125
+ action_type: ActionRecord['action_type'];
126
+ action_name: string;
127
+ input: any;
128
+ output: any;
129
+ latency_ms: number;
130
+ cost_usd?: number;
131
+ tokens_in?: number;
132
+ tokens_out?: number;
133
+ model?: string;
134
+ provider?: string;
135
+ store_raw?: boolean;
136
+ }): Promise<ActionRecord> {
137
+ const trail = this.trails.get(params.thread_id) || [];
138
+ const sequence = trail.length;
139
+ const previousHash = sequence > 0 ? trail[sequence - 1].action_hash : '0'.repeat(64);
140
+
141
+ // Hash inputs/outputs
142
+ const inputStr = JSON.stringify(params.input);
143
+ const outputStr = JSON.stringify(params.output);
144
+ const inputHash = crypto.createHash('sha256').update(inputStr).digest('hex');
145
+ const outputHash = crypto.createHash('sha256').update(outputStr).digest('hex');
146
+
147
+ // Create action record
148
+ const record: ActionRecord = {
149
+ id: `act_${crypto.randomUUID()}`,
150
+ thread_id: params.thread_id,
151
+ sequence,
152
+ timestamp: new Date(),
153
+
154
+ agent_id: params.agent_id,
155
+ agent_type: params.agent_type,
156
+
157
+ action_type: params.action_type,
158
+ action_name: params.action_name,
159
+
160
+ input_hash: inputHash,
161
+ output_hash: outputHash,
162
+ input_size_bytes: Buffer.byteLength(inputStr),
163
+ output_size_bytes: Buffer.byteLength(outputStr),
164
+
165
+ latency_ms: params.latency_ms,
166
+ cost_usd: params.cost_usd,
167
+ tokens_in: params.tokens_in,
168
+ tokens_out: params.tokens_out,
169
+ model: params.model,
170
+ provider: params.provider,
171
+
172
+ previous_hash: previousHash,
173
+ action_hash: '', // Computed below
174
+ };
175
+
176
+ // Optionally encrypt and store raw data
177
+ if (params.store_raw && this.encryptionKey) {
178
+ record.input_encrypted = this.encrypt(inputStr);
179
+ record.output_encrypted = this.encrypt(outputStr);
180
+ }
181
+
182
+ // Compute action hash (chain integrity)
183
+ record.action_hash = this.computeActionHash(record);
184
+
185
+ // Store
186
+ trail.push(record);
187
+ this.trails.set(params.thread_id, trail);
188
+
189
+ return record;
190
+ }
191
+
192
+ /**
193
+ * Get complete execution trail for a thread
194
+ */
195
+ async getExecutionTrail(threadId: string, options?: ReplayOptions): Promise<ExecutionTrail | null> {
196
+ const actions = this.trails.get(threadId);
197
+ if (!actions || actions.length === 0) return null;
198
+
199
+ let filtered = [...actions];
200
+
201
+ // Apply filters
202
+ if (options?.from_sequence !== undefined) {
203
+ filtered = filtered.filter(a => a.sequence >= options.from_sequence!);
204
+ }
205
+ if (options?.to_sequence !== undefined) {
206
+ filtered = filtered.filter(a => a.sequence <= options.to_sequence!);
207
+ }
208
+ if (options?.agent_filter?.length) {
209
+ filtered = filtered.filter(a => options.agent_filter!.includes(a.agent_id));
210
+ }
211
+ if (options?.action_type_filter?.length) {
212
+ filtered = filtered.filter(a => options.action_type_filter!.includes(a.action_type));
213
+ }
214
+
215
+ // Remove encrypted data if not requested
216
+ if (!options?.include_encrypted) {
217
+ filtered = filtered.map(a => {
218
+ const { input_encrypted, output_encrypted, ...rest } = a;
219
+ return rest as ActionRecord;
220
+ });
221
+ }
222
+
223
+ // Compute stats
224
+ const totalCost = filtered.reduce((sum, a) => sum + (a.cost_usd || 0), 0);
225
+ const totalLatency = filtered.reduce((sum, a) => sum + a.latency_ms, 0);
226
+ const agents = [...new Set(filtered.map(a => a.agent_id))];
227
+
228
+ // Verify integrity
229
+ const integrityValid = this.verifyTrailIntegrity(actions);
230
+
231
+ return {
232
+ thread_id: threadId,
233
+ created_at: actions[0].timestamp,
234
+ updated_at: actions[actions.length - 1].timestamp,
235
+ action_count: filtered.length,
236
+ total_cost_usd: totalCost,
237
+ total_latency_ms: totalLatency,
238
+ agents_involved: agents,
239
+ merkle_root: this.computeMerkleRoot(actions),
240
+ actions: filtered,
241
+ integrity_valid: integrityValid,
242
+ };
243
+ }
244
+
245
+ /**
246
+ * Replay actions for debugging/analysis
247
+ */
248
+ async replayActions(threadId: string, options?: ReplayOptions): Promise<{
249
+ actions: ActionRecord[];
250
+ timeline: { timestamp: Date; description: string }[];
251
+ summary: {
252
+ total_actions: number;
253
+ duration_ms: number;
254
+ cost_usd: number;
255
+ agents: string[];
256
+ };
257
+ }> {
258
+ const trail = await this.getExecutionTrail(threadId, options);
259
+ if (!trail) {
260
+ throw new Error(`No trail found for thread ${threadId}`);
261
+ }
262
+
263
+ const timeline = trail.actions.map(action => ({
264
+ timestamp: action.timestamp,
265
+ description: `[${action.agent_id}] ${action.action_type}: ${action.action_name} (${action.latency_ms}ms)`,
266
+ }));
267
+
268
+ const duration = trail.actions.length > 1
269
+ ? trail.actions[trail.actions.length - 1].timestamp.getTime() - trail.actions[0].timestamp.getTime()
270
+ : 0;
271
+
272
+ return {
273
+ actions: trail.actions,
274
+ timeline,
275
+ summary: {
276
+ total_actions: trail.action_count,
277
+ duration_ms: duration,
278
+ cost_usd: trail.total_cost_usd,
279
+ agents: trail.agents_involved,
280
+ },
281
+ };
282
+ }
283
+
284
+ /**
285
+ * Query actions across threads
286
+ */
287
+ async queryActions(query: AuditQuery): Promise<ActionRecord[]> {
288
+ let results: ActionRecord[] = [];
289
+
290
+ for (const [threadId, actions] of this.trails) {
291
+ if (query.thread_id && threadId !== query.thread_id) continue;
292
+
293
+ for (const action of actions) {
294
+ if (query.agent_id && action.agent_id !== query.agent_id) continue;
295
+ if (query.action_type && action.action_type !== query.action_type) continue;
296
+ if (query.start_time && action.timestamp < query.start_time) continue;
297
+ if (query.end_time && action.timestamp > query.end_time) continue;
298
+ if (query.min_cost_usd && (action.cost_usd || 0) < query.min_cost_usd) continue;
299
+ if (query.min_latency_ms && action.latency_ms < query.min_latency_ms) continue;
300
+
301
+ results.push(action);
302
+ }
303
+ }
304
+
305
+ // Sort by timestamp
306
+ results.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
307
+
308
+ // Apply limit
309
+ if (query.limit) {
310
+ results = results.slice(0, query.limit);
311
+ }
312
+
313
+ return results;
314
+ }
315
+
316
+ /**
317
+ * Generate audit report
318
+ */
319
+ async generateAuditReport(query: AuditQuery): Promise<AuditReport> {
320
+ const actions = await this.queryActions(query);
321
+
322
+ const actionsByType: Record<string, number> = {};
323
+ const actionsByAgent: Record<string, number> = {};
324
+ const costByAgent: Record<string, number> = {};
325
+ let totalCost = 0;
326
+ let totalLatency = 0;
327
+
328
+ for (const action of actions) {
329
+ actionsByType[action.action_type] = (actionsByType[action.action_type] || 0) + 1;
330
+ actionsByAgent[action.agent_id] = (actionsByAgent[action.agent_id] || 0) + 1;
331
+ costByAgent[action.agent_id] = (costByAgent[action.agent_id] || 0) + (action.cost_usd || 0);
332
+ totalCost += action.cost_usd || 0;
333
+ totalLatency += action.latency_ms;
334
+ }
335
+
336
+ // Detect anomalies
337
+ const anomalies = this.detectAnomalies(actions);
338
+
339
+ return {
340
+ query,
341
+ generated_at: new Date(),
342
+ total_actions: actions.length,
343
+ total_cost_usd: totalCost,
344
+ total_latency_ms: totalLatency,
345
+ actions_by_type: actionsByType,
346
+ actions_by_agent: actionsByAgent,
347
+ cost_by_agent: costByAgent,
348
+ anomalies,
349
+ };
350
+ }
351
+
352
+ /**
353
+ * Verify trail integrity
354
+ */
355
+ verifyTrailIntegrity(actions: ActionRecord[]): boolean {
356
+ if (actions.length === 0) return true;
357
+
358
+ for (let i = 0; i < actions.length; i++) {
359
+ const action = actions[i];
360
+ const expectedPrevHash = i === 0 ? '0'.repeat(64) : actions[i - 1].action_hash;
361
+
362
+ if (action.previous_hash !== expectedPrevHash) {
363
+ return false;
364
+ }
365
+
366
+ const computedHash = this.computeActionHash(action);
367
+ if (action.action_hash !== computedHash) {
368
+ return false;
369
+ }
370
+ }
371
+
372
+ return true;
373
+ }
374
+
375
+ /**
376
+ * Export trail for external storage/verification
377
+ */
378
+ async exportTrail(threadId: string): Promise<string> {
379
+ const trail = await this.getExecutionTrail(threadId, { include_encrypted: true });
380
+ if (!trail) throw new Error(`No trail found for thread ${threadId}`);
381
+
382
+ return JSON.stringify(trail, null, 2);
383
+ }
384
+
385
+ /**
386
+ * Import trail from external source
387
+ */
388
+ async importTrail(data: string): Promise<{ thread_id: string; actions_imported: number; valid: boolean }> {
389
+ const trail: ExecutionTrail = JSON.parse(data);
390
+
391
+ // Verify integrity
392
+ const valid = this.verifyTrailIntegrity(trail.actions);
393
+
394
+ // Store
395
+ this.trails.set(trail.thread_id, trail.actions);
396
+
397
+ return {
398
+ thread_id: trail.thread_id,
399
+ actions_imported: trail.actions.length,
400
+ valid,
401
+ };
402
+ }
403
+
404
+ // ===========================================================================
405
+ // Private Methods
406
+ // ===========================================================================
407
+
408
+ private computeActionHash(action: ActionRecord): string {
409
+ const data = [
410
+ action.thread_id,
411
+ action.sequence.toString(),
412
+ action.agent_id,
413
+ action.action_type,
414
+ action.action_name,
415
+ action.input_hash,
416
+ action.output_hash,
417
+ action.timestamp.toISOString(),
418
+ action.previous_hash,
419
+ ].join('|');
420
+
421
+ return crypto.createHash('sha256').update(data).digest('hex');
422
+ }
423
+
424
+ private computeMerkleRoot(actions: ActionRecord[]): string {
425
+ if (actions.length === 0) return '0'.repeat(64);
426
+
427
+ let hashes = actions.map(a => a.action_hash);
428
+
429
+ while (hashes.length > 1) {
430
+ const newHashes: string[] = [];
431
+ for (let i = 0; i < hashes.length; i += 2) {
432
+ const left = hashes[i];
433
+ const right = hashes[i + 1] || left;
434
+ const combined = crypto.createHash('sha256').update(left + right).digest('hex');
435
+ newHashes.push(combined);
436
+ }
437
+ hashes = newHashes;
438
+ }
439
+
440
+ return hashes[0];
441
+ }
442
+
443
+ private encrypt(data: string): string {
444
+ if (!this.encryptionKey) throw new Error('Encryption key not set');
445
+
446
+ const iv = crypto.randomBytes(16);
447
+ const cipher = crypto.createCipheriv('aes-256-gcm', this.encryptionKey, iv);
448
+
449
+ let encrypted = cipher.update(data, 'utf8', 'hex');
450
+ encrypted += cipher.final('hex');
451
+
452
+ const authTag = cipher.getAuthTag();
453
+
454
+ return iv.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted;
455
+ }
456
+
457
+ private decrypt(encrypted: string): string {
458
+ if (!this.encryptionKey) throw new Error('Encryption key not set');
459
+
460
+ const [ivHex, authTagHex, data] = encrypted.split(':');
461
+ const iv = Buffer.from(ivHex, 'hex');
462
+ const authTag = Buffer.from(authTagHex, 'hex');
463
+
464
+ const decipher = crypto.createDecipheriv('aes-256-gcm', this.encryptionKey, iv);
465
+ decipher.setAuthTag(authTag);
466
+
467
+ let decrypted = decipher.update(data, 'hex', 'utf8');
468
+ decrypted += decipher.final('utf8');
469
+
470
+ return decrypted;
471
+ }
472
+
473
+ private detectAnomalies(actions: ActionRecord[]): AuditAnomaly[] {
474
+ const anomalies: AuditAnomaly[] = [];
475
+
476
+ // High latency detection (>10s)
477
+ const highLatency = actions.filter(a => a.latency_ms > 10000);
478
+ if (highLatency.length > 0) {
479
+ anomalies.push({
480
+ type: 'high_latency',
481
+ severity: 'medium',
482
+ description: `${highLatency.length} actions exceeded 10s latency`,
483
+ action_ids: highLatency.map(a => a.id),
484
+ detected_at: new Date(),
485
+ });
486
+ }
487
+
488
+ // High cost detection (>$1 per action)
489
+ const highCost = actions.filter(a => (a.cost_usd || 0) > 1);
490
+ if (highCost.length > 0) {
491
+ anomalies.push({
492
+ type: 'high_cost',
493
+ severity: 'high',
494
+ description: `${highCost.length} actions exceeded $1 cost`,
495
+ action_ids: highCost.map(a => a.id),
496
+ detected_at: new Date(),
497
+ });
498
+ }
499
+
500
+ return anomalies;
501
+ }
502
+ }
503
+
504
+ export default ExecutionReplayManager;