@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.
- 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/SECURITY-AUDIT.md +654 -0
- package/plugins/cli.js +43 -3
- package/plugins/implementations/e2e-encryption.js +57 -12
- package/plugins/plugin-loader.js +610 -21
- package/tests/model-optimizer.test.js +644 -0
- package/tests/network-genesis.test.js +562 -0
- package/tests/plugin-benchmark.js +1239 -0
- package/tests/plugin-system-test.js +163 -0
- package/tests/wasm-core.test.js +368 -0
package/plugins/plugin-loader.js
CHANGED
|
@@ -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
|
|
118
|
-
if (manifest.signature) {
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
719
|
+
// Immutable capability set
|
|
720
|
+
get capabilities() {
|
|
721
|
+
return new Set(allowedCapabilities);
|
|
722
|
+
},
|
|
241
723
|
|
|
242
724
|
hasCapability(cap) {
|
|
243
|
-
return
|
|
725
|
+
return allowedCapabilities.has(cap);
|
|
244
726
|
},
|
|
245
727
|
|
|
246
728
|
require(cap) {
|
|
247
|
-
if (!
|
|
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
|
-
|
|
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
|
}
|