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,137 @@
|
|
|
1
|
+
import { hashJson } from '../artifacts/signatures.js';
|
|
2
|
+
|
|
3
|
+
export const TOOL_OFFER_MESSAGE = 'reploid:tool-offer';
|
|
4
|
+
export const TOOL_OFFER_ACK = 'reploid:tool-offer-ack';
|
|
5
|
+
const copy = value => JSON.parse(JSON.stringify(value));
|
|
6
|
+
const assert = (ok, message) => { if (!ok) throw new Error(message); };
|
|
7
|
+
const bytes = value => new TextEncoder().encode(value);
|
|
8
|
+
const digest = hashJson;
|
|
9
|
+
const hashPattern = /^sha256:[a-f0-9]{64}$/;
|
|
10
|
+
const identifier = value => typeof value === 'string' && /^[a-zA-Z0-9:._-]{1,180}$/.test(value);
|
|
11
|
+
|
|
12
|
+
/** Bounded, explicitly retried delivery. Acknowledgement means retained preview, never evaluation or adoption.
|
|
13
|
+
* The host supplies a durable store and serialization lock. The connection remains borrowed.
|
|
14
|
+
*/
|
|
15
|
+
export function createToolOfferChannel({ peerId, roomId, policy, ports }) {
|
|
16
|
+
policy = Object.freeze(copy(policy));
|
|
17
|
+
assert(identifier(peerId) && identifier(roomId), 'Explicit peer and room identities are required');
|
|
18
|
+
for (const key of ['maxBytes', 'maxRecords', 'maxPending', 'ttlMs', 'maxAttempts']) {
|
|
19
|
+
assert(Number.isSafeInteger(policy[key]) && policy[key] > 0, 'Invalid transfer limit: ' + key);
|
|
20
|
+
}
|
|
21
|
+
let closed = false, pending = 0;
|
|
22
|
+
const now = ports.now || Date.now;
|
|
23
|
+
const load = async () => (await ports.load()) || { schema: 'reploid.tool-transfers/v1', inbox: [], outbox: [] };
|
|
24
|
+
const commit = async state => {
|
|
25
|
+
assert(!closed, 'Candidate channel is closed');
|
|
26
|
+
await ports.save(copy(state)); ports.onChange?.(copy(state));
|
|
27
|
+
};
|
|
28
|
+
const transact = operation => {
|
|
29
|
+
assert(!closed, 'Candidate channel is closed');
|
|
30
|
+
assert(pending < policy.maxPending, 'Candidate channel is busy');
|
|
31
|
+
pending++;
|
|
32
|
+
return Promise.resolve().then(() => ports.lock(async () => {
|
|
33
|
+
assert(!closed, 'Candidate channel is closed'); return operation(await load());
|
|
34
|
+
})).finally(() => pending--);
|
|
35
|
+
};
|
|
36
|
+
const validate = async (remote, envelope) => {
|
|
37
|
+
assert(envelope && Object.keys(envelope).sort().join(',') === 'bytes,createdAt,expiresAt,offerHash,recipient,roomId,schema,sender,targetContract,targetId,text,transferId', 'Malformed transfer');
|
|
38
|
+
assert(envelope.schema === 'reploid.tool-transfer/v1' && envelope.roomId === roomId
|
|
39
|
+
&& envelope.sender === remote && envelope.recipient === peerId && remote !== peerId
|
|
40
|
+
&& identifier(remote) && identifier(envelope.transferId) && identifier(envelope.targetId), 'Transfer identity mismatch');
|
|
41
|
+
assert(Number.isSafeInteger(envelope.createdAt) && Number.isSafeInteger(envelope.expiresAt)
|
|
42
|
+
&& envelope.createdAt <= now() && envelope.expiresAt > now()
|
|
43
|
+
&& envelope.expiresAt - envelope.createdAt > 0 && envelope.expiresAt - envelope.createdAt <= policy.ttlMs, 'Transfer expired or invalid lifetime');
|
|
44
|
+
assert(typeof envelope.text === 'string' && envelope.bytes === bytes(envelope.text).byteLength
|
|
45
|
+
&& envelope.bytes <= policy.maxBytes && hashPattern.test(envelope.targetContract), 'Transfer size or contract is invalid');
|
|
46
|
+
assert(envelope.offerHash === await digest(envelope.text), 'Transfer content hash does not match');
|
|
47
|
+
return digest(envelope);
|
|
48
|
+
};
|
|
49
|
+
const acknowledge = async record => {
|
|
50
|
+
assert(!closed, 'Candidate channel is closed');
|
|
51
|
+
return ports.send(record.sender, TOOL_OFFER_ACK, {
|
|
52
|
+
schema: 'reploid.tool-transfer-ack/v1', roomId, sender: peerId, recipient: record.sender,
|
|
53
|
+
transferId: record.transferId, envelopeHash: record.envelopeHash, status: record.status,
|
|
54
|
+
reason: record.reason || ''
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
const dispatch = async (state, record) => {
|
|
58
|
+
assert(record.envelope.sender === peerId && record.envelope.expiresAt > now(), 'Transfer expired or belongs to another sender');
|
|
59
|
+
assert(record.attempts < policy.maxAttempts, 'Transfer retry limit reached');
|
|
60
|
+
assert(await ports.authorize({ action: 'offer.send', envelope: copy(record.envelope) }) === true, 'Host denied candidate sending');
|
|
61
|
+
record.attempts++; record.status = 'sending'; record.error = null;
|
|
62
|
+
await commit(state);
|
|
63
|
+
assert(!closed, 'Candidate channel is closed');
|
|
64
|
+
try { assert(await ports.send(record.envelope.recipient, TOOL_OFFER_MESSAGE, copy(record.envelope)) === true, 'Peer connection is unavailable'); }
|
|
65
|
+
catch (error) { record.status = 'interrupted'; record.error = String(error.message || error); await commit(state); }
|
|
66
|
+
return copy(record);
|
|
67
|
+
};
|
|
68
|
+
return Object.freeze({
|
|
69
|
+
async list() { return copy(await load()); },
|
|
70
|
+
async send({ recipient, targetId, targetContract, text }) {
|
|
71
|
+
assert(identifier(recipient) && recipient !== peerId && identifier(targetId) && hashPattern.test(targetContract), 'Invalid candidate destination or contract');
|
|
72
|
+
assert(typeof text === 'string' && bytes(text).byteLength <= policy.maxBytes, 'Candidate exceeds peer transfer allowance');
|
|
73
|
+
const createdAt = now();
|
|
74
|
+
const envelope = { schema: 'reploid.tool-transfer/v1', transferId: crypto.randomUUID(), sender: peerId, recipient, roomId,
|
|
75
|
+
createdAt, expiresAt: createdAt + policy.ttlMs, bytes: bytes(text).byteLength, targetId, targetContract, text, offerHash: await digest(text) };
|
|
76
|
+
const envelopeHash = await digest(envelope);
|
|
77
|
+
return transact(async state => {
|
|
78
|
+
assert(state.inbox.length + state.outbox.length < policy.maxRecords, 'Candidate transfer history is full');
|
|
79
|
+
const record = { envelope, envelopeHash, attempts: 0, status: 'sending', error: null };
|
|
80
|
+
state.outbox.push(record); return dispatch(state, record);
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
retry(transferId) {
|
|
84
|
+
return transact(async state => {
|
|
85
|
+
const record = state.outbox.find(item => item.envelope.transferId === transferId);
|
|
86
|
+
assert(record && ['sending', 'interrupted'].includes(record.status), 'Transfer cannot be retried');
|
|
87
|
+
return dispatch(state, record);
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
async receive(remote, type, payload) {
|
|
91
|
+
// Snapshot untrusted data before any await. Do not let transport callbacks mutate an in-flight validation.
|
|
92
|
+
const message = copy(payload);
|
|
93
|
+
return transact(async state => {
|
|
94
|
+
if (type === TOOL_OFFER_ACK) {
|
|
95
|
+
const record = state.outbox.find(item => item.envelope.transferId === message.transferId);
|
|
96
|
+
assert(record && message.schema === 'reploid.tool-transfer-ack/v1' && message.roomId === roomId
|
|
97
|
+
&& remote === record.envelope.recipient && message.sender === remote && message.recipient === peerId
|
|
98
|
+
&& message.envelopeHash === record.envelopeHash && ['received', 'refused'].includes(message.status)
|
|
99
|
+
&& typeof message.reason === 'string' && message.reason.length <= 256, 'Invalid candidate acknowledgement');
|
|
100
|
+
if (['received', 'refused'].includes(record.status)) {
|
|
101
|
+
assert(record.status === message.status, 'Conflicting candidate acknowledgement'); return;
|
|
102
|
+
}
|
|
103
|
+
record.status = message.status; record.reason = message.reason; record.acknowledgedAt = now();
|
|
104
|
+
await commit(state); return;
|
|
105
|
+
}
|
|
106
|
+
assert(type === TOOL_OFFER_MESSAGE, 'Unsupported candidate message');
|
|
107
|
+
const envelopeHash = await validate(remote, message);
|
|
108
|
+
const existing = state.inbox.find(item => item.sender === remote && item.transferId === message.transferId);
|
|
109
|
+
if (existing) {
|
|
110
|
+
assert(existing.envelopeHash === envelopeHash, 'Conflicting candidate redelivery');
|
|
111
|
+
await acknowledge(existing); return;
|
|
112
|
+
}
|
|
113
|
+
const record = { transferId: message.transferId, sender: remote, recipient: peerId, roomId, envelopeHash,
|
|
114
|
+
receivedAt: now(), status: 'refused', reason: '', envelope: null };
|
|
115
|
+
if (state.inbox.length + state.outbox.length >= policy.maxRecords) {
|
|
116
|
+
record.reason = 'Candidate transfer history is full'; await acknowledge(record); return;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
assert(await ports.authorize({ action: 'offer.receive', envelope: copy(message) }) === true, 'Recipient is not accepting candidates');
|
|
120
|
+
await ports.validateOffer(copy(message));
|
|
121
|
+
assert(await ports.authorize({ action: 'offer.receive', envelope: copy(message) }) === true, 'Recipient is not accepting candidates');
|
|
122
|
+
record.status = 'received'; record.envelope = message;
|
|
123
|
+
} catch (error) { record.reason = String(error.message || error).slice(0, 256); }
|
|
124
|
+
state.inbox.push(record); await commit(state);
|
|
125
|
+
await acknowledge(record);
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
dismiss(transferId) {
|
|
129
|
+
return transact(async state => {
|
|
130
|
+
const record = state.inbox.find(item => item.transferId === transferId);
|
|
131
|
+
assert(record && record.status === 'received', 'Received candidate not found');
|
|
132
|
+
record.dismissed = true; await commit(state);
|
|
133
|
+
});
|
|
134
|
+
},
|
|
135
|
+
close() { closed = true; }
|
|
136
|
+
});
|
|
137
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source roots of the served browser tree. Each root holds files at their served
|
|
3
|
+
* paths, so a served path such as /config/chat-models.json resolves to the one root
|
|
4
|
+
* that contains it. Dependency-free: native delivery and the hosted backend use it.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
export const REPOSITORY_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
11
|
+
/** Agent substrate, code both surfaces load, and the Poolday product. */
|
|
12
|
+
export const BROWSER_ROOTS = Object.freeze(['self', 'shared', 'apps/poolday']);
|
|
13
|
+
|
|
14
|
+
const toPosix = value => String(value).split(path.sep).join('/');
|
|
15
|
+
|
|
16
|
+
/** The root that contains a repository-relative path, or null. */
|
|
17
|
+
export function rootOf(sourcePath, roots = BROWSER_ROOTS) {
|
|
18
|
+
const normalized = toPosix(sourcePath).replace(/^\.\//, '');
|
|
19
|
+
return [...roots].sort((a, b) => b.length - a.length)
|
|
20
|
+
.find(root => normalized.startsWith(root + '/')) || null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The served path of a repository-relative source path, or null outside the roots. */
|
|
24
|
+
export function servedPathOf(sourcePath, roots = BROWSER_ROOTS) {
|
|
25
|
+
const normalized = toPosix(sourcePath).replace(/^\.\//, '');
|
|
26
|
+
const root = rootOf(normalized, roots);
|
|
27
|
+
return root ? normalized.slice(root.length + 1) : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Absolute path of the source that serves a path or origin-relative URL. A path no
|
|
32
|
+
* root holds resolves inside the first root, so reading it fails as a missing file.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveServedFile(servedPath, { repoRoot = REPOSITORY_ROOT, roots = BROWSER_ROOTS } = {}) {
|
|
35
|
+
const served = String(servedPath).replace(/[?#].*$/, '').replace(/^\/+/, '');
|
|
36
|
+
if (!served || served.split('/').some(part => part === '..' || part === '.')) throw new Error(`Invalid served path: ${servedPath}`);
|
|
37
|
+
for (const root of roots) {
|
|
38
|
+
const candidate = path.join(repoRoot, root, served);
|
|
39
|
+
if (existsSync(candidate)) return candidate;
|
|
40
|
+
}
|
|
41
|
+
return path.join(repoRoot, roots[0], served);
|
|
42
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** Operator-owned unsigned-in search profile. Only Google receives browser cookies;
|
|
2
|
+
* result documents use the existing public, DNS-guarded, cookie-free reader. */
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { createPublicWebWorker } from './public-web-worker.js';
|
|
6
|
+
|
|
7
|
+
const GOOGLE = 'https://www.google.com';
|
|
8
|
+
const MARKER = '.reploid-search-profile';
|
|
9
|
+
|
|
10
|
+
export async function openBrowserWebWorker({ profileDirectory, launch, pageFetch } = {}) {
|
|
11
|
+
if (!path.isAbsolute(profileDirectory || '')) throw new Error('Browser search needs an absolute dedicated profile directory');
|
|
12
|
+
await fs.mkdir(profileDirectory, { recursive: true, mode: 0o700 });
|
|
13
|
+
const entries = await fs.readdir(profileDirectory);
|
|
14
|
+
if (entries.length && !entries.includes(MARKER)) throw new Error('Refusing a browser profile not created for Reploid search');
|
|
15
|
+
await fs.writeFile(path.join(profileDirectory, MARKER), 'Dedicated Reploid public search profile; do not sign in.\n', { mode: 0o600 });
|
|
16
|
+
if (!launch) {
|
|
17
|
+
const { chromium } = await import('playwright').catch(() => { throw new Error('Browser search requires Playwright and Chrome; see docs/native-participant.md'); });
|
|
18
|
+
launch = chromium.launchPersistentContext.bind(chromium);
|
|
19
|
+
}
|
|
20
|
+
const context = await launch(profileDirectory, { channel: 'chrome', headless: false, serviceWorkers: 'block' });
|
|
21
|
+
const worker = createBrowserWebWorker({ context, pageFetch });
|
|
22
|
+
return Object.freeze({ ...worker, async close() { await worker.close(); await context.close(); } });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createBrowserWebWorker({ context, pageFetch }) {
|
|
26
|
+
let closed = false, active = null, verificationPage = null;
|
|
27
|
+
context.on?.('close', () => { closed = true; active?.controller.abort(new Error('Contributor browser closed')); });
|
|
28
|
+
const searchResults = async ({ query, maxResults, signal }) => {
|
|
29
|
+
signal?.throwIfAborted();
|
|
30
|
+
if (closed) throw new Error('Browser search is closed');
|
|
31
|
+
if (active) throw new Error('Browser search is busy');
|
|
32
|
+
const controller = new AbortController();
|
|
33
|
+
const forward = () => controller.abort(signal.reason);
|
|
34
|
+
signal?.addEventListener('abort', forward, { once: true });
|
|
35
|
+
const job = { controller, settled: null }; active = job;
|
|
36
|
+
job.settled = (async () => {
|
|
37
|
+
const page = await context.newPage();
|
|
38
|
+
const stop = () => { void page.close().catch(() => {}); };
|
|
39
|
+
controller.signal.addEventListener('abort', stop, { once: true });
|
|
40
|
+
try {
|
|
41
|
+
controller.signal.throwIfAborted();
|
|
42
|
+
// Search never follows result links in the browser. Block unrelated subresources.
|
|
43
|
+
await page.route('**/*', route => {
|
|
44
|
+
const url = new URL(route.request().url());
|
|
45
|
+
const allowed = url.protocol === 'https:' && /(^|\.)(google\.com|gstatic\.com|googleusercontent\.com)$/.test(url.hostname);
|
|
46
|
+
return allowed ? route.continue() : route.abort();
|
|
47
|
+
});
|
|
48
|
+
const url = new URL('/search', GOOGLE); url.searchParams.set('q', query);
|
|
49
|
+
await page.goto(url.href, { waitUntil: 'domcontentloaded', timeout: 8000 });
|
|
50
|
+
if (new URL(page.url()).pathname.startsWith('/sorry')) {
|
|
51
|
+
const operatorPage = context.pages().find(tab => tab !== page);
|
|
52
|
+
verificationPage = operatorPage;
|
|
53
|
+
await operatorPage?.goto(page.url(), { waitUntil: 'domcontentloaded', timeout: 4000 }).catch(() => {});
|
|
54
|
+
throw new Error('Google needs human verification in the contributor browser');
|
|
55
|
+
}
|
|
56
|
+
await page.locator('a:has(h3)').first().waitFor({ state: 'visible', timeout: 4000 });
|
|
57
|
+
const rows = await page.locator('a:has(h3)').evaluateAll((anchors, limit) => anchors.filter(a => a.getClientRects().length).slice(0, limit).map(a => ({
|
|
58
|
+
url: a.href, title: a.querySelector('h3').innerText,
|
|
59
|
+
excerpt: (a.closest('div.MjjYud')?.innerText || a.innerText).slice(0, 1500)
|
|
60
|
+
})), maxResults);
|
|
61
|
+
const settled = await Promise.allSettled(rows.map(async row => {
|
|
62
|
+
let target = new URL(row.url);
|
|
63
|
+
if (target.origin === GOOGLE && ['/goto', '/url'].includes(target.pathname)) {
|
|
64
|
+
// Resolve only the search engine hop with its own session; never forward cookies to a result.
|
|
65
|
+
const response = await context.request.get(target.href, { maxRedirects: 0, timeout: 4000 });
|
|
66
|
+
try {
|
|
67
|
+
if (![301, 302, 303, 307, 308].includes(response.status()) || !response.headers().location) return null;
|
|
68
|
+
target = new URL(response.headers().location, target);
|
|
69
|
+
} finally { await response.dispose(); }
|
|
70
|
+
}
|
|
71
|
+
if (!['https:', 'http:'].includes(target.protocol) || target.username || target.password || /(^|\.)google\.com$/.test(target.hostname)) return null;
|
|
72
|
+
return { ...row, url: target.href };
|
|
73
|
+
}));
|
|
74
|
+
controller.signal.throwIfAborted();
|
|
75
|
+
const failed = settled.find(result => result.status === 'rejected');
|
|
76
|
+
if (failed) throw failed.reason;
|
|
77
|
+
const results = settled.map(result => result.value).filter(Boolean);
|
|
78
|
+
if (!results.length) throw new Error('Browser search returned no usable public results');
|
|
79
|
+
return results;
|
|
80
|
+
} finally {
|
|
81
|
+
controller.signal.removeEventListener('abort', stop);
|
|
82
|
+
await page.close().catch(() => {});
|
|
83
|
+
}
|
|
84
|
+
})();
|
|
85
|
+
try { return await job.settled; }
|
|
86
|
+
finally { signal?.removeEventListener('abort', forward); if (active === job) active = null; }
|
|
87
|
+
};
|
|
88
|
+
const worker = createPublicWebWorker({ service: 'google-browser', searchResults, pageFetch });
|
|
89
|
+
return Object.freeze({ ...worker,
|
|
90
|
+
isAvailable: () => !closed && (!verificationPage || verificationPage.isClosed() || !new URL(verificationPage.url()).pathname.startsWith('/sorry')),
|
|
91
|
+
extract(args, owner) { if (closed) return Promise.reject(new Error('Browser search is closed')); return worker.extract(args, owner); },
|
|
92
|
+
async close() { closed = true; active?.controller.abort(new Error('Browser search closed')); await active?.settled.catch(() => {}); }
|
|
93
|
+
});
|
|
94
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Text Chat Completions protocol over the existing authenticated HTTP requester. */
|
|
2
|
+
import policy from '../apps/poolday/config/partition-policy.json' with { type: 'json' };
|
|
3
|
+
import { createTextRequester, fail, object, freeze } from './text-requester.js';
|
|
4
|
+
|
|
5
|
+
export function parseTextChatCompletion(body) {
|
|
6
|
+
if (!object(body)) throw fail(400, 'A JSON object is required');
|
|
7
|
+
for (const key of Object.keys(body)) if (!['model', 'messages', 'stream'].includes(key)) throw fail(400, `Unsupported parameter: ${key}`, key);
|
|
8
|
+
if (typeof body.model !== 'string' || !body.model.trim() || body.model.length > 256) throw fail(400, 'A bounded model id is required', 'model');
|
|
9
|
+
if (body.stream !== undefined && typeof body.stream !== 'boolean') throw fail(400, 'stream must be boolean', 'stream');
|
|
10
|
+
if (!Array.isArray(body.messages) || !body.messages.length || body.messages.length > 128) throw fail(400, 'Require 1–128 text messages', 'messages');
|
|
11
|
+
let characters = 0;
|
|
12
|
+
const messages = body.messages.map((message, index) => {
|
|
13
|
+
const param = `messages[${index}]`;
|
|
14
|
+
if (!object(message) || Object.keys(message).some(key => !['role', 'content'].includes(key))
|
|
15
|
+
|| !['system', 'user', 'assistant'].includes(message.role) || typeof message.content !== 'string') {
|
|
16
|
+
throw fail(400, 'Only system/user/assistant string-content messages are supported', param);
|
|
17
|
+
}
|
|
18
|
+
characters += message.content.length;
|
|
19
|
+
if (characters > policy.inputLimits.maxInputCharacters) throw fail(413, 'Input text limit exceeded', 'messages');
|
|
20
|
+
return { role: message.role, content: message.content };
|
|
21
|
+
});
|
|
22
|
+
return freeze({ model: body.model, messages, stream: body.stream === true });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
export function createChatCompletions(options) {
|
|
27
|
+
return createTextRequester(options, { path: '/v1/chat/completions', idPrefix: 'chatcmpl-', parse: parseTextChatCompletion,
|
|
28
|
+
output({ id, created, model, input, write, end, json }) {
|
|
29
|
+
let started = false;
|
|
30
|
+
const chunk = (delta, finishReason = null, usage = undefined) => write(`data: ${JSON.stringify({ id, object: 'chat.completion.chunk', created, model, ...(usage ? { usage } : {}),
|
|
31
|
+
choices: [{ index: 0, delta, logprobs: null, finish_reason: finishReason }] })}\n\n`);
|
|
32
|
+
return {
|
|
33
|
+
delta(text) { chunk({ ...(started ? {} : { role: 'assistant' }), content: text }); started = true; },
|
|
34
|
+
complete(content, finishReason, _reason, measured) {
|
|
35
|
+
const usage = measured ? { prompt_tokens: measured.inputTokens, completion_tokens: measured.outputTokens,
|
|
36
|
+
total_tokens: measured.inputTokens + measured.outputTokens } : undefined;
|
|
37
|
+
if (input.stream) { chunk({}, finishReason, usage); end('data: [DONE]\n\n'); }
|
|
38
|
+
else json({ id, object: 'chat.completion', created, model, ...(usage ? { usage } : {}),
|
|
39
|
+
choices: [{ index: 0, message: { role: 'assistant', content, refusal: null }, logprobs: null, finish_reason: finishReason }] });
|
|
40
|
+
},
|
|
41
|
+
error(error) { end(`data: ${JSON.stringify({ error })}\n\n`); }
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import https from 'node:https';
|
|
3
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
4
|
+
|
|
5
|
+
const assert = (ok, message) => { if (!ok) throw Error(message); };
|
|
6
|
+
|
|
7
|
+
function normalizeBasePath(basePath = '') {
|
|
8
|
+
const clean = String(basePath || '').replace(/\/+$/, '');
|
|
9
|
+
assert(clean === '' || (/^\/[A-Za-z0-9/_-]+$/.test(clean) && !clean.includes('//')
|
|
10
|
+
&& !clean.split('/').some(part => part === '.' || part === '..')), 'Clean browser bridge path prefix required');
|
|
11
|
+
return clean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Explicitly paired access to an owner-operated native requester over loopback or TLS/bound-tunnel HTTPS.
|
|
15
|
+
* The pairing secret authorizes delegation to this host's enrolled private pool;
|
|
16
|
+
* it never exposes the signing identity, room credential, file paths or raw config. */
|
|
17
|
+
export function createFleetBrowserBridgeHandler({ host, token, origins, basePath = '', maxBodyBytes = 262144, maxConcurrent = 8, maxSeenAttempts = 10000 }) {
|
|
18
|
+
assert(typeof token === 'string' && token.length >= 32, 'Strong pairing secret required');
|
|
19
|
+
assert(Array.isArray(origins) && origins.length && origins.every(origin => new URL(origin).origin === origin), 'Exact allowed browser origins required');
|
|
20
|
+
assert(Number.isSafeInteger(maxSeenAttempts) && maxSeenAttempts >= maxConcurrent, 'Positive replay window required');
|
|
21
|
+
const prefix = normalizeBasePath(basePath);
|
|
22
|
+
const pending = new Map(), seen = new Set();
|
|
23
|
+
const authenticate = request => {
|
|
24
|
+
const received = Buffer.from(request.headers.authorization || ''), expected = Buffer.from(`Bearer ${token}`);
|
|
25
|
+
return received.length === expected.length && timingSafeEqual(received, expected);
|
|
26
|
+
};
|
|
27
|
+
const handle = async (request, response) => {
|
|
28
|
+
let attemptId = null;
|
|
29
|
+
try {
|
|
30
|
+
assert(origins.includes(request.headers.origin), 'Browser origin is not paired');
|
|
31
|
+
response.setHeader('Access-Control-Allow-Origin', request.headers.origin);
|
|
32
|
+
response.setHeader('Vary', 'Origin');
|
|
33
|
+
response.setHeader('Cache-Control', 'no-store');
|
|
34
|
+
response.setHeader('X-Content-Type-Options', 'nosniff');
|
|
35
|
+
response.setHeader('Access-Control-Allow-Headers', 'authorization, content-type');
|
|
36
|
+
response.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
|
37
|
+
response.setHeader('Access-Control-Allow-Private-Network', 'true');
|
|
38
|
+
if (request.method === 'OPTIONS') { response.writeHead(204); response.end(); return; }
|
|
39
|
+
assert(request.method === 'POST' && authenticate(request), 'Pair this browser with the native host first');
|
|
40
|
+
const pathname = new URL(request.url || '/', 'http://127.0.0.1').pathname;
|
|
41
|
+
assert(!prefix || pathname.startsWith(prefix + '/'), 'Unknown browser bridge operation');
|
|
42
|
+
const routePath = prefix ? pathname.slice(prefix.length) : pathname;
|
|
43
|
+
let length = 0; const chunks = [];
|
|
44
|
+
for await (const chunk of request) { length += chunk.length; assert(length <= maxBodyBytes, 'Request exceeds browser bridge limit'); chunks.push(chunk); }
|
|
45
|
+
const body = JSON.parse(Buffer.concat(chunks));
|
|
46
|
+
if (routePath === '/state') {
|
|
47
|
+
response.setHeader('Content-Type', 'application/json'); response.end(JSON.stringify(await host.browserState())); return;
|
|
48
|
+
}
|
|
49
|
+
if (routePath === '/receipt') {
|
|
50
|
+
assert(typeof body.id === 'string' && body.id.length < 512, 'Receipt identity required');
|
|
51
|
+
response.setHeader('Content-Type', 'application/json'); response.end(JSON.stringify(await host.receipt(body.id))); return;
|
|
52
|
+
}
|
|
53
|
+
if (routePath === '/accounts') {
|
|
54
|
+
response.setHeader('Content-Type', 'application/json'); response.end(JSON.stringify(await host.balances())); return;
|
|
55
|
+
}
|
|
56
|
+
if (routePath === '/cancel') {
|
|
57
|
+
assert(pending.has(body.attemptId), 'No active bridge request');
|
|
58
|
+
host.cancel(body.attemptId); response.setHeader('Content-Type', 'application/json'); response.end(JSON.stringify({ status: 'stopping' })); return;
|
|
59
|
+
}
|
|
60
|
+
if (routePath === '/recover') {
|
|
61
|
+
assert(typeof host.recover === 'function', 'Recovery is not configured on this host');
|
|
62
|
+
assert(typeof body.reservationId === 'string' && body.reservationId.length > 0 && body.reservationId.length < 512
|
|
63
|
+
&& Array.isArray(body.messages) && body.messages.length > 0
|
|
64
|
+
&& body.messages.every(row => ['user', 'assistant', 'system'].includes(row.role) && typeof row.content === 'string'),
|
|
65
|
+
'Bound reservation and original messages required');
|
|
66
|
+
response.setHeader('Content-Type', 'application/json');
|
|
67
|
+
response.end(JSON.stringify(await host.recover({
|
|
68
|
+
reservationId: body.reservationId,
|
|
69
|
+
messages: body.messages,
|
|
70
|
+
...(typeof body.providerId === 'string' && body.providerId.length <= 256 ? { providerId: body.providerId } : {})
|
|
71
|
+
})));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
assert(routePath === '/generate', 'Unknown browser bridge operation');
|
|
75
|
+
assert(typeof body.attemptId === 'string' && body.attemptId.length <= 128 && typeof body.threadId === 'string'
|
|
76
|
+
&& body.threadId.length <= 128 && Array.isArray(body.messages) && body.messages.length > 0
|
|
77
|
+
&& body.messages.every(row => ['user', 'assistant', 'system'].includes(row.role) && typeof row.content === 'string'), 'Invalid conversation request');
|
|
78
|
+
assert(pending.size < maxConcurrent && !seen.has(body.attemptId), 'Request already used or bridge limit reached');
|
|
79
|
+
while (seen.size >= maxSeenAttempts) {
|
|
80
|
+
const oldest = [...seen].find(id => !pending.has(id));
|
|
81
|
+
assert(oldest !== undefined, 'Request already used or bridge limit reached');
|
|
82
|
+
seen.delete(oldest);
|
|
83
|
+
}
|
|
84
|
+
attemptId = body.attemptId; seen.add(attemptId); pending.set(attemptId, Promise.resolve());
|
|
85
|
+
const state = await host.browserState();
|
|
86
|
+
const model = state.models.find(row => row.id === body.model?.id && row.identity === body.model?.identity);
|
|
87
|
+
assert(model && ['ready', 'busy'].includes(model.availability), 'No private computer is ready for this model');
|
|
88
|
+
assert(body.poolIdentity === state.poolIdentity, 'Private computer enrollment changed; reconnect before sending');
|
|
89
|
+
// Never accept a browser-supplied route, source, adapter, generation policy or provider.
|
|
90
|
+
response.writeHead(200, { 'Content-Type': 'application/x-ndjson; charset=utf-8', 'X-Accel-Buffering': 'no' });
|
|
91
|
+
const emit = event => { if (!response.destroyed) response.write(JSON.stringify(event) + '\n'); };
|
|
92
|
+
response.on('close', () => { if (pending.has(attemptId)) host.cancel(attemptId); });
|
|
93
|
+
let lastAdmission = null;
|
|
94
|
+
const operation = host.generate(
|
|
95
|
+
{ threadId: body.threadId, attemptId, model: { id: model.id, identity: model.identity }, messages: body.messages },
|
|
96
|
+
delta => emit({ delta }),
|
|
97
|
+
disclosure => {
|
|
98
|
+
const route = disclosure.route;
|
|
99
|
+
emit({ status: 'executing', execution: { provider: 'peer', placement: route ? 'ordered-layer-partition' : 'peer-whole-request',
|
|
100
|
+
...(route ? { identity: { schema: 'reploid.partition-attempt/v2', route, requesterId: state.requester.participantId, threadId: body.threadId, attemptId } }
|
|
101
|
+
: { peerId: disclosure.recipientIdentity }),
|
|
102
|
+
...(lastAdmission ? { admission: lastAdmission } : {}),
|
|
103
|
+
serviceCredit: { status: 'pending', id: `service:${state.requester.participantId}:${route ? attemptId : disclosure.requestId}` } } });
|
|
104
|
+
},
|
|
105
|
+
admission => {
|
|
106
|
+
lastAdmission = admission;
|
|
107
|
+
if (admission?.status === 'queued') emit({ status: 'queued', execution: { admission } });
|
|
108
|
+
}
|
|
109
|
+
);
|
|
110
|
+
pending.set(attemptId, operation);
|
|
111
|
+
try { emit({ result: await operation }); }
|
|
112
|
+
catch (error) { emit({ error: error.message, serviceCredit: error.serviceCredit || null, ...(error.disconnect ? { disconnect: error.disconnect } : {}) }); }
|
|
113
|
+
finally { pending.delete(attemptId); response.end(); }
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (attemptId) pending.delete(attemptId);
|
|
116
|
+
if (!response.headersSent) response.writeHead(400, { 'Content-Type': 'application/json' });
|
|
117
|
+
response.end(JSON.stringify({ error: error.message }));
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
return {
|
|
121
|
+
prefix,
|
|
122
|
+
handle,
|
|
123
|
+
async close() {
|
|
124
|
+
for (const id of pending.keys()) host.cancel(id);
|
|
125
|
+
await Promise.allSettled([...pending.values()]);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function startFleetBrowserBridge({ host, token, origins, listenHost = '127.0.0.1', port = 0, tls = null, basePath = '', maxBodyBytes = 262144, maxConcurrent = 8, maxSeenAttempts = 10000 }) {
|
|
131
|
+
const loopback = ['127.0.0.1', '::1', 'localhost'].includes(listenHost);
|
|
132
|
+
assert(loopback || Boolean(tls?.key && tls?.cert), 'Non-loopback browser bridge requires TLS');
|
|
133
|
+
const bridge = createFleetBrowserBridgeHandler({ host, token, origins, basePath, maxBodyBytes, maxConcurrent, maxSeenAttempts });
|
|
134
|
+
const server = tls ? https.createServer(tls, bridge.handle) : http.createServer(bridge.handle);
|
|
135
|
+
server.requestTimeout = 30000;
|
|
136
|
+
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(port, listenHost, resolve); });
|
|
137
|
+
const hostText = listenHost.includes(':') ? `[${listenHost}]` : listenHost;
|
|
138
|
+
return {
|
|
139
|
+
url: `${tls ? 'https' : 'http'}://${hostText}:${server.address().port}${bridge.prefix}`,
|
|
140
|
+
async close() {
|
|
141
|
+
await bridge.close();
|
|
142
|
+
await new Promise(resolve => { server.close(resolve); server.closeAllConnections(); });
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { canonicalize } from '../../packages/reploid/src/artifacts/canonical-json.js';
|
|
2
|
+
import { participantIdFor, normalizeParticipantPublicKey } from '../../packages/reploid/src/artifacts/participant-proofs.js';
|
|
3
|
+
|
|
4
|
+
export const assert = (condition, message) => { if (!condition) throw Error(message); };
|
|
5
|
+
export const integer = value => Number.isSafeInteger(value) && value >= 0;
|
|
6
|
+
export const digest = value => typeof value === 'string' && /^sha256:[a-f0-9]{64}$/.test(value);
|
|
7
|
+
export const text = value => typeof value === 'string' && value.length > 0 && value.length <= 256;
|
|
8
|
+
export const same = (a, b) => canonicalize(a) === canonicalize(b);
|
|
9
|
+
|
|
10
|
+
/** Enrollment comes from the operator, never from a remote advertisement. */
|
|
11
|
+
export async function enrollComputers(computers) {
|
|
12
|
+
assert(Array.isArray(computers) && computers.length > 0 && computers.length <= 256, 'Explicit fleet enrollment required');
|
|
13
|
+
const byPeer = new Map(), byId = new Map();
|
|
14
|
+
for (const source of computers) {
|
|
15
|
+
const computer = structuredClone(source);
|
|
16
|
+
assert(text(computer.id) && text(computer.name) && text(computer.ownerId) && !byId.has(computer.id)
|
|
17
|
+
&& Array.isArray(computer.providers) && computer.providers.length > 0, 'Invalid computer enrollment');
|
|
18
|
+
byId.set(computer.id, computer);
|
|
19
|
+
for (const provider of computer.providers) {
|
|
20
|
+
const publicJwk = normalizeParticipantPublicKey(provider.publicJwk);
|
|
21
|
+
assert(publicJwk && !Object.hasOwn(provider.publicJwk, 'd'), 'Only public enrollment keys are admitted');
|
|
22
|
+
const participantId = await participantIdFor(publicJwk);
|
|
23
|
+
assert(provider.participantId === participantId && !byPeer.has(participantId), 'Provider key must belong to exactly one enrolled computer');
|
|
24
|
+
byPeer.set(participantId, { computerId: computer.id, ownerId: computer.ownerId, publicJwk });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return { byId, byPeer };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function creditPolicy(source) {
|
|
31
|
+
const policy = structuredClone(source);
|
|
32
|
+
assert(policy?.schema === 'reploid.service-credit-policy/v1' && policy.unit === 'noncash-service-credit'
|
|
33
|
+
&& text(policy.version) && integer(policy.creditsPerOutputToken) && policy.creditsPerOutputToken > 0
|
|
34
|
+
&& integer(policy.maxDebt) && integer(policy.maxOutputTokens) && policy.maxOutputTokens > 0
|
|
35
|
+
&& integer(policy.maxPendingPerComputer) && policy.maxPendingPerComputer > 0
|
|
36
|
+
&& integer(policy.maxClockSkewMs) && integer(policy.maxRequestMs) && policy.maxRequestMs > 0
|
|
37
|
+
&& typeof policy.allowSameOwner === 'boolean'
|
|
38
|
+
&& (policy.wholeModelTerminalCharge === undefined || policy.wholeModelTerminalCharge === 0),
|
|
39
|
+
'Explicit non-cash credit policy required');
|
|
40
|
+
assert(Number.isSafeInteger(policy.maxOutputTokens * policy.creditsPerOutputToken + policy.maxDebt), 'Credit policy exceeds integer precision');
|
|
41
|
+
return Object.freeze(policy);
|
|
42
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { verifyGenerationTerminal } from '../../packages/reploid/src/artifacts/generation-terminal.js';
|
|
2
|
+
import { verifyReceipt } from '../../packages/reploid/src/artifacts/receipt.js';
|
|
3
|
+
import { verifyStageReceipt, verifyOutputAcknowledgment, attemptRouteHash, outputChainGenesis } from '../../packages/reploid/src/mesh/partitions/partition-attempt-evidence.js';
|
|
4
|
+
import { canonicalRouteAttemptBinding } from '../../packages/reploid/src/mesh/partitions/partition-contract.js';
|
|
5
|
+
import { normalizeParticipantPublicKey } from '../../packages/reploid/src/artifacts/participant-proofs.js';
|
|
6
|
+
import { assert, same, integer } from './contract.js';
|
|
7
|
+
|
|
8
|
+
/** Reuse execution signatures; the credit service never treats a completed counter as evidence. */
|
|
9
|
+
export async function verifyCreditEvidence(request, evidence, enrollment, now, skew, { unadmitted = [] } = {}) {
|
|
10
|
+
const within = at => Number.isFinite(at) && at >= request.createdAt - skew
|
|
11
|
+
&& at <= Math.min(now, request.deadline) + skew;
|
|
12
|
+
if (request.mode === 'whole-model') {
|
|
13
|
+
if (evidence?.schema === 'reploid.generation-terminal/v1') {
|
|
14
|
+
const provider = request.providers[0]?.participantId;
|
|
15
|
+
assert(request.providers.length === 1 && await verifyGenerationTerminal(evidence, {
|
|
16
|
+
requestId: request.executionId, provider, consumer: request.consumer, model: request.model.id,
|
|
17
|
+
modelIdentity: request.model.identity, adapterIdentities: request.model.adapterIdentities,
|
|
18
|
+
inputDigest: request.inputDigest
|
|
19
|
+
}) && same(normalizeParticipantPublicKey(evidence.publicJwk), enrollment.byPeer.get(provider)?.publicJwk)
|
|
20
|
+
&& evidence.timestamp >= request.createdAt - skew && evidence.timestamp <= now + skew,
|
|
21
|
+
'Terminal cleanup does not match the accepted request');
|
|
22
|
+
// Cleanup is not a token meter. A fixed terminal charge must have been
|
|
23
|
+
// accepted separately; missing terms leave this execution pending.
|
|
24
|
+
return { outputTokens: null, terminalStatus: evidence.status, fixedCharge: request.terminalCharge ?? null, outputIdentity: null,
|
|
25
|
+
workIds: [`whole:${provider}:${request.consumer}:request:${request.executionId}`] };
|
|
26
|
+
}
|
|
27
|
+
const receipt = structuredClone(evidence);
|
|
28
|
+
assert(request.providers.length === 1 && receipt.version === 2 && receipt.status === 'completed'
|
|
29
|
+
&& receipt.cleanup === 'settled' && receipt.provider === request.providers[0].participantId
|
|
30
|
+
&& receipt.consumer === request.consumer && receipt.jobHash === `request:${request.executionId}`
|
|
31
|
+
&& receipt.model === request.model.id && receipt.modelIdentity === request.model.identity
|
|
32
|
+
&& same(receipt.adapterIdentities, request.model.adapterIdentities) && receipt.inputDigest === request.inputDigest
|
|
33
|
+
&& within(receipt.timestamp) && integer(receipt.outputTokens) && receipt.outputTokens <= request.maxOutputTokens,
|
|
34
|
+
'Completed receipt does not match the accepted request');
|
|
35
|
+
assert(same(normalizeParticipantPublicKey(receipt.providerKey), enrollment.byPeer.get(receipt.provider).publicJwk)
|
|
36
|
+
&& same(normalizeParticipantPublicKey(receipt.consumerKey), enrollment.byPeer.get(receipt.consumer).publicJwk), 'Receipt enrollment changed');
|
|
37
|
+
const verified = await verifyReceipt(receipt);
|
|
38
|
+
assert(verified.providerValid && verified.consumerValid, 'Countersigned completed receipt required');
|
|
39
|
+
return { outputTokens: receipt.outputTokens, outputIdentity: receipt.outputDigest, workIds: [`whole:${receipt.provider}:${receipt.consumer}:${receipt.jobHash}`] };
|
|
40
|
+
}
|
|
41
|
+
const proof = await verifyPartitionComponents(request, evidence, enrollment, now, skew);
|
|
42
|
+
const { receipts, acknowledgment, acknowledgedReceipt } = proof;
|
|
43
|
+
assert(acknowledgment, 'Matching requester acknowledgment required');
|
|
44
|
+
const workIds = [];
|
|
45
|
+
for (let index = 0; index < request.providers.length; index++) {
|
|
46
|
+
const receipt = receipts.find(row => row.partitionIndex === index), provider = request.providers[index];
|
|
47
|
+
if (!receipt && unadmitted.includes(index) && acknowledgment.count === 0) continue;
|
|
48
|
+
assert(receipt && ['settled', 'failed'].includes(receipt.status) && receipt.timing.closedAt !== null
|
|
49
|
+
&& receipt.steps >= acknowledgment.count, 'Settled stage receipt does not match the acknowledged work');
|
|
50
|
+
workIds.push(`stage:${receipt.participantId}:${receipt.reservationId}:${receipt.attemptId}:${index}`);
|
|
51
|
+
}
|
|
52
|
+
if (acknowledgment.count > 0) {
|
|
53
|
+
const final = acknowledgedReceipt ?? receipts.find(row => row.partitionIndex === request.providers.length - 1);
|
|
54
|
+
assert(final?.output?.count === acknowledgment.count && final.output.chain === acknowledgment.chain,
|
|
55
|
+
'Final output differs from requester acknowledgment');
|
|
56
|
+
}
|
|
57
|
+
return { outputTokens: acknowledgment.count, outputIdentity: acknowledgment.chain, workIds };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Validate independently submitted pieces before retaining them. A signed failure is a closing
|
|
61
|
+
* receipt only when it carries closedAt; executing receipts never establish cleanup. */
|
|
62
|
+
export async function verifyPartitionComponents(request, evidence, enrollment, now, skew) {
|
|
63
|
+
assert(request.mode === 'partition' && evidence && same(canonicalRouteAttemptBinding(evidence.identity), request.route),
|
|
64
|
+
'Partition route differs from accepted route');
|
|
65
|
+
const routeHash = await attemptRouteHash(evidence.identity);
|
|
66
|
+
const signedTime = value => Number.isFinite(Date.parse(value)) && Date.parse(value) >= request.createdAt - skew
|
|
67
|
+
&& Date.parse(value) <= now + skew;
|
|
68
|
+
const executionTime = value => signedTime(value) && Date.parse(value) <= request.deadline + skew;
|
|
69
|
+
const checkReceipt = async receipt => {
|
|
70
|
+
const provider = request.providers[receipt?.partitionIndex];
|
|
71
|
+
assert(provider, 'Unknown receipt stage');
|
|
72
|
+
const checked = await verifyStageReceipt(receipt, { publicJwk: enrollment.byPeer.get(provider.participantId).publicJwk });
|
|
73
|
+
assert(checked.ok && receipt.participantId === provider.participantId && receipt.reservationId === request.id
|
|
74
|
+
&& receipt.attemptId === request.executionId && receipt.routeHash === routeHash && receipt.steps <= request.maxOutputTokens
|
|
75
|
+
&& signedTime(receipt.signedAt) && executionTime(receipt.timing.reservedAt)
|
|
76
|
+
&& (receipt.steps === 0 || executionTime(receipt.timing.firstStepAt) && executionTime(receipt.timing.lastStepAt))
|
|
77
|
+
&& (receipt.status === 'executing' ? receipt.timing.closedAt === null : signedTime(receipt.timing.closedAt)),
|
|
78
|
+
'Stage receipt does not match the admitted work');
|
|
79
|
+
assert(receipt.partitionIndex === request.providers.length - 1 ? receipt.output !== null : receipt.output === null,
|
|
80
|
+
'Only the final stage commits outputs');
|
|
81
|
+
return structuredClone(receipt);
|
|
82
|
+
};
|
|
83
|
+
assert(Array.isArray(evidence.receipts) && evidence.receipts.length <= request.providers.length, 'Bounded stage receipts required');
|
|
84
|
+
const receipts = await Promise.all(evidence.receipts.map(checkReceipt));
|
|
85
|
+
assert(new Set(receipts.map(row => row.partitionIndex)).size === receipts.length, 'Duplicate stage receipt');
|
|
86
|
+
const acknowledgment = evidence.acknowledgment ?? evidence.acknowledged?.acknowledgment ?? null;
|
|
87
|
+
const acknowledgedReceipt = evidence.acknowledgedReceipt ?? evidence.acknowledged?.receipt ?? null;
|
|
88
|
+
if (acknowledgment) {
|
|
89
|
+
const ack = await verifyOutputAcknowledgment(acknowledgment, { publicJwk: enrollment.byPeer.get(request.consumer).publicJwk });
|
|
90
|
+
assert(ack.ok && acknowledgment.requesterId === request.consumer && acknowledgment.reservationId === request.id
|
|
91
|
+
&& acknowledgment.attemptId === request.executionId && acknowledgment.routeHash === routeHash
|
|
92
|
+
&& signedTime(acknowledgment.signedAt) && acknowledgment.count <= request.maxOutputTokens, 'Matching requester acknowledgment required');
|
|
93
|
+
if (acknowledgment.count === 0) assert(acknowledgment.chain === await outputChainGenesis({ reservationId: request.id, routeHash }),
|
|
94
|
+
'Zero acknowledgment must bind the genesis chain');
|
|
95
|
+
if (acknowledgedReceipt) {
|
|
96
|
+
await checkReceipt(acknowledgedReceipt);
|
|
97
|
+
assert(acknowledgedReceipt.partitionIndex === request.providers.length - 1
|
|
98
|
+
&& acknowledgedReceipt.output.count === acknowledgment.count && acknowledgedReceipt.output.chain === acknowledgment.chain,
|
|
99
|
+
'Acknowledged output prefix differs');
|
|
100
|
+
}
|
|
101
|
+
} else assert(!acknowledgedReceipt, 'Output prefix needs its acknowledgment');
|
|
102
|
+
return { identity: request.route, receipts, acknowledgment, acknowledgedReceipt };
|
|
103
|
+
}
|