@ruvector/edge-net 0.5.1 → 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.
- package/README.md +281 -10
- package/core-invariants.js +942 -0
- package/models/adapter-hub.js +1008 -0
- package/models/adapter-security.js +792 -0
- package/models/benchmark.js +688 -0
- package/models/distribution.js +791 -0
- package/models/index.js +109 -0
- package/models/integrity.js +753 -0
- package/models/loader.js +725 -0
- package/models/microlora.js +1298 -0
- package/models/model-loader.js +922 -0
- package/models/model-optimizer.js +1245 -0
- package/models/model-registry.js +696 -0
- package/models/model-utils.js +548 -0
- package/models/models-cli.js +914 -0
- package/models/registry.json +214 -0
- package/models/training-utils.js +1418 -0
- package/models/wasm-core.js +1025 -0
- package/network-genesis.js +2847 -0
- package/onnx-worker.js +462 -8
- package/package.json +33 -3
- package/plugins/plugin-loader.js +381 -0
- package/tests/model-optimizer.test.js +644 -0
- package/tests/network-genesis.test.js +562 -0
- package/tests/plugin-system-test.js +163 -0
- package/tests/wasm-core.test.js +368 -0
package/plugins/plugin-loader.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
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
|
|
@@ -15,6 +19,274 @@ import { EventEmitter } from 'events';
|
|
|
15
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
|
+
|
|
18
290
|
// ============================================
|
|
19
291
|
// ED25519 SIGNATURE VERIFICATION
|
|
20
292
|
// ============================================
|
|
@@ -143,6 +415,15 @@ export class PluginLoader extends EventEmitter {
|
|
|
143
415
|
rateLimitEnabled: options.rateLimitEnabled ?? true,
|
|
144
416
|
rateLimitRequests: options.rateLimitRequests ?? 100,
|
|
145
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,
|
|
146
427
|
...options,
|
|
147
428
|
};
|
|
148
429
|
|
|
@@ -157,6 +438,21 @@ export class PluginLoader extends EventEmitter {
|
|
|
157
438
|
windowMs: this.options.rateLimitWindowMs,
|
|
158
439
|
});
|
|
159
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
|
+
|
|
160
456
|
// Stats
|
|
161
457
|
this.stats = {
|
|
162
458
|
loaded: 0,
|
|
@@ -164,9 +460,18 @@ export class PluginLoader extends EventEmitter {
|
|
|
164
460
|
verified: 0,
|
|
165
461
|
rejected: 0,
|
|
166
462
|
rateLimited: 0,
|
|
463
|
+
quarantined: 0,
|
|
464
|
+
failures: 0,
|
|
167
465
|
};
|
|
168
466
|
}
|
|
169
467
|
|
|
468
|
+
/**
|
|
469
|
+
* Set CoreInvariants for economic boundary enforcement
|
|
470
|
+
*/
|
|
471
|
+
setCoreInvariants(coreInvariants) {
|
|
472
|
+
this.options.coreInvariants = coreInvariants;
|
|
473
|
+
}
|
|
474
|
+
|
|
170
475
|
/**
|
|
171
476
|
* Get catalog of available plugins
|
|
172
477
|
*/
|
|
@@ -404,6 +709,11 @@ export class PluginLoader extends EventEmitter {
|
|
|
404
709
|
'__dirname', '__filename', 'module', 'exports'
|
|
405
710
|
]);
|
|
406
711
|
|
|
712
|
+
// Get economic boundary from CoreInvariants if available
|
|
713
|
+
const economicView = this.options.coreInvariants
|
|
714
|
+
? this.options.coreInvariants.getPluginEconomicView()
|
|
715
|
+
: this._createMockEconomicView();
|
|
716
|
+
|
|
407
717
|
// Create isolated sandbox context
|
|
408
718
|
const sandbox = {
|
|
409
719
|
// Immutable capability set
|
|
@@ -421,6 +731,10 @@ export class PluginLoader extends EventEmitter {
|
|
|
421
731
|
}
|
|
422
732
|
},
|
|
423
733
|
|
|
734
|
+
// Economic boundary (READ-ONLY)
|
|
735
|
+
// Plugins can observe credits but NEVER modify
|
|
736
|
+
credits: economicView,
|
|
737
|
+
|
|
424
738
|
// Resource limits
|
|
425
739
|
limits: Object.freeze({
|
|
426
740
|
maxMemoryMB: 128,
|
|
@@ -461,6 +775,72 @@ export class PluginLoader extends EventEmitter {
|
|
|
461
775
|
return Object.freeze(sandbox);
|
|
462
776
|
}
|
|
463
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);
|
|
842
|
+
}
|
|
843
|
+
|
|
464
844
|
/**
|
|
465
845
|
* Create plugin API based on capabilities
|
|
466
846
|
*/
|
|
@@ -622,6 +1002,7 @@ export class PluginLoader extends EventEmitter {
|
|
|
622
1002
|
...this.stats,
|
|
623
1003
|
catalogSize: Object.keys(PLUGIN_CATALOG).length,
|
|
624
1004
|
bundleCount: Object.keys(PLUGIN_BUNDLES).length,
|
|
1005
|
+
health: this.failureContract.getSummary(),
|
|
625
1006
|
};
|
|
626
1007
|
}
|
|
627
1008
|
}
|