@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.
@@ -1,20 +1,392 @@
1
1
  /**
2
2
  * Edge-Net Secure Plugin Loader
3
3
  *
4
+ * Cogito, Creo, Codex — Plugins extend, Core enforces.
5
+ *
4
6
  * Features:
5
7
  * - Ed25519 signature verification
6
8
  * - SHA-256 integrity checks
7
9
  * - Lazy loading with caching
8
10
  * - Capability-based sandboxing
11
+ * - Plugin Failure Isolation
12
+ * - Economic boundary enforcement
9
13
  * - Zero telemetry
10
14
  *
11
15
  * @module @ruvector/edge-net/plugins/loader
12
16
  */
13
17
 
14
18
  import { EventEmitter } from 'events';
15
- import { createHash } from 'crypto';
19
+ import { createHash, createVerify, generateKeyPairSync, sign, verify } from 'crypto';
16
20
  import { PLUGIN_CATALOG, PLUGIN_BUNDLES, Capability, PluginTier } from './plugin-manifest.js';
17
21
 
22
+ // ============================================
23
+ // PLUGIN FAILURE CONTRACT
24
+ // ============================================
25
+
26
+ /**
27
+ * PluginFailureContract - Defines what happens when plugins fail
28
+ *
29
+ * Contract guarantees:
30
+ * 1. Plugin failures NEVER crash core
31
+ * 2. Failed plugins enter quarantine
32
+ * 3. Core continues with degraded functionality
33
+ * 4. Failures are logged for diagnostics
34
+ */
35
+ export class PluginFailureContract extends EventEmitter {
36
+ constructor(options = {}) {
37
+ super();
38
+
39
+ this.config = {
40
+ // Retry policy
41
+ maxRetries: options.maxRetries ?? 3,
42
+ retryDelayMs: options.retryDelayMs ?? 1000,
43
+ retryBackoffMultiplier: options.retryBackoffMultiplier ?? 2,
44
+
45
+ // Quarantine policy
46
+ quarantineDurationMs: options.quarantineDurationMs ?? 5 * 60 * 1000, // 5 minutes
47
+ maxQuarantineCount: options.maxQuarantineCount ?? 3,
48
+
49
+ // Timeout policy
50
+ executionTimeoutMs: options.executionTimeoutMs ?? 5000,
51
+
52
+ // Memory management
53
+ maxFailureHistory: options.maxFailureHistory ?? 100, // Limit failure records per plugin
54
+ };
55
+
56
+ // Failure tracking
57
+ this.failures = new Map(); // pluginId -> FailureRecord[]
58
+ this.quarantine = new Map(); // pluginId -> QuarantineRecord
59
+ this.circuitBreakers = new Map(); // pluginId -> { open, openedAt, failures }
60
+ this._quarantineTimers = new Map(); // pluginId -> timerId (prevent timer stacking)
61
+ }
62
+
63
+ /**
64
+ * Record a plugin failure
65
+ */
66
+ recordFailure(pluginId, error, context = {}) {
67
+ if (!this.failures.has(pluginId)) {
68
+ this.failures.set(pluginId, []);
69
+ }
70
+
71
+ const record = {
72
+ error: error.message,
73
+ stack: error.stack,
74
+ context,
75
+ timestamp: Date.now(),
76
+ };
77
+
78
+ const failures = this.failures.get(pluginId);
79
+ failures.push(record);
80
+
81
+ // Prune old failures to prevent memory leak
82
+ if (failures.length > this.config.maxFailureHistory) {
83
+ failures.splice(0, failures.length - this.config.maxFailureHistory);
84
+ }
85
+
86
+ // Update circuit breaker
87
+ this._updateCircuitBreaker(pluginId);
88
+
89
+ this.emit('plugin:failure', { pluginId, ...record });
90
+
91
+ return record;
92
+ }
93
+
94
+ /**
95
+ * Update circuit breaker state
96
+ * @private
97
+ */
98
+ _updateCircuitBreaker(pluginId) {
99
+ if (!this.circuitBreakers.has(pluginId)) {
100
+ this.circuitBreakers.set(pluginId, { open: false, openedAt: null, failures: 0 });
101
+ }
102
+
103
+ const breaker = this.circuitBreakers.get(pluginId);
104
+ breaker.failures++;
105
+
106
+ if (breaker.failures >= this.config.maxRetries) {
107
+ breaker.open = true;
108
+ breaker.openedAt = Date.now();
109
+ this._quarantinePlugin(pluginId, 'circuit_breaker_tripped');
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Quarantine a failed plugin
115
+ * @private
116
+ */
117
+ _quarantinePlugin(pluginId, reason) {
118
+ const existingQuarantine = this.quarantine.get(pluginId);
119
+ const quarantineCount = existingQuarantine ? existingQuarantine.count + 1 : 1;
120
+
121
+ const record = {
122
+ pluginId,
123
+ reason,
124
+ count: quarantineCount,
125
+ startedAt: Date.now(),
126
+ expiresAt: Date.now() + this.config.quarantineDurationMs,
127
+ permanent: quarantineCount >= this.config.maxQuarantineCount,
128
+ };
129
+
130
+ this.quarantine.set(pluginId, record);
131
+
132
+ this.emit('plugin:quarantined', record);
133
+
134
+ // Clear existing timer to prevent stacking
135
+ const existingTimer = this._quarantineTimers.get(pluginId);
136
+ if (existingTimer) {
137
+ clearTimeout(existingTimer);
138
+ }
139
+
140
+ // Schedule unquarantine if not permanent
141
+ if (!record.permanent) {
142
+ const timerId = setTimeout(() => {
143
+ this._quarantineTimers.delete(pluginId);
144
+ this._tryUnquarantine(pluginId);
145
+ }, this.config.quarantineDurationMs);
146
+ this._quarantineTimers.set(pluginId, timerId);
147
+ }
148
+
149
+ return record;
150
+ }
151
+
152
+ /**
153
+ * Try to release plugin from quarantine
154
+ * @private
155
+ */
156
+ _tryUnquarantine(pluginId) {
157
+ const record = this.quarantine.get(pluginId);
158
+ if (!record || record.permanent) return;
159
+
160
+ if (Date.now() >= record.expiresAt) {
161
+ // Reset circuit breaker
162
+ const breaker = this.circuitBreakers.get(pluginId);
163
+ if (breaker) {
164
+ breaker.open = false;
165
+ breaker.failures = 0;
166
+ }
167
+
168
+ this.quarantine.delete(pluginId);
169
+ this.emit('plugin:unquarantined', { pluginId });
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Check if plugin can execute
175
+ */
176
+ canExecute(pluginId) {
177
+ const quarantine = this.quarantine.get(pluginId);
178
+ if (quarantine) {
179
+ if (quarantine.permanent) {
180
+ return { allowed: false, reason: 'Permanently quarantined', permanent: true };
181
+ }
182
+ if (Date.now() < quarantine.expiresAt) {
183
+ const remainingMs = quarantine.expiresAt - Date.now();
184
+ return { allowed: false, reason: 'In quarantine', remainingMs };
185
+ }
186
+ }
187
+
188
+ const breaker = this.circuitBreakers.get(pluginId);
189
+ if (breaker?.open) {
190
+ return { allowed: false, reason: 'Circuit breaker open' };
191
+ }
192
+
193
+ return { allowed: true };
194
+ }
195
+
196
+ /**
197
+ * Execute with failure isolation
198
+ */
199
+ async executeIsolated(pluginId, fn, context = {}) {
200
+ const canExec = this.canExecute(pluginId);
201
+ if (!canExec.allowed) {
202
+ throw new Error(`Plugin ${pluginId} blocked: ${canExec.reason}`);
203
+ }
204
+
205
+ // Create timeout with proper cleanup
206
+ let timeoutId;
207
+ const timeoutPromise = new Promise((_, reject) => {
208
+ timeoutId = setTimeout(
209
+ () => reject(new Error('Execution timeout')),
210
+ this.config.executionTimeoutMs
211
+ );
212
+ });
213
+
214
+ try {
215
+ // Execute with timeout
216
+ const result = await Promise.race([fn(), timeoutPromise]);
217
+
218
+ // Success - reset failure count for this plugin
219
+ const breaker = this.circuitBreakers.get(pluginId);
220
+ if (breaker) {
221
+ breaker.failures = Math.max(0, breaker.failures - 1);
222
+ }
223
+
224
+ return result;
225
+ } catch (error) {
226
+ this.recordFailure(pluginId, error, context);
227
+ throw error;
228
+ } finally {
229
+ // Always clean up timeout to prevent memory leak
230
+ clearTimeout(timeoutId);
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Get plugin health status
236
+ */
237
+ getHealth(pluginId) {
238
+ const failures = this.failures.get(pluginId) || [];
239
+ const quarantine = this.quarantine.get(pluginId);
240
+ const breaker = this.circuitBreakers.get(pluginId);
241
+
242
+ return {
243
+ pluginId,
244
+ healthy: !quarantine && !breaker?.open,
245
+ failureCount: failures.length,
246
+ recentFailures: failures.slice(-5),
247
+ quarantine: quarantine ? {
248
+ reason: quarantine.reason,
249
+ permanent: quarantine.permanent,
250
+ expiresAt: quarantine.expiresAt,
251
+ } : null,
252
+ circuitBreaker: breaker ? {
253
+ open: breaker.open,
254
+ failures: breaker.failures,
255
+ } : null,
256
+ };
257
+ }
258
+
259
+ /**
260
+ * Get overall health summary
261
+ */
262
+ getSummary() {
263
+ return {
264
+ totalPlugins: this.circuitBreakers.size,
265
+ quarantined: this.quarantine.size,
266
+ permanentlyQuarantined: Array.from(this.quarantine.values()).filter(q => q.permanent).length,
267
+ circuitBreakersOpen: Array.from(this.circuitBreakers.values()).filter(b => b.open).length,
268
+ };
269
+ }
270
+
271
+ /**
272
+ * Clean up all timers and resources
273
+ */
274
+ destroy() {
275
+ // Clear all quarantine timers
276
+ for (const timerId of this._quarantineTimers.values()) {
277
+ clearTimeout(timerId);
278
+ }
279
+ this._quarantineTimers.clear();
280
+
281
+ // Clear all tracking data
282
+ this.failures.clear();
283
+ this.quarantine.clear();
284
+ this.circuitBreakers.clear();
285
+
286
+ this.removeAllListeners();
287
+ }
288
+ }
289
+
290
+ // ============================================
291
+ // ED25519 SIGNATURE VERIFICATION
292
+ // ============================================
293
+
294
+ /**
295
+ * Built-in trusted public keys for official plugins
296
+ * In production, these would be loaded from a secure registry
297
+ */
298
+ const TRUSTED_PUBLIC_KEYS = {
299
+ 'ruvector': 'MCowBQYDK2VwAyEAMock_ruvector_official_key_replace_in_production_1234567890=',
300
+ 'edge-net-official': 'MCowBQYDK2VwAyEAMock_edgenet_official_key_replace_in_production_abcdef12=',
301
+ };
302
+
303
+ /**
304
+ * Verify Ed25519 signature
305
+ * @param {Buffer|string} data - The data that was signed
306
+ * @param {string} signature - Base64-encoded signature
307
+ * @param {string} publicKey - PEM or DER formatted public key
308
+ * @returns {boolean} - True if signature is valid
309
+ */
310
+ function verifyEd25519Signature(data, signature, publicKey) {
311
+ try {
312
+ const signatureBuffer = Buffer.from(signature, 'base64');
313
+ const dataBuffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
314
+
315
+ // Handle both PEM and raw key formats
316
+ let keyObject;
317
+ if (publicKey.startsWith('-----BEGIN')) {
318
+ keyObject = publicKey;
319
+ } else {
320
+ // Assume base64-encoded DER format
321
+ keyObject = {
322
+ key: Buffer.from(publicKey, 'base64'),
323
+ format: 'der',
324
+ type: 'spki',
325
+ };
326
+ }
327
+
328
+ return verify(null, dataBuffer, keyObject, signatureBuffer);
329
+ } catch (error) {
330
+ // Signature verification failed
331
+ return false;
332
+ }
333
+ }
334
+
335
+ // ============================================
336
+ // RATE LIMITER
337
+ // ============================================
338
+
339
+ class RateLimiter {
340
+ constructor(options = {}) {
341
+ this.maxRequests = options.maxRequests || 100;
342
+ this.windowMs = options.windowMs || 60000; // 1 minute
343
+ this.requests = new Map(); // key -> { count, windowStart }
344
+ }
345
+
346
+ /**
347
+ * Check if action is allowed, returns true if allowed
348
+ */
349
+ check(key) {
350
+ const now = Date.now();
351
+ let record = this.requests.get(key);
352
+
353
+ if (!record || (now - record.windowStart) > this.windowMs) {
354
+ // New window
355
+ record = { count: 0, windowStart: now };
356
+ this.requests.set(key, record);
357
+ }
358
+
359
+ if (record.count >= this.maxRequests) {
360
+ return false;
361
+ }
362
+
363
+ record.count++;
364
+ return true;
365
+ }
366
+
367
+ /**
368
+ * Get remaining requests in current window
369
+ */
370
+ remaining(key) {
371
+ const record = this.requests.get(key);
372
+ if (!record) return this.maxRequests;
373
+
374
+ const now = Date.now();
375
+ if ((now - record.windowStart) > this.windowMs) {
376
+ return this.maxRequests;
377
+ }
378
+
379
+ return Math.max(0, this.maxRequests - record.count);
380
+ }
381
+
382
+ /**
383
+ * Reset limits for a key
384
+ */
385
+ reset(key) {
386
+ this.requests.delete(key);
387
+ }
388
+ }
389
+
18
390
  // ============================================
19
391
  // PLUGIN LOADER
20
392
  // ============================================
@@ -28,6 +400,7 @@ export class PluginLoader extends EventEmitter {
28
400
  verifySignatures: options.verifySignatures ?? true,
29
401
  allowedTiers: options.allowedTiers ?? [PluginTier.STABLE, PluginTier.BETA],
30
402
  trustedAuthors: options.trustedAuthors ?? ['ruvector', 'edge-net-official'],
403
+ trustedPublicKeys: options.trustedPublicKeys ?? {},
31
404
 
32
405
  // Loading
33
406
  lazyLoad: options.lazyLoad ?? true,
@@ -37,6 +410,20 @@ export class PluginLoader extends EventEmitter {
37
410
  // Permissions
38
411
  maxCapabilities: options.maxCapabilities ?? 10,
39
412
  deniedCapabilities: options.deniedCapabilities ?? [Capability.SYSTEM_EXEC],
413
+
414
+ // Rate limiting
415
+ rateLimitEnabled: options.rateLimitEnabled ?? true,
416
+ rateLimitRequests: options.rateLimitRequests ?? 100,
417
+ rateLimitWindowMs: options.rateLimitWindowMs ?? 60000,
418
+
419
+ // Failure isolation
420
+ failureIsolation: options.failureIsolation ?? true,
421
+ maxRetries: options.maxRetries ?? 3,
422
+ quarantineDurationMs: options.quarantineDurationMs ?? 5 * 60 * 1000,
423
+ executionTimeoutMs: options.executionTimeoutMs ?? 5000,
424
+
425
+ // Economic boundary (CoreInvariants integration)
426
+ coreInvariants: options.coreInvariants ?? null,
40
427
  ...options,
41
428
  };
42
429
 
@@ -45,15 +432,46 @@ export class PluginLoader extends EventEmitter {
45
432
  this.pluginConfigs = new Map(); // id -> config
46
433
  this.pendingLoads = new Map(); // id -> Promise
47
434
 
435
+ // Rate limiter
436
+ this.rateLimiter = new RateLimiter({
437
+ maxRequests: this.options.rateLimitRequests,
438
+ windowMs: this.options.rateLimitWindowMs,
439
+ });
440
+
441
+ // Failure isolation contract
442
+ this.failureContract = new PluginFailureContract({
443
+ maxRetries: this.options.maxRetries,
444
+ quarantineDurationMs: this.options.quarantineDurationMs,
445
+ executionTimeoutMs: this.options.executionTimeoutMs,
446
+ });
447
+
448
+ // Wire failure events
449
+ this.failureContract.on('plugin:failure', (data) => {
450
+ this.emit('plugin:failure', data);
451
+ });
452
+ this.failureContract.on('plugin:quarantined', (data) => {
453
+ this.emit('plugin:quarantined', data);
454
+ });
455
+
48
456
  // Stats
49
457
  this.stats = {
50
458
  loaded: 0,
51
459
  cached: 0,
52
460
  verified: 0,
53
461
  rejected: 0,
462
+ rateLimited: 0,
463
+ quarantined: 0,
464
+ failures: 0,
54
465
  };
55
466
  }
56
467
 
468
+ /**
469
+ * Set CoreInvariants for economic boundary enforcement
470
+ */
471
+ setCoreInvariants(coreInvariants) {
472
+ this.options.coreInvariants = coreInvariants;
473
+ }
474
+
57
475
  /**
58
476
  * Get catalog of available plugins
59
477
  */
@@ -99,14 +517,14 @@ export class PluginLoader extends EventEmitter {
99
517
  }
100
518
 
101
519
  /**
102
- * Verify plugin integrity
520
+ * Verify plugin integrity using Ed25519 signatures
103
521
  */
104
522
  async _verifyPlugin(manifest, code) {
105
523
  if (!this.options.verifySignatures) {
106
524
  return { verified: true, reason: 'Verification disabled' };
107
525
  }
108
526
 
109
- // Verify checksum
527
+ // Verify checksum first (fast check)
110
528
  if (manifest.checksum) {
111
529
  const hash = createHash('sha256').update(code).digest('hex');
112
530
  if (hash !== manifest.checksum) {
@@ -114,21 +532,58 @@ export class PluginLoader extends EventEmitter {
114
532
  }
115
533
  }
116
534
 
117
- // Verify signature (simplified - in production use Ed25519)
118
- if (manifest.signature) {
119
- // TODO: Implement Ed25519 signature verification
120
- // For now, trust if author is in trusted list
121
- if (this.options.trustedAuthors.includes(manifest.author)) {
122
- return { verified: true, reason: 'Trusted author' };
535
+ // Verify Ed25519 signature if present
536
+ if (manifest.signature && manifest.author) {
537
+ // Get the public key for this author
538
+ const publicKey = this.options.trustedPublicKeys?.[manifest.author]
539
+ || TRUSTED_PUBLIC_KEYS[manifest.author];
540
+
541
+ if (!publicKey) {
542
+ return {
543
+ verified: false,
544
+ reason: `Unknown author: ${manifest.author}. No public key registered.`
545
+ };
123
546
  }
547
+
548
+ // Create canonical data for signature verification
549
+ // Include manifest fields that should be signed
550
+ const signedData = JSON.stringify({
551
+ id: manifest.id,
552
+ name: manifest.name,
553
+ version: manifest.version,
554
+ author: manifest.author,
555
+ checksum: manifest.checksum,
556
+ });
557
+
558
+ // Verify the signature using Ed25519
559
+ const isValid = verifyEd25519Signature(signedData, manifest.signature, publicKey);
560
+
561
+ if (!isValid) {
562
+ return {
563
+ verified: false,
564
+ reason: 'Ed25519 signature verification failed'
565
+ };
566
+ }
567
+
568
+ return { verified: true, reason: 'Ed25519 signature valid' };
124
569
  }
125
570
 
126
- // If no security metadata, only allow stable tier
127
- if (!manifest.checksum && !manifest.signature) {
128
- if (manifest.tier === PluginTier.STABLE) {
571
+ // Built-in stable plugins from the catalog don't require external signatures
572
+ // They are verified by code review and included in the package
573
+ if (!manifest.signature && !manifest.checksum) {
574
+ const isBuiltIn = PLUGIN_CATALOG[manifest.id] !== undefined;
575
+ if (isBuiltIn && manifest.tier === PluginTier.STABLE) {
129
576
  return { verified: true, reason: 'Built-in stable plugin' };
130
577
  }
131
- return { verified: false, reason: 'No verification metadata' };
578
+ return { verified: false, reason: 'No verification metadata for external plugin' };
579
+ }
580
+
581
+ // Checksum passed but no signature - allow for stable tier only
582
+ if (manifest.checksum && !manifest.signature) {
583
+ if (manifest.tier === PluginTier.STABLE) {
584
+ return { verified: true, reason: 'Checksum verified (stable tier)' };
585
+ }
586
+ return { verified: false, reason: 'Non-stable plugins require signature' };
132
587
  }
133
588
 
134
589
  return { verified: true };
@@ -138,6 +593,18 @@ export class PluginLoader extends EventEmitter {
138
593
  * Load a plugin by ID
139
594
  */
140
595
  async load(pluginId, config = {}) {
596
+ // Rate limit check
597
+ if (this.options.rateLimitEnabled) {
598
+ const rateLimitKey = `load:${pluginId}`;
599
+ if (!this.rateLimiter.check(rateLimitKey)) {
600
+ this.stats.rateLimited++;
601
+ throw new Error(
602
+ `Rate limit exceeded for plugin ${pluginId}. ` +
603
+ `Try again in ${Math.ceil(this.options.rateLimitWindowMs / 1000)}s.`
604
+ );
605
+ }
606
+ }
607
+
141
608
  // Check if already loaded
142
609
  if (this.loadedPlugins.has(pluginId)) {
143
610
  return this.loadedPlugins.get(pluginId);
@@ -206,8 +673,8 @@ export class PluginLoader extends EventEmitter {
206
673
  * Create plugin instance with sandbox
207
674
  */
208
675
  async _createPluginInstance(manifest, config) {
209
- // Create sandboxed context
210
- const sandbox = this._createSandbox(manifest.capabilities || []);
676
+ // Create sandboxed context with proper isolation
677
+ const sandbox = this._createSandbox(manifest.capabilities || [], manifest);
211
678
 
212
679
  // Return plugin wrapper
213
680
  return {
@@ -233,24 +700,145 @@ export class PluginLoader extends EventEmitter {
233
700
  }
234
701
 
235
702
  /**
236
- * Create capability-based sandbox
703
+ * Create capability-based sandbox with proper isolation
237
704
  */
238
- _createSandbox(capabilities) {
705
+ _createSandbox(capabilities, manifest) {
706
+ const allowedCapabilities = new Set(capabilities);
707
+ const deniedGlobals = new Set([
708
+ 'process', 'require', 'eval', 'Function',
709
+ '__dirname', '__filename', 'module', 'exports'
710
+ ]);
711
+
712
+ // Get economic boundary from CoreInvariants if available
713
+ const economicView = this.options.coreInvariants
714
+ ? this.options.coreInvariants.getPluginEconomicView()
715
+ : this._createMockEconomicView();
716
+
717
+ // Create isolated sandbox context
239
718
  const sandbox = {
240
- capabilities: new Set(capabilities),
719
+ // Immutable capability set
720
+ get capabilities() {
721
+ return new Set(allowedCapabilities);
722
+ },
241
723
 
242
724
  hasCapability(cap) {
243
- return this.capabilities.has(cap);
725
+ return allowedCapabilities.has(cap);
244
726
  },
245
727
 
246
728
  require(cap) {
247
- if (!this.hasCapability(cap)) {
729
+ if (!allowedCapabilities.has(cap)) {
248
730
  throw new Error(`Missing capability: ${cap}`);
249
731
  }
250
732
  },
733
+
734
+ // Economic boundary (READ-ONLY)
735
+ // Plugins can observe credits but NEVER modify
736
+ credits: economicView,
737
+
738
+ // Resource limits
739
+ limits: Object.freeze({
740
+ maxMemoryMB: 128,
741
+ maxCpuTimeMs: 5000,
742
+ maxNetworkConnections: 10,
743
+ maxStorageBytes: 10 * 1024 * 1024, // 10MB
744
+ }),
745
+
746
+ // Execution context (read-only)
747
+ context: Object.freeze({
748
+ pluginId: manifest.id,
749
+ pluginVersion: manifest.version,
750
+ startTime: Date.now(),
751
+ }),
752
+
753
+ // Check if global is allowed
754
+ isGlobalAllowed(name) {
755
+ return !deniedGlobals.has(name);
756
+ },
757
+
758
+ // Secure timer functions (returns cleanup functions)
759
+ setTimeout: (fn, delay) => {
760
+ const maxDelay = 30000; // 30 seconds max
761
+ const safeDelay = Math.min(delay, maxDelay);
762
+ const timer = setTimeout(fn, safeDelay);
763
+ return () => clearTimeout(timer);
764
+ },
765
+
766
+ setInterval: (fn, delay) => {
767
+ const minDelay = 100; // Minimum 100ms
768
+ const safeDelay = Math.max(delay, minDelay);
769
+ const timer = setInterval(fn, safeDelay);
770
+ return () => clearInterval(timer);
771
+ },
251
772
  };
252
773
 
253
- return sandbox;
774
+ // Freeze the sandbox to prevent modification
775
+ return Object.freeze(sandbox);
776
+ }
777
+
778
+ /**
779
+ * Create mock economic view when CoreInvariants not available
780
+ * @private
781
+ */
782
+ _createMockEconomicView() {
783
+ return Object.freeze({
784
+ getBalance: () => 0,
785
+ getTransactionHistory: () => [],
786
+ getSummary: () => Object.freeze({ balance: 0, transactions: 0 }),
787
+ on: () => {},
788
+ mint: () => { throw new Error('INVARIANT VIOLATION: Plugins cannot mint credits'); },
789
+ burn: () => { throw new Error('INVARIANT VIOLATION: Plugins cannot burn credits'); },
790
+ settle: () => { throw new Error('INVARIANT VIOLATION: Plugins cannot settle credits'); },
791
+ transfer: () => { throw new Error('INVARIANT VIOLATION: Plugins cannot transfer credits'); },
792
+ });
793
+ }
794
+
795
+ /**
796
+ * Execute plugin function with failure isolation
797
+ * Core NEVER crashes from plugin failures
798
+ */
799
+ async execute(pluginId, fnName, args = []) {
800
+ const plugin = this.loadedPlugins.get(pluginId);
801
+ if (!plugin) {
802
+ throw new Error(`Plugin not loaded: ${pluginId}`);
803
+ }
804
+
805
+ // Check if plugin is allowed to execute
806
+ const canExec = this.failureContract.canExecute(pluginId);
807
+ if (!canExec.allowed) {
808
+ this.stats.quarantined++;
809
+ throw new Error(`Plugin ${pluginId} blocked: ${canExec.reason}`);
810
+ }
811
+
812
+ // Execute with failure isolation
813
+ try {
814
+ const result = await this.failureContract.executeIsolated(
815
+ pluginId,
816
+ async () => {
817
+ const fn = plugin.api?.[fnName] || plugin[fnName];
818
+ if (typeof fn !== 'function') {
819
+ throw new Error(`Plugin ${pluginId} has no function: ${fnName}`);
820
+ }
821
+ return fn.apply(plugin, args);
822
+ },
823
+ { fnName, args }
824
+ );
825
+
826
+ return result;
827
+ } catch (error) {
828
+ this.stats.failures++;
829
+ // Re-throw but core doesn't crash
830
+ throw error;
831
+ }
832
+ }
833
+
834
+ /**
835
+ * Get plugin health including failure status
836
+ */
837
+ getHealth(pluginId) {
838
+ if (!pluginId) {
839
+ return this.failureContract.getSummary();
840
+ }
841
+ return this.failureContract.getHealth(pluginId);
254
842
  }
255
843
 
256
844
  /**
@@ -414,6 +1002,7 @@ export class PluginLoader extends EventEmitter {
414
1002
  ...this.stats,
415
1003
  catalogSize: Object.keys(PLUGIN_CATALOG).length,
416
1004
  bundleCount: Object.keys(PLUGIN_BUNDLES).length,
1005
+ health: this.failureContract.getSummary(),
417
1006
  };
418
1007
  }
419
1008
  }