reploid 0.0.0-stage → 0.1.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/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 +534 -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 +83 -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,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Host-neutral participant identity proofs.
|
|
3
|
+
*
|
|
4
|
+
* A device's persistent participant identity (the Ed25519 or ECDSA P-256
|
|
5
|
+
* bundle browser and native hosts already keep) signs domain-separated
|
|
6
|
+
* canonical statements: device registration proofs and session bindings here, custody-key
|
|
7
|
+
* certificates in custody-certificate.js. WebCrypto only, so browser and
|
|
8
|
+
* native hosts can sign with the same code the server verifies with.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { canonicalize } from './canonical-json.js';
|
|
12
|
+
import {
|
|
13
|
+
createPeerIdFromPublicJwk,
|
|
14
|
+
encodeBytes,
|
|
15
|
+
fromBase64Url,
|
|
16
|
+
getIdentitySignAlgorithm,
|
|
17
|
+
importSigningKey,
|
|
18
|
+
importVerificationKey,
|
|
19
|
+
toBase64Url
|
|
20
|
+
} from './identity.js';
|
|
21
|
+
|
|
22
|
+
export const REGISTRATION_PROOF_SCHEMA = 'reploid.device-registration-proof/v1';
|
|
23
|
+
export const SESSION_BINDING_SCHEMA = 'reploid.device-session-binding/v1';
|
|
24
|
+
export const DEVICE_HOSTS = Object.freeze(['browser', 'native']);
|
|
25
|
+
|
|
26
|
+
const PROOF_KEYS = Object.freeze(['schema', 'challengeId', 'nonce', 'accountId', 'audience', 'participantId', 'publicJwk', 'host', 'signature']);
|
|
27
|
+
const SESSION_BINDING_KEYS = Object.freeze(['schema', 'challengeId', 'nonce', 'accountId', 'sessionId', 'participantId', 'signature']);
|
|
28
|
+
const COORDINATE = /^[A-Za-z0-9_-]{16,128}$/;
|
|
29
|
+
|
|
30
|
+
const sameKeys = (value, keys) => Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
31
|
+
&& Object.keys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key));
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The public half of a participant identity: an Ed25519 or ECDSA P-256 JWK
|
|
35
|
+
* reduced to its coordinates. Returns null for anything else, including a
|
|
36
|
+
* JWK carrying private key material.
|
|
37
|
+
*/
|
|
38
|
+
export function normalizeParticipantPublicKey(jwk) {
|
|
39
|
+
if (!jwk || typeof jwk !== 'object' || Array.isArray(jwk) || Object.hasOwn(jwk, 'd')) return null;
|
|
40
|
+
if (jwk.kty === 'OKP' && jwk.crv === 'Ed25519' && COORDINATE.test(jwk.x ?? '') && jwk.y === undefined) {
|
|
41
|
+
return { kty: 'OKP', crv: 'Ed25519', x: jwk.x };
|
|
42
|
+
}
|
|
43
|
+
if (jwk.kty === 'EC' && jwk.crv === 'P-256' && COORDINATE.test(jwk.x ?? '') && COORDINATE.test(jwk.y ?? '')) {
|
|
44
|
+
return { kty: 'EC', crv: 'P-256', x: jwk.x, y: jwk.y };
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The participant id every host derives from its identity public key. */
|
|
50
|
+
export const participantIdFor = (publicJwk) => createPeerIdFromPublicJwk(publicJwk);
|
|
51
|
+
|
|
52
|
+
const signedBytes = (domain, body) => encodeBytes(canonicalize({ signatureDomain: domain, payload: body }));
|
|
53
|
+
|
|
54
|
+
/** Sign `body` under `domain` with a participant identity bundle ({ publicJwk, privateJwk, algorithm? }). */
|
|
55
|
+
export async function signAsParticipant(identity, domain, body) {
|
|
56
|
+
const key = await importSigningKey(identity);
|
|
57
|
+
return toBase64Url(await crypto.subtle.sign(getIdentitySignAlgorithm(identity), key, signedBytes(domain, body)));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Verify a participant signature; malformed keys or signatures fail closed. */
|
|
61
|
+
export async function verifyParticipantSignature(publicJwk, domain, body, signature) {
|
|
62
|
+
if (typeof signature !== 'string' || signature.length === 0 || signature.length > 256) return false;
|
|
63
|
+
try {
|
|
64
|
+
const key = await importVerificationKey(publicJwk);
|
|
65
|
+
return await crypto.subtle.verify(getIdentitySignAlgorithm(publicJwk), key, fromBase64Url(signature), signedBytes(domain, body));
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The statement a device signs to answer one registration challenge. */
|
|
72
|
+
export const registrationProofBody = ({ challenge, participantId, publicJwk, host }) => ({
|
|
73
|
+
schema: REGISTRATION_PROOF_SCHEMA,
|
|
74
|
+
challengeId: challenge.challengeId,
|
|
75
|
+
nonce: challenge.nonce,
|
|
76
|
+
accountId: challenge.accountId,
|
|
77
|
+
audience: challenge.audience,
|
|
78
|
+
participantId,
|
|
79
|
+
publicJwk,
|
|
80
|
+
host
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
/** Device side: answer a server registration challenge with the persistent participant identity. */
|
|
84
|
+
export async function createDeviceRegistrationProof({ identity, challenge, host }) {
|
|
85
|
+
const publicJwk = normalizeParticipantPublicKey(identity?.publicJwk);
|
|
86
|
+
if (!publicJwk || !identity.privateJwk) throw new TypeError('A persistent participant signing identity is required');
|
|
87
|
+
if (!DEVICE_HOSTS.includes(host)) throw new TypeError(`host must be one of ${DEVICE_HOSTS.join(', ')}`);
|
|
88
|
+
const body = registrationProofBody({ challenge, participantId: await participantIdFor(publicJwk), publicJwk, host });
|
|
89
|
+
return { ...body, signature: await signAsParticipant(identity, REGISTRATION_PROOF_SCHEMA, body) };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The statement a device signs to make the session that asked for
|
|
94
|
+
* `challenge` act as that device. Only a host holding that session signs it;
|
|
95
|
+
* a device registered from another session's challenge signs only the proof.
|
|
96
|
+
*/
|
|
97
|
+
export const sessionBindingBody = ({ challenge, participantId }) => ({
|
|
98
|
+
schema: SESSION_BINDING_SCHEMA,
|
|
99
|
+
challengeId: challenge.challengeId,
|
|
100
|
+
nonce: challenge.nonce,
|
|
101
|
+
accountId: challenge.accountId,
|
|
102
|
+
sessionId: challenge.sessionId,
|
|
103
|
+
participantId
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/** Device side: bind the challenge's session to this persistent participant identity. */
|
|
107
|
+
export async function createSessionBinding({ identity, challenge }) {
|
|
108
|
+
const publicJwk = normalizeParticipantPublicKey(identity?.publicJwk);
|
|
109
|
+
if (!publicJwk || !identity.privateJwk) throw new TypeError('A persistent participant signing identity is required');
|
|
110
|
+
if (typeof challenge?.sessionId !== 'string' || challenge.sessionId.length === 0) throw new TypeError('The challenge must name its session');
|
|
111
|
+
const body = sessionBindingBody({ challenge, participantId: await participantIdFor(publicJwk) });
|
|
112
|
+
return { ...body, signature: await signAsParticipant(identity, SESSION_BINDING_SCHEMA, body) };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Whether `binding` is `publicJwk`'s signed statement binding `challenge.sessionId` to it. */
|
|
116
|
+
export async function checkSessionBinding(binding, { publicJwk, challenge }) {
|
|
117
|
+
if (!sameKeys(binding, SESSION_BINDING_KEYS) || binding.schema !== SESSION_BINDING_SCHEMA) return false;
|
|
118
|
+
const participantId = await participantIdFor(publicJwk);
|
|
119
|
+
const { signature, ...body } = binding;
|
|
120
|
+
const expected = sessionBindingBody({ challenge, participantId });
|
|
121
|
+
if (Object.keys(expected).some((key) => body[key] !== expected[key])) return false;
|
|
122
|
+
return verifyParticipantSignature(publicJwk, SESSION_BINDING_SCHEMA, body, signature);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Check a registration proof's form, key-to-participant binding and
|
|
127
|
+
* signature. Challenge freshness and ownership are the store's checks.
|
|
128
|
+
* Returns { problem, publicJwk, participantId }; problem is null when valid.
|
|
129
|
+
*/
|
|
130
|
+
export async function checkDeviceRegistrationProof(proof) {
|
|
131
|
+
if (!sameKeys(proof, PROOF_KEYS) || proof.schema !== REGISTRATION_PROOF_SCHEMA) return { problem: 'proof_malformed' };
|
|
132
|
+
const publicJwk = normalizeParticipantPublicKey(proof.publicJwk);
|
|
133
|
+
if (!publicJwk || !sameKeys(proof.publicJwk, Object.keys(publicJwk))) return { problem: 'proof_malformed' };
|
|
134
|
+
if (!DEVICE_HOSTS.includes(proof.host)) return { problem: 'proof_malformed' };
|
|
135
|
+
for (const key of ['challengeId', 'nonce', 'accountId', 'audience', 'participantId']) {
|
|
136
|
+
if (typeof proof[key] !== 'string' || proof[key].length === 0 || proof[key].length > 256) return { problem: 'proof_malformed' };
|
|
137
|
+
}
|
|
138
|
+
const participantId = await participantIdFor(publicJwk);
|
|
139
|
+
if (participantId !== proof.participantId) return { problem: 'participant_key_mismatch' };
|
|
140
|
+
const { signature, ...body } = proof;
|
|
141
|
+
if (!await verifyParticipantSignature(publicJwk, REGISTRATION_PROOF_SCHEMA, body, signature)) return { problem: 'proof_signature_invalid' };
|
|
142
|
+
return { problem: null, publicJwk, participantId };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export default {
|
|
146
|
+
REGISTRATION_PROOF_SCHEMA,
|
|
147
|
+
SESSION_BINDING_SCHEMA,
|
|
148
|
+
DEVICE_HOSTS,
|
|
149
|
+
normalizeParticipantPublicKey,
|
|
150
|
+
participantIdFor,
|
|
151
|
+
signAsParticipant,
|
|
152
|
+
verifyParticipantSignature,
|
|
153
|
+
registrationProofBody,
|
|
154
|
+
createDeviceRegistrationProof,
|
|
155
|
+
checkDeviceRegistrationProof,
|
|
156
|
+
sessionBindingBody,
|
|
157
|
+
createSessionBinding,
|
|
158
|
+
checkSessionBinding
|
|
159
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { bytesToBase64, base64ToBytes } from './binary.js';
|
|
2
|
+
import { canonicalize } from './canonical-json.js';
|
|
3
|
+
import { sha256Bytes, sha256HexUnprefixed, sha256Hex } from '../hashing.js';
|
|
4
|
+
|
|
5
|
+
export { sha256Bytes, sha256HexUnprefixed, sha256Hex };
|
|
6
|
+
|
|
7
|
+
const textEncoder = new TextEncoder();
|
|
8
|
+
|
|
9
|
+
export async function hashJson(value, cryptoApi = globalThis.crypto) {
|
|
10
|
+
return sha256Hex(canonicalize(value), cryptoApi);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function createSigningKeyPair() {
|
|
14
|
+
return crypto.subtle.generateKey(
|
|
15
|
+
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
16
|
+
true,
|
|
17
|
+
['sign', 'verify']
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function exportPublicKey(publicKey) {
|
|
22
|
+
const spki = await crypto.subtle.exportKey('spki', publicKey);
|
|
23
|
+
return bytesToBase64(new Uint8Array(spki));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function exportPrivateKey(privateKey) {
|
|
27
|
+
const pkcs8 = await crypto.subtle.exportKey('pkcs8', privateKey);
|
|
28
|
+
return bytesToBase64(new Uint8Array(pkcs8));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function importPublicKey(publicKeyBase64) {
|
|
32
|
+
const bytes = base64ToBytes(publicKeyBase64);
|
|
33
|
+
return crypto.subtle.importKey(
|
|
34
|
+
'spki',
|
|
35
|
+
bytes,
|
|
36
|
+
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
37
|
+
true,
|
|
38
|
+
['verify']
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function importPrivateKey(privateKeyBase64) {
|
|
43
|
+
const bytes = base64ToBytes(privateKeyBase64);
|
|
44
|
+
return crypto.subtle.importKey(
|
|
45
|
+
'pkcs8',
|
|
46
|
+
bytes,
|
|
47
|
+
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
48
|
+
true,
|
|
49
|
+
['sign']
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function importSigningKeyPair({ privateKey, publicKey } = {}) {
|
|
54
|
+
if (!privateKey || !publicKey) throw new Error('privateKey and publicKey are required');
|
|
55
|
+
return {
|
|
56
|
+
privateKey: await importPrivateKey(privateKey),
|
|
57
|
+
publicKey: await importPublicKey(publicKey)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function domainSeparatedPayload(domain, payload) {
|
|
62
|
+
const normalized = String(domain || '').trim();
|
|
63
|
+
if (!normalized) throw new Error('signature domain is required');
|
|
64
|
+
return {
|
|
65
|
+
signatureDomain: normalized,
|
|
66
|
+
payload
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function signCanonical(value, privateKey, { domain = null } = {}) {
|
|
71
|
+
const signingValue = domain ? domainSeparatedPayload(domain, value) : value;
|
|
72
|
+
const payload = textEncoder.encode(canonicalize(signingValue));
|
|
73
|
+
const signature = await crypto.subtle.sign(
|
|
74
|
+
{ name: 'ECDSA', hash: 'SHA-256' },
|
|
75
|
+
privateKey,
|
|
76
|
+
payload
|
|
77
|
+
);
|
|
78
|
+
return bytesToBase64(new Uint8Array(signature));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function verifyCanonicalSignature(value, publicKeyBase64, signatureBase64, { domain = null, allowLegacy = false } = {}) {
|
|
82
|
+
const publicKey = await importPublicKey(publicKeyBase64);
|
|
83
|
+
const verifyValue = async (candidate) => crypto.subtle.verify(
|
|
84
|
+
{ name: 'ECDSA', hash: 'SHA-256' },
|
|
85
|
+
publicKey,
|
|
86
|
+
base64ToBytes(signatureBase64),
|
|
87
|
+
textEncoder.encode(canonicalize(candidate))
|
|
88
|
+
);
|
|
89
|
+
if (!domain) return verifyValue(value);
|
|
90
|
+
const domainOk = await verifyValue(domainSeparatedPayload(domain, value));
|
|
91
|
+
if (domainOk || !allowLegacy) return domainOk;
|
|
92
|
+
return verifyValue(value);
|
|
93
|
+
}
|
|
94
|
+
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "reploid.config/v1",
|
|
3
|
+
"agent": {
|
|
4
|
+
"maxCycles": 2048,
|
|
5
|
+
"maxBatchTools": 5,
|
|
6
|
+
"singleToolNudgeThreshold": 3,
|
|
7
|
+
"milestoneAutoparkThreshold": 3,
|
|
8
|
+
"maxContextChars": 2000000,
|
|
9
|
+
"timeoutMs": 300000,
|
|
10
|
+
"retryDelayMs": 30000,
|
|
11
|
+
"maxRetryDelayMs": 300000,
|
|
12
|
+
"rgrArchiveLimit": 24,
|
|
13
|
+
"requiredAnchorObservations": 3,
|
|
14
|
+
"toolStringPreviewChars": 2048,
|
|
15
|
+
"toolResultPayloadChars": 4096,
|
|
16
|
+
"toolBatchBodyChars": 12288,
|
|
17
|
+
"toolSanitizeMaxDepth": 4,
|
|
18
|
+
"toolSanitizeArrayLimit": 25
|
|
19
|
+
},
|
|
20
|
+
"tools": {
|
|
21
|
+
"allowed": [],
|
|
22
|
+
"parallelSafe": [
|
|
23
|
+
"ReadFile"
|
|
24
|
+
],
|
|
25
|
+
"ordered": [
|
|
26
|
+
"WriteFile",
|
|
27
|
+
"LoadModule"
|
|
28
|
+
],
|
|
29
|
+
"exclusive": [
|
|
30
|
+
"Promote"
|
|
31
|
+
],
|
|
32
|
+
"loaderId": null,
|
|
33
|
+
"allowDynamic": false
|
|
34
|
+
},
|
|
35
|
+
"memory": {
|
|
36
|
+
"storeId": null,
|
|
37
|
+
"checkpointPrefix": "/checkpoints",
|
|
38
|
+
"maxCheckpointBytes": 16777216
|
|
39
|
+
},
|
|
40
|
+
"models": {
|
|
41
|
+
"providerId": null,
|
|
42
|
+
"contract": null
|
|
43
|
+
},
|
|
44
|
+
"mesh": {
|
|
45
|
+
"enabled": false,
|
|
46
|
+
"executeJobs": false,
|
|
47
|
+
"supplyArtifacts": false,
|
|
48
|
+
"shareCandidates": false,
|
|
49
|
+
"roomId": null,
|
|
50
|
+
"routing": "local-only",
|
|
51
|
+
"generationTimeoutMs": 45000,
|
|
52
|
+
"maxPendingJobs": 8,
|
|
53
|
+
"maxInboundJobs": 1,
|
|
54
|
+
"maxRetainedJobs": 32
|
|
55
|
+
},
|
|
56
|
+
"webrtc": {
|
|
57
|
+
"protocol": "assignment/v1",
|
|
58
|
+
"signalingUrl": null,
|
|
59
|
+
"rtcConfig": {
|
|
60
|
+
"iceServers": [],
|
|
61
|
+
"iceTransportPolicy": "all"
|
|
62
|
+
},
|
|
63
|
+
"dataChannelLabel": "reploid-pool",
|
|
64
|
+
"dataChannelOptions": {
|
|
65
|
+
"ordered": true,
|
|
66
|
+
"maxRetransmits": null
|
|
67
|
+
},
|
|
68
|
+
"maxPendingRemoteIceCandidates": 64,
|
|
69
|
+
"pendingRemoteIceTtlMs": 30000,
|
|
70
|
+
"maxMessageBytes": 65536,
|
|
71
|
+
"maxBufferedBytes": 1048576,
|
|
72
|
+
"connectTimeoutMs": 30000,
|
|
73
|
+
"reconnectBaseMs": 1000,
|
|
74
|
+
"maxBackoffMs": 30000,
|
|
75
|
+
"heartbeatIntervalMs": 30000,
|
|
76
|
+
"peerTimeoutMs": 60000,
|
|
77
|
+
"announceIntervalMs": 15000,
|
|
78
|
+
"signalingProbeTimeoutMs": 2000,
|
|
79
|
+
"transportOrder": [
|
|
80
|
+
"webrtc"
|
|
81
|
+
],
|
|
82
|
+
"sessionId": null,
|
|
83
|
+
"signaling": {
|
|
84
|
+
"pollIntervalMs": 1000,
|
|
85
|
+
"pollTimeoutMs": 5000,
|
|
86
|
+
"failureThreshold": 3,
|
|
87
|
+
"backoffBaseMs": 1000,
|
|
88
|
+
"backoffMaxMs": 30000
|
|
89
|
+
},
|
|
90
|
+
"broadcastRoomId": null
|
|
91
|
+
},
|
|
92
|
+
"artifacts": {
|
|
93
|
+
"root": "/artifacts",
|
|
94
|
+
"maxBytes": 268435456
|
|
95
|
+
},
|
|
96
|
+
"improvement": {
|
|
97
|
+
"enabled": false,
|
|
98
|
+
"evaluatorId": null,
|
|
99
|
+
"approvalId": null,
|
|
100
|
+
"isolationId": null
|
|
101
|
+
},
|
|
102
|
+
"observation": {
|
|
103
|
+
"enabled": true
|
|
104
|
+
},
|
|
105
|
+
"legacyAgent": {
|
|
106
|
+
"discoveryLimit": 3,
|
|
107
|
+
"maxToolCalls": 8,
|
|
108
|
+
"settings": null
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import defaults from './defaults.json' with { type: 'json' };
|
|
2
|
+
import requestPolicy from './request-overrides.json' with { type: 'json' };
|
|
3
|
+
import routing from '../rules/routing.rules.json' with { type: 'json' };
|
|
4
|
+
import { sha256Hex } from '../hashing.js';
|
|
5
|
+
|
|
6
|
+
const resolvedConfigurations = new WeakSet();
|
|
7
|
+
const forbiddenKeys = new Set(['__proto__', 'prototype', 'constructor']);
|
|
8
|
+
|
|
9
|
+
export class ConfigurationError extends TypeError {
|
|
10
|
+
constructor(path, message) {
|
|
11
|
+
super(`Reploid configuration ${path}: ${message}`);
|
|
12
|
+
this.name = 'ConfigurationError';
|
|
13
|
+
this.path = path;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function snapshotJson(value, path = '$') {
|
|
18
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
|
|
19
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
20
|
+
if (Array.isArray(value)) return value.map((entry, index) => snapshotJson(entry, `${path}[${index}]`));
|
|
21
|
+
if (value && Object.getPrototypeOf(value) === Object.prototype) {
|
|
22
|
+
return Object.fromEntries(Object.keys(value).sort().map(key => {
|
|
23
|
+
if (forbiddenKeys.has(key)) throw new ConfigurationError(path, 'unsafe object key');
|
|
24
|
+
return [key, snapshotJson(value[key], `${path}.${key}`)];
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
throw new ConfigurationError(path, 'expected JSON data; inject functions, resources and credentials as ports');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function freezeJson(value) {
|
|
31
|
+
if (value && typeof value === 'object') {
|
|
32
|
+
Object.values(value).forEach(freezeJson);
|
|
33
|
+
Object.freeze(value);
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function merge(target, patch, source, provenance, path = '') {
|
|
39
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
40
|
+
const field = path ? `${path}.${key}` : key;
|
|
41
|
+
if (!Object.hasOwn(target, key)) throw new ConfigurationError(field, 'unknown field');
|
|
42
|
+
const old = target[key];
|
|
43
|
+
if (old && typeof old === 'object' && !Array.isArray(old) && value && typeof value === 'object' && !Array.isArray(value)) {
|
|
44
|
+
merge(old, value, source, provenance, field);
|
|
45
|
+
} else {
|
|
46
|
+
target[key] = value;
|
|
47
|
+
provenance[field] = source;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function recordSources(value, sources, path = '') {
|
|
53
|
+
for (const [key, item] of Object.entries(value)) {
|
|
54
|
+
const field = path ? `${path}.${key}` : key;
|
|
55
|
+
if (item && typeof item === 'object' && !Array.isArray(item)) recordSources(item, sources, field);
|
|
56
|
+
else sources[field] = 'schema-defaults';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function requireType(value, expected, path) {
|
|
61
|
+
if (typeof value !== expected) throw new ConfigurationError(path, `expected ${expected}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function validate(config) {
|
|
65
|
+
if (config.schema !== defaults.schema) throw new ConfigurationError('schema', 'unsupported schema');
|
|
66
|
+
for (const section of Object.keys(defaults).filter(key => key !== 'schema')) {
|
|
67
|
+
if (!config[section] || typeof config[section] !== 'object' || Array.isArray(config[section])) {
|
|
68
|
+
throw new ConfigurationError(section, 'expected object');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
for (const section of ['agent', 'legacyAgent', 'memory', 'mesh', 'webrtc', 'artifacts']) {
|
|
72
|
+
for (const [key, value] of Object.entries(defaults[section])) {
|
|
73
|
+
if (typeof value === 'number' && (!Number.isSafeInteger(config[section][key]) || config[section][key] < 1)) {
|
|
74
|
+
throw new ConfigurationError(`${section}.${key}`, 'expected positive safe integer');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (config.legacyAgent.settings !== null) {
|
|
79
|
+
const settings = config.legacyAgent.settings;
|
|
80
|
+
if (!settings || typeof settings !== 'object' || Array.isArray(settings)
|
|
81
|
+
|| Object.keys(settings).some(key => !['providerThrottle', 'cycleIntervalMs', 'functionGemma'].includes(key))
|
|
82
|
+
|| !Number.isSafeInteger(settings.cycleIntervalMs) || settings.cycleIntervalMs < 0) {
|
|
83
|
+
throw new ConfigurationError('legacyAgent.settings', 'invalid attempt settings');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
for (const key of ['enabled', 'executeJobs', 'supplyArtifacts', 'shareCandidates']) requireType(config.mesh[key], 'boolean', `mesh.${key}`);
|
|
87
|
+
requireType(config.tools.allowDynamic, 'boolean', 'tools.allowDynamic');
|
|
88
|
+
requireType(config.webrtc.dataChannelOptions?.ordered, 'boolean', 'webrtc.dataChannelOptions.ordered');
|
|
89
|
+
const retransmits = config.webrtc.dataChannelOptions?.maxRetransmits;
|
|
90
|
+
if (retransmits !== null && (!Number.isSafeInteger(retransmits) || retransmits < 0 || retransmits > 65535)) {
|
|
91
|
+
throw new ConfigurationError('webrtc.dataChannelOptions.maxRetransmits', 'expected uint16 or explicit null');
|
|
92
|
+
}
|
|
93
|
+
requireType(config.webrtc.dataChannelLabel, 'string', 'webrtc.dataChannelLabel');
|
|
94
|
+
if (!config.webrtc.dataChannelLabel) throw new ConfigurationError('webrtc.dataChannelLabel', 'must not be empty');
|
|
95
|
+
for (const [key, value] of Object.entries(config.webrtc.signaling)) {
|
|
96
|
+
if (!Number.isSafeInteger(value) || value < 1) throw new ConfigurationError(`webrtc.signaling.${key}`, 'expected positive safe integer');
|
|
97
|
+
}
|
|
98
|
+
if (Object.keys(config.webrtc.signaling).length !== Object.keys(defaults.webrtc.signaling).length) {
|
|
99
|
+
throw new ConfigurationError('webrtc.signaling', 'missing required policy');
|
|
100
|
+
}
|
|
101
|
+
requireType(config.improvement.enabled, 'boolean', 'improvement.enabled');
|
|
102
|
+
requireType(config.observation.enabled, 'boolean', 'observation.enabled');
|
|
103
|
+
for (const field of ['allowed', 'parallelSafe', 'ordered', 'exclusive']) {
|
|
104
|
+
if (!Array.isArray(config.tools[field]) || config.tools[field].some(value => typeof value !== 'string' || !value.trim())
|
|
105
|
+
|| new Set(config.tools[field]).size !== config.tools[field].length) {
|
|
106
|
+
throw new ConfigurationError(`tools.${field}`, 'expected unique non-empty string array');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
for (const [section, key] of [['tools','loaderId'],['memory','storeId'],['models','providerId'],
|
|
110
|
+
['mesh','roomId'],['webrtc','signalingUrl'],['webrtc','sessionId'],['webrtc','broadcastRoomId'],
|
|
111
|
+
['improvement','evaluatorId'],['improvement','approvalId'],['improvement','isolationId']]) {
|
|
112
|
+
const value = config[section][key];
|
|
113
|
+
if (value !== null && (typeof value !== 'string' || !value.trim())) {
|
|
114
|
+
throw new ConfigurationError(`${section}.${key}`, 'expected non-empty identifier or explicit null');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (config.models.contract !== null && (!config.models.contract || typeof config.models.contract !== 'object' || Array.isArray(config.models.contract))) {
|
|
118
|
+
throw new ConfigurationError('models.contract', 'expected exact model contract or null');
|
|
119
|
+
}
|
|
120
|
+
if (!routing.rules.some(rule => rule.id === config.mesh.routing)) throw new ConfigurationError('mesh.routing', 'unknown routing rule');
|
|
121
|
+
if (!['assignment/v1', 'swarm/v1'].includes(config.webrtc.protocol)) throw new ConfigurationError('webrtc.protocol', 'unknown wire protocol');
|
|
122
|
+
if (!Array.isArray(config.webrtc.transportOrder) || !config.webrtc.transportOrder.length
|
|
123
|
+
|| config.webrtc.transportOrder.some(type => !['webrtc', 'broadcast'].includes(type))) {
|
|
124
|
+
throw new ConfigurationError('webrtc.transportOrder', 'expected explicit transport order');
|
|
125
|
+
}
|
|
126
|
+
if (config.mesh.enabled && config.webrtc.transportOrder.includes('broadcast') && config.webrtc.broadcastRoomId === null) {
|
|
127
|
+
throw new ConfigurationError('webrtc.broadcastRoomId', 'BroadcastChannel requires its explicit protocol room');
|
|
128
|
+
}
|
|
129
|
+
const rtc = config.webrtc.rtcConfig;
|
|
130
|
+
if (!rtc || !Array.isArray(rtc.iceServers) || !['all', 'relay'].includes(rtc.iceTransportPolicy)) {
|
|
131
|
+
throw new ConfigurationError('webrtc.rtcConfig', 'ICE servers and transport policy are required');
|
|
132
|
+
}
|
|
133
|
+
for (const server of rtc.iceServers) {
|
|
134
|
+
if (!server || typeof server !== 'object' || Object.keys(server).some(key => key !== 'urls')) {
|
|
135
|
+
throw new ConfigurationError('webrtc.rtcConfig.iceServers', 'JSON carries URLs only; inject TURN credentials through RTC ports');
|
|
136
|
+
}
|
|
137
|
+
const urls = Array.isArray(server.urls) ? server.urls : [server.urls];
|
|
138
|
+
if (!urls.length || urls.some(url => typeof url !== 'string' || !/^(stun|stuns|turn|turns):/.test(url))) {
|
|
139
|
+
throw new ConfigurationError('webrtc.rtcConfig.iceServers.urls', 'invalid ICE URL');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (config.mesh.enabled && config.mesh.roomId === null) throw new ConfigurationError('mesh.roomId', 'enabled networking requires an explicit room');
|
|
143
|
+
if (config.improvement.enabled && ['evaluatorId', 'approvalId', 'isolationId'].some(key => config.improvement[key] === null)) {
|
|
144
|
+
throw new ConfigurationError('improvement', 'enabled improvement requires evaluator, approval and isolation ports');
|
|
145
|
+
}
|
|
146
|
+
for (const [section,key] of [['artifacts','root'],['memory','checkpointPrefix']]) {
|
|
147
|
+
const value = config[section][key];
|
|
148
|
+
if (typeof value !== 'string' || !value.startsWith('/') || value.split('/').includes('..')) {
|
|
149
|
+
throw new ConfigurationError(`${section}.${key}`, 'expected absolute logical path without traversal');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const secrets = new Set(['apiKey', 'accessToken', 'refreshToken', 'authorization', 'privateKey', 'privateJwk', 'password', 'credential']);
|
|
153
|
+
const rejectSecrets = (value, path) => {
|
|
154
|
+
if (!value || typeof value !== 'object') return;
|
|
155
|
+
for (const [key, item] of Object.entries(value)) {
|
|
156
|
+
if (secrets.has(key)) throw new ConfigurationError(`${path}.${key}`, 'credentials belong in host ports');
|
|
157
|
+
rejectSecrets(item, `${path}.${key}`);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
rejectSecrets(config, '$');
|
|
161
|
+
if (config.webrtc.signalingUrl !== null) {
|
|
162
|
+
let url;
|
|
163
|
+
try { url = new URL(config.webrtc.signalingUrl); } catch { throw new ConfigurationError('webrtc.signalingUrl', 'expected absolute WebSocket URL'); }
|
|
164
|
+
if (!['ws:', 'wss:'].includes(url.protocol) || url.username || url.password
|
|
165
|
+
|| [...url.searchParams.keys()].some(key => secrets.has(key) || key === 'token')) {
|
|
166
|
+
throw new ConfigurationError('webrtc.signalingUrl', 'use a credential-free WebSocket URL and host credential ports');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return config;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function resolveConfig({ chain = [], profile = null, overrides = {}, request = {} } = {}) {
|
|
173
|
+
if (!Array.isArray(chain)) throw new ConfigurationError('chain', 'expected array');
|
|
174
|
+
const value = snapshotJson(defaults);
|
|
175
|
+
const provenance = {};
|
|
176
|
+
recordSources(value, provenance);
|
|
177
|
+
chain.forEach((layer, index) => merge(value, snapshotJson(layer), `chain[${index}]`, provenance));
|
|
178
|
+
if (profile !== null) {
|
|
179
|
+
const selected = snapshotJson(profile);
|
|
180
|
+
if (selected.schema !== 'reploid.profile/v1' || typeof selected.id !== 'string' || !selected.config) {
|
|
181
|
+
throw new ConfigurationError('profile', 'expected versioned profile');
|
|
182
|
+
}
|
|
183
|
+
merge(value, selected.config, `profile:${selected.id}`, provenance);
|
|
184
|
+
}
|
|
185
|
+
merge(value, snapshotJson(overrides), 'application-overrides', provenance);
|
|
186
|
+
const requestLayer = snapshotJson(request);
|
|
187
|
+
const paths = {};
|
|
188
|
+
recordSources(requestLayer, paths);
|
|
189
|
+
for (const field of Object.keys(paths)) {
|
|
190
|
+
if (!requestPolicy.paths.includes(field)) throw new ConfigurationError(field, 'request override is not allowlisted');
|
|
191
|
+
}
|
|
192
|
+
merge(value, requestLayer, 'request-overrides', provenance);
|
|
193
|
+
validate(value);
|
|
194
|
+
// Canonical JSON is the full identity, not an unverified short checksum.
|
|
195
|
+
const identity = `reploid.config/v1:${JSON.stringify(snapshotJson(value))}`;
|
|
196
|
+
const resolved = freezeJson({ value, provenance, identity });
|
|
197
|
+
resolvedConfigurations.add(resolved);
|
|
198
|
+
return resolved;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function requireResolvedConfig(config) {
|
|
202
|
+
if (!resolvedConfigurations.has(config)) throw new ConfigurationError('$', 'call resolveConfig() before construction');
|
|
203
|
+
return config.value;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function hashConfiguration(config, cryptoApi = globalThis.crypto) {
|
|
207
|
+
requireResolvedConfig(config);
|
|
208
|
+
return sha256Hex(config.identity, cryptoApi);
|
|
209
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The library's one browser SHA-256 over Web Crypto. It lives at the library
|
|
3
|
+
* root, outside artifacts/, so modules any surface ships (including the Zero source
|
|
4
|
+
* image, which never exports artifacts paths) can import it.
|
|
5
|
+
*/
|
|
6
|
+
const textEncoder = new TextEncoder();
|
|
7
|
+
const bytesToHex = (bytes) => Array.from(bytes)
|
|
8
|
+
.map((byte) => byte.toString(16).padStart(2, '0'))
|
|
9
|
+
.join('');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The library's one SHA-256 over Web Crypto, as the raw ArrayBuffer digest: a
|
|
13
|
+
* Uint8Array is hashed as is, any other value as the UTF-8 bytes of
|
|
14
|
+
* String(value). Hosted services use their node:crypto twin, server/hashing.js.
|
|
15
|
+
*/
|
|
16
|
+
export async function sha256Bytes(value, cryptoApi = globalThis.crypto) {
|
|
17
|
+
const subtle = (cryptoApi || globalThis.crypto)?.subtle;
|
|
18
|
+
if (!subtle) throw new Error('WebCrypto unavailable');
|
|
19
|
+
return subtle.digest('SHA-256', value instanceof Uint8Array ? value : textEncoder.encode(String(value)));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Lowercase hex form of sha256Bytes, with no prefix. */
|
|
23
|
+
export async function sha256HexUnprefixed(value, cryptoApi = globalThis.crypto) {
|
|
24
|
+
return bytesToHex(new Uint8Array(await sha256Bytes(value, cryptoApi)));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** `sha256:`-prefixed form of sha256HexUnprefixed. */
|
|
28
|
+
export async function sha256Hex(value, cryptoApi = globalThis.crypto) {
|
|
29
|
+
return `sha256:${await sha256HexUnprefixed(value, cryptoApi)}`;
|
|
30
|
+
}
|