@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
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ruvector/edge-net Adapter Security
|
|
3
|
+
*
|
|
4
|
+
* Security for MicroLoRA adapters:
|
|
5
|
+
* - Quarantine before activation
|
|
6
|
+
* - Local evaluation gating
|
|
7
|
+
* - Base model matching
|
|
8
|
+
* - Signature verification
|
|
9
|
+
* - Merge lineage tracking
|
|
10
|
+
*
|
|
11
|
+
* Invariant: Adapters never applied without full verification.
|
|
12
|
+
*
|
|
13
|
+
* @module @ruvector/edge-net/models/adapter-security
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { createHash } from 'crypto';
|
|
17
|
+
import { canonicalize, hashCanonical, TrustRoot, ManifestVerifier } from './integrity.js';
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// ADAPTER VERIFICATION
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Adapter verification rules
|
|
25
|
+
*/
|
|
26
|
+
export const ADAPTER_REQUIREMENTS = Object.freeze({
|
|
27
|
+
// Base model must match exactly
|
|
28
|
+
requireExactBaseMatch: true,
|
|
29
|
+
|
|
30
|
+
// Checksum must match manifest
|
|
31
|
+
requireChecksumMatch: true,
|
|
32
|
+
|
|
33
|
+
// Signature must be verified
|
|
34
|
+
requireSignature: true,
|
|
35
|
+
|
|
36
|
+
// Must pass local evaluation OR have trusted quality proof
|
|
37
|
+
requireQualityGate: true,
|
|
38
|
+
|
|
39
|
+
// Minimum evaluation score to pass gate (0-1)
|
|
40
|
+
minEvaluationScore: 0.7,
|
|
41
|
+
|
|
42
|
+
// Maximum adapter size relative to base model
|
|
43
|
+
maxAdapterSizeRatio: 0.1, // 10% of base model
|
|
44
|
+
|
|
45
|
+
// Trusted quality proof publishers
|
|
46
|
+
trustedQualityProvers: ['ruvector-eval-2024', 'community-eval-2024'],
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Adapter manifest structure
|
|
51
|
+
*/
|
|
52
|
+
export function createAdapterManifest(adapter) {
|
|
53
|
+
return {
|
|
54
|
+
schemaVersion: '2.0.0',
|
|
55
|
+
adapter: {
|
|
56
|
+
id: adapter.id,
|
|
57
|
+
name: adapter.name,
|
|
58
|
+
version: adapter.version,
|
|
59
|
+
baseModelId: adapter.baseModelId,
|
|
60
|
+
baseModelVersion: adapter.baseModelVersion,
|
|
61
|
+
rank: adapter.rank,
|
|
62
|
+
alpha: adapter.alpha,
|
|
63
|
+
targetModules: adapter.targetModules || ['q_proj', 'v_proj'],
|
|
64
|
+
},
|
|
65
|
+
artifacts: [{
|
|
66
|
+
path: adapter.path,
|
|
67
|
+
size: adapter.size,
|
|
68
|
+
sha256: adapter.sha256,
|
|
69
|
+
format: 'safetensors',
|
|
70
|
+
}],
|
|
71
|
+
quality: {
|
|
72
|
+
evaluationScore: adapter.evaluationScore,
|
|
73
|
+
evaluationDataset: adapter.evaluationDataset,
|
|
74
|
+
evaluationProof: adapter.evaluationProof,
|
|
75
|
+
domain: adapter.domain,
|
|
76
|
+
capabilities: adapter.capabilities,
|
|
77
|
+
},
|
|
78
|
+
lineage: adapter.lineage || null,
|
|
79
|
+
provenance: {
|
|
80
|
+
creator: adapter.creator,
|
|
81
|
+
createdAt: adapter.createdAt || new Date().toISOString(),
|
|
82
|
+
trainedOn: adapter.trainedOn,
|
|
83
|
+
trainingConfig: adapter.trainingConfig,
|
|
84
|
+
},
|
|
85
|
+
integrity: {
|
|
86
|
+
manifestHash: null, // Computed
|
|
87
|
+
signatures: [],
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ============================================================================
|
|
93
|
+
// QUARANTINE SYSTEM
|
|
94
|
+
// ============================================================================
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Quarantine states for adapters
|
|
98
|
+
*/
|
|
99
|
+
export const QuarantineState = Object.freeze({
|
|
100
|
+
PENDING: 'pending',
|
|
101
|
+
EVALUATING: 'evaluating',
|
|
102
|
+
PASSED: 'passed',
|
|
103
|
+
FAILED: 'failed',
|
|
104
|
+
TRUSTED: 'trusted', // Has trusted quality proof
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Quarantine manager for adapter verification
|
|
109
|
+
*/
|
|
110
|
+
export class AdapterQuarantine {
|
|
111
|
+
constructor(options = {}) {
|
|
112
|
+
this.trustRoot = options.trustRoot || new TrustRoot();
|
|
113
|
+
this.requirements = { ...ADAPTER_REQUIREMENTS, ...options.requirements };
|
|
114
|
+
|
|
115
|
+
// Quarantined adapters awaiting evaluation
|
|
116
|
+
this.quarantine = new Map();
|
|
117
|
+
|
|
118
|
+
// Approved adapters
|
|
119
|
+
this.approved = new Map();
|
|
120
|
+
|
|
121
|
+
// Failed adapters (blocked)
|
|
122
|
+
this.blocked = new Map();
|
|
123
|
+
|
|
124
|
+
// Evaluation test sets by domain
|
|
125
|
+
this.testSets = new Map();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Register a test set for a domain
|
|
130
|
+
*/
|
|
131
|
+
registerTestSet(domain, testCases) {
|
|
132
|
+
this.testSets.set(domain, testCases);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Quarantine an adapter for evaluation
|
|
137
|
+
*/
|
|
138
|
+
async quarantineAdapter(manifest, adapterData) {
|
|
139
|
+
const adapterId = manifest.adapter.id;
|
|
140
|
+
|
|
141
|
+
// 1. Verify checksum
|
|
142
|
+
const actualHash = createHash('sha256')
|
|
143
|
+
.update(Buffer.from(adapterData))
|
|
144
|
+
.digest('hex');
|
|
145
|
+
|
|
146
|
+
if (actualHash !== manifest.artifacts[0].sha256) {
|
|
147
|
+
const failure = {
|
|
148
|
+
adapterId,
|
|
149
|
+
reason: 'checksum_mismatch',
|
|
150
|
+
expected: manifest.artifacts[0].sha256,
|
|
151
|
+
actual: actualHash,
|
|
152
|
+
timestamp: Date.now(),
|
|
153
|
+
};
|
|
154
|
+
this.blocked.set(adapterId, failure);
|
|
155
|
+
return { state: QuarantineState.FAILED, failure };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 2. Verify signature if required
|
|
159
|
+
if (this.requirements.requireSignature) {
|
|
160
|
+
const sigResult = this._verifySignatures(manifest);
|
|
161
|
+
if (!sigResult.valid) {
|
|
162
|
+
const failure = {
|
|
163
|
+
adapterId,
|
|
164
|
+
reason: 'invalid_signature',
|
|
165
|
+
details: sigResult.errors,
|
|
166
|
+
timestamp: Date.now(),
|
|
167
|
+
};
|
|
168
|
+
this.blocked.set(adapterId, failure);
|
|
169
|
+
return { state: QuarantineState.FAILED, failure };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 3. Check for trusted quality proof
|
|
174
|
+
if (manifest.quality?.evaluationProof) {
|
|
175
|
+
const proofValid = await this._verifyQualityProof(manifest);
|
|
176
|
+
if (proofValid) {
|
|
177
|
+
this.approved.set(adapterId, {
|
|
178
|
+
manifest,
|
|
179
|
+
state: QuarantineState.TRUSTED,
|
|
180
|
+
approvedAt: Date.now(),
|
|
181
|
+
});
|
|
182
|
+
return { state: QuarantineState.TRUSTED };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 4. Add to quarantine for local evaluation
|
|
187
|
+
this.quarantine.set(adapterId, {
|
|
188
|
+
manifest,
|
|
189
|
+
adapterData,
|
|
190
|
+
state: QuarantineState.PENDING,
|
|
191
|
+
quarantinedAt: Date.now(),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return { state: QuarantineState.PENDING };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Evaluate a quarantined adapter locally
|
|
199
|
+
*/
|
|
200
|
+
async evaluateAdapter(adapterId, inferenceSession) {
|
|
201
|
+
const quarantined = this.quarantine.get(adapterId);
|
|
202
|
+
if (!quarantined) {
|
|
203
|
+
throw new Error(`Adapter ${adapterId} not in quarantine`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
quarantined.state = QuarantineState.EVALUATING;
|
|
207
|
+
|
|
208
|
+
const manifest = quarantined.manifest;
|
|
209
|
+
const domain = manifest.quality?.domain || 'general';
|
|
210
|
+
|
|
211
|
+
// Get test set for domain
|
|
212
|
+
const testSet = this.testSets.get(domain) || this._getDefaultTestSet();
|
|
213
|
+
|
|
214
|
+
if (testSet.length === 0) {
|
|
215
|
+
throw new Error(`No test set available for domain: ${domain}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Run evaluation
|
|
219
|
+
const results = await this._runEvaluation(
|
|
220
|
+
quarantined.adapterData,
|
|
221
|
+
testSet,
|
|
222
|
+
inferenceSession,
|
|
223
|
+
manifest.adapter.baseModelId
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
// Check if passed
|
|
227
|
+
const passed = results.score >= this.requirements.minEvaluationScore;
|
|
228
|
+
|
|
229
|
+
if (passed) {
|
|
230
|
+
this.quarantine.delete(adapterId);
|
|
231
|
+
this.approved.set(adapterId, {
|
|
232
|
+
manifest,
|
|
233
|
+
state: QuarantineState.PASSED,
|
|
234
|
+
evaluationResults: results,
|
|
235
|
+
approvedAt: Date.now(),
|
|
236
|
+
});
|
|
237
|
+
return { state: QuarantineState.PASSED, results };
|
|
238
|
+
} else {
|
|
239
|
+
this.quarantine.delete(adapterId);
|
|
240
|
+
this.blocked.set(adapterId, {
|
|
241
|
+
adapterId,
|
|
242
|
+
reason: 'evaluation_failed',
|
|
243
|
+
score: results.score,
|
|
244
|
+
required: this.requirements.minEvaluationScore,
|
|
245
|
+
timestamp: Date.now(),
|
|
246
|
+
});
|
|
247
|
+
return { state: QuarantineState.FAILED, results };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Check if an adapter can be used
|
|
253
|
+
*/
|
|
254
|
+
canUseAdapter(adapterId, baseModelId) {
|
|
255
|
+
const approved = this.approved.get(adapterId);
|
|
256
|
+
if (!approved) {
|
|
257
|
+
return { allowed: false, reason: 'not_approved' };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Verify base model match
|
|
261
|
+
if (this.requirements.requireExactBaseMatch) {
|
|
262
|
+
const expectedBase = approved.manifest.adapter.baseModelId;
|
|
263
|
+
if (expectedBase !== baseModelId) {
|
|
264
|
+
return {
|
|
265
|
+
allowed: false,
|
|
266
|
+
reason: 'base_model_mismatch',
|
|
267
|
+
expected: expectedBase,
|
|
268
|
+
actual: baseModelId,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return { allowed: true, state: approved.state };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Get approved adapter data
|
|
278
|
+
*/
|
|
279
|
+
getApprovedAdapter(adapterId) {
|
|
280
|
+
return this.approved.get(adapterId) || null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Verify signatures on adapter manifest
|
|
285
|
+
*/
|
|
286
|
+
_verifySignatures(manifest) {
|
|
287
|
+
if (!manifest.integrity?.signatures?.length) {
|
|
288
|
+
return { valid: false, errors: ['No signatures present'] };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return this.trustRoot.verifySignatureThreshold(
|
|
292
|
+
manifest.integrity.signatures,
|
|
293
|
+
1 // At least one valid signature for adapters
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Verify a trusted quality proof
|
|
299
|
+
*/
|
|
300
|
+
async _verifyQualityProof(manifest) {
|
|
301
|
+
const proof = manifest.quality.evaluationProof;
|
|
302
|
+
if (!proof) return false;
|
|
303
|
+
|
|
304
|
+
// Check if prover is trusted
|
|
305
|
+
if (!this.requirements.trustedQualityProvers.includes(proof.proverId)) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Verify proof signature
|
|
310
|
+
const proofPayload = {
|
|
311
|
+
adapterId: manifest.adapter.id,
|
|
312
|
+
evaluationScore: manifest.quality.evaluationScore,
|
|
313
|
+
evaluationDataset: manifest.quality.evaluationDataset,
|
|
314
|
+
timestamp: proof.timestamp,
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
// In production, verify actual signature here
|
|
318
|
+
return proof.signature && proof.proverId;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Run local evaluation on adapter
|
|
323
|
+
*/
|
|
324
|
+
async _runEvaluation(adapterData, testSet, inferenceSession, baseModelId) {
|
|
325
|
+
const results = {
|
|
326
|
+
total: testSet.length,
|
|
327
|
+
passed: 0,
|
|
328
|
+
failed: 0,
|
|
329
|
+
errors: 0,
|
|
330
|
+
details: [],
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
for (const testCase of testSet) {
|
|
334
|
+
try {
|
|
335
|
+
// Apply adapter temporarily
|
|
336
|
+
await inferenceSession.loadAdapter(adapterData, { temporary: true });
|
|
337
|
+
|
|
338
|
+
// Run inference
|
|
339
|
+
const output = await inferenceSession.generate(testCase.input, {
|
|
340
|
+
maxTokens: testCase.maxTokens || 64,
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// Check against expected
|
|
344
|
+
const passed = this._checkOutput(output, testCase.expected, testCase.criteria);
|
|
345
|
+
|
|
346
|
+
results.details.push({
|
|
347
|
+
input: testCase.input.slice(0, 50),
|
|
348
|
+
passed,
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
if (passed) {
|
|
352
|
+
results.passed++;
|
|
353
|
+
} else {
|
|
354
|
+
results.failed++;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Unload temporary adapter
|
|
358
|
+
await inferenceSession.unloadAdapter();
|
|
359
|
+
} catch (error) {
|
|
360
|
+
results.errors++;
|
|
361
|
+
results.details.push({
|
|
362
|
+
input: testCase.input.slice(0, 50),
|
|
363
|
+
error: error.message,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
results.score = results.passed / results.total;
|
|
369
|
+
return results;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Check if output matches expected criteria
|
|
374
|
+
*/
|
|
375
|
+
_checkOutput(output, expected, criteria = 'contains') {
|
|
376
|
+
const outputLower = output.toLowerCase();
|
|
377
|
+
const expectedLower = expected.toLowerCase();
|
|
378
|
+
|
|
379
|
+
switch (criteria) {
|
|
380
|
+
case 'exact':
|
|
381
|
+
return output.trim() === expected.trim();
|
|
382
|
+
case 'contains':
|
|
383
|
+
return outputLower.includes(expectedLower);
|
|
384
|
+
case 'startsWith':
|
|
385
|
+
return outputLower.startsWith(expectedLower);
|
|
386
|
+
case 'regex':
|
|
387
|
+
return new RegExp(expected).test(output);
|
|
388
|
+
default:
|
|
389
|
+
return outputLower.includes(expectedLower);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Get default test set for unknown domains
|
|
395
|
+
*/
|
|
396
|
+
_getDefaultTestSet() {
|
|
397
|
+
return [
|
|
398
|
+
{
|
|
399
|
+
input: 'Hello, how are you?',
|
|
400
|
+
expected: 'hello',
|
|
401
|
+
criteria: 'contains',
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
input: 'What is 2 + 2?',
|
|
405
|
+
expected: '4',
|
|
406
|
+
criteria: 'contains',
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
input: 'Translate to French: hello',
|
|
410
|
+
expected: 'bonjour',
|
|
411
|
+
criteria: 'contains',
|
|
412
|
+
},
|
|
413
|
+
];
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Export quarantine state
|
|
418
|
+
*/
|
|
419
|
+
export() {
|
|
420
|
+
return {
|
|
421
|
+
quarantine: Array.from(this.quarantine.entries()),
|
|
422
|
+
approved: Array.from(this.approved.entries()),
|
|
423
|
+
blocked: Array.from(this.blocked.entries()),
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Import quarantine state
|
|
429
|
+
*/
|
|
430
|
+
import(data) {
|
|
431
|
+
if (data.quarantine) {
|
|
432
|
+
this.quarantine = new Map(data.quarantine);
|
|
433
|
+
}
|
|
434
|
+
if (data.approved) {
|
|
435
|
+
this.approved = new Map(data.approved);
|
|
436
|
+
}
|
|
437
|
+
if (data.blocked) {
|
|
438
|
+
this.blocked = new Map(data.blocked);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ============================================================================
|
|
444
|
+
// MERGE LINEAGE TRACKING
|
|
445
|
+
// ============================================================================
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Lineage entry for merged adapters
|
|
449
|
+
*/
|
|
450
|
+
export function createMergeLineage(options) {
|
|
451
|
+
return {
|
|
452
|
+
parentAdapterIds: options.parentIds,
|
|
453
|
+
mergeMethod: options.method, // 'ties', 'dare', 'task_arithmetic', 'linear'
|
|
454
|
+
mergeParameters: options.parameters, // Method-specific params
|
|
455
|
+
mergeSeed: options.seed || Math.floor(Math.random() * 2 ** 32),
|
|
456
|
+
evaluationMetrics: options.metrics || {},
|
|
457
|
+
mergerIdentity: options.mergerId,
|
|
458
|
+
mergeTimestamp: new Date().toISOString(),
|
|
459
|
+
signature: null, // To be filled after signing
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Lineage tracker for adapter merges
|
|
465
|
+
*/
|
|
466
|
+
export class AdapterLineage {
|
|
467
|
+
constructor(options = {}) {
|
|
468
|
+
this.trustRoot = options.trustRoot || new TrustRoot();
|
|
469
|
+
|
|
470
|
+
// DAG of adapter lineage
|
|
471
|
+
this.lineageGraph = new Map();
|
|
472
|
+
|
|
473
|
+
// Root adapters (no parents)
|
|
474
|
+
this.roots = new Set();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Register a new adapter in lineage
|
|
479
|
+
*/
|
|
480
|
+
registerAdapter(adapterId, manifest) {
|
|
481
|
+
const lineage = manifest.lineage;
|
|
482
|
+
|
|
483
|
+
const node = {
|
|
484
|
+
adapterId,
|
|
485
|
+
version: manifest.adapter.version,
|
|
486
|
+
baseModelId: manifest.adapter.baseModelId,
|
|
487
|
+
parents: lineage?.parentAdapterIds || [],
|
|
488
|
+
children: [],
|
|
489
|
+
lineage,
|
|
490
|
+
registeredAt: Date.now(),
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
this.lineageGraph.set(adapterId, node);
|
|
494
|
+
|
|
495
|
+
// Update parent-child relationships
|
|
496
|
+
if (node.parents.length === 0) {
|
|
497
|
+
this.roots.add(adapterId);
|
|
498
|
+
} else {
|
|
499
|
+
for (const parentId of node.parents) {
|
|
500
|
+
const parent = this.lineageGraph.get(parentId);
|
|
501
|
+
if (parent) {
|
|
502
|
+
parent.children.push(adapterId);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return node;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Get full ancestry path for an adapter
|
|
512
|
+
*/
|
|
513
|
+
getAncestry(adapterId) {
|
|
514
|
+
const ancestry = [];
|
|
515
|
+
const visited = new Set();
|
|
516
|
+
const queue = [adapterId];
|
|
517
|
+
|
|
518
|
+
while (queue.length > 0) {
|
|
519
|
+
const current = queue.shift();
|
|
520
|
+
if (visited.has(current)) continue;
|
|
521
|
+
visited.add(current);
|
|
522
|
+
|
|
523
|
+
const node = this.lineageGraph.get(current);
|
|
524
|
+
if (node) {
|
|
525
|
+
ancestry.push({
|
|
526
|
+
adapterId: current,
|
|
527
|
+
version: node.version,
|
|
528
|
+
baseModelId: node.baseModelId,
|
|
529
|
+
mergeMethod: node.lineage?.mergeMethod,
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
for (const parentId of node.parents) {
|
|
533
|
+
queue.push(parentId);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return ancestry;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Verify lineage integrity
|
|
543
|
+
*/
|
|
544
|
+
verifyLineage(adapterId) {
|
|
545
|
+
const node = this.lineageGraph.get(adapterId);
|
|
546
|
+
if (!node) {
|
|
547
|
+
return { valid: false, error: 'Adapter not found' };
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const errors = [];
|
|
551
|
+
|
|
552
|
+
// Check all parents exist
|
|
553
|
+
for (const parentId of node.parents) {
|
|
554
|
+
if (!this.lineageGraph.has(parentId)) {
|
|
555
|
+
errors.push(`Missing parent: ${parentId}`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Verify lineage signature if present
|
|
560
|
+
if (node.lineage?.signature) {
|
|
561
|
+
// In production, verify actual signature
|
|
562
|
+
const sigValid = true; // Placeholder
|
|
563
|
+
if (!sigValid) {
|
|
564
|
+
errors.push('Invalid lineage signature');
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Check for circular references
|
|
569
|
+
const hasCircle = this._detectCircle(adapterId, new Set());
|
|
570
|
+
if (hasCircle) {
|
|
571
|
+
errors.push('Circular lineage detected');
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return {
|
|
575
|
+
valid: errors.length === 0,
|
|
576
|
+
errors,
|
|
577
|
+
ancestry: this.getAncestry(adapterId),
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Detect circular references in lineage
|
|
583
|
+
*/
|
|
584
|
+
_detectCircle(adapterId, visited) {
|
|
585
|
+
if (visited.has(adapterId)) return true;
|
|
586
|
+
visited.add(adapterId);
|
|
587
|
+
|
|
588
|
+
const node = this.lineageGraph.get(adapterId);
|
|
589
|
+
if (!node) return false;
|
|
590
|
+
|
|
591
|
+
for (const parentId of node.parents) {
|
|
592
|
+
if (this._detectCircle(parentId, new Set(visited))) {
|
|
593
|
+
return true;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Get descendants of an adapter
|
|
602
|
+
*/
|
|
603
|
+
getDescendants(adapterId) {
|
|
604
|
+
const descendants = [];
|
|
605
|
+
const queue = [adapterId];
|
|
606
|
+
const visited = new Set();
|
|
607
|
+
|
|
608
|
+
while (queue.length > 0) {
|
|
609
|
+
const current = queue.shift();
|
|
610
|
+
if (visited.has(current)) continue;
|
|
611
|
+
visited.add(current);
|
|
612
|
+
|
|
613
|
+
const node = this.lineageGraph.get(current);
|
|
614
|
+
if (node) {
|
|
615
|
+
for (const childId of node.children) {
|
|
616
|
+
descendants.push(childId);
|
|
617
|
+
queue.push(childId);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return descendants;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Compute reproducibility hash for a merge
|
|
627
|
+
*/
|
|
628
|
+
computeReproducibilityHash(lineage) {
|
|
629
|
+
const payload = {
|
|
630
|
+
parents: lineage.parentAdapterIds.sort(),
|
|
631
|
+
method: lineage.mergeMethod,
|
|
632
|
+
parameters: lineage.mergeParameters,
|
|
633
|
+
seed: lineage.mergeSeed,
|
|
634
|
+
};
|
|
635
|
+
return hashCanonical(payload);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Export lineage graph
|
|
640
|
+
*/
|
|
641
|
+
export() {
|
|
642
|
+
return {
|
|
643
|
+
nodes: Array.from(this.lineageGraph.entries()),
|
|
644
|
+
roots: Array.from(this.roots),
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Import lineage graph
|
|
650
|
+
*/
|
|
651
|
+
import(data) {
|
|
652
|
+
if (data.nodes) {
|
|
653
|
+
this.lineageGraph = new Map(data.nodes);
|
|
654
|
+
}
|
|
655
|
+
if (data.roots) {
|
|
656
|
+
this.roots = new Set(data.roots);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// ============================================================================
|
|
662
|
+
// ADAPTER POOL WITH SECURITY
|
|
663
|
+
// ============================================================================
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Secure adapter pool with quarantine integration
|
|
667
|
+
*/
|
|
668
|
+
export class SecureAdapterPool {
|
|
669
|
+
constructor(options = {}) {
|
|
670
|
+
this.maxSlots = options.maxSlots || 16;
|
|
671
|
+
this.quarantine = new AdapterQuarantine(options);
|
|
672
|
+
this.lineage = new AdapterLineage(options);
|
|
673
|
+
|
|
674
|
+
// Active adapters (LRU)
|
|
675
|
+
this.activeAdapters = new Map();
|
|
676
|
+
this.accessOrder = [];
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Add adapter with full security checks
|
|
681
|
+
*/
|
|
682
|
+
async addAdapter(manifest, adapterData, inferenceSession = null) {
|
|
683
|
+
const adapterId = manifest.adapter.id;
|
|
684
|
+
|
|
685
|
+
// 1. Quarantine and verify
|
|
686
|
+
const quarantineResult = await this.quarantine.quarantineAdapter(manifest, adapterData);
|
|
687
|
+
|
|
688
|
+
if (quarantineResult.state === QuarantineState.FAILED) {
|
|
689
|
+
throw new Error(`Adapter blocked: ${quarantineResult.failure.reason}`);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// 2. If not trusted, run local evaluation
|
|
693
|
+
if (quarantineResult.state === QuarantineState.PENDING) {
|
|
694
|
+
if (!inferenceSession) {
|
|
695
|
+
throw new Error('Inference session required for local evaluation');
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const evalResult = await this.quarantine.evaluateAdapter(adapterId, inferenceSession);
|
|
699
|
+
if (evalResult.state === QuarantineState.FAILED) {
|
|
700
|
+
throw new Error(`Adapter failed evaluation: score ${evalResult.results.score}`);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// 3. Register in lineage
|
|
705
|
+
this.lineage.registerAdapter(adapterId, manifest);
|
|
706
|
+
|
|
707
|
+
// 4. Add to active pool
|
|
708
|
+
await this._addToPool(adapterId, adapterData, manifest);
|
|
709
|
+
|
|
710
|
+
return { adapterId, state: 'active' };
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Get an adapter if allowed
|
|
715
|
+
*/
|
|
716
|
+
getAdapter(adapterId, baseModelId) {
|
|
717
|
+
// Check if can use
|
|
718
|
+
const check = this.quarantine.canUseAdapter(adapterId, baseModelId);
|
|
719
|
+
if (!check.allowed) {
|
|
720
|
+
return { allowed: false, reason: check.reason };
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Get from pool
|
|
724
|
+
const adapter = this.activeAdapters.get(adapterId);
|
|
725
|
+
if (!adapter) {
|
|
726
|
+
return { allowed: false, reason: 'not_in_pool' };
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Update access order
|
|
730
|
+
this._updateAccessOrder(adapterId);
|
|
731
|
+
|
|
732
|
+
return { allowed: true, adapter };
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Add to pool with LRU eviction
|
|
737
|
+
*/
|
|
738
|
+
async _addToPool(adapterId, adapterData, manifest) {
|
|
739
|
+
// Evict if at capacity
|
|
740
|
+
while (this.activeAdapters.size >= this.maxSlots) {
|
|
741
|
+
const evictId = this.accessOrder.shift();
|
|
742
|
+
this.activeAdapters.delete(evictId);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
this.activeAdapters.set(adapterId, {
|
|
746
|
+
data: adapterData,
|
|
747
|
+
manifest,
|
|
748
|
+
loadedAt: Date.now(),
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
this._updateAccessOrder(adapterId);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Update LRU access order
|
|
756
|
+
*/
|
|
757
|
+
_updateAccessOrder(adapterId) {
|
|
758
|
+
const index = this.accessOrder.indexOf(adapterId);
|
|
759
|
+
if (index > -1) {
|
|
760
|
+
this.accessOrder.splice(index, 1);
|
|
761
|
+
}
|
|
762
|
+
this.accessOrder.push(adapterId);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Get pool statistics
|
|
767
|
+
*/
|
|
768
|
+
getStats() {
|
|
769
|
+
return {
|
|
770
|
+
activeCount: this.activeAdapters.size,
|
|
771
|
+
maxSlots: this.maxSlots,
|
|
772
|
+
quarantinedCount: this.quarantine.quarantine.size,
|
|
773
|
+
approvedCount: this.quarantine.approved.size,
|
|
774
|
+
blockedCount: this.quarantine.blocked.size,
|
|
775
|
+
lineageNodes: this.lineage.lineageGraph.size,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// ============================================================================
|
|
781
|
+
// EXPORTS
|
|
782
|
+
// ============================================================================
|
|
783
|
+
|
|
784
|
+
export default {
|
|
785
|
+
ADAPTER_REQUIREMENTS,
|
|
786
|
+
QuarantineState,
|
|
787
|
+
createAdapterManifest,
|
|
788
|
+
AdapterQuarantine,
|
|
789
|
+
createMergeLineage,
|
|
790
|
+
AdapterLineage,
|
|
791
|
+
SecureAdapterPool,
|
|
792
|
+
};
|