reploid 0.0.0-stage → 0.1.1
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/apps/poolday/config/chat-files.json +27 -0
- package/apps/poolday/config/partition-generation.js +21 -0
- package/apps/poolday/config/partition-policy.json +74 -0
- package/apps/poolday/config/partition-releases.json +3 -0
- package/apps/poolday/config/work-profile.json +122 -0
- package/apps/poolday/host/chat-view.js +47 -0
- package/apps/poolday/host/earning/client.js +199 -0
- package/apps/poolday/host/earning/contribution.js +168 -0
- package/apps/poolday/host/partition-release.js +33 -0
- package/apps/poolday/host/work-partition-artifact-source.js +37 -0
- package/apps/poolday/host/work-partition-capsule.js +39 -0
- package/apps/poolday/host/work-partition-plan.js +167 -0
- package/apps/poolday/host/work-partition-tokenizer-guard.js +21 -0
- package/apps/poolday/infrastructure/model-storage-lock.js +22 -0
- package/apps/poolday/infrastructure/pack-release-storage.js +52 -0
- package/apps/poolday/pool/pack-operation.js +161 -0
- package/apps/poolday/pool/pack-release-policy.js +44 -0
- package/apps/poolday/pool/peer-pack-data-channel.js +217 -0
- package/apps/poolday/providers/work-adapter.js +67 -0
- package/apps/poolday/providers/work-device.js +33 -0
- package/apps/poolday/providers/work-generation-completion.js +55 -0
- package/apps/poolday/providers/work-provider.js +209 -0
- package/apps/poolday/providers/work-resident-provider.js +113 -0
- package/deploy/artifacts/doppler-gpu-0.7.1.tgz +0 -0
- package/docs/fleet-service.md +214 -0
- package/docs/native-participant.md +542 -0
- package/native-delivery.json +1174 -0
- package/package.json +29 -4
- package/packages/reploid/src/artifacts/binary.js +25 -0
- package/packages/reploid/src/artifacts/canonical-json.js +23 -0
- package/packages/reploid/src/artifacts/custody/exchange.js +402 -0
- package/packages/reploid/src/artifacts/custody/piece-acquisition.js +70 -0
- package/packages/reploid/src/artifacts/custody/runtime.js +404 -0
- package/packages/reploid/src/artifacts/custody/supply-budget.js +19 -0
- package/packages/reploid/src/artifacts/custody-certificate.js +123 -0
- package/packages/reploid/src/artifacts/delivery-attestation.js +64 -0
- package/packages/reploid/src/artifacts/generation-terminal.js +36 -0
- package/packages/reploid/src/artifacts/identity.js +91 -0
- package/packages/reploid/src/artifacts/participant-proofs.js +159 -0
- package/packages/reploid/src/artifacts/receipt.js +131 -0
- package/packages/reploid/src/artifacts/signatures.js +94 -0
- package/packages/reploid/src/artifacts/whole-model-receipt.js +42 -0
- package/packages/reploid/src/cancellation.js +44 -0
- package/packages/reploid/src/config/defaults.json +110 -0
- package/packages/reploid/src/config/index.js +209 -0
- package/packages/reploid/src/config/request-overrides.json +9 -0
- package/packages/reploid/src/config/transport-defaults.json +7 -0
- package/packages/reploid/src/hashing.js +30 -0
- package/packages/reploid/src/measurement/capacity-utilization.js +321 -0
- package/packages/reploid/src/measurement/index.js +5 -0
- package/packages/reploid/src/measurement/readiness-records.js +310 -0
- package/packages/reploid/src/mesh/computer-identity.js +50 -0
- package/packages/reploid/src/mesh/computer-identity.policy.json +8 -0
- package/packages/reploid/src/mesh/contribution.js +141 -0
- package/packages/reploid/src/mesh/generation-recovery.js +58 -0
- package/packages/reploid/src/mesh/gpu-signature-shader.js +27 -0
- package/packages/reploid/src/mesh/gpu-signature.js +136 -0
- package/packages/reploid/src/mesh/gpu-signature.policy.json +11 -0
- package/packages/reploid/src/mesh/index.js +66 -0
- package/packages/reploid/src/mesh/partitions/automatic-ordered-partitions.js +540 -0
- package/packages/reploid/src/mesh/partitions/automatic-partitions.js +298 -0
- package/packages/reploid/src/mesh/partitions/partition-attempt-evidence.js +282 -0
- package/packages/reploid/src/mesh/partitions/partition-attempt-queue-policy.json +6 -0
- package/packages/reploid/src/mesh/partitions/partition-attempt-queue.js +112 -0
- package/packages/reploid/src/mesh/partitions/partition-canonical-contract.js +167 -0
- package/packages/reploid/src/mesh/partitions/partition-canonical-execution.js +168 -0
- package/packages/reploid/src/mesh/partitions/partition-chat-canonical.js +49 -0
- package/packages/reploid/src/mesh/partitions/partition-chat.js +125 -0
- package/packages/reploid/src/mesh/partitions/partition-contract.js +192 -0
- package/packages/reploid/src/mesh/partitions/partition-device-admission.js +96 -0
- package/packages/reploid/src/mesh/partitions/partition-discovery.js +96 -0
- package/packages/reploid/src/mesh/partitions/partition-entry-publication.js +183 -0
- package/packages/reploid/src/mesh/partitions/partition-entry.js +434 -0
- package/packages/reploid/src/mesh/partitions/partition-generation-request.js +33 -0
- package/packages/reploid/src/mesh/partitions/partition-grants.js +277 -0
- package/packages/reploid/src/mesh/partitions/partition-link.js +103 -0
- package/packages/reploid/src/mesh/partitions/partition-network.js +109 -0
- package/packages/reploid/src/mesh/partitions/partition-ordered-chat.js +290 -0
- package/packages/reploid/src/mesh/partitions/partition-ordered-peer.js +415 -0
- package/packages/reploid/src/mesh/partitions/partition-ordered-publication.js +100 -0
- package/packages/reploid/src/mesh/partitions/partition-peer.js +314 -0
- package/packages/reploid/src/mesh/partitions/partition-placement.js +143 -0
- package/packages/reploid/src/mesh/partitions/partition-preparation-progress.js +25 -0
- package/packages/reploid/src/mesh/partitions/partition-receiver-policy.json +7 -0
- package/packages/reploid/src/mesh/partitions/partition-reservations.js +75 -0
- package/packages/reploid/src/mesh/partitions/partition-route-envelope-policy.json +16 -0
- package/packages/reploid/src/mesh/partitions/partition-route-envelope.js +74 -0
- package/packages/reploid/src/mesh/partitions/partition-route-execution.js +409 -0
- package/packages/reploid/src/mesh/partitions/partition-route-network.js +179 -0
- package/packages/reploid/src/mesh/partitions/partition-route-ranking.js +268 -0
- package/packages/reploid/src/mesh/partitions/partition-runner.js +319 -0
- package/packages/reploid/src/mesh/partitions/partition-stage-assignment.js +109 -0
- package/packages/reploid/src/mesh/partitions/partition-stage-commit.js +103 -0
- package/packages/reploid/src/mesh/partitions/partition-step-receiver.js +291 -0
- package/packages/reploid/src/mesh/partitions/partition-summary.js +122 -0
- package/packages/reploid/src/mesh/partitions/preparation-owner.js +40 -0
- package/packages/reploid/src/mesh/partitions/resident-error-codes.json +10 -0
- package/packages/reploid/src/mesh/partitions/resident-partition.js +333 -0
- package/packages/reploid/src/mesh/partitions/resident-route.js +46 -0
- package/packages/reploid/src/mesh/partitions/signed-resident-factory.js +59 -0
- package/packages/reploid/src/mesh/peer-generation.js +896 -0
- package/packages/reploid/src/mesh/peer-identity.js +87 -0
- package/packages/reploid/src/mesh/placement-beliefs.js +107 -0
- package/packages/reploid/src/mesh/preparation-progress.js +46 -0
- package/packages/reploid/src/mesh/provider-hardware.js +11 -0
- package/packages/reploid/src/mesh/remote-generation-requests.js +53 -0
- package/packages/reploid/src/mesh/swarm-coordination.js +140 -0
- package/packages/reploid/src/mesh/web-access-network.js +112 -0
- package/packages/reploid/src/mesh/web-access-policy.json +17 -0
- package/packages/reploid/src/retrieval/citations.js +156 -0
- package/packages/reploid/src/retrieval/claim-support.js +113 -0
- package/packages/reploid/src/retrieval/costs.js +101 -0
- package/packages/reploid/src/retrieval/fixed-provider.js +34 -0
- package/packages/reploid/src/retrieval/index.js +20 -0
- package/packages/reploid/src/retrieval/lexical-support.js +27 -0
- package/packages/reploid/src/retrieval/model-reviewer.js +67 -0
- package/packages/reploid/src/retrieval/page-fetch.json +10 -0
- package/packages/reploid/src/retrieval/page-text.js +182 -0
- package/packages/reploid/src/retrieval/passage.js +37 -0
- package/packages/reploid/src/retrieval/policy.json +14 -0
- package/packages/reploid/src/retrieval/retrieval.js +23 -0
- package/packages/reploid/src/retrieval/review-queue.js +76 -0
- package/packages/reploid/src/retrieval/sources.js +263 -0
- package/packages/reploid/src/retrieval/support-review.js +155 -0
- package/packages/reploid/src/retrieval/text.js +21 -0
- package/packages/reploid/src/rules/contribution.rules.json +8 -0
- package/packages/reploid/src/rules/routing.rules.json +24 -0
- package/packages/reploid/src/transport/assignment.js +571 -0
- package/packages/reploid/src/transport/bounded-channel-writer.js +46 -0
- package/packages/reploid/src/transport/index.js +11 -0
- package/packages/reploid/src/transport/introduction-admission.js +24 -0
- package/packages/reploid/src/transport/introduction-bootstrap.js +112 -0
- package/packages/reploid/src/transport/introduction-policy.json +12 -0
- package/packages/reploid/src/transport/introduction-records.js +90 -0
- package/packages/reploid/src/transport/introductions.js +209 -0
- package/packages/reploid/src/transport/partition-data-channel.js +421 -0
- package/packages/reploid/src/transport/partition-errors.json +16 -0
- package/packages/reploid/src/transport/retry-policy.js +36 -0
- package/packages/reploid/src/transport/room.js +502 -0
- package/packages/reploid/src/transport/sdp-fingerprint.js +5 -0
- package/packages/reploid/src/transport/signaling.js +571 -0
- package/packages/reploid/src/transport/swarm.js +1247 -0
- package/packages/reploid/src/transport/tool-offer-channel.js +137 -0
- package/server/browser-roots.js +42 -0
- package/server/browser-web-worker.js +94 -0
- package/server/chat-completions.js +45 -0
- package/server/fleet/browser-bridge.js +145 -0
- package/server/fleet/contract.js +42 -0
- package/server/fleet/credit-evidence.js +103 -0
- package/server/fleet/credit-recovery.js +175 -0
- package/server/fleet/credits.js +178 -0
- package/server/fleet/execution-admission.js +32 -0
- package/server/fleet/native-host.js +817 -0
- package/server/fleet/placement.js +132 -0
- package/server/fleet/requester.js +346 -0
- package/server/fleet/service.js +311 -0
- package/server/fleet/terminal-policy.js +83 -0
- package/server/fleet/text-api.js +94 -0
- package/server/hashing.js +35 -0
- package/server/http-request-owner.js +49 -0
- package/server/messages.js +57 -0
- package/server/native-canonical-epochs.js +90 -0
- package/server/native-change-log.js +40 -0
- package/server/native-earning.js +203 -0
- package/server/native-generation-journal.js +46 -0
- package/server/native-model-config.js +57 -0
- package/server/native-model-files.js +382 -0
- package/server/native-model-origin.js +54 -0
- package/server/native-model-origins.json +12 -0
- package/server/native-node.js +85 -0
- package/server/native-owner/controls.js +162 -0
- package/server/native-owner/monitor.js +251 -0
- package/server/native-owner/policy.js +273 -0
- package/server/native-owner/rate.js +92 -0
- package/server/native-participant.js +866 -0
- package/server/native-readiness-log.js +30 -0
- package/server/native-release-storage.js +64 -0
- package/server/native-shader-cache.js +369 -0
- package/server/native-supply-budget.js +66 -0
- package/server/native-whole-earning.js +67 -0
- package/server/native-whole-model-fill.js +182 -0
- package/server/native-whole-model.js +130 -0
- package/server/public-web-worker.js +66 -0
- package/server/responses.js +92 -0
- package/server/search/errors.js +47 -0
- package/server/search/page-fetch.js +185 -0
- package/server/text-api.js +83 -0
- package/server/text-recovery.js +45 -0
- package/server/text-request-options.js +23 -0
- package/server/text-requester.js +231 -0
- package/server/token-auth.js +9 -0
- package/server/whole-model-requester.js +99 -0
- package/shared/config/chat-models.json +4685 -0
- package/shared/config/doppler-execution-contracts.js +32 -0
- package/shared/config/doppler-execution-contracts.json +43 -0
- package/shared/config/doppler-generation-contract.js +223 -0
- package/shared/config/doppler-local-models.js +79 -0
- package/shared/config/doppler-package.json +7 -0
- package/shared/config/model-manifests/a0dc69e917133a82780823d17fc771c0f6b882d7c86c571deeaef45be2ff108b.json +6434 -0
- package/shared/config/model-manifests/e985e3cdad60d0cb57d84b436161e66ab5b3f6589a168de6f00b4610e5f1fb59.json +16534 -0
- package/shared/config/model-manifests/fa6eb7bbc3ba63571f8cede780b877d9272bde948697af93dd3d33837cb46137.json +7175 -0
- package/shared/config/model-pieces/qwen-0-8b-pieces.json +1506 -0
- package/shared/config/model-pieces/qwen-2b-pieces.json +4800 -0
- package/shared/config/model-pieces/qwen35b-q4_0-pieces.json +28880 -0
- package/shared/config/reploid-library.json +49 -0
- package/shared/config/swarm-bootstrap.json +21 -0
- package/shared/infrastructure/doppler-runtime-service.js +261 -0
- package/shared/pool/executable-pack.js +87 -0
- package/shared/pool/pack-operation-adapters.js +124 -0
- package/shared/pool/pack-operation-policy.js +75 -0
- package/shared/pool/pool-config.json +2280 -0
- package/shared/vendor/reploid/adapters/doppler-session.js +372 -0
- package/shared/vendor/reploid/artifacts/binary.js +25 -0
- package/shared/vendor/reploid/artifacts/canonical-json.js +23 -0
- package/shared/vendor/reploid/artifacts/delivery-attestation.js +64 -0
- package/shared/vendor/reploid/artifacts/identity.js +91 -0
- package/shared/vendor/reploid/artifacts/manifest-identity.js +17 -0
- package/shared/vendor/reploid/artifacts/participant-proofs.js +159 -0
- package/shared/vendor/reploid/artifacts/signatures.js +94 -0
- package/shared/vendor/reploid/config/defaults.json +110 -0
- package/shared/vendor/reploid/config/index.js +209 -0
- package/shared/vendor/reploid/config/request-overrides.json +9 -0
- package/shared/vendor/reploid/hashing.js +30 -0
- package/shared/vendor/reploid/mesh/generation-context-cache.js +97 -0
- package/shared/vendor/reploid/mesh/partitions/partition-attempt-evidence.js +282 -0
- package/shared/vendor/reploid/mesh/partitions/partition-contract.js +192 -0
- package/shared/vendor/reploid/mesh/partitions/partition-receiver-policy.json +7 -0
- package/shared/vendor/reploid/mesh/partitions/partition-route-ranking.js +268 -0
- package/shared/vendor/reploid/mesh/preparation-progress.js +46 -0
- package/shared/vendor/reploid/mesh/provider-hardware.js +11 -0
- package/shared/vendor/reploid/rules/routing.rules.json +24 -0
- package/shared/vendor/reploid/transport/partition-errors.json +16 -0
- package/README.md +0 -3
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { fetchAdapterFile } from '../apps/poolday/providers/work-adapter.js';
|
|
2
|
+
import { resolvePartitionArtifactSource } from '../apps/poolday/host/work-partition-artifact-source.js';
|
|
3
|
+
/** Filesystem host ports for the existing signed custody protocol. Model weights come from the approved local
|
|
4
|
+
* directory, then authorized custody peers, then the model's pinned origin (native-model-origin.js) when one is
|
|
5
|
+
* pinned; every byte is verified against pinned identities. Explicit adapter acquisition permits its pinned origin. */
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
import { openNativeSupplyBudget } from './native-supply-budget.js';
|
|
8
|
+
import { constants } from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { sha256Hex as digest } from './hashing.js';
|
|
11
|
+
import { createCustodyExchange, createCustodyContracts, createPieceAcquisition } from '../packages/reploid/src/artifacts/custody/runtime.js';
|
|
12
|
+
import { createSigningKeyPair, exportPublicKey, sha256Hex, signCanonical, verifyCanonicalSignature } from '../packages/reploid/src/artifacts/signatures.js';
|
|
13
|
+
import { createPeerPackDataChannel } from '../apps/poolday/pool/peer-pack-data-channel.js';
|
|
14
|
+
import { hashDopplerEvidence, validateExecutablePack, executablePacksMatch } from '../shared/pool/executable-pack.js';
|
|
15
|
+
import policy from '../apps/poolday/config/chat-files.json' with { type: 'json' };
|
|
16
|
+
import { resolveServedFile } from './browser-roots.js';
|
|
17
|
+
import { fillWholeModelDirectory, wholeModelDirectoryComplete } from './native-whole-model-fill.js';
|
|
18
|
+
import { resolvePinnedOrigin, readPinnedOrigin } from './native-model-origin.js';
|
|
19
|
+
|
|
20
|
+
// How long a whole-model fill with a pinned origin waits for peers to announce before using the origin.
|
|
21
|
+
const PEER_GRACE_MS = 15000;
|
|
22
|
+
|
|
23
|
+
const assert = (ok, message) => { if (!ok) throw new Error(message); };
|
|
24
|
+
const key = file => { assert(file.hashAlgorithm === 'sha256' && /^[a-f0-9]{64}$/.test(file.hash), 'Pinned SHA-256 file required'); return 'sha256-' + file.hash; };
|
|
25
|
+
const verify = async (file, bytes) => {
|
|
26
|
+
assert(bytes instanceof Uint8Array && bytes.byteLength === file.sizeBytes && bytes.byteLength <= policy.maxArtifactBytes,
|
|
27
|
+
'Native artifact size mismatch');
|
|
28
|
+
assert(digest(bytes) === file.hash, 'Native artifact integrity mismatch');
|
|
29
|
+
};
|
|
30
|
+
const pieceFile = piece => ({ path: `piece-${piece.identity.slice(7)}.bin`, role: 'model-weights',
|
|
31
|
+
hashAlgorithm: 'sha256', hash: piece.identity.slice(7), sizeBytes: piece.size });
|
|
32
|
+
|
|
33
|
+
/** Complete verified pieces survive interruption. Temporary replacements never advertise. */
|
|
34
|
+
export async function createNativeArtifactCache({ directory, maxBytes = policy.maxStoredBytes }) {
|
|
35
|
+
assert(Number.isSafeInteger(maxBytes) && maxBytes > 0, 'Explicit cache budget required');
|
|
36
|
+
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
37
|
+
const root = await fs.realpath(directory); let writing = Promise.resolve(); const quarantined = [];
|
|
38
|
+
const get = async file => {
|
|
39
|
+
let handle;
|
|
40
|
+
try {
|
|
41
|
+
handle = await fs.open(path.join(root, key(file)), constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
42
|
+
const stat = await handle.stat(); assert(stat.isFile(), 'Native cache entry must be a regular file');
|
|
43
|
+
if (stat.size !== file.sizeBytes || stat.size > policy.maxArtifactBytes) throw Object.assign(new Error('Native cache size mismatch'), { code: 'NATIVE_CACHE_CORRUPT' });
|
|
44
|
+
const bytes = new Uint8Array(await handle.readFile());
|
|
45
|
+
if (digest(bytes) !== file.hash) throw Object.assign(new Error('Native cache integrity mismatch'), { code: 'NATIVE_CACHE_CORRUPT' });
|
|
46
|
+
return bytes;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (error.code === 'ENOENT') return null;
|
|
49
|
+
if (error.code !== 'NATIVE_CACHE_CORRUPT') throw error;
|
|
50
|
+
await handle?.close(); handle = null;
|
|
51
|
+
const name = key(file) + '.corrupt.' + crypto.randomUUID();
|
|
52
|
+
await fs.rename(path.join(root, key(file)), path.join(root, name));
|
|
53
|
+
quarantined.push({ name, expectedHash: file.hash, reason: error.message }); return null;
|
|
54
|
+
} finally { await handle?.close(); }
|
|
55
|
+
};
|
|
56
|
+
const put = (file, bytes, { signal } = {}) => {
|
|
57
|
+
const operation = writing.catch(() => {}).then(async () => {
|
|
58
|
+
signal?.throwIfAborted(); await verify(file, bytes);
|
|
59
|
+
if (await get(file)) return;
|
|
60
|
+
let used = 0;
|
|
61
|
+
for (const name of await fs.readdir(root)) {
|
|
62
|
+
const info = await fs.lstat(path.join(root, name));
|
|
63
|
+
assert(!info.isSymbolicLink(), 'Native cache cannot contain symbolic links'); used += info.size;
|
|
64
|
+
}
|
|
65
|
+
assert(used + bytes.byteLength <= maxBytes, 'Native artifact cache budget exhausted');
|
|
66
|
+
const temporary = path.join(root, key(file) + '.' + crypto.randomUUID() + '.tmp');
|
|
67
|
+
let handle;
|
|
68
|
+
try {
|
|
69
|
+
handle = await fs.open(temporary, 'wx', 0o600); await handle.writeFile(bytes); await handle.sync(); await handle.close(); handle = null;
|
|
70
|
+
signal?.throwIfAborted(); await fs.rename(temporary, path.join(root, key(file)));
|
|
71
|
+
} finally { await handle?.close(); await fs.unlink(temporary).catch(error => { if (error.code !== 'ENOENT') throw error; }); }
|
|
72
|
+
});
|
|
73
|
+
writing = operation; return operation;
|
|
74
|
+
};
|
|
75
|
+
// Drops a verified copy once the same bytes are owned elsewhere (a filled model directory).
|
|
76
|
+
const remove = file => {
|
|
77
|
+
const operation = writing.catch(() => {}).then(() => fs.unlink(path.join(root, key(file)))
|
|
78
|
+
.catch(error => { if (error.code !== 'ENOENT') throw error; }));
|
|
79
|
+
writing = operation; return operation;
|
|
80
|
+
};
|
|
81
|
+
return Object.freeze({ get, put, remove, getState: () => ({ quarantined: structuredClone(quarantined) }), close: () => writing });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** `owner` is the optional native owner gate (server/native-owner/controls.js). Without
|
|
85
|
+
* it, channels, supply admission and acquisition deadlines are exactly as before.
|
|
86
|
+
* `certifyCustodyKey` (packages/reploid/src/artifacts/custody-certificate.js) lets the persistent
|
|
87
|
+
* participant identity certify this process's custody key; without it the key stays
|
|
88
|
+
* unattributable to any device. */
|
|
89
|
+
export async function createNativeModelFiles({ transport, modelDirectory, cacheDirectory, onChange = () => {}, owner = null, certifyCustodyKey = null,
|
|
90
|
+
modelOrigins = undefined, originFetch = undefined, peerGraceMs = PEER_GRACE_MS }) {
|
|
91
|
+
const root = await fs.realpath(modelDirectory), cache = await createNativeArtifactCache({ directory: cacheDirectory });
|
|
92
|
+
let budget = null, budgetError = null;
|
|
93
|
+
try { budget = await openNativeSupplyBudget({ directory: cacheDirectory, maxBytes: policy.maxSupplyBytes }); }
|
|
94
|
+
catch (error) { budgetError = error; }
|
|
95
|
+
const supplyBudget = { getState: () => budget?.getState() || null,
|
|
96
|
+
reserve: bytes => { if (!budget) throw budgetError; return budget.reserve(bytes); } };
|
|
97
|
+
const pair = await createSigningKeyPair(), lifetime = new AbortController(), sources = new Set();
|
|
98
|
+
const custodyKey = { peerId: transport._getPeerId(), publicKey: await exportPublicKey(pair.publicKey), privateKey: pair.privateKey };
|
|
99
|
+
const custodyCertificate = async () => certifyCustodyKey
|
|
100
|
+
? certifyCustodyKey({ custodyPeerId: custodyKey.peerId, custodyPublicKey: custodyKey.publicKey }) : null;
|
|
101
|
+
const receipts = [], retained = new Map(), operations = new Set(), offeredFiles = new Map(), localPieces = new Map();
|
|
102
|
+
let closed = false, shareController = null, closing = null, suppliedModel = null;
|
|
103
|
+
const own = operation => { operations.add(operation); operation.finally(() => operations.delete(operation)).catch(() => {}); return operation; };
|
|
104
|
+
const readLocal = async (name, { offset, size, signal } = {}) => {
|
|
105
|
+
signal?.throwIfAborted(); assert(/^[\w.-]+$/.test(name), 'Plain model filename required');
|
|
106
|
+
let handle;
|
|
107
|
+
try {
|
|
108
|
+
const filename = await fs.realpath(path.join(root, name));
|
|
109
|
+
assert(path.dirname(filename) === root, 'Model file escapes approved directory');
|
|
110
|
+
handle = await fs.open(filename, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
111
|
+
const stat = await handle.stat(); assert(stat.isFile(), 'Regular model file required');
|
|
112
|
+
if (offset === undefined) { assert(stat.size <= policy.maxArtifactBytes, 'Native file exceeds artifact budget'); return new Uint8Array(await handle.readFile()); }
|
|
113
|
+
assert(Number.isSafeInteger(offset) && offset >= 0 && Number.isSafeInteger(size) && size > 0
|
|
114
|
+
&& size <= policy.maxArtifactBytes && offset + size <= stat.size, 'Invalid assigned model piece');
|
|
115
|
+
const bytes = new Uint8Array(size); let received = 0;
|
|
116
|
+
while (received < size) { signal?.throwIfAborted(); const part = await handle.read(bytes, received, size - received, offset + received);
|
|
117
|
+
assert(part.bytesRead > 0, 'Truncated model piece'); received += part.bytesRead; }
|
|
118
|
+
return bytes;
|
|
119
|
+
} catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
120
|
+
finally { await handle?.close(); }
|
|
121
|
+
};
|
|
122
|
+
const observe = receipt => { receipts.push(receipt); if (receipts.length > policy.maxReceipts) receipts.shift(); onChange(); };
|
|
123
|
+
// Transfer evidence with both parties' certificates, for whoever claims the delivery.
|
|
124
|
+
const deliveries = [], observeDelivery = record => { deliveries.push(record); if (deliveries.length > policy.maxReceipts) deliveries.shift(); };
|
|
125
|
+
const contracts = createCustodyContracts({ hashDopplerEvidence, validateExecutablePack, executablePacksMatch, sha256Hex, signCanonical, verifyCanonicalSignature });
|
|
126
|
+
const exchange = createCustodyExchange({ transport, identity: custodyKey, policy, ports: { supplyBudget,
|
|
127
|
+
createSupplier: contracts.createPeerPackSupplier, createStore: contracts.createPeerPackArtifactStore,
|
|
128
|
+
createChannel: owner ? owner.paceChannel(createPeerPackDataChannel) : createPeerPackDataChannel,
|
|
129
|
+
...(owner ? { admitSupply: request => owner.admitSupply(request) } : {}), hash: hashDopplerEvidence, hashBytes: sha256Hex,
|
|
130
|
+
readArtifact: async file => {
|
|
131
|
+
const local = localPieces.get(key(file));
|
|
132
|
+
const bytes = await cache.get(file) || (local && await readLocal(local.path, { ...local, signal: lifetime.signal }));
|
|
133
|
+
assert(bytes, 'Advertised native bytes are unavailable'); await verify(file, bytes); return bytes;
|
|
134
|
+
},
|
|
135
|
+
verifyArtifact: verify, commitArtifact: (file, bytes, controls) => cache.put(file, bytes, controls), observe, onChange,
|
|
136
|
+
certificate: custodyCertificate, observeDelivery
|
|
137
|
+
} });
|
|
138
|
+
const readAvailableMetadata = async (file, signal) => {
|
|
139
|
+
signal.throwIfAborted(); let bytes = await cache.get(file);
|
|
140
|
+
// Capsule metadata paths are logical names, not paths inside the weight directory.
|
|
141
|
+
if (!bytes && /^[a-zA-Z0-9_.-]+$/.test(file.path)) bytes = await readLocal(file.path, { signal });
|
|
142
|
+
if (!bytes && /^\/config\/model-(manifests|pieces|artifacts)\/[a-zA-Z0-9_.-]+$/.test(file.url || '')) {
|
|
143
|
+
bytes = new Uint8Array(await fs.readFile(resolveServedFile(file.url)));
|
|
144
|
+
}
|
|
145
|
+
return bytes;
|
|
146
|
+
};
|
|
147
|
+
const readMetadata = async (file, signal) => {
|
|
148
|
+
let bytes = await readAvailableMetadata(file, signal);
|
|
149
|
+
if (!bytes) {
|
|
150
|
+
assert(exchange.has(file), 'No authorized peer offers required model metadata'); bytes = await exchange.acquire(file, { signal });
|
|
151
|
+
}
|
|
152
|
+
await verify(file, bytes); signal.throwIfAborted(); await cache.put(file, bytes, { signal }); retained.set(key(file), file); return bytes;
|
|
153
|
+
};
|
|
154
|
+
const metadata = async (model, signal) => {
|
|
155
|
+
const manifestFile = model.source.files.find(file => file.path === 'manifest.json');
|
|
156
|
+
const indexFile = model.source.files.find(file => file.role === 'model-piece-index');
|
|
157
|
+
assert(manifestFile && 'sha256:' + manifestFile.hash === model.identity && indexFile, 'Pinned manifest and piece index required');
|
|
158
|
+
const manifestBytes = await readMetadata(manifestFile, signal), indexBytes = await readMetadata(indexFile, signal);
|
|
159
|
+
const runtime = await import('doppler-gpu/partitions');
|
|
160
|
+
// Doppler validates the index closure before any piece is offered or read.
|
|
161
|
+
const checked = await runtime.createVerifiedPieceStorage({ manifestBytes, indexBytes, indexIdentity: 'sha256:' + indexFile.hash,
|
|
162
|
+
signal, acquire: () => { throw new Error('Metadata validation cannot acquire weights'); } });
|
|
163
|
+
await checked.storage.close();
|
|
164
|
+
return { runtime, manifestBytes, indexBytes, indexIdentity: 'sha256:' + indexFile.hash, manifest: checked.manifest,
|
|
165
|
+
index: JSON.parse(new TextDecoder().decode(indexBytes)) };
|
|
166
|
+
};
|
|
167
|
+
const getState = ({ includeReceipts = true } = {}) => ({ ...exchange.getState(), cache: cache.getState(),
|
|
168
|
+
supplyBudgetError: budgetError && { code: budgetError.code || 'CUSTODY_BUDGET_STORAGE', message: budgetError.message },
|
|
169
|
+
retainedFiles: retained.size, receiptCount: receipts.length,
|
|
170
|
+
...(includeReceipts ? { receipts: structuredClone(receipts) } : {}) });
|
|
171
|
+
exchange.announce();
|
|
172
|
+
return Object.freeze({ getState, announce: () => exchange.announce(), custodyCertificate, getDeliveryEvidence: () => structuredClone(deliveries),
|
|
173
|
+
acquireAdapter(artifact, { signal }) {
|
|
174
|
+
return own((async () => {
|
|
175
|
+
const combined = AbortSignal.any([lifetime.signal, signal]);
|
|
176
|
+
combined.throwIfAborted();
|
|
177
|
+
assert(artifact.role === 'adapter', 'Adapter acquisition requires a pinned adapter artifact');
|
|
178
|
+
assert(Number.isSafeInteger(artifact.sizeBytes) && artifact.sizeBytes > 0
|
|
179
|
+
&& artifact.sizeBytes <= policy.maxArtifactBytes, 'Adapter exceeds the file allowance');
|
|
180
|
+
const file = { ...artifact, hash: artifact.hash.replace(/^sha256:/, ''), hashAlgorithm: 'sha256' };
|
|
181
|
+
let bytes = await readAvailableMetadata(file, combined);
|
|
182
|
+
if (!bytes) bytes = exchange.has(file) ? await exchange.acquire(file, { signal: combined })
|
|
183
|
+
: await fetchAdapterFile(artifact, { signal: combined });
|
|
184
|
+
await verify(file, bytes); combined.throwIfAborted();
|
|
185
|
+
await cache.put(file, bytes, { signal: combined }); retained.set(key(file), file);
|
|
186
|
+
return bytes;
|
|
187
|
+
})());
|
|
188
|
+
},
|
|
189
|
+
preparePartitionSource(model, { signal } = {}) { return own((async () => {
|
|
190
|
+
signal = AbortSignal.any([lifetime.signal, ...(signal ? [signal] : [])]);
|
|
191
|
+
const data = await metadata(model, signal), descriptors = new Map(); signal.throwIfAborted();
|
|
192
|
+
const partitionOrigin = resolvePinnedOrigin(model, modelOrigins, originFetch);
|
|
193
|
+
for (const file of data.index.files) for (const piece of file.pieces) descriptors.set(piece.identity, { ...piece, path: file.path });
|
|
194
|
+
const acquisition = createPieceAcquisition({
|
|
195
|
+
limits: { maxConcurrent: policy.maxPieceAcquisitions, maxQueued: policy.maxQueuedPieceAcquisitions,
|
|
196
|
+
maxPieceBytes: policy.maxArtifactBytes,
|
|
197
|
+
timeoutMs: owner ? owner.pieceTimeoutMs(policy.channel.timeoutMs, policy.maxArtifactBytes) : policy.channel.timeoutMs },
|
|
198
|
+
cache: { get: async id => {
|
|
199
|
+
const piece = descriptors.get(id);
|
|
200
|
+
// Existing owner-approved files are reusable custody, not another cache download.
|
|
201
|
+
// createPieceAcquisition verifies these bytes before returning them.
|
|
202
|
+
return await cache.get(pieceFile(piece)) ?? await readLocal(piece.path, { ...piece, signal });
|
|
203
|
+
},
|
|
204
|
+
put: async (id, bytes) => {
|
|
205
|
+
const file = pieceFile(descriptors.get(id)); await cache.put(file, bytes, { signal }); retained.set(key(file), file);
|
|
206
|
+
if (!closed && suppliedModel === model.identity) {
|
|
207
|
+
assert(offeredFiles.has(key(file)) || offeredFiles.size < policy.maxInventoryFiles, 'Native inventory limit exceeded');
|
|
208
|
+
offeredFiles.set(key(file), file); exchange.offer([...offeredFiles.values()]);
|
|
209
|
+
}
|
|
210
|
+
} },
|
|
211
|
+
authorize: async () => !signal.aborted, foregroundIdle: async current => current.throwIfAborted(),
|
|
212
|
+
verify: async (piece, bytes) => bytes.byteLength === piece.size && 'sha256:' + digest(bytes) === piece.identity,
|
|
213
|
+
sources: piece => [
|
|
214
|
+
{ id: 'approved-local-directory', read: async (_, controls) => {
|
|
215
|
+
const bytes = await readLocal(piece.path, { ...piece, signal: controls.signal }); assert(bytes, 'Assigned piece absent locally'); return bytes;
|
|
216
|
+
} },
|
|
217
|
+
{ id: 'authorized-custody-peers', read: async (_, controls) => {
|
|
218
|
+
const file = pieceFile(piece);
|
|
219
|
+
if (exchange.has(file)) return exchange.acquire(file, controls);
|
|
220
|
+
const shard = data.manifest.shards.find(row => row.filename === piece.path);
|
|
221
|
+
const whole = shard ? { path: shard.filename, role: 'model-weights', sizeBytes: shard.size, hash: shard.hash, hashAlgorithm: data.manifest.hashAlgorithm }
|
|
222
|
+
: model.source.files.find(file => file.path === piece.path);
|
|
223
|
+
assert(whole, 'Piece is outside pinned model files');
|
|
224
|
+
assert(exchange.has(whole), 'No peer offers the assigned verified piece');
|
|
225
|
+
return exchange.acquire(whole, { ...controls, range: { offset: piece.offset, size: piece.size, identity: piece.identity } });
|
|
226
|
+
} },
|
|
227
|
+
...(partitionOrigin ? [{ id: partitionOrigin.id, read: (_, controls) => readPinnedOrigin(partitionOrigin, piece.path,
|
|
228
|
+
{ offset: piece.offset, size: piece.size, signal: controls.signal }) }] : [])
|
|
229
|
+
] });
|
|
230
|
+
const abort = () => { acquisition.close().catch(() => {}); };
|
|
231
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
232
|
+
let opened;
|
|
233
|
+
try {
|
|
234
|
+
opened = await data.runtime.createVerifiedPieceStorage({ ...data, signal,
|
|
235
|
+
acquire: (piece, controls) => acquisition.acquire(piece, controls) });
|
|
236
|
+
} catch (error) {
|
|
237
|
+
signal.removeEventListener('abort', abort);
|
|
238
|
+
try { await acquisition.close(); }
|
|
239
|
+
catch (cleanup) { throw new AggregateError([error, cleanup], 'Native source opening and cleanup failed'); }
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
let sourceClosed = false;
|
|
243
|
+
const admittedFile = artifact => {
|
|
244
|
+
assert(!sourceClosed && !closed, 'Partition artifact source is closed');
|
|
245
|
+
return resolvePartitionArtifactSource(model, data.manifest, model.source.files, artifact);
|
|
246
|
+
};
|
|
247
|
+
const artifactStore = Object.freeze({
|
|
248
|
+
async readArtifact(artifact, controls = {}) {
|
|
249
|
+
const file = admittedFile(artifact);
|
|
250
|
+
assert(!data.manifest.shards.some(row => row.filename === file.path), 'Native weights require complete pinned piece ranges');
|
|
251
|
+
return readMetadata(file, AbortSignal.any([signal, controls.signal].filter(Boolean)));
|
|
252
|
+
},
|
|
253
|
+
async readArtifactRange(artifact, offset, size, controls = {}) {
|
|
254
|
+
const file = admittedFile(artifact);
|
|
255
|
+
const piece = data.index.files.find(row => row.path === file.path)?.pieces
|
|
256
|
+
.find(piece => piece.offset === offset && piece.size === size);
|
|
257
|
+
assert(piece, 'Capsule range is not a complete pinned piece');
|
|
258
|
+
return acquisition.acquire({ ...piece, path: file.path }, {
|
|
259
|
+
...controls, signal: AbortSignal.any([signal, controls.signal].filter(Boolean)) });
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
const source = { manifest: opened.manifest, artifactStore,
|
|
263
|
+
storage: { ...opened.storage, async close() {
|
|
264
|
+
sourceClosed = true;
|
|
265
|
+
signal.removeEventListener('abort', abort);
|
|
266
|
+
try {
|
|
267
|
+
const settled = await Promise.allSettled([Promise.resolve().then(() => acquisition.close()), Promise.resolve().then(() => opened.storage.close())]);
|
|
268
|
+
const failures = settled.filter(result => result.status === 'rejected').map(result => result.reason);
|
|
269
|
+
if (failures.length) throw new AggregateError(failures, 'Native partition source cleanup failed');
|
|
270
|
+
} finally { sources.delete(source); }
|
|
271
|
+
} }, getReceipt: () => ({ ...opened.getReceipt(), custody: acquisition.getReceipt() }) };
|
|
272
|
+
sources.add(source); return source;
|
|
273
|
+
})()); },
|
|
274
|
+
/** Completes the model directory for whole-model execution from authorized peers. A complete
|
|
275
|
+
* directory is left untouched. Pieces come from the verified cache or authorized custody peers
|
|
276
|
+
* only; the fill verifies each piece and each assembled file. Until some supplier has
|
|
277
|
+
* delivered, it waits for one; afterwards a piece no connected supplier offers within one
|
|
278
|
+
* channel timeout fails the fill. */
|
|
279
|
+
fillWholeModel(model, { signal, onProgress = () => {} } = {}) { return own((async () => {
|
|
280
|
+
signal = AbortSignal.any([lifetime.signal, ...(signal ? [signal] : [])]);
|
|
281
|
+
if (await wholeModelDirectoryComplete(root, model)) return { complete: true, filled: false };
|
|
282
|
+
const data = await metadata(model, signal);
|
|
283
|
+
const origin = resolvePinnedOrigin(model, modelOrigins, originFetch), graceUntil = Date.now() + peerGraceMs;
|
|
284
|
+
let supplied = false, waiting = false, originAnnounced = false;
|
|
285
|
+
const peerOffers = (file, whole) => exchange.has(file) || Boolean(whole && exchange.has(whole));
|
|
286
|
+
// Peers are preferred. Without a pinned origin the fill waits for a first supplier and then
|
|
287
|
+
// allows one channel timeout per piece; with one, it waits only a short grace for peers to announce.
|
|
288
|
+
const awaitPeers = async (file, whole) => {
|
|
289
|
+
const deadline = origin ? (supplied ? 0 : graceUntil)
|
|
290
|
+
: supplied ? Date.now() + (owner ? owner.pieceTimeoutMs(policy.channel.timeoutMs, policy.maxArtifactBytes) : policy.channel.timeoutMs) : Infinity;
|
|
291
|
+
while (!peerOffers(file, whole)) {
|
|
292
|
+
signal.throwIfAborted();
|
|
293
|
+
if (Date.now() >= deadline) {
|
|
294
|
+
assert(origin, `No authorized peer offers piece ${file.hash} of ${whole?.path ?? file.path}`);
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
if (!waiting) { waiting = true; onProgress({ phase: 'waiting-for-supplier' }); }
|
|
298
|
+
exchange.announce(); await new Promise(resolve => setTimeout(resolve, 1000));
|
|
299
|
+
}
|
|
300
|
+
waiting = false; return true;
|
|
301
|
+
};
|
|
302
|
+
const fromOrigin = async (name, range, reason) => {
|
|
303
|
+
if (!originAnnounced) { originAnnounced = true; onProgress({ phase: 'source', source: origin.id, origin: origin.label, reason }); }
|
|
304
|
+
return { bytes: await readPinnedOrigin(origin, name, { ...range, signal }), source: origin.id };
|
|
305
|
+
};
|
|
306
|
+
const acquire = async (piece, controls) => {
|
|
307
|
+
const file = pieceFile(piece), cached = await cache.get(file);
|
|
308
|
+
if (cached) return { bytes: cached, source: 'verified-cache' };
|
|
309
|
+
const shard = data.manifest.shards.find(row => row.filename === piece.path);
|
|
310
|
+
const whole = shard ? { path: shard.filename, role: 'model-weights', sizeBytes: shard.size, hash: shard.hash, hashAlgorithm: data.manifest.hashAlgorithm } : null;
|
|
311
|
+
let reason = 'no authorized peer offers the piece';
|
|
312
|
+
if (await awaitPeers(file, whole)) {
|
|
313
|
+
try {
|
|
314
|
+
const bytes = exchange.has(file) ? await exchange.acquire(file, controls)
|
|
315
|
+
: await exchange.acquire(whole, { ...controls, range: { offset: piece.offset, size: piece.size, identity: piece.identity } });
|
|
316
|
+
supplied = true; return { bytes, source: 'authorized-custody-peers' };
|
|
317
|
+
} catch (error) {
|
|
318
|
+
if (!origin || signal.aborted) throw error;
|
|
319
|
+
reason = 'authorized peers failed: ' + error.message;
|
|
320
|
+
onProgress({ phase: 'source-failed', source: 'authorized-custody-peers', piece: piece.identity, error: error.message });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return fromOrigin(piece.path, { offset: piece.offset, size: piece.size }, reason);
|
|
324
|
+
};
|
|
325
|
+
const readPinned = async (file, current) => {
|
|
326
|
+
try { return await readMetadata(file, current); }
|
|
327
|
+
catch (error) {
|
|
328
|
+
// The manifest is always the bundled pinned copy, never an origin's.
|
|
329
|
+
if (!origin || file.path === 'manifest.json' || current.aborted) throw error;
|
|
330
|
+
const read = await fromOrigin(file.path, { size: file.sizeBytes, whole: true }, error.message);
|
|
331
|
+
await verify(file, read.bytes); return read;
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
const result = await fillWholeModelDirectory({ root, model, manifest: data.manifest, index: data.index, signal, onProgress, acquire,
|
|
335
|
+
// The same piece concurrency as partition acquisition: one peer channel admits few pending chunk requests.
|
|
336
|
+
concurrency: policy.maxPieceAcquisitions,
|
|
337
|
+
readMetadata: readPinned,
|
|
338
|
+
release: async file => { for (const piece of file.pieces) await cache.remove(pieceFile(piece)); },
|
|
339
|
+
freeBytes: async directory => { const stat = await fs.statfs(directory); return stat.bavail * stat.bsize; } });
|
|
340
|
+
return { complete: true, filled: true, ...result };
|
|
341
|
+
})()); },
|
|
342
|
+
share(model, approved) { return own((async () => {
|
|
343
|
+
assert(approved === true && !closed, 'Approve file contribution separately from compute');
|
|
344
|
+
if (budgetError) throw budgetError;
|
|
345
|
+
assert(!shareController, 'File inventory preparation already active');
|
|
346
|
+
const controller = new AbortController(); shareController = controller;
|
|
347
|
+
const signal = AbortSignal.any([lifetime.signal, controller.signal]);
|
|
348
|
+
try {
|
|
349
|
+
const data = await metadata(model, signal), offered = [], approvedLocalPieces = new Map();
|
|
350
|
+
for (const file of [...model.source.files, ...(model.adapters || []).map(adapter => ({ ...adapter.artifact, hash: adapter.artifact.hash.replace(/^sha256:/, ''), hashAlgorithm: 'sha256' }))]) {
|
|
351
|
+
// Program/evidence metadata ships in the application. Custody offers
|
|
352
|
+
// manifest-local model files and verified pieces, never nested program paths.
|
|
353
|
+
if (!/^[a-zA-Z0-9_.-]+$/.test(file.path)) continue;
|
|
354
|
+
const bytes = await readAvailableMetadata(file, signal);
|
|
355
|
+
if (bytes) { await verify(file, bytes); await cache.put(file, bytes, { signal }); offered.push(file); }
|
|
356
|
+
}
|
|
357
|
+
for (const file of data.index.files) for (const piece of file.pieces) {
|
|
358
|
+
signal.throwIfAborted(); const descriptor = pieceFile(piece);
|
|
359
|
+
const cached = await cache.get(descriptor);
|
|
360
|
+
const bytes = cached || await readLocal(file.path, { ...piece, signal });
|
|
361
|
+
if (!bytes) continue;
|
|
362
|
+
await verify(descriptor, bytes);
|
|
363
|
+
if (!cached) approvedLocalPieces.set(key(descriptor), { ...piece, path: file.path });
|
|
364
|
+
offered.push(descriptor); assert(offered.length <= policy.maxInventoryFiles, 'Native inventory limit exceeded');
|
|
365
|
+
}
|
|
366
|
+
signal.throwIfAborted(); offeredFiles.clear(); localPieces.clear();
|
|
367
|
+
for (const [id, piece] of approvedLocalPieces) localPieces.set(id, piece);
|
|
368
|
+
for (const file of offered) offeredFiles.set(key(file), file);
|
|
369
|
+
suppliedModel = model.identity; exchange.offer(offered); return getState();
|
|
370
|
+
} finally { if (shareController === controller) shareController = null; }
|
|
371
|
+
})()); },
|
|
372
|
+
stopSupply() { suppliedModel = null; offeredFiles.clear(); localPieces.clear(); shareController?.abort(new Error('Native file contribution stopped')); exchange.stopSupply(); },
|
|
373
|
+
close() { return closing ||= (async () => {
|
|
374
|
+
closed = true; suppliedModel = null; offeredFiles.clear(); localPieces.clear(); lifetime.abort(new Error('Native file host closed')); exchange.stopSupply();
|
|
375
|
+
await Promise.allSettled([...operations]);
|
|
376
|
+
const settled = await Promise.allSettled([...sources].map(source => Promise.resolve().then(() => source.storage.close()))
|
|
377
|
+
.concat([Promise.resolve().then(() => exchange.close()), Promise.resolve().then(() => cache.close()), Promise.resolve().then(() => budget?.close())]));
|
|
378
|
+
const failures = settled.filter(result => result.status === 'rejected').map(result => result.reason);
|
|
379
|
+
if (failures.length) throw new AggregateError(failures, 'Native artifact source cleanup failed');
|
|
380
|
+
})(); }
|
|
381
|
+
});
|
|
382
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Pinned public origin for model files: the last source after the approved local directory and
|
|
2
|
+
* authorized custody peers. The origin is fixed here per exact model identity (repository and
|
|
3
|
+
* immutable revision), never taken from peers or requests, and fetched without credentials.
|
|
4
|
+
* Callers verify every byte against pinned piece identities or file hashes; this module only
|
|
5
|
+
* bounds what it returns. The manifest never comes from an origin: the bundled pinned copy is used. */
|
|
6
|
+
import origins from './native-model-origins.json' with { type: 'json' };
|
|
7
|
+
|
|
8
|
+
const assert = (ok, message, code = 'MODEL_ORIGIN_INVALID') => { if (!ok) throw Object.assign(new Error(message), { code }); };
|
|
9
|
+
export const PINNED_ORIGIN_SOURCE = 'pinned-origin';
|
|
10
|
+
|
|
11
|
+
/** The pinned origin for this exact model, or null. `table` and `fetch` replace the shipped pins and
|
|
12
|
+
* the network in tests. */
|
|
13
|
+
export function resolvePinnedOrigin(model, table = origins, fetch = (...args) => globalThis.fetch(...args)) {
|
|
14
|
+
assert(table?.schema === 'reploid.native-model-origins/v1', 'Unknown model origin table');
|
|
15
|
+
const pin = table.origins?.[model?.id];
|
|
16
|
+
if (!pin) return null;
|
|
17
|
+
assert(pin.modelIdentity === model.identity, 'Pinned origin names another model identity');
|
|
18
|
+
assert(pin.kind === 'huggingface' && /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(pin.repoId)
|
|
19
|
+
&& /^[a-f0-9]{40}$/.test(pin.revision) && typeof pin.path === 'string'
|
|
20
|
+
&& pin.path.split('/').every(part => /^[A-Za-z0-9_.-]+$/.test(part) && !/^\.\.?$/.test(part)), 'Pinned origin must name a repository, an immutable revision and a plain path');
|
|
21
|
+
const base = `https://huggingface.co/${pin.repoId}/resolve/${pin.revision}/${pin.path}/`;
|
|
22
|
+
return Object.freeze({ id: `${PINNED_ORIGIN_SOURCE}:huggingface`, label: `huggingface:${pin.repoId}@${pin.revision}/${pin.path}`, fetch,
|
|
23
|
+
url: name => { assert(/^[A-Za-z0-9_.-]+$/.test(name) && !/^\.\.?$/.test(name), 'Plain model filename required'); return base + name; } });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Reads `size` bytes at `offset` of `name` (a range request), or the whole file when `whole` is
|
|
27
|
+
* set. Returns exactly the requested length or throws. Credentials and cookies are never sent. */
|
|
28
|
+
export async function readPinnedOrigin(origin, name, { offset = 0, size, whole = false, signal, fetch = origin.fetch }) {
|
|
29
|
+
assert(Number.isSafeInteger(offset) && offset >= 0 && Number.isSafeInteger(size) && size > 0, 'Explicit origin range required');
|
|
30
|
+
const response = await fetch(origin.url(name), { signal, credentials: 'omit', redirect: 'follow', cache: 'no-store',
|
|
31
|
+
headers: whole ? {} : { range: `bytes=${offset}-${offset + size - 1}` } });
|
|
32
|
+
const ranged = response.status === 206, complete = response.status === 200 && (whole || offset === 0);
|
|
33
|
+
if (!ranged && !complete) { await response.body?.cancel(); assert(false, `Pinned origin answered ${response.status} for ${name}`, 'MODEL_ORIGIN_UNAVAILABLE'); }
|
|
34
|
+
if (ranged) {
|
|
35
|
+
const match = /^bytes (\d+)-(\d+)\/(\d+|\*)$/.exec(response.headers.get('content-range') || '');
|
|
36
|
+
if (!match || Number(match[1]) !== offset || Number(match[2]) !== offset + size - 1) {
|
|
37
|
+
await response.body?.cancel(); assert(false, `Pinned origin returned another range of ${name}`, 'MODEL_ORIGIN_RANGE');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const bytes = new Uint8Array(size); let received = 0;
|
|
41
|
+
const reader = response.body.getReader();
|
|
42
|
+
try {
|
|
43
|
+
for (;;) {
|
|
44
|
+
const { done, value } = await reader.read();
|
|
45
|
+
if (done) break;
|
|
46
|
+
const take = Math.min(value.byteLength, size - received);
|
|
47
|
+
bytes.set(value.subarray(0, take), received); received += take;
|
|
48
|
+
// A whole-file answer to a range request at offset 0 is cut to the requested length.
|
|
49
|
+
if (received === size) { if (ranged || whole) assert((await reader.read()).done, `Pinned origin sent more than ${size} bytes of ${name}`, 'MODEL_ORIGIN_RANGE'); break; }
|
|
50
|
+
}
|
|
51
|
+
} finally { await reader.cancel().catch(() => {}); }
|
|
52
|
+
assert(received === size, `Pinned origin sent ${received} of ${size} bytes of ${name}`, 'MODEL_ORIGIN_RANGE');
|
|
53
|
+
return bytes;
|
|
54
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "reploid.native-model-origins/v1",
|
|
3
|
+
"origins": {
|
|
4
|
+
"qwen-3-5-2b-q4k-ehaf16": {
|
|
5
|
+
"modelIdentity": "sha256:a0dc69e917133a82780823d17fc771c0f6b882d7c86c571deeaef45be2ff108b",
|
|
6
|
+
"kind": "huggingface",
|
|
7
|
+
"repoId": "clocksmith/rdrr",
|
|
8
|
+
"revision": "3a05b346b1c86abf02c7620f2369a9a7b82cb62f",
|
|
9
|
+
"path": "models/qwen-3-5-2b-q4k-ehaf16"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* One short command for a native participant: `reploid <catalog model id>`.
|
|
4
|
+
* It only fills in the conventional values the participant otherwise asks for
|
|
5
|
+
* (state under ~/.reploid, the supported transport and GPU packages) and says
|
|
6
|
+
* what it chose. The participant, its consent rules and its checks are unchanged.
|
|
7
|
+
*/
|
|
8
|
+
import { realpathSync } from 'node:fs';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import process from 'node:process';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
|
|
14
|
+
const MODES = ['--connect-only', '--share-files', '--contribute'];
|
|
15
|
+
// The model `reploid` serves when no model id is given: the one public peers supply.
|
|
16
|
+
export const DEFAULT_MODEL = 'qwen-3-5-2b-q4k-ehaf16';
|
|
17
|
+
// Options that only exist for partition execution; giving one keeps the participant's own default.
|
|
18
|
+
const PARTITION_ONLY = ['--partition-index', '--ordered-config', '--canonical', '--readiness-log'];
|
|
19
|
+
const USAGE = `Reploid node (Node 22.13 or newer)
|
|
20
|
+
Usage: reploid [catalog model id] [participant options]
|
|
21
|
+
|
|
22
|
+
reploid (same as: reploid qwen-3-5-2b-q4k-ehaf16)
|
|
23
|
+
reploid qwen-3-5-2b-q4k-ehaf16
|
|
24
|
+
|
|
25
|
+
Contributes this computer's GPU for that model and runs the whole model here,
|
|
26
|
+
so it serves requests on its own. A model not yet on this computer is fetched
|
|
27
|
+
from authorized peers piece by piece (or, when no peer offers a piece, from the
|
|
28
|
+
model's pinned public origin), each piece and each file verified against the
|
|
29
|
+
catalog, into ~/.reploid/weights/<model id>; an interrupted fetch resumes.
|
|
30
|
+
|
|
31
|
+
Chosen for you unless given: --contribute, --execution-mode whole-model,
|
|
32
|
+
--model-directory ~/.reploid/weights/<model id>, --identity-file
|
|
33
|
+
~/.reploid/identity.json, --rtc-module @roamhq/wrtc, --ws-module ws,
|
|
34
|
+
--gpu-module webgpu. --execution-mode partition (or a partition-only option
|
|
35
|
+
such as --partition-index) shares the model with other contributors instead.
|
|
36
|
+
Pass --connect-only or --share-files to choose another mode. Every other option
|
|
37
|
+
goes to the participant unchanged: run reploid-participant --help for the full
|
|
38
|
+
list. Stop with Ctrl+C.`;
|
|
39
|
+
|
|
40
|
+
/** The participant arguments this command stands for. Pure, so it can be checked. */
|
|
41
|
+
export function nodeArguments(argv, { home }) {
|
|
42
|
+
// Without a model id (nothing, or options only) the default model is served.
|
|
43
|
+
const [model, ...rest] = argv.length && !argv[0].startsWith('-') ? argv : [DEFAULT_MODEL, ...argv];
|
|
44
|
+
const given = name => rest.includes(name) || rest.some(value => value.startsWith(name + '='));
|
|
45
|
+
const state = path.join(home, '.reploid');
|
|
46
|
+
const args = ['--model', model];
|
|
47
|
+
const chosen = {};
|
|
48
|
+
if (!MODES.some(given)) { args.push('--contribute'); chosen.mode = 'contribute'; }
|
|
49
|
+
const contributes = !MODES.some(given) || given('--contribute');
|
|
50
|
+
const fill = (name, value) => { if (!given(name)) { args.push(name, value); chosen[name.slice(2)] = value; } };
|
|
51
|
+
if (contributes && !PARTITION_ONLY.some(given)) fill('--execution-mode', 'whole-model');
|
|
52
|
+
// One directory per model: catalog models share file names such as manifest.json.
|
|
53
|
+
fill('--model-directory', path.join(state, 'weights', model));
|
|
54
|
+
fill('--identity-file', path.join(state, 'identity.json'));
|
|
55
|
+
fill('--rtc-module', '@roamhq/wrtc');
|
|
56
|
+
fill('--ws-module', 'ws');
|
|
57
|
+
// The participant requires a GPU provider only for compute.
|
|
58
|
+
if (!given('--connect-only') && !(given('--share-files') && !given('--contribute'))) fill('--gpu-module', 'webgpu');
|
|
59
|
+
return { model, args: [...args, ...rest], chosen };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The process arguments the participant parses. The entry path stays this command's: the participant
|
|
63
|
+
* module starts itself when the entry path is its own file, so naming it here ran a second participant
|
|
64
|
+
* after the first one stopped on SIGINT or SIGTERM. */
|
|
65
|
+
export function participantProcessArgv(processArgv, args) {
|
|
66
|
+
return [processArgv[0], processArgv[1], ...args];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function main() {
|
|
70
|
+
const argv = process.argv.slice(2);
|
|
71
|
+
if (argv.includes('--help') || argv.includes('-h')) { console.log(USAGE); return; }
|
|
72
|
+
const node = nodeArguments(argv, { home: os.homedir() });
|
|
73
|
+
console.log(JSON.stringify({ event: 'reploid-node', model: node.model, chosen: node.chosen }));
|
|
74
|
+
const { mkdir } = await import('node:fs/promises');
|
|
75
|
+
// The participant reads weights from an existing directory; it creates the identity's.
|
|
76
|
+
const directory = node.args[node.args.indexOf('--model-directory') + 1];
|
|
77
|
+
if (directory) await mkdir(directory, { recursive: true });
|
|
78
|
+
process.argv = participantProcessArgv(process.argv, node.args);
|
|
79
|
+
const { main: runParticipant } = await import('./native-participant.js');
|
|
80
|
+
await runParticipant();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// npm starts a package command through a symbolic link; compare real paths.
|
|
84
|
+
const entered = (() => { try { return Boolean(process.argv[1]) && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url); } catch { return false; } })();
|
|
85
|
+
if (entered) await main();
|