@ruvector/edge-net 0.5.0 → 0.5.3

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,942 @@
1
+ /**
2
+ * Edge-Net Core Invariants
3
+ *
4
+ * Cogito, Creo, Codex — The system thinks collectively, creates through
5
+ * contribution, and codifies trust in cryptographic proof.
6
+ *
7
+ * These invariants are NOT configurable. They define what Edge-net IS.
8
+ *
9
+ * @module @ruvector/edge-net/core-invariants
10
+ */
11
+
12
+ import { EventEmitter } from 'events';
13
+ import { createHash, randomBytes } from 'crypto';
14
+
15
+ // ============================================
16
+ // INVARIANT 1: ECONOMIC SETTLEMENT ISOLATION
17
+ // ============================================
18
+
19
+ /**
20
+ * EconomicBoundary - Enforces that plugins can NEVER execute settlement
21
+ *
22
+ * Core operations (immutable):
23
+ * - rUv minting
24
+ * - rUv burning
25
+ * - Credit settlement
26
+ * - Balance enforcement
27
+ * - Slashing execution
28
+ *
29
+ * Plugin operations (read-only):
30
+ * - Pricing suggestions
31
+ * - Reputation scoring
32
+ * - Economic modeling
33
+ * - Auction mechanisms
34
+ */
35
+ export class EconomicBoundary {
36
+ constructor(creditSystem) {
37
+ this.creditSystem = creditSystem;
38
+ this.settlementLock = false;
39
+ this.coreOperationCounts = {
40
+ mint: 0,
41
+ burn: 0,
42
+ settle: 0,
43
+ slash: 0,
44
+ };
45
+
46
+ // Seal the boundary - plugins get a read-only proxy
47
+ this._sealed = false;
48
+ }
49
+
50
+ /**
51
+ * Get a read-only view for plugins
52
+ * Plugins can observe but NEVER modify
53
+ */
54
+ getPluginView() {
55
+ return Object.freeze({
56
+ // Read-only accessors
57
+ getBalance: (nodeId) => this.creditSystem.getBalance(nodeId),
58
+ getTransactionHistory: (nodeId, limit) =>
59
+ this.creditSystem.getTransactionHistory(nodeId, limit),
60
+ getSummary: () => {
61
+ const summary = this.creditSystem.getSummary();
62
+ // Remove sensitive internal state
63
+ delete summary.recentTransactions;
64
+ return Object.freeze(summary);
65
+ },
66
+
67
+ // Event subscription (read-only)
68
+ on: (event, handler) => {
69
+ // Only allow observation events
70
+ const allowedEvents = [
71
+ 'credits-earned',
72
+ 'credits-spent',
73
+ 'insufficient-funds',
74
+ ];
75
+ if (allowedEvents.includes(event)) {
76
+ this.creditSystem.on(event, (data) => {
77
+ // Clone data to prevent mutation
78
+ handler(JSON.parse(JSON.stringify(data)));
79
+ });
80
+ }
81
+ },
82
+
83
+ // Explicit denial methods (throw if called)
84
+ mint: () => {
85
+ throw new Error('INVARIANT VIOLATION: Plugins cannot mint credits');
86
+ },
87
+ burn: () => {
88
+ throw new Error('INVARIANT VIOLATION: Plugins cannot burn credits');
89
+ },
90
+ settle: () => {
91
+ throw new Error('INVARIANT VIOLATION: Plugins cannot settle credits');
92
+ },
93
+ transfer: () => {
94
+ throw new Error('INVARIANT VIOLATION: Plugins cannot transfer credits');
95
+ },
96
+ });
97
+ }
98
+
99
+ /**
100
+ * Core-only: Mint credits (bootstrap, rewards)
101
+ * @private - Only callable from core system
102
+ */
103
+ _coreMint(nodeId, amount, reason, proofOfWork) {
104
+ if (!proofOfWork) {
105
+ throw new Error('INVARIANT: Minting requires proof of work');
106
+ }
107
+
108
+ this.coreOperationCounts.mint++;
109
+ return this.creditSystem.ledger.credit(amount, JSON.stringify({
110
+ type: 'core_mint',
111
+ reason,
112
+ proofHash: createHash('sha256').update(JSON.stringify(proofOfWork)).digest('hex'),
113
+ timestamp: Date.now(),
114
+ }));
115
+ }
116
+
117
+ /**
118
+ * Core-only: Burn credits (slashing, expiry)
119
+ * @private - Only callable from core system
120
+ */
121
+ _coreBurn(nodeId, amount, reason, evidence) {
122
+ this.coreOperationCounts.burn++;
123
+ return this.creditSystem.ledger.debit(amount, JSON.stringify({
124
+ type: 'core_burn',
125
+ reason,
126
+ evidenceHash: evidence ? createHash('sha256').update(JSON.stringify(evidence)).digest('hex') : null,
127
+ timestamp: Date.now(),
128
+ }));
129
+ }
130
+
131
+ /**
132
+ * Core-only: Execute slashing
133
+ * @private - Only callable from core system
134
+ */
135
+ _coreSlash(nodeId, amount, violation, evidence) {
136
+ this.coreOperationCounts.slash++;
137
+
138
+ // Record slashing event
139
+ const slashRecord = {
140
+ type: 'slash',
141
+ nodeId,
142
+ amount,
143
+ violation,
144
+ evidenceHash: createHash('sha256').update(JSON.stringify(evidence)).digest('hex'),
145
+ timestamp: Date.now(),
146
+ };
147
+
148
+ // Execute burn
149
+ this._coreBurn(nodeId, amount, `Slashed: ${violation}`, evidence);
150
+
151
+ return slashRecord;
152
+ }
153
+ }
154
+
155
+ // ============================================
156
+ // INVARIANT 2: IDENTITY ANTI-SYBIL MEASURES
157
+ // ============================================
158
+
159
+ /**
160
+ * IdentityFriction - Prevents Sybil attacks on the plugin marketplace
161
+ *
162
+ * Mechanisms:
163
+ * - Delayed activation (24h window)
164
+ * - Reputation warm-up (0.1 → 1.0 over 100 tasks)
165
+ * - Stake requirement for priority
166
+ * - Witness diversity requirement
167
+ */
168
+ export class IdentityFriction extends EventEmitter {
169
+ constructor(options = {}) {
170
+ super();
171
+
172
+ this.config = {
173
+ // Delayed activation
174
+ activationDelayMs: options.activationDelayMs ?? 24 * 60 * 60 * 1000, // 24 hours
175
+
176
+ // Reputation warm-up
177
+ initialReputation: options.initialReputation ?? 0.1,
178
+ maxReputation: options.maxReputation ?? 1.0,
179
+ warmupTasks: options.warmupTasks ?? 100,
180
+
181
+ // Stake requirements
182
+ stakeForPriority: options.stakeForPriority ?? 10, // rUv
183
+ stakeSlashPercent: options.stakeSlashPercent ?? 0.5, // 50%
184
+
185
+ // Witness diversity
186
+ minWitnessDiversity: options.minWitnessDiversity ?? 3,
187
+ witnessDiversityWindowMs: options.witnessDiversityWindowMs ?? 7 * 24 * 60 * 60 * 1000, // 7 days
188
+ };
189
+
190
+ // Identity registry
191
+ this.identities = new Map(); // nodeId -> IdentityRecord
192
+ this._activationTimers = new Map(); // nodeId -> timerId (for cleanup)
193
+ }
194
+
195
+ /**
196
+ * Register a new identity
197
+ */
198
+ registerIdentity(nodeId, publicKey) {
199
+ if (this.identities.has(nodeId)) {
200
+ throw new Error('Identity already registered');
201
+ }
202
+
203
+ const record = {
204
+ nodeId,
205
+ publicKeyHash: createHash('sha256').update(publicKey).digest('hex'),
206
+ createdAt: Date.now(),
207
+ activatedAt: null, // Set after delay
208
+ reputation: this.config.initialReputation,
209
+ tasksCompleted: 0,
210
+ stake: 0,
211
+ witnesses: [], // { nodeId, createdAt, attestedAt }
212
+ status: 'pending', // pending | active | suspended | slashed
213
+ };
214
+
215
+ this.identities.set(nodeId, record);
216
+
217
+ // Schedule activation with tracked timer
218
+ const timerId = setTimeout(() => {
219
+ this._activationTimers.delete(nodeId);
220
+ this._activateIdentity(nodeId);
221
+ }, this.config.activationDelayMs);
222
+ this._activationTimers.set(nodeId, timerId);
223
+
224
+ this.emit('identity:registered', { nodeId, activatesAt: Date.now() + this.config.activationDelayMs });
225
+
226
+ return record;
227
+ }
228
+
229
+ /**
230
+ * Activate identity after delay
231
+ * @private
232
+ */
233
+ _activateIdentity(nodeId) {
234
+ const record = this.identities.get(nodeId);
235
+ if (!record) return;
236
+
237
+ if (record.status === 'pending') {
238
+ record.activatedAt = Date.now();
239
+ record.status = 'active';
240
+ this.emit('identity:activated', { nodeId });
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Check if identity can execute tasks
246
+ */
247
+ canExecuteTasks(nodeId) {
248
+ const record = this.identities.get(nodeId);
249
+ if (!record) return { allowed: false, reason: 'Unknown identity' };
250
+
251
+ if (record.status === 'pending') {
252
+ const remainingMs = (record.createdAt + this.config.activationDelayMs) - Date.now();
253
+ return {
254
+ allowed: false,
255
+ reason: 'Pending activation',
256
+ remainingMs: Math.max(0, remainingMs),
257
+ };
258
+ }
259
+
260
+ if (record.status === 'suspended' || record.status === 'slashed') {
261
+ return { allowed: false, reason: `Identity ${record.status}` };
262
+ }
263
+
264
+ return { allowed: true, reputation: record.reputation };
265
+ }
266
+
267
+ /**
268
+ * Record task completion and update reputation
269
+ */
270
+ recordTaskCompletion(nodeId, taskId, success, witnesses = []) {
271
+ const record = this.identities.get(nodeId);
272
+ if (!record) return null;
273
+
274
+ if (success) {
275
+ record.tasksCompleted++;
276
+
277
+ // Warm-up reputation curve
278
+ const progress = Math.min(record.tasksCompleted / this.config.warmupTasks, 1);
279
+ const reputationRange = this.config.maxReputation - this.config.initialReputation;
280
+ record.reputation = this.config.initialReputation + (reputationRange * progress);
281
+
282
+ // Record witnesses
283
+ for (const witness of witnesses) {
284
+ const witnessRecord = this.identities.get(witness.nodeId);
285
+ if (witnessRecord && this._isWitnessDiverse(record, witnessRecord)) {
286
+ record.witnesses.push({
287
+ nodeId: witness.nodeId,
288
+ createdAt: witnessRecord.createdAt,
289
+ attestedAt: Date.now(),
290
+ });
291
+ }
292
+ }
293
+ }
294
+
295
+ this.emit('task:recorded', {
296
+ nodeId,
297
+ taskId,
298
+ success,
299
+ reputation: record.reputation,
300
+ tasksCompleted: record.tasksCompleted,
301
+ });
302
+
303
+ return record;
304
+ }
305
+
306
+ /**
307
+ * Check witness diversity (different creation times)
308
+ * @private
309
+ */
310
+ _isWitnessDiverse(identity, witness) {
311
+ // Witnesses must be created at different times (not Sybil cluster)
312
+ const timeDiff = Math.abs(identity.createdAt - witness.createdAt);
313
+ return timeDiff > this.config.witnessDiversityWindowMs;
314
+ }
315
+
316
+ /**
317
+ * Stake for priority access
318
+ */
319
+ stake(nodeId, amount) {
320
+ const record = this.identities.get(nodeId);
321
+ if (!record) throw new Error('Unknown identity');
322
+
323
+ record.stake += amount;
324
+ this.emit('identity:staked', { nodeId, amount, totalStake: record.stake });
325
+
326
+ return record;
327
+ }
328
+
329
+ /**
330
+ * Slash stake for violations
331
+ */
332
+ slashStake(nodeId, violation, evidence) {
333
+ const record = this.identities.get(nodeId);
334
+ if (!record) throw new Error('Unknown identity');
335
+
336
+ const slashAmount = Math.floor(record.stake * this.config.stakeSlashPercent);
337
+ record.stake -= slashAmount;
338
+
339
+ // Reduce reputation
340
+ record.reputation = Math.max(this.config.initialReputation, record.reputation * 0.5);
341
+
342
+ if (record.stake <= 0 && record.reputation <= this.config.initialReputation) {
343
+ record.status = 'slashed';
344
+ }
345
+
346
+ this.emit('identity:slashed', {
347
+ nodeId,
348
+ violation,
349
+ slashAmount,
350
+ remainingStake: record.stake,
351
+ reputation: record.reputation,
352
+ status: record.status,
353
+ });
354
+
355
+ return { slashAmount, record };
356
+ }
357
+
358
+ /**
359
+ * Get identity status
360
+ */
361
+ getIdentity(nodeId) {
362
+ const record = this.identities.get(nodeId);
363
+ if (!record) return null;
364
+
365
+ return {
366
+ nodeId: record.nodeId,
367
+ status: record.status,
368
+ reputation: record.reputation,
369
+ tasksCompleted: record.tasksCompleted,
370
+ stake: record.stake,
371
+ witnessCount: record.witnesses.length,
372
+ createdAt: record.createdAt,
373
+ activatedAt: record.activatedAt,
374
+ };
375
+ }
376
+
377
+ /**
378
+ * Check if identity has priority (staked)
379
+ */
380
+ hasPriority(nodeId) {
381
+ const record = this.identities.get(nodeId);
382
+ if (!record) return false;
383
+ return record.stake >= this.config.stakeForPriority;
384
+ }
385
+
386
+ /**
387
+ * Clean up all timers and resources
388
+ */
389
+ destroy() {
390
+ // Clear all activation timers
391
+ for (const timerId of this._activationTimers.values()) {
392
+ clearTimeout(timerId);
393
+ }
394
+ this._activationTimers.clear();
395
+
396
+ // Clear identity data
397
+ this.identities.clear();
398
+
399
+ this.removeAllListeners();
400
+ }
401
+ }
402
+
403
+ // ============================================
404
+ // INVARIANT 3: VERIFIABLE WORK
405
+ // ============================================
406
+
407
+ /**
408
+ * WorkVerifier - Ensures "Verifiable work or no reward"
409
+ *
410
+ * All credit issuance requires cryptographic proof of work completion.
411
+ * Unverifiable claims are rejected, not trusted.
412
+ */
413
+ export class WorkVerifier extends EventEmitter {
414
+ constructor(options = {}) {
415
+ super();
416
+
417
+ this.config = {
418
+ // Redundancy for verification
419
+ redundancyPercent: options.redundancyPercent ?? 0.05, // 5% of tasks
420
+ redundancyThreshold: options.redundancyThreshold ?? 2, // 2 matching results
421
+
422
+ // Challenger system
423
+ challengeWindowMs: options.challengeWindowMs ?? 60 * 60 * 1000, // 1 hour
424
+ challengeStake: options.challengeStake ?? 5, // rUv to challenge
425
+ challengeReward: options.challengeReward ?? 10, // rUv if challenge succeeds
426
+
427
+ // Result hashing
428
+ hashAlgorithm: 'sha256',
429
+ };
430
+
431
+ // Pending verifications
432
+ this.pendingWork = new Map(); // taskId -> WorkRecord
433
+ this.challenges = new Map(); // taskId -> Challenge[]
434
+ }
435
+
436
+ /**
437
+ * Submit work for verification
438
+ */
439
+ submitWork(taskId, nodeId, result, executionProof) {
440
+ const resultHash = this._hashResult(result);
441
+ const proofHash = createHash(this.config.hashAlgorithm)
442
+ .update(JSON.stringify(executionProof))
443
+ .digest('hex');
444
+
445
+ const workRecord = {
446
+ taskId,
447
+ nodeId,
448
+ resultHash,
449
+ proofHash,
450
+ submittedAt: Date.now(),
451
+ challengeDeadline: Date.now() + this.config.challengeWindowMs,
452
+ status: 'pending', // pending | verified | challenged | rejected
453
+ redundantResults: [],
454
+ };
455
+
456
+ this.pendingWork.set(taskId, workRecord);
457
+
458
+ // Check if this should be redundantly executed
459
+ if (Math.random() < this.config.redundancyPercent) {
460
+ workRecord.requiresRedundancy = true;
461
+ }
462
+
463
+ this.emit('work:submitted', { taskId, nodeId, resultHash });
464
+
465
+ return workRecord;
466
+ }
467
+
468
+ /**
469
+ * Submit redundant execution result
470
+ */
471
+ submitRedundantResult(taskId, nodeId, result) {
472
+ const workRecord = this.pendingWork.get(taskId);
473
+ if (!workRecord) throw new Error('Unknown task');
474
+
475
+ const resultHash = this._hashResult(result);
476
+
477
+ workRecord.redundantResults.push({
478
+ nodeId,
479
+ resultHash,
480
+ submittedAt: Date.now(),
481
+ });
482
+
483
+ // Check for consensus
484
+ const matchingResults = workRecord.redundantResults.filter(
485
+ r => r.resultHash === workRecord.resultHash
486
+ );
487
+
488
+ if (matchingResults.length >= this.config.redundancyThreshold) {
489
+ workRecord.status = 'verified';
490
+ this.emit('work:verified', { taskId, method: 'redundancy' });
491
+ }
492
+
493
+ return workRecord;
494
+ }
495
+
496
+ /**
497
+ * Challenge a work result
498
+ */
499
+ challenge(taskId, challengerNodeId, stake, evidence) {
500
+ const workRecord = this.pendingWork.get(taskId);
501
+ if (!workRecord) throw new Error('Unknown task');
502
+
503
+ if (Date.now() > workRecord.challengeDeadline) {
504
+ throw new Error('Challenge window closed');
505
+ }
506
+
507
+ if (stake < this.config.challengeStake) {
508
+ throw new Error(`Insufficient stake: ${stake} < ${this.config.challengeStake}`);
509
+ }
510
+
511
+ const challenge = {
512
+ challengerId: challengerNodeId,
513
+ stake,
514
+ evidenceHash: createHash(this.config.hashAlgorithm)
515
+ .update(JSON.stringify(evidence))
516
+ .digest('hex'),
517
+ submittedAt: Date.now(),
518
+ status: 'pending', // pending | accepted | rejected
519
+ };
520
+
521
+ if (!this.challenges.has(taskId)) {
522
+ this.challenges.set(taskId, []);
523
+ }
524
+ this.challenges.get(taskId).push(challenge);
525
+
526
+ workRecord.status = 'challenged';
527
+
528
+ this.emit('work:challenged', { taskId, challengerId: challengerNodeId });
529
+
530
+ return challenge;
531
+ }
532
+
533
+ /**
534
+ * Resolve a challenge (requires arbitration)
535
+ */
536
+ resolveChallenge(taskId, challengeIndex, isValid, arbitrationProof) {
537
+ const workRecord = this.pendingWork.get(taskId);
538
+ const challenges = this.challenges.get(taskId);
539
+
540
+ if (!workRecord || !challenges || !challenges[challengeIndex]) {
541
+ throw new Error('Challenge not found');
542
+ }
543
+
544
+ const challenge = challenges[challengeIndex];
545
+
546
+ if (isValid) {
547
+ // Challenge succeeded - work was invalid
548
+ challenge.status = 'accepted';
549
+ workRecord.status = 'rejected';
550
+
551
+ this.emit('challenge:accepted', {
552
+ taskId,
553
+ challengerId: challenge.challengerId,
554
+ reward: this.config.challengeReward,
555
+ });
556
+
557
+ return {
558
+ challengerReward: this.config.challengeReward + challenge.stake,
559
+ workerSlash: true,
560
+ };
561
+ } else {
562
+ // Challenge failed - work was valid
563
+ challenge.status = 'rejected';
564
+ workRecord.status = 'verified';
565
+
566
+ this.emit('challenge:rejected', {
567
+ taskId,
568
+ challengerId: challenge.challengerId,
569
+ stakeLost: challenge.stake,
570
+ });
571
+
572
+ return {
573
+ challengerLoss: challenge.stake,
574
+ workerReward: challenge.stake * 0.5, // Half of stake goes to worker
575
+ };
576
+ }
577
+ }
578
+
579
+ /**
580
+ * Finalize work after challenge window
581
+ */
582
+ finalizeWork(taskId) {
583
+ const workRecord = this.pendingWork.get(taskId);
584
+ if (!workRecord) throw new Error('Unknown task');
585
+
586
+ if (Date.now() < workRecord.challengeDeadline) {
587
+ throw new Error('Challenge window still open');
588
+ }
589
+
590
+ if (workRecord.status === 'pending') {
591
+ // No challenges and redundancy passed (if required)
592
+ if (workRecord.requiresRedundancy) {
593
+ const matchingResults = workRecord.redundantResults.filter(
594
+ r => r.resultHash === workRecord.resultHash
595
+ );
596
+ if (matchingResults.length < this.config.redundancyThreshold) {
597
+ workRecord.status = 'rejected';
598
+ this.emit('work:rejected', { taskId, reason: 'Insufficient redundancy' });
599
+ return workRecord;
600
+ }
601
+ }
602
+
603
+ workRecord.status = 'verified';
604
+ this.emit('work:finalized', { taskId });
605
+ }
606
+
607
+ return workRecord;
608
+ }
609
+
610
+ /**
611
+ * Check if work is verified
612
+ */
613
+ isVerified(taskId) {
614
+ const workRecord = this.pendingWork.get(taskId);
615
+ return workRecord?.status === 'verified';
616
+ }
617
+
618
+ /**
619
+ * Hash result deterministically
620
+ * @private
621
+ */
622
+ _hashResult(result) {
623
+ // Canonical JSON serialization with deep key sorting
624
+ const sortKeys = (obj) => {
625
+ if (obj === null || typeof obj !== 'object') return obj;
626
+ if (Array.isArray(obj)) return obj.map(sortKeys);
627
+ return Object.keys(obj).sort().reduce((acc, key) => {
628
+ acc[key] = sortKeys(obj[key]);
629
+ return acc;
630
+ }, {});
631
+ };
632
+ const canonical = JSON.stringify(sortKeys(result));
633
+ return createHash(this.config.hashAlgorithm).update(canonical).digest('hex');
634
+ }
635
+
636
+ /**
637
+ * Clean up old work records to prevent memory leak
638
+ */
639
+ pruneOldRecords(maxAgeMs = 24 * 60 * 60 * 1000) {
640
+ const cutoff = Date.now() - maxAgeMs;
641
+ let pruned = 0;
642
+
643
+ for (const [taskId, record] of this.pendingWork) {
644
+ if (record.submittedAt < cutoff && record.status !== 'pending') {
645
+ this.pendingWork.delete(taskId);
646
+ this.challenges.delete(taskId);
647
+ pruned++;
648
+ }
649
+ }
650
+
651
+ return pruned;
652
+ }
653
+ }
654
+
655
+ // ============================================
656
+ // INVARIANT 4: DEGRADATION OVER HALT
657
+ // ============================================
658
+
659
+ /**
660
+ * DegradationController - Ensures system never halts
661
+ *
662
+ * The system degrades gracefully under load, attack, or partial failure.
663
+ * Consistency is sacrificed before availability.
664
+ */
665
+ export class DegradationController extends EventEmitter {
666
+ constructor(options = {}) {
667
+ super();
668
+
669
+ this.config = {
670
+ // Load thresholds
671
+ warningLoadPercent: options.warningLoadPercent ?? 70,
672
+ criticalLoadPercent: options.criticalLoadPercent ?? 90,
673
+ maxLoadPercent: options.maxLoadPercent ?? 100,
674
+
675
+ // Degradation levels
676
+ levels: ['normal', 'elevated', 'degraded', 'emergency'],
677
+
678
+ // Auto-recovery
679
+ recoveryCheckIntervalMs: options.recoveryCheckIntervalMs ?? 30000,
680
+
681
+ // Memory management
682
+ maxHistorySize: options.maxHistorySize ?? 1000,
683
+ };
684
+
685
+ this.currentLevel = 'normal';
686
+ // Protected metrics keys - prevents prototype pollution
687
+ this._allowedMetricKeys = new Set(['cpuLoad', 'memoryUsage', 'pendingTasks', 'errorRate']);
688
+ this.metrics = {
689
+ cpuLoad: 0,
690
+ memoryUsage: 0,
691
+ pendingTasks: 0,
692
+ errorRate: 0,
693
+ lastUpdated: Date.now(),
694
+ };
695
+
696
+ this.degradationHistory = [];
697
+ }
698
+
699
+ /**
700
+ * Update system metrics
701
+ * Protected against prototype pollution - only allowed keys are updated
702
+ */
703
+ updateMetrics(metrics) {
704
+ // Only copy allowed metric keys to prevent prototype pollution
705
+ for (const key of this._allowedMetricKeys) {
706
+ if (Object.hasOwn(metrics, key) && typeof metrics[key] === 'number') {
707
+ this.metrics[key] = metrics[key];
708
+ }
709
+ }
710
+ this.metrics.lastUpdated = Date.now();
711
+
712
+ this._evaluateDegradation();
713
+ }
714
+
715
+ /**
716
+ * Evaluate and apply degradation level
717
+ * @private
718
+ */
719
+ _evaluateDegradation() {
720
+ const load = this._calculateOverallLoad();
721
+ const previousLevel = this.currentLevel;
722
+
723
+ if (load >= this.config.maxLoadPercent) {
724
+ this.currentLevel = 'emergency';
725
+ } else if (load >= this.config.criticalLoadPercent) {
726
+ this.currentLevel = 'degraded';
727
+ } else if (load >= this.config.warningLoadPercent) {
728
+ this.currentLevel = 'elevated';
729
+ } else {
730
+ this.currentLevel = 'normal';
731
+ }
732
+
733
+ if (this.currentLevel !== previousLevel) {
734
+ this.degradationHistory.push({
735
+ from: previousLevel,
736
+ to: this.currentLevel,
737
+ load,
738
+ timestamp: Date.now(),
739
+ });
740
+
741
+ // Prune history to prevent memory leak
742
+ if (this.degradationHistory.length > this.config.maxHistorySize) {
743
+ this.degradationHistory.splice(0, this.degradationHistory.length - this.config.maxHistorySize);
744
+ }
745
+
746
+ this.emit('level:changed', {
747
+ from: previousLevel,
748
+ to: this.currentLevel,
749
+ load,
750
+ });
751
+ }
752
+ }
753
+
754
+ /**
755
+ * Calculate overall load
756
+ * @private
757
+ */
758
+ _calculateOverallLoad() {
759
+ return Math.max(
760
+ this.metrics.cpuLoad || 0,
761
+ this.metrics.memoryUsage || 0,
762
+ (this.metrics.errorRate || 0) * 100
763
+ );
764
+ }
765
+
766
+ /**
767
+ * Get current degradation policy
768
+ */
769
+ getPolicy() {
770
+ const policies = {
771
+ normal: {
772
+ acceptNewTasks: true,
773
+ pluginsEnabled: true,
774
+ redundancyEnabled: true,
775
+ maxConcurrency: Infinity,
776
+ taskTimeout: 30000,
777
+ },
778
+ elevated: {
779
+ acceptNewTasks: true,
780
+ pluginsEnabled: true,
781
+ redundancyEnabled: false, // Disable redundancy to reduce load
782
+ maxConcurrency: 100,
783
+ taskTimeout: 20000,
784
+ },
785
+ degraded: {
786
+ acceptNewTasks: true,
787
+ pluginsEnabled: false, // Disable non-core plugins
788
+ redundancyEnabled: false,
789
+ maxConcurrency: 50,
790
+ taskTimeout: 10000,
791
+ },
792
+ emergency: {
793
+ acceptNewTasks: false, // Shed load
794
+ pluginsEnabled: false,
795
+ redundancyEnabled: false,
796
+ maxConcurrency: 10,
797
+ taskTimeout: 5000,
798
+ },
799
+ };
800
+
801
+ return {
802
+ level: this.currentLevel,
803
+ ...policies[this.currentLevel],
804
+ };
805
+ }
806
+
807
+ /**
808
+ * Check if action is allowed under current policy
809
+ */
810
+ isAllowed(action) {
811
+ const policy = this.getPolicy();
812
+
813
+ switch (action) {
814
+ case 'accept_task':
815
+ return policy.acceptNewTasks;
816
+ case 'load_plugin':
817
+ return policy.pluginsEnabled;
818
+ case 'redundant_execution':
819
+ return policy.redundancyEnabled;
820
+ default:
821
+ return true;
822
+ }
823
+ }
824
+
825
+ /**
826
+ * Get current status
827
+ */
828
+ getStatus() {
829
+ return {
830
+ level: this.currentLevel,
831
+ metrics: { ...this.metrics },
832
+ policy: this.getPolicy(),
833
+ history: this.degradationHistory.slice(-10),
834
+ };
835
+ }
836
+ }
837
+
838
+ // ============================================
839
+ // CORE SYSTEM ORCHESTRATOR
840
+ // ============================================
841
+
842
+ /**
843
+ * CoreInvariants - Orchestrates all invariant enforcement
844
+ *
845
+ * Cogito, Creo, Codex
846
+ */
847
+ export class CoreInvariants extends EventEmitter {
848
+ constructor(creditSystem, options = {}) {
849
+ super();
850
+
851
+ // Initialize all invariant enforcers
852
+ this.economicBoundary = new EconomicBoundary(creditSystem);
853
+ this.identityFriction = new IdentityFriction(options.identity);
854
+ this.workVerifier = new WorkVerifier(options.verification);
855
+ this.degradationController = new DegradationController(options.degradation);
856
+
857
+ // Cross-wire events
858
+ this._wireEvents();
859
+
860
+ console.log('[CoreInvariants] Cogito, Creo, Codex — Invariants initialized');
861
+ }
862
+
863
+ /**
864
+ * Wire cross-component events
865
+ * @private
866
+ */
867
+ _wireEvents() {
868
+ // Slash identity when work is rejected
869
+ this.workVerifier.on('work:rejected', ({ taskId }) => {
870
+ const work = this.workVerifier.pendingWork.get(taskId);
871
+ if (work) {
872
+ this.identityFriction.slashStake(
873
+ work.nodeId,
874
+ 'work_rejected',
875
+ { taskId }
876
+ );
877
+ }
878
+ });
879
+
880
+ // Emit unified events
881
+ this.identityFriction.on('identity:slashed', (data) => {
882
+ this.emit('invariant:slashing', data);
883
+ });
884
+
885
+ this.degradationController.on('level:changed', (data) => {
886
+ this.emit('invariant:degradation', data);
887
+ });
888
+ }
889
+
890
+ /**
891
+ * Get plugin-safe view of economic system
892
+ */
893
+ getPluginEconomicView() {
894
+ return this.economicBoundary.getPluginView();
895
+ }
896
+
897
+ /**
898
+ * Register new identity with friction
899
+ */
900
+ registerIdentity(nodeId, publicKey) {
901
+ return this.identityFriction.registerIdentity(nodeId, publicKey);
902
+ }
903
+
904
+ /**
905
+ * Submit and verify work
906
+ */
907
+ submitWork(taskId, nodeId, result, proof) {
908
+ // Check identity can execute
909
+ const canExecute = this.identityFriction.canExecuteTasks(nodeId);
910
+ if (!canExecute.allowed) {
911
+ throw new Error(`Identity cannot execute: ${canExecute.reason}`);
912
+ }
913
+
914
+ // Check degradation allows
915
+ if (!this.degradationController.isAllowed('accept_task')) {
916
+ throw new Error('System in emergency mode, shedding load');
917
+ }
918
+
919
+ return this.workVerifier.submitWork(taskId, nodeId, result, proof);
920
+ }
921
+
922
+ /**
923
+ * Update system load metrics
924
+ */
925
+ updateMetrics(metrics) {
926
+ this.degradationController.updateMetrics(metrics);
927
+ }
928
+
929
+ /**
930
+ * Get comprehensive status
931
+ */
932
+ getStatus() {
933
+ return {
934
+ degradation: this.degradationController.getStatus(),
935
+ pendingVerifications: this.workVerifier.pendingWork.size,
936
+ activeChallenges: this.workVerifier.challenges.size,
937
+ identityCount: this.identityFriction.identities.size,
938
+ };
939
+ }
940
+ }
941
+
942
+ export default CoreInvariants;