@ruvector/edge-net 0.4.6 → 0.5.0
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/package.json +13 -2
- package/plugins/cli.js +395 -0
- package/plugins/implementations/compression.js +132 -0
- package/plugins/implementations/e2e-encryption.js +160 -0
- package/plugins/implementations/federated-learning.js +249 -0
- package/plugins/implementations/reputation-staking.js +243 -0
- package/plugins/implementations/swarm-intelligence.js +386 -0
- package/plugins/index.js +90 -0
- package/plugins/plugin-loader.js +440 -0
- package/plugins/plugin-manifest.js +702 -0
- package/plugins/plugin-sdk.js +496 -0
- package/tests/plugin-system-test.js +382 -0
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edge-Net Secure Plugin Loader
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Ed25519 signature verification
|
|
6
|
+
* - SHA-256 integrity checks
|
|
7
|
+
* - Lazy loading with caching
|
|
8
|
+
* - Capability-based sandboxing
|
|
9
|
+
* - Zero telemetry
|
|
10
|
+
*
|
|
11
|
+
* @module @ruvector/edge-net/plugins/loader
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { EventEmitter } from 'events';
|
|
15
|
+
import { createHash } from 'crypto';
|
|
16
|
+
import { PLUGIN_CATALOG, PLUGIN_BUNDLES, Capability, PluginTier } from './plugin-manifest.js';
|
|
17
|
+
|
|
18
|
+
// ============================================
|
|
19
|
+
// PLUGIN LOADER
|
|
20
|
+
// ============================================
|
|
21
|
+
|
|
22
|
+
export class PluginLoader extends EventEmitter {
|
|
23
|
+
constructor(options = {}) {
|
|
24
|
+
super();
|
|
25
|
+
|
|
26
|
+
this.options = {
|
|
27
|
+
// Security
|
|
28
|
+
verifySignatures: options.verifySignatures ?? true,
|
|
29
|
+
allowedTiers: options.allowedTiers ?? [PluginTier.STABLE, PluginTier.BETA],
|
|
30
|
+
trustedAuthors: options.trustedAuthors ?? ['ruvector', 'edge-net-official'],
|
|
31
|
+
|
|
32
|
+
// Loading
|
|
33
|
+
lazyLoad: options.lazyLoad ?? true,
|
|
34
|
+
cachePlugins: options.cachePlugins ?? true,
|
|
35
|
+
pluginPath: options.pluginPath ?? './plugins/implementations',
|
|
36
|
+
|
|
37
|
+
// Permissions
|
|
38
|
+
maxCapabilities: options.maxCapabilities ?? 10,
|
|
39
|
+
deniedCapabilities: options.deniedCapabilities ?? [Capability.SYSTEM_EXEC],
|
|
40
|
+
...options,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Plugin state
|
|
44
|
+
this.loadedPlugins = new Map(); // id -> instance
|
|
45
|
+
this.pluginConfigs = new Map(); // id -> config
|
|
46
|
+
this.pendingLoads = new Map(); // id -> Promise
|
|
47
|
+
|
|
48
|
+
// Stats
|
|
49
|
+
this.stats = {
|
|
50
|
+
loaded: 0,
|
|
51
|
+
cached: 0,
|
|
52
|
+
verified: 0,
|
|
53
|
+
rejected: 0,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get catalog of available plugins
|
|
59
|
+
*/
|
|
60
|
+
getCatalog() {
|
|
61
|
+
return Object.entries(PLUGIN_CATALOG).map(([id, manifest]) => ({
|
|
62
|
+
id,
|
|
63
|
+
...manifest,
|
|
64
|
+
isLoaded: this.loadedPlugins.has(id),
|
|
65
|
+
isAllowed: this._isPluginAllowed(manifest),
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get plugin bundles
|
|
71
|
+
*/
|
|
72
|
+
getBundles() {
|
|
73
|
+
return PLUGIN_BUNDLES;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if plugin is allowed by security policy
|
|
78
|
+
*/
|
|
79
|
+
_isPluginAllowed(manifest) {
|
|
80
|
+
// Check tier
|
|
81
|
+
if (!this.options.allowedTiers.includes(manifest.tier)) {
|
|
82
|
+
return { allowed: false, reason: `Tier ${manifest.tier} not allowed` };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check capabilities
|
|
86
|
+
const deniedCaps = manifest.capabilities?.filter(c =>
|
|
87
|
+
this.options.deniedCapabilities.includes(c)
|
|
88
|
+
);
|
|
89
|
+
if (deniedCaps?.length > 0) {
|
|
90
|
+
return { allowed: false, reason: `Denied capabilities: ${deniedCaps.join(', ')}` };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check capability count
|
|
94
|
+
if (manifest.capabilities?.length > this.options.maxCapabilities) {
|
|
95
|
+
return { allowed: false, reason: 'Too many capabilities requested' };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { allowed: true };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Verify plugin integrity
|
|
103
|
+
*/
|
|
104
|
+
async _verifyPlugin(manifest, code) {
|
|
105
|
+
if (!this.options.verifySignatures) {
|
|
106
|
+
return { verified: true, reason: 'Verification disabled' };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Verify checksum
|
|
110
|
+
if (manifest.checksum) {
|
|
111
|
+
const hash = createHash('sha256').update(code).digest('hex');
|
|
112
|
+
if (hash !== manifest.checksum) {
|
|
113
|
+
return { verified: false, reason: 'Checksum mismatch' };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
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' };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// If no security metadata, only allow stable tier
|
|
127
|
+
if (!manifest.checksum && !manifest.signature) {
|
|
128
|
+
if (manifest.tier === PluginTier.STABLE) {
|
|
129
|
+
return { verified: true, reason: 'Built-in stable plugin' };
|
|
130
|
+
}
|
|
131
|
+
return { verified: false, reason: 'No verification metadata' };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { verified: true };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Load a plugin by ID
|
|
139
|
+
*/
|
|
140
|
+
async load(pluginId, config = {}) {
|
|
141
|
+
// Check if already loaded
|
|
142
|
+
if (this.loadedPlugins.has(pluginId)) {
|
|
143
|
+
return this.loadedPlugins.get(pluginId);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Check if loading in progress
|
|
147
|
+
if (this.pendingLoads.has(pluginId)) {
|
|
148
|
+
return this.pendingLoads.get(pluginId);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const loadPromise = this._loadPlugin(pluginId, config);
|
|
152
|
+
this.pendingLoads.set(pluginId, loadPromise);
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const plugin = await loadPromise;
|
|
156
|
+
this.pendingLoads.delete(pluginId);
|
|
157
|
+
return plugin;
|
|
158
|
+
} catch (error) {
|
|
159
|
+
this.pendingLoads.delete(pluginId);
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async _loadPlugin(pluginId, config) {
|
|
165
|
+
const manifest = PLUGIN_CATALOG[pluginId];
|
|
166
|
+
if (!manifest) {
|
|
167
|
+
throw new Error(`Plugin not found: ${pluginId}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Security check
|
|
171
|
+
const allowed = this._isPluginAllowed(manifest);
|
|
172
|
+
if (!allowed.allowed) {
|
|
173
|
+
this.stats.rejected++;
|
|
174
|
+
throw new Error(`Plugin ${pluginId} not allowed: ${allowed.reason}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Load dependencies first
|
|
178
|
+
if (manifest.dependencies) {
|
|
179
|
+
for (const depId of manifest.dependencies) {
|
|
180
|
+
if (!this.loadedPlugins.has(depId)) {
|
|
181
|
+
await this.load(depId);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Merge config with defaults
|
|
187
|
+
const finalConfig = {
|
|
188
|
+
...manifest.defaultConfig,
|
|
189
|
+
...config,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Create plugin instance
|
|
193
|
+
const plugin = await this._createPluginInstance(manifest, finalConfig);
|
|
194
|
+
|
|
195
|
+
// Store
|
|
196
|
+
this.loadedPlugins.set(pluginId, plugin);
|
|
197
|
+
this.pluginConfigs.set(pluginId, finalConfig);
|
|
198
|
+
this.stats.loaded++;
|
|
199
|
+
|
|
200
|
+
this.emit('plugin:loaded', { pluginId, manifest, config: finalConfig });
|
|
201
|
+
|
|
202
|
+
return plugin;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Create plugin instance with sandbox
|
|
207
|
+
*/
|
|
208
|
+
async _createPluginInstance(manifest, config) {
|
|
209
|
+
// Create sandboxed context
|
|
210
|
+
const sandbox = this._createSandbox(manifest.capabilities || []);
|
|
211
|
+
|
|
212
|
+
// Return plugin wrapper
|
|
213
|
+
return {
|
|
214
|
+
id: manifest.id,
|
|
215
|
+
name: manifest.name,
|
|
216
|
+
version: manifest.version,
|
|
217
|
+
config,
|
|
218
|
+
sandbox,
|
|
219
|
+
manifest,
|
|
220
|
+
|
|
221
|
+
// Plugin API
|
|
222
|
+
api: this._createPluginAPI(manifest, sandbox),
|
|
223
|
+
|
|
224
|
+
// Lifecycle
|
|
225
|
+
async init() {
|
|
226
|
+
// Plugin-specific initialization
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
async destroy() {
|
|
230
|
+
// Cleanup
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Create capability-based sandbox
|
|
237
|
+
*/
|
|
238
|
+
_createSandbox(capabilities) {
|
|
239
|
+
const sandbox = {
|
|
240
|
+
capabilities: new Set(capabilities),
|
|
241
|
+
|
|
242
|
+
hasCapability(cap) {
|
|
243
|
+
return this.capabilities.has(cap);
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
require(cap) {
|
|
247
|
+
if (!this.hasCapability(cap)) {
|
|
248
|
+
throw new Error(`Missing capability: ${cap}`);
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
return sandbox;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Create plugin API based on capabilities
|
|
258
|
+
*/
|
|
259
|
+
_createPluginAPI(manifest, sandbox) {
|
|
260
|
+
const api = {};
|
|
261
|
+
|
|
262
|
+
// Network API (if permitted)
|
|
263
|
+
if (sandbox.hasCapability(Capability.NETWORK_CONNECT)) {
|
|
264
|
+
api.network = {
|
|
265
|
+
connect: async (url) => {
|
|
266
|
+
sandbox.require(Capability.NETWORK_CONNECT);
|
|
267
|
+
// Implementation delegated to edge-net core
|
|
268
|
+
return { connected: true, url };
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Crypto API (if permitted)
|
|
274
|
+
if (sandbox.hasCapability(Capability.CRYPTO_ENCRYPT)) {
|
|
275
|
+
api.crypto = {
|
|
276
|
+
encrypt: async (data, key) => {
|
|
277
|
+
sandbox.require(Capability.CRYPTO_ENCRYPT);
|
|
278
|
+
// Implementation delegated to WASM crypto
|
|
279
|
+
return { encrypted: true };
|
|
280
|
+
},
|
|
281
|
+
decrypt: async (data, key) => {
|
|
282
|
+
sandbox.require(Capability.CRYPTO_ENCRYPT);
|
|
283
|
+
return { decrypted: true };
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (sandbox.hasCapability(Capability.CRYPTO_SIGN)) {
|
|
289
|
+
api.crypto = api.crypto || {};
|
|
290
|
+
api.crypto.sign = async (data, privateKey) => {
|
|
291
|
+
sandbox.require(Capability.CRYPTO_SIGN);
|
|
292
|
+
return { signed: true };
|
|
293
|
+
};
|
|
294
|
+
api.crypto.verify = async (data, signature, publicKey) => {
|
|
295
|
+
sandbox.require(Capability.CRYPTO_SIGN);
|
|
296
|
+
return { valid: true };
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Storage API (if permitted)
|
|
301
|
+
if (sandbox.hasCapability(Capability.STORAGE_READ)) {
|
|
302
|
+
api.storage = {
|
|
303
|
+
get: async (key) => {
|
|
304
|
+
sandbox.require(Capability.STORAGE_READ);
|
|
305
|
+
return null; // Implementation delegated
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
if (sandbox.hasCapability(Capability.STORAGE_WRITE)) {
|
|
310
|
+
api.storage = api.storage || {};
|
|
311
|
+
api.storage.set = async (key, value) => {
|
|
312
|
+
sandbox.require(Capability.STORAGE_WRITE);
|
|
313
|
+
return true;
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Compute API (if permitted)
|
|
318
|
+
if (sandbox.hasCapability(Capability.COMPUTE_WASM)) {
|
|
319
|
+
api.compute = {
|
|
320
|
+
runWasm: async (module, fn, args) => {
|
|
321
|
+
sandbox.require(Capability.COMPUTE_WASM);
|
|
322
|
+
return { result: null }; // Implementation delegated
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return api;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Unload a plugin
|
|
332
|
+
*/
|
|
333
|
+
async unload(pluginId) {
|
|
334
|
+
const plugin = this.loadedPlugins.get(pluginId);
|
|
335
|
+
if (!plugin) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Check for dependents
|
|
340
|
+
for (const [id, p] of this.loadedPlugins) {
|
|
341
|
+
const manifest = PLUGIN_CATALOG[id];
|
|
342
|
+
if (manifest.dependencies?.includes(pluginId)) {
|
|
343
|
+
throw new Error(`Cannot unload ${pluginId}: required by ${id}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Cleanup
|
|
348
|
+
if (plugin.destroy) {
|
|
349
|
+
await plugin.destroy();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
this.loadedPlugins.delete(pluginId);
|
|
353
|
+
this.pluginConfigs.delete(pluginId);
|
|
354
|
+
this.stats.loaded--;
|
|
355
|
+
|
|
356
|
+
this.emit('plugin:unloaded', { pluginId });
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Load a bundle of plugins
|
|
362
|
+
*/
|
|
363
|
+
async loadBundle(bundleName) {
|
|
364
|
+
const bundle = PLUGIN_BUNDLES[bundleName];
|
|
365
|
+
if (!bundle) {
|
|
366
|
+
throw new Error(`Bundle not found: ${bundleName}`);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const results = [];
|
|
370
|
+
for (const pluginId of bundle.plugins) {
|
|
371
|
+
try {
|
|
372
|
+
const plugin = await this.load(pluginId);
|
|
373
|
+
results.push({ pluginId, success: true, plugin });
|
|
374
|
+
} catch (error) {
|
|
375
|
+
results.push({ pluginId, success: false, error: error.message });
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
this.emit('bundle:loaded', { bundleName, results });
|
|
380
|
+
return results;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Get loaded plugin
|
|
385
|
+
*/
|
|
386
|
+
get(pluginId) {
|
|
387
|
+
return this.loadedPlugins.get(pluginId);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Check if plugin is loaded
|
|
392
|
+
*/
|
|
393
|
+
isLoaded(pluginId) {
|
|
394
|
+
return this.loadedPlugins.has(pluginId);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Get all loaded plugins
|
|
399
|
+
*/
|
|
400
|
+
getLoaded() {
|
|
401
|
+
return Array.from(this.loadedPlugins.entries()).map(([id, plugin]) => ({
|
|
402
|
+
id,
|
|
403
|
+
name: plugin.name,
|
|
404
|
+
version: plugin.version,
|
|
405
|
+
config: this.pluginConfigs.get(id),
|
|
406
|
+
}));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Get loader stats
|
|
411
|
+
*/
|
|
412
|
+
getStats() {
|
|
413
|
+
return {
|
|
414
|
+
...this.stats,
|
|
415
|
+
catalogSize: Object.keys(PLUGIN_CATALOG).length,
|
|
416
|
+
bundleCount: Object.keys(PLUGIN_BUNDLES).length,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// ============================================
|
|
422
|
+
// PLUGIN MANAGER (Singleton)
|
|
423
|
+
// ============================================
|
|
424
|
+
|
|
425
|
+
export class PluginManager {
|
|
426
|
+
static instance = null;
|
|
427
|
+
|
|
428
|
+
static getInstance(options = {}) {
|
|
429
|
+
if (!PluginManager.instance) {
|
|
430
|
+
PluginManager.instance = new PluginLoader(options);
|
|
431
|
+
}
|
|
432
|
+
return PluginManager.instance;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
static reset() {
|
|
436
|
+
PluginManager.instance = null;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export default PluginLoader;
|